diff --git a/SQL/database_changelog.txt b/SQL/database_changelog.txt index 0149305f2a9..ce5c2c654df 100644 --- a/SQL/database_changelog.txt +++ b/SQL/database_changelog.txt @@ -1,3 +1,18 @@ + +20th July 2017, by Shadowlight213 +Added role_time table to track time spent playing departments. +Also, added flags column to the player table. + +CREATE TABLE `role_time` ( `ckey` VARCHAR(32) NOT NULL , `job` VARCHAR(128) NOT NULL , `minutes` INT UNSIGNED NOT NULL, PRIMARY KEY (`ckey`, `job`) ) ENGINE = InnoDB; + +ALTER TABLE `player` ADD `flags` INT NOT NULL default '0' AFTER `accountjoindate`; + +UPDATE `schema_revision` SET minor = 1; + +Remember to add a prefix to the table name if you use them. + +---------------------------------------------------- + Any time you make a change to the schema files, remember to increment the database schema version. Generally increment the minor number, major should be reserved for significant changes to the schema. Both values go up to 255. The latest database version is 3.0; The query to update the schema revision table is: @@ -7,7 +22,6 @@ or INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (3, 0); ---------------------------------------------------- - 28 June 2017, by oranges Added schema_revision to store the current db revision, why start at 3.0? @@ -112,7 +126,6 @@ ALTER TABLE `player` DROP COLUMN `id`, ADD COLUMN `accountjoindate` DATE NULL AF Remember to add a prefix to the table name if you use them. ---------------------------------------------------- - 10 March 2017, by Jordie0608 Modified table 'death', adding the columns 'toxloss', 'cloneloss', and 'staminaloss' and table 'legacy_population', adding the columns 'server_ip' and 'server_port'. diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql index 870417a2fdc..9795b906720 100644 --- a/SQL/tgstation_schema.sql +++ b/SQL/tgstation_schema.sql @@ -253,6 +253,21 @@ CREATE TABLE `messages` ( ) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `role_time` +-- + +DROP TABLE IF EXISTS `role_time`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; + +CREATE TABLE `role_time` +( `ckey` VARCHAR(32) NOT NULL , + `job` VARCHAR(32) NOT NULL , + `minutes` INT UNSIGNED NOT NULL, + PRIMARY KEY (`ckey`, `job`) + ) ENGINE = InnoDB; + -- -- Table structure for table `player` -- @@ -268,6 +283,7 @@ CREATE TABLE `player` ( `computerid` varchar(32) NOT NULL, `lastadminrank` varchar(32) NOT NULL DEFAULT 'Player', `accountjoindate` DATE DEFAULT NULL, + `flags` smallint(5) unsigned DEFAULT '0' NOT NULL, PRIMARY KEY (`ckey`), KEY `idx_player_cid_ckey` (`computerid`,`ckey`), KEY `idx_player_ip_ckey` (`ip`,`ckey`) diff --git a/SQL/tgstation_schema_prefixed.sql b/SQL/tgstation_schema_prefixed.sql index be5de60440b..b810a82ca3e 100644 --- a/SQL/tgstation_schema_prefixed.sql +++ b/SQL/tgstation_schema_prefixed.sql @@ -253,6 +253,21 @@ CREATE TABLE `SS13_messages` ( ) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `SS13_role_time` +-- + +DROP TABLE IF EXISTS `SS13_role_time`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; + +CREATE TABLE `SS13_role_time` +( `ckey` VARCHAR(32) NOT NULL , + `job` VARCHAR(32) NOT NULL , + `minutes` INT UNSIGNED NOT NULL, + PRIMARY KEY (`ckey`, `job`) + ) ENGINE = InnoDB; + -- -- Table structure for table `SS13_player` -- @@ -268,6 +283,7 @@ CREATE TABLE `SS13_player` ( `computerid` varchar(32) NOT NULL, `lastadminrank` varchar(32) NOT NULL DEFAULT 'Player', `accountjoindate` DATE DEFAULT NULL, + `flags` smallint(5) unsigned DEFAULT '0' NOT NULL, PRIMARY KEY (`ckey`), KEY `idx_player_cid_ckey` (`computerid`,`ckey`), KEY `idx_player_ip_ckey` (`ip`,`ckey`) diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm index 1c105e523c9..a3127ca1cad 100644 --- a/code/__DEFINES/preferences.dm +++ b/code/__DEFINES/preferences.dm @@ -46,4 +46,22 @@ #define SEC_DEPT_ENGINEERING "Engineering" #define SEC_DEPT_MEDICAL "Medical" #define SEC_DEPT_SCIENCE "Science" -#define SEC_DEPT_SUPPLY "Supply" \ No newline at end of file +#define SEC_DEPT_SUPPLY "Supply" + +// Playtime tracking system, see jobs_exp.dm +#define EXP_TYPE_LIVING "Living" +#define EXP_TYPE_CREW "Crew" +#define EXP_TYPE_COMMAND "Command" +#define EXP_TYPE_ENGINEERING "Engineering" +#define EXP_TYPE_MEDICAL "Medical" +#define EXP_TYPE_SCIENCE "Science" +#define EXP_TYPE_SUPPLY "Supply" +#define EXP_TYPE_SECURITY "Security" +#define EXP_TYPE_SILICON "Silicon" +#define EXP_TYPE_SERVICE "Service" +#define EXP_TYPE_ANTAG "Antag" +#define EXP_TYPE_SPECIAL "Special" +#define EXP_TYPE_GHOST "Ghost" + +//Flags in the players table in the db +#define DB_FLAG_EXEMPT 1 \ No newline at end of file diff --git a/code/_compile_options.dm b/code/_compile_options.dm index 177899bf7e9..80309e73ee6 100644 --- a/code/_compile_options.dm +++ b/code/_compile_options.dm @@ -72,4 +72,4 @@ //Update this whenever the db schema changes //make sure you add an update to the schema_version stable in the db changelog #define DB_MAJOR_VERSION 3 -#define DB_MINOR_VERSION 0 +#define DB_MINOR_VERSION 1 diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index abf2c1f937e..c9efa1bae35 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -103,6 +103,13 @@ var/use_account_age_for_jobs = 0 //Uses the time they made the account for the job restriction stuff. New player joining alerts should be unaffected. var/see_own_notes = 0 //Can players see their own admin notes (read-only)? Config option in config.txt + var/use_exp_tracking = FALSE + var/use_exp_restrictions_heads = FALSE + var/use_exp_restrictions_heads_hours = 0 + var/use_exp_restrictions_heads_department = FALSE + var/use_exp_restrictions_other = FALSE + var/use_exp_restrictions_admin_bypass = FALSE + //Population cap vars var/soft_popcap = 0 var/hard_popcap = 0 @@ -335,6 +342,18 @@ use_age_restriction_for_jobs = 1 if("use_account_age_for_jobs") use_account_age_for_jobs = 1 + if("use_exp_tracking") + use_exp_tracking = TRUE + if("use_exp_restrictions_heads") + use_exp_restrictions_heads = TRUE + if("use_exp_restrictions_heads_hours") + use_exp_restrictions_heads_hours = text2num(value) + if("use_exp_restrictions_heads_department") + use_exp_restrictions_heads_department = TRUE + if("use_exp_restrictions_other") + use_exp_restrictions_other = TRUE + if("use_exp_restrictions_admin_bypass") + use_exp_restrictions_admin_bypass = TRUE if("lobby_countdown") lobby_countdown = text2num(value) if("round_end_countdown") diff --git a/code/controllers/subsystem/blackbox.dm b/code/controllers/subsystem/blackbox.dm index d8c70b1ed6d..90c6b80cf9c 100644 --- a/code/controllers/subsystem/blackbox.dm +++ b/code/controllers/subsystem/blackbox.dm @@ -1,7 +1,7 @@ SUBSYSTEM_DEF(blackbox) name = "Blackbox" wait = 6000 - flags = SS_NO_TICK_CHECK | SS_NO_INIT + flags = SS_NO_TICK_CHECK runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME init_order = INIT_ORDER_BLACKBOX @@ -18,9 +18,14 @@ SUBSYSTEM_DEF(blackbox) var/list/msg_other = list() var/list/feedback = list() //list of datum/feedback_variable - + var/triggertime = 0 var/sealed = FALSE //time to stop tracking stats? + +/datum/controller/subsystem/blackbox/Initialize() + triggertime = world.time + . = ..() + //poll population /datum/controller/subsystem/blackbox/fire() if(!SSdbcore.Connect()) @@ -33,6 +38,11 @@ SUBSYSTEM_DEF(blackbox) var/datum/DBQuery/query_record_playercount = SSdbcore.NewQuery("INSERT INTO [format_table_name("legacy_population")] (playercount, admincount, time, server_ip, server_port) VALUES ([playercount], [admincount], '[SQLtime()]', INET_ATON(IF('[world.internet_address]' LIKE '', '0', '[world.internet_address]')), '[world.port]')") query_record_playercount.Execute() + if(config.use_exp_tracking) + if((triggertime < 0) || (world.time > (triggertime +3000))) //subsystem fires once at roundstart then once every 10 minutes. a 5 min check skips the first fire. The <0 is midnight rollover check + update_exp(10,FALSE) + + /datum/controller/subsystem/blackbox/Recover() msg_common = SSblackbox.msg_common msg_science = SSblackbox.msg_science diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm index f28373ef224..1be36e5ac75 100644 --- a/code/controllers/subsystem/job.dm +++ b/code/controllers/subsystem/job.dm @@ -73,6 +73,8 @@ SUBSYSTEM_DEF(job) return 0 if(!job.player_old_enough(player.client)) return 0 + if(job.required_playtime_remaining(player.client)) + return 0 var/position_limit = job.total_positions if(!latejoin) position_limit = job.spawn_positions @@ -95,6 +97,9 @@ SUBSYSTEM_DEF(job) if(!job.player_old_enough(player.client)) Debug("FOC player not old enough, Player: [player]") continue + if(job.required_playtime_remaining(player.client)) + Debug("FOC player not enough xp, Player: [player]") + continue if(flag && (!(flag in player.client.prefs.be_special))) Debug("FOC flag failed, Player: [player], Flag: [flag], ") continue @@ -130,6 +135,10 @@ SUBSYSTEM_DEF(job) Debug("GRJ player not old enough, Player: [player]") continue + if(job.required_playtime_remaining(player.client)) + Debug("GRJ player not enough xp, Player: [player]") + continue + if(player.mind && job.title in player.mind.restricted_roles) Debug("GRJ incompatible with antagonist role, Player: [player], Job: [job.title]") continue @@ -300,6 +309,10 @@ SUBSYSTEM_DEF(job) Debug("DO player not old enough, Player: [player], Job:[job.title]") continue + if(job.required_playtime_remaining(player.client)) + Debug("DO player not enough xp, Player: [player], Job:[job.title]") + continue + if(player.mind && job.title in player.mind.restricted_roles) Debug("DO incompatible with antagonist role, Player: [player], Job:[job.title]") continue @@ -463,6 +476,9 @@ SUBSYSTEM_DEF(job) if(!job.player_old_enough(player.client)) level6++ continue + if(job.required_playtime_remaining(player.client)) + level6++ + continue if(player.client.prefs.GetJobDepartment(job, 1) & job.flag) level1++ else if(player.client.prefs.GetJobDepartment(job, 2) & job.flag) diff --git a/code/datums/mind.dm b/code/datums/mind.dm index ae79d93c649..9809d817b7a 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -1458,6 +1458,7 @@ if(!(src in SSticker.mode.syndicates)) SSticker.mode.syndicates += src SSticker.mode.update_synd_icons_added(src) + assigned_role = "Syndicate" special_role = "Syndicate" SSticker.mode.forge_syndicate_objectives(src) SSticker.mode.greet_syndicate(src) @@ -1705,7 +1706,7 @@ /mob/living/carbon/human/mind_initialize() ..() if(!mind.assigned_role) - mind.assigned_role = "Assistant" //defualt + mind.assigned_role = "Unassigned" //default //XENO /mob/living/carbon/alien/mind_initialize() diff --git a/code/game/gamemodes/antag_spawner.dm b/code/game/gamemodes/antag_spawner.dm index a485bc2d1ae..ab9c041efe6 100644 --- a/code/game/gamemodes/antag_spawner.dm +++ b/code/game/gamemodes/antag_spawner.dm @@ -108,6 +108,7 @@ new_objective.explanation_text = "Protect [usr.real_name], the wizard." M.mind.objectives += new_objective SSticker.mode.apprentices += M.mind + M.mind.assigned_role = "Apprentice" M.mind.special_role = "apprentice" SSticker.mode.update_wiz_icons_added(M.mind) SEND_SOUND(M, sound('sound/effects/magic.ogg')) diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 5943e753a04..b88451b7247 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -31,6 +31,8 @@ if(M.client) body += " played by [M.client] " body += "\[[M.client.holder ? M.client.holder.rank : "Player"]\]" + if(config.use_exp_tracking) + body += "\[" + M.client.get_exp_living() + "\]" if(isnewplayer(M)) body += " Hasn't Entered Game " diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 79cd54b31ea..5e013525de3 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -60,6 +60,7 @@ GLOBAL_LIST_INIT(admin_verbs_admin, world.AVerbsAdmin()) /client/proc/cmd_admin_local_narrate, /*sends text to all mobs within view of atom*/ /client/proc/cmd_admin_create_centcom_report, /client/proc/cmd_change_command_name, + /client/proc/cmd_admin_check_player_exp, /* shows players by playtime */ /client/proc/toggle_antag_hud, /*toggle display of the admin antag hud*/ /client/proc/toggle_AI_interact, /*toggle admin ability to interact with machines as an AI*/ /client/proc/customiseSNPC, /* Customise any interactive crewmembers in the world */ diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 3da643c4da0..779a9c4e530 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -22,6 +22,24 @@ else if(href_list["stickyban"]) stickyban(href_list["stickyban"],href_list) + else if(href_list["getplaytimewindow"]) + if(!check_rights(R_ADMIN)) + return + var/mob/M = locate(href_list["getplaytimewindow"]) in GLOB.mob_list + if(!M) + to_chat(usr, "ERROR: Mob not found.") + return + cmd_show_exp_panel(M.client) + + else if(href_list["toggleexempt"]) + if(!check_rights(R_ADMIN)) + return + var/client/C = locate(href_list["toggleexempt"]) in GLOB.clients + if(!C) + to_chat(usr, "ERROR: Client not found.") + return + toggle_exempt_status(C) + else if(href_list["makeAntag"]) if (!SSticker.mode) to_chat(usr, "Not until the round starts!") diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm index 7e8d5f726d8..0fcd5bef6fe 100644 --- a/code/modules/admin/verbs/randomverbs.dm +++ b/code/modules/admin/verbs/randomverbs.dm @@ -1211,3 +1211,52 @@ GLOBAL_LIST_EMPTY(custom_outfits) //Admin created outfits message_admins("[key_name_admin(usr)] triggered a CentCom recall, with the admiral message of: [message]") log_game("[key_name(usr)] triggered a CentCom recall, with the message of: [message]") SSshuttle.centcom_recall(SSshuttle.emergency.timer, message) + +/client/proc/cmd_admin_check_player_exp() //Allows admins to determine who the newer players are. + set category = "Admin" + set name = "Check Player Playtime" + if(!check_rights(R_ADMIN)) + return + + var/list/msg = list() + msg += "Playtime ReportPlaytime:
" + src << browse(msg.Join(), "window=Player_playtime_check") + +/datum/admins/proc/cmd_show_exp_panel(client/C) + if(!check_rights(R_ADMIN)) + return + if(!C) + to_chat(usr, "ERROR: Client not found.") + return + + var/list/body = list() + body += "Playtime for [C.key]
Playtime:" + body += C.get_exp_report() + body += "Toggle Exempt status" + body += "" + usr << browse(body.Join(), "window=playerplaytime[C.ckey];size=550x615") + +/datum/admins/proc/toggle_exempt_status(client/C) + if(!check_rights(R_ADMIN)) + return + if(!C) + to_chat(usr, "ERROR: Client not found.") + return + + if(!C.set_db_player_flags()) + to_chat(usr, "ERROR: Unable read player flags from database. Please check logs.") + var/dbflags = C.prefs.db_flags + var/newstate = FALSE + if(dbflags & DB_FLAG_EXEMPT) + newstate = FALSE + else + newstate = TRUE + + if(C.update_flag_db(DB_FLAG_EXEMPT, newstate)) + to_chat(usr, "ERROR: Unable to update player flags. Please check logs.") + else + message_admins("[key_name_admin(usr)] has [newstate ? "activated" : "deactivated"] job exp exempt status on [key_name_admin(C)]") + log_admin("[key_name(usr)] has [newstate ? "activated" : "deactivated"] job exp exempt status on [key_name(C)]") \ No newline at end of file diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 27105f6b03c..87401d8b9a9 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -36,6 +36,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/tgui_lock = TRUE var/windowflashing = TRUE var/toggles = TOGGLES_DEFAULT + var/db_flags var/chat_toggles = TOGGLES_DEFAULT_CHAT var/ghost_form = "ghost" var/ghost_orbit = GHOST_ORBIT_CIRCLE @@ -105,6 +106,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/uplink_spawn_loc = UPLINK_PDA + var/list/exp var/list/menuoptions /datum/preferences/New(client/C) @@ -132,7 +134,6 @@ GLOBAL_LIST_EMPTY(preferences_datums) menuoptions = list() return - /datum/preferences/proc/ShowChoices(mob/user) if(!user || !user.client) return @@ -536,6 +537,10 @@ GLOBAL_LIST_EMPTY(preferences_datums) if(jobban_isbanned(user, rank)) HTML += "[rank] BANNED" continue + var/required_playtime_remaining = job.required_playtime_remaining(user.client) + if(required_playtime_remaining) + HTML += "[rank] \[ [get_exp_format(required_playtime_remaining)] as [job.get_exp_req_type()] \] " + continue if(!job.player_old_enough(user.client)) var/available_in_days = job.available_in_days(user.client) HTML += "[rank] \[IN [(available_in_days)] DAYS\]" diff --git a/code/modules/jobs/job_exp.dm b/code/modules/jobs/job_exp.dm new file mode 100644 index 00000000000..66430876ef4 --- /dev/null +++ b/code/modules/jobs/job_exp.dm @@ -0,0 +1,279 @@ +GLOBAL_LIST_EMPTY(exp_to_update) +GLOBAL_PROTECT(exp_to_update) + + +// Procs +/datum/job/proc/required_playtime_remaining(client/C) + if(!C) + return 0 + if(!config.use_exp_tracking) + return 0 + if(!exp_requirements || !exp_type) + return 0 + if(!job_is_xp_locked(src.title)) + return 0 + if(config.use_exp_restrictions_admin_bypass && check_rights(R_ADMIN, FALSE, C.mob)) + return 0 + var/isexempt = C.prefs.db_flags & DB_FLAG_EXEMPT + if(isexempt) + return 0 + var/my_exp = C.calc_exp_type(get_exp_req_type()) + var/job_requirement = get_exp_req_amount() + if(my_exp >= job_requirement) + return 0 + else + return (job_requirement - my_exp) + +/datum/job/proc/get_exp_req_amount() + if(title in GLOB.command_positions) + if(config.use_exp_restrictions_heads_hours) + return config.use_exp_restrictions_heads_hours * 60 + return exp_requirements + +/datum/job/proc/get_exp_req_type() + if(title in GLOB.command_positions) + if(config.use_exp_restrictions_heads_department && exp_type_department) + return exp_type_department + return exp_type + +/proc/job_is_xp_locked(jobtitle) + if(!config.use_exp_restrictions_heads && jobtitle in GLOB.command_positions) + return FALSE + if(!config.use_exp_restrictions_other && !(jobtitle in GLOB.command_positions)) + return FALSE + return TRUE + +/client/proc/calc_exp_type(exptype) + var/list/explist = prefs.exp.Copy() + var/amount = 0 + var/list/typelist = GLOB.exp_jobsmap[exptype] + if(!typelist) + return -1 + for(var/job in typelist["titles"]) + if(job in explist) + amount += explist[job] + return amount + +/client/proc/get_exp_report() + if(!config.use_exp_tracking) + return "Tracking is disabled in the server configuration file." + var/list/play_records = prefs.exp + if(!play_records.len) + set_exp_from_db() + play_records = prefs.exp + if(!play_records.len) + return "[key] has no records." + var/return_text = list() + return_text += "" + var/list/jobs_locked = list() + var/list/jobs_unlocked = list() + for(var/datum/job/job in SSjob.occupations) + if(job.exp_requirements && job.exp_type) + if(!job_is_xp_locked(job.title)) + continue + else if(!job.required_playtime_remaining(mob.client)) + jobs_unlocked += job.title + else + var/xp_req = job.get_exp_req_amount() + jobs_locked += "[job.title] [get_exp_format(text2num(calc_exp_type(job.get_exp_req_type())))] / [get_exp_format(xp_req)] as [job.get_exp_req_type()])" + if(jobs_unlocked.len) + return_text += "

Jobs Unlocked:" + if(jobs_locked.len) + return_text += "

Jobs Not Unlocked:" + return return_text + + +/client/proc/get_exp_living() + if(!prefs.exp) + return "No data" + var/exp_living = text2num(prefs.exp[EXP_TYPE_LIVING]) + return get_exp_format(exp_living) + +/proc/get_exp_format(expnum) + if(expnum > 60) + return num2text(round(expnum / 60)) + "h" + else if(expnum > 0) + return num2text(expnum) + "m" + else + return "0h" + +/datum/controller/subsystem/blackbox/proc/update_exp(mins, ann = FALSE) + if(!SSdbcore.Connect()) + return -1 + for(var/client/L in GLOB.clients) + if(L.is_afk()) + continue + addtimer(CALLBACK(L,/client/proc/update_exp_list,mins,ann),10) + +/datum/controller/subsystem/blackbox/proc/update_exp_db() + SSdbcore.MassInsert(format_table_name("role_time"),GLOB.exp_to_update,TRUE) + LAZYCLEARLIST(GLOB.exp_to_update) + +//resets a client's exp to what was in the db. +/client/proc/set_exp_from_db() + if(!config.use_exp_tracking) + return -1 + if(!SSdbcore.Connect()) + return -1 + var/datum/DBQuery/exp_read = SSdbcore.NewQuery("SELECT job, minutes FROM [format_table_name("role_time")] WHERE ckey = '[sanitizeSQL(ckey)]'") + if(!exp_read.Execute()) + return -1 + var/list/play_records = list() + while(exp_read.NextRow()) + play_records[exp_read.item[1]] = text2num(exp_read.item[2]) + + for(var/rtype in SSjob.name_occupations) + if(!play_records[rtype]) + play_records[rtype] = 0 + for(var/rtype in GLOB.exp_specialmap) + if(!play_records[rtype]) + play_records[rtype] = 0 + + prefs.exp = play_records + + +//updates player db flags +/client/proc/update_flag_db(newflag, state = FALSE) + + if(!SSdbcore.Connect()) + return -1 + + if(!set_db_player_flags()) + return -1 + + if((prefs.db_flags & newflag) && !state) + prefs.db_flags &= ~newflag + else + prefs.db_flags |= newflag + + var/datum/DBQuery/flag_update = SSdbcore.NewQuery("UPDATE [format_table_name("player")] SET flags = '[prefs.db_flags]' WHERE ckey='[sanitizeSQL(ckey)]'") + + if(!flag_update.Execute()) + return -1 + + +/client/proc/update_exp_list(minutes, announce_changes = FALSE) + if(!config.use_exp_tracking) + return -1 + if(!SSdbcore.Connect()) + return -1 + var/datum/DBQuery/exp_read = SSdbcore.NewQuery("SELECT job, minutes FROM [format_table_name("role_time")] WHERE ckey = '[sanitizeSQL(ckey)]'") + if(!exp_read.Execute()) + return -1 + var/list/play_records = list() + while(exp_read.NextRow()) + play_records[exp_read.item[1]] = text2num(exp_read.item[2]) + + for(var/rtype in SSjob.name_occupations) + if(!play_records[rtype]) + play_records[rtype] = 0 + for(var/rtype in GLOB.exp_specialmap) + if(!play_records[rtype]) + play_records[rtype] = 0 + var/list/old_records = play_records.Copy() + if(isliving(mob)) + if(mob.stat != DEAD) + var/rolefound = FALSE + play_records[EXP_TYPE_LIVING] += minutes + if(announce_changes) + to_chat(src,"You got: [minutes] Living EXP!") + if(mob.mind.assigned_role) + for(var/job in SSjob.name_occupations) + if(mob.mind.assigned_role == job) + rolefound = TRUE + play_records[job] += minutes + if(announce_changes) + to_chat(src,"You got: [minutes] [job] EXP!") + if(!rolefound) + for(var/role in GLOB.exp_specialmap[EXP_TYPE_SPECIAL]) + if(mob.mind.assigned_role == role) + rolefound = TRUE + play_records[role] += minutes + if(announce_changes) + to_chat(mob,"You got: [minutes] [role] EXP!") + if(mob.mind.special_role && !mob.mind.var_edited) + var/trackedrole = mob.mind.special_role + var/gangrole = lookforgangrole(mob.mind.special_role) + if(gangrole) + trackedrole = gangrole + play_records[trackedrole] += minutes + if(announce_changes) + to_chat(src,"You got: [minutes] [trackedrole] EXP!") + if(!rolefound) + play_records["Unknown"] += minutes + else + play_records[EXP_TYPE_GHOST] += minutes + if(announce_changes) + to_chat(src,"You got: [minutes] Ghost EXP!") + else if(isobserver(mob)) + play_records[EXP_TYPE_GHOST] += minutes + if(announce_changes) + to_chat(src,"You got: [minutes] Ghost EXP!") + else if(minutes) //Let "refresh" checks go through + return + prefs.exp = play_records + + for(var/jtype in play_records) + if(play_records[jtype] != old_records[jtype]) + LAZYINITLIST(GLOB.exp_to_update) + GLOB.exp_to_update.Add(list(list( + "job" = "'[sanitizeSQL(jtype)]'", + "ckey" = "'[sanitizeSQL(ckey)]'", + "minutes" = play_records[jtype]))) + addtimer(CALLBACK(SSblackbox,/datum/controller/subsystem/blackbox/proc/update_exp_db),20,TIMER_OVERRIDE|TIMER_UNIQUE) + + +//ALWAYS call this at beginning to any proc touching player flags, or your database admin will probably be mad +/client/proc/set_db_player_flags() + if(!SSdbcore.Connect()) + return FALSE + + var/datum/DBQuery/flags_read = SSdbcore.NewQuery("SELECT flags FROM [format_table_name("player")] WHERE ckey='[ckey]'") + + if(!flags_read.Execute()) + return FALSE + + if(flags_read.NextRow()) + prefs.db_flags = text2num(flags_read.item[1]) + else if(isnull(prefs.db_flags)) + prefs.db_flags = 0 //This PROBABLY won't happen, but better safe than sorry. + return TRUE + +//Since each gang is tracked as a different antag type, records need to be generalized or you get up to 57 different possible records +/proc/lookforgangrole(rolecheck) + if(findtext(rolecheck,"Gangster")) + return "Gangster" + else if(findtext(rolecheck,"Gang Boss")) + return "Gang Boss" + else if(findtext(rolecheck,"Gang Lieutenant")) + return "Gang Lieutenant" + else + return FALSE \ No newline at end of file diff --git a/code/modules/jobs/job_types/captain.dm b/code/modules/jobs/job_types/captain.dm index abb9ca2035d..38418e9916d 100755 --- a/code/modules/jobs/job_types/captain.dm +++ b/code/modules/jobs/job_types/captain.dm @@ -13,6 +13,8 @@ Captain selection_color = "#ccccff" req_admin_notify = 1 minimal_player_age = 14 + exp_requirements = 180 + exp_type = EXP_TYPE_CREW outfit = /datum/outfit/job/captain @@ -64,6 +66,9 @@ Head of Personnel selection_color = "#ddddff" req_admin_notify = 1 minimal_player_age = 10 + exp_requirements = 180 + exp_type = EXP_TYPE_CREW + exp_type_department = EXP_TYPE_SUPPLY outfit = /datum/outfit/job/hop diff --git a/code/modules/jobs/job_types/engineering.dm b/code/modules/jobs/job_types/engineering.dm index daad870ec9d..b61c61f6c8b 100644 --- a/code/modules/jobs/job_types/engineering.dm +++ b/code/modules/jobs/job_types/engineering.dm @@ -14,6 +14,9 @@ Chief Engineer selection_color = "#ffeeaa" req_admin_notify = 1 minimal_player_age = 7 + exp_requirements = 180 + exp_type = EXP_TYPE_CREW + exp_type_department = EXP_TYPE_ENGINEERING outfit = /datum/outfit/job/ce @@ -72,6 +75,8 @@ Station Engineer spawn_positions = 5 supervisors = "the chief engineer" selection_color = "#fff5cc" + exp_requirements = 60 + exp_type = EXP_TYPE_CREW outfit = /datum/outfit/job/engineer @@ -127,6 +132,8 @@ Atmospheric Technician spawn_positions = 2 supervisors = "the chief engineer" selection_color = "#fff5cc" + exp_requirements = 60 + exp_type = EXP_TYPE_CREW outfit = /datum/outfit/job/atmos diff --git a/code/modules/jobs/job_types/job.dm b/code/modules/jobs/job_types/job.dm index 04a384357fd..6b736db7fef 100644 --- a/code/modules/jobs/job_types/job.dm +++ b/code/modules/jobs/job_types/job.dm @@ -43,6 +43,11 @@ var/outfit = null + var/exp_requirements = 0 + + var/exp_type = "" + var/exp_type_department = "" + //Only override this proc //H is usually a human unless an /equip override transformed it /datum/job/proc/after_spawn(mob/living/H, mob/M) diff --git a/code/modules/jobs/job_types/medical.dm b/code/modules/jobs/job_types/medical.dm index 1f5bf684aa3..d85ee9f62d2 100644 --- a/code/modules/jobs/job_types/medical.dm +++ b/code/modules/jobs/job_types/medical.dm @@ -14,6 +14,9 @@ Chief Medical Officer selection_color = "#ffddf0" req_admin_notify = 1 minimal_player_age = 7 + exp_requirements = 180 + exp_type = EXP_TYPE_CREW + exp_type_department = EXP_TYPE_MEDICAL outfit = /datum/outfit/job/cmo @@ -90,6 +93,8 @@ Chemist spawn_positions = 2 supervisors = "the chief medical officer" selection_color = "#ffeef0" + exp_type = EXP_TYPE_CREW + exp_requirements = 60 outfit = /datum/outfit/job/chemist @@ -124,6 +129,8 @@ Geneticist spawn_positions = 2 supervisors = "the chief medical officer and research director" selection_color = "#ffeef0" + exp_type = EXP_TYPE_CREW + exp_requirements = 60 outfit = /datum/outfit/job/geneticist @@ -158,6 +165,8 @@ Virologist spawn_positions = 1 supervisors = "the chief medical officer" selection_color = "#ffeef0" + exp_type = EXP_TYPE_CREW + exp_requirements = 60 outfit = /datum/outfit/job/virologist diff --git a/code/modules/jobs/job_types/science.dm b/code/modules/jobs/job_types/science.dm index c93ddf008a1..7fc1dada776 100644 --- a/code/modules/jobs/job_types/science.dm +++ b/code/modules/jobs/job_types/science.dm @@ -14,6 +14,9 @@ Research Director selection_color = "#ffddff" req_admin_notify = 1 minimal_player_age = 7 + exp_type_department = EXP_TYPE_SCIENCE + exp_requirements = 180 + exp_type = EXP_TYPE_CREW outfit = /datum/outfit/job/rd @@ -68,6 +71,8 @@ Scientist spawn_positions = 3 supervisors = "the research director" selection_color = "#ffeeff" + exp_requirements = 60 + exp_type = EXP_TYPE_CREW outfit = /datum/outfit/job/scientist @@ -101,6 +106,8 @@ Roboticist spawn_positions = 2 supervisors = "research director" selection_color = "#ffeeff" + exp_requirements = 60 + exp_type = EXP_TYPE_CREW outfit = /datum/outfit/job/roboticist diff --git a/code/modules/jobs/job_types/security.dm b/code/modules/jobs/job_types/security.dm index 84f5a82e570..33f030fd22a 100644 --- a/code/modules/jobs/job_types/security.dm +++ b/code/modules/jobs/job_types/security.dm @@ -20,6 +20,9 @@ Head of Security selection_color = "#ffdddd" req_admin_notify = 1 minimal_player_age = 14 + exp_requirements = 300 + exp_type = EXP_TYPE_CREW + exp_type_department = EXP_TYPE_SECURITY outfit = /datum/outfit/job/hos @@ -71,6 +74,8 @@ Warden supervisors = "the head of security" selection_color = "#ffeeee" minimal_player_age = 7 + exp_requirements = 300 + exp_type = EXP_TYPE_CREW outfit = /datum/outfit/job/warden @@ -121,6 +126,8 @@ Detective supervisors = "the head of security" selection_color = "#ffeeee" minimal_player_age = 7 + exp_requirements = 300 + exp_type = EXP_TYPE_CREW outfit = /datum/outfit/job/detective @@ -169,6 +176,8 @@ Security Officer supervisors = "the head of security, and the head of your assigned department (if applicable)" selection_color = "#ffeeee" minimal_player_age = 7 + exp_requirements = 300 + exp_type = EXP_TYPE_CREW outfit = /datum/outfit/job/security diff --git a/code/modules/jobs/job_types/silicon.dm b/code/modules/jobs/job_types/silicon.dm index d959d6cbcef..6ca7570bb59 100644 --- a/code/modules/jobs/job_types/silicon.dm +++ b/code/modules/jobs/job_types/silicon.dm @@ -12,6 +12,8 @@ AI supervisors = "your laws" req_admin_notify = 1 minimal_player_age = 30 + exp_requirements = 180 + exp_type = EXP_TYPE_CREW /datum/job/ai/equip(mob/living/carbon/human/H) return H.AIize(FALSE) @@ -44,6 +46,8 @@ Cyborg supervisors = "your laws and the AI" //Nodrak selection_color = "#ddffdd" minimal_player_age = 21 + exp_requirements = 120 + exp_type = EXP_TYPE_CREW /datum/job/cyborg/equip(mob/living/carbon/human/H) return H.Robotize(FALSE, FALSE) diff --git a/code/modules/jobs/jobs.dm b/code/modules/jobs/jobs.dm index 2d7caa5fc48..b5ec8fa27a4 100644 --- a/code/modules/jobs/jobs.dm +++ b/code/modules/jobs/jobs.dm @@ -59,6 +59,26 @@ GLOBAL_LIST_INIT(nonhuman_positions, list( "Cyborg", "pAI")) +GLOBAL_LIST_INIT(exp_jobsmap, list( + EXP_TYPE_CREW = list("titles" = command_positions | engineering_positions | medical_positions | science_positions | supply_positions | security_positions | civilian_positions | list("AI","Cyborg")), // crew positions + EXP_TYPE_COMMAND = list("titles" = command_positions), + EXP_TYPE_ENGINEERING = list("titles" = engineering_positions), + EXP_TYPE_MEDICAL = list("titles" = medical_positions), + EXP_TYPE_SCIENCE = list("titles" = science_positions), + EXP_TYPE_SUPPLY = list("titles" = supply_positions), + EXP_TYPE_SECURITY = list("titles" = security_positions), + EXP_TYPE_SILICON = list("titles" = list("AI","Cyborg")), + EXP_TYPE_SERVICE = list("titles" = civilian_positions), +)) + +GLOBAL_LIST_INIT(exp_specialmap, list( + EXP_TYPE_LIVING = list(), // all living mobs + EXP_TYPE_ANTAG = list(), + EXP_TYPE_SPECIAL = list("Lifebringer","Ash Walker","Exile","Servant Golem","Free Golem","Hermit","Translocated Vet","Escaped Prisoner","Hotel Staff","SuperFriend","Space Syndicate","Ancient Crew","Space Doctor","Space Bartender","Beach Bum","Skeleton","Zombie","Space Bar Patron","Lavaland Syndicate","Ghost Role"), // Ghost roles + EXP_TYPE_GHOST = list() // dead people, observers +)) +GLOBAL_PROTECT(exp_jobsmap) +GLOBAL_PROTECT(exp_specialmap) /proc/guest_jobbans(job) return ((job in GLOB.command_positions) || (job in GLOB.nonhuman_positions) || (job in GLOB.security_positions)) diff --git a/code/modules/mob/dead/new_player/login.dm b/code/modules/mob/dead/new_player/login.dm index 9a49c23cfca..4de3b1d4dd6 100644 --- a/code/modules/mob/dead/new_player/login.dm +++ b/code/modules/mob/dead/new_player/login.dm @@ -1,4 +1,7 @@ /mob/dead/new_player/Login() + if(config.use_exp_tracking) + client.set_exp_from_db() + client.set_db_player_flags() if(!mind) mind = new /datum/mind(key) mind.active = 1 diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index edf2245d363..816e3b655a2 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -307,6 +307,8 @@ return 0 if(!job.player_old_enough(src.client)) return 0 + if(job.required_playtime_remaining(client)) + return 0 if(config.enforce_human_authority && !client.prefs.pref_species.qualifies_for_rank(rank, client.prefs.features)) return 0 return 1 diff --git a/config/config.txt b/config/config.txt index 2ed021a36b1..fc219c254d8 100644 --- a/config/config.txt +++ b/config/config.txt @@ -32,6 +32,20 @@ BAN_LEGACY_SYSTEM ## Uncomment this to have the job system use the player's account creation date, rather than the when they first joined the server for job timers. #USE_ACCOUNT_AGE_FOR_JOBS +## Unhash this to track player playtime in the database. Requires database to be enabled. +#USE_EXP_TRACKING +## Unhash this to enable playtime requirements for head jobs. +#USE_EXP_RESTRICTIONS_HEADS +## Unhash this to override head jobs' playtime requirements with this number of hours. +#USE_EXP_RESTRICTIONS_HEADS_HOURS 15 +## Unhash this to change head jobs' playtime requirements so that they're based on department playtime, rather than crew playtime. +#USE_EXP_RESTRICTIONS_HEADS_DEPARTMENT +## Unhash this to enable playtime requirements for certain non-head jobs, like Engineer and Scientist. +#USE_EXP_RESTRICTIONS_OTHER +## Allows admins to bypass job playtime requirements. +#USE_EXP_RESTRICTIONS_ADMIN_BYPASS + + ## log OOC channel LOG_OOC diff --git a/tgstation.dme b/tgstation.dme index 1f8f322f8e4..663995fb326 100755 --- a/tgstation.dme +++ b/tgstation.dme @@ -1404,6 +1404,7 @@ #include "code\modules\hydroponics\grown\tomato.dm" #include "code\modules\hydroponics\grown\towercap.dm" #include "code\modules\jobs\access.dm" +#include "code\modules\jobs\job_exp.dm" #include "code\modules\jobs\jobs.dm" #include "code\modules\jobs\job_types\assistant.dm" #include "code\modules\jobs\job_types\captain.dm"