Skip to content

Reliability retrofit: thread-safe WebSockets, getPlayer hardening, request validation, dedicated-server packaging#298

Open
coredmp95 wants to merge 37 commits into
porisius:devfrom
coredmp95:gsd/phase-05-dedicated-server-asset-packaging-pr
Open

Reliability retrofit: thread-safe WebSockets, getPlayer hardening, request validation, dedicated-server packaging#298
coredmp95 wants to merge 37 commits into
porisius:devfrom
coredmp95:gsd/phase-05-dedicated-server-asset-packaging-pr

Conversation

@coredmp95

Copy link
Copy Markdown

Summary

A targeted reliability retrofit of the HTTP/WebSocket monitoring API so it returns accurate,
real-time game state without crashing or corrupting the dedicated server — reliability of the
monitoring channel over feature breadth.

No new external dependencies. SML ^3.12.0 compatibility preserved. All public API response
shapes are unchanged or additive-only (see Compatibility).

Because the project has no automated test framework, every runtime change was verified
live against a deployed Satisfactory Linux dedicated server, not just by compiling.

Area What changed Why it matters
WebSocket threading All inbound callbacks and outbound sends are marshaled through the game thread. Removes a whole class of race-condition crashes / shared-state corruption.
getPlayer Crash-safe reads, new location.lookPitch, persistent player names across disconnect. Endpoint no longer crashes the server; richer, more reliable data.
WS request validation Malformed/malicious subscribe envelopes are rejected with a clear error before dispatch. Bad input can no longer reach endpoint logic on the now-guaranteed game thread.
Dedicated-server packaging Branded 503 fallback when the web UI is missing; icon staging verified against a real packaged build. Servers without the web UI bundle degrade gracefully instead of breaking silently.
Dev tooling Reproducible build/package pipeline, WS stress + validation harnesses, deploy/verify runbook. Makes the plugin buildable and verifiable on Linux.

Changes

1. Thread-safe WebSocket request handling

FicsitRemoteMonitoring.cpp/.h — inbound open/close/message callbacks and the push loop no
longer touch shared subsystem state (ConnectedClients, EndpointSubscribers, subscription maps)
from the uWS background thread. Introduces generation-tagged client identity
(ClientGenerations + IsClientCurrent), marshals all mutation to the game thread via
AsyncTask, and defers outbound send()/close() onto the captured uWS loop with liveness +
generation re-checks. Retires the old OnClientDisconnected path; fixes a connect/disconnect log
bug. No client-visible behavior change — purely where the work runs.

2. getPlayer endpoint reliability

PlayerLibrary.cpp, FicsitRemoteMonitoring.h, FicsitRemoteMonitoring.build.csIsValid()
guards on the two known crash sites (GetInventory(), GetHealthComponent()) with safe fallbacks;
new location.lookPitch (camera/aim pitch, clamped [-90,90]); a session-persistent PlayerNameCache
(keyed by the crash-safe APlayerState::GetUniqueId(), not AFGPlayerState::GetUserID() which
SIGSEGVs on dedicated servers) so offline players are emitted without dereferencing despawned
actors. Links the CoreOnline module.

3. WebSocket request validation (VALD-01)

FicsitRemoteMonitoring.cpp/.h — new ValidateWSSubscriptionEnvelope performs strict envelope
validation before any endpoint logic runs (action present/known; endpoints must be a string or
array of strings; GET-only registry match; capped problem reporting). Wired into
ProcessClientRequest and OnMessageReceived; rejected input returns an {"error": …} frame and
never reaches dispatch. HTTP CallEndpoint path untouched (no REST regression).

4. Dedicated-server asset packaging & fallback

