Compare commits

...

4 commits

Author SHA1 Message Date
MiTHRAL
df86553b77 ghost: capture local player transform from the body item, not the UBT hook
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.
2026-08-28 01:20:52 -04:00
MiTHRAL
d809bbc25a windows-build.cmd: also exclude MSYS environment conversion (%LIB%)
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.
2026-08-27 14:19:46 -04:00
MiTHRAL
81189576c0 releases: harden for Windows + one-click windows-build.cmd launcher
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).
2026-08-27 14:02:24 -04:00
MiTHRAL
4ec504ac6c ghost: fix auto-assigned room code flickering + copy button
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.
2026-08-27 12:56:22 -04:00
8 changed files with 213 additions and 42 deletions

View file

@ -50,12 +50,37 @@ Per-platform notes:
| Platform | Build machine | Command | | Platform | Build machine | Command |
|---|---|---| |---|---|---|
| Linux | any Linux with the toolchain (README: Ubuntu 24.04 + Clang 20) | `./tools/release/release.sh` | | Linux | any Linux with the toolchain (README: Ubuntu 24.04 + Clang 20) | `./tools/release/release.sh` |
| Windows | a **Windows** machine (MSVC/clang-cl + Windows SDK, D3D12) | run the same script on Windows (`--os windows`) | | Windows | a **Windows** machine (Visual Studio MSVC + Windows SDK, D3D12) | `tools\release\windows-build.cmd` (see below) |
| macOS | an **Apple Silicon/Intel Mac** (Homebrew LLVM + MoltenVK, per README) | run the same script on macOS | | macOS | an **Apple Silicon/Intel Mac** (Homebrew LLVM + MoltenVK, per README) | `./tools/release/release.sh` |
Cross-compiling Windows/macOS from a Linux box is not practical for this Cross-compiling Windows/macOS from a Linux box is not practical for this
stack (D3D12 on Windows, Cocoa/MoltenVK on macOS need their native SDKs), so stack (D3D12 needs the Windows SDK, Cocoa/MoltenVK need a Mac), so each
each platform's release is built on that platform. platform's release is built on that platform.
### Windows build
On a Windows box, the known build issues (multiple Windows SDKs, Git Bash
`%LIB%` mangling, missing submodules) are handled by the launcher:
```bat
tools\release\windows-build.cmd --tag v2.0.2-mp1 --upload
```
It loads the MSVC environment (`vcvarsall.bat x64 <SDK>`), sets
`MSYS2_ARG_CONV_EXCL=*` so Git Bash doesn't corrupt `%LIB%`, initializes the
submodules (including the self-heal), and runs `release.sh`. Config via env:
- `VCVARSALL` — path to `vcvarsall.bat` (default: VS 18 Community x64).
- `WINSDK` — Windows SDK version (default `10.0.26100.0`; try `10.0.22621.0`
if yours differs — a broken SDK default is what causes `kernel32.lib`
`LNK1104`).
- `GITBASH` — path to Git for Windows `bash.exe`.
`release.sh` now auto-detects the Title Update package (repo root or
`game/`), falls back to PowerShell `Compress-Archive` when `zip` is missing,
and no longer requires `python3` for the upload JSON parsing. The Windows
build also needs the game dump (`game/default.xex` + `EAWebkit.xex`) and the
TU package present on that box (build-time only, never shipped).
## Option B — automated via Forgejo Actions (optional) ## Option B — automated via Forgejo Actions (optional)

View file

