From f7f2e720694e50b49d33534acf3d1ae4bd475dd1 Mon Sep 17 00:00:00 2001 From: skull132 Date: Fri, 18 Nov 2016 18:05:32 +0200 Subject: [PATCH 01/12] Pass 1 --- baystation12.dme | 2 -- .../preference_setup/preference_setup.dm | 14 ++++++-- .../client/preferences_system_control.dm | 35 ------------------- 3 files changed, 12 insertions(+), 39 deletions(-) delete mode 100644 code/modules/client/preferences_system_control.dm diff --git a/baystation12.dme b/baystation12.dme index eebc61c47d4..61267102ff8 100644 --- a/baystation12.dme +++ b/baystation12.dme @@ -1007,10 +1007,8 @@ #include "code\modules\client\preferences_factions.dm" #include "code\modules\client\preferences_gear.dm" #include "code\modules\client\preferences_notification.dm" -#include "code\modules\client\preferences_save_migration.dm" #include "code\modules\client\preferences_savefile.dm" #include "code\modules\client\preferences_spawnpoints.dm" -#include "code\modules\client\preferences_system_control.dm" #include "code\modules\client\preferences_toggles.dm" #include "code\modules\client\ui_style.dm" #include "code\modules\client\preference_setup\preference_setup.dm" diff --git a/code/modules/client/preference_setup/preference_setup.dm b/code/modules/client/preference_setup/preference_setup.dm index 90a15851dc4..0fa0bfa10c8 100644 --- a/code/modules/client/preference_setup/preference_setup.dm +++ b/code/modules/client/preference_setup/preference_setup.dm @@ -117,11 +117,21 @@ /datum/category_group/player_setup_category/proc/load_character(var/savefile/S) // Load all data, then sanitize it. // Need due to, for example, the 01_basic module relying on species having been loaded to sanitize correctly but that isn't loaded until module 03_body. - for(var/datum/category_item/player_setup_item/PI in items) - PI.load_character(S) + if (!config.sql_saves && !establish_db_connection(dbcon)) + for(var/datum/category_item/player_setup_item/PI in items) + PI.load_character(S) + else + src.load_character_sql() for(var/datum/category_item/player_setup_item/PI in items) PI.sanitize_character() +/datum/category_group/player_setup_category/proc/load_character_sql() + var/static/list/query_cache = list() + if (isnull(query_cache[type])) + var/query = "FROM ss13_characters" + for (var/datum/category_item/player_setup_item/PI in items) + + /datum/category_group/player_setup_category/proc/save_character(var/savefile/S) // Sanitize all data, then save it for(var/datum/category_item/player_setup_item/PI in items) diff --git a/code/modules/client/preferences_system_control.dm b/code/modules/client/preferences_system_control.dm deleted file mode 100644 index be0dd3829a6..00000000000 --- a/code/modules/client/preferences_system_control.dm +++ /dev/null @@ -1,35 +0,0 @@ -/* - * These procs manage the calling of savefile or SQL oriented save and load procs for characters and preference data. - * This way of doing it is necessary for migration to be possible without a huge load of hacks. - * Will also ensure in neat and modular compatibility between the two systems, and the potential introduction of further systems. - */ - -/datum/preferences/proc/handle_preferences_load(var/client/C) -// if (config.sql_saves) -// return load_preferences_sql(C) -// else - return load_preferences() - -/datum/preferences/proc/handle_preferences_save(var/client/C) -// if (config.sql_saves) -// return save_preferences_sql(C) -// else - return save_preferences() - -/datum/preferences/proc/handle_character_load(var/slot = 0, var/client/C) -// if (config.sql_saves) -// return load_character_sql(slot, C) -// else - return load_character(slot) - -/datum/preferences/proc/handle_character_save(var/client/C) -// if (config.sql_saves) -// return save_character_sql(C) -// else - return save_character() - -/datum/preferences/proc/get_default_character() -// if (config.sql_saves) -// return current_character -// else - return default_slot From cc1d254b6a6e0c1823b409def8bd1ea7ee376f55 Mon Sep 17 00:00:00 2001 From: skull132 Date: Mon, 21 Nov 2016 01:01:40 +0200 Subject: [PATCH 02/12] Partially implement loading TODO: sanitization --- .../antagonism/01_candidacy.dm | 3 + .../preference_setup/antagonism/02_setup.dm | 4 + .../preference_setup/general/01_basic.dm | 9 ++ .../preference_setup/general/02_language.dm | 3 + .../preference_setup/general/03_body.dm | 17 ++- .../preference_setup/general/04_equipment.dm | 3 + .../preference_setup/general/05_background.dm | 7 ++ .../preference_setup/general/06_flavor.dm | 17 +++ .../client/preference_setup/global/01_ui.dm | 11 +- .../preference_setup/global/02_settings.dm | 3 + .../preference_setup/global/03_language.dm | 4 +- .../client/preference_setup/global/04_pai.dm | 2 + .../preference_setup/occupation/occupation.dm | 3 + .../preference_setup/preference_setup.dm | 101 +++++++++++++++++- .../client/preference_setup/skills/skills.dm | 3 + 15 files changed, 180 insertions(+), 10 deletions(-) diff --git a/code/modules/client/preference_setup/antagonism/01_candidacy.dm b/code/modules/client/preference_setup/antagonism/01_candidacy.dm index 94a86080253..0c9a4a0cb3d 100644 --- a/code/modules/client/preference_setup/antagonism/01_candidacy.dm +++ b/code/modules/client/preference_setup/antagonism/01_candidacy.dm @@ -8,6 +8,9 @@ /datum/category_item/player_setup_item/antagonism/candidacy/save_character(var/savefile/S) S["be_special"] << pref.be_special_role +/datum/category_item/player_setup_item/antagonism/candidacy/gather_load_query() + return list("ss13_characters" = list("vars" = list("be_special_role"), "args" = list("id"))) + /datum/category_item/player_setup_item/antagonism/candidacy/sanitize_character() if(!istype(pref.be_special_role)) pref.be_special_role = list() diff --git a/code/modules/client/preference_setup/antagonism/02_setup.dm b/code/modules/client/preference_setup/antagonism/02_setup.dm index 249d7bf2bb5..c84c2a6781f 100644 --- a/code/modules/client/preference_setup/antagonism/02_setup.dm +++ b/code/modules/client/preference_setup/antagonism/02_setup.dm @@ -12,6 +12,10 @@ var/global/list/uplink_locations = list("PDA", "Headset", "None") S["uplinklocation"] << pref.uplinklocation S["exploit_record"] << pref.exploit_record +/datum/category_item/player_setup_item/antagonism/basic/gather_load_query() + return list("ss13_characters_flavour" = list("vars" = list("records_exploit" = "exploit_record"), "args" = list("char_id")), + "ss13_characters" = list("vars" = list("uplink_location" = "uplinklocation"), "args" = list("id"))) + /datum/category_item/player_setup_item/antagonism/basic/sanitize_character() pref.uplinklocation = sanitize_inlist(pref.uplinklocation, uplink_locations, initial(pref.uplinklocation)) diff --git a/code/modules/client/preference_setup/general/01_basic.dm b/code/modules/client/preference_setup/general/01_basic.dm index 440cf90e59c..ac430e4caac 100644 --- a/code/modules/client/preference_setup/general/01_basic.dm +++ b/code/modules/client/preference_setup/general/01_basic.dm @@ -19,6 +19,15 @@ S["spawnpoint"] << pref.spawnpoint S["OOC_Notes"] << pref.metadata +/datum/category_item/player_setup_item/general/basic/gather_load_query() + return list("ss13_characters" = list("vars" = list("name" = "real_name", + "random_name" = "be_random_name", + "gender", + "age", + "metadata", + "spawnpoint"), + "args" = list("id"))) + /datum/category_item/player_setup_item/general/basic/sanitize_character() pref.age = sanitize_integer(pref.age, AGE_MIN, AGE_MAX, initial(pref.age)) pref.gender = sanitize_inlist(pref.gender, valid_player_genders, pick(valid_player_genders)) diff --git a/code/modules/client/preference_setup/general/02_language.dm b/code/modules/client/preference_setup/general/02_language.dm index c7cb7afca74..96864e8eed8 100644 --- a/code/modules/client/preference_setup/general/02_language.dm +++ b/code/modules/client/preference_setup/general/02_language.dm @@ -8,6 +8,9 @@ /datum/category_item/player_setup_item/general/language/save_character(var/savefile/S) S["language"] << pref.alternate_languages +/datum/category_item/player_setup_item/general/language/gather_load_query() + return list("ss13_characters" = list("vars" = list("language" = "alternate_languages"), "args" = list("id"))) + /datum/category_item/player_setup_item/general/language/sanitize_character() if(!islist(pref.alternate_languages)) pref.alternate_languages = list() diff --git a/code/modules/client/preference_setup/general/03_body.dm b/code/modules/client/preference_setup/general/03_body.dm index 08330e5e811..7c66c59901e 100644 --- a/code/modules/client/preference_setup/general/03_body.dm +++ b/code/modules/client/preference_setup/general/03_body.dm @@ -48,6 +48,21 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O S["organ_data"] << pref.organ_data S["rlimb_data"] << pref.rlimb_data +/datum/category_item/player_setup_item/general/body/gather_load_query() + return list("ss13_characters" = list("vars" = list("species", + "hair_colour", + "facial_colour", + "skin_tone" = "s_tone", + "skin_colour", + "hair_style" = "h_style", + "facial_style" = "f_style", + "eyes_colour", + "b_type", + "disabilities", + "organs_data" = "organ_data", + "organs_robotic" = "rlimb_data"), + "args" = list("id"))) + /datum/category_item/player_setup_item/general/body/sanitize_character(var/savefile/S) if(!pref.species || !(pref.species in playable_species)) pref.species = "Human" @@ -210,7 +225,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O pref.species = href_list["set_species"] if(prev_species != pref.species) mob_species = all_species[pref.species] - + //grab one of the valid hair styles for the newly chosen species var/list/valid_hairstyles = list() for(var/hairstyle in hair_styles_list) diff --git a/code/modules/client/preference_setup/general/04_equipment.dm b/code/modules/client/preference_setup/general/04_equipment.dm index bf933932b83..3d816dd6547 100644 --- a/code/modules/client/preference_setup/general/04_equipment.dm +++ b/code/modules/client/preference_setup/general/04_equipment.dm @@ -14,6 +14,9 @@ S["backbag"] << pref.backbag S["gear"] << pref.gear +/datum/category_item/player_setup_item/general/equipment/gather_load_query() + return list("ss13_characters" = list("vars" = list("underwear", "undershirt", "backbag", "gear"), "args" = list("id"))) + /datum/category_item/player_setup_item/general/equipment/sanitize_character() pref.backbag = sanitize_integer(pref.backbag, 1, backbaglist.len, initial(pref.backbag)) diff --git a/code/modules/client/preference_setup/general/05_background.dm b/code/modules/client/preference_setup/general/05_background.dm index 5708d3c757e..2abcb0bf989 100644 --- a/code/modules/client/preference_setup/general/05_background.dm +++ b/code/modules/client/preference_setup/general/05_background.dm @@ -22,6 +22,13 @@ S["religion"] << pref.religion S["nanotrasen_relation"] << pref.nanotrasen_relation +/datum/category_item/player_setup_item/general/background/gather_load_query() + return list("ss13_characters_flavour" = list("vars" = list("records_employment" = "gen_record", + "records_medical" = "med_record", + "records_security" = "sec_record"), + "args" = list("char_id")), + "ss13_characters" = list("vars" = list("home_system", "citizenship", "faction", "religion"), "args" = list("id"))) + /datum/category_item/player_setup_item/general/background/sanitize_character() if(!pref.home_system) pref.home_system = "Unset" if(!pref.citizenship) pref.citizenship = "None" diff --git a/code/modules/client/preference_setup/general/06_flavor.dm b/code/modules/client/preference_setup/general/06_flavor.dm index b312bb95e9b..19873858e61 100644 --- a/code/modules/client/preference_setup/general/06_flavor.dm +++ b/code/modules/client/preference_setup/general/06_flavor.dm @@ -33,6 +33,23 @@ for(var/module in robot_module_types) S["flavour_texts_robot_[module]"] << pref.flavour_texts_robot[module] +/datum/category_item/player_setup_item/general/flavor/gather_load_query() + var/list/var_list = list("flavour_general" = "flavor_texts/general", + "flavour_head" = "flavour_texts/head", + "flavour_face" = "flavour_texts/face", + "flavour_eyes" = "flavour_texts/eyes", + "flavour_torso" = "flavour_texts/torso", + "flavour_arms" = "flavour_texts/arms", + "flavour_hands" = "flavour_texts/hands", + "flavour_legs" = "flavour_texts/legs", + "flavour_feet" = "flavour_texts/feet", + "robot_default" = "flavour_texts_robot/default") + + for (var/module in robot_module_types) + var_list["robot_[module]"] = "flavour_texts_robot/[module]" + + return list("ss13_characters_flavour" = list("vars" = var_list, "args" = list("char_id"))) + /datum/category_item/player_setup_item/general/flavor/sanitize_character() return diff --git a/code/modules/client/preference_setup/global/01_ui.dm b/code/modules/client/preference_setup/global/01_ui.dm index 777a51d3549..3f21b4f2622 100644 --- a/code/modules/client/preference_setup/global/01_ui.dm +++ b/code/modules/client/preference_setup/global/01_ui.dm @@ -14,6 +14,9 @@ S["UI_style_alpha"] << pref.UI_style_alpha S["ooccolor"] << pref.ooccolor +/datum/category_item/player_setup_item/player_global/ui/gather_load_query() + return list("ss13_player_preferences" = list("vars" = list("UI_style", "UI_style_color", "UI_style_alpha", "ooccolor"), "args" = list("ckey"))) + /datum/category_item/player_setup_item/player_global/ui/sanitize_preferences() pref.UI_style = sanitize_inlist(pref.UI_style, all_ui_styles, initial(pref.UI_style)) pref.UI_style_color = sanitize_hexcolor(pref.UI_style_color, initial(pref.UI_style_color)) @@ -24,14 +27,14 @@ . += "UI Settings
" . += "UI Style: [pref.UI_style]
" . += "Custom UI (recommended for White UI):
" - . += "-Color: [pref.UI_style_color] 
__
 reset
" - . += "-Alpha(transparency): [pref.UI_style_alpha] reset
" + . += "-Color: [pref.UI_style_color]�
__
�reset
" + . += "-Alpha(transparency): [pref.UI_style_alpha]�reset
" if(can_select_ooc_color(user)) - . += "OOC Color: " + . += "OOC Color:�" if(pref.ooccolor == initial(pref.ooccolor)) . += "Using Default
" else - . += "[pref.ooccolor]
__
 reset
