mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
feat: write the character attributes live saved and the client reads
LWOCharacterComponent::LoadFromSaveData (0x00ca4910) reads them, and live saves had every one (450 of 450 characters saved at least once): - cm, the most coins a character can hold: 9223372036854775807 on every live save, the client's own value without it. Written when missing, an existing one is kept, and nothing caps the coins by it. - edit (editor level), gid and gn (guild id and name). - mldt (when the property moderation notice was last shown): 0 when missing, as most live saves. - v and vd: the votes left and the day they are for (GetVotesLeft / AdjustVotesLeft). Read back; a new day fills them up to the 3 of WorldConfig's character_votes_per_day. - lrx.. lrrw: the last respawn point reached, once one was reached in the world. - lzcs: the checksum of the zone in lzid. Saves without them load as before. stime is not written: live kept the session's seconds in it and the time before the session in time, which would change what time means in existing saves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -290,6 +290,8 @@ void Character::SaveXMLToDatabase() {
|
||||
lzidConcat = (lzidConcat << 16) | uint16_t(zoneInfo.GetInstanceID());
|
||||
lzidConcat = (lzidConcat << 16) | uint16_t(zoneInfo.GetMapID());
|
||||
character->SetAttribute("lzid", lzidConcat);
|
||||
// The checksum of that zone, as live wrote next to it (LWOCharacterComponent::LoadFromSaveData 0x00ca4910 reads it)
|
||||
character->SetAttribute("lzcs", Game::zoneManager->GetZone()->GetChecksum());
|
||||
character->SetAttribute("lnzid", GetLastNonInstanceZoneID());
|
||||
|
||||
//Darwin's backup:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <limits>
|
||||
#include "CharacterComponent.h"
|
||||
#include "BitStream.h"
|
||||
#include "tinyxml2.h"
|
||||
@@ -328,6 +329,12 @@ void CharacterComponent::LoadFromSaveData(const tinyxml2::XMLDocument& doc) {
|
||||
m_TooltipFlags = 0;
|
||||
character->QueryUnsigned64Attribute("ttip", &m_TooltipFlags);
|
||||
|
||||
// Saves from before these were written have neither: a full day's votes, filled up at the next save
|
||||
m_VotesLeft = VOTES_PER_DAY;
|
||||
m_VoteDay = 0;
|
||||
character->QueryIntAttribute("v", &m_VotesLeft);
|
||||
character->QueryUnsigned64Attribute("vd", &m_VoteDay);
|
||||
|
||||
// Load the statistics
|
||||
const auto* statisticsAttribute = character->FindAttribute("stt");
|
||||
if (statisticsAttribute) {
|
||||
@@ -448,6 +455,36 @@ void CharacterComponent::WriteSaveData(tinyxml2::XMLDocument& doc) {
|
||||
character->SetAttribute("ls", m_Uscore);
|
||||
// Live wrote ttip on every character, 0 when no tooltip was flagged
|
||||
character->SetAttribute("ttip", m_TooltipFlags);
|
||||
// Live wrote these on every character (450 of 450 saves that had been saved once); the client reads them in
|
||||
// LWOCharacterComponent::LoadFromSaveData (0x00ca4910).
|
||||
// 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);
|
||||
// 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)
|
||||
character->SetAttribute("gid", static_cast<int64_t>(m_GuildID));
|
||||
character->SetAttribute("gn", GeneralUtils::UTF16ToWTF8(m_GuildName).c_str());
|
||||
// v and vd, the votes left and the day they are for: a new day fills them up
|
||||
const uint64_t today = static_cast<uint64_t>(std::time(nullptr)) / 86400;
|
||||
if (m_VoteDay != today) {
|
||||
m_VotesLeft = VOTES_PER_DAY;
|
||||
m_VoteDay = today;
|
||||
}
|
||||
character->SetAttribute("v", m_VotesLeft);
|
||||
character->SetAttribute("vd", m_VoteDay);
|
||||
// lrx.., the last respawn point reached, once one was reached in this world
|
||||
if (m_respawnPos != NiPoint3Constant::ZERO) {
|
||||
character->SetAttribute("lrx", m_respawnPos.x);
|
||||
character->SetAttribute("lry", m_respawnPos.y);
|
||||
character->SetAttribute("lrz", m_respawnPos.z);
|
||||
character->SetAttribute("lrrx", m_respawnRot.x);
|
||||
character->SetAttribute("lrry", m_respawnRot.y);
|
||||
character->SetAttribute("lrrz", m_respawnRot.z);
|
||||
character->SetAttribute("lrrw", m_respawnRot.w);
|
||||
}
|
||||
// Custom attribute to keep track of reputation.
|
||||
character->SetAttribute("rpt", GetReputation());
|
||||
character->SetAttribute("stt", StatisticsToString().c_str());
|
||||
|
||||
@@ -368,6 +368,11 @@ public:
|
||||
void SetTooltipFlag(int32_t tooltip, bool flag);
|
||||
uint64_t GetTooltipFlags() const { return m_TooltipFlags; }
|
||||
|
||||
// WorldConfig's character_votes_per_day
|
||||
static constexpr int32_t VOTES_PER_DAY = 3;
|
||||
int32_t GetVotesLeft() const { return m_VotesLeft; }
|
||||
uint64_t GetVoteDay() const { return m_VoteDay; }
|
||||
|
||||
uint64_t GetTotalTimePlayed() const { return m_TotalTimePlayed; };
|
||||
|
||||
/**
|
||||
@@ -408,6 +413,11 @@ private:
|
||||
// The tooltips the player has seen, one bit each (char@ttip)
|
||||
uint64_t m_TooltipFlags{};
|
||||
|
||||
// The votes the character has left today (char@v) and the day, in days since 1970, they were last filled up on
|
||||
// (char@vd), as LWOCharacterComponent::LoadFromSaveData (0x00ca4910) reads them for GetVotesLeft
|
||||
int32_t m_VotesLeft = VOTES_PER_DAY;
|
||||
uint64_t m_VoteDay{};
|
||||
|
||||
/**
|
||||
* The lifetime reputation earned by the entity
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include "tinyxml2.h"
|
||||
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
@@ -115,6 +116,81 @@ TEST_F(CharacterSaveFieldsTests, TooltipFlagsMissingInOldSave) {
|
||||
EXPECT_STREQ(doc.FirstChildElement("obj")->FirstChildElement("char")->Attribute("ttip"), "0");
|
||||
}
|
||||
|
||||
// The <char> attributes live wrote on every character and the client reads: cm, edit, mldt, gid, gn, v, vd and the
|
||||
// last respawn point. A save that has them keeps what the server does not own, and nothing touches the coins.
|
||||
TEST_F(CharacterSaveFieldsTests, LiveCharacterAttributesRoundTrip) {
|
||||
tinyxml2::XMLDocument doc;
|
||||
Parse(doc, R"(<obj v="1"><mf/><char cc="22609" cm="5000" ls="0" mldt="1301715419" v="1" vd="1" lrx="1" lry="2" lrz="3" lrrx="0" lrry="0.5" lrrz="0" lrrw="0.5" stime="349" lzcs="2233038349"/></obj>)");
|
||||
|
||||
Character character(1, nullptr);
|
||||
Entity player(40, info);
|
||||
auto* const characterComponent = player.AddComponent<CharacterComponent>(-1, &character, UNASSIGNED_SYSTEM_ADDRESS);
|
||||
characterComponent->LoadFromSaveData(doc);
|
||||
EXPECT_EQ(characterComponent->GetVotesLeft(), 1);
|
||||
EXPECT_EQ(characterComponent->GetVoteDay(), 1u);
|
||||
|
||||
characterComponent->WriteSaveData(doc);
|
||||
const auto* const saved = doc.FirstChildElement("obj")->FirstChildElement("char");
|
||||
EXPECT_STREQ(saved->Attribute("cc"), "22609");
|
||||
EXPECT_STREQ(saved->Attribute("cm"), "5000"); // kept, not replaced and not applied
|
||||
EXPECT_STREQ(saved->Attribute("mldt"), "1301715419");
|
||||
EXPECT_STREQ(saved->Attribute("edit"), "0");
|
||||
EXPECT_STREQ(saved->Attribute("gid"), "0");
|
||||
EXPECT_STREQ(saved->Attribute("gn"), "");
|
||||
// A new day: the votes are filled up
|
||||
const uint64_t today = static_cast<uint64_t>(std::time(nullptr)) / 86400;
|
||||
EXPECT_LE(saved->Unsigned64Attribute("vd") - today, 1u); // (the day may turn during the test)
|
||||
EXPECT_STREQ(saved->Attribute("v"), "3");
|
||||
// No respawn point was reached in this world: the saved one stays
|
||||
EXPECT_STREQ(saved->Attribute("lrx"), "1");
|
||||
EXPECT_STREQ(saved->Attribute("lrrw"), "0.5");
|
||||
// Not written by the server: left alone
|
||||
EXPECT_STREQ(saved->Attribute("stime"), "349");
|
||||
EXPECT_STREQ(saved->Attribute("lzcs"), "2233038349");
|
||||
|
||||
// The same day: the votes left are kept
|
||||
Entity reloaded(41, info);
|
||||
auto* const reloadedComponent = reloaded.AddComponent<CharacterComponent>(-1, &character, UNASSIGNED_SYSTEM_ADDRESS);
|
||||
doc.FirstChildElement("obj")->FirstChildElement("char")->SetAttribute("v", 2);
|
||||
reloadedComponent->LoadFromSaveData(doc);
|
||||
EXPECT_EQ(reloadedComponent->GetVotesLeft(), 2);
|
||||
reloadedComponent->SetGuild(77, u"Builders");
|
||||
reloadedComponent->WriteSaveData(doc);
|
||||
EXPECT_STREQ(saved->Attribute("v"), "2");
|
||||
EXPECT_STREQ(saved->Attribute("gid"), "77");
|
||||
EXPECT_STREQ(saved->Attribute("gn"), "Builders");
|
||||
}
|
||||
|
||||
// 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) {
|
||||
tinyxml2::XMLDocument doc;
|
||||
Parse(doc, R"(<obj v="1"><mf/><char acct="1" cc="123456789012" gm="0" ls="15700" ttip="4" time="54390"/></obj>)");
|
||||
|
||||
Character character(1, nullptr);
|
||||
Entity player(42, info);
|
||||
auto* const characterComponent = player.AddComponent<CharacterComponent>(-1, &character, UNASSIGNED_SYSTEM_ADDRESS);
|
||||
characterComponent->LoadFromSaveData(doc);
|
||||
EXPECT_EQ(characterComponent->GetVotesLeft(), 3);
|
||||
EXPECT_EQ(characterComponent->GetTooltipFlags(), 4u);
|
||||
EXPECT_EQ(characterComponent->GetUScore(), 15700);
|
||||
|
||||
characterComponent->WriteSaveData(doc);
|
||||
const auto* const saved = doc.FirstChildElement("obj")->FirstChildElement("char");
|
||||
EXPECT_STREQ(saved->Attribute("cc"), "123456789012");
|
||||
EXPECT_STREQ(saved->Attribute("acct"), "1");
|
||||
EXPECT_STREQ(saved->Attribute("ls"), "15700");
|
||||
EXPECT_STREQ(saved->Attribute("cm"), "9223372036854775807");
|
||||
EXPECT_STREQ(saved->Attribute("edit"), "0");
|
||||
EXPECT_STREQ(saved->Attribute("mldt"), "0");
|
||||
EXPECT_STREQ(saved->Attribute("gid"), "0");
|
||||
EXPECT_STREQ(saved->Attribute("gn"), "");
|
||||
EXPECT_STREQ(saved->Attribute("v"), "3");
|
||||
EXPECT_NE(saved->Attribute("vd"), nullptr);
|
||||
EXPECT_EQ(saved->Attribute("lrx"), nullptr); // no respawn point reached yet
|
||||
EXPECT_EQ(saved->Attribute("stime"), nullptr);
|
||||
}
|
||||
|
||||
// <skil sc>: the cooldown groups still running and the seconds left, as live wrote them (sc="17:15.6958;") and the
|
||||
// client reads them (split on ';' and ':').
|
||||
TEST_F(CharacterSaveFieldsTests, SkillCooldownsRoundTrip) {
|
||||
|
||||
Reference in New Issue
Block a user