diff --git a/.editorconfig b/.editorconfig index 72e85029ef6..58fd1cf290f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,6 +15,9 @@ indent_size = 2 [*.py] indent_style = space +[*.sql] +indent_style = space + [*.md] trim_trailing_whitespace = false diff --git a/.github/workflows/byond.yml b/.github/workflows/byond.yml index 33d0948b55c..5eccfc12957 100644 --- a/.github/workflows/byond.yml +++ b/.github/workflows/byond.yml @@ -317,7 +317,7 @@ jobs: services: mariadb: - image: mariadb:10.4 + image: mariadb:10.11 ports: - 3306 env: @@ -440,7 +440,7 @@ jobs: services: mariadb: - image: mariadb:10.4 + image: mariadb:10.11 ports: - 3306 env: @@ -565,7 +565,7 @@ jobs: services: mariadb: - image: mariadb:10.4 + image: mariadb:10.11 ports: - 3306 env: @@ -689,7 +689,7 @@ jobs: services: mariadb: - image: mariadb:10.4 + image: mariadb:10.11 ports: - 3306 env: diff --git a/SQL/migrate-2023/V006__loadout_overhaul.sql b/SQL/migrate-2023/V006__loadout_overhaul.sql new file mode 100644 index 00000000000..eec52a8429d --- /dev/null +++ b/SQL/migrate-2023/V006__loadout_overhaul.sql @@ -0,0 +1,179 @@ +-- +-- Overhauls the way the loadout is stored +-- +-- -------------------------------------------------------- +-- Server version: 10.11.5 +-- Server OS: debian-linux-gnu +-- HeidiSQL Version: 12.3.0.6589 +-- -------------------------------------------------------- + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET NAMES utf8 */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- Dumping structure for procedure convert_characters_gear +DELIMITER // +CREATE PROCEDURE `convert_characters_gear`( + IN `parameter_char_id` INT +) + MODIFIES SQL DATA + SQL SECURITY INVOKER + COMMENT 'Converts the character gear from the V1 (only one gear set) and V2 (multiple gear sets) storage formats to the ss13_characters_gear table' +BEGIN + DECLARE info_gear_version VARCHAR(50) DEFAULT NULL; + DECLARE value_fetch_geardata MEDIUMTEXT DEFAULT NULL; + + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + END; + + SELECT gear INTO value_fetch_geardata FROM ss13_characters WHERE id = parameter_char_id; + + IF JSON_VALID(value_fetch_geardata) THEN + SET info_gear_version = "valid json"; + + IF JSON_CONTAINS_PATH(value_fetch_geardata, 'one', '$.1','$.2','$.3') THEN + SET info_gear_version = "version 2"; + + START TRANSACTION; + DELETE FROM ss13_characters_gear WHERE char_id = parameter_char_id; + + IF JSON_CONTAINS_PATH(value_fetch_geardata, 'one', '$.1') THEN + CALL `convert_characters_gear_step2`(parameter_char_id, 1, JSON_EXTRACT(value_fetch_geardata, '$.1')); + END IF; + + IF JSON_CONTAINS_PATH(value_fetch_geardata, 'one', '$.2') THEN + CALL `convert_characters_gear_step2`(parameter_char_id, 2, JSON_EXTRACT(value_fetch_geardata, '$.2')); + END IF; + + IF JSON_CONTAINS_PATH(value_fetch_geardata, 'one', '$.3') THEN + CALL `convert_characters_gear_step2`(parameter_char_id, 3, JSON_EXTRACT(value_fetch_geardata, '$.3')); + END IF; + COMMIT; + + ELSE + SET info_gear_version = "version 1"; + START TRANSACTION; + DELETE FROM ss13_characters_gear WHERE char_id = parameter_char_id; + CALL `convert_characters_gear_step2`(parameter_char_id, 1, value_fetch_geardata); + COMMIT; + END IF; + + ELSE + SET info_gear_version = "invalid"; + END IF; + +END// +DELIMITER ; + +-- Dumping structure for procedure convert_characters_gear_all +DELIMITER // +CREATE PROCEDURE `convert_characters_gear_all`() + MODIFIES SQL DATA + SQL SECURITY INVOKER + COMMENT 'Runs convert_characters_gear for all characters' +BEGIN + DECLARE done BOOLEAN DEFAULT FALSE; + DECLARE _id BIGINT UNSIGNED; + DECLARE cur CURSOR FOR SELECT id FROM ss13_characters; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET done := TRUE; + + OPEN cur; + + charLoop: LOOP + FETCH cur INTO _id; + IF done THEN + LEAVE charLoop; + END IF; + CALL convert_characters_gear(_id); + END LOOP charLoop; + + CLOSE cur; +END// +DELIMITER ; + +-- Dumping structure for procedure convert_characters_gear_step2 +DELIMITER // +CREATE PROCEDURE `convert_characters_gear_step2`( + IN `parameter_char_id` INT, + IN `parameter_gear_slot_id` INT, + IN `parameter_gear_string` MEDIUMTEXT +) + MODIFIES SQL DATA + SQL SECURITY INVOKER + COMMENT 'Converts a characters gear to the new (v3) storage system. Requires a char id, the current gear_slot and and the gear string to convert' +BEGIN + INSERT INTO ss13_characters_gear (char_id, slot, NAME, tweaks) + SELECT + parameter_char_id AS char_id, + parameter_gear_slot_id AS gear_slot, + JSON_UNQUOTE(JSON_EXTRACT(JSON_KEYS(parameter_gear_string), CONCAT('$[', geardata.rowid-1, ']'))) AS gearname, + IF(STRCMP(geardata.geartweaks,'null')=0,NULL,geardata.geartweaks) + FROM JSON_TABLE(parameter_gear_string, '$.*' COLUMNS ( + rowid FOR ORDINALITY, + geartweaks JSON PATH '$' + )) geardata; +END// +DELIMITER ; + +-- Dumping structure for table ss13_characters_gear +CREATE TABLE IF NOT EXISTS `ss13_characters_gear` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `char_id` int(11) NOT NULL, + `slot` tinyint(4) NOT NULL, + `name` varchar(255) NOT NULL, + `tweaks` mediumtext DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + KEY `char_id_gear_slot` (`char_id`,`slot`) USING BTREE, + CONSTRAINT `fk_ss13_characters_char_id` FOREIGN KEY (`char_id`) REFERENCES `ss13_characters` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `json_valid_tweaks` CHECK (json_valid(`tweaks`)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- Data exporting was unselected. + +-- Dumping structure for procedure update_character_gear +DELIMITER // +CREATE PROCEDURE `update_character_gear`( + IN `parameter_char_id` INT, + IN `parameter_gear_slot_id` INT, + IN `parameter_gear_string` MEDIUMTEXT +) + MODIFIES SQL DATA + SQL SECURITY INVOKER + COMMENT 'Updates a specific gear slot of a character to the supplied gear string' +BEGIN + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + END; + + START TRANSACTION; + + DELETE FROM ss13_characters_gear WHERE char_id = parameter_char_id AND slot = parameter_gear_slot_id; + + INSERT INTO ss13_characters_gear (char_id, slot, NAME, tweaks) + SELECT + parameter_char_id AS char_id, + parameter_gear_slot_id AS gear_slot, + JSON_UNQUOTE(JSON_EXTRACT(JSON_KEYS(parameter_gear_string), CONCAT('$[', geardata.rowid-1, ']'))) AS gearname, + IF(STRCMP(geardata.geartweaks,'null')=0,NULL,geardata.geartweaks) + FROM JSON_TABLE(parameter_gear_string, '$.*' COLUMNS ( + rowid FOR ORDINALITY, + geartweaks JSON PATH '$' + )) geardata; + + COMMIT; +END// +DELIMITER ; + +/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */; +/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */; +/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */; diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index f775e7cdd28..0e715b6356c 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -39,8 +39,6 @@ // Some arbitrary defines to be used by self-pruning global lists. (see master_controller) #define PROCESS_KILL 26 // Used to trigger removal from a processing list. -#define MAX_GEAR_COST 15 // Used in chargen for accessory loadout limit. - // Preference toggles. #define SOUND_ADMINHELP 0x1 #define SOUND_MIDI 0x2 diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index 22653db3b58..5e4fcf94f79 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -225,6 +225,7 @@ GLOBAL_LIST_EMPTY(gamemode_cache) var/character_slots = 10 // The number of available character slots var/loadout_slots = 3 // The number of loadout slots per character + var/loadout_cost = 15 // The maximum cost of the loadout per slot var/max_maint_drones = 5 //This many drones can spawn, var/allow_drone_spawn = 1 //assuming the admin allow them to. @@ -843,6 +844,9 @@ GENERAL_PROTECT_DATUM(/datum/configuration) if("loadout_slots") GLOB.config.loadout_slots = text2num(value) + if("loadout_cost") + GLOB.config.loadout_cost = text2num(value) + if("allow_drone_spawn") GLOB.config.allow_drone_spawn = text2num(value) diff --git a/code/modules/client/preference_setup/general/01_basic.dm b/code/modules/client/preference_setup/general/01_basic.dm index ae0fdd71029..71bb02ccb6a 100644 --- a/code/modules/client/preference_setup/general/01_basic.dm +++ b/code/modules/client/preference_setup/general/01_basic.dm @@ -115,7 +115,7 @@ "ckey" = PREF_CLIENT_CKEY ) -/datum/category_item/player_setup_item/general/basic/load_special() +/datum/category_item/player_setup_item/general/basic/load_character_special() pref.can_edit_name = TRUE pref.can_edit_ipc_tag = TRUE @@ -134,7 +134,7 @@ if(GLOB.config.ipc_timelock_active) pref.can_edit_ipc_tag = FALSE else - log_world("ERROR: SQL CHARACTER LOAD: Logic error, general/basic/load_special() didn't return any rows when it should have. Character ID: [pref.current_character].") + log_world("ERROR: SQL CHARACTER LOAD: Logic error, general/basic/load_character_special() didn't return any rows when it should have. Character ID: [pref.current_character].") /datum/category_item/player_setup_item/general/basic/sanitize_character() if(!pref.species) diff --git a/code/modules/client/preference_setup/general/07_psionics.dm b/code/modules/client/preference_setup/general/07_psionics.dm index ad207faabf7..12f9df76aa1 100644 --- a/code/modules/client/preference_setup/general/07_psionics.dm +++ b/code/modules/client/preference_setup/general/07_psionics.dm @@ -48,7 +48,7 @@ "ckey" = PREF_CLIENT_CKEY ) -/datum/category_item/player_setup_item/general/psionics/load_special(savefile/S) +/datum/category_item/player_setup_item/general/psionics/load_character_special(savefile/S) if(!pref.psionics) pref.psionics = "{}" diff --git a/code/modules/client/preference_setup/loadout/loadout.dm b/code/modules/client/preference_setup/loadout/loadout.dm index 7d59dbb7c35..4708680ad02 100644 --- a/code/modules/client/preference_setup/loadout/loadout.dm +++ b/code/modules/client/preference_setup/loadout/loadout.dm @@ -41,27 +41,58 @@ var/list/gear_datums = list() /datum/category_item/player_setup_item/loadout/load_character(var/savefile/S) S["gear"] >> pref.gear S["gear_list"] >> pref.gear_list - if(pref.gear_list!=null && pref.gear_slot!=null) - pref.gear = pref.gear_list["[pref.gear_slot]"] - else - S["gear"] >> pref.gear + +/datum/category_item/player_setup_item/loadout/load_character_special(var/savefile/S) + pref.gear_modified = FALSE + //Query the ss13_characters_gear table + if(GLOB.config.sql_saves && establish_db_connection(GLOB.dbcon)) + var/fetched_gear = list() + + var/DBQuery/character_gear_query = GLOB.dbcon.NewQuery("SELECT slot, name, tweaks FROM ss13_characters_gear WHERE char_id = :char_id:") + character_gear_query.Execute(list("char_id"=pref.current_character)) + while(character_gear_query.NextRow()) + CHECK_TICK + var/slot = character_gear_query.item[1] + var/name = character_gear_query.item[2] + + if(!fetched_gear[slot]) + fetched_gear[slot] = list() + + var/tweaks = list() + if(character_gear_query.item[3]) + tweaks = json_decode(character_gear_query.item[3]) + + fetched_gear[slot][name]=tweaks + + pref.gear_list = fetched_gear + return + +/datum/category_item/player_setup_item/loadout/save_character_special(var/savefile/S) + //Persist the loadout to the database + if(GLOB.config.sql_saves && establish_db_connection(GLOB.dbcon) && pref.gear_modified) + var/gear_string = json_encode(pref.gear_list["[pref.gear_slot]"]) + var/DBQuery/character_gear_save_query = GLOB.dbcon.NewQuery("CALL `update_character_gear`(:char_id:, :slot_id:, :gear_data:)") + character_gear_save_query.Execute(list("char_id"=pref.current_character,"slot_id"=pref.gear_slot,"gear_data"=gear_string)) + pref.gear_modified = FALSE + return /datum/category_item/player_setup_item/loadout/save_character(var/savefile/S) pref.gear_list["[pref.gear_slot]"] = pref.gear to_file(S["gear_list"], pref.gear_list) to_file(S["gear_slot"], pref.gear_slot) +/datum/category_item/player_setup_item/loadout/gather_load_query() + return list("ss13_characters" = list("vars" = list("gear_slot"), "args" = list("id"))) + /datum/category_item/player_setup_item/loadout/gather_load_parameters() return list("id" = pref.current_character) -/datum/category_item/player_setup_item/loadout/gather_load_query() - return list("ss13_characters" = list("vars" = list("gear" = "gear_list", "gear_slot"), "args" = list("id"))) - /datum/category_item/player_setup_item/loadout/gather_save_query() - return list("ss13_characters" = list("gear", "gear_slot", "id" = 1, "ckey" = 1)) + return list("ss13_characters" = list("gear_slot", "id" = 1, "ckey" = 1)) /datum/category_item/player_setup_item/loadout/gather_save_parameters() - return list("gear" = json_encode(pref.gear_list), "gear_slot" = pref.gear_slot, "id" = pref.current_character, "ckey" = PREF_CLIENT_CKEY) + return list("gear_slot" = pref.gear_slot, "id" = pref.current_character, "ckey" = PREF_CLIENT_CKEY) + /datum/category_item/player_setup_item/loadout/proc/valid_gear_choices(var/max_cost) . = list() @@ -131,7 +162,7 @@ var/list/gear_datums = list() pref.gear -= gear_name else var/datum/gear/G = gear_datums[gear_name] - if(total_cost + G.cost > MAX_GEAR_COST) + if(total_cost + G.cost > GLOB.config.loadout_cost) pref.gear -= gear_name to_chat(preference_mob, "You cannot afford to take \the [gear_name]") else @@ -146,13 +177,13 @@ var/list/gear_datums = list() total_cost += G.cost var/fcolor = "#3366CC" - if(total_cost < MAX_GEAR_COST) + if(total_cost < GLOB.config.loadout_cost) fcolor = "#E67300" . = list() . += "" if (gear_reset) . += "" - . += "" + . += "" . += "
Your loadout failed to load and will be reset if you save this slot.
\<\<\[[pref.gear_slot]\] \>\>[total_cost]/[MAX_GEAR_COST] loadout points spent. \[Clear Loadout\]
\<\<\[[pref.gear_slot]\] \>\>[total_cost]/[GLOB.config.loadout_cost] loadout points spent. \[Clear Loadout\]
" var/firstcat = 1 @@ -307,6 +338,7 @@ var/list/gear_datums = list() /datum/category_item/player_setup_item/loadout/OnTopic(href, href_list, user) if(href_list["toggle_gear"]) + pref.gear_modified = TRUE var/datum/gear/TG = gear_datums[href_list["toggle_gear"]] if(TG.display_name in pref.gear) pref.gear -= TG.display_name @@ -315,10 +347,11 @@ var/list/gear_datums = list() for(var/gear_name in pref.gear) var/datum/gear/G = gear_datums[gear_name] if(istype(G)) total_cost += G.cost - if((total_cost+TG.cost) <= MAX_GEAR_COST) + if((total_cost+TG.cost) <= GLOB.config.loadout_cost) pref.gear += TG.display_name return TOPIC_REFRESH_UPDATE_PREVIEW if(href_list["gear"] && href_list["tweak"]) + pref.gear_modified = TRUE var/datum/gear/gear = gear_datums[href_list["gear"]] var/datum/gear_tweak/tweak = locate(href_list["tweak"]) if(!tweak || !istype(gear) || !(tweak in gear.gear_tweaks)) @@ -330,6 +363,9 @@ var/list/gear_datums = list() return TOPIC_REFRESH_UPDATE_PREVIEW if(href_list["next_slot"] || href_list["prev_slot"]) + if(pref.gear_modified) + tgui_alert(user, "Gear has been Modified - Save First or Reload", "Gear Modified", list("OK")) + return TOPIC_NOACTION //Set the current slot in the gear list to the currently selected gear pref.gear_list["[pref.gear_slot]"] = pref.gear @@ -357,12 +393,16 @@ var/list/gear_datums = list() else if(href_list["select_category"]) current_tab = href_list["select_category"] return TOPIC_REFRESH + else if(href_list["clear_loadout"]) + pref.gear_modified = TRUE pref.gear.Cut() return TOPIC_REFRESH_UPDATE_PREVIEW + else if(href_list["search_input_refresh"] != null) // empty str is false search_input_value = sanitize(href_list["search_input_refresh"], 100) return TOPIC_REFRESH_UPDATE_PREVIEW + return ..() /datum/gear diff --git a/code/modules/client/preference_setup/other/01_incidents.dm b/code/modules/client/preference_setup/other/01_incidents.dm index f16b2b69292..b0f8088ed7b 100644 --- a/code/modules/client/preference_setup/other/01_incidents.dm +++ b/code/modules/client/preference_setup/other/01_incidents.dm @@ -2,7 +2,7 @@ name = "Incidents" sort_order = 7 -/datum/category_item/player_setup_item/other/incidents/load_special(var/savefile/S) +/datum/category_item/player_setup_item/other/incidents/load_character_special(var/savefile/S) pref.incidents = list() pref.ccia_actions = list() diff --git a/code/modules/client/preference_setup/preference_setup.dm b/code/modules/client/preference_setup/preference_setup.dm index 1ca9cad4b13..8be5a8721e2 100644 --- a/code/modules/client/preference_setup/preference_setup.dm +++ b/code/modules/client/preference_setup/preference_setup.dm @@ -153,7 +153,7 @@ handle_sql_loading(SQL_CHARACTER) for(var/datum/category_item/player_setup_item/PI in items) - PI.load_special(S) + PI.load_character_special(S) PI.sanitize_character(GLOB.config.sql_saves) /datum/category_group/player_setup_category/proc/save_character(var/savefile/S) @@ -161,10 +161,14 @@ for (var/datum/category_item/player_setup_item/PI in items) PI.sanitize_character() - if (!GLOB.config.sql_saves || !establish_db_connection(GLOB.dbcon)) - for (var/datum/category_item/player_setup_item/PI in items) + var/db_available = GLOB.config.sql_saves && establish_db_connection(GLOB.dbcon) + + for (var/datum/category_item/player_setup_item/PI in items) + PI.save_character_special(S) + if(!db_available) PI.save_character(S) - else if (modified) + + if (db_available && modified) // No save here, because this is only called from the menu and needs to save /everything/. handle_sql_saving(SQL_CHARACTER) modified = 0 @@ -238,17 +242,23 @@ return /* -* Called no matter if sql safes are enabled or disabled +* Called no matter if sql saves are enabled or disabled (After load_character) */ -/datum/category_item/player_setup_item/proc/load_special(var/savefile/S) +/datum/category_item/player_setup_item/proc/load_character_special(var/savefile/S) return /* -* Called when the item is asked to save per character settings +* Called when the item is asked to save per character settings - Only called when sql saves are disabled or unavailable */ /datum/category_item/player_setup_item/proc/save_character(var/savefile/S) return +/* +* Called no matter if sql saves are enabled or disabled (Before save_character / handle_sql_saving) +*/ +/datum/category_item/player_setup_item/proc/save_character_special(var/savefile/S) + return + /* * Called when the item is asked to load user/global settings */ diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index eff138955a1..630ed4efaa2 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -85,9 +85,10 @@ var/list/preferences_datums = list() var/list/alternate_languages = list() //Secondary language(s) var/list/language_prefixes = list() // Language prefix keys var/autohiss_setting = AUTOHISS_OFF - var/list/gear // Custom/fluff item loadout. - var/list/gear_list = list() //Custom/fluff item loadouts. + var/list/gear // The gear in the currently selected loadout item preset + var/list/gear_list = list() // The gear list holds all the different loadout item prests var/gear_slot = 1 //The current gear save slot + var/gear_modified = FALSE // IPC Stuff var/machine_tag_status = TRUE @@ -605,6 +606,8 @@ var/list/preferences_datums = list() can_edit_name = 1 gear = list() + gear_list = list() //Dont copy the loadout + gear_modified = FALSE //Reset the records when making a new char med_record = "" @@ -614,8 +617,6 @@ var/list/preferences_datums = list() exploit_record = "" ccia_record = "" - gear_list = list() //Dont copy the loadout - // Do we need to reinitialize a whole bunch more vars? if (re_initialize) be_special_role = list() diff --git a/html/changelogs/arrow768-loadout-update.yml b/html/changelogs/arrow768-loadout-update.yml new file mode 100644 index 00000000000..1bee849c550 --- /dev/null +++ b/html/changelogs/arrow768-loadout-update.yml @@ -0,0 +1,41 @@ +################################ +# Example Changelog File +# +# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb. +# +# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.) +# When it is, any changes listed below will disappear. +# +# Valid Prefixes: +# bugfix +# wip (For works in progress) +# tweak +# soundadd +# sounddel +# rscadd (general adding of nice things) +# rscdel (general deleting of nice things) +# imageadd +# imagedel +# maptweak +# spellcheck (typo fixes) +# experiment +# balance +# admin +# backend +# security +# refactor +################################# + +# Your name. +author: Arrow768 + +# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again. +delete-after: True + +# Any changes you've made. See valid prefix list above. +# INDENT WITH TWO SPACES. NOT TABS. SPACES. +# SCREW THIS UP AND IT WON'T WORK. +# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries. +# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog. +changes: + - backend: "Updated the way the loadout is stored so we might be able to increase the available loadout points."