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:
Aaron Kimbrell
2026-11-10 14:50:33 -06:00
parent 95e0746441
commit dc5f13cdbc
4 changed files with 125 additions and 0 deletions

View File

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