The game's frontend refuses 'Xbox Live features' based on its OWN static
flag (0x83028851, read by sub_8263E788) BEFORE calling any rexglue API -
which is why no trace logged during the party attempt. The game never sets
the flag without a real Live session. The ghost network loop now forces it
to 1 every tick so the frontend gate opens and the flow reaches the traced
XNet/Xam calls. Fragile: address tied to this title's build.
With --verbose, the relay logs a windowed summary of forwarded game
datagrams ('game traffic: N datagram(s) relayed to M member(s) in room
'XXX'') so the game's own RakNet netcode is observable in the relay log -
distinct from ghost pose joins. Counted per 2s window to avoid per-packet
spam.
- Relay --verbose now logs which client timed out (ip:port, session, room)
instead of silently dropping, so '2 client(s) -> 1 client(s)' is
explainable.
- The M menu Status block shows the netcode transport state: 'Netcode:
relay joined / not connected' and how many sessions are discoverable,
so the RakNet pipeline is visible without grepping logs.
- Pin rexglue to 3ec27d1 (transport receive timeout - the silent-client
disconnect fix).
Typing a room or relay and pressing Enter did not commit: the fields only
wrote the cvar on ImGuiInputTextFlags_EnterReturnsTrue, and Enter was flaky
in the game. Now:
- Room code commits on every keystroke (uppercase), so the relay follows the
typed code immediately. A room change re-joins the relay on the next tick
instead of waiting up to the 5s heartbeat.
- Relay server commits on Enter OR when the field loses focus (clicking
away), so a swallowed Enter can't leave the old relay active; kept
commit-on-deactivate (not per keystroke) so partial hostnames don't flap
the connection while typing.
- Display name commits on change (same Enter flakiness).
- The network loop now splits a 'host:port' server string and uses the
embedded port for both the relay resolve and the game's netcode transport,
so typing '127.0.0.1:40001' works (previously it produced a broken
'host:port:port' resolve).
- SendJoin carries the machine's virtual IP (v5) so the relay can route the
game's UDP and answer session queries.
- While the public relay is connected, keep rexglue's XNetTransport on the
same relay + room (idempotent; reconnects on any change), so the game's
RakNet netcode follows the room the player joined.
The game's own RakNet UDP now has a path through the relay (v5 protocol):
- kTypeGame: opaque game datagrams forwarded to the destination member
(dst_virtual_ip, or broadcast to all room members when 0); the relay
re-stamps the sender's virtual IP so the recipient can reply. This gives
the netcode a P2P route with the server as a pure relay/stabilizer.
- kTypeSessionQuery / kTypeSessionReply: a client asks 'who's in my room?'
and gets the other members' session ids + virtual IPs + names - exactly
what XNetQosLookup must hand the game's session search.
- JoinPayload now carries the client's virtual IP (10.80.0.x identity) so
the relay can route and answer discovery.
Tested end-to-end (unicast + broadcast + room isolation + discovery).
XNetGetTitleXnAddr identity, XnAddrToInAddr/ServerToInAddr virtual-IP
resolution, QosListen/QosLookup, session keys, Connect/GetConnectStatus and
getsockopt are now real in the rexglue netcode-revival branch.
The self-check cube (skate3_ghost_draw_local) appeared on a nearby NPC
because OnLocalSkaterTick read the world from the UpdateBoneTransforms
hook's pack-owner entity, and UpdateBoneTransforms fires for NPC skaters too
- the last writer won, often an NPC.
Local-player capture now happens in OnFrameSceneBuilt (the same per-frame
body-item heuristic the ghost models use): resolve the body item's OWNING
entity via the ctx->entity store and read its world matrix (entity+416),
falling back to the body root-bone translation. The UBT-based capture and
hook call are removed.
The kernel32.lib LNK1104 persists despite MSYS2_ARG_CONV_EXCL because that
only protects command-line ARGUMENTS - MSYS/Git Bash still rewrites the
%LIB% ENVIRONMENT variable (backslashes + semicolons), so link.exe receives
POSIX paths and cannot find kernel32.lib. Set MSYS2_ENV_CONV_EXCL=* too, and
print LIB + warn when it is empty after vcvarsall.
Addresses the known Windows build issues (see /mnt/archive/Issues.txt):
- windows-build.cmd: loads vcvarsall with a stable Windows SDK (WINSDK,
default 10.0.26100.0) fixing kernel32.lib LNK1104; sets
MSYS2_ARG_CONV_EXCL=* so Git Bash does not corrupt %LIB%; inits submodules
(including the broken-pin self-heal) before building.
- release.sh: auto-detect the Title Update package (repo root or game dir);
zip fallback to 7z or PowerShell Compress-Archive (Git Bash has no zip);
upload JSON parsing falls back from python3/python to grep (Git Bash ships
neither python).
When the relay auto-assigns a room, the code lives in the connection state
while the skate3_ghost_room cvar stays empty. The network loop was
overwriting the active room with the empty cvar every 25 ms, making the code
flicker in and out of the menu, and the Copy button copied the empty cvar.
- Loop: only sync the cvar into the active room when it is non-empty;
preserve the server-assigned code otherwise.
- Menu: Copy grabs the ACTIVE room from GetStatus() (works for auto-assigned
rooms, with a brief 'copied' hint); the room field now surfaces the
auto-assigned code instead of staying blank.
Connection state should be visible in the default logs for beta testers:
- 'ghost: joined <relay> room=<code> roster=N' now logs on connection
TRANSITIONS (first join, reconnect, room change) instead of being gated
behind skate3_ghost_debug (which spams every 5s re-JOIN HELLO).
- 'ghost: relay connection lost; reconnecting' logs at warn instead of
debug.
The rexglue codegen tool segfaults/aborts during static teardown AFTER
writing its output (cvar registry SIOF + heap corruption at exit). The
generated source is complete and valid, so generate-skate3/generate-eawebkit
now wrap the invocation with bash -c that verifies the written output files
instead of the exit code - a genuine failure (unresolved calls without the
Title Update package) produces no output and is still caught. Fixes the
release build on toolchains that hit the teardown bug (also applies to CI).
release.sh: glob the actual rexglue runtime lib name (librexruntimerd.so for
relwithdebinfo vs librexruntime.so for release) so packaging ships the lib
the binary links.
A submodule with only a .git pointer looks non-empty to ls -A, so the
self-heal skipped every un-checked-out nested submodule. Check for real
worktree content instead (find excluding .git).
Clarifies that releases ship ONLY the recompiled binaries (skate3 +
librexruntime + ghost_relay + README) - never the game files; the game dump
(default.xex + EAWebkit.xex) is required at BUILD TIME for codegen only.
- tools/release/release.sh: builds + packages a per-platform release zip
(configure -> codegen -> build -> zip), prints a sha256sum, and with
--upload creates/updates a Forgejo Release and attaches the zip via the
API (GITEA_TOKEN). Supports linux/windows/macos presets on their native
hosts.
- .forgejo/workflows/release.yml: optional automation - builds on a
self-hosted Docker runner (ubuntu-24.04 label), self-heals the broken
nested submodule pins via tools/ci/init-submodules.sh, and publishes a
Linux release on v* tags. Needs the game dump mounted on the runner
(codegen only, never uploaded), SUBMODULE_KEY + RELEASE_TOKEN secrets.
- tools/ci/init-submodules.sh: recursive submodule init that self-heals
upstream pins a fresh clone can no longer fetch (the rexglue fork pins
thirdparty/imgui to a commit ocornut/imgui dropped; the script checks out
the upstream default tip for any nested submodule left empty).
- docs/releases.md: what's in a release, the local path, and the optional
CI path.
- .gitignore: keep tools/ci + tools/release, ignore local release zips.
- #pragma pack(1) on all protocol structs: alignas(1) set the struct
alignment but still let the compiler pad members to their natural
alignment - the uint16_t payload_size landed on an even offset, so
PacketHeader was 8 bytes instead of 7 and the relay's
guard dropped every real 7-byte packet
(n=7 < 8). The in-process tests passed only because BuildPacket/PacketSize
used the same inflated size on both sides. With true packing the struct
sizes now equal the wire sizes exactly (PacketHeader=7, PosePayload=3140,
HelloPayload=687, RoomsPayload=1266, ...), verified.
- kGhostDefaultServer -> relay.mithraic.lol (the production beta relay on
the OVH VPS; IPv6 PTR set, IPv4 PTR still pending at OVH).
- docs/ghost-multiplayer.md: the full beta guide - player usage, cvars,
relay deployment (VPS build/install, free-tunnel option, verification),
logging/troubleshooting and security notes.
- tools/ghost_relay.service: hardened systemd unit for the public relay
(Restart=on-failure, NoNewPrivileges, PrivateTmp, ProtectSystem).
- tools/ghost_relay_deploy.sh: one-shot build + install + enable script for
the VPS (GHOST_RELAY_PORT / GHOST_RELAY_VERBOSE overrides; opens the UDP
port via ufw). Verified: the exact build line compiles and runs.
- README: multiplayer pointer under 'How Do I Play?'.
The M menu now has a room browser: a Refresh button asks the relay for its
room list, and the list (room code, player count, up to 4 member names) is
shown as clickable rows - clicking one sets the room code and joins on the
next heartbeat.
- Protocol: kTypeList (client -> relay, empty payload) and kTypeRooms
(relay -> client, RoomsPayload: up to 16 rooms, code + count + names).
LIST works from unjoined addresses so the browser can query before
joining; a joined client's LIST is answered too. Version bumped to 4.
- Relay: BuildRoomList aggregates the client table by room (refused clients
excluded), most-populated first, capped at kGhostMaxRooms.
- Client: RequestRooms()/GetRooms() - the network thread sends LIST to the
best current peer (public relay, LAN peer, or the local host when
hosting) and stores the reply under the shared lock; ROOMS replies from
any other address are ignored. The list is cleared on disable.
- Dialog: CollapsingHeader 'Browse rooms (lobby)' with refresh + selectable
rows.
New relay test (ghost_relay_test6) covers the lobby path; all suites pass.
Production-readiness pass toward 1.0:
- Protocol handshake: JOIN carries kGhostVersion; a mismatched client is
REFUSED by the relay (HELLO session 0) and its poses are never forwarded,
so two builds can never silently corrupt each other's pose stream. The
client surfaces the refusal in the menu and skips refused LAN hosts during
discovery.
- Crash-proofing: every received payload is bounds-checked against its
packet size before parsing (HELLO / PoseChunk / JOIN); both the client
network thread and the relay thread are exception-guarded (a throw logs
and keeps the loop alive instead of hitting std::terminate). The relay no
longer creates client records for unknown-address POSE/LEAVE floods and
enforces a hard client cap (--max-clients, default 64).
- Logging: rate-limited error logging throughout (send failures, resolve
failures, refusals, thread exceptions, client-cap drops). The relay core
uses a build-flag shim (REXLOG in-game, a tiny {} formatter in the
dependency-free standalone), with --verbose join/leave lines for the
server. Game crashes themselves are already handled by the SDK's crash
handlers + file logs; this layer now avoids triggering them and explains
itself when it can't connect.
- Settings persistence: server/room/name/port/rate save into the app's cvar
config on change, so a player's multiplayer setup survives a restart.
- Menu: status now shows protocol refusals and the last error.
Tests updated for the handshake (version-refusal case added); all suites
pass.
Phase 4:
- Cloth: remote ghosts now wear this player's cloth garments (tee/pants).
The local player's rigid character.cloth_ropa items (CPU-sim deformed
root-local VB) are cloned per ghost and drawn with the REMOTE entity world
from the pose, so each ghost is draped in its own root frame. No mesh or
cloth data is transmitted; the shader already applies item.world to the
rigid cloth path (zero blend weights). Sim-inactive (skinned) garments and
garments not near the local body are skipped.
- Nameplates: display names ride the wire. JOIN carries a display name
(skate3_ghost_name cvar, default the OS user name); the relay stores it
per client and echoes {session_id, name} pairs in the HELLO roster
(RosterEntry). Clients re-JOIN every 5 s so the roster/names stay current
as members join and leave. A GhostNameplateOverlay (always registered,
draws only while enabled) projects each ghost's entity origin +1.75 m
through the published camera and draws a colored name label above it.
Uses skate3::native_scene::GetPublishedCamera (new thread-safe snapshot of
the published view-proj/cam) so the UI thread can project world points.
- GhostDialog gains a display-name field. Protocol version bumped to 3.
Phase 3: two robustness fixes for full-model ghosts.
- Wire: a full skeleton pose is ~3.1 KB, above the 1500-byte path MTU, so it
now travels as up to 3 MTU-safe kTypePoseChunk datagrams (<=1.2 KB each,
~1.2 KB total with headers). The relay forwards each chunk verbatim
(re-stamping only the chunk header session id), so its code path is
shared with the legacy single-datagram POSE and needs no pose-body
parsing. GhostPoseBuildChunks / GhostPoseReassembler (inline in the proto
header, standalone-testable) tolerate out-of-order and duplicate chunks,
drop incomplete sequences (the next frame resends), discard a partial on
a newer seq, and special-case a header-only pose as a single chunk.
Receive identity comes from the chunk header (the relay re-stamps it),
not the blob, which the relay never touches.
- Smoothing: each remote ghost now holds a target skeleton (latest received
pose) and a display skeleton chased per render query with exponential
catch-up (time constant ~33 ms), so the 15 Hz bone stream renders as
smooth motion instead of stepping. New/reshaped skeletons snap instead of
blending between layouts.
Reassembly state is per-session on the network thread, bounded (64) and
cleared on prune/disable. Protocol version bumped to 2.
third_party/rexglue-sdk now fetches from the migrated copy on
git.mithraic.cloud (ad3laid3/rexglue-skate3, skate3-sdk-clean) instead of
the upstream GitHub fork, so the project no longer depends on the original
author's repo for its rexglue SDK.
Phase 2: replace the placeholder-cube ghost rendering with real character
models driven by the remote player's skeleton.
- Wire: POSE now carries the sender's skater skeleton (up to 64 bones, 3
float4 rows each, index-ordered exactly like DrawItem::bones), padded with
identity rows so every mesh's referenced bones stay in range. The relay
forwards POSE payloads verbatim (re-stamping only the sender session id),
so the protocol can grow without a relay update.
- Capture: OnFrameSceneBuilt (called from BuildFrameScene pre-publish)
snapshots the local player's body item bone palette (highest-bone-count
skinned non-ropa non-LivingWorld char item near the camera) as the
broadcast pose; defaults the broadcast rate to 15 Hz.
- Render: in RenderScene, each remote ghost is drawn by cloning this
player's own character pieces (body + nearby skinned char items) and
substituting the remote skeleton into the bone ring - same meshes,
textures and cached GPU decodes, no mesh transfer. Ghosts without a pose
(older peers / capture not ready) fall back to the solid-color cube, which
skate3_ghost_draw_local still uses for the self-check.
Known Phase-2 limitations (documented in code): ghosts render with this
player's own character model/outfit, cloth (ropa) garments are skipped (no
remote cloth sim), and hair/alpha pieces render opaque. All confined to
src/native and the hand-written scene hooks.
Bolt-on multiplayer layer confined to src/native and hand-written code (no
codegen output touched):
- GhostRelay core (src/native/skate3_native_ghost_relay.*): small UDP relay
matching clients into rooms by a shareable code; forwards each client's
pose to the other room members. Used by both the standalone binary and
the in-game LAN host fallback.
- Standalone server (tools/ghost_relay_main.cpp, ghost_relay target): runs
on a public VPS or behind a free tunnel; clients connect OUTBOUND, so no
port forwarding is needed on any client.
- In-game module (skate3_native_ghost.*): captures the local player's world
transform at Sk8::UpdateBoneTransforms exit (pack-owner entity +416,
verified layout) and broadcasts it at a fixed rate; auto-connects to the
default public relay, falls back to LAN auto-host + UDP broadcast
discovery when unreachable.
- Renderer: draws a solid-color cube per remote player in RenderScene via
the existing scene PSO tint.a>0 branch (no new shader/SPIR-V).
- UI: GhostDialog (M key) with enable toggle, room code, relay override,
status readout. Off by default.
Wire protocol in skate3_native_ghost_proto.h (JOIN/HELLO/POSE/PING/LEAVE).