mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
Live wrote all three optional blocks of the character component in every player construction (9,726 LOT 1 constructions: gm_pvp_info with is_gm false and gm_level 0 for civilians, current_activity Some(None) on 9,620, social_info always Some with guild 0 and an empty name). DLU wrote each one only after something set its dirty flag, so civilians got none of them. Serializations are unchanged (still only when dirty). Writing the social block every time exposed two bugs in it: the guild ID was never initialized (now 0), and the guild name was written with sizeof(wchar_t) (4 on Linux) bits per character, reading past the end of the UTF-16 string; it is now written as 16-bit characters. Test: ReplicaConstructionTest.CharacterConstructionAlwaysWritesGmActivityAndSocialBlocks. Check in game: log in as a civilian and as a GM, with a second player watching; both see each other normally (name tags, GM tag/level for the GM, no PvP flag), and starting a quickbuild or pet taming still shows the activity to the other player. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1066 lines
36 KiB
C++
1066 lines
36 KiB
C++
#include "CharacterComponent.h"
|
|
#include "BitStream.h"
|
|
#include "tinyxml2.h"
|
|
#include "Game.h"
|
|
#include "Logger.h"
|
|
#include "GeneralUtils.h"
|
|
#include "dServer.h"
|
|
#include "dZoneManager.h"
|
|
#include "CDClientManager.h"
|
|
#include "InventoryComponent.h"
|
|
#include "ControllablePhysicsComponent.h"
|
|
#include "EntityManager.h"
|
|
#include "HavokVehiclePhysicsComponent.h"
|
|
#include "GameMessages.h"
|
|
#include "EffectsMessages.h"
|
|
#include "PlayerMessages.h"
|
|
#include "Item.h"
|
|
#include "Amf3.h"
|
|
#include "eGameMasterLevel.h"
|
|
#include "eGameActivity.h"
|
|
#include "User.h"
|
|
#include "EconomyLedger.h"
|
|
#include "Database.h"
|
|
#include "CDRewardCodesTable.h"
|
|
#include "CDObjectsTable.h"
|
|
#include "CDObjectSkillsTable.h"
|
|
#include "CDSkillBehaviorTable.h"
|
|
#include "CDBehaviorParameterTable.h"
|
|
#include "Behavior.h"
|
|
|
|
#include <set>
|
|
#include <unordered_map>
|
|
#include "Mail.h"
|
|
#include "ZoneInstanceManager.h"
|
|
#include "ClientPackets.h"
|
|
#include "MessageType/Game.h"
|
|
#include "PlayerMessages.h"
|
|
#include <ctime>
|
|
#include <cmath>
|
|
#include <ranges>
|
|
|
|
CharacterComponent::CharacterComponent(Entity* parent, const int32_t componentID, Character* character, const SystemAddress& systemAddress) : Component(parent, componentID) {
|
|
m_Character = character;
|
|
|
|
m_IsRacing = false;
|
|
m_IsGM = false;
|
|
m_IsLanding = false;
|
|
m_IsLEGOClubMember = true;
|
|
|
|
m_DirtyCurrentActivity = false;
|
|
m_DirtyGMInfo = false;
|
|
m_DirtySocialInfo = false;
|
|
|
|
m_PvpEnabled = false;
|
|
m_GMLevel = character->GetGMLevel();
|
|
|
|
m_EditorEnabled = false;
|
|
m_EditorLevel = m_GMLevel;
|
|
m_Reputation = 0;
|
|
|
|
m_CurrentActivity = eGameActivity::NONE;
|
|
m_CountryCode = 0;
|
|
m_LastUpdateTimestamp = std::time(nullptr);
|
|
m_SystemAddress = systemAddress;
|
|
|
|
RegisterMsg(&CharacterComponent::OnGetObjectReportInfo);
|
|
}
|
|
|
|
bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
|
|
|
auto& cmptType = reportInfo.info->PushDebug("Character");
|
|
|
|
cmptType.PushDebug<AMFIntValue>("Component ID") = GeneralUtils::ToUnderlying(ComponentType);
|
|
cmptType.PushDebug<AMFIntValue>("Character's account ID") = m_Character->GetParentUser()->GetAccountID();
|
|
cmptType.PushDebug<AMFBoolValue>("Last log out time") = m_Character->GetLastLogin();
|
|
cmptType.PushDebug<AMFDoubleValue>("Seconds played this session") = 0;
|
|
cmptType.PushDebug<AMFBoolValue>("Editor enabled") = false;
|
|
cmptType.PushDebug<AMFDoubleValue>("Total number of seconds played") = m_TotalTimePlayed;
|
|
cmptType.PushDebug<AMFStringValue>("Total currency") = std::to_string(m_Character->GetCoins());
|
|
cmptType.PushDebug<AMFStringValue>("Currency able to be picked up") = std::to_string(m_DroppedCoins);
|
|
cmptType.PushDebug<AMFStringValue>("Tooltip flags value") = "0";
|
|
auto& vl = cmptType.PushDebug("Visited Levels");
|
|
for (const auto zoneID : m_VisitedLevels) {
|
|
std::stringstream sstream;
|
|
sstream << "MapID: " << zoneID.GetMapID() << " CloneID: " << zoneID.GetCloneID();
|
|
vl.PushDebug(sstream.str());
|
|
}
|
|
|
|
// visited locations
|
|
cmptType.PushDebug<AMFBoolValue>("is a GM") = m_GMLevel > eGameMasterLevel::CIVILIAN;
|
|
cmptType.PushDebug<AMFBoolValue>("Has PVP flag turned on") = m_PvpEnabled;
|
|
cmptType.PushDebug<AMFIntValue>("GM Level") = GeneralUtils::ToUnderlying(m_GMLevel);
|
|
cmptType.PushDebug<AMFIntValue>("Editor level") = GeneralUtils::ToUnderlying(m_EditorLevel);
|
|
cmptType.PushDebug<AMFStringValue>("Guild ID") = "0";
|
|
cmptType.PushDebug<AMFStringValue>("Guild Name") = "";
|
|
cmptType.PushDebug<AMFDoubleValue>("Reputation") = m_Reputation;
|
|
cmptType.PushDebug<AMFIntValue>("Current Activity Type") = GeneralUtils::ToUnderlying(m_CurrentActivity);
|
|
cmptType.PushDebug<AMFDoubleValue>("Property Clone ID") = m_Character->GetPropertyCloneID();
|
|
|
|
auto& flagCmptType = reportInfo.info->PushDebug("Player Flag");
|
|
auto& allFlags = flagCmptType.PushDebug("All flags");
|
|
|
|
for (const auto& [id, flagChunk] : m_Character->GetPlayerFlags()) {
|
|
const auto base = id * 64;
|
|
auto flagChunkCopy = flagChunk;
|
|
for (int i = 0; i < 64; i++) {
|
|
if (static_cast<bool>(flagChunkCopy & 1)) {
|
|
const int32_t flagId = base + i;
|
|
std::stringstream stream;
|
|
stream << "Flag: " << flagId;
|
|
allFlags.PushDebug(stream.str());
|
|
}
|
|
flagChunkCopy >>= 1;
|
|
}
|
|
}
|
|
|
|
auto& sessionFlags = flagCmptType.PushDebug("Session Only Flags");
|
|
for (const auto flagId : m_Character->GetSessionFlags()) {
|
|
std::stringstream stream;
|
|
stream << "Flag: " << flagId;
|
|
sessionFlags.PushDebug(stream.str());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CharacterComponent::LandingAnimDisabled(int zoneID) {
|
|
switch (zoneID) {
|
|
case 0:
|
|
case 556:
|
|
case 1101:
|
|
case 1202:
|
|
case 1150:
|
|
case 1151:
|
|
case 1203:
|
|
case 1204:
|
|
case 1250:
|
|
case 1251:
|
|
case 1261:
|
|
case 1301:
|
|
case 1302:
|
|
case 1303:
|
|
case 1350:
|
|
case 1401:
|
|
case 1402:
|
|
case 1403:
|
|
case 1450:
|
|
case 1603:
|
|
case 2001:
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
CharacterComponent::~CharacterComponent() {
|
|
}
|
|
|
|
void CharacterComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
|
|
|
|
if (bIsInitialUpdate) {
|
|
if (!m_Character || !m_Character->GetParentUser()) return;
|
|
|
|
outBitStream.Write(m_ClaimCodes[0] != 0);
|
|
if (m_ClaimCodes[0] != 0) outBitStream.Write(m_ClaimCodes[0]);
|
|
outBitStream.Write(m_ClaimCodes[1] != 0);
|
|
if (m_ClaimCodes[1] != 0) outBitStream.Write(m_ClaimCodes[1]);
|
|
outBitStream.Write(m_ClaimCodes[2] != 0);
|
|
if (m_ClaimCodes[2] != 0) outBitStream.Write(m_ClaimCodes[2]);
|
|
outBitStream.Write(m_ClaimCodes[3] != 0);
|
|
if (m_ClaimCodes[3] != 0) outBitStream.Write(m_ClaimCodes[3]);
|
|
|
|
outBitStream.Write(m_Character->GetHairColor());
|
|
outBitStream.Write(m_Character->GetHairStyle());
|
|
outBitStream.Write<uint32_t>(0); //Default "head"
|
|
outBitStream.Write(m_Character->GetShirtColor());
|
|
outBitStream.Write(m_Character->GetPantsColor());
|
|
outBitStream.Write(m_Character->GetShirtStyle());
|
|
outBitStream.Write<uint32_t>(0); //Default "head color"
|
|
outBitStream.Write(m_Character->GetEyebrows());
|
|
outBitStream.Write(m_Character->GetEyes());
|
|
outBitStream.Write(m_Character->GetMouth());
|
|
outBitStream.Write<uint64_t>(m_Character->GetParentUser()->GetAccountID());
|
|
outBitStream.Write(m_Character->GetLastLogin()); //Last login
|
|
outBitStream.Write<uint64_t>(0); //"prop mod last display time"
|
|
outBitStream.Write<uint64_t>(m_Uscore); //u-score
|
|
outBitStream.Write0(); //Not free-to-play (disabled in DLU)
|
|
|
|
//Stats:
|
|
outBitStream.Write(m_CurrencyCollected);
|
|
outBitStream.Write(m_BricksCollected);
|
|
outBitStream.Write(m_SmashablesSmashed);
|
|
outBitStream.Write(m_QuickBuildsCompleted);
|
|
outBitStream.Write(m_EnemiesSmashed);
|
|
outBitStream.Write(m_RocketsUsed);
|
|
outBitStream.Write(m_MissionsCompleted);
|
|
outBitStream.Write(m_PetsTamed);
|
|
outBitStream.Write(m_ImaginationPowerUpsCollected);
|
|
outBitStream.Write(m_LifePowerUpsCollected);
|
|
outBitStream.Write(m_ArmorPowerUpsCollected);
|
|
outBitStream.Write(m_MetersTraveled);
|
|
outBitStream.Write(m_TimesSmashed);
|
|
outBitStream.Write(m_TotalDamageTaken);
|
|
outBitStream.Write(m_TotalDamageHealed);
|
|
outBitStream.Write(m_TotalArmorRepaired);
|
|
outBitStream.Write(m_TotalImaginationRestored);
|
|
outBitStream.Write(m_TotalImaginationUsed);
|
|
outBitStream.Write(m_DistanceDriven);
|
|
outBitStream.Write(m_TimeAirborneInCar);
|
|
outBitStream.Write(m_RacingImaginationPowerUpsCollected);
|
|
outBitStream.Write(m_RacingImaginationCratesSmashed);
|
|
outBitStream.Write(m_RacingCarBoostsActivated);
|
|
outBitStream.Write(m_RacingTimesWrecked);
|
|
outBitStream.Write(m_RacingSmashablesSmashed);
|
|
outBitStream.Write(m_RacesFinished);
|
|
outBitStream.Write(m_FirstPlaceRaceFinishes);
|
|
|
|
outBitStream.Write0();
|
|
outBitStream.Write(m_IsLanding);
|
|
if (m_IsLanding) {
|
|
outBitStream.Write<uint16_t>(m_LastRocketConfig.size());
|
|
for (uint16_t character : m_LastRocketConfig) {
|
|
outBitStream.Write(character);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Live wrote the GM, current-activity and social blocks on every player construction (civilians included),
|
|
// and afterwards only when they changed.
|
|
const bool writeGMInfo = bIsInitialUpdate || m_DirtyGMInfo;
|
|
outBitStream.Write(writeGMInfo);
|
|
if (writeGMInfo) {
|
|
outBitStream.Write(m_PvpEnabled);
|
|
outBitStream.Write(m_IsGM);
|
|
outBitStream.Write(m_GMLevel);
|
|
outBitStream.Write(m_EditorEnabled);
|
|
outBitStream.Write(m_EditorLevel);
|
|
}
|
|
|
|
const bool writeCurrentActivity = bIsInitialUpdate || m_DirtyCurrentActivity;
|
|
outBitStream.Write(writeCurrentActivity);
|
|
if (writeCurrentActivity) outBitStream.Write(m_CurrentActivity);
|
|
|
|
const bool writeSocialInfo = bIsInitialUpdate || m_DirtySocialInfo;
|
|
outBitStream.Write(writeSocialInfo);
|
|
if (writeSocialInfo) {
|
|
outBitStream.Write(m_GuildID);
|
|
outBitStream.Write<uint8_t>(m_GuildName.size());
|
|
for (const char16_t character : m_GuildName) outBitStream.Write<uint16_t>(character);
|
|
|
|
outBitStream.Write(m_IsLEGOClubMember);
|
|
outBitStream.Write(m_CountryCode);
|
|
}
|
|
}
|
|
|
|
bool CharacterComponent::GetPvpEnabled() const {
|
|
return m_PvpEnabled;
|
|
}
|
|
|
|
void CharacterComponent::SetPvpEnabled(const bool value) {
|
|
m_DirtyGMInfo = true;
|
|
|
|
m_PvpEnabled = value;
|
|
}
|
|
|
|
void CharacterComponent::SetGMLevel(eGameMasterLevel gmlevel) {
|
|
m_DirtyGMInfo = true;
|
|
if (gmlevel > eGameMasterLevel::CIVILIAN) m_IsGM = true;
|
|
else m_IsGM = false;
|
|
m_GMLevel = gmlevel;
|
|
}
|
|
|
|
void CharacterComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) {
|
|
|
|
auto* character = doc.FirstChildElement("obj")->FirstChildElement("char");
|
|
if (!character) {
|
|
LOG("Failed to find char tag while loading XML!");
|
|
return;
|
|
}
|
|
if (character->QueryAttribute("rpt", &m_Reputation) == tinyxml2::XML_NO_ATTRIBUTE) {
|
|
SetReputation(0);
|
|
}
|
|
|
|
auto* vl = character->FirstChildElement("vl");
|
|
if (vl) LoadVisitedLevelsXml(*vl);
|
|
character->QueryUnsigned64Attribute("co", &m_ClaimCodes[0]);
|
|
character->QueryUnsigned64Attribute("co1", &m_ClaimCodes[1]);
|
|
character->QueryUnsigned64Attribute("co2", &m_ClaimCodes[2]);
|
|
character->QueryUnsigned64Attribute("co3", &m_ClaimCodes[3]);
|
|
|
|
AwardClaimCodes();
|
|
|
|
character->QueryInt64Attribute("ls", &m_Uscore);
|
|
|
|
m_TooltipFlags = 0;
|
|
character->QueryUnsigned64Attribute("ttip", &m_TooltipFlags);
|
|
|
|
// Load the statistics
|
|
const auto* statisticsAttribute = character->FindAttribute("stt");
|
|
if (statisticsAttribute) {
|
|
InitializeStatisticsFromString(std::string(statisticsAttribute->Value()));
|
|
} else {
|
|
InitializeEmptyStatistics();
|
|
}
|
|
|
|
// Load the zone statistics
|
|
m_ZoneStatistics = {};
|
|
auto zoneStatistics = character->FirstChildElement("zs");
|
|
|
|
if (zoneStatistics) {
|
|
auto child = zoneStatistics->FirstChildElement();
|
|
while (child) {
|
|
ZoneStatistics statistics = {};
|
|
|
|
child->QueryUnsigned64Attribute("ac", &statistics.m_AchievementsCollected);
|
|
child->QueryInt64Attribute("bc", &statistics.m_BricksCollected);
|
|
child->QueryUnsigned64Attribute("cc", &statistics.m_CoinsCollected);
|
|
child->QueryUnsigned64Attribute("es", &statistics.m_EnemiesSmashed);
|
|
child->QueryUnsigned64Attribute("qbc", &statistics.m_QuickBuildsCompleted);
|
|
|
|
uint32_t mapID;
|
|
child->QueryAttribute("map", &mapID);
|
|
|
|
m_ZoneStatistics.insert({ static_cast<LWOMAPID>(mapID), statistics });
|
|
|
|
child = child->NextSiblingElement();
|
|
}
|
|
}
|
|
|
|
const tinyxml2::XMLAttribute* rocketConfig = character->FindAttribute("lcbp");
|
|
|
|
if (rocketConfig) {
|
|
m_LastRocketConfig = GeneralUtils::ASCIIToUTF16(rocketConfig->Value());
|
|
} else {
|
|
m_LastRocketConfig = u"";
|
|
}
|
|
|
|
//
|
|
// Begin custom attributes
|
|
//
|
|
|
|
// Load the last rocket item ID
|
|
const tinyxml2::XMLAttribute* lastRocketItemID = character->FindAttribute("lrid");
|
|
if (lastRocketItemID) {
|
|
m_LastRocketItemID = lastRocketItemID->Int64Value();
|
|
}
|
|
|
|
//
|
|
// End custom attributes
|
|
//
|
|
|
|
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
|
|
}
|
|
|
|
//Annoying guild bs:
|
|
const tinyxml2::XMLAttribute* guildName = character->FindAttribute("gn");
|
|
if (guildName) {
|
|
const char* gn = guildName->Value();
|
|
int64_t gid = 0;
|
|
character->QueryInt64Attribute("gid", &gid);
|
|
if (gid != 0) {
|
|
std::string guildname(gn);
|
|
m_GuildName = GeneralUtils::UTF8ToUTF16(guildname);
|
|
m_GuildID = gid;
|
|
m_DirtySocialInfo = true;
|
|
}
|
|
}
|
|
|
|
if (character->FindAttribute("time")) {
|
|
character->QueryUnsigned64Attribute("time", &m_TotalTimePlayed);
|
|
} else {
|
|
m_TotalTimePlayed = 0;
|
|
}
|
|
|
|
if (!m_Character) return;
|
|
|
|
//Check to see if we're landing:
|
|
if (m_Character->GetZoneID() != Game::server->GetZoneID()) {
|
|
m_IsLanding = true;
|
|
}
|
|
|
|
if (LandingAnimDisabled(m_Character->GetZoneID()) || LandingAnimDisabled(Game::server->GetZoneID()) || m_LastRocketConfig.empty()) {
|
|
m_IsLanding = false; //Don't make us land on VE/minigames lol
|
|
}
|
|
}
|
|
|
|
void CharacterComponent::UpdateXml(tinyxml2::XMLDocument& doc) {
|
|
tinyxml2::XMLElement* minifig = doc.FirstChildElement("obj")->FirstChildElement("mf");
|
|
if (!minifig) {
|
|
LOG("Failed to find mf tag while updating XML!");
|
|
return;
|
|
}
|
|
|
|
// write minifig information that might have been changed by commands
|
|
|
|
minifig->SetAttribute("es", m_Character->GetEyebrows());
|
|
minifig->SetAttribute("ess", m_Character->GetEyes());
|
|
minifig->SetAttribute("hc", m_Character->GetHairColor());
|
|
minifig->SetAttribute("hs", m_Character->GetHairStyle());
|
|
minifig->SetAttribute("l", m_Character->GetPantsColor());
|
|
minifig->SetAttribute("lh", m_Character->GetLeftHand());
|
|
minifig->SetAttribute("ms", m_Character->GetMouth());
|
|
minifig->SetAttribute("rh", m_Character->GetRightHand());
|
|
minifig->SetAttribute("t", m_Character->GetShirtColor());
|
|
|
|
// done with minifig
|
|
|
|
tinyxml2::XMLElement* character = doc.FirstChildElement("obj")->FirstChildElement("char");
|
|
if (!character) {
|
|
LOG("Failed to find char tag while updating XML!");
|
|
return;
|
|
}
|
|
|
|
auto* vl = character->FirstChildElement("vl");
|
|
if (!vl) vl = character->InsertNewChildElement("vl");
|
|
UpdateVisitedLevelsXml(*vl);
|
|
|
|
if (m_ClaimCodes[0] != 0) character->SetAttribute("co", m_ClaimCodes[0]);
|
|
if (m_ClaimCodes[1] != 0) character->SetAttribute("co1", m_ClaimCodes[1]);
|
|
if (m_ClaimCodes[2] != 0) character->SetAttribute("co2", m_ClaimCodes[2]);
|
|
if (m_ClaimCodes[3] != 0) character->SetAttribute("co3", m_ClaimCodes[3]);
|
|
|
|
character->SetAttribute("ls", m_Uscore);
|
|
// Live wrote ttip on every character, 0 when no tooltip was flagged
|
|
character->SetAttribute("ttip", m_TooltipFlags);
|
|
// Custom attribute to keep track of reputation.
|
|
character->SetAttribute("rpt", GetReputation());
|
|
character->SetAttribute("stt", StatisticsToString().c_str());
|
|
|
|
// Set the zone statistics of the form <zs><s/> ... <s/></zs>
|
|
auto zoneStatistics = character->FirstChildElement("zs");
|
|
if (!zoneStatistics) zoneStatistics = doc.NewElement("zs");
|
|
zoneStatistics->DeleteChildren();
|
|
|
|
for (auto pair : m_ZoneStatistics) {
|
|
auto zoneStatistic = doc.NewElement("s");
|
|
|
|
zoneStatistic->SetAttribute("map", pair.first);
|
|
zoneStatistic->SetAttribute("ac", pair.second.m_AchievementsCollected);
|
|
zoneStatistic->SetAttribute("bc", pair.second.m_BricksCollected);
|
|
zoneStatistic->SetAttribute("cc", pair.second.m_CoinsCollected);
|
|
zoneStatistic->SetAttribute("es", pair.second.m_EnemiesSmashed);
|
|
zoneStatistic->SetAttribute("qbc", pair.second.m_QuickBuildsCompleted);
|
|
|
|
zoneStatistics->LinkEndChild(zoneStatistic);
|
|
}
|
|
|
|
character->LinkEndChild(zoneStatistics);
|
|
|
|
if (!m_LastRocketConfig.empty()) {
|
|
std::string config = GeneralUtils::UTF16ToWTF8(m_LastRocketConfig);
|
|
character->SetAttribute("lcbp", config.c_str());
|
|
} else {
|
|
character->DeleteAttribute("lcbp");
|
|
}
|
|
|
|
//
|
|
// Begin custom attributes
|
|
//
|
|
|
|
// Store last rocket item ID
|
|
character->SetAttribute("lrid", m_LastRocketItemID);
|
|
|
|
//
|
|
// End custom attributes
|
|
//
|
|
|
|
auto newUpdateTimestamp = std::time(nullptr);
|
|
LOG("Time since last save: %d", newUpdateTimestamp - m_LastUpdateTimestamp);
|
|
|
|
m_TotalTimePlayed += newUpdateTimestamp - m_LastUpdateTimestamp;
|
|
character->SetAttribute("time", m_TotalTimePlayed);
|
|
|
|
m_LastUpdateTimestamp = newUpdateTimestamp;
|
|
}
|
|
|
|
void CharacterComponent::SetTooltipFlag(const int32_t tooltip, const bool flag) {
|
|
if (tooltip > 127) return;
|
|
// x86 64-bit shifts (__allshl) by 64 or more give 0; the shift count is the tooltip's low byte
|
|
const auto shift = static_cast<uint8_t>(tooltip);
|
|
const auto shifted = [shift](const uint64_t value) { return shift < 64 ? value << shift : 0; };
|
|
if (flag) m_TooltipFlags |= shifted(1);
|
|
else m_TooltipFlags &= shifted(~uint64_t{ 1 });
|
|
}
|
|
|
|
void CharacterComponent::SetLastRocketConfig(std::u16string config) {
|
|
m_IsLanding = !config.empty();
|
|
m_LastRocketConfig = config;
|
|
}
|
|
|
|
Item* CharacterComponent::GetRocket(Entity* player) {
|
|
Item* rocket = nullptr;
|
|
|
|
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
|
|
|
|
if (!inventoryComponent) return rocket;
|
|
|
|
// Select the rocket
|
|
if (!rocket) {
|
|
rocket = inventoryComponent->FindItemById(GetLastRocketItemID());
|
|
}
|
|
|
|
if (!rocket) {
|
|
rocket = inventoryComponent->FindItemByLot(6416);
|
|
}
|
|
|
|
if (!rocket) {
|
|
LOG("Unable to find rocket to equip!");
|
|
return rocket;
|
|
}
|
|
return rocket;
|
|
}
|
|
|
|
Item* CharacterComponent::RocketEquip(Entity* player) {
|
|
Item* rocket = GetRocket(player);
|
|
if (!rocket) return rocket;
|
|
|
|
// build and define the rocket config
|
|
for (const auto& data : rocket->GetConfig().values | std::views::values) {
|
|
if (data->GetKey() == u"assemblyPartLOTs") {
|
|
std::string newRocketStr = data->GetValueAsString() + ";";
|
|
GeneralUtils::ReplaceInString(newRocketStr, "+", ";");
|
|
SetLastRocketConfig(GeneralUtils::ASCIIToUTF16(newRocketStr));
|
|
}
|
|
}
|
|
|
|
// Store the last used rocket item's ID
|
|
SetLastRocketItemID(rocket->GetId());
|
|
// carry the rocket
|
|
rocket->Equip(true);
|
|
return rocket;
|
|
}
|
|
|
|
void CharacterComponent::RocketUnEquip(Entity* player) {
|
|
Item* rocket = GetRocket(player);
|
|
if (!rocket) return;
|
|
// We don't want to carry it anymore
|
|
rocket->UnEquip();
|
|
}
|
|
|
|
void CharacterComponent::TrackMissionCompletion(bool isAchievement) {
|
|
// Achievements are tracked separately for the zone
|
|
if (isAchievement) {
|
|
const auto mapID = Game::zoneManager->GetZoneID().GetMapID();
|
|
GetZoneStatisticsForMap(mapID).m_AchievementsCollected++;
|
|
} else {
|
|
UpdatePlayerStatistic(MissionsCompleted);
|
|
}
|
|
}
|
|
|
|
void CharacterComponent::SendZoneStatisticIncrement(const std::u16string& statName) const {
|
|
GameMessages::ModifyPlayerZoneStatistic statistic;
|
|
statistic.target = m_Parent->GetObjectID();
|
|
statistic.bSet = false;
|
|
statistic.statName = statName;
|
|
statistic.statValue = 1;
|
|
statistic.zoneID = Game::zoneManager->GetZoneID().GetMapID();
|
|
statistic.SendToClient(m_Parent->GetSystemAddress());
|
|
}
|
|
|
|
void CharacterComponent::TrackLOTCollection(LOT lot) {
|
|
const auto statistic = GetPowerUpStatistic(lot);
|
|
if (statistic) UpdatePlayerStatistic(statistic.value());
|
|
}
|
|
|
|
std::optional<StatisticID> CharacterComponent::GetPowerUpStatistic(const LOT lot) {
|
|
static std::unordered_map<LOT, std::optional<StatisticID>> cache;
|
|
const auto cached = cache.find(lot);
|
|
if (cached != cache.end()) return cached->second;
|
|
|
|
std::optional<StatisticID> statistic;
|
|
|
|
// A power-up counts towards the statistic of what its pickup skill restores
|
|
// (e.g. Imagination Powerup 2 points: skill 129 -> Imagination behavior)
|
|
const auto& object = CDClientManager::GetTable<CDObjectsTable>()->GetByID(lot);
|
|
if (object.id != 0 && object.type == "Powerup") {
|
|
auto* const skillTable = CDClientManager::GetTable<CDSkillBehaviorTable>();
|
|
auto* const parameterTable = CDClientManager::GetTable<CDBehaviorParameterTable>();
|
|
|
|
std::vector<uint32_t> toVisit;
|
|
for (const auto& skill : CDClientManager::GetTable<CDObjectSkillsTable>()->Get(lot)) {
|
|
toVisit.push_back(skillTable->GetSkillByID(skill.skillID).behaviorID);
|
|
}
|
|
|
|
std::set<uint32_t> visited;
|
|
while (!toVisit.empty() && !statistic) {
|
|
const auto behaviorID = toVisit.back();
|
|
toVisit.pop_back();
|
|
if (behaviorID == 0 || !visited.insert(behaviorID).second) continue;
|
|
|
|
switch (Behavior::GetBehaviorTemplate(behaviorID)) {
|
|
case BehaviorTemplate::IMAGINATION: statistic = ImaginationPowerUpsCollected; break;
|
|
case BehaviorTemplate::REPAIR_ARMOR: statistic = ArmorPowerUpsCollected; break;
|
|
case BehaviorTemplate::HEAL: statistic = LifePowerUpsCollected; break;
|
|
default:
|
|
// Parameters that are behaviors (e.g. "action", "behavior 1") lead further down the tree
|
|
for (const auto& [name, value] : parameterTable->GetParametersByBehaviorID(behaviorID)) {
|
|
if (value > 0.0f && (name == "action" || name.starts_with("behavior "))) toVisit.push_back(static_cast<uint32_t>(value));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
cache.emplace(lot, statistic);
|
|
return statistic;
|
|
}
|
|
|
|
void CharacterComponent::TrackHealthDelta(int32_t health) {
|
|
// Live sent no TotalDamageHealed or TotalDamageTaken of 0
|
|
if (health > 0) {
|
|
UpdatePlayerStatistic(TotalDamageHealed, health);
|
|
} else if (health < 0) {
|
|
UpdatePlayerStatistic(TotalDamageTaken, -health);
|
|
}
|
|
}
|
|
|
|
void CharacterComponent::TrackImaginationDelta(int32_t imagination) {
|
|
if (imagination >= 0) {
|
|
UpdatePlayerStatistic(TotalImaginationRestored, imagination);
|
|
} else {
|
|
UpdatePlayerStatistic(TotalImaginationUsed, -imagination);
|
|
}
|
|
}
|
|
|
|
void CharacterComponent::TrackArmorDelta(int32_t armor) {
|
|
if (armor >= 0) {
|
|
UpdatePlayerStatistic(TotalArmorRepaired, armor);
|
|
}
|
|
}
|
|
|
|
void CharacterComponent::TrackItemsAdded(eInventoryType inventoryType, uint32_t count) {
|
|
if (inventoryType == eInventoryType::BRICKS && count > 0) UpdatePlayerStatistic(BricksCollected, count);
|
|
}
|
|
|
|
void CharacterComponent::TrackQuickBuildComplete() {
|
|
UpdatePlayerStatistic(QuickBuildsCompleted);
|
|
|
|
const auto mapID = Game::zoneManager->GetZoneID().GetMapID();
|
|
GetZoneStatisticsForMap(mapID).m_QuickBuildsCompleted++;
|
|
}
|
|
|
|
void CharacterComponent::TrackRaceCompleted(bool won) {
|
|
UpdatePlayerStatistic(RacesFinished);
|
|
if (won)
|
|
UpdatePlayerStatistic(FirstPlaceRaceFinishes);
|
|
}
|
|
|
|
void CharacterComponent::TrackPositionUpdate(const NiPoint3& newPosition) {
|
|
const auto distance = NiPoint3::Distance(newPosition, m_Parent->GetPosition());
|
|
if (!std::isfinite(distance)) return;
|
|
|
|
// Whole meters count at once; the parts of a meter add up
|
|
auto& remainder = m_IsRacing ? m_DrivenRemainder : m_MetersRemainder;
|
|
remainder += distance;
|
|
const auto meters = static_cast<uint64_t>(remainder);
|
|
remainder -= static_cast<float>(meters);
|
|
|
|
if (m_IsRacing) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (m_LastDrivenUpdate != std::chrono::steady_clock::time_point{}) m_DrivenSinceSend += std::chrono::duration<float>(now - m_LastDrivenUpdate).count();
|
|
m_LastDrivenUpdate = now;
|
|
if (meters > 0) UpdatePlayerStatistic(DistanceDriven, meters);
|
|
m_UnsentDriven += meters;
|
|
if (m_DrivenSinceSend >= DISTANCE_DRIVEN_INTERVAL) {
|
|
SendPlayerStatistic(DistanceDriven, m_UnsentDriven);
|
|
m_UnsentDriven = 0;
|
|
m_DrivenSinceSend = 0.0f;
|
|
}
|
|
} else {
|
|
if (meters > 0) UpdatePlayerStatistic(MetersTraveled, meters);
|
|
m_UnsentMeters += meters;
|
|
if (m_UnsentMeters >= METERS_TRAVELED_BATCH) {
|
|
SendPlayerStatistic(MetersTraveled, m_UnsentMeters);
|
|
m_UnsentMeters = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CharacterComponent::FlushMovementStatistics() {
|
|
if (m_UnsentDriven > 0) SendPlayerStatistic(DistanceDriven, m_UnsentDriven);
|
|
SendPlayerStatistic(MetersTraveled, m_UnsentMeters);
|
|
m_UnsentDriven = 0;
|
|
m_UnsentMeters = 0;
|
|
m_DrivenSinceSend = 0.0f;
|
|
}
|
|
|
|
void CharacterComponent::SendPlayerStatistic(StatisticID updateID, uint64_t updateValue) const {
|
|
if (m_SystemAddress == UNASSIGNED_SYSTEM_ADDRESS) return;
|
|
GameMessages::UpdatePlayerStatistic statistic;
|
|
statistic.target = m_Parent->GetObjectID();
|
|
statistic.updateID = static_cast<int32_t>(updateID);
|
|
statistic.updateValue = static_cast<int64_t>(updateValue);
|
|
statistic.SendToClient(m_SystemAddress);
|
|
}
|
|
|
|
void CharacterComponent::HandleZoneStatisticsUpdate(LWOMAPID zoneID, const std::u16string& name, int32_t value) {
|
|
auto zoneStatistics = &GetZoneStatisticsForMap(zoneID);
|
|
|
|
// The client's per-zone counts; the passport totals are the server's (UpdatePlayerStatistic), so they aren't
|
|
// counted twice
|
|
if (name == u"BricksCollected") {
|
|
zoneStatistics->m_BricksCollected += value;
|
|
} else if (name == u"CoinsCollected") {
|
|
zoneStatistics->m_CoinsCollected += value;
|
|
} else if (name == u"EnemiesSmashed") {
|
|
zoneStatistics->m_EnemiesSmashed += value;
|
|
}
|
|
}
|
|
|
|
void CharacterComponent::UpdatePlayerStatistic(StatisticID updateID, uint64_t updateValue, bool fromClient) {
|
|
if (!fromClient) EconomyLedger::RecordStat(m_Parent, updateID, updateValue);
|
|
|
|
switch (updateID) {
|
|
case CurrencyCollected:
|
|
m_CurrencyCollected += updateValue;
|
|
break;
|
|
case BricksCollected:
|
|
m_BricksCollected += updateValue;
|
|
break;
|
|
case SmashablesSmashed:
|
|
m_SmashablesSmashed += updateValue;
|
|
break;
|
|
case QuickBuildsCompleted:
|
|
m_QuickBuildsCompleted += updateValue;
|
|
break;
|
|
case EnemiesSmashed:
|
|
m_EnemiesSmashed += updateValue;
|
|
break;
|
|
case RocketsUsed:
|
|
m_RocketsUsed += updateValue;
|
|
break;
|
|
case MissionsCompleted:
|
|
m_MissionsCompleted += updateValue;
|
|
break;
|
|
case PetsTamed:
|
|
m_PetsTamed += updateValue;
|
|
break;
|
|
case ImaginationPowerUpsCollected:
|
|
m_ImaginationPowerUpsCollected += updateValue;
|
|
break;
|
|
case LifePowerUpsCollected:
|
|
m_LifePowerUpsCollected += updateValue;
|
|
break;
|
|
case ArmorPowerUpsCollected:
|
|
m_ArmorPowerUpsCollected += updateValue;
|
|
break;
|
|
case MetersTraveled:
|
|
m_MetersTraveled += updateValue;
|
|
break;
|
|
case TimesSmashed:
|
|
m_TimesSmashed += updateValue;
|
|
break;
|
|
case TotalDamageTaken:
|
|
m_TotalDamageTaken += updateValue;
|
|
break;
|
|
case TotalDamageHealed:
|
|
m_TotalDamageHealed += updateValue;
|
|
break;
|
|
case TotalArmorRepaired:
|
|
m_TotalArmorRepaired += updateValue;
|
|
break;
|
|
case TotalImaginationRestored:
|
|
m_TotalImaginationRestored += updateValue;
|
|
break;
|
|
case TotalImaginationUsed:
|
|
m_TotalImaginationUsed += updateValue;
|
|
break;
|
|
case DistanceDriven:
|
|
m_DistanceDriven += updateValue;
|
|
break;
|
|
case TimeAirborneInCar:
|
|
m_TimeAirborneInCar += updateValue;
|
|
break;
|
|
case RacingImaginationPowerUpsCollected:
|
|
m_RacingImaginationPowerUpsCollected += updateValue;
|
|
break;
|
|
case RacingImaginationCratesSmashed:
|
|
m_RacingImaginationCratesSmashed += updateValue;
|
|
break;
|
|
case RacingCarBoostsActivated:
|
|
m_RacingCarBoostsActivated += updateValue;
|
|
break;
|
|
case RacingTimesWrecked:
|
|
m_RacingTimesWrecked += updateValue;
|
|
break;
|
|
case RacingSmashablesSmashed:
|
|
m_RacingSmashablesSmashed += updateValue;
|
|
break;
|
|
case RacesFinished:
|
|
m_RacesFinished += updateValue;
|
|
break;
|
|
case FirstPlaceRaceFinishes:
|
|
m_FirstPlaceRaceFinishes += updateValue;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Live told the client at once; the meters go out in batches (TrackPositionUpdate)
|
|
if (!fromClient && updateID != MetersTraveled && updateID != DistanceDriven) SendPlayerStatistic(updateID, updateValue);
|
|
}
|
|
|
|
void CharacterComponent::InitializeStatisticsFromString(const std::string& statisticsString) {
|
|
auto split = GeneralUtils::SplitString(statisticsString, ';');
|
|
|
|
m_CurrencyCollected = GetStatisticFromSplit(split, 0);
|
|
m_BricksCollected = GetStatisticFromSplit(split, 1);
|
|
m_SmashablesSmashed = GetStatisticFromSplit(split, 2);
|
|
m_QuickBuildsCompleted = GetStatisticFromSplit(split, 3);
|
|
m_EnemiesSmashed = GetStatisticFromSplit(split, 4);
|
|
m_RocketsUsed = GetStatisticFromSplit(split, 5);
|
|
m_MissionsCompleted = GetStatisticFromSplit(split, 6);
|
|
m_PetsTamed = GetStatisticFromSplit(split, 7);
|
|
m_ImaginationPowerUpsCollected = GetStatisticFromSplit(split, 8);
|
|
m_LifePowerUpsCollected = GetStatisticFromSplit(split, 9);
|
|
m_ArmorPowerUpsCollected = GetStatisticFromSplit(split, 10);
|
|
m_MetersTraveled = GetStatisticFromSplit(split, 11);
|
|
m_TimesSmashed = GetStatisticFromSplit(split, 12);
|
|
m_TotalDamageTaken = GetStatisticFromSplit(split, 13);
|
|
m_TotalDamageHealed = GetStatisticFromSplit(split, 14);
|
|
m_TotalArmorRepaired = GetStatisticFromSplit(split, 15);
|
|
m_TotalImaginationRestored = GetStatisticFromSplit(split, 16);
|
|
m_TotalImaginationUsed = GetStatisticFromSplit(split, 17);
|
|
m_DistanceDriven = GetStatisticFromSplit(split, 18);
|
|
m_TimeAirborneInCar = GetStatisticFromSplit(split, 19); // WONTFIX
|
|
m_RacingImaginationPowerUpsCollected = GetStatisticFromSplit(split, 20);
|
|
m_RacingImaginationCratesSmashed = GetStatisticFromSplit(split, 21);
|
|
m_RacingCarBoostsActivated = GetStatisticFromSplit(split, 22);
|
|
m_RacingTimesWrecked = GetStatisticFromSplit(split, 23);
|
|
m_RacingSmashablesSmashed = GetStatisticFromSplit(split, 24);
|
|
m_RacesFinished = GetStatisticFromSplit(split, 25);
|
|
m_FirstPlaceRaceFinishes = GetStatisticFromSplit(split, 26);
|
|
}
|
|
|
|
void CharacterComponent::InitializeEmptyStatistics() {
|
|
m_CurrencyCollected = 0;
|
|
m_BricksCollected = 0;
|
|
m_SmashablesSmashed = 0;
|
|
m_QuickBuildsCompleted = 0;
|
|
m_EnemiesSmashed = 0;
|
|
m_RocketsUsed = 0;
|
|
m_MissionsCompleted = 0;
|
|
m_PetsTamed = 0;
|
|
m_ImaginationPowerUpsCollected = 0;
|
|
m_LifePowerUpsCollected = 0;
|
|
m_ArmorPowerUpsCollected = 0;
|
|
m_MetersTraveled = 0;
|
|
m_TimesSmashed = 0;
|
|
m_TotalDamageTaken = 0;
|
|
m_TotalDamageHealed = 0;
|
|
m_TotalArmorRepaired = 0;
|
|
m_TotalImaginationRestored = 0;
|
|
m_TotalImaginationUsed = 0;
|
|
m_DistanceDriven = 0;
|
|
m_TimeAirborneInCar = 0;
|
|
m_RacingImaginationPowerUpsCollected = 0;
|
|
m_RacingImaginationCratesSmashed = 0;
|
|
m_RacingCarBoostsActivated = 0;
|
|
m_RacingTimesWrecked = 0;
|
|
m_RacingSmashablesSmashed = 0;
|
|
m_RacesFinished = 0;
|
|
m_FirstPlaceRaceFinishes = 0;
|
|
}
|
|
|
|
std::string CharacterComponent::StatisticsToString() const {
|
|
std::stringstream result;
|
|
result << std::to_string(m_CurrencyCollected) << ';'
|
|
<< std::to_string(m_BricksCollected) << ';'
|
|
<< std::to_string(m_SmashablesSmashed) << ';'
|
|
<< std::to_string(m_QuickBuildsCompleted) << ';'
|
|
<< std::to_string(m_EnemiesSmashed) << ';'
|
|
<< std::to_string(m_RocketsUsed) << ';'
|
|
<< std::to_string(m_MissionsCompleted) << ';'
|
|
<< std::to_string(m_PetsTamed) << ';'
|
|
<< std::to_string(m_ImaginationPowerUpsCollected) << ';'
|
|
<< std::to_string(m_LifePowerUpsCollected) << ';'
|
|
<< std::to_string(m_ArmorPowerUpsCollected) << ';'
|
|
<< std::to_string(m_MetersTraveled) << ';'
|
|
<< std::to_string(m_TimesSmashed) << ';'
|
|
<< std::to_string(m_TotalDamageTaken) << ';'
|
|
<< std::to_string(m_TotalDamageHealed) << ';'
|
|
<< std::to_string(m_TotalArmorRepaired) << ';'
|
|
<< std::to_string(m_TotalImaginationRestored) << ';'
|
|
<< std::to_string(m_TotalImaginationUsed) << ';'
|
|
<< std::to_string(m_DistanceDriven) << ';'
|
|
<< std::to_string(m_TimeAirborneInCar) << ';'
|
|
<< std::to_string(m_RacingImaginationPowerUpsCollected) << ';'
|
|
<< std::to_string(m_RacingImaginationCratesSmashed) << ';'
|
|
<< std::to_string(m_RacingCarBoostsActivated) << ';'
|
|
<< std::to_string(m_RacingTimesWrecked) << ';'
|
|
<< std::to_string(m_RacingSmashablesSmashed) << ';'
|
|
<< std::to_string(m_RacesFinished) << ';'
|
|
<< std::to_string(m_FirstPlaceRaceFinishes) << ';';
|
|
|
|
return result.str();
|
|
}
|
|
|
|
uint64_t CharacterComponent::GetStatisticFromSplit(const std::vector<std::string>& split, const uint32_t index) {
|
|
uint64_t toReturn = 0;
|
|
if (index < split.size()) {
|
|
const auto parsed = GeneralUtils::TryParse<uint64_t>(split[index]);
|
|
if (parsed) toReturn = *parsed;
|
|
}
|
|
|
|
return toReturn;
|
|
}
|
|
|
|
ZoneStatistics& CharacterComponent::GetZoneStatisticsForMap(LWOMAPID mapID) {
|
|
auto stats = m_ZoneStatistics.find(mapID);
|
|
if (stats == m_ZoneStatistics.end())
|
|
m_ZoneStatistics.insert({ mapID, {0, 0, 0, 0, 0 } });
|
|
return m_ZoneStatistics.at(mapID);
|
|
}
|
|
|
|
void CharacterComponent::AddVentureVisionEffect(std::string ventureVisionType) {
|
|
const auto ventureVisionTypeIterator = m_ActiveVentureVisionEffects.find(ventureVisionType);
|
|
|
|
if (ventureVisionTypeIterator != m_ActiveVentureVisionEffects.end()) {
|
|
ventureVisionTypeIterator->second = ++ventureVisionTypeIterator->second;
|
|
} else {
|
|
// If the effect it not found, insert it into the active effects.
|
|
m_ActiveVentureVisionEffects.insert(std::make_pair(ventureVisionType, 1U));
|
|
}
|
|
|
|
UpdateClientMinimap(true, ventureVisionType);
|
|
}
|
|
|
|
void CharacterComponent::RemoveVentureVisionEffect(std::string ventureVisionType) {
|
|
const auto ventureVisionTypeIterator = m_ActiveVentureVisionEffects.find(ventureVisionType);
|
|
|
|
if (ventureVisionTypeIterator != m_ActiveVentureVisionEffects.end()) {
|
|
ventureVisionTypeIterator->second = --ventureVisionTypeIterator->second;
|
|
UpdateClientMinimap(ventureVisionTypeIterator->second != 0U, ventureVisionType);
|
|
}
|
|
}
|
|
|
|
void CharacterComponent::UpdateClientMinimap(bool showFaction, std::string ventureVisionType) const {
|
|
if (!m_Parent) return;
|
|
AMFArrayValue arrayToSend;
|
|
arrayToSend.Insert(ventureVisionType, showFaction);
|
|
GameMessages::UIMessageServerToSingleClient uiMessage;
|
|
uiMessage.target = m_Parent->GetObjectID();
|
|
uiMessage.strMessageName = "SetFactionVisibility";
|
|
uiMessage.args = std::move(arrayToSend);
|
|
uiMessage.SendToClient(m_Parent ? m_Parent->GetSystemAddress() : UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
void CharacterComponent::AwardClaimCodes() {
|
|
if (!m_Parent || !m_Parent->GetCharacter()) return;
|
|
auto* user = m_Parent->GetCharacter()->GetParentUser();
|
|
if (!user) return;
|
|
|
|
auto rewardCodes = Database::Get()->GetRewardCodesByAccountID(user->GetAccountID());
|
|
if (rewardCodes.empty()) return;
|
|
|
|
auto* cdrewardCodes = CDClientManager::GetTable<CDRewardCodesTable>();
|
|
for (auto const rewardCode : rewardCodes) {
|
|
LOG_DEBUG("Processing RewardCode %i", rewardCode);
|
|
const uint32_t rewardCodeIndex = rewardCode >> 6;
|
|
const uint32_t bitIndex = rewardCode % 64;
|
|
if (GeneralUtils::CheckBit(m_ClaimCodes[rewardCodeIndex], bitIndex)) continue;
|
|
m_ClaimCodes[rewardCodeIndex] = GeneralUtils::SetBit(m_ClaimCodes[rewardCodeIndex], bitIndex);
|
|
|
|
// Don't send it on this one since it's default and the mail doesn't make sense
|
|
if (rewardCode == 30) continue;
|
|
|
|
auto attachmentLOT = cdrewardCodes->GetAttachmentLOT(rewardCode);
|
|
std::ostringstream subject;
|
|
subject << "%[RewardCodes_" << rewardCode << "_subjectText]";
|
|
std::ostringstream body;
|
|
body << "%[RewardCodes_" << rewardCode << "_bodyText]";
|
|
Mail::SendMail(m_Parent, subject.str(), body.str(), attachmentLOT, 1);
|
|
}
|
|
}
|
|
|
|
void CharacterComponent::SendToZone(LWOMAPID zoneId, LWOCLONEID cloneId) const {
|
|
const auto objid = m_Parent->GetObjectID();
|
|
|
|
ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, zoneId, cloneId, false, [objid](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) {
|
|
auto* entity = Game::entityManager->GetEntity(objid);
|
|
|
|
if (!entity) return;
|
|
|
|
const auto sysAddr = entity->GetSystemAddress();
|
|
|
|
auto* character = entity->GetCharacter();
|
|
auto* characterComponent = entity->GetComponent<CharacterComponent>();
|
|
|
|
if (character && characterComponent) {
|
|
character->SetZoneID(zoneID);
|
|
character->SetZoneInstance(zoneInstance);
|
|
character->SetZoneClone(zoneClone);
|
|
|
|
characterComponent->SetLastRocketConfig(u"");
|
|
characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone));
|
|
|
|
character->SaveXMLToDatabase();
|
|
}
|
|
|
|
ClientPackets::TransferToWorld transfer;
|
|
transfer.serverIP = LUString(serverIP);
|
|
transfer.serverPort = serverPort;
|
|
transfer.mythranShift = mythranShift;
|
|
transfer.Send(sysAddr);
|
|
|
|
Game::entityManager->DestructEntity(entity);
|
|
});
|
|
}
|
|
|
|
const SystemAddress& CharacterComponent::GetSystemAddress() const {
|
|
return m_SystemAddress;
|
|
}
|
|
|
|
void CharacterComponent::SetRespawnPos(const NiPoint3& position) {
|
|
if (!m_Character) return;
|
|
|
|
m_respawnPos = position;
|
|
|
|
m_Character->SetRespawnPoint(Game::zoneManager->GetZone()->GetWorldID(), position);
|
|
|
|
}
|
|
|
|
void CharacterComponent::SetRespawnRot(const NiQuaternion& rotation) {
|
|
m_respawnRot = rotation;
|
|
}
|
|
|
|
void CharacterComponent::AddVisitedLevel(const LWOZONEID zoneID) {
|
|
LWOZONEID toInsert(zoneID.GetMapID(), LWOINSTANCEID_INVALID, zoneID.GetCloneID());
|
|
m_VisitedLevels.insert(toInsert);
|
|
}
|
|
|
|
void CharacterComponent::UpdateVisitedLevelsXml(tinyxml2::XMLElement& vl) {
|
|
vl.DeleteChildren();
|
|
// <vl>
|
|
for (const auto zoneID : m_VisitedLevels) {
|
|
// <l id=\"1100\" cid=\"0\"/>
|
|
auto* l = vl.InsertNewChildElement("l");
|
|
l->SetAttribute("id", zoneID.GetMapID());
|
|
l->SetAttribute("cid", zoneID.GetCloneID());
|
|
}
|
|
// </vl>
|
|
}
|
|
|
|
void CharacterComponent::LoadVisitedLevelsXml(const tinyxml2::XMLElement& vl) {
|
|
// <vl>
|
|
for (const auto* l = vl.FirstChildElement("l"); l != nullptr; l = l->NextSiblingElement("l")) {
|
|
// <l id=\"1100\" cid=\"0\"/>
|
|
LWOZONEID toInsert(l->IntAttribute("id"), LWOINSTANCEID_INVALID, l->IntAttribute("cid"));
|
|
m_VisitedLevels.insert(toInsert);
|
|
}
|
|
// </vl>
|
|
}
|
|
|
|
void CharacterComponent::SetUScore(int64_t uscore, uint32_t source) {
|
|
if (const auto* character = m_Parent->GetCharacter()) EconomyLedger::RecordUScore(character->GetID(), uscore - m_Uscore, source);
|
|
m_Uscore = uscore;
|
|
}
|