Skip to content

UI Subsystem UniqueId Guard: Learnings and Design

Problem

Remote PlayerState entries on a listen server could reach the UI subsystem before the UniqueId is valid. This caused WidgetControllerReady to run too early, resulting in null/invalid UniqueId usage.

Root Cause

  • Listen server never cached a v2 UniqueNetId for the local player, so PlayerState->UniqueId stayed invalid for both self and remote players.
  • OnRep_UniqueId only fires on clients when the replicated field updates. It does not run on the listen server for remote PlayerStates.
  • The UI subsystem previously only gated on PostNetInit, which is not enough when UniqueId arrives later or is never set.

Solution

  • Key fix for validity (not just timing):
  • CacheLocalPlayerUniqueNetId() seeds the v2 account id from EOS PUID, caches it on the LocalPlayer, and (when authority) sets it on the PlayerState.
  • This makes PlayerState->GetUniqueId() valid on listen servers (even without a NetConnection) and ensures the login packet carries a valid UniqueId for remote clients.
  • The UI timing fixes (OnSetUniqueId) only affect readiness signals; they do not make the UniqueId valid on their own.
  • Gate remote PlayerState UI initialization on both:
  • PostNetInit (replication readiness), and
  • UniqueId readiness (identity assigned).
  • Use two hooks to trigger the UniqueId readiness:
  • OnRep_UniqueId for clients (replication path).
  • OnSetUniqueId for listen server (server-only path).
  • When UniqueId is not ready, queue the PlayerState in the UI subsystem and trigger WidgetControllerReady only after the UniqueId-ready delegate fires.
  • Ensure the UniqueId is actually assigned on listen servers by caching the local player’s v2 UniqueNetId from the EOS PUID and setting it on the PlayerState (done after auth login, on world init, and right before ClientTravel).

Where Implemented

  • Source/Metal_terra/Private/Core/Subsystems/OnlineManagerSubsystem.cpp
  • Builds FUniqueNetIdRepl from PUID, calls LocalPlayer->SetCachedUniqueNetId, and PlayerState->SetUniqueId on authority.
  • Called after auth login and on world init.
  • Source/Metal_terra/Private/Core/Subsystems/Online/OnlineNakamaHouseService.cpp
  • Calls CacheLocalPlayerUniqueNetId() right before ClientTravel to ensure the login message includes the UniqueId.

Pitfalls and Timing Notes

  • Handshake logs can show a PUID, but PlayerState->UniqueId can remain invalid if the local UniqueNetId is never cached/assigned.
  • Reading UniqueId too early will return null/invalid even if EOS P2P is connected.
  • Listen server nuance:
  • Server-side PlayerState never receives OnRep_UniqueId, so OnSetUniqueId is required to notify local UI systems.
  • Dedicated server nuance:
  • UniqueId assignment happens server-side, but the UI runs on clients where OnRep_UniqueId fires as expected.

Client vs Listen vs Dedicated

  • Client:
  • Receives OnRep_UniqueId for remote PlayerStates.
  • UI can rely on the replication callback for readiness.
  • Listen server:
  • Hosts authority and local UI in the same process.
  • Remote PlayerState UniqueId is set on the server, but no OnRep_UniqueId is fired.
  • Must use OnSetUniqueId to mark readiness for local UI.
  • Dedicated server:
  • No local UI.
  • Client UIs get OnRep_UniqueId normally for remote PlayerStates.

Design Guidance

  • Treat UniqueId readiness as a separate lifecycle event from PostNetInit.
  • Keep a queue of PlayerStates awaiting UniqueId for UI binding.
  • Trigger WidgetControllerReady only after both PostNetInit and UniqueId readiness.
  • Log or trace UniqueId readiness when investigating login or UI timing issues.