From 78b2c0f4061b76744ccd0555aa4cc563ae714381 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 17:34:13 -0600 Subject: [PATCH] feat(character): the save's edit attribute is 1 for accounts at editor_gm_level The client takes a non-zero edit as the editor being enabled. A character whose account GM level is at least editor_gm_level (worldconfig.ini, default 9) is saved with edit="1"; everyone else keeps 0. A civilian never gets it, whatever the setting. The setting is in the dashboard's settings catalog. Co-Authored-By: Claude Fable 5.2 --- dDashboardServer/routes/SettingsCatalog.cpp | 2 + .../Character/CharacterComponent.cpp | 9 ++++- .../Character/CharacterComponent.h | 17 ++++++++ resources/worldconfig.ini | 4 ++ .../CharacterSaveFieldsTests.cpp | 39 +++++++++++++++++++ 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/dDashboardServer/routes/SettingsCatalog.cpp b/dDashboardServer/routes/SettingsCatalog.cpp index 8c0750314..1cab6d944 100644 --- a/dDashboardServer/routes/SettingsCatalog.cpp +++ b/dDashboardServer/routes/SettingsCatalog.cpp @@ -159,6 +159,8 @@ namespace { c.Add(Int(SHARED, "version_current", "Version: current", "", "10", 0, 65535)); c.Add(Int(SHARED, "version_minor", "Version: minor", "", "64", 0, 65535)); + c.Add(Int(WORLD, "editor_gm_level", "Client editor from GM level", "Characters of accounts at or above this GM level are saved with the client's editor enabled (edit=\"1\" in the character data); everyone else with 0. Used the next time a character is saved.", "9", 1, 9)); + c.AddSection("Logging and crashes"); c.Add(Bool(SHARED, "log_to_console", "Log to the console", "Also print log lines to the terminal.", true, true)); c.Add(Bool(SHARED, "log_debug_statements", "Debug logging", "Extra log lines for developers.", false, true)); diff --git a/dGame/dComponents/Character/CharacterComponent.cpp b/dGame/dComponents/Character/CharacterComponent.cpp index e826ab6bd..6b4c25ed2 100644 --- a/dGame/dComponents/Character/CharacterComponent.cpp +++ b/dGame/dComponents/Character/CharacterComponent.cpp @@ -4,6 +4,7 @@ #include "BitStream.h" #include "tinyxml2.h" #include "Game.h" +#include "dConfig.h" #include "Logger.h" #include "GeneralUtils.h" #include "dServer.h" @@ -480,8 +481,12 @@ void CharacterComponent::WriteSaveData(tinyxml2::XMLDocument& doc) { // cm, the most coins the character can hold: always 9223372036854775807 on live, the client's own value without // it. Nothing here caps coins by it, and one that is already in the save is left as it is. if (!character->FindAttribute("cm")) character->SetAttribute("cm", std::numeric_limits::max()); - // edit, the editor level: the client takes anything but 0 as the editor being on - character->SetAttribute("edit", m_EditorEnabled ? static_cast(m_EditorLevel) : 0u); + // edit, the editor level: the client takes anything but 0 as the editor being on. 1 for a character whose account + // has the GM level of editor_gm_level (9 unless set), 0 for everyone else + const auto* const parentUser = m_Character ? m_Character->GetParentUser() : nullptr; + const auto accountLevel = parentUser ? parentUser->GetMaxGMLevel() : eGameMasterLevel::CIVILIAN; + const auto editorGmLevel = GeneralUtils::TryParse(Game::config->GetValue("editor_gm_level")).value_or(DEFAULT_EDITOR_GM_LEVEL); + character->SetAttribute("edit", EditAttribute(accountLevel, editorGmLevel, m_EditorEnabled, m_EditorLevel)); // mldt, when the property moderation notice was last shown: not kept here, 0 as on most live saves if (!character->FindAttribute("mldt")) character->SetAttribute("mldt", 0); // gid and gn, the guild (0 and empty without one) diff --git a/dGame/dComponents/Character/CharacterComponent.h b/dGame/dComponents/Character/CharacterComponent.h index 70ceb1b07..9ca58b388 100644 --- a/dGame/dComponents/Character/CharacterComponent.h +++ b/dGame/dComponents/Character/CharacterComponent.h @@ -1,6 +1,7 @@ #ifndef CHARACTERCOMPONENT_H #define CHARACTERCOMPONENT_H +#include "eGameMasterLevel.h" #include "dCommonVars.h" #include "RakNetTypes.h" #include "Character.h" @@ -477,6 +478,22 @@ private: */ bool m_EditorEnabled; +public: + // editor_gm_level when the settings don't have it + static constexpr uint32_t DEFAULT_EDITOR_GM_LEVEL = 9; + + /** + * The save's edit attribute (the client takes anything but 0 as the editor being on): 1 for an account whose GM + * level is at least editorGmLevel, else the editor level when the editor flag is on, else 0 + */ + static uint32_t EditAttribute(const eGameMasterLevel accountLevel, const uint32_t editorGmLevel, const bool editorEnabled, const eGameMasterLevel editorLevel) { + // (a civilian never, whatever the setting) + if (accountLevel != eGameMasterLevel::CIVILIAN && static_cast(accountLevel) >= editorGmLevel) return 1; + return editorEnabled ? static_cast(editorLevel) : 0; + } + +private: + /** * The level of the character in HF */ diff --git a/resources/worldconfig.ini b/resources/worldconfig.ini index a41526d56..324058bab 100644 --- a/resources/worldconfig.ini +++ b/resources/worldconfig.ini @@ -118,6 +118,10 @@ save_lxfmls=0 # 1: saving a brick by brick model uses up the bricks in it, as live did. 0: they go back to the backpack. bbb_consume_bricks=0 +# Characters of accounts at or above this GM level are saved with the client's editor enabled (edit="1" in the +# character data); everyone else with 0. +editor_gm_level=9 + # 1: logging out on a property and back in returns to that property. 0 (as live): to the last world before it. save_property_location=0 diff --git a/tests/dGameTests/dComponentsTests/CharacterSaveFieldsTests.cpp b/tests/dGameTests/dComponentsTests/CharacterSaveFieldsTests.cpp index 3c3357804..be5878b94 100644 --- a/tests/dGameTests/dComponentsTests/CharacterSaveFieldsTests.cpp +++ b/tests/dGameTests/dComponentsTests/CharacterSaveFieldsTests.cpp @@ -6,6 +6,8 @@ #include "Character.h" #include "CharacterComponent.h" #include "Entity.h" +#include "User.h" +#include "dConfig.h" #include "GameMessages.h" #include "MissionComponent.h" #include "SkillComponent.h" @@ -163,6 +165,43 @@ TEST_F(CharacterSaveFieldsTests, LiveCharacterAttributesRoundTrip) { EXPECT_STREQ(saved->Attribute("gn"), "Builders"); } +// edit: 1 for a character whose account has the GM level of editor_gm_level (9 unless set), 0 for everyone else +TEST_F(CharacterSaveFieldsTests, EditAttributeFollowsTheAccountGmLevel) { + using enum eGameMasterLevel; + EXPECT_EQ(CharacterComponent::EditAttribute(OPERATOR, 9, false, OPERATOR), 1u); + EXPECT_EQ(CharacterComponent::EditAttribute(DEVELOPER, 9, false, DEVELOPER), 0u); + EXPECT_EQ(CharacterComponent::EditAttribute(CIVILIAN, 9, false, CIVILIAN), 0u); + EXPECT_EQ(CharacterComponent::EditAttribute(DEVELOPER, 8, false, DEVELOPER), 1u); + EXPECT_EQ(CharacterComponent::EditAttribute(JUNIOR_DEVELOPER, 8, false, JUNIOR_DEVELOPER), 0u); + // A civilian never, whatever the setting + EXPECT_EQ(CharacterComponent::EditAttribute(CIVILIAN, 0, false, CIVILIAN), 0u); + // The editor flag still writes its level + EXPECT_EQ(CharacterComponent::EditAttribute(DEVELOPER, 9, true, DEVELOPER), 8u); + + const auto savedEdit = [this](const eGameMasterLevel accountLevel) { + tinyxml2::XMLDocument doc; + Parse(doc, R"()"); + User user(UNASSIGNED_SYSTEM_ADDRESS, "editor", "key"); + user.SetMaxGMLevel(accountLevel); + Character character(1, &user); + Entity player(43, info); + auto* const characterComponent = player.AddComponent(-1, &character, UNASSIGNED_SYSTEM_ADDRESS); + characterComponent->LoadFromSaveData(doc); + characterComponent->WriteSaveData(doc); + return std::string(doc.FirstChildElement("obj")->FirstChildElement("char")->Attribute("edit")); + }; + // The default level, 9 + EXPECT_EQ(savedEdit(OPERATOR), "1"); + EXPECT_EQ(savedEdit(DEVELOPER), "0"); + EXPECT_EQ(savedEdit(CIVILIAN), "0"); + + // editor_gm_level from the settings + Game::config->SetDatabaseValues({ { "editor_gm_level", "8" } }, {}); + EXPECT_EQ(savedEdit(DEVELOPER), "1"); + EXPECT_EQ(savedEdit(JUNIOR_DEVELOPER), "0"); + Game::config->SetDatabaseValues({}, {}); +} + // Saves from before they were written load as before and gain them with the values live had: the coins are not // touched and cm is the client's own "no limit". TEST_F(CharacterSaveFieldsTests, LiveCharacterAttributesMissingInOldSave) {