" + . += "[pref.ooccolor]
__
�reset
" /datum/category_item/player_setup_item/player_global/ui/OnTopic(var/href,var/list/href_list, var/mob/user) if(href_list["select_style"]) diff --git a/code/modules/client/preference_setup/global/02_settings.dm b/code/modules/client/preference_setup/global/02_settings.dm index 7b61a86cb44..2e18610039e 100644 --- a/code/modules/client/preference_setup/global/02_settings.dm +++ b/code/modules/client/preference_setup/global/02_settings.dm @@ -12,6 +12,9 @@ S["default_slot"] << pref.default_slot S["toggles"] << pref.toggles +/datum/category_item/player_setup_item/player_global/settings/gather_load_query() + return list("ss13_player_preferences" = list("vars" = list("lastchangelog", "current_character", "toggles", "asfx_toggles"), "args" = list("ckey"))) + /datum/category_item/player_setup_item/player_global/settings/sanitize_preferences() pref.lastchangelog = sanitize_text(pref.lastchangelog, initial(pref.lastchangelog)) pref.default_slot = sanitize_integer(pref.default_slot, 1, config.character_slots, initial(pref.default_slot)) diff --git a/code/modules/client/preference_setup/global/03_language.dm b/code/modules/client/preference_setup/global/03_language.dm index fa3fdbe6e9b..bacb97564f4 100644 --- a/code/modules/client/preference_setup/global/03_language.dm +++ b/code/modules/client/preference_setup/global/03_language.dm @@ -8,6 +8,9 @@ /datum/category_item/player_setup_item/player_global/language/save_preferences(var/savefile/S) S["language_prefixes"] << pref.language_prefixes +/datum/category_item/player_setup_item/player_global/language/gather_load_query() + return list("ss13_player_preferences" = list("vars" = list("language_prefixes"), "args" = list("ckey"))) + /datum/category_item/player_setup_item/player_global/language/sanitize_preferences() if(isnull(pref.language_prefixes) || !pref.language_prefixes.len) pref.language_prefixes = config.language_prefixes.Copy() @@ -52,4 +55,3 @@ if(istype(prefixes) && prefixes.len) preferences["language_prefixes"] = prefixes.Copy() return 1 - diff --git a/code/modules/client/preference_setup/global/04_pai.dm b/code/modules/client/preference_setup/global/04_pai.dm index a7102069e1b..529a751f335 100644 --- a/code/modules/client/preference_setup/global/04_pai.dm +++ b/code/modules/client/preference_setup/global/04_pai.dm @@ -13,6 +13,8 @@ candidate.savefile_load(preference_mob()) +// #TODO-MERGE: Figure out how to handle pAI saving on SQL. + /datum/category_item/player_setup_item/player_global/pai/save_preferences(var/savefile/S) if(!candidate) return diff --git a/code/modules/client/preference_setup/occupation/occupation.dm b/code/modules/client/preference_setup/occupation/occupation.dm index 92c6320c646..694efb92eeb 100644 --- a/code/modules/client/preference_setup/occupation/occupation.dm +++ b/code/modules/client/preference_setup/occupation/occupation.dm @@ -33,6 +33,9 @@ S["job_engsec_low"] << pref.job_engsec_low S["player_alt_titles"] << pref.player_alt_titles +/datum/category_item/player_setup_item/occupation/gather_load_query() + return list("ss13_characters" = list("vars" = list("jobs" = "unsanitized_jobs", "alternate_option", "alternate_titles" = "player_alt_titles"), "args" = list("id"))) + /datum/category_item/player_setup_item/occupation/sanitize_character() pref.alternate_option = sanitize_integer(pref.alternate_option, 0, 2, initial(pref.alternate_option)) pref.job_civilian_high = sanitize_integer(pref.job_civilian_high, 0, 65535, initial(pref.job_civilian_high)) diff --git a/code/modules/client/preference_setup/preference_setup.dm b/code/modules/client/preference_setup/preference_setup.dm index 0fa0bfa10c8..7c9f7a055e5 100644 --- a/code/modules/client/preference_setup/preference_setup.dm +++ b/code/modules/client/preference_setup/preference_setup.dm @@ -125,12 +125,93 @@ for(var/datum/category_item/player_setup_item/PI in items) PI.sanitize_character() +// For loading character by category. Utilizes query caching. /datum/category_group/player_setup_category/proc/load_character_sql() - var/static/list/query_cache = list() - if (isnull(query_cache[type])) - var/query = "FROM ss13_characters" - for (var/datum/category_item/player_setup_item/PI in items) + var/static/list/query_cache + if (isnull(query_cache)) + query_cache = list() + + // This handles the generation of queries. One per category, total of 5 UNLESS one category wants to query more than one table. + // It uses like, 3+ levels of lists. But it works, and is only run once per category in the global sense. So technically, efficient! + // :D - Skull132 + if (!query_cache.len || isnull(query_cache[type])) + query_cache[type] = list() + var/list/tables = list() + + // First, consolidate the different lists into one. + for (var/datum/category_item/player_setup_item/PI in items) + var/list/pi_tables = PI.gather_load_query() + // Expected layout: list("table A name" = list("vars" = list("column name" = "var name"), "args" = list()), + // "table B name" = list("vars" = list(), "args" = list())) + + for (var/table in pi_tables) + var/list/A = pi_tables[table] + if (isnull(tables[table])) + tables[table] = list("vars" = list(), "args" = list()) + + tables[table]["vars"] |= A["vars"] + tables[table]["args"] |= A["args"] + + // Second, create the queries and save them. + for (var/table in tables) + var/query = "SELECT " + var/list/var_names = tables[table]["vars"] + var/count = var_names.len + + // Process the variables and rows we want. + var/i = 1 + for (var/name in var_names) + query += name + + if (!isnull(var_names[name])) + var/new_name = var_names[name] + var_names.Remove(name) + var_names.Insert(i, new_name) + + if (i != count) + query += ", " + else + query += " " + + i++ + + query += "FROM [table] WHERE " + + // Process the args. + var/list/arg_names = tables[table]["args"] + count = arg_names.len + for (i = 1, i <= count, i++) + query += "[arg_names[i]] = :[arg_names[i]]" + arg_names[i] = ":[arg_names[i]]" + + if (i != count) + query += " AND " + else + query += ";" + + // Save it. + query_cache[type][query] = var_names + + // Actually utilize the query. + var/list/arg_list = gather_load_parameters() + for (var/query in query_cache[type]) + var/DBQuery/query = dbcon.NewQuery(query) + query.Execute(arg_list, 1) + + // Each query should only return exactly 1 row. + var/list/var_names = query_cache[type] + if (query.NextRow()) + for (var/i = 1, i <= var_names.len, i++) + // We can do this, because when generating, we ensured that + preferences.vars[var_names[i]] = query.item[i] + +/datum/category_group/player_setup_category/proc/gather_load_parameters() + var/list/arg_list = list() + for (var/datum/category_item/player_setup_item/PI in items) + arg_list |= PI.gather_load_parameters() + + return arg_list /datum/category_group/player_setup_category/proc/save_character(var/savefile/S) // Sanitize all data, then save it @@ -219,6 +300,18 @@ /datum/category_item/player_setup_item/proc/update_setup(var/savefile/preferences, var/savefile/character) return 0 +/* +* Called when the owner category is composing its load query +*/ +/datum/category_item/player_setup_item/proc/gather_load_query() + return list() + +/* +* Called when the owner category is composing its query parameters for loading. +*/ +/datum/category_item/player_setup_item/proc/gather_load_parameters() + return list() + /datum/category_item/player_setup_item/proc/content() return diff --git a/code/modules/client/preference_setup/skills/skills.dm b/code/modules/client/preference_setup/skills/skills.dm index 1f60cd3bb7f..7d525f315b7 100644 --- a/code/modules/client/preference_setup/skills/skills.dm +++ b/code/modules/client/preference_setup/skills/skills.dm @@ -12,6 +12,9 @@ S["used_skillpoints"] << pref.used_skillpoints S["skill_specialization"] << pref.skill_specialization +/datum/category_item/player_setup_item/skills/gather_load_query() + return list("ss13_characters" = list("vars" = list("skills", "skill_specialization"), "args" = list("id"))) + /datum/category_item/player_setup_item/skills/sanitize_character() if(SKILLS == null) setup_skills() if(!pref.skills) pref.skills = list() From 7c8e6f730ed8d78d88f6408d4fa7317c15f45a62 Mon Sep 17 00:00:00 2001 From: skull132 Date: Sat, 10 Dec 2016 17:04:39 +0200 Subject: [PATCH 03/12] Finalize loading queries No more compile errors or logic errors from these. --- .../preference_setup/preference_setup.dm | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/code/modules/client/preference_setup/preference_setup.dm b/code/modules/client/preference_setup/preference_setup.dm index 7c9f7a055e5..36342ce24fb 100644 --- a/code/modules/client/preference_setup/preference_setup.dm +++ b/code/modules/client/preference_setup/preference_setup.dm @@ -122,10 +122,16 @@ PI.load_character(S) else src.load_character_sql() + for(var/datum/category_item/player_setup_item/PI in items) PI.sanitize_character() -// For loading character by category. Utilizes query caching. +/* + * A proc for dynamically loading a character from the database. + * See the category items for procs on what pieces are given. + * + * Each query is cached as well, after generation. Only things recompiled are the arguments and the finalized query text (with arguments in it). + */ /datum/category_group/player_setup_category/proc/load_character_sql() var/static/list/query_cache @@ -193,18 +199,24 @@ // Save it. query_cache[type][query] = var_names - // Actually utilize the query. + // Need to typecast due to reasons. + var/datum/category_collection/player_setup_collection/cc = collection + + // Actually utilize the queries. var/list/arg_list = gather_load_parameters() - for (var/query in query_cache[type]) - var/DBQuery/query = dbcon.NewQuery(query) + for (var/query_text in query_cache[type]) + var/DBQuery/query = dbcon.NewQuery(query_text) query.Execute(arg_list, 1) // Each query should only return exactly 1 row. - var/list/var_names = query_cache[type] + var/list/var_names = query_cache[type][query_text] if (query.NextRow()) for (var/i = 1, i <= var_names.len, i++) - // We can do this, because when generating, we ensured that - preferences.vars[var_names[i]] = query.item[i] + var/list/layers = splittext(var_names[i], "/") + if (layers.len == 1) + cc.preferences.vars[var_names[i]] = query.item[i] + else + cc.preferences.vars[layers[1]][layers[2]] = query.item[i] /datum/category_group/player_setup_category/proc/gather_load_parameters() var/list/arg_list = list() From 7925063160c8a992e3005f1c5e48d9d09220af0c Mon Sep 17 00:00:00 2001 From: skull132 Date: Sat, 10 Dec 2016 19:48:00 +0200 Subject: [PATCH 04/12] SQL Saves - Sanitization Everything is now proper good sanitized! --- .../antagonism/01_candidacy.dm | 12 ++++-- .../preference_setup/general/01_basic.dm | 6 +-- .../preference_setup/general/02_language.dm | 8 +++- .../preference_setup/general/03_body.dm | 39 +++++++++++++++++-- .../preference_setup/general/04_equipment.dm | 9 ++++- .../preference_setup/general/05_background.dm | 12 ++++-- .../client/preference_setup/global/01_ui.dm | 2 +- .../preference_setup/global/02_settings.dm | 9 +++-- .../preference_setup/global/03_language.dm | 7 +++- .../preference_setup/occupation/occupation.dm | 24 ++++++------ .../preference_setup/preference_setup.dm | 6 +-- .../client/preference_setup/skills/skills.dm | 16 +++++--- code/modules/client/preferences.dm | 4 ++ 13 files changed, 111 insertions(+), 43 deletions(-) diff --git a/code/modules/client/preference_setup/antagonism/01_candidacy.dm b/code/modules/client/preference_setup/antagonism/01_candidacy.dm index 0c9a4a0cb3d..1bb96e80224 100644 --- a/code/modules/client/preference_setup/antagonism/01_candidacy.dm +++ b/code/modules/client/preference_setup/antagonism/01_candidacy.dm @@ -11,12 +11,16 @@ /datum/category_item/player_setup_item/antagonism/candidacy/gather_load_query() return list("ss13_characters" = list("vars" = list("be_special_role"), "args" = list("id"))) -/datum/category_item/player_setup_item/antagonism/candidacy/sanitize_character() - if(!istype(pref.be_special_role)) +/datum/category_item/player_setup_item/antagonism/candidacy/sanitize_character(var/sql_load = 0) + if (sql_load) + if (pref.be_special_role) + pref.be_special_role = params2list(pref.be_special_role) + + if (!istype(pref.be_special_role)) pref.be_special_role = list() - for(var/role in pref.be_special_role) - if(!(role in valid_special_roles())) + for (var/role in pref.be_special_role) + if (!(role in valid_special_roles())) pref.be_special_role -= role /datum/category_item/player_setup_item/antagonism/candidacy/content(var/mob/user) diff --git a/code/modules/client/preference_setup/general/01_basic.dm b/code/modules/client/preference_setup/general/01_basic.dm index ac430e4caac..4665e87d82a 100644 --- a/code/modules/client/preference_setup/general/01_basic.dm +++ b/code/modules/client/preference_setup/general/01_basic.dm @@ -25,17 +25,17 @@ "gender", "age", "metadata", - "spawnpoint"), + "spawnpoint",), "args" = list("id"))) /datum/category_item/player_setup_item/general/basic/sanitize_character() - pref.age = sanitize_integer(pref.age, AGE_MIN, AGE_MAX, initial(pref.age)) + pref.age = sanitize_integer(text2num(pref.age), AGE_MIN, AGE_MAX, initial(pref.age)) pref.gender = sanitize_inlist(pref.gender, valid_player_genders, pick(valid_player_genders)) pref.real_name = sanitize_name(pref.real_name, pref.species) if(!pref.real_name) pref.real_name = random_name(pref.gender, pref.species) pref.spawnpoint = sanitize_inlist(pref.spawnpoint, spawntypes, initial(pref.spawnpoint)) - pref.be_random_name = sanitize_integer(pref.be_random_name, 0, 1, initial(pref.be_random_name)) + pref.be_random_name = sanitize_integer(text2num(pref.be_random_name), 0, 1, initial(pref.be_random_name)) /datum/category_item/player_setup_item/general/basic/content() . = "Name: " diff --git a/code/modules/client/preference_setup/general/02_language.dm b/code/modules/client/preference_setup/general/02_language.dm index 96864e8eed8..101695cc0c4 100644 --- a/code/modules/client/preference_setup/general/02_language.dm +++ b/code/modules/client/preference_setup/general/02_language.dm @@ -11,8 +11,12 @@ /datum/category_item/player_setup_item/general/language/gather_load_query() return list("ss13_characters" = list("vars" = list("language" = "alternate_languages"), "args" = list("id"))) -/datum/category_item/player_setup_item/general/language/sanitize_character() - if(!islist(pref.alternate_languages)) pref.alternate_languages = list() +/datum/category_item/player_setup_item/general/language/sanitize_character(var/sql_load = 0) + if (sql_load) + pref.alternate_languages = params2list(pref.alternate_languages) + + if(!islist(pref.alternate_languages)) + pref.alternate_languages = list() /datum/category_item/player_setup_item/general/language/content() . += "Languages
" diff --git a/code/modules/client/preference_setup/general/03_body.dm b/code/modules/client/preference_setup/general/03_body.dm index 7c66c59901e..81868b07db6 100644 --- a/code/modules/client/preference_setup/general/03_body.dm +++ b/code/modules/client/preference_setup/general/03_body.dm @@ -63,9 +63,40 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O "organs_robotic" = "rlimb_data"), "args" = list("id"))) -/datum/category_item/player_setup_item/general/body/sanitize_character(var/savefile/S) +/datum/category_item/player_setup_item/general/body/sanitize_character(var/sql_load = 0) if(!pref.species || !(pref.species in playable_species)) pref.species = "Human" + + if (sql_load) + pref.hair_colour = sanitize_hexcolor(pref.hair_colour) + pref.r_hair = GetRedPart(pref.hair_colour) + pref.g_hair = GetGreenPart(pref.hair_colour) + pref.b_hair = GetBluePart(pref.hair_colour) + + pref.facial_colour = sanitize_hexcolor(pref.facial_colour) + pref.r_facial = GetRedPart(pref.facial_colour) + pref.g_facial = GetGreenPart(pref.facial_colour) + pref.b_facial = GetBluePart(pref.facial_colour) + + pref.s_tone = text2num(pref.s_tone) + + pref.skin_colour = sanitize_hexcolor(pref.skin_colour) + pref.r_skin = GetRedPart(pref.skin_colour) + pref.g_skin = GetGreenPart(pref.skin_colour) + pref.b_skin = GetBluePart(pref.skin_colour) + + pref.skin_colour = sanitize_hexcolor(pref.skin_colour) + pref.r_eyes = GetRedPart(pref.eyes_colour) + pref.g_eyes = GetGreenPart(pref.eyes_colour) + pref.b_eyes = GetBluePart(pref.eyes_colour) + + pref.disabilities = text2num(pref.disabilities) + + if (pref.organ_data) + pref.organ_data = params2list(pref.organ_data) + if (pref.rlimb_data) + pref.rlimb_data = params2list(pref.rlimb_data) + pref.r_hair = sanitize_integer(pref.r_hair, 0, 255, initial(pref.r_hair)) pref.g_hair = sanitize_integer(pref.g_hair, 0, 255, initial(pref.g_hair)) pref.b_hair = sanitize_integer(pref.b_hair, 0, 255, initial(pref.b_hair)) @@ -84,8 +115,10 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O pref.b_type = sanitize_text(pref.b_type, initial(pref.b_type)) pref.disabilities = sanitize_integer(pref.disabilities, 0, 65535, initial(pref.disabilities)) - if(!pref.organ_data) pref.organ_data = list() - if(!pref.rlimb_data) pref.rlimb_data = list() + if (!pref.organ_data || !islist(pref.organ_data)) + pref.organ_data = list() + if (!pref.rlimb_data || !islist(pref.rlimb_data)) + pref.rlimb_data = list() /datum/category_item/player_setup_item/general/body/content(var/mob/user) pref.update_preview_icon() diff --git a/code/modules/client/preference_setup/general/04_equipment.dm b/code/modules/client/preference_setup/general/04_equipment.dm index 3d816dd6547..8d53fb4c60a 100644 --- a/code/modules/client/preference_setup/general/04_equipment.dm +++ b/code/modules/client/preference_setup/general/04_equipment.dm @@ -17,10 +17,15 @@ /datum/category_item/player_setup_item/general/equipment/gather_load_query() return list("ss13_characters" = list("vars" = list("underwear", "undershirt", "backbag", "gear"), "args" = list("id"))) -/datum/category_item/player_setup_item/general/equipment/sanitize_character() +/datum/category_item/player_setup_item/general/equipment/sanitize_character(var/sql_load = 0) + if (sql_load) + pref.backbag = text2num(pref.backbag) + pref.gear = params2list(pref.gear) + pref.backbag = sanitize_integer(pref.backbag, 1, backbaglist.len, initial(pref.backbag)) - if(!islist(pref.gear)) pref.gear = list() + if (!islist(pref.gear)) + pref.gear = list() var/undies = get_undies() if(!get_key_by_value(undies, pref.underwear)) diff --git a/code/modules/client/preference_setup/general/05_background.dm b/code/modules/client/preference_setup/general/05_background.dm index 2abcb0bf989..f110bb5df38 100644 --- a/code/modules/client/preference_setup/general/05_background.dm +++ b/code/modules/client/preference_setup/general/05_background.dm @@ -30,10 +30,14 @@ "ss13_characters" = list("vars" = list("home_system", "citizenship", "faction", "religion"), "args" = list("id"))) /datum/category_item/player_setup_item/general/background/sanitize_character() - if(!pref.home_system) pref.home_system = "Unset" - if(!pref.citizenship) pref.citizenship = "None" - if(!pref.faction) pref.faction = "None" - if(!pref.religion) pref.religion = "None" + if(!pref.home_system) + pref.home_system = "Unset" + if(!pref.citizenship) + pref.citizenship = "None" + if(!pref.faction) + pref.faction = "None" + if(!pref.religion) + pref.religion = "None" pref.nanotrasen_relation = sanitize_inlist(pref.nanotrasen_relation, COMPANY_ALIGNMENTS, initial(pref.nanotrasen_relation)) diff --git a/code/modules/client/preference_setup/global/01_ui.dm b/code/modules/client/preference_setup/global/01_ui.dm index 3f21b4f2622..d69de5b8ea8 100644 --- a/code/modules/client/preference_setup/global/01_ui.dm +++ b/code/modules/client/preference_setup/global/01_ui.dm @@ -20,7 +20,7 @@ /datum/category_item/player_setup_item/player_global/ui/sanitize_preferences() pref.UI_style = sanitize_inlist(pref.UI_style, all_ui_styles, initial(pref.UI_style)) pref.UI_style_color = sanitize_hexcolor(pref.UI_style_color, initial(pref.UI_style_color)) - pref.UI_style_alpha = sanitize_integer(pref.UI_style_alpha, 0, 255, initial(pref.UI_style_alpha)) + pref.UI_style_alpha = sanitize_integer(text2num(pref.UI_style_alpha), 0, 255, initial(pref.UI_style_alpha)) pref.ooccolor = sanitize_hexcolor(pref.ooccolor, initial(pref.ooccolor)) /datum/category_item/player_setup_item/player_global/ui/content(var/mob/user) diff --git a/code/modules/client/preference_setup/global/02_settings.dm b/code/modules/client/preference_setup/global/02_settings.dm index 2e18610039e..21508bac980 100644 --- a/code/modules/client/preference_setup/global/02_settings.dm +++ b/code/modules/client/preference_setup/global/02_settings.dm @@ -15,10 +15,13 @@ /datum/category_item/player_setup_item/player_global/settings/gather_load_query() return list("ss13_player_preferences" = list("vars" = list("lastchangelog", "current_character", "toggles", "asfx_toggles"), "args" = list("ckey"))) -/datum/category_item/player_setup_item/player_global/settings/sanitize_preferences() +/datum/category_item/player_setup_item/player_global/settings/sanitize_preferences(var/sql_load = 0) + if (sql_load) + pref.current_character = text2num(pref.current_character) + pref.lastchangelog = sanitize_text(pref.lastchangelog, initial(pref.lastchangelog)) - pref.default_slot = sanitize_integer(pref.default_slot, 1, config.character_slots, initial(pref.default_slot)) - pref.toggles = sanitize_integer(pref.toggles, 0, 65535, initial(pref.toggles)) + pref.default_slot = sanitize_integer(text2num(pref.default_slot), 1, config.character_slots, initial(pref.default_slot)) + pref.toggles = sanitize_integer(text2num(pref.toggles), 0, 65535, initial(pref.toggles)) /datum/category_item/player_setup_item/player_global/settings/content(var/mob/user) . += "Play admin midis: [(pref.toggles & SOUND_MIDI) ? "Yes" : "No"]
" diff --git a/code/modules/client/preference_setup/global/03_language.dm b/code/modules/client/preference_setup/global/03_language.dm index bacb97564f4..740806cef7d 100644 --- a/code/modules/client/preference_setup/global/03_language.dm +++ b/code/modules/client/preference_setup/global/03_language.dm @@ -11,8 +11,11 @@ /datum/category_item/player_setup_item/player_global/language/gather_load_query() return list("ss13_player_preferences" = list("vars" = list("language_prefixes"), "args" = list("ckey"))) -/datum/category_item/player_setup_item/player_global/language/sanitize_preferences() - if(isnull(pref.language_prefixes) || !pref.language_prefixes.len) +/datum/category_item/player_setup_item/player_global/language/sanitize_preferences(var/sql_load = 0) + if (sql_load && pref.language_prefixes) + pref.language_prefixes = params2list(pref.language_prefixes) + + if (isnull(pref.language_prefixes) || !pref.language_prefixes.len) pref.language_prefixes = config.language_prefixes.Copy() /datum/category_item/player_setup_item/player_global/language/content(var/mob/user) diff --git a/code/modules/client/preference_setup/occupation/occupation.dm b/code/modules/client/preference_setup/occupation/occupation.dm index 694efb92eeb..0e5fd7b24f6 100644 --- a/code/modules/client/preference_setup/occupation/occupation.dm +++ b/code/modules/client/preference_setup/occupation/occupation.dm @@ -37,17 +37,19 @@ return list("ss13_characters" = list("vars" = list("jobs" = "unsanitized_jobs", "alternate_option", "alternate_titles" = "player_alt_titles"), "args" = list("id"))) /datum/category_item/player_setup_item/occupation/sanitize_character() - pref.alternate_option = sanitize_integer(pref.alternate_option, 0, 2, initial(pref.alternate_option)) - pref.job_civilian_high = sanitize_integer(pref.job_civilian_high, 0, 65535, initial(pref.job_civilian_high)) - pref.job_civilian_med = sanitize_integer(pref.job_civilian_med, 0, 65535, initial(pref.job_civilian_med)) - pref.job_civilian_low = sanitize_integer(pref.job_civilian_low, 0, 65535, initial(pref.job_civilian_low)) - pref.job_medsci_high = sanitize_integer(pref.job_medsci_high, 0, 65535, initial(pref.job_medsci_high)) - pref.job_medsci_med = sanitize_integer(pref.job_medsci_med, 0, 65535, initial(pref.job_medsci_med)) - pref.job_medsci_low = sanitize_integer(pref.job_medsci_low, 0, 65535, initial(pref.job_medsci_low)) - pref.job_engsec_high = sanitize_integer(pref.job_engsec_high, 0, 65535, initial(pref.job_engsec_high)) - pref.job_engsec_med = sanitize_integer(pref.job_engsec_med, 0, 65535, initial(pref.job_engsec_med)) - pref.job_engsec_low = sanitize_integer(pref.job_engsec_low, 0, 65535, initial(pref.job_engsec_low)) - if(!pref.player_alt_titles) pref.player_alt_titles = new() + pref.alternate_option = sanitize_integer(text2num(pref.alternate_option), 0, 2, initial(pref.alternate_option)) + pref.job_civilian_high = sanitize_integer(text2num(pref.job_civilian_high), 0, 65535, initial(pref.job_civilian_high)) + pref.job_civilian_med = sanitize_integer(text2num(pref.job_civilian_med), 0, 65535, initial(pref.job_civilian_med)) + pref.job_civilian_low = sanitize_integer(text2num(pref.job_civilian_low), 0, 65535, initial(pref.job_civilian_low)) + pref.job_medsci_high = sanitize_integer(text2num(pref.job_medsci_high), 0, 65535, initial(pref.job_medsci_high)) + pref.job_medsci_med = sanitize_integer(text2num(pref.job_medsci_med), 0, 65535, initial(pref.job_medsci_med)) + pref.job_medsci_low = sanitize_integer(text2num(pref.job_medsci_low), 0, 65535, initial(pref.job_medsci_low)) + pref.job_engsec_high = sanitize_integer(text2num(pref.job_engsec_high), 0, 65535, initial(pref.job_engsec_high)) + pref.job_engsec_med = sanitize_integer(text2num(pref.job_engsec_med), 0, 65535, initial(pref.job_engsec_med)) + pref.job_engsec_low = sanitize_integer(text2num(pref.job_engsec_low), 0, 65535, initial(pref.job_engsec_low)) + + if (!pref.player_alt_titles) + pref.player_alt_titles = new() if(!job_master) return diff --git a/code/modules/client/preference_setup/preference_setup.dm b/code/modules/client/preference_setup/preference_setup.dm index 36342ce24fb..16fa6779c0d 100644 --- a/code/modules/client/preference_setup/preference_setup.dm +++ b/code/modules/client/preference_setup/preference_setup.dm @@ -124,7 +124,7 @@ src.load_character_sql() for(var/datum/category_item/player_setup_item/PI in items) - PI.sanitize_character() + PI.sanitize_character(config.sql_saves) /* * A proc for dynamically loading a character from the database. @@ -327,10 +327,10 @@ /datum/category_item/player_setup_item/proc/content() return -/datum/category_item/player_setup_item/proc/sanitize_character() +/datum/category_item/player_setup_item/proc/sanitize_character(var/sql_load = 0) return -/datum/category_item/player_setup_item/proc/sanitize_preferences() +/datum/category_item/player_setup_item/proc/sanitize_preferences(var/sql_load = 0) return /datum/category_item/player_setup_item/Topic(var/href,var/list/href_list) diff --git a/code/modules/client/preference_setup/skills/skills.dm b/code/modules/client/preference_setup/skills/skills.dm index 7d525f315b7..8960673f0e8 100644 --- a/code/modules/client/preference_setup/skills/skills.dm +++ b/code/modules/client/preference_setup/skills/skills.dm @@ -15,11 +15,17 @@ /datum/category_item/player_setup_item/skills/gather_load_query() return list("ss13_characters" = list("vars" = list("skills", "skill_specialization"), "args" = list("id"))) -/datum/category_item/player_setup_item/skills/sanitize_character() - if(SKILLS == null) setup_skills() - if(!pref.skills) pref.skills = list() - if(!pref.skills.len) pref.ZeroSkills() - if(pref.used_skillpoints < 0) pref.used_skillpoints = 0 +/datum/category_item/player_setup_item/skills/sanitize_character(var/sql_load = 0) + if (SKILLS == null) + setup_skills() + if (!pref.skills) + pref.skills = list() + if (sql_load) + pref.skills = params2list(pref.skills) + if (!pref.skills.len) + pref.ZeroSkills() + if (pref.used_skillpoints < 0) + pref.used_skillpoints = 0 /datum/category_item/player_setup_item/skills/content() . += "Select your Skills
" diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index fca0f009e71..6ea1f057db3 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -40,17 +40,21 @@ datum/preferences var/undershirt //undershirt type var/backbag = 2 //backpack type var/h_style = "Bald" //Hair type + var/hair_colour = "#000000" //Hair colour hex value, for SQL loading var/r_hair = 0 //Hair color var/g_hair = 0 //Hair color var/b_hair = 0 //Hair color var/f_style = "Shaved" //Face hair type + var/facial_colour = "#000000" //Facial colour hex value, for SQL loading var/r_facial = 0 //Face hair color var/g_facial = 0 //Face hair color var/b_facial = 0 //Face hair color var/s_tone = 0 //Skin tone + var/skin_colour = "#000000" //Skin colour hex value, for SQL loading var/r_skin = 0 //Skin color var/g_skin = 0 //Skin color var/b_skin = 0 //Skin color + var/eyes_colour = "#000000" //Eye colour hex value, for SQL loading var/r_eyes = 0 //Eye color var/g_eyes = 0 //Eye color var/b_eyes = 0 //Eye color From 57501a305c77af96455dc1c5fd20d089ddf1942f Mon Sep 17 00:00:00 2001 From: skull132 Date: Sat, 10 Dec 2016 23:28:35 +0200 Subject: [PATCH 05/12] SQL Loading - preferences vs character A simple whitelist so we don't end up running bad queries when we only want the character. --- .../preference_setup/preference_setup.dm | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/code/modules/client/preference_setup/preference_setup.dm b/code/modules/client/preference_setup/preference_setup.dm index 16fa6779c0d..da59832595d 100644 --- a/code/modules/client/preference_setup/preference_setup.dm +++ b/code/modules/client/preference_setup/preference_setup.dm @@ -3,6 +3,9 @@ #define TOPIC_HANDLED 1 #define TOPIC_REFRESH 2 +#define SQL_CHARACTER 1 +#define SQL_PREFERENCES 2 + /datum/category_group/player_setup_category/general_preferences name = "General" sort_order = 1 @@ -27,6 +30,7 @@ name = "Global" sort_order = 5 category_item_type = /datum/category_item/player_setup_item/player_global + sql_role = SQL_PREFERENCES /**************************** * Category Collection Setup * @@ -104,6 +108,7 @@ **************************/ /datum/category_group/player_setup_category var/sort_order = 0 + var/sql_role = SQL_CHARACTER /datum/category_group/player_setup_category/dd_SortValue() return sort_order @@ -117,11 +122,12 @@ /datum/category_group/player_setup_category/proc/load_character(var/savefile/S) // Load all data, then sanitize it. // Need due to, for example, the 01_basic module relying on species having been loaded to sanitize correctly but that isn't loaded until module 03_body. - if (!config.sql_saves && !establish_db_connection(dbcon)) + if (!config.sql_saves || !establish_db_connection(dbcon)) for(var/datum/category_item/player_setup_item/PI in items) PI.load_character(S) else - src.load_character_sql() + // Load every category minus the global + handle_sql_loading(SQL_CHARACTER) for(var/datum/category_item/player_setup_item/PI in items) PI.sanitize_character(config.sql_saves) @@ -132,9 +138,13 @@ * * Each query is cached as well, after generation. Only things recompiled are the arguments and the finalized query text (with arguments in it). */ -/datum/category_group/player_setup_category/proc/load_character_sql() +/datum/category_group/player_setup_category/proc/handle_sql_loading(var/load_type) var/static/list/query_cache + // We aren't loading this category. Bye. + if (sql_role != load_type) + return + if (isnull(query_cache)) query_cache = list() @@ -233,10 +243,14 @@ PI.save_character(S) /datum/category_group/player_setup_category/proc/load_preferences(var/savefile/S) + if (!config.sql_saves || !establish_db_connection(dbcon)) + for(var/datum/category_item/player_setup_item/PI in items) + PI.load_preferences(S) + else + handle_sql_loading(SQL_PREFERENCES) + for(var/datum/category_item/player_setup_item/PI in items) - PI.load_preferences(S) - for(var/datum/category_item/player_setup_item/PI in items) - PI.sanitize_preferences() + PI.sanitize_preferences(config.sql_saves) /datum/category_group/player_setup_category/proc/save_preferences(var/savefile/S) for(var/datum/category_item/player_setup_item/PI in items) @@ -353,3 +367,6 @@ /datum/category_item/player_setup_item/proc/preference_mob() if(pref && pref.client && pref.client.mob) return pref.client.mob + +#undef SQL_CHARACTER +#undef SQL_PREFERENCES From 2b63c7e394d4ec9a59cfd43c54524bcd960673cb Mon Sep 17 00:00:00 2001 From: skull132 Date: Sun, 11 Dec 2016 00:35:58 +0200 Subject: [PATCH 06/12] SQL Saves - Loading more vars Forgot to load some of the new vars. --- .../modules/client/preference_setup/global/02_settings.dm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/code/modules/client/preference_setup/global/02_settings.dm b/code/modules/client/preference_setup/global/02_settings.dm index 21508bac980..33c1b13b117 100644 --- a/code/modules/client/preference_setup/global/02_settings.dm +++ b/code/modules/client/preference_setup/global/02_settings.dm @@ -6,14 +6,20 @@ S["lastchangelog"] >> pref.lastchangelog S["default_slot"] >> pref.default_slot S["toggles"] >> pref.toggles + S["asfx_togs"] >> pref.asfx_togs + S["motd_hash"] >> pref.motd_hash + S["memo_hash"] >> pref.memo_hash /datum/category_item/player_setup_item/player_global/settings/save_preferences(var/savefile/S) S["lastchangelog"] << pref.lastchangelog S["default_slot"] << pref.default_slot S["toggles"] << pref.toggles + S["asfx_togs"] << pref.asfx_togs + S["motd_hash"] << pref.motd_hash + S["memo_hash"] << pref.memo_hash /datum/category_item/player_setup_item/player_global/settings/gather_load_query() - return list("ss13_player_preferences" = list("vars" = list("lastchangelog", "current_character", "toggles", "asfx_toggles"), "args" = list("ckey"))) + return list("ss13_player_preferences" = list("vars" = list("lastchangelog", "current_character", "toggles", "asfx_toggles" = "asfx_togs", "motd_hash", "memo_hash"), "args" = list("ckey"))) /datum/category_item/player_setup_item/player_global/settings/sanitize_preferences(var/sql_load = 0) if (sql_load) From 5fbd15a846747d55a4396f97d922dcb6332a0bb0 Mon Sep 17 00:00:00 2001 From: skull132 Date: Sun, 11 Dec 2016 01:39:00 +0200 Subject: [PATCH 07/12] Job sanitization Because I forgot, K? Will runtime because I haven't added the second var yet but sure. Yay. --- .../preference_setup/occupation/occupation.dm | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/code/modules/client/preference_setup/occupation/occupation.dm b/code/modules/client/preference_setup/occupation/occupation.dm index 0e5fd7b24f6..08d771c98d1 100644 --- a/code/modules/client/preference_setup/occupation/occupation.dm +++ b/code/modules/client/preference_setup/occupation/occupation.dm @@ -36,7 +36,19 @@ /datum/category_item/player_setup_item/occupation/gather_load_query() return list("ss13_characters" = list("vars" = list("jobs" = "unsanitized_jobs", "alternate_option", "alternate_titles" = "player_alt_titles"), "args" = list("id"))) -/datum/category_item/player_setup_item/occupation/sanitize_character() +/datum/category_item/player_setup_item/occupation/sanitize_character(var/sql_load = 0) + if (sql_load) + pref.alternate_option = text2num(alternate_option) + + var/list/jobs = params2list(pref.unsanitized_jobs) + + for (var/preference in jobs) + try + pref.vars[preference] = text2num(jobs[preference]) + catch(var/exception/e) + log_debug("LOADING: Bad job preference key: [preference].") + log_debug(e.desc) + pref.alternate_option = sanitize_integer(text2num(pref.alternate_option), 0, 2, initial(pref.alternate_option)) pref.job_civilian_high = sanitize_integer(text2num(pref.job_civilian_high), 0, 65535, initial(pref.job_civilian_high)) pref.job_civilian_med = sanitize_integer(text2num(pref.job_civilian_med), 0, 65535, initial(pref.job_civilian_med)) From ff7f93cbd8a56246f1aa81857e3e2269aaeae4f0 Mon Sep 17 00:00:00 2001 From: skull132 Date: Sun, 11 Dec 2016 21:44:35 +0200 Subject: [PATCH 08/12] Saving + Updating + ALL THE THINGS Everything. Absoloutely positively everything. Fuck. Hey, it compiles now, doe! --- baystation12.dme | 1 + code/datums/server_greeting.dm | 2 +- code/modules/admin/admin_verbs.dm | 4 +- .../antagonism/01_candidacy.dm | 9 + .../preference_setup/antagonism/02_setup.dm | 10 + .../preference_setup/general/01_basic.dm | 23 +- .../preference_setup/general/02_language.dm | 11 + .../preference_setup/general/03_body.dm | 33 +++ .../preference_setup/general/04_equipment.dm | 9 + .../preference_setup/general/05_background.dm | 21 ++ .../preference_setup/general/06_flavor.dm | 54 ++++- .../client/preference_setup/global/01_ui.dm | 9 + .../preference_setup/global/02_settings.dm | 18 ++ .../preference_setup/global/03_language.dm | 9 + .../preference_setup/occupation/occupation.dm | 21 +- .../preference_setup/preference_setup.dm | 154 ++++---------- .../preference_setup/preferences_sql.dm | 197 ++++++++++++++++++ .../client/preference_setup/skills/skills.dm | 9 + code/modules/client/preferences.dm | 32 +-- code/modules/client/preferences_ambience.dm | 6 +- code/modules/client/preferences_toggles.dm | 26 +-- code/modules/mob/mob.dm | 2 +- code/modules/mob/typing_indicator.dm | 2 +- 23 files changed, 494 insertions(+), 168 deletions(-) create mode 100644 code/modules/client/preference_setup/preferences_sql.dm diff --git a/baystation12.dme b/baystation12.dme index e4c660ca7e1..884ad691a88 100644 --- a/baystation12.dme +++ b/baystation12.dme @@ -1013,6 +1013,7 @@ #include "code\modules\client\preferences_toggles.dm" #include "code\modules\client\ui_style.dm" #include "code\modules\client\preference_setup\preference_setup.dm" +#include "code\modules\client\preference_setup\preferences_sql.dm" #include "code\modules\client\preference_setup\antagonism\01_candidacy.dm" #include "code\modules\client\preference_setup\antagonism\02_setup.dm" #include "code\modules\client\preference_setup\general\01_basic.dm" diff --git a/code/datums/server_greeting.dm b/code/datums/server_greeting.dm index 70c9409ef95..0a9511d018f 100644 --- a/code/datums/server_greeting.dm +++ b/code/datums/server_greeting.dm @@ -207,7 +207,7 @@ user << output(JS_SANITIZE(data), "greeting.browser:AddContent") if (save_prefs) - user.prefs.handle_preferences_save(user) + user.prefs.save_preferences() /* * Basically the Topic proc for the greeting datum. diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 732421ab782..56be3304b8e 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -551,7 +551,7 @@ var/list/admin_verbs_cciaa = list( prefs.ooccolor = input(src, "Please select your OOC colour.", "OOC colour") as color else if(response == "Reset to default") prefs.ooccolor = initial(prefs.ooccolor) - prefs.handle_preferences_save(src) + prefs.save_preferences() feedback_add_details("admin_verb","OC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! return @@ -920,7 +920,7 @@ var/list/admin_verbs_cciaa = list( usr << "You now will get attack log messages" else usr << "You now won't get attack log messages" - prefs.handle_preferences_save(src) + prefs.save_preferences() /client/proc/toggleghostwriters() diff --git a/code/modules/client/preference_setup/antagonism/01_candidacy.dm b/code/modules/client/preference_setup/antagonism/01_candidacy.dm index 1bb96e80224..8909069f3d4 100644 --- a/code/modules/client/preference_setup/antagonism/01_candidacy.dm +++ b/code/modules/client/preference_setup/antagonism/01_candidacy.dm @@ -11,6 +11,15 @@ /datum/category_item/player_setup_item/antagonism/candidacy/gather_load_query() return list("ss13_characters" = list("vars" = list("be_special_role"), "args" = list("id"))) +/datum/category_item/player_setup_item/antagonism/candidacy/gather_load_parameters() + return list(":id" = pref.current_character) + +/datum/category_item/player_setup_item/antagonism/candidacy/gather_save_query() + return list("ss13_characters" = list("be_special_role", "id" = 1)) + +/datum/category_item/player_setup_item/antagonism/candidacy/gather_save_parameters() + return list(":be_special_role" = list2params(pref.be_special_role), ":id" = pref.current_character) + /datum/category_item/player_setup_item/antagonism/candidacy/sanitize_character(var/sql_load = 0) if (sql_load) if (pref.be_special_role) diff --git a/code/modules/client/preference_setup/antagonism/02_setup.dm b/code/modules/client/preference_setup/antagonism/02_setup.dm index c84c2a6781f..59e06d2b3dc 100644 --- a/code/modules/client/preference_setup/antagonism/02_setup.dm +++ b/code/modules/client/preference_setup/antagonism/02_setup.dm @@ -16,6 +16,16 @@ var/global/list/uplink_locations = list("PDA", "Headset", "None") return list("ss13_characters_flavour" = list("vars" = list("records_exploit" = "exploit_record"), "args" = list("char_id")), "ss13_characters" = list("vars" = list("uplink_location" = "uplinklocation"), "args" = list("id"))) +/datum/category_item/player_setup_item/antagonism/basic/gather_load_parameters() + return list(":char_id" = pref.current_character, ":id" = pref.current_character) + +/datum/category_item/player_setup_item/antagonism/basic/gather_save_query() + return list("ss13_characters_flavour" = list("records_exploit", "char_id" = 1), + "ss13_characters" = list("uplink_location", "id" = 1)) + +/datum/category_item/player_setup_item/antagonism/basic/gather_save_parameters() + return list(":records_exploit" = pref.exploit_record, ":char_id" = pref.current_character, ":uplink_location" = pref.uplinklocation, ":id" = pref.current_character) + /datum/category_item/player_setup_item/antagonism/basic/sanitize_character() pref.uplinklocation = sanitize_inlist(pref.uplinklocation, uplink_locations, initial(pref.uplinklocation)) diff --git a/code/modules/client/preference_setup/general/01_basic.dm b/code/modules/client/preference_setup/general/01_basic.dm index 4665e87d82a..651c7ad331b 100644 --- a/code/modules/client/preference_setup/general/01_basic.dm +++ b/code/modules/client/preference_setup/general/01_basic.dm @@ -1,7 +1,7 @@ /datum/category_item/player_setup_item/general/basic name = "Basic" sort_order = 1 - var/list/valid_player_genders = list(MALE, FEMALE) + var/static/list/valid_player_genders = list(MALE, FEMALE) /datum/category_item/player_setup_item/general/basic/load_character(var/savefile/S) S["real_name"] >> pref.real_name @@ -28,6 +28,27 @@ "spawnpoint",), "args" = list("id"))) +/datum/category_item/player_setup_item/general/basic/gather_load_parameters() + return list(":id" = pref.current_character) + +/datum/category_item/player_setup_item/general/basic/gather_save_query() + return list("ss13_characters" = list("name", + "random_name", + "gender", + "age", + "metadata", + "spawnpoint", + "id" = 1)) + +/datum/category_item/player_setup_item/general/basic/gather_save_parameters() + return list("ss13_characters" = list(":name" = pref.real_name, + ":random_name" = pref.be_random_name, + ":gender" = pref.gender, + ":age" = pref.age, + ":metadata" = pref.metadata, + ":spawnpoint" = pref.spawnpoint, + ":id" = pref.current_character)) + /datum/category_item/player_setup_item/general/basic/sanitize_character() pref.age = sanitize_integer(text2num(pref.age), AGE_MIN, AGE_MAX, initial(pref.age)) pref.gender = sanitize_inlist(pref.gender, valid_player_genders, pick(valid_player_genders)) diff --git a/code/modules/client/preference_setup/general/02_language.dm b/code/modules/client/preference_setup/general/02_language.dm index 101695cc0c4..61857ffbf48 100644 --- a/code/modules/client/preference_setup/general/02_language.dm +++ b/code/modules/client/preference_setup/general/02_language.dm @@ -11,6 +11,17 @@ /datum/category_item/player_setup_item/general/language/gather_load_query() return list("ss13_characters" = list("vars" = list("language" = "alternate_languages"), "args" = list("id"))) +/datum/category_item/player_setup_item/general/language/gather_load_parameters() + return list(":id" = pref.current_character) + +/datum/category_item/player_setup_item/general/language/gather_save_query() + return list("ss13_characters" = list("language", + "id" = 1)) + +/datum/category_item/player_setup_item/general/language/gather_save_parameters() + return list("ss13_characters" = list(":language" = list2params(pref.alternate_languages), + ":id" = pref.current_character)) + /datum/category_item/player_setup_item/general/language/sanitize_character(var/sql_load = 0) if (sql_load) pref.alternate_languages = params2list(pref.alternate_languages) diff --git a/code/modules/client/preference_setup/general/03_body.dm b/code/modules/client/preference_setup/general/03_body.dm index 81868b07db6..eb68f418c98 100644 --- a/code/modules/client/preference_setup/general/03_body.dm +++ b/code/modules/client/preference_setup/general/03_body.dm @@ -63,6 +63,39 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O "organs_robotic" = "rlimb_data"), "args" = list("id"))) +/datum/category_item/player_setup_item/general/body/gather_load_parameters() + return list(":id" = pref.current_character) + +/datum/category_item/player_setup_item/general/body/gather_save_query() + return list("ss13_characters" = list("species", + "hair_colour", + "facial_colour", + "skin_tone", + "skin_colour", + "hair_style", + "facial_style", + "eyes_colour", + "b_type", + "disabilities", + "organs_data", + "organs_robotic", + "id" = 1)) + +/datum/category_item/player_setup_item/general/body/gather_save_parameters() + return list("ss13_characters" = list(":species" = pref.species, + ":hair_colour" = "#" + num2hex(pref.r_hair) + num2hex(pref.g_hair) + num2hex(pref.b_hair), + ":facial_colour" = "#" + num2hex(pref.r_facial) + num2hex(pref.g_facial) + num2hex(pref.b_facial), + ":skin_tone" = pref.s_tone, + ":skin_colour" = "#" + num2hex(pref.r_skin) + num2hex(pref.g_skin) + num2hex(pref.b_skin), + ":hair_style" = pref.h_style, + ":facial_style" = pref.f_style, + ":eyes_colour" = "#" + num2hex(pref.r_eyes) + num2hex(pref.g_eyes) + num2hex(pref.b_eyes), + ":b_type" = pref.b_type, + ":disabilities" = pref.disabilities, + ":organs_data" = list2params(pref.organ_data), + ":organs_robotic"= list2params(pref.rlimb_data), + ":id" = pref.current_character)) + /datum/category_item/player_setup_item/general/body/sanitize_character(var/sql_load = 0) if(!pref.species || !(pref.species in playable_species)) pref.species = "Human" diff --git a/code/modules/client/preference_setup/general/04_equipment.dm b/code/modules/client/preference_setup/general/04_equipment.dm index 8d53fb4c60a..a4297812471 100644 --- a/code/modules/client/preference_setup/general/04_equipment.dm +++ b/code/modules/client/preference_setup/general/04_equipment.dm @@ -17,6 +17,15 @@ /datum/category_item/player_setup_item/general/equipment/gather_load_query() return list("ss13_characters" = list("vars" = list("underwear", "undershirt", "backbag", "gear"), "args" = list("id"))) +/datum/category_item/player_setup_item/general/equipment/gather_load_parameters() + return list(":id" = pref.current_character) + +/datum/category_item/player_setup_item/general/equipment/gather_save_query() + return list("ss13_characters" = list("underwear", "undershirt", "backbag", "gear", "id" = 1)) + +/datum/category_item/player_setup_item/general/equipment/gather_save_parameters() + return list(":underwear" = pref.underwear, ":undershirt" = pref.undershirt, ":backbag" = pref.backbag, ":gear" = list2params(pref.gear), ":id" = pref.current_character) + /datum/category_item/player_setup_item/general/equipment/sanitize_character(var/sql_load = 0) if (sql_load) pref.backbag = text2num(pref.backbag) diff --git a/code/modules/client/preference_setup/general/05_background.dm b/code/modules/client/preference_setup/general/05_background.dm index f110bb5df38..7fe7a7be13e 100644 --- a/code/modules/client/preference_setup/general/05_background.dm +++ b/code/modules/client/preference_setup/general/05_background.dm @@ -29,6 +29,27 @@ "args" = list("char_id")), "ss13_characters" = list("vars" = list("home_system", "citizenship", "faction", "religion"), "args" = list("id"))) +/datum/category_item/player_setup_item/general/background/gather_load_parameters() + return list(":id" = pref.current_character, ":char_id" = pref.current_character) + +/datum/category_item/player_setup_item/general/background/gather_save_query() + return list("ss13_characters_flavour" = list("records_employment", + "records_medical", + "records_security", + "char_id" = 1), + "ss13_characters" = list("home_system", "citizenship", "faction", "religion", "id" = 1)) + +/datum/category_item/player_setup_item/general/background/gather_save_parameters() + return list(":records_employment" = pref.gen_record, + ":records_medical" = pref.med_record, + ":records_security" = pref.sec_record, + ":char_id" = pref.current_character, + ":home_system" = pref.home_system, + ":citizenship" = pref.citizenship, + ":faction" = pref.faction, + ":religion" = pref.religion, + ":id" = pref.current_character) + /datum/category_item/player_setup_item/general/background/sanitize_character() if(!pref.home_system) pref.home_system = "Unset" diff --git a/code/modules/client/preference_setup/general/06_flavor.dm b/code/modules/client/preference_setup/general/06_flavor.dm index 19873858e61..f4cbdbe1b43 100644 --- a/code/modules/client/preference_setup/general/06_flavor.dm +++ b/code/modules/client/preference_setup/general/06_flavor.dm @@ -35,14 +35,14 @@ /datum/category_item/player_setup_item/general/flavor/gather_load_query() var/list/var_list = list("flavour_general" = "flavor_texts/general", - "flavour_head" = "flavour_texts/head", - "flavour_face" = "flavour_texts/face", - "flavour_eyes" = "flavour_texts/eyes", - "flavour_torso" = "flavour_texts/torso", - "flavour_arms" = "flavour_texts/arms", - "flavour_hands" = "flavour_texts/hands", - "flavour_legs" = "flavour_texts/legs", - "flavour_feet" = "flavour_texts/feet", + "flavour_head" = "flavor_texts/head", + "flavour_face" = "flavor_texts/face", + "flavour_eyes" = "flavor_texts/eyes", + "flavour_torso" = "flavor_texts/torso", + "flavour_arms" = "flavor_texts/arms", + "flavour_hands" = "flavor_texts/hands", + "flavour_legs" = "flavor_texts/legs", + "flavour_feet" = "flavor_texts/feet", "robot_default" = "flavour_texts_robot/default") for (var/module in robot_module_types) @@ -50,6 +50,44 @@ return list("ss13_characters_flavour" = list("vars" = var_list, "args" = list("char_id"))) +/datum/category_item/player_setup_item/general/flavor/gather_load_parameters() + return list(":char_id" = pref.current_character) + +/datum/category_item/player_setup_item/general/flavor/gather_save_query() + var/list/var_list = list("flavour_general", + "flavour_head", + "flavour_face", + "flavour_eyes", + "flavour_torso", + "flavour_arms", + "flavour_hands", + "flavour_legs", + "flavour_feet", + "robot_default", + "char_id" = 1) + + for (var/module in robot_module_types) + var_list += "robot_[module]" + + return list("ss13_characters_flavour" = var_list) + +/datum/category_item/player_setup_item/general/flavor/gather_save_parameters() + var/list/var_list = list(":char_id" = pref.current_character, + ":flavour_head" = pref.flavor_texts["head"], + ":flavour_face" = pref.flavor_texts["face"], + ":flavour_eyes" = pref.flavor_texts["eyes"], + ":flavour_torso" = pref.flavor_texts["torso"], + ":flavour_arms" = pref.flavor_texts["arms"], + ":flavour_hands" = pref.flavor_texts["hands"], + ":flavour_legs" = pref.flavor_texts["legs"], + ":flavour_feet" = pref.flavor_texts["feet"], + ":robot_default" = pref.flavour_texts_robot["default"]) + + for (var/module in robot_module_types) + var_list[":robot_[module]"] += pref.flavour_texts_robot[module] + + return list("ss13_characters_flavour" = var_list) + /datum/category_item/player_setup_item/general/flavor/sanitize_character() return diff --git a/code/modules/client/preference_setup/global/01_ui.dm b/code/modules/client/preference_setup/global/01_ui.dm index d69de5b8ea8..0585039cad7 100644 --- a/code/modules/client/preference_setup/global/01_ui.dm +++ b/code/modules/client/preference_setup/global/01_ui.dm @@ -17,6 +17,15 @@ /datum/category_item/player_setup_item/player_global/ui/gather_load_query() return list("ss13_player_preferences" = list("vars" = list("UI_style", "UI_style_color", "UI_style_alpha", "ooccolor"), "args" = list("ckey"))) +/datum/category_item/player_setup_item/player_global/ui/gather_load_parameters() + return list(":ckey" = pref.client.ckey) + +/datum/category_item/player_setup_item/player_global/ui/gather_save_query() + return list("ss13_player_preferences" = list("UI_style", "UI_style_color", "UI_style_alpha", "ooccolor", "ckey" = 1)) + +/datum/category_item/player_setup_item/player_global/ui/gather_save_parameters() + return list(":ckey" = pref.client.ckey, ":UI_style" = pref.UI_style, ":UI_style_alpha" = pref.UI_style_alpha, ":UI_style_color" = pref.UI_style_color, ":ooccolor" = pref.ooccolor) + /datum/category_item/player_setup_item/player_global/ui/sanitize_preferences() pref.UI_style = sanitize_inlist(pref.UI_style, all_ui_styles, initial(pref.UI_style)) pref.UI_style_color = sanitize_hexcolor(pref.UI_style_color, initial(pref.UI_style_color)) diff --git a/code/modules/client/preference_setup/global/02_settings.dm b/code/modules/client/preference_setup/global/02_settings.dm index 33c1b13b117..332e6bdd0a5 100644 --- a/code/modules/client/preference_setup/global/02_settings.dm +++ b/code/modules/client/preference_setup/global/02_settings.dm @@ -21,6 +21,21 @@ /datum/category_item/player_setup_item/player_global/settings/gather_load_query() return list("ss13_player_preferences" = list("vars" = list("lastchangelog", "current_character", "toggles", "asfx_toggles" = "asfx_togs", "motd_hash", "memo_hash"), "args" = list("ckey"))) +/datum/category_item/player_setup_item/player_global/settings/gather_load_parameters() + return list(":ckey" = pref.client.ckey) + +/datum/category_item/player_setup_item/player_global/settings/gather_save_query() + return list("ss13_player_preferences" = list("lastchangelog", "current_character", "toggles", "asfx_toggles", "motd_hash", "memo_hash", "ckey" = 1)) + +/datum/category_item/player_setup_item/player_global/settings/gather_save_parameters() + return list(":ckey" = pref.client.ckey, + ":lastchangelog" = pref.lastchangelog, + ":current_character" = pref.current_character, + ":toggles" = pref.toggles, + ":asfx_toggles" = pref.asfx_togs, + ":motd_hash" = pref.motd_hash, + ":memo_hash" = pref.memo_hash) + /datum/category_item/player_setup_item/player_global/settings/sanitize_preferences(var/sql_load = 0) if (sql_load) pref.current_character = text2num(pref.current_character) @@ -28,6 +43,9 @@ pref.lastchangelog = sanitize_text(pref.lastchangelog, initial(pref.lastchangelog)) pref.default_slot = sanitize_integer(text2num(pref.default_slot), 1, config.character_slots, initial(pref.default_slot)) pref.toggles = sanitize_integer(text2num(pref.toggles), 0, 65535, initial(pref.toggles)) + pref.asfx_togs = sanitize_integer(text2num(pref.asfx_togs), 0, 65535, initial(pref.toggles)) + pref.motd_hash = sanitize_text(pref.motd_hash, initial(pref.motd_hash)) + pref.memo_hash = sanitize_text(pref.memo_hash, initial(pref.memo_hash)) /datum/category_item/player_setup_item/player_global/settings/content(var/mob/user) . += "Play admin midis: [(pref.toggles & SOUND_MIDI) ? "Yes" : "No"]
" diff --git a/code/modules/client/preference_setup/global/03_language.dm b/code/modules/client/preference_setup/global/03_language.dm index 740806cef7d..c5f8a7d1164 100644 --- a/code/modules/client/preference_setup/global/03_language.dm +++ b/code/modules/client/preference_setup/global/03_language.dm @@ -11,6 +11,15 @@ /datum/category_item/player_setup_item/player_global/language/gather_load_query() return list("ss13_player_preferences" = list("vars" = list("language_prefixes"), "args" = list("ckey"))) +/datum/category_item/player_setup_item/player_global/language/gather_load_parameters() + return list(":ckey" = pref.client.ckey) + +/datum/category_item/player_setup_item/player_global/ui/gather_save_query() + return list("ss13_player_preferences" = list("language_prefixes", "ckey" = 1)) + +/datum/category_item/player_setup_item/player_global/language/gather_save_parameters() + return list(":ckey" = pref.client.ckey, ":language_prefixes" = list2params(pref.language_prefixes)) + /datum/category_item/player_setup_item/player_global/language/sanitize_preferences(var/sql_load = 0) if (sql_load && pref.language_prefixes) pref.language_prefixes = params2list(pref.language_prefixes) diff --git a/code/modules/client/preference_setup/occupation/occupation.dm b/code/modules/client/preference_setup/occupation/occupation.dm index 08d771c98d1..0a9fa071a61 100644 --- a/code/modules/client/preference_setup/occupation/occupation.dm +++ b/code/modules/client/preference_setup/occupation/occupation.dm @@ -36,9 +36,28 @@ /datum/category_item/player_setup_item/occupation/gather_load_query() return list("ss13_characters" = list("vars" = list("jobs" = "unsanitized_jobs", "alternate_option", "alternate_titles" = "player_alt_titles"), "args" = list("id"))) +/datum/category_item/player_setup_item/occupation/gather_load_parameters() + return list(":id" = pref.current_character) + +/datum/category_item/player_setup_item/occupation/gather_save_query() + return list("ss13_characters" = list("jobs", "alternate_option", "alternate_titles", "id" = 1)) + +/datum/category_item/player_setup_item/occupation/gather_save_parameters() + var/list/compiled_jobs = list("job_civilian_high" = pref.job_civilian_high, + "job_civilian_med" = pref.job_civilian_med, + "job_civilian_low" = pref.job_civilian_low, + "job_medsci_high" = pref.job_medsci_high, + "job_medsci_med" = pref.job_medsci_med, + "job_medsci_low" = pref.job_medsci_low, + "job_engsec_high" = pref.job_engsec_high, + "job_engsec_med" = pref.job_engsec_med, + "job_engsec_low" = pref.job_engsec_low) + + return list(":jobs" = list2params(compiled_jobs), ":alternate_option" = pref.alternate_option, ":alternate_titles" = list2params(pref.player_alt_titles), ":id" = pref.current_character) + /datum/category_item/player_setup_item/occupation/sanitize_character(var/sql_load = 0) if (sql_load) - pref.alternate_option = text2num(alternate_option) + pref.alternate_option = text2num(pref.alternate_option) var/list/jobs = params2list(pref.unsanitized_jobs) diff --git a/code/modules/client/preference_setup/preference_setup.dm b/code/modules/client/preference_setup/preference_setup.dm index da59832595d..6d80bb3f315 100644 --- a/code/modules/client/preference_setup/preference_setup.dm +++ b/code/modules/client/preference_setup/preference_setup.dm @@ -109,6 +109,7 @@ /datum/category_group/player_setup_category var/sort_order = 0 var/sql_role = SQL_CHARACTER + var/modified = 0 /datum/category_group/player_setup_category/dd_SortValue() return sort_order @@ -132,131 +133,40 @@ for(var/datum/category_item/player_setup_item/PI in items) PI.sanitize_character(config.sql_saves) -/* - * A proc for dynamically loading a character from the database. - * See the category items for procs on what pieces are given. - * - * Each query is cached as well, after generation. Only things recompiled are the arguments and the finalized query text (with arguments in it). - */ -/datum/category_group/player_setup_category/proc/handle_sql_loading(var/load_type) - var/static/list/query_cache - - // We aren't loading this category. Bye. - if (sql_role != load_type) - return - - if (isnull(query_cache)) - query_cache = list() - - // This handles the generation of queries. One per category, total of 5 UNLESS one category wants to query more than one table. - // It uses like, 3+ levels of lists. But it works, and is only run once per category in the global sense. So technically, efficient! - // :D - Skull132 - if (!query_cache.len || isnull(query_cache[type])) - query_cache[type] = list() - var/list/tables = list() - - // First, consolidate the different lists into one. - for (var/datum/category_item/player_setup_item/PI in items) - var/list/pi_tables = PI.gather_load_query() - // Expected layout: list("table A name" = list("vars" = list("column name" = "var name"), "args" = list()), - // "table B name" = list("vars" = list(), "args" = list())) - - for (var/table in pi_tables) - var/list/A = pi_tables[table] - if (isnull(tables[table])) - tables[table] = list("vars" = list(), "args" = list()) - - tables[table]["vars"] |= A["vars"] - tables[table]["args"] |= A["args"] - - // Second, create the queries and save them. - for (var/table in tables) - var/query = "SELECT " - var/list/var_names = tables[table]["vars"] - var/count = var_names.len - - // Process the variables and rows we want. - var/i = 1 - for (var/name in var_names) - query += name - - if (!isnull(var_names[name])) - var/new_name = var_names[name] - var_names.Remove(name) - var_names.Insert(i, new_name) - - if (i != count) - query += ", " - else - query += " " - - i++ - - query += "FROM [table] WHERE " - - // Process the args. - var/list/arg_names = tables[table]["args"] - count = arg_names.len - for (i = 1, i <= count, i++) - query += "[arg_names[i]] = :[arg_names[i]]" - arg_names[i] = ":[arg_names[i]]" - - if (i != count) - query += " AND " - else - query += ";" - - // Save it. - query_cache[type][query] = var_names - - // Need to typecast due to reasons. - var/datum/category_collection/player_setup_collection/cc = collection - - // Actually utilize the queries. - var/list/arg_list = gather_load_parameters() - for (var/query_text in query_cache[type]) - var/DBQuery/query = dbcon.NewQuery(query_text) - query.Execute(arg_list, 1) - - // Each query should only return exactly 1 row. - var/list/var_names = query_cache[type][query_text] - if (query.NextRow()) - for (var/i = 1, i <= var_names.len, i++) - var/list/layers = splittext(var_names[i], "/") - if (layers.len == 1) - cc.preferences.vars[var_names[i]] = query.item[i] - else - cc.preferences.vars[layers[1]][layers[2]] = query.item[i] - -/datum/category_group/player_setup_category/proc/gather_load_parameters() - var/list/arg_list = list() - for (var/datum/category_item/player_setup_item/PI in items) - arg_list |= PI.gather_load_parameters() - - return arg_list - /datum/category_group/player_setup_category/proc/save_character(var/savefile/S) // Sanitize all data, then save it - for(var/datum/category_item/player_setup_item/PI in items) + for (var/datum/category_item/player_setup_item/PI in items) PI.sanitize_character() - for(var/datum/category_item/player_setup_item/PI in items) - PI.save_character(S) + + if (!config.sql_saves || !establish_db_connection(dbcon)) + for (var/datum/category_item/player_setup_item/PI in items) + PI.save_character(S) + else + if (modified && sql_role == SQL_CHARACTER) + handle_sql_saving(SQL_CHARACTER) + modified = 0 /datum/category_group/player_setup_category/proc/load_preferences(var/savefile/S) if (!config.sql_saves || !establish_db_connection(dbcon)) - for(var/datum/category_item/player_setup_item/PI in items) + for (var/datum/category_item/player_setup_item/PI in items) PI.load_preferences(S) else handle_sql_loading(SQL_PREFERENCES) - for(var/datum/category_item/player_setup_item/PI in items) + for (var/datum/category_item/player_setup_item/PI in items) PI.sanitize_preferences(config.sql_saves) /datum/category_group/player_setup_category/proc/save_preferences(var/savefile/S) - for(var/datum/category_item/player_setup_item/PI in items) + for (var/datum/category_item/player_setup_item/PI in items) PI.sanitize_preferences() - for(var/datum/category_item/player_setup_item/PI in items) - PI.save_preferences(S) + + if (!config.sql_saves || !establish_db_connection(dbcon)) + for (var/datum/category_item/player_setup_item/PI in items) + PI.save_preferences(S) + else + if (modified && sql_role == SQL_PREFERENCES) + handle_sql_saving(SQL_PREFERENCES) + modified = 0 /datum/category_group/player_setup_category/proc/update_setup(var/savefile/preferences, var/savefile/character) for(var/datum/category_item/player_setup_item/PI in items) @@ -332,12 +242,24 @@ /datum/category_item/player_setup_item/proc/gather_load_query() return list() +/* +* Called when the owner category is composing its insert query +*/ +/datum/category_item/player_setup_item/proc/gather_save_query() + return list() + /* * Called when the owner category is composing its query parameters for loading. */ /datum/category_item/player_setup_item/proc/gather_load_parameters() return list() +/* +* Called when the owner category is composing its query parameters for inserting a new record. +*/ +/datum/category_item/player_setup_item/proc/gather_save_parameters() + return list() + /datum/category_item/player_setup_item/proc/content() return @@ -351,11 +273,14 @@ if(..()) return 1 var/mob/user = usr - if(!user.client) + if (!user.client) return 1 . = OnTopic(href, href_list, user) - if(. == TOPIC_REFRESH) + if (. != TOPIC_NOACTION) + var/datum/category_group/player_setup_category/cat = category + cat.modified = 1 + if (. == TOPIC_REFRESH) user.client.prefs.ShowChoices(user) /datum/category_item/player_setup_item/CanUseTopic(var/mob/user) @@ -367,6 +292,3 @@ /datum/category_item/player_setup_item/proc/preference_mob() if(pref && pref.client && pref.client.mob) return pref.client.mob - -#undef SQL_CHARACTER -#undef SQL_PREFERENCES diff --git a/code/modules/client/preference_setup/preferences_sql.dm b/code/modules/client/preference_setup/preferences_sql.dm new file mode 100644 index 00000000000..369dbcbac21 --- /dev/null +++ b/code/modules/client/preference_setup/preferences_sql.dm @@ -0,0 +1,197 @@ +/* + * A proc for dynamically loading a character from the database. + * See the category items for procs on what pieces are given. + * + * Each query is cached as well, after generation. Only things recompiled are the arguments and the finalized query text (with arguments in it). + */ +/datum/category_group/player_setup_category/proc/handle_sql_loading(var/role_type) + var/static/list/query_cache + + // We aren't loading this category. Bye. + if (role_type && sql_role != role_type) + return + + if (isnull(query_cache)) + query_cache = list() + + // This handles the generation of queries. One per category, total of 5 UNLESS one category wants to query more than one table. + // It uses like, 3+ levels of lists. But it works, and is only run once per category in the global sense. So technically, efficient! + // :D - Skull132 + if (!query_cache.len || isnull(query_cache[type])) + query_cache[type] = list() + var/list/tables = list() + + // First, consolidate the different lists into one. + for (var/datum/category_item/player_setup_item/PI in items) + var/list/pi_tables = PI.gather_load_query() + // Expected layout: list("table A name" = list("vars" = list("column name" = "var name"), "args" = list()), + // "table B name" = list("vars" = list(), "args" = list())) + + for (var/table in pi_tables) + var/list/A = pi_tables[table] + if (isnull(tables[table])) + tables[table] = list("vars" = list(), "args" = list()) + + tables[table]["vars"] |= A["vars"] + tables[table]["args"] |= A["args"] + + // Second, create the queries and save them. + for (var/table in tables) + var/query = "SELECT " + var/list/var_names = tables[table]["vars"] + var/count = var_names.len + + // Process the variables and rows we want. + var/i = 1 + for (var/name in var_names) + query += name + + if (!isnull(var_names[name])) + var/new_name = var_names[name] + var_names.Remove(name) + var_names.Insert(i, new_name) + + if (i != count) + query += ", " + else + query += " " + + i++ + + query += "FROM [table] WHERE " + + // Process the args. + var/list/arg_names = tables[table]["args"] + count = arg_names.len + for (i = 1, i <= count, i++) + query += "[arg_names[i]] = :[arg_names[i]]" + arg_names[i] = ":[arg_names[i]]" + + if (i != count) + query += " AND " + else + query += ";" + + // Save it. + query_cache[type][query] = var_names + + // Need to typecast due to reasons. + var/datum/category_collection/player_setup_collection/cc = collection + + // Actually utilize the queries. + var/list/arg_list = gather_load_parameters() + for (var/query_text in query_cache[type]) + var/DBQuery/query = dbcon.NewQuery(query_text) + query.Execute(arg_list, 1) + if (query.ErrorMsg()) + error("Error loading character from SQL: [query.ErrorMsg()]") + + // Each query should only return exactly 1 row. + var/list/var_names = query_cache[type][query_text] + if (query.NextRow()) + for (var/i = 1, i <= var_names.len, i++) + var/list/layers = splittext(var_names[i], "/") + try + if (layers.len == 1) + cc.preferences.vars[var_names[i]] = query.item[i] + else + cc.preferences.vars[layers[1]][layers[2]] = query.item[i] + catch(var/exception/e) + error("Error loading character from SQL: [e.name]") + log_debug("SQL Saves: [e.name]") + log_debug("SQL Saves: [e.desc]") + +/datum/category_group/player_setup_category/proc/gather_load_parameters() + var/list/arg_list = list() + for (var/datum/category_item/player_setup_item/PI in items) + arg_list |= PI.gather_load_parameters() + + return arg_list + +/* + * A proc for dynamically inserting new character and preference records into the database. + * See the category items for procs on what pieces are given. + * + * Each query is cached as well, after generation. Only things recompiled are the arguments and the finalized query text (with arguments in it). + */ +/datum/category_group/player_setup_category/proc/handle_sql_saving(var/role_type) + var/static/list/query_cache + + // We aren't loading this category. Bye. + if (role_type && sql_role != role_type) + return + + if (isnull(query_cache)) + query_cache = list() + + // This handles the generation of queries. One per category, total of 5 UNLESS one category wants to query more than one table. + // It uses like, 3+ levels of lists. But it works, and is only run once per category in the global sense. So technically, efficient! + // :D - Skull132 + if (!query_cache.len || isnull(query_cache[type])) + query_cache[type] = list() + var/list/tables = list() + + // First, consolidate the different lists into one. + for (var/datum/category_item/player_setup_item/PI in items) + var/list/pi_tables = PI.gather_save_query() + // Expected layout: list("table A name" = list("var1" = 1, "var2" = 0, "var3" = 1), + // "table B name" = list("var4" = 0, "var5" = 0, "var6" = 0)) + + for (var/table in pi_tables) + var/list/A = pi_tables[table] + if (isnull(tables[table])) + tables[table] = list() + + tables[table] |= A + + // Second, create the queries and save them. + for (var/table in tables) + var/query = "INSERT INTO [table] (" + var/list/var_names = tables[table] + + query += "[jointext(var_names, ", ")]) VALUES (" + + // Process the args. + var/list/arg_names = list() + for (var/variable in var_names) + arg_names += ":[variable]" + + query += "[jointext(arg_names, ", ")]) ON DUPLICATE KEY UPDATE" + + var/i = 1 + for (var/variable in var_names) + if (isnull(var_names[variable])) + query += " [variable] = [arg_names[i]]" + + i++ + + // Save it. + query_cache[type] += query + + // Actually utilize the queries. + var/list/arg_list = gather_save_parameters() + for (var/query_text in query_cache[type]) + var/DBQuery/query = dbcon.NewQuery(query_text) + query.Execute(arg_list, 1) + + if (query.ErrorMsg()) + error("Error saving character to SQL: [query.ErrorMsg()]") + + // Get the character ID if we inserted a new character. + if (role_type == SQL_CHARACTER) + // Need to typecast due to reasons. + var/datum/category_collection/player_setup_collection/cc = collection + + if (!cc.preferences.current_character) + var/DBQuery/query = dbcon.NewQuery("SELECT id FROM ss13_characters WHERE ckey = :ckey ORDER BY id DESC LIMIT 1") + query.Execute(list(":ckey" = cc.preferences.client.ckey)) + + if (query.NextRow()) + cc.preferences.current_character = text2num(query.item[1]) + +/datum/category_group/player_setup_category/proc/gather_save_parameters() + var/list/arg_list = list() + for (var/datum/category_item/player_setup_item/PI in items) + arg_list |= PI.gather_save_parameters() + + return arg_list diff --git a/code/modules/client/preference_setup/skills/skills.dm b/code/modules/client/preference_setup/skills/skills.dm index 8960673f0e8..1cb25641828 100644 --- a/code/modules/client/preference_setup/skills/skills.dm +++ b/code/modules/client/preference_setup/skills/skills.dm @@ -15,6 +15,15 @@ /datum/category_item/player_setup_item/skills/gather_load_query() return list("ss13_characters" = list("vars" = list("skills", "skill_specialization"), "args" = list("id"))) +/datum/category_item/player_setup_item/skills/gather_load_parameters() + return list(":id" = pref.current_character) + +/datum/category_item/player_setup_item/skills/gather_save_query() + return list("ss13_characters" = list("skills", "skill_specialization", "id" = 1)) + +/datum/category_item/player_setup_item/skills/gather_save_parameters() + return list(":skills" = list2params(pref.skills), ":skill_specialization" = pref.skill_specialization, ":id" = pref.current_character) + /datum/category_item/player_setup_item/skills/sanitize_character(var/sql_load = 0) if (SKILLS == null) setup_skills() diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 6ea1f057db3..22467520d60 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -88,6 +88,9 @@ datum/preferences var/job_engsec_med = 0 var/job_engsec_low = 0 + // A text blob which temporarily houses data from the SQL. + var/unsanitized_jobs = "" + //Keeps track of preferrence for not getting any wanted jobs var/alternate_option = 0 @@ -126,22 +129,6 @@ datum/preferences var/datum/category_collection/player_setup_collection/player_setup /datum/preferences/New(client/C) - // #TODO-MERGE: Review SQL saving and migration - if (config.sql_saves && C.need_saves_migrated) - var/dat = "
The server attempted to automatically migrate your saves from the old file-system onto an SQL database.
" - dat += "This operation was " - if (!handle_saves_migration(C)) - error("Failed migrating saves from [C.ckey] to the database!") - dat += "unsuccessful!

