From 6bcc376c06289b8ba0d6ccdde34fbcff1d4ca795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 17 Aug 2026 19:57:36 +0200 Subject: [PATCH] Accept all four broadcaster names, and alias memory reads to uintValue broadcast.config.set rejected "game" and "stepping" as unsupported, though both are documented and both are real broadcasters. The valid keys are whatever already exists in the client's disallowed map, and that map starts empty and only grows as a side effect of operator[] the first time each category broadcasts - so which keys were accepted depended on what had happened to fire yet. "logger" and "input" work because the broadcast loop touches them every lap; "game" and "stepping" only appear once one actually occurs. Seed all four at connection setup. Unknown keys are still refused, which is the useful half of the old behaviour. The numeric memory reads answered with "value" while cpu.getReg and cpu.getAllRegs answer with "uintValue". Nothing marks which is which, so a client that guesses gets a missing key - and one that defaults a missing key to zero silently reports plausible nonsense, which cost real time during the CrossCraft investigation (an empty vtable that wasn't). Write both names. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9 --- Core/Debugger/WebSocket.cpp | 7 +++++++ Core/Debugger/WebSocket/MemorySubscriber.cpp | 14 ++++++++++++++ docs/WebSocketDebugger.md | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Core/Debugger/WebSocket.cpp b/Core/Debugger/WebSocket.cpp index 4b0023bab9..7ff42a2d29 100644 --- a/Core/Debugger/WebSocket.cpp +++ b/Core/Debugger/WebSocket.cpp @@ -188,6 +188,13 @@ void HandleDebuggerRequest(const http::ServerRequest &request) { WebSocketClientInfo client_info; auto& disallowed_config = client_info.disallowed; + // Seed every broadcaster category. broadcast.config.set only accepts keys that already exist + // here (so a typo is rejected rather than silently ignored), and these otherwise only appear + // as a side effect of operator[] the first time each category actually broadcasts - which + // meant "game" and "stepping" were rejected as unsupported until one happened to fire, even + // though they're documented and valid. Keep in sync with the Broadcast calls further down. + for (const char *category : { "logger", "input", "game", "stepping" }) + disallowed_config[category] = false; LogBroadcaster logger; InputBroadcaster input; diff --git a/Core/Debugger/WebSocket/MemorySubscriber.cpp b/Core/Debugger/WebSocket/MemorySubscriber.cpp index 2f63cbf064..37e75816ed 100644 --- a/Core/Debugger/WebSocket/MemorySubscriber.cpp +++ b/Core/Debugger/WebSocket/MemorySubscriber.cpp @@ -103,6 +103,8 @@ AutoDisabledReplacements::~AutoDisabledReplacements() { // // Response (same event name): // - value: unsigned integer +// - uintValue: the same number under the name cpu.getReg/cpu.getAllRegs use, so a client can +// read either without special-casing which event it came from void WebSocketMemoryReadU8(DebuggerRequest &req) { uint32_t addr; if (!req.ParamU32("address", &addr, false)) { @@ -122,6 +124,8 @@ void WebSocketMemoryReadU8(DebuggerRequest &req) { AutoDisabledReplacements memLock = LockMemory(true); JsonWriter &json = req.Respond(); json.writeUint("value", Memory::ReadUnchecked_U8(addr)); + // Alias: cpu.getReg and cpu.getAllRegs call this uintValue. Same number, both names. + json.writeUint("uintValue", Memory::ReadUnchecked_U8(addr)); }); } @@ -132,6 +136,8 @@ void WebSocketMemoryReadU8(DebuggerRequest &req) { // // Response (same event name): // - value: unsigned integer +// - uintValue: the same number under the name cpu.getReg/cpu.getAllRegs use, so a client can +// read either without special-casing which event it came from void WebSocketMemoryReadU16(DebuggerRequest &req) { uint32_t addr; if (!req.ParamU32("address", &addr, false)) { @@ -151,6 +157,7 @@ void WebSocketMemoryReadU16(DebuggerRequest &req) { AutoDisabledReplacements memLock = LockMemory(true); JsonWriter &json = req.Respond(); json.writeUint("value", Memory::ReadUnchecked_U16(addr)); + json.writeUint("uintValue", Memory::ReadUnchecked_U16(addr)); }); } @@ -161,6 +168,8 @@ void WebSocketMemoryReadU16(DebuggerRequest &req) { // // Response (same event name): // - value: unsigned integer +// - uintValue: the same number under the name cpu.getReg/cpu.getAllRegs use, so a client can +// read either without special-casing which event it came from void WebSocketMemoryReadU32(DebuggerRequest &req) { uint32_t addr; if (!req.ParamU32("address", &addr, false)) { @@ -180,6 +189,7 @@ void WebSocketMemoryReadU32(DebuggerRequest &req) { AutoDisabledReplacements memLock = LockMemory(true); JsonWriter &json = req.Respond(); json.writeUint("value", Memory::ReadUnchecked_U32(addr)); + json.writeUint("uintValue", Memory::ReadUnchecked_U32(addr)); }); } @@ -330,6 +340,8 @@ void WebSocketMemoryWriteU8(DebuggerRequest &req) { JsonWriter &json = req.Respond(); json.writeUint("value", Memory::ReadUnchecked_U8(addr)); + // Alias: cpu.getReg and cpu.getAllRegs call this uintValue. Same number, both names. + json.writeUint("uintValue", Memory::ReadUnchecked_U8(addr)); }); } @@ -367,6 +379,7 @@ void WebSocketMemoryWriteU16(DebuggerRequest &req) { JsonWriter &json = req.Respond(); json.writeUint("value", Memory::ReadUnchecked_U16(addr)); + json.writeUint("uintValue", Memory::ReadUnchecked_U16(addr)); }); } @@ -404,6 +417,7 @@ void WebSocketMemoryWriteU32(DebuggerRequest &req) { JsonWriter &json = req.Respond(); json.writeUint("value", Memory::ReadUnchecked_U32(addr)); + json.writeUint("uintValue", Memory::ReadUnchecked_U32(addr)); }); } diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index 9b44ef17e1..38fee74c72 100644 --- a/docs/WebSocketDebugger.md +++ b/docs/WebSocketDebugger.md @@ -118,7 +118,7 @@ file - this is just an index. | CPU core | `cpu.stepping`, `cpu.resume`, `cpu.status` (reports `ticks` plus `us`, emulated microseconds, and `clockHz` - use `us` to line up with wall-clock timings, since games change the clock frequency and the ticks-per-second ratio isn't fixed), `cpu.getAllRegs`, `cpu.getReg`, `cpu.setReg`, `cpu.evaluate` | `CPUCoreSubscriber.cpp` | | Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.runUntilTime` (run until a point in emulated time - `us` absolute or `relativeUs` from now - and break there; this is how to get a scripted repro reproducibly "N seconds into the game" instead of polling `cpu.status` in a loop), `cpu.nextHLE` | `SteppingSubscriber.cpp` | | Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list`, `cpu.regBreakpoint.add/update/remove/list` (break when a register is written to, by any instruction anywhere - currently GPRs only; interpreter-only, no effect under a JIT backend) | `BreakpointSubscriber.cpp` | -| Memory read/write | `memory.read_u8/u16/u32`, `memory.read`, `memory.readString`, `memory.write_u8/u16/u32`, `memory.write` | `MemorySubscriber.cpp` | +| Memory read/write | `memory.read_u8/u16/u32`, `memory.read`, `memory.readString`, `memory.write_u8/u16/u32`, `memory.write`. The numeric ones report the result as both `value` and `uintValue` - the latter is what `cpu.getReg`/`cpu.getAllRegs` call it, so a client can read either without caring which event answered | `MemorySubscriber.cpp` | | Memory search | `memory.search` - scan a range for a `u8`/`u16`/`u32`/`float` value or a `bytes` pattern (with an optional wildcard mask), for narrowing down where an unknown value lives (Cheat Engine style) | `MemorySubscriber.cpp` | | Memory info/annotations | `memory.mapping`, `memory.info.config/set/list/search` | `MemoryInfoSubscriber.cpp` | | Disassembly | `memory.base`, `memory.disasm` (add `compact=true` for plain-text lines instead of full per-field objects), `memory.searchDisasm` (add `findAll=true` for every match instead of just the first - e.g. "every caller of this address"), `memory.assemble` | `DisasmSubscriber.cpp` |