@ -17,6 +17,7 @@
#include "skate3_native_ghost_net.h" #include "skate3_native_ghost_net.h"
#include "skate3_native_ghost_relay.h" #include "skate3_native_ghost_relay.h"
#include "skate3_native_entity.h"
#include "skate3_native_guest_read.h" #include "skate3_native_guest_read.h"
#include "skate3_native_palette.h" #include "skate3_native_palette.h"
@ -738,9 +739,16 @@ void GhostNetworkLoop() {
RoomSet(room_buf, room_len, room_str.c_str()); RoomSet(room_buf, room_len, room_str.c_str());
{ {
std::lock_guard<std::mutex> lock(g_mu); std::lock_guard<std::mutex> lock(g_mu);
// A non-empty room cvar is the user's explicit choice: sync it into
// the active room. An EMPTY cvar means "server auto-assigns" - keep
// the code the relay handed back on HELLO. Overwriting the assigned
// code with the empty cvar every 25 ms is what made the room code
// flicker in and out of the multiplayer menu.
if (room_len != 0) {
std::memcpy(g_room, room_buf, kGhostMaxRoomLen); std::memcpy(g_room, room_buf, kGhostMaxRoomLen);
g_room_len = room_len; g_room_len = room_len;
} }
}
if (sock == GHOST_INVALID_SOCKET) { if (sock == GHOST_INVALID_SOCKET) {
sock = GhostSocketCreate(/*allow_broadcast=*/true); sock = GhostSocketCreate(/*allow_broadcast=*/true);
@ -894,24 +902,7 @@ void GhostNetworkLoop() {
} // namespace } // namespace
// ---- capture (sim thread; hook in skate3_native_render.cpp) ------------------ // ---- capture (per built frame; see OnFrameSceneBuilt) ----------------------
void OnLocalSkaterTick(uint8_t* base) {
if (!Enabled()) {
return;
}
const uint32_t entity = skate3::native_palette::CurrentPackOwner();
if (entity == 0) {
return;
}
float world[16];
if (!ReadEntityWorld(base, entity, world)) {
return;
}
std::lock_guard<std::mutex> lock(g_mu);
std::memcpy(g_local_world, world, sizeof(g_local_world));
g_local_valid = true;
g_local_time = NowSec();
}
namespace { namespace {
// Identity rows for a bone (column-vector affine [R|t]): the safe padding // Identity rows for a bone (column-vector affine [R|t]): the safe padding
@ -981,6 +972,33 @@ void OnFrameSceneBuilt(uint8_t* base,
g_local_pose_count = rows; g_local_pose_count = rows;
g_local_pose_valid = true; g_local_pose_valid = true;
g_local_pose_time = NowSec(); g_local_pose_time = NowSec();
// Local player's world transform for the self-check cube / the wire
// pos/rot. Resolve the body item's OWNING ENTITY (ctx -> entity store)
// and read its world matrix - the reliable player signal. The previous
// UBT-hook capture could pick an NPC skater (UpdateBoneTransforms fires
// for them too), which put the cube on a nearby NPC.
float world[16];
bool have_world = false;
if (body->ctx != 0) {
skate3::native_entity::CtxInfo info;
if (skate3::native_entity::LookupCtx(body->ctx, &info) && info.entity != 0 &&
ReadEntityWorld(base, info.entity, world)) {
have_world = true;
}
}
if (!have_world) {
// Fallback: the body item's root bone translation (the player's pelvis
// world position); identity rotation is fine for a debug cube.
std::memset(world, 0, sizeof(world));
world[0] = world[5] = world[10] = world[15] = 1.0f;
world[12] = body->bones[3];
world[13] = body->bones[7];
world[14] = body->bones[11];
}
std::memcpy(g_local_world, world, sizeof(g_local_world));
g_local_valid = true;
g_local_time = NowSec();
} }
void EnsureNetworkThread() { void EnsureNetworkThread() {

View file

@ -31,13 +31,12 @@ struct FrameScene;
namespace skate3::native_ghost { namespace skate3::native_ghost {
// ---- host-side capture (called from skate3_native_render.cpp) -------------
void OnLocalSkaterTick(uint8_t* base);
// ---- host-side pose capture (called from BuildFrameScene pre-publish) ----- // ---- host-side pose capture (called from BuildFrameScene pre-publish) -----
// Snapshot the local player's skater skeleton (the highest-bone-count // Snapshot the local player's skater skeleton (the highest-bone-count
// skinned non-ropa character item near the camera) as the pose this client // skinned non-ropa character item near the camera) as the pose this client
// broadcasts. Called every built frame; gated internally on the enable cvar. // broadcasts, plus the local player's world transform (resolved from that
// item's owning entity). Called every built frame; gated internally on the
// enable cvar.
void OnFrameSceneBuilt(uint8_t* base, const skate3::native_scene::FrameScene& scene); void OnFrameSceneBuilt(uint8_t* base, const skate3::native_scene::FrameScene& scene);
// ---- network thread lifecycle ------------------------------------------------- // ---- network thread lifecycle -------------------------------------------------

View file

@ -45,7 +45,19 @@ void GhostDialog::Toggle() {
void GhostDialog::SyncBuffersFromCvars() { void GhostDialog::SyncBuffersFromCvars() {
const std::string room = REXCVAR_GET(skate3_ghost_room); const std::string room = REXCVAR_GET(skate3_ghost_room);
if (!room.empty()) {
std::snprintf(room_input_, sizeof(room_input_), "%s", room.c_str()); std::snprintf(room_input_, sizeof(room_input_), "%s", room.c_str());
} else {
// Auto-assigned room: the active code comes from the connection, not the
// (empty) cvar - surface it in the input so the user can see/copy it.
skate3::native_ghost::Status st;
if (skate3::native_ghost::GetStatus(&st) && st.room_len > 0) {
std::snprintf(room_input_, sizeof(room_input_), "%.*s",
int(st.room_len), st.room);
} else {
room_input_[0] = '\0';
}
}
const std::string server = REXCVAR_GET(skate3_ghost_server); const std::string server = REXCVAR_GET(skate3_ghost_server);
std::snprintf(server_input_, sizeof(server_input_), "%s", server.c_str()); std::snprintf(server_input_, sizeof(server_input_), "%s", server.c_str());
const std::string name = REXCVAR_GET(skate3_ghost_name); const std::string name = REXCVAR_GET(skate3_ghost_name);
@ -92,8 +104,21 @@ void GhostDialog::OnDraw(ImGuiIO& io) {
ImGui::SetKeyboardFocusHere(-1); ImGui::SetKeyboardFocusHere(-1);
} }
ImGui::SameLine(); ImGui::SameLine();
// Copy the ACTIVE room (the one this player is actually in), which for an
// auto-assigned room lives in the connection state, not the cvar.
if (ImGui::Button("Copy")) { if (ImGui::Button("Copy")) {
ImGui::SetClipboardText(REXCVAR_GET(skate3_ghost_room).c_str()); skate3::native_ghost::Status st;
if (skate3::native_ghost::GetStatus(&st) && st.room_len > 0) {
char buf[skate3::native_ghost::kGhostMaxRoomLen + 1] = {};
std::memcpy(buf, st.room, st.room_len);
ImGui::SetClipboardText(buf);
copy_feedback_ = true;
}
}
if (copy_feedback_) {
ImGui::SameLine();
ImGui::TextDisabled("copied");
copy_feedback_ = false;
} }
ImGui::TextDisabled("Empty = the server assigns one; share it with friends."); ImGui::TextDisabled("Empty = the server assigns one; share it with friends.");

View file

@ -27,6 +27,7 @@ class GhostDialog final : public rex::ui::ImGuiDialog {
bool visible_ = false; bool visible_ = false;
bool synced_ = false; bool synced_ = false;
bool copy_feedback_ = false;
char room_input_[16] = {}; char room_input_[16] = {};
char server_input_[128] = {}; char server_input_[128] = {};
char name_input_[skate3::native_ghost::kGhostMaxNameLen + 1] = {}; char name_input_[skate3::native_ghost::kGhostMaxNameLen + 1] = {};

View file

@ -473,9 +473,6 @@ extern "C" REX_FUNC(sub_827A52C8) {
__imp__sub_827A52C8(ctx, base); __imp__sub_827A52C8(ctx, base);
if (skate3::native_render::Enabled()) { if (skate3::native_render::Enabled()) {
skate3::native_palette::OnBoneTransforms(base, parts, count); skate3::native_palette::OnBoneTransforms(base, parts, count);
// Ghost multiplayer: capture the local player's world transform (the
// pack-owner entity at this point is the player's SkaterPresEntity).
skate3::native_ghost::OnLocalSkaterTick(base);
} }
} }

View file

@ -78,14 +78,46 @@ if [ ! -f "$GAME_ROOT/default.xex" ]; then
exit 1 exit 1
fi fi
# ---- helpers (Windows Git Bash lacks zip/python; provide fallbacks) ---------
json_id() { # read JSON on stdin, print the value of "id"
if command -v python3 >/dev/null 2>&1; then
python3 -c "import sys,json
try: print(json.load(sys.stdin).get('id',''))
except Exception: print('')"
elif command -v python >/dev/null 2>&1; then
python -c "import sys,json
try: print(json.load(sys.stdin).get('id',''))
except Exception: print('')"
else
grep -o '"id":[0-9]*' | head -1 | cut -d: -f2
fi
}
make_zip() { # make_zip <zipname> <dir>
if command -v zip >/dev/null 2>&1; then
(cd "$(dirname "$2")" && zip -r "$(basename "$1")" "$(basename "$2")")
elif command -v 7z >/dev/null 2>&1; then
7z a -tzip "$1" "$2" >/dev/null
elif command -v powershell >/dev/null 2>&1; then
powershell -NoProfile -Command "Compress-Archive -Path '$2' -DestinationPath '$1' -Force"
else
echo "ERROR: no zip tool found (install zip, 7z, or use PowerShell)"; exit 1
fi
}
# ---- build ------------------------------------------------------------------
TU_ARGS="" TU_ARGS=""
if [ -n "$TU" ]; then if [ -n "$TU" ]; then
if [ ! -f "$TU" ]; then echo "ERROR: title update package not found: $TU"; exit 1; fi if [ ! -f "$TU" ]; then echo "ERROR: title update package not found: $TU"; exit 1; fi
TU_ARGS="-DSKATE3_TITLE_UPDATE_PACKAGE=$TU" TU_ARGS="-DSKATE3_TITLE_UPDATE_PACKAGE=$TU"
elif [ -n "$(ls "$REPO"/TU_12K2276_* "$GAME_ROOT"/TU_12K2276_* 2>/dev/null | head -1)" ]; then
TU="$(ls "$REPO"/TU_12K2276_* "$GAME_ROOT"/TU_12K2276_* 2>/dev/null | head -1)"
echo "==> Using title update package: $TU"
TU_ARGS="-DSKATE3_TITLE_UPDATE_PACKAGE=$TU"
fi fi
echo "==> Release $TAG ($EXT) preset=$PRESET game=$GAME_ROOT" echo "==> Release $TAG ($EXT) preset=$PRESET game=$GAME_ROOT"
echo "==> Configure (extra args: ${CMAKE_ARGS:-none})" echo "==> Configure"
cmake --preset "$PRESET" -DSKATE3_GAME_DATA_ROOT="$GAME_ROOT" $TU_ARGS $CMAKE_ARGS cmake --preset "$PRESET" -DSKATE3_GAME_DATA_ROOT="$GAME_ROOT" $TU_ARGS $CMAKE_ARGS
echo "==> Generate recompiled code" echo "==> Generate recompiled code"
cmake --build --preset "$PRESET" --target generate-all --parallel cmake --build --preset "$PRESET" --target generate-all --parallel
@ -116,7 +148,7 @@ if [ -f "$OUT/game/default.xexp" ]; then
fi fi
cp README.md "$STAGE/" cp README.md "$STAGE/"
rm -f "$NAME.zip" rm -f "$NAME.zip"
zip -r "$NAME.zip" "$STAGE" make_zip "$NAME.zip" "$STAGE"
rm -rf "$STAGE" rm -rf "$STAGE"
echo "==> Packaged: $NAME.zip ($(du -h "$NAME.zip" | cut -f1))" echo "==> Packaged: $NAME.zip ($(du -h "$NAME.zip" | cut -f1))"
sha256sum "$NAME.zip" sha256sum "$NAME.zip"
@ -132,15 +164,11 @@ if [ "$UPLOAD" = 1 ]; then
RELEASE_ID="$(curl -s -X POST "$INSTANCE/api/v1/repos/$REPO_FULL/releases" \ RELEASE_ID="$(curl -s -X POST "$INSTANCE/api/v1/repos/$REPO_FULL/releases" \
-H "$AUTH" -H "Content-Type: application/json" \ -H "$AUTH" -H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"Skate 3 Recomp $TAG\",\"target_commitish\":\"main\"}" \ -d "{\"tag_name\":\"$TAG\",\"name\":\"Skate 3 Recomp $TAG\",\"target_commitish\":\"main\"}" \
| python3 -c "import sys,json | json_id)"
try: print(json.load(sys.stdin).get('id',''))
except Exception: print('')")"
if [ -z "$RELEASE_ID" ]; then if [ -z "$RELEASE_ID" ]; then
RELEASE_ID="$(curl -s "$INSTANCE/api/v1/repos/$REPO_FULL/releases/tags/$TAG" -H "$AUTH" \ RELEASE_ID="$(curl -s "$INSTANCE/api/v1/repos/$REPO_FULL/releases/tags/$TAG" -H "$AUTH" \
| python3 -c "import sys,json | json_id)"
try: print(json.load(sys.stdin).get('id',''))
except Exception: print('')")"
fi fi
if [ -z "$RELEASE_ID" ]; then if [ -z "$RELEASE_ID" ]; then
echo "ERROR: could not create/find release '$TAG'"; exit 1 echo "ERROR: could not create/find release '$TAG'"; exit 1
@ -148,9 +176,14 @@ except Exception: print('')")"
curl -s -X POST "$INSTANCE/api/v1/repos/$REPO_FULL/releases/$RELEASE_ID/assets" \ curl -s -X POST "$INSTANCE/api/v1/repos/$REPO_FULL/releases/$RELEASE_ID/assets" \
-H "$AUTH" -F "attachment=@$NAME.zip" -F "name=$NAME.zip" \ -H "$AUTH" -F "attachment=@$NAME.zip" -F "name=$NAME.zip" \
| python3 -c "import sys,json | if command -v python3 >/dev/null 2>&1 || command -v python >/dev/null 2>&1; then
PY=python3; command -v python3 >/dev/null 2>&1 || PY=python
"$PY" -c "import sys,json
try: try:
a=json.load(sys.stdin); print('Uploaded asset', a.get('name'), a.get('size'), 'bytes') a=json.load(sys.stdin); print('Uploaded asset', a.get('name'), a.get('size'), 'bytes')
except Exception as e: print('upload response parse failed:', e)" except Exception as e: print('upload response parse failed:', e)"
else
echo "Upload request sent (no python to parse the response)"
fi
echo "==> Done: https://git.mithraic.cloud/$REPO_FULL/releases/tag/$TAG" echo "==> Done: https://git.mithraic.cloud/$REPO_FULL/releases/tag/$TAG"
fi fi

View file

@ -0,0 +1,73 @@
@echo off
rem One-click Windows release build for skate3recomp-multiplayer.
rem
rem Fixes the known Windows build issues documented in /mnt/archive/Issues.txt
rem (and mirrored in docs/releases.md):
rem 1. stable Windows SDK so kernel32.lib resolves (vcvarsall with an SDK
rem version instead of the broken default),
rem 2. Git Bash %LIB% mangling (MSYS2_ARG_CONV_EXCL=*),
rem 3. missing rexglue submodules (git submodule update --init + self-heal).
rem
rem Usage (from a normal cmd.exe / PowerShell prompt):
rem tools\release\windows-build.cmd [--tag vX.Y.Z] [--upload]
rem
rem Config via environment variables:
rem VCVARSALL path to vcvarsall.bat (default: VS 18 Community x64)
rem WINSDK Windows SDK version (default 10.0.26100.0; try 10.0.22621.0
rem if yours is different)
rem GITBASH path to Git for Windows bash.exe
setlocal
if "%VCVARSALL%"=="" set "VCVARSALL=%ProgramFiles%\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvarsall.bat"
if "%WINSDK%"=="" set "WINSDK=10.0.26100.0"
if "%GITBASH%"=="" set "GITBASH=%ProgramFiles%\Git\bin\bash.exe"
if not exist "%VCVARSALL%" (
echo [ERROR] vcvarsall.bat not found: %VCVARSALL%
echo Set VCVARSALL to your Visual Studio install, e.g.
echo "%%ProgramFiles%%\Microsoft Visual Studio\17\Community\VC\Auxiliary\Build\vcvarsall.bat"
exit /b 1
)
if not exist "%GITBASH%" (
echo [ERROR] Git Bash not found: %GITBASH%
echo Install Git for Windows or set GITBASH.
exit /b 1
)
echo [1/3] Loading MSVC environment (x64, SDK %WINSDK%)
call "%VCVARSALL%" x64 %WINSDK%
if errorlevel 1 (
echo [ERROR] vcvarsall failed. Try a different WINSDK, e.g.
echo set WINSDK=10.0.22621.0
exit /b 1
)
rem Prevent Git Bash from converting/corrupting the MSVC environment.
rem MSYS2_ARG_CONV_EXCL protects command-line ARGUMENTS; the actual
rem kernel32.lib LNK1104 is caused by MSYS rewriting the %LIB% ENVIRONMENT
rem variable (backslashes + semicolons), which requires MSYS2_ENV_CONV_EXCL.
set "MSYS2_ARG_CONV_EXCL=*"
set "MSYS2_ENV_CONV_EXCL=*"
if "%LIB%"=="" (
echo [WARN] LIB is empty after vcvarsall - try a different WINSDK
echo e.g. set WINSDK=10.0.22621.0
)
echo LIB=%LIB%
rem Convert the repo path to forward slashes for Git Bash (%~dp0 is
rem tools\release\ with a trailing backslash; ..\.. reaches the repo root).
set "REPO=%~dp0..\.."
set "REPO=%REPO:\=/%"
echo [2/3] Initializing submodules (rexglue SDK + nested)
"%GITBASH%" --login -c "cd '%REPO%' && git submodule update --init --jobs 4 && bash tools/ci/init-submodules.sh"
if errorlevel 1 (
echo [ERROR] submodule init failed.
exit /b 1
)
echo [3/3] Building release
"%GITBASH%" --login -c "cd '%REPO%' && bash tools/release/release.sh %*"
exit /b %errorlevel%
endlocal