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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 17:34:13 -06:00
parent 98edeb0c72
commit 78b2c0f406
5 changed files with 69 additions and 2 deletions

View File

@@ -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));

View File

@@ -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<int64_t>::max());
// edit, the editor level: the client takes anything but 0 as the editor being on
character->SetAttribute("edit", m_EditorEnabled ? static_cast<uint32_t>(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<uint32_t>(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)

View File

@@ -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<uint32_t>(accountLevel) >= editorGmLevel) return 1;
return editorEnabled ? static_cast<uint32_t>(editorLevel) : 0;
}
private:
/**
* The level of the character in HF
*/

View File

@@ -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

View File

@@ -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"(<obj v="1"><mf/><char ls="0" edit="0"/></obj>)");
User user(UNASSIGNED_SYSTEM_ADDRESS, "editor", "key");
user.SetMaxGMLevel(accountLevel);
Character character(1, &user);
Entity player(43, info);
auto* const characterComponent = player.AddComponent<CharacterComponent>(-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) {