FicsitRemoteMonitoring.cpp, FRM_Request.cpp/.h, Config/FilterPlugin.ini — a branded 503
fallback page (SendFallbackPage) when the www/ web UI bundle isn't deployed, with carve-outs so
sub-resources / /Icons/* / /api/* keep their normal 404/JSON content-type contract, and a
guard so extensionless bare-form API calls (e.g. /getWorldInv) keep returning JSON. Confirmed
that Config/PluginSettings.ini (unchanged) is what stages the Icons/ directory into a packaged
build — verified against a real LinuxServer zip; FilterPlugin.ini marked inert.

Developer tooling (non-shipping)

build/package.sh (reproducible compile/package), build/RUNBOOK.md (deploy/verify runbook),
build/tools/ws-stress-harness.mjs + ws-validation-probe.mjs (zero-dependency Node test
instruments), .gitignore, and this CHANGELOG.md.

Compatibility

Unchanged: Config/PluginSettings.ini (already correct); HTTP CallEndpoint dispatch; SML
^3.12.0; no new external C++/JS libraries (uses already-linked Unreal Json + vendored uWS).

Additive-only: getPlayer gains location.lookPitch and may now include offline players; WS
subscribe/unsubscribe now returns an {"error": …} frame for malformed envelopes (previously
undefined behavior). Well-formed requests are unaffected.

Note on upstream #297 (feat: Actor Pitch)

This PR intentionally exposes the player pitch as location.lookPitch, not pitch, so it
coexists cleanly with the generic actor-rotation pitch field added in
#297 (which lives in the shared getActorJSON helper). The two are
complementary and use distinct keys:

  • pitch (from feat: Actor Pitch #297) = actor body rotation — meaningful for vehicles/trains tilting on
    terrain; ~0 for an upright player.
  • location.lookPitch (this PR) = the player's camera/aim pitch — the only pitch that
    carries information for a player.

Different files, so there is no merge conflict, and no key shadowing when both land.

Verification

No automated test framework exists (by design). Verified live on a deployed Linux dedicated
server
:

  • Threading: WS stress harness under connection churn — clean closes, Remaining connections: 0
    after each cycle, no crash/leak.
  • Validation: ws-validation-probe.mjs — 8/8 malformed-payload cases rejected; 25-client churn,
    no regression for valid clients.
  • Packaging: real LinuxServer.zip inspected (Icons/ staged); PKG-02 six-case curl battery
    (503 fallback + carve-outs + bare-form JSON guard) all green.

See CHANGELOG.md for the full per-area detail and reviewer file map.


🤖 Generated with Claude Code

porisius and others added 30 commits June 22, 2026 14:32
…ckage.sh compile stage

- Symlinked repo into /home/fabrice/dev/SatisfactoryModLoader/Mods/GameFeatures/FicsitRemoteMonitoring
- Manually applied SML header patch (FGServerAPIManager.h private:->public: at line 116) after discovering the build.cs auto-apply's line-index check silently skipped it
- Added build/package.sh (compile stage, env-var parameterized via UE_DIR/SML_PROJECT)
- Added build/RUNBOOK.md skeleton documenting symlink -> patch-verify -> compile
- RunUBT.sh FactoryEditor Linux Development exits 0 with both FicsitRemoteMonitoring and FicsitRemoteMonitoringServer modules compiled
- Appends RunUAT.sh PackagePlugin invocation scoped to Win64 client + Linux
  server (D-01/D-02), matching the CI-verified command shape from
  .github/workflows/build.yml, adapted for RunUAT.sh on Linux
- Deliberately omits -serverplatform=Win64+Linux, -installed, and -merge
- First execution confirmed RunUAT.sh flag parity with RunUAT.bat (no flag
  parsing errors) — Assumption A1 from 01-RESEARCH.md resolved
- Cook stage failed on missing ArduinoKit script package reference from
  Content/Subsystems/FicsitRemoteMonitoring_BP.uasset (RESEARCH.md Pitfall 3),
  exactly the anticipated gated-fallback scenario — human approval required
  before cloning ArduinoKit per plan Task 2, not yet resolved in this commit
…come

- Human approved cloning github.com/porisius/ArduinoKit (per CONTRIBUTING.md)
  into SatisfactoryModLoader/Mods/ArduinoKit (not Mods/GameFeatures/) after
  Task 1's cook failed on missing /Script/ArduinoKit Blueprint references.
  DiscIt was NOT cloned — it never surfaced in the cook failure.
- After cloning, the cook failed a second time with "Plugin 'ArduinoKit'
  failed to load because module 'ArduinoKit' could not be found" since
  the package stage uses -nocompileeditor and ArduinoKit's module was only
  source, never built. Re-ran the compile stage (build/package.sh compile)
  to build ArduinoKit's module, then re-ran the package stage successfully.
- Both platform zips now produced under
  Saved/ArchivedPlugins/FicsitRemoteMonitoring/: FicsitRemoteMonitoring-Windows.zip
  and FicsitRemoteMonitoring-LinuxServer.zip. Win64 zip's Binaries/Win64/
  contains uv.dll and zlib1.dll via automatic RuntimeDependencies staging —
  no manual DLL copy step needed.
- Updates build/RUNBOOK.md Step 4 from placeholder to documented outcome,
  including the ArduinoKit dependency and required recompile-before-package
  order; updates Known Pitfalls accordingly.
Root cause: FRM is a Game Feature mod; its pak is cooked with a baked-in
mount point of Mods/GameFeatures/FicsitRemoteMonitoring/. Deploying the
plugin to Mods/FicsitRemoteMonitoring/ (one level too shallow) made the
plugin content root mismatch the pak mount point, so Content/InitGameWorld
(the GameWorldModule) never resolved and SML's root-module scan missed FRM
("Discovered 2 world modules", 0 endpoints). Fix: deploy under the
GameFeatures segment, matching the official layout. RUNBOOK Step 5 (Deploy)
and Step 6 (Smoke test) updated with the correct path + verification guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fall

- Document the canonical :8080/api/getWorldInv smoke test to match the
  phase's stated verification bar, with a note for overriding uWS.Port
  when 8080 is occupied (e.g. a colocated game client) and that
  uWS.Autostart=1 is required for the port to open.
- Add the confirmed GameFeatures-vs-Mods deploy-path pitfall to the
  Known pitfalls section (Discovered 2 world modules / 0 endpoints
  when deployed one level too shallow).
…connect churn)

- Node ESM script using built-in global WebSocket (zero npm install, ws
  package audited and rejected per RESEARCH.md Package Legitimacy Audit)
- Drives N concurrent virtual clients through churn loop against FRM's
  uWS endpoint, matching ProcessClientRequest's subscribe/unsubscribe
  message shape ({"action":..., "endpoints":[...]})
- Performs an HTTP liveness probe (built-in fetch) after the run; exits
  non-zero on probe failure or unexpected socket error pattern, turning
  a server crash/hang into an automated failure signal
- Smoke-tested against the live dedicated server (:8091): 99 clean
  churn cycles, liveness probe 200, exit 0; server log confirms
  "Remaining connections: 0" after each cycle (no subscriber/client leak)
…UNBOOK

- New "WebSocket stress harness (Phase 2 verification)" section:
  prerequisite (deploy + uWS.Autostart=1), run command/flags, and the
  D-01 pass criterion (no crash + no leak)
- No-leak criterion documented as reading ConnectedClients/EndpointSubscribers
  counts from the server's existing LogHttpServer connection-count log
  lines (no new endpoint added), with a live confirmation example from
  this machine's smoke run
- States the harness is a throwaway verification artifact, run manually,
  not wired into build/package.sh
- Add ClientGenerations (game-thread-owned ws->ClientID generation map)
- Add NextClientIDCounter, LoopLiveSockets, CapturedLoop (loop-thread-only bookkeeping for Plan 03's outbound path)
- Declare IsClientCurrent(ws, ClientID) game-thread validity helper
- No FCriticalSection introduced; FWebSocketUserData unchanged (THRD-01 prep)
- open: mint ClientID + record LoopLiveSockets synchronously on the loop
  thread, then fire-and-forget AsyncTask(GameThread) adds ws to
  ConnectedClients/ClientGenerations (weak-ptr capture, bShouldStop guard)
- close: last safe loop-thread touch removes ws from LoopLiveSockets, then
  a marshaled game-thread task removes ws from ConnectedClients,
  ClientGenerations, and every EndpointSubscribers entry, guarded by
  IsClientCurrent generation-tag validation (D-03)
- Retire OnClientDisconnected: its EndpointSubscribers cleanup is folded
  into close's marshaled block so no loop-thread mutation of that
  registry remains
- Fix copy-paste log text in open ("Client Disconnected" -> "Client
  Connected")
- Add IsClientCurrent(ws, ClientID) game-thread validity helper
  implementation
…e game thread (THRD-01)

- ProcessClientRequest now extracts Action/endpoint names/(ws,ClientID) on
  the loop thread only, then performs all EndpointSubscribers Add/Remove
  and the StartWebSocketPushDataLoop trigger inside a single
  AsyncTask(GameThread) block, guarded by TWeakObjectPtr + bShouldStop and
  re-validated via IsClientCurrent (drops stale/not-yet-registered
  messages per Assumption A2 rather than treating them as errors)
- Unifies the endpoints-array and endpoints-string subscribe/unsubscribe
  forms behind one loop body; unsubscribe now consistently checks
  EndpointSubscribers.Contains() first, avoiding a pre-existing quirk
  where TMap::operator[] on the string-form could spuriously create an
  empty EndpointSubscribers entry (Rule 1 auto-fix)
- StartWebSocketPushDataLoop: bHasRunningPushDataLoop=true is now set
  synchronously by its caller (the game-thread marshaled subscribe
  branch, its only call site) instead of inside the push-pacing thread;
  the terminal =false write is marshaled back via AsyncTask(GameThread)
  with a TWeakObjectPtr capture, so both reads/writes are game-thread
  owned end-to-end
- OnMessageReceived's malformed-JSON drop path and the app.post/
  res->onData HTTP dispatch path are unchanged (D-04 scope)
…ocketServer

CapturedLoop is now assigned via uWS::Loop::get() once, from inside the
loop-thread lambda (thread-local — must be called from the thread running
app.run()), and reset to nullptr when that thread exits (success or
exception) so no caller can defer() onto a stale/torn-down loop. Enables
Task 2/3's outbound Loop::defer() work (THRD-02).
…game-thread snapshot

Reworks PushUpdatedData (THRD-02) so it no longer reads EndpointSubscribers
or calls Client->send() on the push-pacing thread. The recipient list and
per-endpoint JSON are now built inside a game-thread AsyncTask hop that
snapshots {ws, ClientID, payload} tuples (closing the read-side race,
RESEARCH Pitfall 3); each entry is then routed through
CapturedLoop->defer(), which re-validates ws against the loop-thread-only
LoopLiveSockets set and ClientID generation tag before calling ws->send()
(D-03 use-after-free defense). The UTF-8 payload is copied into an owning
std::string per deferred callback instead of referencing the stack-scoped
FTCHARToUTF8 buffer.
…loop thread

Resolves RESEARCH Open Question #1 (D-04 shutdown safety): ws->close() is a
socket-owning call and the same thread-ownership violation class as
PushUpdatedData's send(), so it must run on the uWS loop thread rather than
the game thread. Snapshots (ws, ClientID) pairs from the still-safe
game-thread ConnectedClients/ClientGenerations maps, then defers the actual
close() loop onto CapturedLoop, re-validating each ws against
LoopLiveSockets before touching it. Safe no-op when CapturedLoop is already
null (loop already torn down). bShouldStop set, listener close, and the
registry Empty() calls are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Guard Cast<AFGCharacterPlayer> with IsValid() before dereferencing
  any dependent getter (defense in depth; array is class-filtered).
- Guard GetInventory() before GetInventoryStacks() (former crash
  site :22); default to an empty Inventory array when null.
- Guard GetHealthComponent() before GetCurrentHealth()/IsDead()
  (former crash site :34/:35); default PlayerHP to 0 and Dead to
  false when null.
- Remove stale "//TODO: Find way to get player's name when they are
  offline" comment and the dead commented-out PlayerID line,
  superseded by the name-cache design landing in plan 03-02.
- No fgcheck()/check() introduced; every null here is a legitimate
  runtime condition (player mid-disconnect), so it degrades to a
  safe default per D-02, never asserts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Capture the object returned by getActorJSON(Player) into a local
  and add a "pitch" number key on it before assigning "location",
  keeping the shared getActorJSON/CreateBaseJsonObject helpers
  unmodified (D-03 blast-radius limit; only PlayerLibrary.cpp
  changes).
- Pitch is read from GetFGPlayerController()->GetControlRotation()
  (camera/look pitch), never GetActorRotation() (body rotation).
  Normalize a >180 raw value by subtracting 360, then clamp to
  [-90, 90]. Defaults to 0.0f behind an IsValid() controller guard.
- Add #include "FGPlayerController.h".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- TMap<FString,FString> PlayerNameCache on the subsystem, keyed by
  AFGPlayerState::GetUserID(), session-persistent, no eviction (D-04)
- InitPlayerConnectCache() installs SUBSCRIBE_UOBJECT_METHOD_AFTER on
  AGameModeBase::PostLogin, guards NewPlayer/PlayerState/empty UserID,
  no AsyncTask hop (PostLogin already runs on the game thread)
- Wired into BeginPlay() alongside InitAPIRegistry() — the sibling
  Init* hooks are dead code precisely because they were never called

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- getPlayer records each live player's stable GetUserID() into a
  VisitedIDs set and opportunistically refreshes PlayerNameCache with
  the name already computed (A1 mitigation for late-populated names)
- Second loop over PlayerNameCache emits a hand-built offline entry
  (Name from cache, Online:false, zeroed/empty defaults incl.
  location.pitch:0) for every cached ID not in VisitedIDs, dedupe by
  stable ID (Pitfall 4) — never via getActorJSON/CreateBaseJsonObject/
  getActorFeaturesJSON with a null actor (Pattern 3/Pitfall 2)
- Subsystem pointer IsValid()-guarded, not fgcheck()

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live-verification of the PostLogin name-cache hook (Task 3 checkpoint)
crashed the dedicated server mid-connect: AFGPlayerState::GetUserID()
dereferences the player's unique-net-id TSharedPtr, which is not yet
populated when PostLogin fires on a dedicated server, so GetUserID()
asserts (SharedPointer.h IsValid) and SIGSEGVs inside the hook.

Fix (per user decision, reliability-first): drop the PostLogin connect
hook (InitPlayerConnectCache + BeginPlay wiring + hook-only includes)
entirely and populate PlayerNameCache from the getPlayer live loop, which
only calls GetUserID() on fully-connected players where the net id is
populated (the proven-safe path). Additionally gate the live-loop
GetUserID()/cache-refresh on IsPlayerOnline()==true so a poll landing in
the narrow mid-login window can never hit the unsafe path either.

Deviation from D-04: cache is 'populated on first poll while online'
rather than 'on connect'. PLYR-03's observable outcome (name survives
disconnect) is preserved for any player observed online at least once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause (from engine source): FUniqueNetIdWrapper::IsValid() and
::ToString() null-check the underlying net-id internally, but the asserting
operator-> / GetV1() path does not. FactoryGame's AFGPlayerState::GetUserID()
uses that unchecked path, so it SIGSEGVs (SharedPointer.h IsValid assert) on
a dedicated server whenever the net-id is unset — which is any time getPlayer
runs with a player present. This crashed the server both from the PostLogin
hook and, after that was removed, from the getPlayer live loop itself.

Fix (elegant, honors D-04's stable-online-id intent): read the stable id via
the ENGINE accessor APlayerState::GetUniqueId() (a plain member ref) guarded
by FUniqueNetIdRepl::IsValid(), then ::ToString() — both null-check
internally, so the deref can never fault. When no valid id is available
ToString yields empty and we skip caching-by-id (never crash). Removed the
earlier bOnline hack. No AFGPlayerState cast needed anymore.

ToString() is COREONLINE_API, so link the first-party CoreOnline engine
module (build.cs) — an engine module, not a vendored/third-party dependency,
so the no-new-external-deps constraint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… actors

Live verification exposed a duplicate: on this dedicated server a disconnected
player keeps their character actor, so the live loop emits them (keyed by actor
name) AND the cache emits them (keyed by stable net-id). The net-id dedupe could
not bridge the two because a disconnected persisted actor has lost its net-id.

Add a VisitedNames set (display names of every live actor, online or persisted-
offline) and skip a cache entry whose name is already shown. Name is the one
identifier consistent across the live-actor and cache representations. Research
had assumed offline players have no actor (Pitfall 4 net-id dedupe only) — false
on this server. 'Exactly one entry' is now honored in both states.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New build/tools/ws-validation-probe.mjs mirroring ws-stress-harness.mjs
  conventions (Node built-in WebSocket/fetch, zero new dependency)
- Encodes all VALD-01 test-map cases: unknown endpoint name, POST-only
  endpoint (setSwitches) rejection, missing/unknown action, wrong-typed
  endpoints field, mixed valid/invalid partial-success (D-03),
  unparseable JSON with open-connection assertion (D-02), and the
  well-formed regression case
- Exit-code contract: 0 all-as-expected, 1 validation regression,
  2 connection/protocol error
- Declare private ValidateWSSubscriptionEnvelope on AFicsitRemoteMonitoring,
  co-located with AddErrorJson (VALD-01)
- Define it in FicsitRemoteMonitoring.cpp: strict TryGetStringField/
  TryGetArrayField + EJson::String extraction, GET-only registry match via
  ContainsByPredicate, D-03 partial-success problem collection capped at 20
  entries with a "...and N more" suffix (T-04-03 backstop)
- Not yet wired into ProcessClientRequest (Task 2)
…ure feedback

- ProcessClientRequest now calls ValidateWSSubscriptionEnvelope synchronously
  on the loop thread before the existing AsyncTask(GameThread) hop; only
  surviving ValidNames feed the unchanged subscribe/unsubscribe mutation body
  (D-01/SC#3)
- Any non-empty ErrorMessage (whole-request reject or D-03 partial-success
  report) is sent as a direct ws->send({"error":...}, TEXT) frame; connection
  is never closed on validation failure (D-02)
- OnMessageReceived's JSON-parse-failure branch now also sends a descriptive
  {"error":...} frame instead of log-only, keeping the connection open (D-02)
- Verified Config/PluginSettings.ini already bundles Icons and www via
  [StageSettings] +AdditionalNonUSFDirectories (no edit needed)
- Added explanatory comment to FilterPlugin.ini noting it is never read
  by build/package.sh's Alpakit PackagePlugin (BuildCookRun) pipeline
- No functional filter entries added (would mislead maintainers per
  05-RESEARCH.md Pitfall 1)
- New "Icon release workflow (PKG-01)" section between package and
  deploy steps: generate icons via /frm icon on a client (dedicated
  servers can't generate icons - no UE textures loaded), copy PNGs
  into source Icons/, package, verify via unzip -l against the real
  packaged zip contents (not which .ini was edited)
- Names Config/PluginSettings.ini's [StageSettings]
  +AdditionalNonUSFDirectories=Icons as the real mechanism and notes
  FilterPlugin.ini is inert for this pipeline
- Includes a live /Icons/<ClassName>_C.png curl check expecting 200
- Declare SendFallbackPage on UFRM_RequestLibrary beside SendErrorJson/SendErrorMessage
- Implement as an inline FString::Printf HTML body (503, text/html, AddResponseHeaders(res, false))
- Body carries a DocsURL anchor and an /api/ still-live note
coredmp95 and others added 7 commits July 19, 2026 08:48
- Add file-scope DocsURL constant (matches FicsitRemoteMonitoring.uplugin)
- /* catch-all else branch: route empty/html/htm extensions to SendFallbackPage,
  keep HandleApiRequest for other extensions (sub-resource 404 JSON unchanged)
- HandleGetRequest !FileLoaded branch: route html/htm Extension to SendFallbackPage,
  keep the existing 500 SendErrorMessage for other extensions
- /Icons/* handler untouched
- Document rename-www -> curl battery -> restore-www procedure
- Covers all five carve-out cases: /index.html 503 + body check,
  missing sub-resource 404 JSON, /Icons/* 404, /api/* JSON, restored 200
- Follows the existing -w 'HTTP %{http_code}\n' curl idiom and port caveat
The web-UI page fallback added in 05-02 classified any extensionless GET
path as a page navigation. Bare-form API endpoints (e.g. /getWorldInv,
/getModList) are also extensionless, so every unprefixed API call was
diverted to the 503 branded HTML page instead of returning JSON — even on a
healthy server — breaking the documented RUNBOOK smoke test and the plugin's
core value (the remote API returning accurate game state).

Fix: consult the endpoint registry before extension alone decides page vs
API. Add AFicsitRemoteMonitoring::IsRegisteredEndpointName and gate the
catch-all page discriminator on it, so any request naming a registered
endpoint (or a mistyped one) keeps its existing HandleApiRequest JSON
contract (200/401/404/405). The /api/* route, /Icons/*, and missing
sub-resources are unaffected. Adds a bare-form regression guard case to the
RUNBOOK PKG-02 battery. Compiles clean (bash build/package.sh compile).

Found by gsd-code-reviewer (CR-01) during phase 05 code-review gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MSVC elevates C4459 (declaration hides global declaration) to an error under
warnings-as-errors. In the unity build FRM_Request.cpp and FicsitRemoteMonitoring.cpp
share a translation unit, so the SendFallbackPage 'DocsURL' parameter shadowed the
file-scope 'static const FString DocsURL'. Linux/Clang did not emit the warning, so
the server-only compile passed while the Windows client target failed to package.
Rename the parameter to InDocsURL (matching the codebase In- prefix convention).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner/reviewer-facing summary of the WebSocket threading, getPlayer,
request-validation, and dedicated-server packaging changes, with a
file map and the live-verification evidence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Coexists with the generic actor-rotation 'pitch' field the upstream owner
adds in porisius#297 (added inside the shared
getActorJSON helper). Our field is the player camera/aim pitch
(GetControlRotation), which is the only pitch meaningful for an upright
player body; keeping a distinct 'lookPitch' key avoids shadowing upstream's
'pitch' (actor body rotation, meaningful for vehicles) when both land.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r file map

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

2 participants