From 62ac65c52024460e5aecb54464f552636472f6f5 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:13:27 -0700 Subject: [PATCH] feat: Mission Component debug (#1901) * feat: Mission Component debug * Add player argument to inspect command * Add completion details * Remove unlocalized server string done on client instead --- dCommon/dEnums/eMissionState.h | 5 +- dGame/Entity.cpp | 5 +- dGame/dComponents/CharacterComponent.cpp | 4 +- dGame/dComponents/MissionComponent.cpp | 111 +++++++++++++++ dGame/dComponents/MissionComponent.h | 1 + dGame/dGameMessages/GameMessages.cpp | 1 + dGame/dMission/Mission.cpp | 6 + dGame/dMission/Mission.h | 2 + .../SlashCommands/DEVGMCommands.cpp | 127 +++++++++--------- docs/Commands.md | 2 +- 10 files changed, 197 insertions(+), 67 deletions(-) diff --git a/dCommon/dEnums/eMissionState.h b/dCommon/dEnums/eMissionState.h index e080f455..8c52c439 100644 --- a/dCommon/dEnums/eMissionState.h +++ b/dCommon/dEnums/eMissionState.h @@ -50,7 +50,10 @@ enum class eMissionState : int { /** * The mission has been completed before and has now been completed again. Used for daily missions. */ - COMPLETE_READY_TO_COMPLETE = 12 + COMPLETE_READY_TO_COMPLETE = 12, + + // The mission is failed (don't know where this is used) + FAILED = 16, }; #endif //!__MISSIONSTATE__H__ diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 7a27c1b0..45136774 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -2247,6 +2247,7 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) { response.Insert("objectID", std::to_string(m_ObjectID)); response.Insert("serverInfo", true); GameMessages::GetObjectReportInfo info{}; + info.bVerbose = requestInfo.bVerbose; info.info = response.InsertArray("data"); auto& objectInfo = info.info->PushDebug("Object Details"); auto* table = CDClientManager::GetTable(); @@ -2260,14 +2261,14 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) { auto& componentDetails = objectInfo.PushDebug("Component Information"); for (const auto [id, component] : m_Components) { - componentDetails.PushDebug(StringifiedEnum::ToString(id)) = ""; + componentDetails.PushDebug(StringifiedEnum::ToString(id)); } auto& configData = objectInfo.PushDebug("Config Data"); for (const auto config : m_Settings) { configData.PushDebug(GeneralUtils::UTF16ToWTF8(config->GetKey())) = config->GetValueAsString(); - } + HandleMsg(info); auto* client = Game::entityManager->GetEntity(requestInfo.clientId); diff --git a/dGame/dComponents/CharacterComponent.cpp b/dGame/dComponents/CharacterComponent.cpp index 8eaa4103..a0c44961 100644 --- a/dGame/dComponents/CharacterComponent.cpp +++ b/dGame/dComponents/CharacterComponent.cpp @@ -70,7 +70,7 @@ bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { for (const auto zoneID : m_VisitedLevels) { std::stringstream sstream; sstream << "MapID: " << zoneID.GetMapID() << " CloneID: " << zoneID.GetCloneID(); - vl.PushDebug(sstream.str()) = ""; + vl.PushDebug(sstream.str()); } // visited locations @@ -95,7 +95,7 @@ bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { const int32_t flagId = base + i; std::stringstream stream; stream << "Flag: " << flagId; - allFlags.PushDebug(stream.str()) = ""; + allFlags.PushDebug(stream.str()); } flagChunkCopy >>= 1; } diff --git a/dGame/dComponents/MissionComponent.cpp b/dGame/dComponents/MissionComponent.cpp index cf9aa767..6c85f908 100644 --- a/dGame/dComponents/MissionComponent.cpp +++ b/dGame/dComponents/MissionComponent.cpp @@ -27,7 +27,10 @@ std::unordered_map> MissionComponent: //! Initializer MissionComponent::MissionComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { + using namespace GameMessages; m_LastUsedMissionOrderUID = Game::zoneManager->GetUniqueMissionIdStartingValue(); + + RegisterMsg(this, &MissionComponent::OnGetObjectReportInfo); } //! Destructor @@ -622,3 +625,111 @@ void MissionComponent::ResetMission(const int32_t missionId) { m_Missions.erase(missionId); GameMessages::SendResetMissions(m_Parent, m_Parent->GetSystemAddress(), missionId); } + +void PushMissions(const std::map& missions, AMFArrayValue& V, bool verbose) { + for (const auto& [id, mission] : missions) { + std::stringstream ss; + if (!mission) { + ss << "Mission ID: " << id; + V.PushDebug(ss.str()); + } else if (!verbose) { + ss << "%[Missions_" << id << "_name]" << ", Mission ID"; + V.PushDebug(ss.str()) = id; + } else { + ss << "%[Missions_" << id << "_name]" << ", Mission ID: " << id; + auto& missionV = V.PushDebug(ss.str()); + auto& missionInformation = missionV.PushDebug("Mission Information"); + + if (mission->IsComplete()) { + missionInformation.PushDebug("Time mission last completed") = std::to_string(mission->GetTimestamp()); + missionInformation.PushDebug("Number of times completed") = mission->GetCompletions(); + } + // Expensive to network this especially when its read from the client anyways + // missionInformation.PushDebug("Description").PushDebug("None"); + // missionInformation.PushDebug("Text").PushDebug("None"); + + auto& statusInfo = missionInformation.PushDebug("Mission statuses for local player"); + if (mission->IsAvalible()) statusInfo.PushDebug("Available"); + if (mission->IsActive()) statusInfo.PushDebug("Active"); + if (mission->IsReadyToComplete()) statusInfo.PushDebug("Ready To Complete"); + if (mission->IsComplete()) statusInfo.PushDebug("Completed"); + if (mission->IsFailed()) statusInfo.PushDebug("Failed"); + const auto& clientInfo = mission->GetClientInfo(); + + statusInfo.PushDebug("Is an achievement mission") = mission->IsAchievement(); + statusInfo.PushDebug("Is an timed mission") = clientInfo.time_limit > 0; + auto& taskInfo = statusInfo.PushDebug("Task Info"); + taskInfo.PushDebug("Number of tasks in this mission") = mission->GetTasks().size(); + int32_t i = 0; + for (const auto* task : mission->GetTasks()) { + auto& thisTask = taskInfo.PushDebug("Task " + std::to_string(i)); + // Expensive to network this especially when its read from the client anyways + // thisTask.PushDebug("Description").PushDebug("%[MissionTasks_" + taskUidStr + "_description]"); + thisTask.PushDebug("Number done") = std::min(task->GetProgress(), static_cast(task->GetClientInfo().targetValue)); + thisTask.PushDebug("Number total needed") = task->GetClientInfo().targetValue; + thisTask.PushDebug("Task Type") = task->GetClientInfo().taskType; + i++; + } + + + // auto& chatText = missionInformation.PushDebug("Chat Text for Mission States"); + // Expensive to network this especially when its read from the client anyways + // chatText.PushDebug("Available Text").PushDebug("%[MissionText_" + idStr + "_chat_state_1]"); + // chatText.PushDebug("Active Text").PushDebug("%[MissionText_" + idStr + "_chat_state_2]"); + // chatText.PushDebug("Ready-to-Complete Text").PushDebug("%[MissionText_" + idStr + "_chat_state_3]"); + // chatText.PushDebug("Complete Text").PushDebug("%[MissionText_" + idStr + "_chat_state_4]"); + + if (clientInfo.time_limit > 0) { + missionInformation.PushDebug("Time Limit") = clientInfo.time_limit; + missionInformation.PushDebug("Time Remaining") = 0; + } + + if (clientInfo.offer_objectID != -1) { + missionInformation.PushDebug("Offer Object LOT") = clientInfo.offer_objectID; + } + + if (clientInfo.target_objectID != -1) { + missionInformation.PushDebug("Complete Object LOT") = clientInfo.target_objectID; + } + + if (!clientInfo.prereqMissionID.empty()) { + missionInformation.PushDebug("Requirement Mission IDs") = clientInfo.prereqMissionID; + } + + missionInformation.PushDebug("Is Repeatable") = clientInfo.repeatable; + const bool hasNoOfferer = clientInfo.offer_objectID == -1 || clientInfo.offer_objectID == 0; + const bool hasNoCompleter = clientInfo.target_objectID == -1 || clientInfo.target_objectID == 0; + missionInformation.PushDebug("Is Achievement") = hasNoOfferer && hasNoCompleter; + } + } +} + +bool MissionComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { + auto& reportMsg = static_cast(msg); + auto& missionInfo = reportMsg.info->PushDebug("Mission (Laggy)"); + missionInfo.PushDebug("Component ID") = GetComponentID(); + // Sort the missions so they are easier to parse and present to the end user + std::map achievements; + std::map missions; + std::map doneMissions; + for (const auto [id, mission] : m_Missions) { + if (!mission) continue; + else if (mission->IsComplete()) doneMissions[id] = mission; + else if (mission->IsAchievement()) achievements[id] = mission; + else if (mission->IsMission()) missions[id] = mission; + } + + // None of these should be empty, but if they are dont print the field + if (!achievements.empty() || !missions.empty()) { + auto& incompleteMissions = missionInfo.PushDebug("Incomplete Missions"); + PushMissions(achievements, incompleteMissions, reportMsg.bVerbose); + PushMissions(missions, incompleteMissions, reportMsg.bVerbose); + } + + if (!doneMissions.empty()) { + auto& completeMissions = missionInfo.PushDebug("Completed Missions"); + PushMissions(doneMissions, completeMissions, reportMsg.bVerbose); + } + + return true; +} diff --git a/dGame/dComponents/MissionComponent.h b/dGame/dComponents/MissionComponent.h index 28727ecf..53f09d41 100644 --- a/dGame/dComponents/MissionComponent.h +++ b/dGame/dComponents/MissionComponent.h @@ -171,6 +171,7 @@ public: void ResetMission(const int32_t missionId); private: + bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); /** * All the missions owned by this entity, mapped by mission ID */ diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 47262629..08261a23 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -6415,6 +6415,7 @@ namespace GameMessages { void RequestServerObjectInfo::Handle(Entity& entity, const SystemAddress& sysAddr) { auto* handlingEntity = Game::entityManager->GetEntity(targetForReport); if (handlingEntity) handlingEntity->HandleMsg(*this); + else LOG("Failed to find target %llu", targetForReport); } bool RequestUse::Deserialize(RakNet::BitStream& stream) { diff --git a/dGame/dMission/Mission.cpp b/dGame/dMission/Mission.cpp index 93bf397d..3d46cde8 100644 --- a/dGame/dMission/Mission.cpp +++ b/dGame/dMission/Mission.cpp @@ -270,6 +270,12 @@ bool Mission::IsReadyToComplete() const { return m_State == eMissionState::READY_TO_COMPLETE || m_State == eMissionState::COMPLETE_READY_TO_COMPLETE; } +bool Mission::IsFailed() const { + const auto underlying = GeneralUtils::ToUnderlying(m_State); + const auto target = GeneralUtils::ToUnderlying(eMissionState::FAILED); + return (underlying & target) != 0; +} + void Mission::MakeReadyToComplete() { SetMissionState(m_Completions == 0 ? eMissionState::READY_TO_COMPLETE : eMissionState::COMPLETE_READY_TO_COMPLETE); } diff --git a/dGame/dMission/Mission.h b/dGame/dMission/Mission.h index 9526c40a..edd21d09 100644 --- a/dGame/dMission/Mission.h +++ b/dGame/dMission/Mission.h @@ -244,6 +244,8 @@ public: const std::set& GetTestedMissions() const; + bool IsFailed() const; + private: /** * Progresses all the newly accepted tasks for this mission after it has been accepted to reflect the state of the diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index 447d5155..0e4cbb5b 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -1475,50 +1475,55 @@ namespace DEVGMCommands { if (splitArgs.empty()) return; Entity* closest = nullptr; + float closestDistance = 0.0f; std::u16string ldf; bool isLDF = false; - auto component = GeneralUtils::TryParse(splitArgs[0]); - if (!component) { - component = eReplicaComponentType::INVALID; + closest = PlayerManager::GetPlayer(splitArgs[0]); + if (!closest) { + auto component = GeneralUtils::TryParse(splitArgs[0]); + if (!component) { + component = eReplicaComponentType::INVALID; - ldf = GeneralUtils::UTF8ToUTF16(splitArgs[0]); + ldf = GeneralUtils::UTF8ToUTF16(splitArgs[0]); - isLDF = true; - } - - auto reference = entity->GetPosition(); - - auto closestDistance = 0.0f; - - const auto candidates = Game::entityManager->GetEntitiesByComponent(component.value()); - - for (auto* candidate : candidates) { - if (candidate->GetLOT() == 1 || candidate->GetLOT() == 8092) { - continue; + isLDF = true; } - if (isLDF && !candidate->HasVar(ldf)) { - continue; - } - - if (!closest) { - closest = candidate; - - closestDistance = NiPoint3::Distance(candidate->GetPosition(), reference); - - continue; - } - - const auto distance = NiPoint3::Distance(candidate->GetPosition(), reference); - - if (distance < closestDistance) { - closest = candidate; - - closestDistance = distance; + auto reference = entity->GetPosition(); + + + const auto candidates = Game::entityManager->GetEntitiesByComponent(component.value()); + + for (auto* candidate : candidates) { + if (candidate->GetLOT() == 1 || candidate->GetLOT() == 8092) { + continue; + } + + if (isLDF && !candidate->HasVar(ldf)) { + continue; + } + + if (!closest) { + closest = candidate; + + closestDistance = NiPoint3::Distance(candidate->GetPosition(), reference); + + continue; + } + + const auto distance = NiPoint3::Distance(candidate->GetPosition(), reference); + + if (distance < closestDistance) { + closest = candidate; + + closestDistance = distance; + } } + } else { + closestDistance = NiPoint3::Distance(entity->GetPosition(), closest->GetPosition()); } if (!closest) return; @@ -1684,7 +1689,7 @@ namespace DEVGMCommands { } const auto splitArgs = GeneralUtils::SplitString(args, ' '); - + // Prevent execute command recursion by checking if this is already an execute command for (const auto& arg : splitArgs) { if (arg == "execute" || arg == "exec") { @@ -1692,51 +1697,51 @@ namespace DEVGMCommands { return; } } - + // Context variables for execution Entity* execEntity = entity; // Entity to execute as NiPoint3 execPosition = entity->GetPosition(); // Position to execute from bool positionOverridden = false; std::string finalCommand; - + // Parse subcommands size_t i = 0; while (i < splitArgs.size()) { const std::string& subcommand = splitArgs[i]; - + if (subcommand == "as") { if (i + 1 >= splitArgs.size()) { ChatPackets::SendSystemMessage(sysAddr, u"Error: 'as' requires a player name"); return; } - + const std::string& targetName = splitArgs[i + 1]; auto* targetPlayer = PlayerManager::GetPlayer(targetName); if (!targetPlayer) { ChatPackets::SendSystemMessage(sysAddr, u"Error: Player '" + GeneralUtils::ASCIIToUTF16(targetName) + u"' not found"); return; } - + execEntity = targetPlayer; i += 2; - + } else if (subcommand == "at") { if (i + 1 >= splitArgs.size()) { ChatPackets::SendSystemMessage(sysAddr, u"Error: 'at' requires a player name"); return; } - + const std::string& targetName = splitArgs[i + 1]; auto* targetPlayer = PlayerManager::GetPlayer(targetName); if (!targetPlayer) { ChatPackets::SendSystemMessage(sysAddr, u"Error: Player '" + GeneralUtils::ASCIIToUTF16(targetName) + u"' not found"); return; } - + execPosition = targetPlayer->GetPosition(); positionOverridden = true; i += 2; - + } else if (subcommand == "positioned") { if (i + 3 >= splitArgs.size()) { ChatPackets::SendSystemMessage(sysAddr, u"Error: 'positioned' requires x, y, z coordinates"); @@ -1754,69 +1759,69 @@ namespace DEVGMCommands { execPosition = NiPoint3(xOpt.value(), yOpt.value(), zOpt.value()); positionOverridden = true; - + i += 4; - + } else if (subcommand == "run") { // Everything after "run" is the command to execute if (i + 1 >= splitArgs.size()) { ChatPackets::SendSystemMessage(sysAddr, u"Error: 'run' requires a command"); return; } - + // Reconstruct the command from remaining args for (size_t j = i + 1; j < splitArgs.size(); ++j) { if (!finalCommand.empty()) finalCommand += " "; finalCommand += splitArgs[j]; } break; - + } else { ChatPackets::SendSystemMessage(sysAddr, u"Error: Unknown subcommand '" + GeneralUtils::ASCIIToUTF16(subcommand) + u"'"); ChatPackets::SendSystemMessage(sysAddr, u"Valid subcommands: as, at, positioned, run"); return; } } - + if (finalCommand.empty()) { ChatPackets::SendSystemMessage(sysAddr, u"Error: No command specified to run. Use 'run ' at the end."); return; } - + // Validate that the command starts with a valid character if (finalCommand.empty() || finalCommand[0] == '/') { ChatPackets::SendSystemMessage(sysAddr, u"Error: Command should not start with '/'. Just specify the command name."); return; } - + // Store original position if we need to restore it NiPoint3 originalPosition; bool needToRestore = false; - + if (positionOverridden && execEntity == entity) { // If we're executing as ourselves but from a different position, // temporarily move the entity originalPosition = entity->GetPosition(); needToRestore = true; - + // Set the position temporarily for the command execution auto* controllable = entity->GetComponent(); if (controllable) { controllable->SetPosition(execPosition); } } - + // Provide feedback about what we're executing std::string execAsName = execEntity->GetCharacter() ? execEntity->GetCharacter()->GetName() : "Unknown"; - ChatPackets::SendSystemMessage(sysAddr, u"[Execute] Running as '" + GeneralUtils::ASCIIToUTF16(execAsName) + - u"' from <" + GeneralUtils::to_u16string(execPosition.x) + u", " + - GeneralUtils::to_u16string(execPosition.y) + u", " + - GeneralUtils::to_u16string(execPosition.z) + u">: /" + - GeneralUtils::ASCIIToUTF16(finalCommand)); - + ChatPackets::SendSystemMessage(sysAddr, u"[Execute] Running as '" + GeneralUtils::ASCIIToUTF16(execAsName) + + u"' from <" + GeneralUtils::to_u16string(execPosition.x) + u", " + + GeneralUtils::to_u16string(execPosition.y) + u", " + + GeneralUtils::to_u16string(execPosition.z) + u">: /" + + GeneralUtils::ASCIIToUTF16(finalCommand)); + // Execute the command through the slash command handler SlashCommandHandler::HandleChatCommand(GeneralUtils::ASCIIToUTF16("/" + finalCommand), execEntity, sysAddr); - + // Restore original position if needed if (needToRestore) { auto* controllable = entity->GetComponent(); diff --git a/docs/Commands.md b/docs/Commands.md index 54d5e531..f9a00452 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -80,7 +80,7 @@ These commands are primarily for development and testing. The usage of many of t |getnavmeshheight|`/getnavmeshheight`|Displays the navmesh height at your current position.|8| |giveuscore|`/giveuscore `|Gives uscore.|8| |gmadditem|`/gmadditem (count)`|Adds the given item to your inventory by id.|8| -|inspect|`/inspect (-m \| -a \| -s \| -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. See [Detailed `/inspect` Usage](#detailed-inspect-usage) below.|8| +|inspect|`/inspect (-m \| -a \| -s \| -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. See [Detailed `/inspect` Usage](#detailed-inspect-usage) below.|8| |list-spawns|`/list-spawns`|Lists all the character spawn points in the zone. Additionally, this command will display the current scene that plays when the character lands in the next zone, if there is one.|8| |locrow|`/locrow`|Prints the your current position and rotation information to the console.|8| |lookup|`/lookup `|Searches through the Objects table in the client SQLite database for items whose display name, name, or description contains the query. Query can be multiple words delimited by spaces.|8|