Skip to content

"Splits" window for tracking different goals for speed runners. - #2001

Draft
jdm080 wants to merge 3 commits into
gwdevhub:devfrom
jdm080:feature/splits-window
Draft

"Splits" window for tracking different goals for speed runners.#2001
jdm080 wants to merge 3 commits into
gwdevhub:devfrom
jdm080:feature/splits-window

Conversation

@jdm080

@jdm080 jdm080 commented May 13, 2026

Copy link
Copy Markdown

A ways to go, but wanted to see if this is even something that might be wanted.

Adds a new built-in ToolboxWindow for tracking different speedrun's and their splits in Guild Wars. Features include:

  • Split timer with real time and game time tracking
  • Goal-based triggers: mission complete/bonus, map enter/exit, vanquish complete, reach level, reach title rank, manual
  • Hard mode flag on mission/bonus triggers
  • Batch goal pickers for missions, explorables, towns, and titles
  • Personal best comparison (real or game time basis)
  • Per-character run history saved to JSON
  • Crash-protection resume on toolbox restart
  • Configurable hotkeys for start, reset, and manual split
  • Settings panel integrated into the standard Toolbox settings UI
image image image

@3vcloud

3vcloud commented May 13, 2026

Copy link
Copy Markdown
Member

@claude between this and the existing objective timer module, how would you approach having a single source of truth for tracking objectives in game without impacting the look and feel of the existing objective timer in toolbox? Could we enhance this version with what's missing to be a replacement of the existing module? We also already have a feature to host a websocket to host live splits, could this easily be integrated?

I'm conscious of how much code this module would add, and would like to merge functionality related to objective tracking

@claude

claude Bot commented May 13, 2026

Copy link
Copy Markdown

Claude finished @3vcloud's task in 2m 7s —— View job


Analysis: Merging Splits with Objective Timer

  • Read CLAUDE.md for repo guidelines
  • Review the new Splits window code
  • Review the existing Objective Timer module
  • Review the existing live splits / websocket feature
  • Analyze overlap and gaps between the modules
  • Provide architectural recommendations

Where the two modules currently overlap

Both windows independently register their own GW event hooks for essentially the same underlying game events:

