mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
feat(character): the editor level is the account's GM level, and the client is told it
A character's editor level is its account's GM level for accounts at editor_gm_level or above, and 0 for everyone else (it was 1 or 0). It is written as the save's edit attribute, sent with the character after the GM level (the editor flag, then the level) and given in answer to the GetEditorLevel game message. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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<uint32_t>(Game::config->GetValue("editor_gm_level")).value_or(DEFAULT_EDITOR_GM_LEVEL) : DEFAULT_EDITOR_GM_LEVEL;
|
||||
const auto level = static_cast<eGameMasterLevel>(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<int64_t>::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<uint32_t>(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<uint32_t>(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)
|
||||
|
||||
@@ -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<uint32_t>(accountLevel) >= editorGmLevel) return 1;
|
||||
return editorEnabled ? static_cast<uint32_t>(editorLevel) : 0;
|
||||
if (accountLevel == eGameMasterLevel::CIVILIAN || static_cast<uint32_t>(accountLevel) < editorGmLevel) return 0;
|
||||
return static_cast<uint32_t>(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<uint8_t>(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
|
||||
|
||||
@@ -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<Entry>& Entries() {
|
||||
{ MessageType::Game::VERIFY_ACK, eDirection::TO_CLIENT, "VerifyAck", &ReadWith<GameMessages::VerifyAck> },
|
||||
{ MessageType::Game::HELP, eDirection::TO_CLIENT, "Help", &ReadWith<GameMessages::Help> },
|
||||
{ MessageType::Game::DISPLAY_GUILD_CREATE_BOX, eDirection::TO_CLIENT, "DisplayGuildCreateBox", &ReadWith<GameMessages::DisplayGuildCreateBox> },
|
||||
{ MessageType::Game::GET_EDITOR_LEVEL, eDirection::TO_SERVER, "GetEditorLevel", &ReadWith<GameMessages::GetEditorLevel> },
|
||||
{ MessageType::Game::OPEN_PROPERTY_VENDOR, eDirection::TO_CLIENT, "OpenPropertyVendor", &ReadWith<GameMessages::OpenPropertyVendor> },
|
||||
{ MessageType::Game::OPEN_PROPERTY_MANAGEMENT, eDirection::TO_CLIENT, "OpenPropertyManagement", &ReadWith<GameMessages::OpenPropertyManagement> },
|
||||
{ MessageType::Game::DOWNLOAD_PROPERTY_DATA, eDirection::TO_CLIENT, "DownloadPropertyData", &ReadWith<GameMessages::DownloadPropertyData> },
|
||||
|
||||
@@ -222,6 +222,7 @@ namespace {
|
||||
{ TOGGLE_GHOST_REFERENCE_OVERRIDE, []() { return std::make_unique<ToggleGhostReferenceOverride>(); } },
|
||||
{ SET_GHOST_REFERENCE_POSITION, []() { return std::make_unique<SetGhostReferencePosition>(); } },
|
||||
{ MODIFY_GHOSTING_DISTANCE, []() { return std::make_unique<ModifyGhostingDistance>(); } },
|
||||
{ GET_EDITOR_LEVEL, []() { return std::make_unique<GetEditorLevel>(); } },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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<CharacterComponent>();
|
||||
if (!character) return;
|
||||
GetEditorLevel answer;
|
||||
answer.target = entity.GetObjectID();
|
||||
answer.editorEnabled = character->GetEditorEnabled();
|
||||
answer.editorLevel = character->GetEditorLevel();
|
||||
answer.SendToClient(sysAddr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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="<level>" 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.
|
||||
|
||||
@@ -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"(<obj v="1"><pet a="0"><p id="1152921510000000001" l="3254" m="2" n="Fluffy" t="0"/></pet></obj>)");
|
||||
}
|
||||
|
||||
// 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<CharacterComponent>(-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<bool, uint8_t>{ editorEnabled, editorLevel };
|
||||
};
|
||||
EXPECT_EQ(gmBlock(eGameMasterLevel::OPERATOR), (std::pair<bool, uint8_t>{ true, 9 }));
|
||||
EXPECT_EQ(gmBlock(eGameMasterLevel::DEVELOPER), (std::pair<bool, uint8_t>{ false, 0 }));
|
||||
EXPECT_EQ(gmBlock(eGameMasterLevel::CIVILIAN), (std::pair<bool, uint8_t>{ 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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user