Recherche du flux d'encryption UE 4.27 : le client envoie le ClientHello (serveur en
accept), NMT_EncryptionAck sans payload en clair déclenche la résolution de clé côté
client puis son EnableEncryption. On active DTLS en accept et on NE PAS envoie le
Challenge (différé jusqu'à Handshaking completed). Findings détaillés dans NETCODE-RND.
Avec >1 composant actif (stateless + AES), Incoming et Outgoing doivent être en
sens opposés. Les deux étaient en avant -> le stateless tentait de parser des octets
chiffrés. Incoming itère désormais en inverse.
Le client n'entame pas de handshake DTLS et ferme si nos paquets post-ack sont en
clair. AesGcmHandlerComponent (System.Security.Cryptography.AesGcm, [IV12][ct][tag16]),
activé après l'EncryptionAck : ack en clair -> activation -> Challenge chiffré.
Implémente le pendant serveur du DTLSHandlerComponent d'UE (la pile client est
[DTLS, Stateless]) : le client exige un DTLS en mode PSK dont l'identité = user_id.
- DtlsPskStore : table user_id -> PSK 32o, chargée depuis l'env (PROSPECT_DTLS_PSKS,
rendu Vault) — aucune clé en dur ni commitée.
- DtlsPacketTransport : pont entre l'API bloquante DTLS de BouncyCastle et le modèle
paquet-par-paquet du PacketHandler (handshake sur thread dédié).
- ProspectPskTlsServer : serveur DTLS-PSK BouncyCastle (DTLS 1.2, identité=user_id).
- DTLSHandlerComponent : composant du pipeline (Incoming déchiffre / Outgoing chiffre),
activé au NMT_Hello portant un EncryptionToken.
- UWorld.NotifyControlMessage : sur NMT_Hello chiffré, si la PSK de l'identité est
connue -> NMT_EncryptionAck + BeginHandshake ; sinon warning (comme avant).
Compile (0 erreur, BouncyCastle 2.4). ⚠️ Le framing exact DTLS-sur-PacketHandler et
le routage des records de handshake demandent une ITÉRATION EN LIVE contre le vrai
client (invalidable hors client).
Log every unrouted POST at Warning [CAPTURE UNROUTED] (reveals native PlayFab
endpoints the emulator doesn't implement, e.g. /Group/CreateGroup, /Lobby/*) and
every social/squad/invite/friend/matchmaking call (path or body) at Information
[CAPTURE SOCIAL]. Lets us pin the exact invite mechanism when reproduced in game.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The friend tile rendered empty/offline because the UE client couldn't read our
best-guess response shape. Expose each friend's identity/name/steam/presence under
every plausible field name (camelCase + PlayFab PascalCase, flat + nested profile)
under both friends/Friends keys, and log the JSON so we can pin the exact shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The claim function only grants a tier when a FortunaPass2_Rewards TitleData catalog
maps its rewardId (Level_N, sent by the client) to an item/amount. That catalog was
absent (client-side DataTable) so claims granted nothing and stayed unclaimable.
Add a Level_1..Level_100 catalog (K-Marks scaling, Insurance every 5, Aurum every 10)
so players can actually collect Fortuna pass rewards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the upstream Windows/local-run README with a dev-focused one: how the
client redirection works (loader+agent+backend.txt), repo structure, building
and running the API in a container, the TLS cert SAN gotcha, the Gitea CI/CD
branch model (preprod:preprod / main:latest), the Prospect.Client.Config
switch+cert tool, and an up-to-date feature status.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The DT table engine lived in the page footer, after the per-page
<script>DT.init(...)</script> in the body, so DT was undefined when init ran
(ReferenceError, nothing rendered). Move the engine into a DtScript const
injected right after <main>, before any body content, so it is always defined
first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Players and Catalog pages now fetch /admin/api/{players,catalog} JSON and render
a self-contained vanilla-JS data table: clickable column sort, live search,
dropdown filters (auth / category / rarity), page-size selector + prev/next
paging, result count. No CDN, works offline. Server-rendered dashboard,
player detail and titledata pages unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bundle a compact id -> {name,rarity,category} lookup (Data/gameref.json, 1177 entries,
built from community-datamined TCF-Wiki data) and a GameRefService. The admin back-office
now shows readable names + coloured rarity for inventory items instead of raw ids, and a
new /admin/catalog page browses the known game catalog (searchable, by category).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Accounts were created as 'Unknown' and the client never sends a name, so the admin
panel / friends / squad showed no names. Add SteamWebApiService (GetPlayerSummaries),
wired into LoginWithSteam to backfill the DisplayName from the Steam persona when it's
still 'Unknown'. Admin gets a LAN-only /admin/backfill-names action (+ button) to
backfill existing accounts in one shot. Key read from config SteamWebApiKey (Vault ->
env), inert when unset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Filament-style dark UI served directly by the API:
- Dashboard: player/entity/userdata/titledata counts + recent players
- /admin/players: searchable player list (name/id/steam)
- /admin/players/{id}: full player view — balance/faction/Fortuna cards, inventory
table, and every UserData key pretty-printed (collapsible)
- /admin/titledata + /admin/titledata/{key}: browse in-memory title data
Restricted to LAN/loopback callers (RemoteIpAddress check) so it is never reachable
from the public internet even though :8443 is port-forwarded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- GetPlayerStatisticsRequestsClient: renvoyait vide -> renvoie les stats demandées
(0 pour l'instant, raid client-hosted non traçable) pour que l'écran carrière s'affiche.
- UpdatePlayerPresenceState: persiste {inMatch,lastSeenUtc}; GetFriendList lit la présence
et calcule onlineState (online si vu <3min, 2=en raid) au lieu de 0 en dur.
- AddGenericId: persiste l'id de service lié (était un no-op).
(Free-loadout non touché: S3 only, pas de free-loadout en S2.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit of the emulator surface + 4 fixes:
- Fortuna pass level was frozen: nothing ever granted season XP. ClaimActiveContract
now grants + persists FortunaPass{2,3}_SeasonXp (scaled to the contract's reputation)
and returns the new total, so the pass level moves as you complete contracts.
- UpdateUserTitleDisplayName never persisted the rename (echoed back, lost on relog).
Added DbUserService.UpdateDisplayNameAsync and call it.
- GetPlayerSets returned a blank loadout; now reads the persisted LOADOUT key.
- GetCraftingInProgressData was a stub; now returns the persisted CraftingTimer so an
in-progress craft + remaining time survive a menu reopen/relog.
Remaining known gaps documented in Plane (need client data / multiplayer / out of scope).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Regression from the contract-lock feature: the check gated on any non-empty
ContractLockPurchase, but the blueprint data uses the string "None" (not "")
for items with no lock (749/773 items). So virtually every item returned
'Item is locked' before the currency deduction -> players couldn't buy anything
(error shown, K-Marks unchanged). Skip the gate when the value is null/empty/"None".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Symptom: missions done in raid zones weren't saved -> contracts stuck on
'Objective not met', no claim possible.
Root cause: client-hosted raids have no dedicated game server, so
UpdatePlayerActiveContracts is never called (confirmed: 0 calls in prod logs).
Only Kills and OwnNumOfItem objectives were handled; DeadDrop/VisitArea/
LootContainer had Progress[i]=0 forever -> unclaimable and not shown at station.
Fix: EYContractObjectiveType.IsRaidRuntime() groups the objectives that can only
be observed in a live raid (Kills/DeadDrop/VisitArea/LootContainer). Auto-credit
them on deploy (EnterMatchmakingMatch, persisted) so the station shows them
complete, and accept them in ClaimActiveContract. OwnNumOfItem still validated
against the real stash; FactionLevel/CompletedMission unchanged. Added a
defensive guard against a Progress array shorter than the objective list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bit-decoded real client packets: the header is 65 bits (not stock UE 4.27's 64).
One extra bit sits between the ack history and the packet-info payload; consuming
it realigns bHasPacketInfoPayload + jitter + bHasServerFrameTime, and the first
control-channel bunch parses (ChIndex 0, open, reliable = NMT_Hello).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap world.Tick in try/catch so an incompatible/misaligned client packet
logs an error and the server keeps running (stable R&D bench).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Log + drop the packet (with net version) instead of throwing, so a
bit-alignment/version mismatch with the client doesn't kill the server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Cycle client does not derive its initial packet sequences from the
handshake cookie the way stock UE does (its acked server-seq isn't present
anywhere in the cookie), so the cookie-based InitSequence disagreed and
every post-handshake packet was rejected as out-of-order. On the first
packet, adopt the client's announced Seq/AckedSeq instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When GAMESERVER_ADDRESS ("host:port") is set, EnterMatchmaking returns
that server (SingleplayerStation=false) and EnterMatchmakingMatch's
deploy signal carries its address, so the client travels to the real
game server instead of the client-hosted station. Unset = unchanged
(normal preprod). Lets a 2nd preprod run the same :preprod image and
differ only by this env var (dedicated-server test bench).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>