Concern ObjectiveTimerWindow SplitsWindow
Mission complete StoC::MessageServer (pattern matching) UI::kMissionComplete callback
Mission bonus StoC::MessageServer UI::kObjectiveComplete callback
Map transition StoC::GameSrvTransfer + InstanceLoadInfo + InstanceTimer polled each frame via GW::Map::GetMapID() diff
Vanquish (none — ObjectiveTimer doesn't track this) polled via GetFoesToKill()
Party defeated StoC::PartyDefeated (none — SplitsWindow never stops mid-run)

There are also 9 additional StoC hooks in ObjectiveTimer (DoorOpen, DoorClose, ServerMessage, DisplayDialogue, ObjectiveUpdateName, ObjectiveDone, AgentUpdateAllegiance, DoACompleteZone, DungeonReward) that SplitsWindow has no equivalent for — these are what drive the fine-grained FoW/UW/Deep/Urgoz/DoA tracking.


What's missing from SplitsWindow to be a full replacement

1. The rich sub-objective event system

GoalTrigger::Type covers course-grained triggers (MissionComplete, MapEnter, VanquishComplete, etc.) but not the fine-grained packet events (DoorOpen for specific door IDs, ServerMessage pattern matching, DisplayDialogue matching, AgentUpdateAllegiance) that are needed to replicate the per-room FoW/UW/Deep/Urgoz/DoA tracking. Without these, SplitsWindow can't express "Room 3 of Urgoz" or "Dhuum starts" as a goal trigger.

2. WebSocket / LiveSplit server

ObjectiveTimerWindow hosts a uWS::App server (GW Toolbox = server, LiveSplit = client). It sends "reset", "start", and "split" commands via the "objective_events" topic. SplitsWindow has zero websocket integration. Note: the existing ThreadedWebSocket utility is an outbound client — it's a different thing and isn't directly reusable here; the server pattern from ObjectiveTimer is what's needed.

3. Historical multi-run display

ObjectiveTimer renders all runs from the current day as collapsible headers with Start/End/Time columns for each individual objective. SplitsWindow only shows the live run against the personal best. The use cases are subtly different: ObjectiveTimer = "how did each objective compare across today's runs"; SplitsWindow = "am I on PB pace right now?"

4. Hierarchical objectives

ObjectiveTimer has parent/child indented objectives (e.g., DoA rooms inside a Foundry parent). SplitsWindow is a flat list. This is important for displaying sub-steps without counting each as a full split.

5. Auto /age on completion

Minor, but SplitsWindow has no equivalent to auto_send_age.


Recommended architecture: shared event bus

The cleanest path to a single source of truth without touching the Objectives window's UI is a dedicated ObjectiveEventDispatcher module that:

  • Owns all the StoC and UI packet registrations currently duplicated across the two windows
  • Exposes a typed subscription API so both windows listen to the same events
  • Is a passive Toolbox module (no window, no settings pane)

Both ObjectiveTimerWindow and GoalEngine subscribe to the dispatcher rather than directly registering their own hooks. ObjectiveTimerWindow's look and feel is completely unchanged — it just receives events from a different source.

For GoalEngine to gain access to the richer event types (doors, server messages, etc.), GoalTrigger::Type would gain new variants corresponding to the ObjectiveTimerWindow::EventType cases that make sense for user-defined splits. The batch pickers in SplitsGoalListWindow would expose UI for configuring these.


WebSocket integration path

The websocket server logic in ObjectiveTimerWindow.cpp (lines ~181–253) is self-contained but embedded in an anonymous namespace. Extracting it into a small LiveSplitServer class (similar to how ThreadedWebSocket wraps the client) would let both windows call LiveSplitServer::Instance().Send("split"):

// New shared class (sketch)
class LiveSplitServer {
public:
    static LiveSplitServer& Instance();
    void Enable(int port, WebsocketMode mode);
    void Disable();
    void Send(std::string_view command); // "start" / "split" / "reset"
};

SplitsWindow would call Send("split") each time a goal fires (already happening in GoalEngine::FireGoal), and Send("reset") / Send("start") from StartRun() / ResetRun(). The settings checkbox currently in ObjectiveTimerWindow::DrawSettingsInternal would move to LiveSplitServer's own settings, or be duplicated in both windows.


What's practical to add now vs. long-term

Now (low effort, high value):

  • Integrate the websocket server — extract EnableWebsocketServer / WebsocketSendMessage into a shared helper and wire it into GoalEngine::FireGoal and SplitsWindow::StartRun / ResetRun
  • Add auto /age on run completion (1 line: call GW::Chat::SendChat('/', "age") in SaveCompletedRun)

Medium term:

  • Extract ObjectiveEventDispatcher to eliminate the duplicate StoC registrations
  • Expand GoalTrigger::Type with DoorOpen(id), ServerMessage(pattern), ObjectiveDone(id) so users can build FoW/UW splits manually

Long term (full replacement of ObjectiveTimer):

  • Add preset goal lists for FoW, UW, Deep, Urgoz, DoA (essentially porting the AddFoWObjectiveSet etc. logic into JSON preset files)
  • Add a hierarchical/nested display mode to SplitsGoalListWindow
  • Add a "past runs" view alongside the live split view

The existing Objectives window and its data format don't need to change at all until you're confident the Splits window covers all its use cases — at which point it becomes a [Deprecated] toggle in settings.


Summary: The WebSocket integration is the most practical immediate addition and is straightforward to wire in. A proper single source of truth requires extracting an event bus module that both windows subscribe to, plus expanding GoalTrigger::Type to support the fine-grained events ObjectiveTimer relies on. Full replacement is feasible but would be a multi-stage effort; the two windows can coexist without redundancy once the event bus is in place.

@jdm080

jdm080 commented May 15, 2026

Copy link
Copy Markdown
Author

So as an overall direction, more integration with the current objective timer (full collapse into or via a shared bus). Webserver integration also.

@3vcloud

3vcloud commented May 15, 2026

Copy link
Copy Markdown
Member

So as an overall direction, more integration with the current objective timer (full collapse into or via a shared bus). Webserver integration also.

Yeh looks like it's a close fit to replace the objective timer to be honest, so with a few more tweaks I wouldn't have a problem using this instead

@jdm080
jdm080 marked this pull request as ready for review July 11, 2026 16:30
@jdm080

jdm080 commented Jul 16, 2026

Copy link
Copy Markdown
Author

Left OT as its own Window(for now) and all its current saving behavior/looks etc, instead both splits and OT just subscribe to the "GwEventBus". 2 profiles in Splits Manual/Running. Cut down a few settings/added a few different ones.
Splits sends commands to web socket server when its enabled. Added a section in the websocket server. Just so people can know why a command (start/reset/split/etc) was sent and from what window+why.
image
image
image
Screenshot 2026-07-11 095559

Happy to make any further tweaks before\after merge.
(Fully integrate current OT into the splits window as a profile as an example)

@jdm080
jdm080 marked this pull request as draft July 23, 2026 02:20
@jdm080

jdm080 commented Jul 23, 2026

Copy link
Copy Markdown
Author

Progressed farther so bringing back to draft ( OT/Web socket still there but once splits is validated its basically ready to go)

SC profile implemented = OT (Validating still lots of stuff to verify)
SC still has its default files, but allows you to create a duplicate list and name it example being 2 man FoW. so you have the generic bucket for people who want the old behavior but adds the ability to section it via different save files.

Quest+Skill added (no internal db just made a button to /wiki game integrations to get the quest id or skill ID you want from there.
Mob Kill goal.
Auto fail on rezone
Comparison toggle for PB/AVG/Last Run

Nuzlocke section
Tracks party deaths player+hench+hero
Points section
added some helpful texts in sections to better explain some stuff.

@3vcloud

3vcloud commented Jul 23, 2026

Copy link
Copy Markdown
Member

@claude theres a lot of code in this PR that is duplicated logic e.g. decoding strings, getting map names, loading settings. The gw event bus thing needs to go; hooks should be done from within the module, not via another. Theres too much StoC handling going on to the point that GWCA edits are suggested; many of these hooks have UIMessage equivalents - give examples such as the quest one, and identify others. There are functions like "GetNByString" or "GetStringByN" which should really just be maps to reduce LOC, and many other examples of over engineering to do a job that would be easier and still maintainable with much less code. The UI layout in the settings is a bit confusing, but I'm not sure how much of a help AI can be with this bit.

Amends have been (accidentally) added to the PR that would affect CI, and these need to go. Agents.md needs consulting against this code aswell.

Its been created using AI, but if it works its a really comprehensive solution - its just too much code for what it does, and it could do the same thing with much less.

Summarise this to provide constructive feedback, using the existing codebase as a reference

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @3vcloud's task in 6m 13s —— View job


Code Review: Splits Window PR

  • Read AGENTS.md and CLAUDE.md
  • Review new Splits files for over-engineering and duplication
  • Check GwEventBus architecture against codebase patterns
  • Identify StoC hooks with UIMessage equivalents
  • Check for CI-affecting amends
  • Cross-reference existing utilities for duplicated logic
  • Post comprehensive feedback

Summary

This is a comprehensive and functional feature, but it carries too much code weight for what it does. The main issues fall into five buckets: architecture (GWEventBus), unnecessary GWCA edits, duplicated utilities, over-engineered patterns, and CI accidents. These are fixable — the core split logic is sound.


1. GWEventBus — remove it

GWEventBus.cpp/.h should not exist. The pattern in this codebase is that every module owns its own StoC/UIMessage registrations. A pub-sub bus is a well-intentioned abstraction but it breaks this convention and adds indirection that costs more than it saves:

  • GWEventBus registers 27 separate hooks at startup regardless of whether either subscriber needs any given event. OT uses about 12 of those; SplitsWindow uses about 20. Neither uses all 27. Every fired packet now pays the cost of going through Emit() → snapshot copy of subscribers vector → callback dispatch, even for events neither consumer cares about.
  • The type-erasure layer (GWEvent with id1/id2/str/spawn union-by-convention) is weaker than direct packet access. Subscribers must interpret id1/id2 by convention, documented only in comments.
  • Fix: OT and SplitsWindow each register the handful of hooks they actually need, directly, as every other module in the codebase does. There will be some duplication (e.g. both listen to InstanceLoadInfo) but that's acceptable and honest — it's what PconsWindow, ObserverModule, ResignLogModule etc. all do with shared packets like AgentState.

2. GWCA edit — drop it, use the UIMessage instead

The PR adds a new QuestRemove StoC struct to Dependencies/GWCA/include/GWCA/Packets/StoC.h. The comment in the PR itself says the layout is "unverified" — that alone is reason to remove it. More importantly, it's unnecessary:

  • GW::UI::UIMessage::kQuestRemoved already fires when a quest leaves the log, with wparam = { quest_id, ... } (documented in UIMessages.h:470).
  • GW::UI::UIMessage::kQuestAdded is the equivalent for QuestAdd StoC, and QuestModule.cpp:511 already shows the usage pattern: *(GW::Constants::QuestID*)wparam.

Replacement for both QuestAdd/QuestRemove StoC callbacks in GWEventBus.cpp:261-277:

// In SplitsWindow::Initialize() directly:
GW::UI::RegisterUIMessageCallback(&on_quest_update_, GW::UI::UIMessage::kQuestAdded,
    [this](GW::HookStatus*, GW::UI::UIMessage, void* wparam, void*) {
        const auto quest_id = *(GW::Constants::QuestID*)wparam;
        engine_.NotifyQuestUpdate(quest_id);
    });
GW::UI::RegisterUIMessageCallback(&on_quest_remove_, GW::UI::UIMessage::kQuestRemoved,
    [this](GW::HookStatus*, GW::UI::UIMessage, void* wparam, void*) {
        const auto quest_id = *(GW::Constants::QuestID*)wparam;
        engine_.NotifyQuestRemoved(quest_id);
    });

Similarly, the following StoC hooks in GWEventBus have UIMessage equivalents that the rest of the codebase uses instead:

GWEventBus StoC hook UIMessage equivalent Existing codebase usage
GW::Packet::StoC::QuestAdd kQuestAdded QuestModule.cpp:511, GameSettings.cpp:1197
GW::Packet::StoC::QuestRemove (new, unverified) kQuestRemoved — (but documented in UIMessages.h:470)
GW::Packet::StoC::PartyPlayerAdd kPartyAddPlayer ResignLogModule.cpp:157, DiscordModule.cpp:391
GW::Packet::StoC::PartyPlayerRemove kPartyRemovePlayer DiscordModule.cpp:720
GW::Packet::StoC::PartyHeroAdd kPartyAddHero AccountInventoryWindow.cpp:1503, DiscordModule.cpp:395
GW::Packet::StoC::PartyHeroRemove kPartyRemoveHero DiscordModule.cpp:396
GW::Packet::StoC::PartyHenchmanAdd kPartyAddHenchman DiscordModule.cpp:393
GW::Packet::StoC::PartyHenchmanRemove kPartyRemoveHenchman DiscordModule.cpp:720
GW::Packet::StoC::SkillActivate kAgentSkillActivated / kSkillActivated Minimap.cpp:869,915 — wparam is kAgentSkillPacket* with agent_id + skill_id
GW::Packet::StoC::ObjectiveAdd kObjectiveAdd ActiveQuestWidget.cpp:70
GW::Packet::StoC::ObjectiveDone kObjectiveComplete PconsWindow.cpp:233, HintsModule.cpp:304, ActiveQuestWidget.cpp:69

Switching to UIMessages eliminates the need for the unverified GWCA struct and aligns with how every other module in the project hooks these events.


3. MapNames.cpp/.h — delete it, use Resources::GetMapName

Windows/Splits/MapNames.cpp re-implements async map-name decoding from scratch (57 lines): it calls GW::UI::UInt32ToEncStr, GW::UI::AsyncDecodeStr, and maintains its own unordered_map<int, string> s_cache. This is exactly what Resources::GetMapName(MapID) already does, returning a GuiUtils::EncString* whose .string() gives the decoded UTF-8 string.

Every reference to MapNames::Get(id) in SplitsGoalListWindow.cpp and SCPresets.cpp should be replaced with Resources::GetMapName(id)->string(). The Resources::GetMapName pattern is already used in ~8 other files including ObjectiveTimerWindow.cpp:565, TravelWindow.cpp:580, DailyQuestsWindow.cpp:1930.


4. Duplicated utility functions

FormatTime (local static in SplitsGoalListWindow.cpp:45) — a hh:mm:ss.cs formatter that's used in both SplitsGoalListWindow.cpp and referenced via SplitsWindow.cpp. OT uses its own time formatting inline. This should either live in TextUtils or just be a free function in SplitsGoalListWindow.cpp shared between both files — but it shouldn't be a local static repeated per-file.

CampaignName() — re-implemented at SplitsGoalListWindow.cpp:69 as a local switch. CompletionWindow_Constants.h:9 already has const char* CampaignName(Campaign camp) and it covers the same campaigns. Either include that header or move the function to a shared location.

RegionName() — re-implemented at SplitsGoalListWindow.cpp:80. Resources::GetRegionName(MapID) already provides this decoded from game data. The switch covers the same regions.

WideChar→UTF-8 in MapNames.cpp:20 — uses WideCharToMultiByte directly when TextUtils::WStringToString() (TextUtils.h:11) exists for exactly this.


5. Over-engineered patterns

Static title list (SplitsGoalListWindow.cpp:1083–1130) — a hardcoded array of 44 TitleEntry structs with names, IDs, and group strings. The game's own title tracks are enumerable via GW::PlayerMgr::GetTitleTrack(TitleID) for all TitleID values — the same API SplitsGoalListWindow.cpp:1170 already calls for rank display. A small loop over the TitleID enum values with GetTitleTrack() would produce the same list dynamically without a hand-maintained table. ChatCommands.cpp:287 does exactly this pattern with its title_names vector.

ExpRow build_exp_list lambda (SplitsGoalListWindow.cpp:1569–1591) — iterates all map IDs on every tab switch, constructing ExpRow with a string. With Resources::GetMapName this is still a small scan, but the intermediate unordered_map deduplication by name_id (to collapse multiple instances of the same explorable to one row) could just be a std::set<uint32_t> of seen name_ids with a flat loop. The current two-pass approach (build best map → convert to vector → sort) is 3 passes where 1 would do.

GoalEntry.h comment blocksGoalEntry.h:15-19 and GoalEntry.h:48-64 contain multi-line prose comment blocks explaining cross-file timing dependencies. Per AGENTS.md, comments should explain why, not what, and should be "one concise line". These blocks document design decisions that belong in the PR description, not in the header.

SplitsWindow.h has 363 lines with 50+ nuzlocke_* member fields. Nuzlocke tracking (party deaths, points per category, henchman merging, hero lives) is a substantial sub-feature stuffed into the main class. It could be a small NuzlockeState struct to reduce the member sprawl.


6. CI-affecting amends — revert these

The PR accidentally picked up workflow changes from merging origin/dev. These should not be part of a feature branch PR:

  • .github/workflows/claude.yml — adds FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true env var
  • .github/workflows/cmake.yml — same env var
  • .github/workflows/pending-release.yml — same env var
  • .github/workflows/site-deploy.yml — reformatted (content-equivalent but whitespace diff)

These should be reverted to their origin/dev state before merge. A targeted git checkout origin/dev -- .github/workflows/ followed by a commit will remove them cleanly without touching anything else.


What's working well

The core split logic in GoalEngine.cpp and GoalList.cpp is clean and correct. The preset data in SCPresets.cpp (FoW/UW/Deep/Urgoz/DoA objective IDs and door triggers) is genuinely useful and well-structured. The websocket integration in WebSocketModule is a clean extraction. The SplitsProfile settings serialization is straightforward. The crash-resume via resume.json is a good user-facing feature.


In short: the feature works and covers real speedrunner use cases. The refactoring ask is: remove GWEventBus (let each module own its hooks), drop the GWCA edit (use kQuestRemoved UIMessage), delete MapNames.cpp (use Resources::GetMapName), and remove the duplicated title table and campaign/region switch functions. That's probably a 30–40% LOC reduction with no functional change.

@jdm080

jdm080 commented Jul 24, 2026

Copy link
Copy Markdown
Author

Cleanup pass: LOC reduction from review feedback

In response to review feedback on duplication/over-engineering, went through and consolidated the genuine duplicates (not a blanket rewrite — verified each one is actually the same logic before touching it). Net -1,047 lines across all Splits/OT/WebSocketModule files since the last feature-build commit (243e2fc7), via git diff --numstat:

File Net lines What changed
SplitsGoalListWindow.cpp −349 Dynamic title list, trigger-type table, DrawSettings split into one method per profile/section, Town/Explorable batch pickers merged into one parameterized picker
SplitsWindow.h −106 NuzlockeState struct extraction
GWEventBus.cpp/.h −443 (deleted) Replaced with direct hooks in OT/SplitsWindow — architecture change, not compression
MapNames.cpp/.h −72 (deleted) Dead code, superseded by Resources::GetMapName
GoalEngine.cpp −68 Dead NotifyMissionBonus removed, ReachTitleRank simplified
GoalEntry.h −40
GoalList.cpp −16 Enum↔string table consolidation
SplitsWindow.cpp −14 Near-neutral (hook additions offset helper extractions)
ObjectiveTimerWindow.cpp +77 Grew — absorbed the hooks GWEventBus used to own

Honest caveat: of the −1,047, roughly half is genuine duplicate/dead-code removal; the rest is the GWEventBus architecture change (relocated logic, not eliminated) and the NuzlockeState reorganization. Not claiming this hits any specific target percentage — just what held up under actually reading the code.

I'll dig a little deeper tomorrow.

@henderkes

henderkes commented Jul 24, 2026

Copy link
Copy Markdown
Member

ok but could you perhaps write anything on your own instead of just prompting the slop machine? use it for code all you want but don't subject us to reading that.

there's also still a merge conflict

@3vcloud

3vcloud commented Jul 24, 2026

Copy link
Copy Markdown
Member

I'm on the phone atm but I can see it hasn't moved the majority of stoc callbacks into ui messages. the websocket module is actually just another part of the objective timer at this point and not a separate module on its own, unless it's a utility that more than 1 module can use but then it still not a module.

Will take a look this evening, thanks

@jdm080

jdm080 commented Jul 24, 2026

Copy link
Copy Markdown
Author

OT/ the original Websocket is not deleted/removed yet. The end goal was to replace it fully. (Do I just fully delete it right now?) was keeping to compare functionality.

image

As for the StoC calls 11 orginally were copies from OT itself.
ManipulateMapObject,InstanceLoadInfo,InstanceLoadFile,GameSrvTransfer,MessageServer,DisplayDialogue,,AgentUpdateAllegiance,DoACompleteZone,CountdownStart are the ones left now. (don't seem to be any replacements)

Splits StoC calls
PartyLock - checking for the mission-que to normalize timer

5 for the Nuzlocke section
There isn't any place to get Henchman name in GWCA besides PartyHenchmanAdd?
Which is used for getting the names into the roster, it would be possible to swap over and have split between StoC for the 1 call. Other 4 could use player/hero add/remove but wouldn't be fully enough to remove StoC so kept for consistency with itself.

image

@3vcloud

3vcloud commented Jul 24, 2026

Copy link
Copy Markdown
Member

@claude is there anything in the toolbox codebase that programmatically gets a hero name?

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @3vcloud's task in 1m 3s —— View job


  • Search codebase for hero name retrieval patterns

Hero name lookup: Resources::GetHeroName

Yes — Resources::GetHeroName(GW::Constants::HeroID) in Modules/Resources.h:137 / Resources.cpp:1213.

It works by calling GW::PartyMgr::GetHeroConstData(hero_id) to retrieve the HeroConstData::name_id game string ID, then decodes it asynchronously via DecodeStringId and caches the result as a GuiUtils::EncString*. The decoded UTF-8 name comes back from .string(). This is the canonical game name (Goren, Koss, etc.) and works for any hero ID whether or not the hero is currently in the party.

For mercenary heroes, GW::HeroInfo::name (wchar_t name[20], defined at GameEntities/Hero.h:66) gives the player-set custom name instead. GW::PartyMgr::GetHeroInfo(hero_id) returns the HeroInfo* for heroes the account owns (same owned-hero restriction the PR's FindOwnedHeroInfo already guards against).

The PR already uses Resources::GetHeroName at SplitsWindow.cpp:739 for the Nuzlocke hero display.


@3vcloud

3vcloud commented Jul 24, 2026

Copy link
Copy Markdown
Member

@claude finally, is there anything in the toolbox codebase that can get a henchman name, either in an explorable area or in an outpost?

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @3vcloud's task in 2m 34s —— View job


  • Search codebase for henchman name retrieval patterns

Henchman name retrieval in the codebase

Short answer: there is no Resources::GetHenchmanName() equivalent. Unlike heroes, the codebase has no dedicated utility for henchman names. There are three viable paths depending on timing:


1. PartyHenchmanAdd StoC packet — name at hire time

GW::Packet::StoC::PartyHenchmanAdd (StoC.h:849) embeds the name directly:

struct PartyHenchmanAdd : Packet<PartyHenchmanAdd> {
    uint32_t party_id;
    uint32_t agent_id;
    wchar_t  name_enc[8];   // ← encoded name, 8 wchars
    uint32_t profession;
    uint32_t level;
};

This is what the PR already uses at SplitsWindow.cpp:325. It's the only moment the name arrives from the server directly in a packet, so capturing it here is correct. The kPartyAddHenchman UIMessage (UIMessages.h:417) exists but its wparam layout is undocumented — DiscordModule.cpp:393 uses it purely as a trigger with no wparam read — so the StoC path is the reliable one.


2. GW::Agents::GetAgentEncName(agent_id) — for henchmen already in party

AgentMgr.h:206 works on any live agent:

const wchar_t* enc = GW::Agents::GetAgentEncName(hench.agent_id);
// decode with: GW::UI::AsyncDecodeStr(enc, &out_wstring)
// or:          GuiUtils::EncString es; es.reset(enc);

PartyStatisticsWindow.cpp:309 uses this for henchmen once they're in the party (HenchmanPartyMember::agent_id is populated). Works in outposts and explorables. TextUtils::SanitizePlayerName() (TextUtils.cpp:534) can then strip the [Warrior]-style bracket suffix if you want just the bare name.

Note the comment in AgentMgr.h:205: "Might be bugged, avoid to use." — but in practice PartyStatisticsWindow relies on it without issue.


3. NPC::name_enc via the NPC array — for henchmen not yet hired

For henchmen visible in an outpost before you've added them to your party, there's no agent yet. The NPC array is the fallback:

// NPC.h:29 — wchar_t* name_enc on every NPC
// NPC.h:34 — NPC::IsHenchman() tests npc_flags & 0x10
const auto* npcs = GW::Agents::GetNPCArray(); // GW::NPCArray*
for (const auto& npc : *npcs) {
    if (npc.IsHenchman() && npc.name_enc) {
        GW::UI::AsyncDecodeStr(npc.name_enc, &decoded_name);
    }
}

There's no toolbox wrapper for this; it's a raw GWCA call.


Bottom line for the PR: the PartyHenchmanAdd StoC callback is the correct approach for Nuzlocke name capture — it's the only server-authoritative moment the encoded name is delivered. GetAgentEncName would work as a fallback lookup after the fact, but the StoC timing is right for the hire-event use case. There's no cleaner built-in alternative.

@3vcloud

3vcloud commented Jul 24, 2026

Copy link
Copy Markdown
Member

ok claude was wrong, but you'd get the henchman name by agent id. I did write somewhere how to find the list of henchmen agent ids available in an outpost, but not sure if thats needed

Comment thread GWToolboxdll/Modules/ToolboxSettings.cpp Outdated
@jdm080

jdm080 commented Aug 4, 2026

Copy link
Copy Markdown
Author

All 3 profiles in, Manual/Running/SC(OT replacement hence a lot of duplication currently)

Found some replacements for some of the current OT hooks for "Splits" to use. UImessage equivalents..

Objective started: OT → ObjectiveUpdateName (StoC), Splits → kObjectiveUpdated (UI message)
Objective done: OT → ObjectiveDone (StoC), Splits → kObjectiveComplete (UI message)
Dungeon reward: OT → DungeonReward (StoC), Splits → kDungeonComplete (UI message)

Added in a few behaviors to let SC function more like OT. (auto starts/resets after failing/completing). Presets folder so they don't clutter the user list. Cleaner to not show every file and instead just show user created ones and if a user never creates their own it just uses the default and functions similarly to current OT.

Nuzlocke rewritten. diff's against GetPartyInfo instead of the previous StoC.

This has gotten way larger than anticipated . So wanted to ask for some clarity or let me know whatever you guys want.

  1. Workable as is for a PR.
  2. Reworked to be multiple smaller PRs over time.
  3. Make it a plugin instead.

jdm080 added 3 commits August 27, 2026 22:12
New built-in ToolboxWindow for tracking speedrun goals/splits in Guild
Wars, decoupled from ObjectiveTimerWindow so it doesn't depend on OT.

- Split timer with real time and game time tracking
- Goal-based triggers: mission complete/bonus, map enter/exit,
  vanquish complete, reach level, reach title rank, manual
- Hard mode flag on mission/bonus triggers
- Batch goal pickers for missions, explorables, towns, and titles
- Personal best comparison (real or game time basis), Sum of Best
- Per-character run history saved to JSON, Recent Runs list
- Crash-protection resume on toolbox restart
- Configurable hotkeys for start, reset, and manual split
- Standalone LiveSplit-compatible websocket server (WebSocketModule),
  own port so it can coexist with OT's own server during the
  transition
- Running/SC unified goal model, Nuzlocke death-rule extraction, DoA
  and dungeon presets
- /wiki chat command, opens a wiki page via the client's own
  kOpenWikiUrl integration
…updates

Replaces polling GetControlledCharacter()->level every tick with a
callback on UI message 0x10000014, reverse-engineered as
AgentLevelChanged (wparam = {agent_id, level}) — GWCA has no name for
it yet. player_level_ is cached and reset to 0 on zone load so a
character switch re-seeds instead of inheriting the previous
character's level.

Credit to Dan for finding this more efficient way to detect the
player's level.
…overhead

- WebSocketModule: app_/loop_ are now atomics; publish()/close()/delete
  are marshalled onto the socket loop's own thread via Loop::defer()
  instead of being called cross-thread, fixing a use-after-free/data
  race on shutdown.
- SplitsWindow::StartRun(): no-op when the clock is auto-paused
  mid-route (running_load_paused_/running_awaiting_movement_) instead
  of falling into the fresh-start branch and wiping completed-goal
  progress.
- SplitsWindow::SwitchProfile(): call ResetRunFlags() instead of
  hand-resetting 5 of its 11 fields, so a switch away from and back to
  Running can't leave stale flags that make the next run silently
  resume instead of starting.
- GoalEngine::CheckPendingMissionBonus(): throttled to once/second via
  a new timer instead of polling CompletionWindow::IsAreaComplete()
  every tick for however long a bonus stays unearned.
- SplitsGoalListWindow::Draw(): bind the active SplitsProfile by const
  reference instead of copying it (two std::string members) every
  rendered frame.
@jdm080
jdm080 force-pushed the feature/splits-window branch from 11f158a to eb2408b Compare August 28, 2026 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants