"Splits" window for tracking different goals for speed runners. - #2001
"Splits" window for tracking different goals for speed runners.#2001jdm080 wants to merge 3 commits into
Conversation
|
@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 finished @3vcloud's task in 2m 7s —— View job Analysis: Merging Splits with Objective Timer
Where the two modules currently overlapBoth windows independently register their own GW event hooks for essentially the same underlying game events:
There are also 9 additional StoC hooks in ObjectiveTimer ( What's missing from SplitsWindow to be a full replacement1. The rich sub-objective event system
2. WebSocket / LiveSplit server ObjectiveTimerWindow hosts a 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 Minor, but SplitsWindow has no equivalent to Recommended architecture: shared event busThe cleanest path to a single source of truth without touching the Objectives window's UI is a dedicated
Both For WebSocket integration pathThe websocket server logic in // 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"
};
What's practical to add now vs. long-termNow (low effort, high value):
Medium term:
Long term (full replacement of ObjectiveTimer):
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 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 |
|
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 |
|
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) 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. Nuzlocke section |
|
@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 finished @3vcloud's task in 6m 13s —— View job Code Review: Splits Window PR
SummaryThis 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
2. GWCA edit — drop it, use the UIMessage insteadThe PR adds a new
Replacement for both // 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:
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.
|
Cleanup pass: LOC reduction from review feedbackIn 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 (
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. |
|
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 |
|
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 |
|
@claude is there anything in the toolbox codebase that programmatically gets a hero name? |
|
Claude finished @3vcloud's task in 1m 3s —— View job
Hero name lookup:
|
|
@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 finished @3vcloud's task in 2m 34s —— View job
Henchman name retrieval in the codebaseShort answer: there is no 1.
|
|
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 |
|
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) 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.
|
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.
11f158a to
eb2408b
Compare






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: