diff --git a/dDashboardServer/routes/SettingsCatalog.cpp b/dDashboardServer/routes/SettingsCatalog.cpp index 9c83fec66..6c3e3fb34 100644 --- a/dDashboardServer/routes/SettingsCatalog.cpp +++ b/dDashboardServer/routes/SettingsCatalog.cpp @@ -169,7 +169,7 @@ 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.Add(Int(WORLD, "editor_gm_level", "Client editor from GM level", "Characters of accounts at or above this GM level have the client's level editor enabled: their editor level is the account's GM level (edit in the character data, and told to the client with the character); everyone else has 0. Used the next time a character loads or 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)); diff --git a/dGame/dComponents/Character/CharacterComponent.cpp b/dGame/dComponents/Character/CharacterComponent.cpp index 6b4c25ed2..a00161818 100644 --- a/dGame/dComponents/Character/CharacterComponent.cpp +++ b/dGame/dComponents/Character/CharacterComponent.cpp @@ -64,8 +64,7 @@ CharacterComponent::CharacterComponent(Entity* parent, const int32_t componentID m_PvpEnabled = false; m_GMLevel = character->GetGMLevel(); - m_EditorEnabled = false; - m_EditorLevel = m_GMLevel; + RefreshEditorLevel(); m_Reputation = 0; m_CurrentActivity = eGameActivity::NONE; @@ -307,6 +306,16 @@ void CharacterComponent::SetPvpEnabled(const bool value) { m_PvpEnabled = value; } +void CharacterComponent::RefreshEditorLevel() { + const auto* const parentUser = m_Character ? m_Character->GetParentUser() : nullptr; + const auto accountLevel = parentUser ? parentUser->GetMaxGMLevel() : eGameMasterLevel::CIVILIAN; + const auto editorGmLevel = Game::config ? GeneralUtils::TryParse(Game::config->GetValue("editor_gm_level")).value_or(DEFAULT_EDITOR_GM_LEVEL) : DEFAULT_EDITOR_GM_LEVEL; + const auto level = static_cast(EditAttribute(accountLevel, editorGmLevel)); + if (level != m_EditorLevel) m_DirtyGMInfo = true; + m_EditorLevel = level; + m_EditorEnabled = level != eGameMasterLevel::CIVILIAN; +} + void CharacterComponent::SetGMLevel(eGameMasterLevel gmlevel) { m_DirtyGMInfo = true; if (gmlevel > eGameMasterLevel::CIVILIAN) m_IsGM = true; @@ -402,8 +411,6 @@ void CharacterComponent::LoadFromSaveData(const tinyxml2::XMLDocument& doc) { if (m_GMLevel > eGameMasterLevel::CIVILIAN) { m_IsGM = true; m_DirtyGMInfo = true; - m_EditorLevel = m_GMLevel; - m_EditorEnabled = false; //We're not currently in HF if we're loading in } LoadGuild(); @@ -481,12 +488,10 @@ 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. 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)); + // edit, the editor level: the client takes anything but 0 as the editor being on. The account's GM level for an + // account at editor_gm_level (9 unless set) or above, 0 for everyone else + RefreshEditorLevel(); + character->SetAttribute("edit", static_cast(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 9ca58b388..a02e3bbcb 100644 --- a/dGame/dComponents/Character/CharacterComponent.h +++ b/dGame/dComponents/Character/CharacterComponent.h @@ -474,30 +474,37 @@ private: eGameMasterLevel m_GMLevel; /** - * Whether the character has HF enabled + * Whether the client's level editor is on for the character: its editor level is not 0 */ - bool m_EditorEnabled; + 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 + * A character's editor level, which is also the save's edit attribute (the client takes anything but 0 as the + * editor being on): the account's GM level for an account at editorGmLevel or above, 0 for everyone else */ - static uint32_t EditAttribute(const eGameMasterLevel accountLevel, const uint32_t editorGmLevel, const bool editorEnabled, const eGameMasterLevel editorLevel) { + static uint32_t EditAttribute(const eGameMasterLevel accountLevel, const uint32_t editorGmLevel) { // (a civilian never, whatever the setting) - if (accountLevel != eGameMasterLevel::CIVILIAN && static_cast(accountLevel) >= editorGmLevel) return 1; - return editorEnabled ? static_cast(editorLevel) : 0; + if (accountLevel == eGameMasterLevel::CIVILIAN || static_cast(accountLevel) < editorGmLevel) return 0; + return static_cast(accountLevel); } + // The editor level as the client is told it (construction, the save, GetEditorLevel): 0 when the editor is off + uint8_t GetEditorLevel() const { return static_cast(m_EditorLevel); } + bool GetEditorEnabled() const { return m_EditorEnabled; } + + // Works the editor level out again from the account's GM level and editor_gm_level + void RefreshEditorLevel(); + private: /** - * The level of the character in HF + * The character's editor level (EditAttribute), 0 for none */ - eGameMasterLevel m_EditorLevel; + eGameMasterLevel m_EditorLevel{}; /** * Whether the currently active activity has been changed diff --git a/dGame/dGameMessages/GameMessageFields.inc b/dGame/dGameMessages/GameMessageFields.inc index 14a7fcfc4..f63fc5538 100644 --- a/dGame/dGameMessages/GameMessageFields.inc +++ b/dGame/dGameMessages/GameMessageFields.inc @@ -204,6 +204,7 @@ json ToJson(const GameMessages::ReportBug& m); json ToJson(const GameMessages::VerifyAck& m); json ToJson(const GameMessages::Help& m); json ToJson(const GameMessages::DisplayGuildCreateBox& m); +json ToJson(const GameMessages::GetEditorLevel& m); json ToJson(const GameMessages::OpenPropertyVendor& m); json ToJson(const GameMessages::OpenPropertyManagement& m); json ToJson(const GameMessages::DownloadPropertyData& m); @@ -1806,6 +1807,12 @@ json ToJson(const GameMessages::DisplayGuildCreateBox& m) { j["bShow"] = ToJson(m.bShow); return j; } +json ToJson(const GameMessages::GetEditorLevel& m) { + json j = json::object(); + j["editorEnabled"] = ToJson(m.editorEnabled); + j["editorLevel"] = ToJson(m.editorLevel); + return j; +} json ToJson(const GameMessages::OpenPropertyVendor& m) { json j = json::object(); return j; @@ -2724,6 +2731,7 @@ const std::vector& Entries() { { MessageType::Game::VERIFY_ACK, eDirection::TO_CLIENT, "VerifyAck", &ReadWith }, { MessageType::Game::HELP, eDirection::TO_CLIENT, "Help", &ReadWith }, { MessageType::Game::DISPLAY_GUILD_CREATE_BOX, eDirection::TO_CLIENT, "DisplayGuildCreateBox", &ReadWith }, + { MessageType::Game::GET_EDITOR_LEVEL, eDirection::TO_SERVER, "GetEditorLevel", &ReadWith }, { MessageType::Game::OPEN_PROPERTY_VENDOR, eDirection::TO_CLIENT, "OpenPropertyVendor", &ReadWith }, { MessageType::Game::OPEN_PROPERTY_MANAGEMENT, eDirection::TO_CLIENT, "OpenPropertyManagement", &ReadWith }, { MessageType::Game::DOWNLOAD_PROPERTY_DATA, eDirection::TO_CLIENT, "DownloadPropertyData", &ReadWith }, diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index ee3ef8c9b..53516f4e9 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -222,6 +222,7 @@ namespace { { TOGGLE_GHOST_REFERENCE_OVERRIDE, []() { return std::make_unique(); } }, { SET_GHOST_REFERENCE_POSITION, []() { return std::make_unique(); } }, { MODIFY_GHOSTING_DISTANCE, []() { return std::make_unique(); } }, + { GET_EDITOR_LEVEL, []() { return std::make_unique(); } }, }; }; @@ -270,8 +271,7 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System } // The Happy Flower editor's messages, logged always until the server handles them (what a client sends while editing) - const bool isHappyFlower = messageID == MessageType::Game::GET_EDITOR_LEVEL - || (messageID >= MessageType::Game::HF_ATTRIBUTES_PUSH && messageID <= MessageType::Game::HF_GETS_WANT_PATH); + const bool isHappyFlower = messageID >= MessageType::Game::HF_ATTRIBUTES_PUSH && messageID <= MessageType::Game::HF_GETS_WANT_PATH; if (isHappyFlower) LOG("Received unhandled Happy Flower GM: %4i, %s", messageID, StringifiedEnum::ToString(messageID).data()); else LOG_DEBUG("Received Unknown GM with ID: %4i, %s", messageID, StringifiedEnum::ToString(messageID).data()); } diff --git a/dGame/dGameMessages/PlayerMessages.cpp b/dGame/dGameMessages/PlayerMessages.cpp index 909ccf619..b484d2adf 100644 --- a/dGame/dGameMessages/PlayerMessages.cpp +++ b/dGame/dGameMessages/PlayerMessages.cpp @@ -1,5 +1,7 @@ #include "PlayerMessages.h" +#include "PlayerManager.h" + #include "Logger.h" #include "BitStreamUtils.h" #include "Character.h" @@ -265,4 +267,28 @@ namespace GameMessages { bool DisplayGuildCreateBox::Deserialize(RakNet::BitStream& bitStream) { return bitStream.Read(bShow); } + + void GetEditorLevel::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(editorEnabled); + bitStream.Write(editorLevel); + } + + bool GetEditorLevel::Deserialize(RakNet::BitStream& bitStream) { + // A question: whatever it carries is not needed to answer it + if (bitStream.GetNumberOfUnreadBits() < 33) return true; + return bitStream.Read(editorEnabled) && bitStream.Read(editorLevel); + } + + void GetEditorLevel::Handle(Entity& entity, const SystemAddress& sysAddr) { + // Only about the asker's own character + auto* const player = PlayerManager::GetPlayer(sysAddr); + if (!player || player != &entity) return; + const auto* const character = entity.GetComponent(); + if (!character) return; + GetEditorLevel answer; + answer.target = entity.GetObjectID(); + answer.editorEnabled = character->GetEditorEnabled(); + answer.editorLevel = character->GetEditorLevel(); + answer.SendToClient(sysAddr); + } } diff --git a/dGame/dGameMessages/PlayerMessages.h b/dGame/dGameMessages/PlayerMessages.h index 7b7908d01..72b7e3aef 100644 --- a/dGame/dGameMessages/PlayerMessages.h +++ b/dGame/dGameMessages/PlayerMessages.h @@ -193,6 +193,18 @@ namespace GameMessages { bool bShow{ true }; }; + + // Client -> server, and the server's answer to that client: whether the client's level editor is on for the + // player and at which level (the character's editor level, 0 for none). What a client sends is not used. + struct GetEditorLevel : public NetGameMsg { + GetEditorLevel() : NetGameMsg(MessageType::Game::GET_EDITOR_LEVEL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool editorEnabled{ false }; + int32_t editorLevel{ 0 }; + }; } #endif // PLAYERMESSAGES_H diff --git a/resources/worldconfig.ini b/resources/worldconfig.ini index 324058bab..6124194d8 100644 --- a/resources/worldconfig.ini +++ b/resources/worldconfig.ini @@ -118,8 +118,9 @@ 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. +# Characters of accounts at or above this GM level have the client's level editor enabled: their editor level is the +# account's GM level (edit="" in the character data, and told to the client with the character); everyone +# else has 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. diff --git a/tests/dGameTests/dComponentsTests/CharacterSaveFieldsTests.cpp b/tests/dGameTests/dComponentsTests/CharacterSaveFieldsTests.cpp index be5878b94..81040f264 100644 --- a/tests/dGameTests/dComponentsTests/CharacterSaveFieldsTests.cpp +++ b/tests/dGameTests/dComponentsTests/CharacterSaveFieldsTests.cpp @@ -5,6 +5,7 @@ #include "CDClientDatabase.h" #include "Character.h" #include "CharacterComponent.h" +#include "PlayerMessages.h" #include "Entity.h" #include "User.h" #include "dConfig.h" @@ -165,18 +166,18 @@ 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 +// edit, the editor level: the account's GM level for an account at editor_gm_level (9 unless set) or above, 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); + EXPECT_EQ(CharacterComponent::EditAttribute(OPERATOR, 9), 9u); + EXPECT_EQ(CharacterComponent::EditAttribute(DEVELOPER, 9), 0u); + EXPECT_EQ(CharacterComponent::EditAttribute(CIVILIAN, 9), 0u); + EXPECT_EQ(CharacterComponent::EditAttribute(DEVELOPER, 8), 8u); + EXPECT_EQ(CharacterComponent::EditAttribute(OPERATOR, 8), 9u); + EXPECT_EQ(CharacterComponent::EditAttribute(JUNIOR_DEVELOPER, 8), 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); + EXPECT_EQ(CharacterComponent::EditAttribute(CIVILIAN, 0), 0u); const auto savedEdit = [this](const eGameMasterLevel accountLevel) { tinyxml2::XMLDocument doc; @@ -191,13 +192,13 @@ TEST_F(CharacterSaveFieldsTests, EditAttributeFollowsTheAccountGmLevel) { return std::string(doc.FirstChildElement("obj")->FirstChildElement("char")->Attribute("edit")); }; // The default level, 9 - EXPECT_EQ(savedEdit(OPERATOR), "1"); + EXPECT_EQ(savedEdit(OPERATOR), "9"); 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(DEVELOPER), "8"); EXPECT_EQ(savedEdit(JUNIOR_DEVELOPER), "0"); Game::config->SetDatabaseValues({}, {}); } @@ -410,3 +411,47 @@ TEST_F(CharacterSaveFieldsTests, PetsAsLiveWroteThem) { inventory->WriteSaveData(doc); EXPECT_EQ(Print(doc), R"(

)"); } + +// The editor level is told to the client with the character: after the GM level, whether the editor is on and at +// which level +TEST_F(CharacterSaveFieldsTests, EditorLevelIsSerializedAfterTheGmLevel) { + const auto gmBlock = [this](const eGameMasterLevel accountLevel) { + User user(UNASSIGNED_SYSTEM_ADDRESS, "editor", "key"); + user.SetMaxGMLevel(accountLevel); + Character character(1, &user); + Entity player(44, info); + auto* const characterComponent = player.AddComponent(-1, &character, UNASSIGNED_SYSTEM_ADDRESS); + EXPECT_EQ(characterComponent->GetEditorLevel(), accountLevel == eGameMasterLevel::OPERATOR ? 9 : 0); + EXPECT_EQ(characterComponent->GetEditorEnabled(), accountLevel == eGameMasterLevel::OPERATOR); + // An update with the GM block: its flag, PvP, is a GM, the GM level, then the two editor fields + characterComponent->SetGMLevel(eGameMasterLevel::CIVILIAN); + RakNet::BitStream stream; + characterComponent->Serialize(stream, false); + bool written{}, pvp{}, isGm{}, editorEnabled{}; + uint8_t gmLevel{}, editorLevel{}; + EXPECT_TRUE(stream.Read(written) && stream.Read(pvp) && stream.Read(isGm) && stream.Read(gmLevel) && stream.Read(editorEnabled) && stream.Read(editorLevel)); + EXPECT_TRUE(written); + EXPECT_EQ(gmLevel, 0); + return std::pair{ editorEnabled, editorLevel }; + }; + EXPECT_EQ(gmBlock(eGameMasterLevel::OPERATOR), (std::pair{ true, 9 })); + EXPECT_EQ(gmBlock(eGameMasterLevel::DEVELOPER), (std::pair{ false, 0 })); + EXPECT_EQ(gmBlock(eGameMasterLevel::CIVILIAN), (std::pair{ false, 0 })); +} + +// GetEditorLevel's answer: one bit, then the level as 32 bits +TEST_F(CharacterSaveFieldsTests, GetEditorLevelAnswerLayout) { + GameMessages::GetEditorLevel answer; + answer.editorEnabled = true; + answer.editorLevel = 9; + RakNet::BitStream stream; + answer.Serialize(stream); + EXPECT_EQ(stream.GetNumberOfBitsUsed(), 33u); + GameMessages::GetEditorLevel read; + ASSERT_TRUE(read.Deserialize(stream)); + EXPECT_TRUE(read.editorEnabled); + EXPECT_EQ(read.editorLevel, 9); + // A question with nothing in it reads too + RakNet::BitStream empty; + EXPECT_TRUE(GameMessages::GetEditorLevel().Deserialize(empty)); +}