" - dat += "There was most likely an error in one of the SQL commands used to handle this migration. Do not worry: your saves are still safe in the old system and retreivable. Please contact Skull132 immediately in order to have the issue resolved. Until such a time, you will have to play without your old characters.
" - else - dat += "successful!

" - dat += "All of your character and preference data should be now neatly housed on the SQL database. Please look over it to see if anything is wrong or otherwise messed up. If you find something like that, please contact Skull132 immediately. Your old saves are still completely safe on the old system, should anything have gone wrong!

" - dat += "You should now immediately load a character. Otherwise you risk overriding one of your characters! Once that is done, everything shuold be fine." - - C << browse(dat, "window=migrate_status") - update_migrate_status(C) - player_setup = new(src) gender = pick(MALE, FEMALE) real_name = random_name(gender,species) @@ -153,16 +140,16 @@ datum/preferences client = C if(!IsGuestKey(C.key)) load_path(C.ckey) - handle_preferences_load(C) + load_preferences() load_and_update_character() ZeroSkills(1) /datum/preferences/proc/load_and_update_character(var/slot) - handle_character_load(get_default_slot(), client) + load_character(slot) if(update_setup(loaded_preferences, loaded_character)) - handle_preferences_save(client) - handle_character_save(client) + save_preferences() + save_character() /datum/preferences/proc/getMinAge(var/age_min) if(species == "Vaurca" || species == "Machine" || species == "Diona") @@ -284,7 +271,10 @@ datum/preferences load_character() else if(href_list["load"]) if(!IsGuestKey(usr.key)) - open_load_dialog(usr) + if (config.sql_saves) + open_load_dialog_sql(usr) + else + open_load_dialog_file(usr) return 1 else if(href_list["changeslot"]) load_character(text2num(href_list["changeslot"])) diff --git a/code/modules/client/preferences_ambience.dm b/code/modules/client/preferences_ambience.dm index 6f081f8512c..7b68efa4cfd 100644 --- a/code/modules/client/preferences_ambience.dm +++ b/code/modules/client/preferences_ambience.dm @@ -18,7 +18,7 @@ set category = "Preferences" set desc = "Toggles hearing ambient sound effects" prefs.asfx_togs ^= ASFX_AMBIENCE - prefs.handle_preferences_save(src) + prefs.save_preferences() if(prefs.asfx_togs & ASFX_AMBIENCE) src << "You will now hear ambient sounds." else @@ -33,7 +33,7 @@ set desc = "Toggles hearing footstep sound effects" prefs.asfx_togs ^= ASFX_FOOTSTEPS - prefs.handle_preferences_save(src) + prefs.save_preferences() if(prefs.asfx_togs & ASFX_FOOTSTEPS) src << "You will now hear footstep sounds." else @@ -45,7 +45,7 @@ set category = "SoundFx Prefs" set desc = "Toggles hearing of the vote alarm" prefs.asfx_togs ^= ASFX_VOTE - prefs.handle_preferences_save(src) + prefs.save_preferences() if(prefs.asfx_togs & ASFX_VOTE) src << "You will now hear the vote alarm." else diff --git a/code/modules/client/preferences_toggles.dm b/code/modules/client/preferences_toggles.dm index 0da4e42815c..b2d0958dbd5 100644 --- a/code/modules/client/preferences_toggles.dm +++ b/code/modules/client/preferences_toggles.dm @@ -5,7 +5,7 @@ set desc = ".Toggle Between seeing all mob speech, and only speech of nearby mobs" prefs.toggles ^= CHAT_GHOSTEARS src << "As a ghost, you will now [(prefs.toggles & CHAT_GHOSTEARS) ? "see all speech in the world" : "only see speech from nearby mobs"]." - prefs.handle_preferences_save(src) + prefs.save_preferences() feedback_add_details("admin_verb","TGE") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/verb/toggle_ghost_sight() @@ -14,7 +14,7 @@ set desc = ".Toggle Between seeing all mob emotes, and only emotes of nearby mobs" prefs.toggles ^= CHAT_GHOSTSIGHT src << "As a ghost, you will now [(prefs.toggles & CHAT_GHOSTSIGHT) ? "see all emotes in the world" : "only see emotes from nearby mobs"]." - prefs.handle_preferences_save(src) + prefs.save_preferences() feedback_add_details("admin_verb","TGS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/verb/toggle_ghost_radio() @@ -23,7 +23,7 @@ set desc = ".Toggle between hearing all radio chatter, or only from nearby speakers" prefs.toggles ^= CHAT_GHOSTRADIO src << "As a ghost, you will now [(prefs.toggles & CHAT_GHOSTRADIO) ? "hear all radio chat in the world" : "only hear from nearby speakers"]." - prefs.handle_preferences_save(src) + prefs.save_preferences() feedback_add_details("admin_verb","TGR") /client/proc/toggle_hear_radio() @@ -32,7 +32,7 @@ set desc = "Toggle seeing radiochatter from radios and speakers" if(!holder) return prefs.toggles ^= CHAT_RADIO - prefs.handle_preferences_save(src) + prefs.save_preferences() usr << "You will [(prefs.toggles & CHAT_RADIO) ? "now" : "no longer"] see radio chatter from radios or speakers" feedback_add_details("admin_verb","THR") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -42,7 +42,7 @@ set desc = "Toggle hearing a notification when admin PMs are recieved" if(!holder) return prefs.toggles ^= SOUND_ADMINHELP - prefs.handle_preferences_save(src) + prefs.save_preferences() usr << "You will [(prefs.toggles & SOUND_ADMINHELP) ? "now" : "no longer"] hear a sound when adminhelps arrive." feedback_add_details("admin_verb","AHS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -51,7 +51,7 @@ set category = "Preferences" set desc ="Toggles seeing deadchat" prefs.toggles ^= CHAT_DEAD - prefs.handle_preferences_save(src) + prefs.save_preferences() if(src.holder) src << "You will [(prefs.toggles & CHAT_DEAD) ? "now" : "no longer"] see deadchat." @@ -65,7 +65,7 @@ set category = "Preferences" set desc = "Toggles seeing prayers" prefs.toggles ^= CHAT_PRAYER - prefs.handle_preferences_save(src) + prefs.save_preferences() src << "You will [(prefs.toggles & CHAT_PRAYER) ? "now" : "no longer"] see prayerchat." feedback_add_details("admin_verb","TP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -74,7 +74,7 @@ set category = "Preferences" set desc = "Toggles hearing the GameLobby music" prefs.toggles ^= SOUND_LOBBY - prefs.handle_preferences_save(src) + prefs.save_preferences() if(prefs.toggles & SOUND_LOBBY) src << "You will now hear music in the game lobby." if(istype(mob, /mob/new_player)) @@ -90,7 +90,7 @@ set category = "Preferences" set desc = "Toggles hearing sounds uploaded by admins" prefs.toggles ^= SOUND_MIDI - prefs.handle_preferences_save(src) + prefs.save_preferences() if(prefs.toggles & SOUND_MIDI) src << "You will now hear any sounds uploaded by admins." var/sound/break_sound = sound(null, repeat = 0, wait = 0, channel = 777) @@ -105,7 +105,7 @@ set category = "Preferences" set desc = "Toggles seeing OutOfCharacter chat" prefs.toggles ^= CHAT_OOC - prefs.handle_preferences_save(src) + prefs.save_preferences() src << "You will [(prefs.toggles & CHAT_OOC) ? "now" : "no longer"] see messages on the OOC channel." feedback_add_details("admin_verb","TOOC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -115,7 +115,7 @@ set category = "Preferences" set desc = "Toggles seeing Local OutOfCharacter chat" prefs.toggles ^= CHAT_LOOC - prefs.handle_preferences_save(src) + prefs.save_preferences() src << "You will [(prefs.toggles & CHAT_LOOC) ? "now" : "no longer"] see messages on the LOOC channel." feedback_add_details("admin_verb","TLOOC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -126,7 +126,7 @@ set category = "Preferences" set desc = "Toggles seeing chat tags/icons" prefs.toggles ^= CHAT_NOICONS - prefs.handle_preferences_save(src) + prefs.save_preferences() src << "You will [!(prefs.toggles & CHAT_NOICONS) ? "now" : "no longer"] see chat tag icons." feedback_add_details("admin_verb","TCTAG") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -137,7 +137,7 @@ set category = "Preferences" set desc = "Toggles hearing ambient sound effects" prefs.toggles ^= SOUND_AMBIENCE - prefs.handle_preferences_save(src) + prefs.save_preferences() if(prefs.toggles & SOUND_AMBIENCE) src << "You will now hear ambient sounds." else diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 38423925bfa..95002223f49 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -512,7 +512,7 @@ src << browse('html/changelog.html', "window=changes;size=675x650") if(prefs.lastchangelog != changelog_hash) prefs.lastchangelog = changelog_hash - prefs.handle_preferences_save(src) + prefs.save_preferences() winset(src, "rpane.changelog", "background-color=none;font-style=;") /mob/verb/observe() diff --git a/code/modules/mob/typing_indicator.dm b/code/modules/mob/typing_indicator.dm index 53d68def64f..b6179c544ce 100644 --- a/code/modules/mob/typing_indicator.dm +++ b/code/modules/mob/typing_indicator.dm @@ -78,7 +78,7 @@ mob/var/obj/effect/decal/typing_indicator set category = "Preferences" set desc = "Toggles showing an indicator when you are typing emote or say message." prefs.toggles ^= SHOW_TYPING - prefs.handle_preferences_save(src) + prefs.save_preferences() src << "You will [(prefs.toggles & SHOW_TYPING) ? "no longer" : "now"] display a typing indicator." // Clear out any existing typing indicator. From 16bf480525c98ed59f642cb4d804bfe0c844018a Mon Sep 17 00:00:00 2001 From: skull132 Date: Sun, 11 Dec 2016 21:45:21 +0200 Subject: [PATCH 09/12] Clean-up time! Removing worthless files. --- .../client/preferences_save_migration.dm | 86 --- code/modules/client/preferences_sql.dm | 709 ------------------ 2 files changed, 795 deletions(-) delete mode 100644 code/modules/client/preferences_save_migration.dm delete mode 100644 code/modules/client/preferences_sql.dm diff --git a/code/modules/client/preferences_save_migration.dm b/code/modules/client/preferences_save_migration.dm deleted file mode 100644 index 70b6c9bdcf8..00000000000 --- a/code/modules/client/preferences_save_migration.dm +++ /dev/null @@ -1,86 +0,0 @@ -/* - * The method by which player saves are migrated to the SQL database from savefile oriented storage. - */ - -/datum/preferences/proc/handle_saves_migration(var/client/C) - if (!C || !C.need_saves_migrated) - return 1 - - if (!path) - load_path(C.ckey) - if (!path) - log_debug("Migration of [C.ckey]'s saves failed at attaining path to save file.") - return 0 - - if (!migrate_client_preferences(C)) - log_debug("Migration of [C.ckey]'s saves failed at the uploading of client preferences.") - return 0 - - if (!migrate_client_characters(C)) - log_debug("Migration of [C.ckey]'s saves failed at the uploading of client characters.") - return 0 - - return 1 - -/* - * Handles the migration of preferences. - * Loads the prefs to the datum, then saves them to SQL. - */ -/datum/preferences/proc/migrate_client_preferences(var/client/C) - load_preferences() - - return insert_preferences_sql(C) - -/* - * Handles the migration of characters. - * Finds all of your saved characters, loads them into the datum, and saves them onto the SQL. - */ -/datum/preferences/proc/migrate_client_characters(var/client/C) - var/savefile/S = new /savefile(path) - if (!S) - return 1 - - S.cd = "/" - var/list/character_slots = list() - - for (var/a in S.dir) - if (findtext(a, "character")) - var/slot_id = replacetext(a, "character", "") - character_slots.Add(text2num(slot_id)) - - if (!character_slots.len) - return 1 - - for (var/slot in character_slots) - load_character(slot) - -// if (!skills || !skills.len) -// ZeroSkills(1) - - if (!insert_character_sql(C)) - log_debug("Character insert error during migration. Client: [C.ckey], character slot: [slot].") - return 0 - - return 1 - -/* - * Update's the client's migrate status. - */ -/datum/preferences/proc/update_migrate_status(var/client/C) - C.need_saves_migrated = 0 - - establish_db_connection(dbcon) - if (!dbcon.IsConnected()) - return - - var/DBQuery/query = dbcon.NewQuery("UPDATE ss13_player SET migration_status = 0 WHERE ckey = :ckey") - query.Execute(list(":ckey" = C.ckey)) - - var/DBQuery/current_query = dbcon.NewQuery("SELECT id FROM ss13_characters WHERE ckey = :ckey ORDER BY id ASC LIMIT 1") - current_query.Execute(list(":ckey" = C.ckey)) - - if (current_query.NextRow()) - current_character = text2num(current_query.item[1]) - load_character_sql(current_character, C) - - return diff --git a/code/modules/client/preferences_sql.dm b/code/modules/client/preferences_sql.dm deleted file mode 100644 index 273b961b4cd..00000000000 --- a/code/modules/client/preferences_sql.dm +++ /dev/null @@ -1,709 +0,0 @@ -/datum/preferences/proc/load_preferences_sql(var/client/C) - if (!C) - return 0 - - var/DBQuery/query = dbcon.NewQuery({"SELECT - ooccolor, - lastchangelog, - UI_style, - current_character, - toggles, - UI_style_color, - UI_style_alpha, - be_special, - asfx_togs, - motd_hash, - memo_hash - FROM ss13_player_preferences - WHERE ckey = :ckey"}) - query.Execute(list(":ckey" = C.ckey)) - - if (!query.NextRow()) - return insert_preferences_sql(C) - - ooccolor = query.item[1] - lastchangelog = query.item[2] - UI_style = query.item[3] - current_character = text2num(query.item[4]) - toggles = text2num(query.item[5]) - UI_style_color = query.item[6] - UI_style_alpha = text2num(query.item[7]) - be_special = text2num(query.item[8]) - asfx_togs = text2num(query.item[9]) - motd_hash = query.item[10] - memo_hash = query.item[11] - - //Sanitize - ooccolor = sanitize_hexcolor(ooccolor, initial(ooccolor)) - lastchangelog = sanitize_text(lastchangelog, initial(lastchangelog)) - motd_hash = sanitize_text(motd_hash, initial(motd_hash)) - memo_hash = sanitize_text(memo_hash, initial(memo_hash)) - UI_style = sanitize_inlist(UI_style, list("White", "Midnight","Orange","old"), initial(UI_style)) - be_special = sanitize_integer(be_special, 0, 65535, initial(be_special)) - default_slot = sanitize_integer(default_slot, 1, config.character_slots, initial(default_slot)) - toggles = sanitize_integer(toggles, 0, 65535, initial(toggles)) - asfx_togs = sanitize_integer(asfx_togs, 0, 65535, initial(asfx_togs)) - UI_style_color = sanitize_hexcolor(UI_style_color, initial(UI_style_color)) - UI_style_alpha = sanitize_integer(UI_style_alpha, 0, 255, initial(UI_style_alpha)) - - return 1 - -/datum/preferences/proc/save_preferences_sql(var/client/C) - if (!C) - return 0 - - var/DBQuery/initial_query = dbcon.NewQuery("SELECT COUNT(*) AS rowsFound FROM ss13_player_preferences WHERE ckey = :ckey") - initial_query.Execute(list(":ckey" = C.ckey)) - - if (!initial_query.NextRow()) - return insert_preferences_sql(C) - - var/DBQuery/update_query = dbcon.NewQuery({"UPDATE ss13_player_preferences SET - ooccolor = :ooccolor, - lastchangelog = :lastchangelog, - UI_style = :ui_style, - current_character = :current_character, - toggles = :toggles, - UI_style_color = :ui_color, - UI_style_alpha = :ui_alpha, - be_special = :be_special, - asfx_togs = :asfx_togs, - motd_hash = :motd_hash, - memo_hash = :memo_hash - WHERE ckey = :ckey"}) - update_query.Execute(get_prefs_update_insert_params(C)) - - return 1 - -/datum/preferences/proc/insert_preferences_sql(var/client/C) - if (!C) - return 0 - - var/DBQuery/query = dbcon.NewQuery({"INSERT INTO ss13_player_preferences (ckey, ooccolor, lastchangelog, UI_style, current_character, toggles, UI_style_color, UI_style_alpha, be_special, asfx_togs, motd_hash, memo_hash) - VALUES (:ckey, :ooccolor, :lastchangelog, :ui_style, :current_character, :toggles, :ui_color, :ui_alpha, :be_special, :asfx_togs, :motd_hash, :memo_hash);"}) - query.Execute(get_prefs_update_insert_params(C)) - - return 1 - -//Helper function. -/datum/preferences/proc/get_prefs_update_insert_params(var/client/C) - var/params[] = list() - - params[":ckey"] = C.ckey - - params[":ooccolor"] = ooccolor - params[":lastchangelog"] = lastchangelog - params[":ui_style"] = UI_style - params[":current_character"] = current_character - params[":toggles"] = toggles - params[":ui_color"] = UI_style_color - params[":ui_alpha"] = UI_style_alpha - params[":be_special"] = be_special - params[":asfx_togs"] = asfx_togs - params[":motd_hash"] = motd_hash - params[":memo_hash"] = memo_hash - - return params - -/datum/preferences/proc/load_character_sql(slot, var/client/C) - if (!C) - return 0 - - if (!slot || slot == 0) - return new_character_sql(C) - - current_character = slot - - establish_db_connection(dbcon) - if (!dbcon.IsConnected()) - error("Error attempting to connect to MySQL server while loading a character.") - return 0 - - var/DBQuery/initial_query = dbcon.NewQuery("SELECT name FROM ss13_characters WHERE id = :character_id AND ckey = :ckey") - initial_query.Execute(list(":character_id" = current_character, ":ckey" = C.ckey)) - - // In case someone got themselves an invalid character ID. - if (!initial_query.NextRow()) - error("[C.ckey] attempted to load character #[current_character] and failed. No such character found.") - new_character_sql(C) - return 0 - - var/DBQuery/character_query = dbcon.NewQuery({"SELECT - dat.name, - dat.metadata, - dat.random_name, - dat.gender, - dat.age, - dat.species, - dat.language, - dat.spawnpoint, - dat.hair_colour, - dat.facial_colour, - dat.skin_tone, - dat.skin_colour, - dat.hair_style, - dat.facial_style, - dat.eyes_colour, - dat.underwear, - dat.undershirt, - dat.backbag, - dat.b_type, - dat.jobs, - dat.alternate_option, - dat.alternate_titles, - flv.flavour_general, - flv.flavour_head, - flv.flavour_face, - flv.flavour_eyes, - flv.flavour_torso, - flv.flavour_arms, - flv.flavour_hands, - flv.flavour_legs, - flv.flavour_feet, - flv.robot_default, - flv.robot_standard, - flv.robot_engineering, - flv.robot_construction, - flv.robot_medical, - flv.robot_rescue, - flv.robot_miner, - flv.robot_custodial, - flv.robot_service, - flv.robot_clerical, - flv.robot_security, - flv.robot_research, - flv.records_employment, - flv.records_medical, - flv.records_security, - flv.records_exploit, - dat.disabilities, - dat.skills, - dat.skills_specialization, - dat.home_system, - dat.citizenship, - dat.faction, - dat.religion, - dat.nt_relation, - dat.uplink_location, - dat.organs_data, - dat.organs_robotic, - dat.gear, - flv.records_ccia - FROM ss13_characters dat - JOIN ss13_characters_flavour flv ON dat.id = flv.char_id - WHERE dat.id = :char_id"}) - if (!character_query.Execute(list(":char_id" = current_character))) - error("Error loading character #[current_character]. SQL error message: '[character_query.ErrorMsg()]'.") - new_character_sql(C) - return 0 - - if (!character_query.NextRow()) - error("Error loading character #[current_character]. No such character exists.") - new_character_sql(C) - return 0 - - var/DBQuery/ccia_action_query = dbcon.NewQuery({"SELECT - act.title, - act.type, - act.issuedby, - act.details, - act.url, - act.expires_at - FROM ss13_ccia_action_char act_chr - JOIN ss13_characters chr ON act_chr.char_id = chr.id - JOIN ss13_ccia_actions act ON act_chr.action_id = act.id - WHERE - act_chr.char_id = ':char_id' AND - (act.expires_at IS NULL OR act.expires_at >= CURRENT_DATE()) AND - act.deleted_at IS NULL; - "}) - if (!ccia_action_query.Execute(list(":char_id" = current_character))) - error("Error CCIA Actions for character #[current_character]. SQL error message: '[character_query.ErrorMsg()]'.") - - while(ccia_action_query.NextRow()) - var/list/action = list( - ccia_action_query.item[1], - ccia_action_query.item[2], - ccia_action_query.item[3], - ccia_action_query.item[4], - ccia_action_query.item[5], - ccia_action_query.item[6] - ) - ccia_actions.Add(list(action)) - - var/DBQuery/char_id_update = dbcon.NewQuery("UPDATE ss13_player_preferences SET current_character = :char_id WHERE ckey = :ckey") - char_id_update.Execute(list(":char_id" = current_character, ":ckey" = C.ckey)) - - // Character - real_name = character_query.item[1] - metadata = character_query.item[2] - be_random_name = text2num(character_query.item[3]) - gender = character_query.item[4] - age = text2num(character_query.item[5]) - species = character_query.item[6] - language = character_query.item[7] - spawnpoint = character_query.item[8] - - // Other customization data - var/list/hair_rgb = length(character_query.item[9]) == 7 ? GetHexColors(character_query.item[9]) : null - if (hair_rgb) - r_hair = hair_rgb[1] - g_hair = hair_rgb[2] - b_hair = hair_rgb[3] - var/list/facial_rgb = length(character_query.item[10]) == 7 ? GetHexColors(character_query.item[10]) : null - if (facial_rgb) - r_facial = facial_rgb[1] - g_facial = facial_rgb[2] - b_facial = facial_rgb[3] - s_tone = text2num(character_query.item[11]) - var/list/skin_rgb = length(character_query.item[12]) == 7 ? GetHexColors(character_query.item[12]) : null - if (skin_rgb) - r_skin = skin_rgb[1] - g_skin = skin_rgb[2] - b_skin = skin_rgb[3] - h_style = character_query.item[13] - f_style = character_query.item[14] - var/list/eyes_rgb = length(character_query.item[15]) == 7 ? GetHexColors(character_query.item[15]) : null - if (eyes_rgb) - r_eyes = eyes_rgb[1] - g_eyes = eyes_rgb[2] - b_eyes = eyes_rgb[3] - underwear = character_query.item[16] - undershirt = character_query.item[17] - backbag = text2num(character_query.item[18]) - b_type = character_query.item[19] - - var/list/jobs_list = params2list(character_query.item[20]) - - // Job preferences + alt titles and options - if (jobs_list.len == 9) - job_civilian_high = text2num(jobs_list["civ_high"]) - job_civilian_med = text2num(jobs_list["civ_med"]) - job_civilian_low = text2num(jobs_list["civ_low"]) - job_medsci_high = text2num(jobs_list["medsci_high"]) - job_medsci_med = text2num(jobs_list["medsci_med"]) - job_medsci_low = text2num(jobs_list["medsci_low"]) - job_engsec_high = text2num(jobs_list["engsec_high"]) - job_engsec_med = text2num(jobs_list["engsec_med"]) - job_engsec_low = text2num(jobs_list["engsec_low"]) - - alternate_option = text2num(character_query.item[21]) - player_alt_titles = params2list(character_query.item[22]) - - // Flavour texts - flavor_texts["general"] = character_query.item[23] - flavor_texts["head"] = character_query.item[24] - flavor_texts["face"] = character_query.item[25] - flavor_texts["eyes"] = character_query.item[26] - flavor_texts["torso"] = character_query.item[27] - flavor_texts["arms"] = character_query.item[28] - flavor_texts["hands"] = character_query.item[29] - flavor_texts["legs"] = character_query.item[30] - flavor_texts["feet"] = character_query.item[31] - - flavour_texts_robot["Default"] = character_query.item[32] - flavour_texts_robot["Standard"] = character_query.item[33] - flavour_texts_robot["Engineering"] = character_query.item[34] - flavour_texts_robot["Construction"] = character_query.item[35] - flavour_texts_robot["Medical"] = character_query.item[36] - flavour_texts_robot["Rescue"] = character_query.item[37] - flavour_texts_robot["Miner"] = character_query.item[38] - flavour_texts_robot["Custodial"] = character_query.item[39] - flavour_texts_robot["Service"] = character_query.item[40] - flavour_texts_robot["Clerical"] = character_query.item[41] - flavour_texts_robot["Security"] = character_query.item[42] - flavour_texts_robot["Research"] = character_query.item[43] - - // Records - gen_record = character_query.item[44] - med_record = character_query.item[45] - sec_record = character_query.item[46] - exploit_record = character_query.item[47] - ccia_record = character_query.item[60] - - // Miscellaneous - disabilities = text2num(character_query.item[48]) - skills = params2list(character_query.item[49]) - - for (var/skill in skills) - if (skills[skill]) - skills[skill] = text2num(skills[skill]) - - skill_specialization = character_query.item[50] - home_system = character_query.item[51] - citizenship = character_query.item[52] - faction = character_query.item[53] - religion = character_query.item[54] - nanotrasen_relation = character_query.item[55] - - uplinklocation = character_query.item[56] - - organ_data = params2list(character_query.item[57]) - rlimb_data = params2list(character_query.item[58]) - gear = params2list(character_query.item[59]) - - // Sanitization - metadata = sanitize_text(metadata, initial(metadata)) - real_name = sanitizeName(real_name) - - if (isnull(species) || !(species in playable_species)) - species = "Human" - - if (isnum(underwear)) - var/list/undies = gender == MALE ? underwear_m : underwear_f - underwear = undies[undies[underwear]] - - if (isnum(undershirt)) - undershirt = undershirt_t[undershirt_t[undershirt]] - - if (isnull(language)) language = "None" - if (isnull(spawnpoint)) spawnpoint = "Arrivals Shuttle" - if (isnull(nanotrasen_relation)) nanotrasen_relation = initial(nanotrasen_relation) - if (!real_name) real_name = random_name(gender) - be_random_name = sanitize_integer(be_random_name, 0, 1, initial(be_random_name)) - gender = sanitize_gender(gender) - age = sanitize_integer(age, getMinAge(), getMaxAge(), initial(age)) - r_hair = sanitize_integer(r_hair, 0, 255, initial(r_hair)) - g_hair = sanitize_integer(g_hair, 0, 255, initial(g_hair)) - b_hair = sanitize_integer(b_hair, 0, 255, initial(b_hair)) - r_facial = sanitize_integer(r_facial, 0, 255, initial(r_facial)) - g_facial = sanitize_integer(g_facial, 0, 255, initial(g_facial)) - b_facial = sanitize_integer(b_facial, 0, 255, initial(b_facial)) - s_tone = sanitize_integer(s_tone, -185, 34, initial(s_tone)) - r_skin = sanitize_integer(r_skin, 0, 255, initial(r_skin)) - g_skin = sanitize_integer(g_skin, 0, 255, initial(g_skin)) - b_skin = sanitize_integer(b_skin, 0, 255, initial(b_skin)) - h_style = sanitize_inlist(h_style, hair_styles_list, initial(h_style)) - f_style = sanitize_inlist(f_style, facial_hair_styles_list, initial(f_style)) - r_eyes = sanitize_integer(r_eyes, 0, 255, initial(r_eyes)) - g_eyes = sanitize_integer(g_eyes, 0, 255, initial(g_eyes)) - b_eyes = sanitize_integer(b_eyes, 0, 255, initial(b_eyes)) - backbag = sanitize_integer(backbag, 1, backbaglist.len, initial(backbag)) - b_type = sanitize_text(b_type, initial(b_type)) - - alternate_option = sanitize_integer(alternate_option, 0, 2, initial(alternate_option)) - job_civilian_high = sanitize_integer(job_civilian_high, 0, 65535, initial(job_civilian_high)) - job_civilian_med = sanitize_integer(job_civilian_med, 0, 65535, initial(job_civilian_med)) - job_civilian_low = sanitize_integer(job_civilian_low, 0, 65535, initial(job_civilian_low)) - job_medsci_high = sanitize_integer(job_medsci_high, 0, 65535, initial(job_medsci_high)) - job_medsci_med = sanitize_integer(job_medsci_med, 0, 65535, initial(job_medsci_med)) - job_medsci_low = sanitize_integer(job_medsci_low, 0, 65535, initial(job_medsci_low)) - job_engsec_high = sanitize_integer(job_engsec_high, 0, 65535, initial(job_engsec_high)) - job_engsec_med = sanitize_integer(job_engsec_med, 0, 65535, initial(job_engsec_med)) - job_engsec_low = sanitize_integer(job_engsec_low, 0, 65535, initial(job_engsec_low)) - - if (!skills) ZeroSkills(1) - if (skills.len) CalculateSkillPoints() - if (!used_skillpoints) used_skillpoints = 0 - if (isnull(disabilities)) disabilities = 0 - if (!player_alt_titles) player_alt_titles = new() - if (!organ_data) organ_data = list() - if (!rlimb_data) rlimb_data = list() - if (!gear) gear = list() - - if (!home_system) home_system = "Unset" - if (!citizenship) citizenship = "None" - if (!faction) faction = "None" - if (!religion) religion = "None" - - load_character_contest(slot) - - return 1 - -/datum/preferences/proc/save_character_sql(var/client/C) - if (!C) - return 0 - - if (!current_character) - return insert_character_sql(C) - - establish_db_connection(dbcon) - if (!dbcon.IsConnected()) - error("Error attempting to connect to MySQL server while saving a character.") - return 0 - - var/DBQuery/initial_query = dbcon.NewQuery("SELECT COUNT(*) AS rowCount FROM ss13_characters WHERE id = :char_id") - initial_query.Execute(list(":char_id" = current_character)) - - if (!initial_query.NextRow()) - current_character = 0 - return insert_character_sql(C) - - var/DBQuery/update_query = dbcon.NewQuery({"UPDATE ss13_characters dat - JOIN ss13_characters_flavour flv ON dat.id = flv.char_id - SET - dat.name = :real_name, - dat.metadata = :metadata, - dat.random_name = :is_random, - dat.gender = :gender, - dat.age = :age, - dat.species = :species, - dat.language = :language, - dat.spawnpoint = :spawnpoint, - dat.hair_colour = :hair_colour, - dat.facial_colour = :facial_colour, - dat.skin_tone = :skin_tone, - dat.skin_colour = :skin_colour, - dat.hair_style = :hair_style, - dat.facial_style = :facial_style, - dat.eyes_colour = :eyes_colour, - dat.underwear = :underwear, - dat.undershirt = :undershirt, - dat.backbag = :backbag, - dat.b_type = :b_type, - dat.jobs = :jobs, - dat.alternate_option = :alternate_option, - dat.alternate_titles = :alternate_titles, - dat.disabilities = :disabilities, - dat.skills = :skills, - dat.skills_specialization = :specialization, - dat.home_system = :home_system, - dat.citizenship = :citizenship, - dat.faction = :faction, - dat.religion = :religion, - dat.nt_relation = :nt_relation, - dat.uplink_location = :uplink_loc, - dat.organs_data = :organ_data, - dat.organs_robotic = :organs_robotic, - dat.gear = :gear, - flv.flavour_general = :flv_general, - flv.flavour_head = :flv_head, - flv.flavour_face = :flv_face, - flv.flavour_eyes = :flv_eyes, - flv.flavour_torso = :flv_torso, - flv.flavour_arms = :flv_arms, - flv.flavour_hands = :flv_hands, - flv.flavour_legs = :flv_legs, - flv.flavour_feet = :flv_feet, - flv.robot_default = :robot_default, - flv.robot_standard = :robot_standard, - flv.robot_engineering = :robot_engineering, - flv.robot_construction = :robot_construction, - flv.robot_medical = :robot_medical, - flv.robot_rescue = :robot_rescue, - flv.robot_miner = :robot_miner, - flv.robot_custodial = :robot_custodial, - flv.robot_service = :robot_service, - flv.robot_clerical = :robot_clerical, - flv.robot_security = :robot_security, - flv.robot_research = :robot_research, - flv.records_medical = :med_rec, - flv.records_security = :sec_rec, - flv.records_employment = :gen_rec, - flv.records_exploit = :exploit_record - WHERE dat.id = :char_id"}) - - update_query.Execute(get_update_insert_params()) - - if (update_query.ErrorMsg()) - error("Error updating character #[current_character]: [update_query.ErrorMsg()]") - return 0 - - return 1 - -/datum/preferences/proc/insert_character_sql(var/client/C) - if (!C) - return 0 - - establish_db_connection(dbcon) - if (!dbcon.IsConnected()) - error("Error attempting to connect to MySQL server while inserting a character.") - return 0 - - var/params[] = get_update_insert_params(0, C) - - var/DBQuery/insert_query = dbcon.NewQuery({"INSERT INTO ss13_characters (id, ckey, name, metadata, random_name, gender, age, species, language, hair_colour, facial_colour, skin_tone, skin_colour, hair_style, facial_style, eyes_colour, underwear, undershirt, backbag, b_type, spawnpoint, jobs, alternate_option, alternate_titles, disabilities, skills, skills_specialization, home_system, citizenship, faction, religion, nt_relation, uplink_location, organs_data, organs_robotic, gear) - VALUES (NULL, :ckey, :real_name, :metadata, :is_random, :gender, :age, :species, :language, :hair_colour, :facial_colour, :skin_tone, :skin_colour, :hair_style, :facial_style, :eyes_colour, :underwear, :undershirt, :backbag, :b_type, :spawnpoint, :jobs, :alternate_option, :alternate_titles, :disabilities, :skills, :specialization, :home_system, :citizenship, :faction, :religion, :nt_relation, :uplink_loc, :organ_data, :organs_robotic, :gear);"}) - insert_query.Execute(params, 1) - - if (insert_query.ErrorMsg()) - return 0 - - var/DBQuery/get_query = dbcon.NewQuery("SELECT id FROM ss13_characters WHERE ckey = :ckey AND name = :real_name ORDER BY id DESC LIMIT 1;") - get_query.Execute(list(":ckey" = C.ckey, ":real_name" = real_name)) - - if (get_query.NextRow()) - current_character = text2num(get_query.item[1]) - params[":char_id"] = current_character - - insert_query = dbcon.NewQuery({"INSERT INTO ss13_characters_flavour (char_id, records_employment, records_medical, records_security, records_exploit, flavour_general, flavour_head, flavour_face, flavour_eyes, flavour_torso, flavour_arms, flavour_hands, flavour_legs, flavour_feet, robot_default, robot_standard, robot_engineering, robot_construction, robot_medical, robot_rescue, robot_miner, robot_custodial, robot_service, robot_clerical, robot_security, robot_research) - VALUES (:char_id, :gen_rec, :med_rec, :sec_rec, :exploit_record, :flv_general, :flv_head, :flv_face, :flv_eyes, :flv_torso, :flv_arms, :flv_hands, :flv_legs, :flv_feet, :robot_default, :robot_standard, :robot_engineering, :robot_construction, :robot_medical, :robot_rescue, :robot_miner, :robot_custodial, :robot_service, :robot_clerical, :robot_security, :robot_research);"}) - insert_query.Execute(params, 1) - - if (insert_query.ErrorMsg()) - return 0 - - return 1 - -// Helper function. -// Returns an array to be used in save_character_sql and insert_character_sql. -/datum/preferences/proc/get_update_insert_params(var/include_id = 1, var/client/C = null) - var/hair_hex = "#" + num2hex(r_hair) + num2hex(g_hair) + num2hex(b_hair) - var/facial_hex = "#" + num2hex(r_facial) + num2hex(g_facial) + num2hex(b_facial) - var/eyes_hex = "#" + num2hex(r_eyes) + num2hex(g_eyes) + num2hex(b_eyes) - var/skin_hex = "#" + num2hex(r_skin) + num2hex(g_skin) + num2hex(b_skin) - - var/language_string = "None" - if (!istext(language)) - if (istype(language, /datum/language)) - for (var/L in all_languages) - if (language == all_languages[L]) - language_string = L - break - else - language_string = language - - var/jobs_list[] = list() - jobs_list["civ_high"] = job_civilian_high - jobs_list["civ_med"] = job_civilian_med - jobs_list["civ_low"] = job_civilian_low - jobs_list["medsci_high"] = job_medsci_high - jobs_list["medsci_med"] = job_medsci_med - jobs_list["medsci_low"] = job_medsci_low - jobs_list["engsec_high"] = job_engsec_high - jobs_list["engsec_med"] = job_engsec_med - jobs_list["engsec_low"] = job_engsec_low - - var/params[] = list() - - params[":real_name"] = real_name - params[":metadata"] = metadata - params[":is_random"] = be_random_name - params[":gender"] = gender - params[":age"] = age - params[":species"] = species - params[":language"] = language_string - params[":spawnpoint"] = spawnpoint - params[":hair_colour"] = hair_hex - params[":facial_colour"] = facial_hex - params[":skin_tone"] = s_tone - params[":skin_colour"] = skin_hex - - if (istext(h_style)) - params[":hair_style"] = h_style - else if (istype(h_style, /datum/sprite_accessory/hair)) - var/datum/sprite_accessory/hair/current_h = h_style - params[":hair_style"] = current_h.name - else - params[":hair_style"] = "Bald" - - if (istext(f_style)) - params[":facial_style"] = f_style - else if (istype(h_style, /datum/sprite_accessory/facial_hair)) - var/datum/sprite_accessory/facial_hair/current_f = f_style - params[":facial_style"] = current_f.name - else - params[":facial_style"] = "Shaved" - - params[":eyes_colour"] = eyes_hex - params[":underwear"] = underwear - params[":undershirt"] = undershirt - params[":backbag"] = backbag - params[":b_type"] = b_type - - params[":alternate_option"] = alternate_option - params[":jobs"] = list2params(jobs_list) - params[":alternate_titles"] = list2params(player_alt_titles) - - params[":flv_general"] = flavor_texts["general"] - params[":flv_head"] = flavor_texts["head"] - params[":flv_face"] = flavor_texts["face"] - params[":flv_eyes"] = flavor_texts["eyes"] - params[":flv_torso"] = flavor_texts["torso"] - params[":flv_arms"] = flavor_texts["arms"] - params[":flv_hands"] = flavor_texts["hands"] - params[":flv_legs"] = flavor_texts["legs"] - params[":flv_feet"] = flavor_texts["feet"] - params[":robot_default"] = flavour_texts_robot["Default"] - params[":robot_standard"] = flavour_texts_robot["Standard"] - params[":robot_engineering"] = flavour_texts_robot["Engineering"] - params[":robot_construction"] = flavour_texts_robot["Construction"] - params[":robot_medical"] = flavour_texts_robot["Medical"] - params[":robot_rescue"] = flavour_texts_robot["Rescue"] - params[":robot_miner"] = flavour_texts_robot["Miner"] - params[":robot_custodial"] = flavour_texts_robot["Custodial"] - params[":robot_service"] = flavour_texts_robot["Service"] - params[":robot_clerical"] = flavour_texts_robot["Clerical"] - params[":robot_security"] = flavour_texts_robot["Security"] - params[":robot_research"] = flavour_texts_robot["Research"] - - params[":med_rec"] = med_record - params[":sec_rec"] = sec_record - params[":gen_rec"] = gen_record - params[":disabilities"] = disabilities - params[":skills"] = list2params(skills) - params[":specialization"] = skill_specialization - params[":home_system"] = home_system - params[":citizenship"] = citizenship - params[":faction"] = faction - params[":religion"] = religion - params[":nt_relation"] = nanotrasen_relation - params[":uplink_loc"] = uplinklocation - params[":exploit_record"] = exploit_record - params[":organ_data"] = list2params(organ_data) - params[":organs_robotic"] = list2params(rlimb_data) - params[":gear"] = list2params(gear) - - if (include_id) - params[":char_id"] = current_character - - if (C) - params[":ckey"] = C.ckey - - return params - -/datum/preferences/proc/new_character_sql(var/client/C) - if (!C) - return 0 - - current_character = 0 - - var/DBQuery/char_id_update = dbcon.NewQuery("UPDATE ss13_player_preferences SET current_character = '0' WHERE ckey = :ckey") - char_id_update.Execute(list(":ckey" = C.ckey)) - - species = "Human" - language = "None" - spawnpoint = "Arrivals Shuttle" - nanotrasen_relation = initial(nanotrasen_relation) - - gender = pick(MALE, FEMALE) - real_name = random_name(gender, species) - gear = list() - - b_type = pick(4;"O-", 36;"O+", 3;"A-", 28;"A+", 1;"B-", 20;"B+", 1;"AB-", 5;"AB+") - return 1 - -/datum/preferences/proc/delete_character_sql(var/client/C) - if (!C) - return 0 - - if (!current_character) - return 0 - - if (alert(C.mob, "This will permanently delete the character. Are you sure you wish to do this?", "Delete Character" ,"Yes", "No") == "No") - return 0 - - establish_db_connection(dbcon) - if (!dbcon.IsConnected()) - error("Error attempting to connect to MySQL server while deleting a character.") - return 0 - - var/DBQuery/query = dbcon.NewQuery("SELECT COUNT(*) AS rowCount FROM ss13_characters WHERE id = :char_id AND ckey = :ckey") - query.Execute(list(":char_id" = current_character, ":ckey" = C.ckey)) - - if (!query.NextRow()) - return 0 - - var/DBQuery/delete_query = dbcon.NewQuery("UPDATE ss13_characters SET deleted_at = NOW() WHERE id = :id") - delete_query.Execute(list(":id" = current_character)) - - var/DBQuery/select_query = dbcon.NewQuery("SELECT id FROM ss13_characters WHERE ckey = :ckey AND deleted_at IS NULL ORDER BY id ASC LIMIT 1") - select_query.Execute(list(":ckey" = C.ckey)) - - if (select_query.NextRow()) - load_character_sql(text2num(select_query.item[1]), C) - else - new_character_sql(C) - - C << "Your character has been deleted from the database." - return 1 From 1bf9e6a389e7fef123cea0041820c6f7b97256c1 Mon Sep 17 00:00:00 2001 From: skull132 Date: Thu, 15 Dec 2016 14:22:08 +0200 Subject: [PATCH 10/12] Fix all the things YAAAAY! --- .../preference_setup/general/01_basic.dm | 14 +++++----- .../preference_setup/general/02_language.dm | 4 +-- .../preference_setup/general/03_body.dm | 26 +++++++++---------- .../preference_setup/general/06_flavor.dm | 2 +- .../preference_setup/preference_setup.dm | 4 +-- .../preference_setup/preferences_sql.dm | 22 ++++++++++------ 6 files changed, 38 insertions(+), 34 deletions(-) diff --git a/code/modules/client/preference_setup/general/01_basic.dm b/code/modules/client/preference_setup/general/01_basic.dm index 651c7ad331b..b6d89103870 100644 --- a/code/modules/client/preference_setup/general/01_basic.dm +++ b/code/modules/client/preference_setup/general/01_basic.dm @@ -41,13 +41,13 @@ "id" = 1)) /datum/category_item/player_setup_item/general/basic/gather_save_parameters() - return list("ss13_characters" = list(":name" = pref.real_name, - ":random_name" = pref.be_random_name, - ":gender" = pref.gender, - ":age" = pref.age, - ":metadata" = pref.metadata, - ":spawnpoint" = pref.spawnpoint, - ":id" = pref.current_character)) + return list(":name" = pref.real_name, + ":random_name" = pref.be_random_name, + ":gender" = pref.gender, + ":age" = pref.age, + ":metadata" = pref.metadata, + ":spawnpoint" = pref.spawnpoint, + ":id" = pref.current_character) /datum/category_item/player_setup_item/general/basic/sanitize_character() pref.age = sanitize_integer(text2num(pref.age), AGE_MIN, AGE_MAX, initial(pref.age)) diff --git a/code/modules/client/preference_setup/general/02_language.dm b/code/modules/client/preference_setup/general/02_language.dm index 61857ffbf48..14e1ae99233 100644 --- a/code/modules/client/preference_setup/general/02_language.dm +++ b/code/modules/client/preference_setup/general/02_language.dm @@ -19,8 +19,8 @@ "id" = 1)) /datum/category_item/player_setup_item/general/language/gather_save_parameters() - return list("ss13_characters" = list(":language" = list2params(pref.alternate_languages), - ":id" = pref.current_character)) + return list(":language" = list2params(pref.alternate_languages), + ":id" = pref.current_character) /datum/category_item/player_setup_item/general/language/sanitize_character(var/sql_load = 0) if (sql_load) diff --git a/code/modules/client/preference_setup/general/03_body.dm b/code/modules/client/preference_setup/general/03_body.dm index eb68f418c98..75f04ed3fa7 100644 --- a/code/modules/client/preference_setup/general/03_body.dm +++ b/code/modules/client/preference_setup/general/03_body.dm @@ -82,19 +82,19 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O "id" = 1)) /datum/category_item/player_setup_item/general/body/gather_save_parameters() - return list("ss13_characters" = list(":species" = pref.species, - ":hair_colour" = "#" + num2hex(pref.r_hair) + num2hex(pref.g_hair) + num2hex(pref.b_hair), - ":facial_colour" = "#" + num2hex(pref.r_facial) + num2hex(pref.g_facial) + num2hex(pref.b_facial), - ":skin_tone" = pref.s_tone, - ":skin_colour" = "#" + num2hex(pref.r_skin) + num2hex(pref.g_skin) + num2hex(pref.b_skin), - ":hair_style" = pref.h_style, - ":facial_style" = pref.f_style, - ":eyes_colour" = "#" + num2hex(pref.r_eyes) + num2hex(pref.g_eyes) + num2hex(pref.b_eyes), - ":b_type" = pref.b_type, - ":disabilities" = pref.disabilities, - ":organs_data" = list2params(pref.organ_data), - ":organs_robotic"= list2params(pref.rlimb_data), - ":id" = pref.current_character)) + return list(":species" = pref.species, + ":hair_colour" = "#" + num2hex(pref.r_hair) + num2hex(pref.g_hair) + num2hex(pref.b_hair), + ":facial_colour" = "#" + num2hex(pref.r_facial) + num2hex(pref.g_facial) + num2hex(pref.b_facial), + ":skin_tone" = pref.s_tone, + ":skin_colour" = "#" + num2hex(pref.r_skin) + num2hex(pref.g_skin) + num2hex(pref.b_skin), + ":hair_style" = pref.h_style, + ":facial_style" = pref.f_style, + ":eyes_colour" = "#" + num2hex(pref.r_eyes) + num2hex(pref.g_eyes) + num2hex(pref.b_eyes), + ":b_type" = pref.b_type, + ":disabilities" = pref.disabilities, + ":organs_data" = list2params(pref.organ_data), + ":organs_robotic"= list2params(pref.rlimb_data), + ":id" = pref.current_character) /datum/category_item/player_setup_item/general/body/sanitize_character(var/sql_load = 0) if(!pref.species || !(pref.species in playable_species)) diff --git a/code/modules/client/preference_setup/general/06_flavor.dm b/code/modules/client/preference_setup/general/06_flavor.dm index f4cbdbe1b43..020f5ec8278 100644 --- a/code/modules/client/preference_setup/general/06_flavor.dm +++ b/code/modules/client/preference_setup/general/06_flavor.dm @@ -86,7 +86,7 @@ for (var/module in robot_module_types) var_list[":robot_[module]"] += pref.flavour_texts_robot[module] - return list("ss13_characters_flavour" = var_list) + return var_list /datum/category_item/player_setup_item/general/flavor/sanitize_character() return diff --git a/code/modules/client/preference_setup/preference_setup.dm b/code/modules/client/preference_setup/preference_setup.dm index 6d80bb3f315..b4a1a008346 100644 --- a/code/modules/client/preference_setup/preference_setup.dm +++ b/code/modules/client/preference_setup/preference_setup.dm @@ -164,9 +164,7 @@ for (var/datum/category_item/player_setup_item/PI in items) PI.save_preferences(S) else - if (modified && sql_role == SQL_PREFERENCES) - handle_sql_saving(SQL_PREFERENCES) - modified = 0 + handle_sql_saving(SQL_PREFERENCES) /datum/category_group/player_setup_category/proc/update_setup(var/savefile/preferences, var/savefile/character) for(var/datum/category_item/player_setup_item/PI in items) diff --git a/code/modules/client/preference_setup/preferences_sql.dm b/code/modules/client/preference_setup/preferences_sql.dm index 369dbcbac21..590b06f9c85 100644 --- a/code/modules/client/preference_setup/preferences_sql.dm +++ b/code/modules/client/preference_setup/preferences_sql.dm @@ -170,6 +170,9 @@ // Actually utilize the queries. var/list/arg_list = gather_save_parameters() + + // Typecast the collection so we can access its preferences var. + var/datum/category_collection/player_setup_collection/cc = collection for (var/query_text in query_cache[type]) var/DBQuery/query = dbcon.NewQuery(query_text) query.Execute(arg_list, 1) @@ -177,17 +180,20 @@ if (query.ErrorMsg()) error("Error saving character to SQL: [query.ErrorMsg()]") - // Get the character ID if we inserted a new character. - if (role_type == SQL_CHARACTER) - // Need to typecast due to reasons. - var/datum/category_collection/player_setup_collection/cc = collection - - if (!cc.preferences.current_character) - var/DBQuery/query = dbcon.NewQuery("SELECT id FROM ss13_characters WHERE ckey = :ckey ORDER BY id DESC LIMIT 1") - query.Execute(list(":ckey" = cc.preferences.client.ckey)) + if (role_type == SQL_CHARACTER && !cc.preferences.current_character) + // No current character, means we're doing insert queries. + // Quickly nab the new ID and substitute it within the args. + query = dbcon.NewQuery("SELECT LAST_INSERT_ID() AS new_id") + query.Execute() if (query.NextRow()) + arg_list[":id"] = text2num(query.item[1]) + arg_list[":char_id"] = text2num(query.item[1]) cc.preferences.current_character = text2num(query.item[1]) + else + error("Error inserting character to SQL: New ID was not recovered.") + if (query.ErrorMsg()) + error("Error retreiving new character ID: [query.ErrorMsg()]") /datum/category_group/player_setup_category/proc/gather_save_parameters() var/list/arg_list = list() From 0b26cd9ab14d57b7f5c58b856633a44c080381c1 Mon Sep 17 00:00:00 2001 From: skull132 Date: Sun, 18 Dec 2016 00:42:49 +0200 Subject: [PATCH 11/12] Fix more things --- code/modules/client/preferences.dm | 6 +- code/modules/client/preferences_savefile.dm | 98 ++++++++++++++------- 2 files changed, 70 insertions(+), 34 deletions(-) diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 22467520d60..11857790540 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -419,14 +419,14 @@ datum/preferences id = text2num(query.item[1]) name = query.item[2] if (id == current_character) - dat += "[name]
" + dat += "[name]
" else - dat += "[name]
" + dat += "[name]
" dat += "
" dat += "[query.RowCount()]/[config.character_slots] slots used
" if (query.RowCount() < config.character_slots) - dat += "New Character" + dat += "New Character" else dat += "New Character" diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index d5fca7eb527..5050bd9dabd 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -7,54 +7,90 @@ savefile_version = SAVEFILE_VERSION_MAX /datum/preferences/proc/load_preferences() - if(!path) return 0 - if(!fexists(path)) return 0 - var/savefile/S = new /savefile(path) - if(!S) return 0 - S.cd = "/" + var/savefile/S + if (!config.sql_saves) + if (!path) + return 0 + if (!fexists(path)) + return 0 + S = new /savefile(path) + if (!S) + return 0 + S.cd = "/" + + S["version"] >> savefile_version - S["version"] >> savefile_version player_setup.load_preferences(S) - loaded_preferences = S + + if (!config.sql_saves) + loaded_preferences = S + return 1 /datum/preferences/proc/save_preferences() - if(!path) return 0 - var/savefile/S = new /savefile(path) - if(!S) return 0 - S.cd = "/" + var/savefile/S + if (!config.sql_saves) + if(!path) + return 0 + S = new /savefile(path) + if(!S) + return 0 + S.cd = "/" + + S["version"] << SAVEFILE_VERSION_MAX - S["version"] << SAVEFILE_VERSION_MAX player_setup.save_preferences(S) - loaded_preferences = S + + if (!config.sql_saves) + loaded_preferences = S + return 1 /datum/preferences/proc/load_character(slot) - if(!path) return 0 - if(!fexists(path)) return 0 - var/savefile/S = new /savefile(path) - if(!S) return 0 - S.cd = "/" - if(!slot) slot = default_slot - slot = sanitize_integer(slot, 1, config.character_slots, initial(default_slot)) - if(slot != default_slot) - default_slot = slot - S["default_slot"] << slot - S.cd = "/character[slot]" + var/savefile/S + if (!config.sql_saves) + if (!path) + return 0 + if (!fexists(path)) + return 0 + S = new /savefile(path) + if (!S) + return 0 + S.cd = "/" + if (!slot) + slot = default_slot + slot = sanitize_integer(slot, 1, config.character_slots, initial(default_slot)) + if(slot != default_slot) + default_slot = slot + S["default_slot"] << slot + S.cd = "/character[slot]" + else + current_character = slot player_setup.load_character(S) - loaded_character = S + + if (!config.sql_saves) + loaded_character = S + return 1 /datum/preferences/proc/save_character() - if(!path) return 0 - var/savefile/S = new /savefile(path) - if(!S) return 0 - S.cd = "/character[default_slot]" + var/savefile/S + if (!config.sql_saves) + if(!path) + return 0 + S = new /savefile(path) + if(!S) + return 0 + S.cd = "/character[default_slot]" + + S["version"] << SAVEFILE_VERSION_MAX - S["version"] << SAVEFILE_VERSION_MAX player_setup.save_character(S) - loaded_character = S + + if (!config.sql_saves) + loaded_character = S + return S /datum/preferences/proc/sanitize_preferences() From 9c7829a3d5fab718dd7d7d730a8681b4a736f347 Mon Sep 17 00:00:00 2001 From: skull132 Date: Sun, 18 Dec 2016 00:43:39 +0200 Subject: [PATCH 12/12] Map clearance (#28) Holy fuck whhhhyyy. Thank you, Fowl, btw. You did good. --- icons/turf/flooring/tiles.dmi | Bin 20874 -> 59354 bytes maps/exodus-1.dmm | 10092 ++++++++++++++++---------------- maps/exodus-2.dmm | 1843 +++--- maps/exodus-3.dmm | 662 ++- maps/exodus-5.dmm | 2279 ++++---- 5 files changed, 7358 insertions(+), 7518 deletions(-) diff --git a/icons/turf/flooring/tiles.dmi b/icons/turf/flooring/tiles.dmi index 8c4367b9799103e57dd2e35c1e4d755cc9309eb5..7d160b70708c02701964159f47244532d4d9f795 100644 GIT binary patch literal 59354 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

;PgnqWbwTDV@7}S32Rr_YL+GwEGtuGL$ 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%0AJA2b0#cxy6nVl&1VUFa-4eZsBDlZr{7 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 Ef^L2c(*OVf literal 20874 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