feat: debug features and implement ObjectDebugger (#1846)

Move the -s and base features of inspect to the object debugger (this file is present in an unmodified, live client)
This commit is contained in:
David Markowitz
2025-07-19 03:11:32 -07:00
committed by GitHub
parent 71f708f1b5
commit 6b52cf67a0
10 changed files with 96 additions and 58 deletions

View File

@@ -97,6 +97,8 @@
#include "CDSkillBehaviorTable.h"
#include "CDZoneTableTable.h"
#include "StringifiedEnum.h"
#include <ranges>
Observable<Entity*, const PositionUpdate&> Entity::OnPlayerPositionUpdate;
@@ -187,6 +189,7 @@ Entity::~Entity() {
}
void Entity::Initialize() {
RegisterMsg(MessageType::Game::REQUEST_SERVER_OBJECT_INFO, this, &Entity::MsgRequestServerObjectInfo);
/**
* Setup trigger
*/
@@ -2209,3 +2212,38 @@ bool Entity::HandleMsg(GameMessages::GameMsg& msg) const {
void Entity::RegisterMsg(const MessageType::Game msgId, std::function<bool(GameMessages::GameMsg&)> handler) {
m_MsgHandlers.emplace(msgId, handler);
}
bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
auto& requestInfo = static_cast<GameMessages::RequestServerObjectInfo&>(msg);
AMFArrayValue response;
response.Insert("visible", true);
response.Insert("objectID", std::to_string(m_ObjectID));
response.Insert("serverInfo", true);
GameMessages::GetObjectReportInfo info{};
info.info = response.InsertArray("data");
auto& objectInfo = info.info->PushDebug("Object Details");
auto* table = CDClientManager::GetTable<CDObjectsTable>();
const auto& objTableInfo = table->GetByID(GetLOT());
objectInfo.PushDebug<AMFStringValue>("Name") = objTableInfo.name;
objectInfo.PushDebug<AMFIntValue>("Template ID(LOT)") = GetLOT();
objectInfo.PushDebug<AMFStringValue>("Object ID") = std::to_string(GetObjectID());
objectInfo.PushDebug<AMFStringValue>("Spawner's Object ID") = std::to_string(GetSpawnerID());
auto& componentDetails = objectInfo.PushDebug("Component Information");
for (const auto [id, component] : m_Components) {
componentDetails.PushDebug<AMFStringValue>(StringifiedEnum::ToString(id)) = "";
}
auto& configData = objectInfo.PushDebug("Config Data");
for (const auto config : m_Settings) {
configData.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(config->GetKey())) = config->GetValueAsString();
}
HandleMsg(info);
auto* targetForReport = Game::entityManager->GetEntity(requestInfo.targetForReport);
if (targetForReport) GameMessages::SendUIMessageServerToSingleClient("ToggleObjectDebugger", response, targetForReport->GetSystemAddress());
return true;
}

View File

@@ -175,6 +175,8 @@ public:
void AddComponent(eReplicaComponentType componentId, Component* component);
bool MsgRequestServerObjectInfo(GameMessages::GameMsg& msg);
// This is expceted to never return nullptr, an assert checks this.
CppScripts::Script* const GetScript() const;
@@ -336,6 +338,10 @@ public:
bool HandleMsg(GameMessages::GameMsg& msg) const;
void RegisterMsg(const MessageType::Game msgId, auto* self, const auto handler) {
RegisterMsg(msgId, std::bind(handler, self, std::placeholders::_1));
}
/**
* @brief The observable for player entity position updates.
*/

View File

@@ -49,19 +49,13 @@ CharacterComponent::CharacterComponent(Entity* parent, Character* character, con
m_LastUpdateTimestamp = std::time(nullptr);
m_SystemAddress = systemAddress;
RegisterMsg(MessageType::Game::REQUEST_SERVER_OBJECT_INFO, this, &CharacterComponent::OnRequestServerObjectInfo);
RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &CharacterComponent::OnGetObjectReportInfo);
}
bool CharacterComponent::OnRequestServerObjectInfo(GameMessages::GameMsg& msg) {
auto& request = static_cast<GameMessages::RequestServerObjectInfo&>(msg);
AMFArrayValue response;
bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportInfo = static_cast<GameMessages::GetObjectReportInfo&>(msg);
response.Insert("visible", true);
response.Insert("objectID", std::to_string(request.targetForReport));
response.Insert("serverInfo", true);
auto& data = *response.InsertArray("data");
auto& cmptType = data.PushDebug("Character");
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();
@@ -72,6 +66,13 @@ bool CharacterComponent::OnRequestServerObjectInfo(GameMessages::GameMsg& msg) {
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<AMFStringValue>(sstream.str()) = "";
}
// visited locations
cmptType.PushDebug<AMFBoolValue>("is a GM") = m_GMLevel > eGameMasterLevel::CIVILIAN;
cmptType.PushDebug<AMFBoolValue>("Has PVP flag turned on") = m_PvpEnabled;
@@ -83,9 +84,6 @@ bool CharacterComponent::OnRequestServerObjectInfo(GameMessages::GameMsg& msg) {
cmptType.PushDebug<AMFIntValue>("Current Activity Type") = GeneralUtils::ToUnderlying(m_CurrentActivity);
cmptType.PushDebug<AMFDoubleValue>("Property Clone ID") = m_Character->GetPropertyCloneID();
GameMessages::SendUIMessageServerToSingleClient("ToggleObjectDebugger", response, m_Parent->GetSystemAddress());
LOG("Handled!");
return true;
}

View File

@@ -331,7 +331,7 @@ public:
void LoadVisitedLevelsXml(const tinyxml2::XMLElement& doc);
private:
bool OnRequestServerObjectInfo(GameMessages::GameMsg& msg);
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
/**
* The map of active venture vision effects

View File

@@ -15,6 +15,7 @@
#include "eGameMasterLevel.h"
class AMFBaseValue;
class AMFArrayValue;
class Entity;
class Item;
class NiQuaternion;
@@ -788,6 +789,13 @@ namespace GameMessages {
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
};
struct GetObjectReportInfo : public GameMsg {
AMFArrayValue* info{};
bool bVerbose{};
GetObjectReportInfo() : GameMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, eGameMasterLevel::DEVELOPER) {}
};
struct RequestUse : public GameMsg {
RequestUse() : GameMsg(MessageType::Game::REQUEST_USE) {}

View File

@@ -1,5 +1,7 @@
#include "DEVGMCommands.h"
#include <ranges>
// Classes
#include "AssetManager.h"
#include "Character.h"
@@ -48,6 +50,7 @@
#include "MessageType/Master.h"
#include "eInventoryType.h"
#include "ePlayerFlag.h"
#include "StringifiedEnum.h"
namespace DEVGMCommands {
@@ -1514,7 +1517,12 @@ namespace DEVGMCommands {
}
if (!closest) return;
LOG("%llu", closest->GetObjectID());
GameMessages::RequestServerObjectInfo objectInfo;
objectInfo.bVerbose = true;
objectInfo.target = closest->GetObjectID();
objectInfo.targetForReport = entity->GetObjectID();
closest->HandleMsg(objectInfo);
Game::entityManager->SerializeEntity(closest);
@@ -1528,16 +1536,6 @@ namespace DEVGMCommands {
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(header.str()));
for (const auto& pair : closest->GetComponents()) {
auto id = pair.first;
std::stringstream stream;
stream << "Component [" << std::to_string(static_cast<uint32_t>(id)) << "]";
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(stream.str()));
}
if (splitArgs.size() >= 2) {
if (splitArgs[1] == "-m" && splitArgs.size() >= 3) {
auto* const movingPlatformComponent = closest->GetComponent<MovingPlatformComponent>();
@@ -1557,13 +1555,6 @@ namespace DEVGMCommands {
Game::entityManager->SerializeEntity(closest);
} else if (splitArgs[1] == "-a" && splitArgs.size() >= 3) {
RenderComponent::PlayAnimation(closest, splitArgs.at(2));
} else if (splitArgs[1] == "-s") {
for (auto* entry : closest->GetSettings()) {
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::UTF8ToUTF16(entry->GetString()));
}
ChatPackets::SendSystemMessage(sysAddr, u"------");
ChatPackets::SendSystemMessage(sysAddr, u"Spawner ID: " + GeneralUtils::to_u16string(closest->GetSpawnerID()));
} else if (splitArgs[1] == "-p") {
const auto postion = closest->GetPosition();

View File

@@ -12,7 +12,7 @@
#include <iostream>
void HTTPMonitorInfo::Serialize(RakNet::BitStream &bitStream) const {
void HTTPMonitorInfo::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(port);
bitStream.Write<uint8_t>(openWeb);
bitStream.Write<uint8_t>(supportsSum);
@@ -81,28 +81,25 @@ void WorldPackets::SendServerState(const SystemAddress& sysAddr) {
SEND_PACKET;
}
void WorldPackets::SendCreateCharacter(const SystemAddress& sysAddr, int64_t reputation, LWOOBJID player, const std::string& xmlData, const std::u16string& username, eGameMasterLevel gm) {
void WorldPackets::SendCreateCharacter(const SystemAddress& sysAddr, int64_t reputation, LWOOBJID player, const std::string& xmlData, const std::u16string& username, eGameMasterLevel gm, const LWOCLONEID cloneID) {
using namespace std;
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::CREATE_CHARACTER);
RakNet::BitStream data;
data.Write<uint32_t>(7); //LDF key count
std::unique_ptr<LDFData<LWOOBJID>> objid(new LDFData<LWOOBJID>(u"objid", player));
std::unique_ptr<LDFData<LOT>> lot(new LDFData<LOT>(u"template", 1));
std::unique_ptr<LDFData<std::string>> xmlConfigData(new LDFData<std::string>(u"xmlData", xmlData));
std::unique_ptr<LDFData<std::u16string>> name(new LDFData<std::u16string>(u"name", username));
std::unique_ptr<LDFData<int32_t>> gmlevel(new LDFData<int32_t>(u"gmlevel", static_cast<int32_t>(gm)));
std::unique_ptr<LDFData<int32_t>> chatmode(new LDFData<int32_t>(u"chatmode", static_cast<int32_t>(gm)));
std::unique_ptr<LDFData<int64_t>> reputationLdf(new LDFData<int64_t>(u"reputation", reputation));
std::vector<std::unique_ptr<LDFBaseData>> ldfData;
ldfData.push_back(move(make_unique<LDFData<LWOOBJID>>(u"objid", player)));
ldfData.push_back(move(make_unique<LDFData<LOT>>(u"template", 1)));
ldfData.push_back(move(make_unique<LDFData<string>>(u"xmlData", xmlData)));
ldfData.push_back(move(make_unique<LDFData<u16string>>(u"name", username)));
ldfData.push_back(move(make_unique<LDFData<int32_t>>(u"gmlevel", static_cast<int32_t>(gm))));
ldfData.push_back(move(make_unique<LDFData<int32_t>>(u"chatmode", static_cast<int32_t>(gm))));
ldfData.push_back(move(make_unique<LDFData<int64_t>>(u"reputation", reputation)));
ldfData.push_back(move(make_unique<LDFData<int32_t>>(u"propertycloneid", cloneID)));
objid->WriteToPacket(data);
lot->WriteToPacket(data);
name->WriteToPacket(data);
gmlevel->WriteToPacket(data);
chatmode->WriteToPacket(data);
xmlConfigData->WriteToPacket(data);
reputationLdf->WriteToPacket(data);
data.Write<uint32_t>(ldfData.size());
for (const auto& toSerialize : ldfData) toSerialize->WriteToPacket(data);
//Compress the data before sending:
const uint32_t reservedSize = ZCompression::GetMaxCompressedLength(data.GetNumberOfBytesUsed());
@@ -177,7 +174,7 @@ void WorldPackets::SendHTTPMonitorInfo(const SystemAddress& sysAddr, const HTTPM
SEND_PACKET;
}
void WorldPackets::SendDebugOuput(const SystemAddress& sysAddr, const std::string& data){
void WorldPackets::SendDebugOuput(const SystemAddress& sysAddr, const std::string& data) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::DEBUG_OUTPUT);
bitStream.Write<uint32_t>(data.size());

View File

@@ -31,7 +31,7 @@ namespace WorldPackets {
void SendCharacterDeleteResponse(const SystemAddress& sysAddr, bool response);
void SendTransferToWorld(const SystemAddress& sysAddr, const std::string& serverIP, uint32_t serverPort, bool mythranShift);
void SendServerState(const SystemAddress& sysAddr);
void SendCreateCharacter(const SystemAddress& sysAddr, int64_t reputation, LWOOBJID player, const std::string& xmlData, const std::u16string& username, eGameMasterLevel gm);
void SendCreateCharacter(const SystemAddress& sysAddr, int64_t reputation, LWOOBJID player, const std::string& xmlData, const std::u16string& username, eGameMasterLevel gm, const LWOCLONEID cloneID);
void SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::set<std::pair<uint8_t, uint8_t>> unacceptedItems);
void SendGMLevelChange(const SystemAddress& sysAddr, bool success, eGameMasterLevel highestLevel, eGameMasterLevel prevLevel, eGameMasterLevel newLevel);
void SendHTTPMonitorInfo(const SystemAddress& sysAddr, const HTTPMonitorInfo& info);

View File

@@ -1040,7 +1040,7 @@ void HandlePacket(Packet* packet) {
auto* characterComponent = player->GetComponent<CharacterComponent>();
if (!characterComponent) return;
WorldPackets::SendCreateCharacter(packet->systemAddress, player->GetComponent<CharacterComponent>()->GetReputation(), player->GetObjectID(), c->GetXMLData(), username, c->GetGMLevel());
WorldPackets::SendCreateCharacter(packet->systemAddress, player->GetComponent<CharacterComponent>()->GetReputation(), player->GetObjectID(), c->GetXMLData(), username, c->GetGMLevel(), c->GetPropertyCloneID());
WorldPackets::SendServerState(packet->systemAddress);
const auto respawnPoint = player->GetCharacter()->GetRespawnPoint(Game::zoneManager->GetZone()->GetWorldID());

View File

@@ -121,15 +121,15 @@ These commands are primarily for development and testing. The usage of many of t
## Detailed `/inspect` Usage
`/inspect <component> (-m <waypoint> | -a <animation> | -s | -p | -f (faction) | -t)`
`/inspect <component> (-m <waypoint> | -a <animation> | -p | -f (faction) | -t)`
Finds the closest entity with the given component or LDF variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the the IDs of all components the entity has.
This info is then shown in a window on the client which the user can navigate
### `/inspect` Options
* `-m`: If the entity has a moving platform component, sends it to the given waypoint, or stops the platform if `waypoint` is `-1`.
* `-a`: Plays the given animation on the entity.
* `-s`: Prints the entity's settings and spawner ID.
* `-p`: Prints the entity's position
* `-f`: If the entity has a destroyable component, prints whether the entity is smashable and its friendly and enemy faction IDs; if `faction` is specified, adds that faction to the entity.
* `-cf`: check if the entity is enemy or friend