From 6f9e603ffced0589bf8ead713a67d68aabe32fe8 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:56:40 -0400 Subject: [PATCH 001/167] Build portability: pkg-config for every desktop dep, OXDK cxbe path pkg-config is the dependency contract now, with Homebrew keg-only paths folded in when brew exists. README and Makefile list deps for apt, dnf, pacman, brew, MacPorts and MSYS2. cxbe follows OXDK's move under xbox/ and fails loudly instead of silently producing nothing. --- .gitignore | 2 + README.md | 39 +++++- build/Makefile | 212 +++++++++++++++++++++++-------- theseus/desktop/xcloud_audio.cpp | 2 +- 4 files changed, 195 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index a97dcc4..b76b5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ *.wrn *.ilk *.idb +# -MMD writes these beside the compiler's cwd when a build is driven by hand. +*.d Debug/ Release/ Profile/ diff --git a/README.md b/README.md index 1d2d6db..0ec9e65 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,10 @@ The rest is build instructions, architecture notes, and the lineage. Skip if you One list, used by both the [Quick Start](#quick-start) and [Building](#building). "Run" is what a downloaded release needs; "Build" is that plus the toolchain and headers. +The build finds all of these through `pkg-config`, so no particular package +manager is required. Pick whichever one you already use, or install the +libraries by hand and point `PKG_CONFIG_PATH` at their `.pc` files. + **macOS (Homebrew):** ``` @@ -223,6 +227,17 @@ brew install sdl2 sdl2_mixer mpv curl ffmpeg opus brew install sdl2 sdl2_mixer mpv curl ffmpeg opus pkg-config cmake openssl@3 ``` +**macOS (MacPorts):** + +``` +# Run a release +sudo port install libsdl2 libsdl2_mixer mpv curl ffmpeg libopus + +# Build from source +sudo port install libsdl2 libsdl2_mixer mpv curl ffmpeg libopus \ + pkgconfig cmake openssl3 +``` + **Linux (Debian / Ubuntu):** ``` @@ -246,11 +261,27 @@ pacman -S make pkg-config mingw-w64-x86_64-gcc \ mingw-w64-x86_64-vulkan-headers mingw-w64-x86_64-vulkan-loader ``` +**Anything else (Fedora, Arch, nix, source builds):** + +``` +# Fedora +sudo dnf install gcc-c++ pkgconf-pkg-config cmake SDL2-devel SDL2_mixer-devel \ + mpv-libs-devel libcurl-devel openssl-devel \ + ffmpeg-devel opus-devel vulkan-loader-devel libX11-devel + +# Arch +sudo pacman -S base-devel pkgconf cmake sdl2 sdl2_mixer mpv curl openssl \ + ffmpeg opus vulkan-icd-loader libx11 +``` + Notes: +- Nothing in the build shells out to a package manager. If `pkg-config --exists + mpv libcurl libavcodec opus` succeeds, the build will find them, however they + got there. Prefixes off the default search path just need `PKG_CONFIG_PATH`. - Some distros ship `libmpv1` instead of `libmpv2`. Either works. - The FFmpeg runtime libs come in with libmpv. `ffmpeg` / `libopus0` cover the xCloud video and audio decoders. -- OpenSSL is a runtime dependency of xCloud. On macOS it arrives with brew's curl / mpv / ffmpeg, and on Debian `libssl3` is already in the base install, so neither needs it listed for the run case. Building libdatachannel does need the headers, which is why `openssl@3` and `libssl-dev` show up above. +- OpenSSL is a runtime dependency of xCloud. On macOS it arrives alongside curl / mpv / ffmpeg, and on Debian `libssl3` is already in the base install, so neither needs it listed for the run case. Building libdatachannel does need the headers, which is why `openssl@3` and `libssl-dev` show up above. ## Building @@ -259,13 +290,13 @@ Both builds live in this repo and use the same Makefile. ### Xbox Cross-compiles from macOS or Linux using clang + lld-link + cxbe. Requires: -- clang and lld-link (`brew install llvm` on macOS, `apt install clang lld` on Linux) +- clang and lld-link (`apt install clang lld` on Debian, `dnf install clang lld` on Fedora, `pacman -S clang lld` on Arch, `brew install llvm` or `port install clang-17 lld-17` on macOS) - [OXDK](https://github.com/MrMilenko/OXDK) cloned and built - An Xbox SDK source tree (path passed as `XDK_BASE`) ``` git clone https://github.com/MrMilenko/OXDK ~/OXDK -cd ~/OXDK/tools/cxbe && make +cd ~/OXDK/xbox/tools/cxbe && make cd /path/to/Theseus/build make CONFIG=retail XDK_BASE=/path/to/xbox ``` @@ -325,7 +356,7 @@ cd theseus/third-party/libdatachannel # macOS cmake -S . -B build-mac -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 \ - -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" + -DOPENSSL_ROOT_DIR="$OPENSSL_ROOT" # only if cmake can't find OpenSSL itself cmake --build build-mac -j # Linux diff --git a/build/Makefile b/build/Makefile index 49656f2..fe23604 100644 --- a/build/Makefile +++ b/build/Makefile @@ -27,25 +27,42 @@ # Requirements (per target) # ------------------------------------------------------------------------ # Xbox (debug/retail): -# - clang + lld-link (`brew install llvm` on macOS, `apt install clang lld` -# on Linux). Apple clang from the Xcode CLT works on macOS too, as long -# as lld is available via Homebrew llvm. +# - clang + lld-link. Debian `apt install clang lld`, Fedora `dnf install +# clang lld`, Arch `pacman -S clang lld`, macOS `brew install llvm` or +# `port install clang-17 lld-17`. Apple clang from the Xcode CLT works +# on macOS too, as long as an lld-link is on PATH. # - OXDK cloned and built: https://github.com/MrMilenko/OXDK # - An Xbox SDK source tree (headers + libs the dashboard ABI needs at # link time). Path passed via XDK_BASE. # # Native desktop: # - C++17 compiler (clang or gcc). -# - SDL2, SDL2_mixer, OpenGL headers. -# macOS: brew install sdl2 sdl2_mixer -# Debian/Ubuntu: sudo apt install build-essential libsdl2-dev \ -# libsdl2-mixer-dev libgl-dev -# Arch: sudo pacman -S sdl2 sdl2_mixer -# MSYS2 (Windows): pacman -S mingw-w64-x86_64-{gcc,SDL2,SDL2_mixer} +# - pkg-config. every library below is located through it, so no specific +# package manager is assumed. Install the deps however you like and, if +# they sit off the default search path, export PKG_CONFIG_PATH. +# - SDL2, SDL2_mixer, libmpv, libcurl, ffmpeg, opus, OpenGL headers. +# Debian/Ubuntu: sudo apt install build-essential pkg-config \ +# libsdl2-dev libsdl2-mixer-dev \ +# libmpv-dev libcurl4-openssl-dev \ +# libavcodec-dev libavutil-dev \ +# libswscale-dev libopus-dev libgl-dev +# Fedora: sudo dnf install gcc-c++ pkgconf-pkg-config \ +# SDL2-devel SDL2_mixer-devel \ +# mpv-libs-devel libcurl-devel \ +# ffmpeg-devel opus-devel +# Arch: sudo pacman -S base-devel pkgconf sdl2 sdl2_mixer \ +# mpv curl ffmpeg opus +# macOS: brew install sdl2 sdl2_mixer mpv curl ffmpeg opus \ +# pkg-config +# (or: sudo port install libsdl2 libsdl2_mixer mpv \ +# curl ffmpeg libopus pkgconfig) +# MSYS2 (Windows): pacman -S pkg-config mingw-w64-x86_64-{gcc,SDL2,SDL2_mixer,mpv,curl} # # Windows cross (mingw-w64): -# - macOS: brew install mingw-w64 -# Linux: sudo apt install mingw-w64 +# - Debian/Ubuntu: sudo apt install mingw-w64 +# Fedora: sudo dnf install mingw64-gcc-c++ +# Arch: sudo pacman -S mingw-w64-gcc +# macOS: brew install mingw-w64 # - Plus the SDL2 + SDL2_mixer mingw devel tarballs and a locally built # mingw GLEW. See the desktop-win64 section header below for the # one-time setup script. @@ -71,6 +88,9 @@ # ------------------------------------------------------------------------ # XDK_BASE=/path/to/xbox MS XDK source tree (Xbox builds) # OXDK_DIR=/path/to/OXDK OXDK toolkit (Xbox builds) +# PKG_CONFIG=pkg-config Override to use a wrapper or cross variant +# PKG_CONFIG_PATH=/path/pkgconfig Where to find .pc files off the default path +# BGFX_BUILD_TAG=osx-arm64 bgfx .build/ subdir, if not the host default # BUILDS_ROOT=/path/to/output Where build dirs live (default: ~/builds/theseus) # SDL2_MINGW_DIR=/path/to/dir SDL2 mingw devel root (Windows cross) # GLEW_MINGW_DIR=/path/to/dir GLEW source root (Windows cross) @@ -97,7 +117,15 @@ NTOS_INC = $(XDK_BASE)/private/ntos/inc NTOS_XAPI = $(XDK_BASE)/private/ntos/xapi/inc SDK_INC = $(XDK_BASE)/public/sdk/inc DDK_INC = $(XDK_BASE)/public/ddk/inc -CXBE = $(OXDK_DIR)/tools/cxbe/cxbe +# OXDK moved its Xbox bits under xbox/. Take the new path, fall back to the old. +# Never leave this empty: the next token in the XBE recipe starts with '-', which +# make would eat as the ignore-errors prefix and report a successful build that +# produced nothing. An unbuilt cxbe has to fail loudly, naming the path it wanted. +CXBE := $(firstword $(wildcard $(OXDK_DIR)/xbox/tools/cxbe/cxbe \ + $(OXDK_DIR)/tools/cxbe/cxbe)) +ifeq ($(strip $(CXBE)),) +CXBE := $(OXDK_DIR)/xbox/tools/cxbe/cxbe +endif # ============================================================ # Build configuration (debug vs retail) @@ -443,11 +471,69 @@ DESKTOP_TARGET = $(DESKTOP_DIR)/theseus # Detect platform UNAME_S := $(shell uname -s) UNAME_M := $(shell uname -m) + +# ------------------------------------------------------------------ +# Dependency discovery +# ------------------------------------------------------------------ +# pkg-config is the portable path and is what every platform uses. No +# package manager is required or assumed: Homebrew, MacPorts, apt, pacman, +# nix, MSYS2 and a hand-built prefix all work the same way, because all of +# them install .pc files. If yours are somewhere unusual, point +# PKG_CONFIG_PATH at them and nothing else needs to change. +# +# Override PKG_CONFIG to use a wrapper or a cross-prefixed pkg-config. +PKG_CONFIG ?= pkg-config + +# macOS convenience only: Homebrew keeps some formulae keg-only, so their +# .pc files sit off the default search path. If brew happens to be +# installed, fold those directories in. Skipped entirely when it is not, +# and a PKG_CONFIG_PATH you set yourself always wins. +ifeq ($(UNAME_S),Darwin) + BREW ?= $(shell command -v brew 2>/dev/null) + ifneq ($(BREW),) + BREW_KEG_PC := $(shell sep=""; for f in openssl@3 curl ffmpeg opus; do \ + d=$$($(BREW) --prefix $$f 2>/dev/null)/lib/pkgconfig ; \ + [ -d "$$d" ] || continue ; \ + printf '%s%s' "$$sep" "$$d" ; sep=":" ; \ + done) + endif +endif + +# Every probe below is a parse-time $(shell), and make 3.81 (the Xcode CLT make) +# exports variables to recipe subshells ONLY -- an `export PKG_CONFIG_PATH` here +# would never reach them. So put the path on the command line instead. +ifeq ($(strip $(BREW_KEG_PC)),) + PKGC := $(PKG_CONFIG) +else + # Append to an existing PKG_CONFIG_PATH, but never introduce an empty element. + ifeq ($(strip $(PKG_CONFIG_PATH)),) + PKGC_PATH := $(BREW_KEG_PC) + else + PKGC_PATH := $(PKG_CONFIG_PATH):$(BREW_KEG_PC) + endif + PKGC := PKG_CONFIG_PATH="$(PKGC_PATH)" $(PKG_CONFIG) + export PKG_CONFIG_PATH := $(PKGC_PATH) +endif + +# yes/empty. use with ifeq to gate on a package being present. +pkg_have = $(shell $(PKGC) --exists $(1) 2>/dev/null && echo yes) + +# SDL2. sdl2-config ships with every SDL2 install and is the most reliable +# probe; pkg-config is the fallback for distros that drop the script. +ifneq ($(shell command -v sdl2-config 2>/dev/null),) + SDL2_CFLAGS := $(shell sdl2-config --cflags) + SDL2_LDFLAGS := $(shell sdl2-config --libs) +else ifeq ($(call pkg_have,sdl2),yes) + SDL2_CFLAGS := $(shell $(PKGC) --cflags sdl2) + SDL2_LDFLAGS := $(shell $(PKGC) --libs sdl2) +else + SDL2_CFLAGS := + SDL2_LDFLAGS := -lSDL2 +endif + ifeq ($(UNAME_S),Darwin) DESKTOP_CXX ?= clang++ DESKTOP_CC ?= clang - SDL2_CFLAGS := $(shell sdl2-config --cflags 2>/dev/null || echo -I/opt/homebrew/include/SDL2 -I/usr/local/include/SDL2) - SDL2_LDFLAGS := $(shell sdl2-config --libs 2>/dev/null || echo -L/opt/homebrew/lib -lSDL2) # OpenGL is for projectM (the milkdrop visualizer), not a dashboard # render backend; the dashboard renders through bgfx (Metal here). DESKTOP_PLATFORM_LDFLAGS = -framework OpenGL -lSDL2_mixer @@ -456,8 +542,6 @@ else ifeq ($(UNAME_S),Linux) # clang installed can still override with `make desktop DESKTOP_CXX=clang++`. DESKTOP_CXX ?= g++ DESKTOP_CC ?= gcc - SDL2_CFLAGS := $(shell sdl2-config --cflags) - SDL2_LDFLAGS := $(shell sdl2-config --libs) # OpenGL is for projectM, not a dashboard render backend. DESKTOP_PLATFORM_LDFLAGS = -lGL -lSDL2_mixer else @@ -469,8 +553,6 @@ else # dbghelp: sdl_main.cpp's StackWalk64-based panic crash handler. DESKTOP_CXX ?= g++ DESKTOP_CC ?= gcc - SDL2_CFLAGS := $(shell sdl2-config --cflags) - SDL2_LDFLAGS := $(shell sdl2-config --libs) DESKTOP_PLATFORM_LDFLAGS = -lopengl32 -lglew32 -lSDL2_mixer -lws2_32 -ldbghelp endif @@ -491,21 +573,25 @@ endif # target. (Detected via MAKECMDGOALS.) DESKTOP_NATIVE_GOALS := desktop desktop-clean ifneq ($(filter $(DESKTOP_NATIVE_GOALS),$(or $(MAKECMDGOALS),desktop)),) - ifeq ($(shell pkg-config --exists mpv 2>/dev/null && echo yes),yes) - MPV_CFLAGS := $(shell pkg-config --cflags mpv) - MPV_LDFLAGS := $(shell pkg-config --libs mpv) + ifeq ($(call pkg_have,mpv),yes) + MPV_CFLAGS := $(shell $(PKGC) --cflags mpv) + MPV_LDFLAGS := $(shell $(PKGC) --libs mpv) else - $(error libmpv (mpv.pc) not found via pkg-config. Install: macOS \ -`brew install mpv`; Debian/Ubuntu `sudo apt install libmpv-dev`; \ -MSYS2 `pacman -S mingw-w64-x86_64-mpv`) + $(error libmpv (mpv.pc) not found by $(PKG_CONFIG). Debian/Ubuntu \ +`apt install libmpv-dev`; Fedora `dnf install mpv-libs-devel`; Arch \ +`pacman -S mpv`; MSYS2 `pacman -S mingw-w64-x86_64-mpv`; macOS \ +`brew install mpv` or `port install mpv`. Built it yourself? Add its \ +pkgconfig dir to PKG_CONFIG_PATH) endif - ifeq ($(shell pkg-config --exists libcurl 2>/dev/null && echo yes),yes) - CURL_CFLAGS := $(shell pkg-config --cflags libcurl) - CURL_LDFLAGS := $(shell pkg-config --libs libcurl) + ifeq ($(call pkg_have,libcurl),yes) + CURL_CFLAGS := $(shell $(PKGC) --cflags libcurl) + CURL_LDFLAGS := $(shell $(PKGC) --libs libcurl) else - $(error libcurl (libcurl.pc) not found via pkg-config. Install: macOS \ -`brew install curl`; Debian/Ubuntu `sudo apt install libcurl4-openssl-dev`; \ -MSYS2 `pacman -S mingw-w64-x86_64-curl`) + $(error libcurl (libcurl.pc) not found by $(PKG_CONFIG). Debian/Ubuntu \ +`apt install libcurl4-openssl-dev`; Fedora `dnf install libcurl-devel`; Arch \ +`pacman -S curl`; MSYS2 `pacman -S mingw-w64-x86_64-curl`; macOS \ +`brew install curl` or `port install curl`. Built it yourself? Add its \ +pkgconfig dir to PKG_CONFIG_PATH) endif # libprojectM is vendored as a git submodule under # theseus/third-party/projectm and built into a local prefix by the @@ -578,20 +664,28 @@ DESKTOP_LDFLAGS = $(SDL2_LDFLAGS) $(DESKTOP_PLATFORM_LDFLAGS) $(MPV_LDFLAGS) $(C # xCloud video/audio decode uses libavcodec/libswscale + opus directly # (headers already come in via mpv's pkg-config cflags). ffmpeg is a hard # dep of the desktop anyway. -ifeq ($(UNAME_S),Darwin) - DESKTOP_LDFLAGS += -L$(shell brew --prefix ffmpeg)/lib -lavcodec -lavutil -lswscale - DESKTOP_CFLAGS += -I$(shell brew --prefix opus)/include - DESKTOP_LDFLAGS += -L$(shell brew --prefix opus)/lib -lopus -else - DESKTOP_CFLAGS += $(shell pkg-config --cflags opus) - DESKTOP_LDFLAGS += $(shell pkg-config --libs libavcodec libavutil libswscale opus) +# Gated on the native desktop goals: the Xbox and cross targets never consume +# these flags (the cross builds carry their own ffmpeg/opus), and a top-level +# $(error) here would abort them -- and `clean` -- at parse time. +XCLOUD_PKGS := libavcodec libavutil libswscale opus +ifneq ($(filter $(DESKTOP_NATIVE_GOALS),$(or $(MAKECMDGOALS),desktop)),) + ifeq ($(call pkg_have,$(XCLOUD_PKGS)),yes) + DESKTOP_CFLAGS += $(shell $(PKGC) --cflags $(XCLOUD_PKGS)) + DESKTOP_LDFLAGS += $(shell $(PKGC) --libs $(XCLOUD_PKGS)) + else + $(error ffmpeg/opus ($(XCLOUD_PKGS)) not found by $(PKG_CONFIG). \ +Debian/Ubuntu `apt install libavcodec-dev libavutil-dev libswscale-dev libopus-dev`; \ +Fedora `dnf install ffmpeg-devel opus-devel`; Arch `pacman -S ffmpeg opus`; \ +macOS `brew install ffmpeg opus` or `port install ffmpeg opus`) + endif endif # Xbox remote play / xCloud streaming uses libdatachannel for the WebRTC # transport (ICE + DTLS + SCTP datachannels + RTP media). Static libs come # from a one time build under third-party/libdatachannel: # cmake -S . -B build-mac -DBUILD_SHARED_LIBS=OFF -DNO_WEBSOCKET=1 \ -# -DNO_EXAMPLES=1 -DNO_TESTS=1 -DOPENSSL_ROOT_DIR=$(brew --prefix openssl@3) +# -DNO_EXAMPLES=1 -DNO_TESTS=1 +# (add -DOPENSSL_ROOT_DIR=... if cmake cannot find OpenSSL on its own) # cmake --build build-mac -j # The block is skipped (no THESEUS_HAVE_WEBRTC) if the lib is not built yet. LDC_ROOT := $(THESEUS_SRC)/third-party/libdatachannel @@ -608,8 +702,8 @@ ifneq ($(wildcard $(LDC_BUILD)/libdatachannel.a),) $(LDC_BUILD)/deps/libjuice/libjuice.a \ $(LDC_BUILD)/deps/libsrtp/libsrtp2.a \ $(LDC_BUILD)/deps/usrsctp/usrsctplib/libusrsctp.a - ifeq ($(UNAME_S),Darwin) - DESKTOP_LDFLAGS += -L$(shell brew --prefix openssl@3)/lib -lssl -lcrypto + ifeq ($(call pkg_have,libssl libcrypto),yes) + DESKTOP_LDFLAGS += $(shell $(PKGC) --libs libssl libcrypto) else DESKTOP_LDFLAGS += -lssl -lcrypto endif @@ -629,10 +723,28 @@ endif BGFX_ROOT := $(THESEUS_SRC)/third-party/bgfx BX_ROOT := $(THESEUS_SRC)/third-party/bx BIMG_ROOT := $(THESEUS_SRC)/third-party/bimg + +# bgfx names its output dir after the config you built with, so the host +# arch has to pick the same name genie did. Intel Macs land in osx-x64, +# Apple Silicon in osx-arm64. Override BGFX_BUILD_TAG if you built with a +# different genie target. +ifeq ($(UNAME_S),Darwin) + ifeq ($(UNAME_M),arm64) + BGFX_BUILD_TAG ?= osx-arm64 + else + BGFX_BUILD_TAG ?= osx-x64 + endif +else ifeq ($(UNAME_S),Linux) + # bx has no arm64 target dir: genie --gcc=linux-gcc + config=release64 writes + # linux64_gcc on aarch64 as well. See bx/scripts/toolchain.lua. + BGFX_BUILD_TAG ?= linux64_gcc +else + BGFX_BUILD_TAG ?= win64_mingw-gcc +endif DESKTOP_CFLAGS += -DTHESEUS_USE_BGFX=1 \ -I$(BGFX_ROOT)/include -I$(BX_ROOT)/include -I$(BIMG_ROOT)/include ifeq ($(UNAME_S),Darwin) - BGFX_BUILD_DIR := $(BGFX_ROOT)/.build/osx-arm64/bin + BGFX_BUILD_DIR := $(BGFX_ROOT)/.build/$(BGFX_BUILD_TAG)/bin DESKTOP_CFLAGS += -I$(BX_ROOT)/include/compat/osx DESKTOP_LDFLAGS += $(BGFX_BUILD_DIR)/libbgfxRelease.a \ $(BGFX_BUILD_DIR)/libbxRelease.a \ @@ -644,7 +756,7 @@ else ifeq ($(UNAME_S),Linux) # make -C theseus/third-party/bgfx linux-gcc-release64 # bgfx on Linux uses Vulkan by default. Runtime needs vulkan-loader # + X11 system libs; -ldl/-lpthread are for bgfx's RTLD use. - BGFX_BUILD_DIR := $(BGFX_ROOT)/.build/linux64_gcc/bin + BGFX_BUILD_DIR := $(BGFX_ROOT)/.build/$(BGFX_BUILD_TAG)/bin DESKTOP_CFLAGS += -I$(BX_ROOT)/include/compat/linux DESKTOP_LDFLAGS += $(BGFX_BUILD_DIR)/libbgfxRelease.a \ $(BGFX_BUILD_DIR)/libbxRelease.a \ @@ -786,18 +898,7 @@ DESKTOP_DEPS = $(DESKTOP_OBJS:.o=.d) # Run manually with `make -C build shaders-bgfx[-dx11]`; not yet # wired into the desktop rebuild flow since shader edits are # infrequent and require a separate run anyway. -ifeq ($(UNAME_S),Darwin) - ifeq ($(UNAME_M),arm64) - SHADERC_BUILD_DIR := osx-arm64 - else - SHADERC_BUILD_DIR := osx-x64 - endif -else ifeq ($(UNAME_S),Linux) - SHADERC_BUILD_DIR := linux64_gcc -else - SHADERC_BUILD_DIR := win64_mingw-gcc -endif -SHADERC ?= $(THESEUS_SRC)/third-party/bgfx/.build/$(SHADERC_BUILD_DIR)/bin/shadercRelease +SHADERC ?= $(BGFX_ROOT)/.build/$(BGFX_BUILD_TAG)/bin/shadercRelease SHADERS_SRC_DIR := $(THESEUS_SRC)/desktop/shaders SHADERS_OUT_DIR := $(THESEUS_SRC)/../Data/shaders BGFX_SHADER_INC := $(THESEUS_SRC)/third-party/bgfx/src @@ -1170,7 +1271,8 @@ ifneq ($(wildcard $(LDC_WIN64)/libdatachannel.a),) # and the link fails on __imp_ prefixed names against the static archive. WIN64_CFLAGS += -DTHESEUS_HAVE_WEBRTC=1 -DRTC_STATIC \ -I$(LDC_ROOT)/include \ - -I$(MINGW_DEPS)/include + -I$(MINGW_DEPS)/include \ + -I$(MINGW_DEPS)/include/opus # avcodec before swscale before avutil: static archives resolve left to right. WIN64_LDFLAGS += $(LDC_WIN64)/libdatachannel.a \ $(LDC_WIN64)/deps/libjuice/libjuice.a \ diff --git a/theseus/desktop/xcloud_audio.cpp b/theseus/desktop/xcloud_audio.cpp index 024c40f..f3a58a7 100644 --- a/theseus/desktop/xcloud_audio.cpp +++ b/theseus/desktop/xcloud_audio.cpp @@ -11,7 +11,7 @@ #include "xcloud_audio.h" #include -#include +#include #include #include From 0d28e55d645a2dd5f14dabc48cf687756d2fc59d Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:57:27 -0400 Subject: [PATCH 002/167] One place decides the platform shared/platform.h is the first include everywhere. A build is _XBOX or THESEUS_DESKTOP, and a desktop build is one of THESEUS_HOST_WIN/MAC/LINUX. Bad combinations stop the compile. _WIN32 is true on the Xbox too, so Xbox-built code no longer reads it. Retires _DESKTOP, UIX_DESKTOP and _WINDOWS. make lint-platform keeps them out and runs in CI. --- .github/workflows/build.yml | 7 +++++++ build/Makefile | 30 ++++++++++++++++----------- theseus/desktop/materials_desktop.cpp | 2 +- theseus/desktop/platform_shim.h | 4 +--- theseus/desktop/std.h | 1 + theseus/desktop/stdcfg.h | 5 +---- theseus/engine/stdcfg.h | 6 ------ theseus/render/scene_groups.cpp | 6 +++--- theseus/shared/activefile.cpp | 2 +- theseus/shared/file_util.h | 5 ----- theseus/shared/overlay_alert.cpp | 4 ++-- theseus/shared/platform.h | 29 ++++++++++++++++++++++++++ theseus/shared/theseus.h | 10 +++++---- theseus/shared/xap_vm.cpp | 2 +- theseus/xbox/std.h | 1 + 15 files changed, 72 insertions(+), 42 deletions(-) create mode 100644 theseus/shared/platform.h mode change 100755 => 100644 theseus/shared/xap_vm.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3884f5..6399c69 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,13 @@ concurrency: # when authoring new shaders (offline by the contributor). jobs: + lint: + name: Lint / platform flags + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: make -C build lint-platform + desktop-macos: name: Desktop / macOS runs-on: macos-latest diff --git a/build/Makefile b/build/Makefile index fe23604..db7596a 100644 --- a/build/Makefile +++ b/build/Makefile @@ -451,6 +451,24 @@ clean: @# Sweep up the in-tree leftovers from the old layout if present. rm -rf bin bin-retail bin-desktop +# Platform flags live in shared/platform.h. The old desktop spellings stay +# retired, and code the Xbox build compiles never reads _WIN32, which is +# true on the Xbox too. +.PHONY: lint-platform +lint-platform: + @bad=0; \ + if grep -rnwE '_DESKTOP|UIX_DESKTOP|_WINDOWS' $(THESEUS_SRC) \ + --include='*.h' --include='*.cpp' --include='*.c' \ + --exclude-dir=third-party --exclude-dir=xdk --exclude-dir=imgui; then \ + echo "lint-platform: retired flag above. Use _XBOX or THESEUS_DESKTOP."; bad=1; \ + fi; \ + if grep -rnw '_WIN32' $(THESEUS_SRC)/shared $(THESEUS_SRC)/engine $(THESEUS_SRC)/render \ + --include='*.h' --include='*.cpp' --include='*.c' \ + --exclude='stb_*.h' --exclude='platform.h'; then \ + echo "lint-platform: _WIN32 above in Xbox-built code. Use THESEUS_HOST_WIN."; bad=1; \ + fi; \ + [ $$bad -eq 0 ] && echo "lint-platform: clean"; exit $$bad + # ============================================================ # Desktop build (macOS / Linux / Windows) # ============================================================ @@ -622,10 +640,6 @@ DESKTOP_CFLAGS = \ -std=c++17 \ $(if $(DEBUG),-g -O0,-O2 -DNDEBUG) \ -MMD -MP \ - -D_DESKTOP \ - -DTHESEUS_DESKTOP \ - -DUIX_DESKTOP=1 \ - -D_WINDOWS=1 \ -D_THESEUS_STD_H=1 \ -fpermissive \ -fms-extensions \ @@ -1178,10 +1192,6 @@ WIN64_CFLAGS = \ -std=c++17 \ $(if $(DEBUG),-g -O0,-O2 -DNDEBUG) \ -MMD -MP \ - -D_DESKTOP \ - -DTHESEUS_DESKTOP \ - -DUIX_DESKTOP=1 \ - -D_WINDOWS=1 \ -D_THESEUS_STD_H=1 \ -DSDL_MAIN_HANDLED \ -DUIX_CDAUDIO=1 \ @@ -1445,10 +1455,6 @@ LINUX_ARM64_CFLAGS = \ -std=c++17 \ $(if $(DEBUG),-g -O0,-O2 -DNDEBUG) \ -MMD -MP \ - -D_DESKTOP \ - -DTHESEUS_DESKTOP \ - -DUIX_DESKTOP=1 \ - -D_WINDOWS=1 \ -D_THESEUS_STD_H=1 \ -DUIX_CDAUDIO=1 \ -fpermissive \ diff --git a/theseus/desktop/materials_desktop.cpp b/theseus/desktop/materials_desktop.cpp index 2ef11d3..dee1fd6 100755 --- a/theseus/desktop/materials_desktop.cpp +++ b/theseus/desktop/materials_desktop.cpp @@ -1966,7 +1966,7 @@ void CMaxMaterial::Render() m_pMatInfo->Setup(this); -#ifdef _DESKTOP +#ifdef THESEUS_DESKTOP extern IDirect3DDevice8* g_pD3DDev; if (g_pD3DDev) m_pMatInfo->m_lastUsedFrame = g_pD3DDev->m_frameNumber; #endif diff --git a/theseus/desktop/platform_shim.h b/theseus/desktop/platform_shim.h index 301c051..853a763 100644 --- a/theseus/desktop/platform_shim.h +++ b/theseus/desktop/platform_shim.h @@ -6,8 +6,6 @@ #pragma once -#define _WINDOWS 1 // Triggers #ifndef _WINDOWS guards on Xbox headers -#define UIX_DESKTOP 1 #define _THESEUS_STD_H 1 // Prevent toolbox xboxinternals.h kernel type redefs #include "xboxfs_drive.h" // XboxFS_DriveToPrefix shared helper, used by stubs below @@ -116,7 +114,7 @@ typedef intptr_t WPARAM; typedef intptr_t LPARAM; typedef intptr_t LRESULT; -// Window handle stubs (not used in SDL build but needed for _WINDOWS code paths) +// Window handle stubs (not used in SDL build but needed by Win32-style code paths) typedef void* HWND; typedef void* HINSTANCE; typedef wchar_t WCHAR; diff --git a/theseus/desktop/std.h b/theseus/desktop/std.h index 5cf1663..5f0d49f 100755 --- a/theseus/desktop/std.h +++ b/theseus/desktop/std.h @@ -2,6 +2,7 @@ // includes with the SDL2 + OpenGL platform layer. Counterpart to // xbox/std.h. +#include "platform.h" #include "stdcfg.h" // Flag so toolbox/xboxinternals.h knows kernel types are already available diff --git a/theseus/desktop/stdcfg.h b/theseus/desktop/stdcfg.h index c1b38dc..a4b8bee 100755 --- a/theseus/desktop/stdcfg.h +++ b/theseus/desktop/stdcfg.h @@ -1,8 +1,5 @@ // stdcfg.h: desktop build configuration. Counterpart to -// engine/stdcfg.h on Xbox; platform detection uses __APPLE__, -// __linux__, _WIN32. - -#define UIX_DESKTOP 1 +// engine/stdcfg.h on Xbox. Which host OS we're on comes from platform.h. // Desktop uses UTF-8 char strings (SDL2, POSIX, OpenGL are all char*-based). // Xbox used UTF-16LE via _UNICODE/TCHAR. That path has been removed. diff --git a/theseus/engine/stdcfg.h b/theseus/engine/stdcfg.h index bdc1db5..3065937 100755 --- a/theseus/engine/stdcfg.h +++ b/theseus/engine/stdcfg.h @@ -37,12 +37,6 @@ #endif #endif -#ifdef _WINDOWS -#define _LOG -#define _JPEG -//#define _LAN -#endif - #ifdef _PROFILE #undef _DEBUG #endif diff --git a/theseus/render/scene_groups.cpp b/theseus/render/scene_groups.cpp index 013c2ed..a47d397 100755 --- a/theseus/render/scene_groups.cpp +++ b/theseus/render/scene_groups.cpp @@ -540,7 +540,7 @@ class CInline : public CGroup DECLARE_NODE_PROPS() -#ifdef UIX_DESKTOP +#ifdef THESEUS_DESKTOP // Hot-reload from new XAP source text (XAP editor "save" loop) bool ReloadFromText(const char* xapText); #endif @@ -1879,7 +1879,7 @@ void CLevel::Deactivate() } -#ifdef UIX_DESKTOP +#ifdef THESEUS_DESKTOP // ===== XAP live-editor helpers (desktop only) =============================== // CInline reload + Inline-node enumeration for the desktop XAP editor's // "save -> hot reload" loop. Touch CInline's privates so they live in this @@ -1990,4 +1990,4 @@ extern "C" bool ReloadInlineNode(CNode* pInlineNode, const char* xapText) { return false; return ((CInline*)pInlineNode)->ReloadFromText(xapText); } -#endif // UIX_DESKTOP +#endif // THESEUS_DESKTOP diff --git a/theseus/shared/activefile.cpp b/theseus/shared/activefile.cpp index 23a7661..29e475b 100755 --- a/theseus/shared/activefile.cpp +++ b/theseus/shared/activefile.cpp @@ -71,7 +71,7 @@ bool CActiveFile::Fetch(const TCHAR *szURL, bool bSearchAppDir /*=false*/, bool { bool bAlreadyAbsolute = (szURL[0] == '\\' && szURL[1] == '\\') || (szURL[0] != 0 && szURL[1] == ':'); -#ifdef _DESKTOP +#ifdef THESEUS_DESKTOP // A host path is absolute too, and the two tests above only know about // UNC and drive letters. Without this a /Users/... or /home/... URL // gets the current base URL glued to the front of it and never opens. diff --git a/theseus/shared/file_util.h b/theseus/shared/file_util.h index a9aa81f..ad8429d 100755 --- a/theseus/shared/file_util.h +++ b/theseus/shared/file_util.h @@ -27,8 +27,3 @@ DWORD CreateUniqueDir(const TCHAR* szBase); void FindFilePath(TCHAR* szFullPath/*[MAX_PATH]*/, const TCHAR* szFile); int GetFileSize(const TCHAR* szFilePath); - -#ifdef _WINDOWS -bool DeleteDirectory(const TCHAR* szPath); -bool CopyDirectory(const TCHAR* szSrcPath, const TCHAR* szDestPath); -#endif diff --git a/theseus/shared/overlay_alert.cpp b/theseus/shared/overlay_alert.cpp index ffdebeb..9917aa6 100755 --- a/theseus/shared/overlay_alert.cpp +++ b/theseus/shared/overlay_alert.cpp @@ -5,7 +5,7 @@ #include "std.h" #include "theseus.h" #include "node.h" -#ifndef _DESKTOP +#ifndef THESEUS_DESKTOP #include "overlay.h" #endif @@ -36,7 +36,7 @@ END_NODE_PROPS() void COverlayAlert::Show(const TCHAR* szMessage) { -#ifdef _DESKTOP +#ifdef THESEUS_DESKTOP // Overlay UI removed on desktop; ImGui handles alerts. if (szMessage && *szMessage) fprintf(stderr, "[Alert] %s\n", szMessage); diff --git a/theseus/shared/platform.h b/theseus/shared/platform.h new file mode 100644 index 0000000..538ea8b --- /dev/null +++ b/theseus/shared/platform.h @@ -0,0 +1,29 @@ +// platform.h: which build this is. First thing both std.h files include. +// +// Every build is exactly one target: _XBOX (the XBE) or THESEUS_DESKTOP. +// A desktop build also gets exactly one host OS. Don't test _WIN32 to mean +// either one: it's true on the Xbox build too. + +#pragma once + +#if defined(_XBOX) +# if defined(THESEUS_DESKTOP) +# error "_XBOX and THESEUS_DESKTOP are both set. Pick one target." +# endif +# if defined(THESEUS_HOST_WIN) || defined(THESEUS_HOST_MAC) || defined(THESEUS_HOST_LINUX) +# error "The Xbox build has no host OS." +# endif +#else +# ifndef THESEUS_DESKTOP +# define THESEUS_DESKTOP 1 +# endif +# if defined(_WIN32) +# define THESEUS_HOST_WIN 1 +# elif defined(__APPLE__) +# define THESEUS_HOST_MAC 1 +# elif defined(__linux__) +# define THESEUS_HOST_LINUX 1 +# else +# error "Unknown desktop host OS. Theseus builds for Windows, macOS and Linux." +# endif +#endif diff --git a/theseus/shared/theseus.h b/theseus/shared/theseus.h index edbd541..0038ae0 100755 --- a/theseus/shared/theseus.h +++ b/theseus/shared/theseus.h @@ -6,6 +6,8 @@ #ifndef THESEUS_H #define THESEUS_H +#include "platform.h" + #ifndef _XBOX #include #include @@ -27,7 +29,7 @@ extern void UpdateCurDirFromFile(const char* szURL); // files use _FND_CLASS. Alias them so files that set _FND_CLASS compile // under desktop's node.h. Temporary until desktop/node.h adopts the same // convention or the files drop into engine/shared/render wholesale. -#ifdef _DESKTOP +#ifdef THESEUS_DESKTOP #define _NFC _FND_CLASS #endif // Time values are stored as double for fractional precision over long uptimes. @@ -199,7 +201,7 @@ inline void TheseusSetScreen(CScreen* p) { g_pScreen = p; } // Desktop always provides real implementations in desktop/debug.cpp. // Xbox debug build has them in engine/debug.cpp; Xbox retail gets the // inline no-op stubs so unused error logging links cleanly. -#if defined(_DESKTOP) || defined(_DEBUG) +#if defined(THESEUS_DESKTOP) || defined(_DEBUG) void TheseusGetErrorString(HRESULT hr, TCHAR* szBuf, int cchBuf); const TCHAR* TheseusGetErrorString(HRESULT hr); void LogComError(HRESULT hr, const char* szFunc = NULL); @@ -702,7 +704,7 @@ inline HANDLE TheseusCreateFile(const TCHAR* szFileName, DWORD dwDesiredAccess, CleanFilePath(sszFileName, szFileName); #ifdef _XBOX return CreateFile(sszFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); -#elif defined(_WIN32) +#elif defined(THESEUS_HOST_WIN) // Native Windows: use real Win32 CreateFileA so subsequent ReadFile/CloseHandle // calls hit real kernel handles. The fopen path below would return a FILE* // that the real Win32 APIs can't interpret. @@ -725,7 +727,7 @@ inline bool TheseusCreateDirectory(LPCTSTR lpPathName, LPSECURITY_ATTRIBUTES lpS return CreateDirectory(sszFileName, lpSecurityAttributes) != FALSE; #else (void)lpSecurityAttributes; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN return CreateDirectoryA(sszFileName, NULL) != FALSE; #else return mkdir(sszFileName, 0755) == 0 || errno == EEXIST; diff --git a/theseus/shared/xap_vm.cpp b/theseus/shared/xap_vm.cpp old mode 100755 new mode 100644 index d7e622a..e4a0352 --- a/theseus/shared/xap_vm.cpp +++ b/theseus/shared/xap_vm.cpp @@ -832,7 +832,7 @@ void CRunner::Error(const TCHAR* szFmt, ...) DumpStack(); #endif -#ifdef UIX_DESKTOP +#ifdef THESEUS_DESKTOP extern int TheseusMessageBox(const char* szText, unsigned int uType); TheseusMessageBox(szMessage, 0); #endif diff --git a/theseus/xbox/std.h b/theseus/xbox/std.h index 2a20afa..2768d64 100755 --- a/theseus/xbox/std.h +++ b/theseus/xbox/std.h @@ -7,6 +7,7 @@ // !_XBOX + !_WIN32 : POSIX shims + SDL + OpenGL (macOS / Linux) // Check Configuration Options +#include "platform.h" #include "stdcfg.h" // Flag so toolbox/xboxinternals.h knows kernel types are already available From 0da7b4de61df5dd67f58732fab3b0b319d9082db Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:30:32 -0400 Subject: [PATCH 003/167] Xbox paths go through one file layer Code holding an Xbox path calls the Theseus file functions in theseus.h: CreateFile, CreateDirectory, DeleteFile, RemoveDirectory, the attribute and free-space queries, FindFirst/Next/Close, MoveFile, CopyFile and FOpen. The Xbox build passes each straight to XTL. The desktop build translates first (xbox_file.cpp) and host paths call the OS directly. xbox_path.h is the one translator, caller-owned buffers, with a write variant that never follows the virtual-game icon redirect. xboxfs.h, the fopen and Win32-name macros, the POSIX Win32 path stubs, the lite translator and the Windows host-path whitelist are gone. Fixes along the way: Windows created TDATA on a real E: drive, TheseusCreateDirectory made literal Q:\ dirs on mac/Linux, lstrcpyn didn't terminate on POSIX, CGameCopier::DeleteFile recursed on desktop, and texture loads decode the bytes they fetched instead of reopening by name. -Wno-macro-redefined is gone; the build is clean without it. --- build/Makefile | 5 +- theseus/desktop/config.cpp | 9 +- theseus/desktop/d3d8_sdl.h | 18 +- theseus/desktop/dashinit.cpp | 6 +- theseus/desktop/desktop_nodes.cpp | 14 +- theseus/desktop/desktop_stubs.cpp | 3 +- theseus/desktop/files.cpp | 6 +- theseus/desktop/image.cpp | 11 +- theseus/desktop/launch.cpp | 17 +- theseus/desktop/locale.cpp | 2 +- theseus/desktop/platform_shim.h | 335 +------------ theseus/desktop/sdl_main.cpp | 25 +- theseus/desktop/std.h | 15 - theseus/desktop/text.cpp | 6 +- theseus/desktop/title_maker.cpp | 19 - theseus/desktop/xap_editor.cpp | 3 - theseus/desktop/xbox_file.cpp | 557 +++++++++++++++++++++ theseus/desktop/xbox_path.h | 165 ++++++ theseus/desktop/xboxfs.h | 745 ---------------------------- theseus/desktop/xip.cpp | 19 +- theseus/engine/locale_node.cpp | 2 +- theseus/render/asset_loader.cpp | 10 +- theseus/render/text.cpp | 8 +- theseus/shared/audio_system.cpp | 2 +- theseus/shared/file_ops.cpp | 38 +- theseus/shared/file_util.cpp | 8 +- theseus/shared/harddrive.cpp | 46 +- theseus/shared/music_collection.cpp | 14 +- theseus/shared/savegame_grid.cpp | 18 +- theseus/shared/screen.cpp | 2 +- theseus/shared/settingsfile.cpp | 2 +- theseus/shared/theseus.h | 60 ++- theseus/shared/title_collection.cpp | 14 +- theseus/shared/xbe.cpp | 8 +- 34 files changed, 912 insertions(+), 1300 deletions(-) create mode 100644 theseus/desktop/xbox_file.cpp create mode 100644 theseus/desktop/xbox_path.h delete mode 100644 theseus/desktop/xboxfs.h mode change 100755 => 100644 theseus/render/text.cpp mode change 100755 => 100644 theseus/shared/settingsfile.cpp diff --git a/build/Makefile b/build/Makefile index db7596a..8bebb6a 100644 --- a/build/Makefile +++ b/build/Makefile @@ -640,7 +640,6 @@ DESKTOP_CFLAGS = \ -std=c++17 \ $(if $(DEBUG),-g -O0,-O2 -DNDEBUG) \ -MMD -MP \ - -D_THESEUS_STD_H=1 \ -fpermissive \ -fms-extensions \ $(SDL2_CFLAGS) \ @@ -663,7 +662,6 @@ DESKTOP_CFLAGS = \ -Wno-dangling-else \ -Wno-format \ -Wno-microsoft-exception-spec \ - -Wno-macro-redefined \ -Wno-microsoft \ -Wno-null-conversion \ -Wno-c++11-narrowing \ @@ -792,6 +790,7 @@ DESKTOP_SRCS_ENGINE = \ $(THESEUS_SRC)/shared/date_node.cpp DESKTOP_SRCS_FILE = \ + $(THESEUS_SRC)/desktop/xbox_file.cpp \ $(THESEUS_SRC)/desktop/xip.cpp \ $(THESEUS_SRC)/shared/file_util.cpp \ $(THESEUS_SRC)/desktop/files.cpp \ @@ -1192,7 +1191,6 @@ WIN64_CFLAGS = \ -std=c++17 \ $(if $(DEBUG),-g -O0,-O2 -DNDEBUG) \ -MMD -MP \ - -D_THESEUS_STD_H=1 \ -DSDL_MAIN_HANDLED \ -DUIX_CDAUDIO=1 \ -fpermissive \ @@ -1455,7 +1453,6 @@ LINUX_ARM64_CFLAGS = \ -std=c++17 \ $(if $(DEBUG),-g -O0,-O2 -DNDEBUG) \ -MMD -MP \ - -D_THESEUS_STD_H=1 \ -DUIX_CDAUDIO=1 \ -fpermissive \ -I$(LINUX_ARM64_SDL2_INC) \ diff --git a/theseus/desktop/config.cpp b/theseus/desktop/config.cpp index d7e29c4..305a8bb 100755 --- a/theseus/desktop/config.cpp +++ b/theseus/desktop/config.cpp @@ -531,15 +531,8 @@ int CConfig::NtFileExists(const char* FileName) // Translate Xbox path to local filesystem { - const char* translated = XboxFS_TranslatePath(szFile); char szLocal[MAX_PATH]; - if (translated) { - strncpy(szLocal, translated, MAX_PATH - 1); - szLocal[MAX_PATH - 1] = 0; - } else { - strncpy(szLocal, szFile, MAX_PATH - 1); - szLocal[MAX_PATH - 1] = 0; - } + XboxPath_Translate(szFile, szLocal, sizeof(szLocal)); for (char* p = szLocal; *p; p++) { if (*p == '\\') *p = '/'; } int result = (access(szLocal, F_OK) == 0) ? 1 : 0; diff --git a/theseus/desktop/d3d8_sdl.h b/theseus/desktop/d3d8_sdl.h index 452dae9..7862e2b 100644 --- a/theseus/desktop/d3d8_sdl.h +++ b/theseus/desktop/d3d8_sdl.h @@ -388,8 +388,10 @@ struct D3DXCOLOR { #define TEXT(x) _T(x) #endif -// _stscanf +// _stscanf. mingw's tchar.h already maps it to sscanf in an ANSI build. +#ifndef _stscanf #define _stscanf sscanf +#endif // D3DX mesh stubs #define D3DXMESH_MANAGED D3DPOOL_MANAGED @@ -2388,20 +2390,8 @@ IDirect3DTexture8* XBX_ParseTexture(const BYTE* pbContent, int cbContent); inline HRESULT D3DXCreateTextureFromFileA(IDirect3DDevice8* dev, const char* path, IDirect3DTexture8** ppTex) { (void)dev; *ppTex = NULL; - // Try to load the file from the xboxfs-mapped path + // Host path only. Xbox paths go through TheseusFOpen first. FILE* f = fopen(path, "rb"); - if (!f) { - // Try drive remap (Q:\path -> Data/path, etc.). - char mapped[1024]; - if (path[0] && path[1] == ':' && (path[2] == '\\' || path[2] == '/')) { - const char* prefix = XboxFS_DriveToPrefix(path[0]); - if (prefix) { - snprintf(mapped, sizeof(mapped), "%s/%s", prefix, path + 3); - for (char* p = mapped; *p; p++) if (*p == '\\') *p = '/'; - f = fopen(mapped, "rb"); - } - } - } if (!f) return E_FAIL; fseek(f, 0, SEEK_END); long size = ftell(f); diff --git a/theseus/desktop/dashinit.cpp b/theseus/desktop/dashinit.cpp index 2be5437..7f2cdde 100755 --- a/theseus/desktop/dashinit.cpp +++ b/theseus/desktop/dashinit.cpp @@ -130,9 +130,9 @@ void DashInit() { // Desktop: drives are mounted via xboxfs path mapping; no symlinks needed. // Ensure music directory exists. - CreateDirectory("e:\\TDATA", NULL); - CreateDirectory("e:\\TDATA\\fffe0000", NULL); - CreateDirectory("e:\\TDATA\\fffe0000\\music", NULL); + TheseusCreateDirectory("e:\\TDATA", NULL); + TheseusCreateDirectory("e:\\TDATA\\fffe0000", NULL); + TheseusCreateDirectory("e:\\TDATA\\fffe0000\\music", NULL); // Set current region g_nCurRegion = 1; // NA diff --git a/theseus/desktop/desktop_nodes.cpp b/theseus/desktop/desktop_nodes.cpp index 3b1e037..f16d8a6 100644 --- a/theseus/desktop/desktop_nodes.cpp +++ b/theseus/desktop/desktop_nodes.cpp @@ -36,8 +36,8 @@ static const char* ResolveAudioURL(const TCHAR* url) // Check if it already has a drive letter (Q:\...) if (url[0] && url[1] == ':') { // Use XboxFS translation - const char* translated = XboxFS_TranslatePath(url); - return translated; + XboxPath_Translate(url, s_path, sizeof(s_path)); + return s_path; } // Relative path; prepend Data/ @@ -1389,7 +1389,7 @@ static bool TryOpenXboxHDD(); // forward declaration // Defined in sdl_main.cpp, loaded from desktop.ini extern char g_qcowPath[512]; -// Called from xboxfs.h CreateFileA fallback to read files from FATX +// Called from TheseusCreateFile (xbox_file.cpp) to read save data off FATX. FILE* XboxFS_FATXReadFile(const char* xboxPath) { if (!s_xboxHDD.IsOpen()) { if (!s_xboxHDDTried) TryOpenXboxHDD(); // forward decl below @@ -1684,7 +1684,7 @@ static bool ParseXboxMetaValue(const char* xboxPath, const char* key, char* outB outBuf[0] = 0; // Read file through xboxfs layer - HANDLE hFile = CreateFile(xboxPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); + HANDLE hFile = TheseusCreateFile(xboxPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) return false; unsigned char data[8192]; DWORD bytesRead = 0; @@ -1826,7 +1826,7 @@ static void EnumerateTitles() title.hasImage = true; } else { // Synthetic UDATA entries fall back to Configs/icons/. - // xboxfs.h's C: routing maps "C:\icons\..." -> "Configs/icons/...". + // xbox_path.h maps "C:\icons\..." to "Configs/icons/...". snprintf(hostProbe, sizeof(hostProbe), "%s", AppPathf("Configs/icons/%s.jpg", entName)); if (access(hostProbe, F_OK) == 0) { snprintf(title.imagePath, sizeof(title.imagePath), @@ -2000,7 +2000,7 @@ static void LoadDesktopIcons() if (stat(iconPath, &_ist) != 0) continue; } - // Store Xbox-style path for VGames icon redirect in xboxfs.h + // Store the Xbox path so xbox_path.h can redirect it to the cached icon. snprintf(s_iconEntries[s_iconCount].iconPath, MAX_PATH, "%s:\\%s\\%s\\icon.jpg", g_vgames.games[i].drive, @@ -4415,7 +4415,7 @@ class CGamesCollection : public CNode return new CStrObject(g_vgames.games[m_slice[t]].launch); } - // Returns the Xbox-style icon path. xboxfs.h intercepts the load and + // Returns the Xbox-style icon path. xbox_path.h intercepts the load and // redirects to Configs/icons/.{jpg,png} via VGames_GetIconPath. CStrObject* GetIcon(int i) { diff --git a/theseus/desktop/desktop_stubs.cpp b/theseus/desktop/desktop_stubs.cpp index 305cccf..42d1ed9 100644 --- a/theseus/desktop/desktop_stubs.cpp +++ b/theseus/desktop/desktop_stubs.cpp @@ -108,7 +108,8 @@ int CXBExecutable::ReadFile(const char* szFileName, const bool, const bool) { strncpy(m_szFileName, szFileName, sizeof(m_szFileName) - 1); // Try to open the real file first - const char* translated = XboxFS_TranslatePath(szFileName); + char translated[512]; + XboxPath_Translate(szFileName, translated, sizeof(translated)); FILE* f = fopen(translated, "rb"); if (f) { // Real XBE file exists; read header for title info. diff --git a/theseus/desktop/files.cpp b/theseus/desktop/files.cpp index 30d1951..b5ac74a 100755 --- a/theseus/desktop/files.cpp +++ b/theseus/desktop/files.cpp @@ -159,7 +159,7 @@ void CFolder::Refresh(const char* szPath) char szWildA [MAX_PATH]; Ansi(szWildA, szWild, MAX_PATH); - h = FindFirstFile(szWildA, &fd); + h = TheseusFindFirstFile(szWildA, &fd); if (h == INVALID_HANDLE_VALUE) { @@ -238,9 +238,9 @@ void CFolder::Refresh(const char* szPath) pFile->m_length = fd.nFileSizeLow; } } - while (FindNextFile(h, &fd)); + while (TheseusFindNextFile(h, &fd)); - FindClose(h); + TheseusFindClose(h); } static int __cdecl CmpName(const void* p1, const void* p2) diff --git a/theseus/desktop/image.cpp b/theseus/desktop/image.cpp index 6c2aaf5..813b4ac 100644 --- a/theseus/desktop/image.cpp +++ b/theseus/desktop/image.cpp @@ -165,11 +165,10 @@ LPDIRECT3DTEXTURE8 LoadTexture(const char *szURL, unsigned int width, unsigned i if (ext && (strcasecmp(ext, ".tga") == 0 || strcasecmp(ext, ".bmp") == 0 || strcasecmp(ext, ".dds") == 0 || strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".jpeg") == 0)) { - // D3DX for standard formats - char ansiPath[MAX_PATH]; - Ansi(ansiPath, szURL, MAX_PATH); + // Decode the bytes Fetch already has; reopening by name would + // miss the virtual-game icon redirect. LPDIRECT3DTEXTURE8 lpTexture = NULL; - if (SUCCEEDED(D3DXCreateTextureFromFileA(TheseusGetD3DDev(), ansiPath, &lpTexture))) + if (SUCCEEDED(D3DXCreateTextureFromFileInMemory(TheseusGetD3DDev(), file.GetContent(), file.GetContentLength(), &lpTexture))) return lpTexture; } else @@ -196,7 +195,7 @@ LPDIRECT3DTEXTURE8 LoadTextureFromXPR(const char *xprfile) if (xprfile == NULL) return NULL; - FILE* f = fopen(xprfile, "rb"); + FILE* f = TheseusFOpen(xprfile, "rb"); if (!f) { // Try drive-letter remap (C: -> Configs, Q: -> Data, E: -> Library). char mapped[MAX_PATH]; @@ -352,7 +351,7 @@ HIMAGE MyLoadImage(const char *szURL) bool DecodeRAW(const char *szFileName, CTexture *pTexture) { - FILE *fp = _tfopen(szFileName, "rb"); + FILE *fp = TheseusFOpen(szFileName, "rb"); if (fp == NULL) return false; diff --git a/theseus/desktop/launch.cpp b/theseus/desktop/launch.cpp index 483f1f2..70c99ae 100644 --- a/theseus/desktop/launch.cpp +++ b/theseus/desktop/launch.cpp @@ -27,7 +27,6 @@ #include "stb_image.h" #include "path_template.h" #include "launchers/launcher.h" -#include extern "C" void DashAudio_MuteAll(void); extern SDL_Window* g_pSDLWindow; @@ -111,13 +110,6 @@ static void TrimOuterQuotes(char* s) s[len - 2] = '\0'; } -#ifdef _WIN32 -// xboxfs.h macro-routes these through the Xbox drive translator. A launch -// spec is a real Windows path by definition, so ask the OS directly. -#undef GetFileAttributesA -#undef GetFileAttributes -#endif - static bool IsExistingFile(const char* path) { #ifdef _WIN32 @@ -485,17 +477,14 @@ void DesktopLaunchTitle(const char* devicePath) return; } - // Translate via the canonical xboxfs.h resolver. handles both + // Translate via xbox_path.h. Handles both // drive-letter ("E:\...") and device-path ("\Device\Harddisk0\...") // forms and routes to Library/Configs/Data as appropriate. - const char* translated = XboxFS_TranslatePath(devicePath); - if (!translated || !*translated) { + char localPath[512]; + if (!XboxPath_Translate(devicePath, localPath, sizeof(localPath)) || !*localPath) { fprintf(stderr, "[launch] DesktopLaunchTitle: unrecognized path format: %s\n", devicePath); return; } - char localPath[512]; - strncpy(localPath, translated, sizeof(localPath) - 1); - localPath[sizeof(localPath) - 1] = '\0'; // Strip trailing slash for VGames matching. size_t pathLen = strlen(localPath); diff --git a/theseus/desktop/locale.cpp b/theseus/desktop/locale.cpp index f975b6d..ce957ac 100755 --- a/theseus/desktop/locale.cpp +++ b/theseus/desktop/locale.cpp @@ -58,7 +58,7 @@ static void LoadXlate(const char* szXlateSection) char * tempPath; tempPath = (char*)malloc(MAX_PATH); sprintf( tempPath, "Q:\\Language\\%s.dat", szXlateSection ); - g_hXlate = CreateFile( tempPath, GENERIC_ALL, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, NULL ); + g_hXlate = TheseusCreateFile( tempPath, GENERIC_ALL, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, NULL ); if (g_hXlate != INVALID_HANDLE_VALUE) { uint32_t datSize = GetFileSize( g_hXlate, NULL ); diff --git a/theseus/desktop/platform_shim.h b/theseus/desktop/platform_shim.h index 853a763..671e132 100644 --- a/theseus/desktop/platform_shim.h +++ b/theseus/desktop/platform_shim.h @@ -6,9 +6,8 @@ #pragma once -#define _THESEUS_STD_H 1 // Prevent toolbox xboxinternals.h kernel type redefs - #include "xboxfs_drive.h" // XboxFS_DriveToPrefix shared helper, used by stubs below +#include "xbox_path.h" // the one Xbox path translator, used by stubs below #include #ifdef __cplusplus @@ -151,7 +150,14 @@ inline void DeleteCriticalSection(CRITICAL_SECTION*) {} #define lstrlen strlen #define lstrcpy strcpy #define lstrcat strcat -#define lstrcpyn strncpy +// Win32 semantics: copies at most n-1 chars and always terminates. +inline char* lstrcpyn(char* dst, const char* src, int n) { + if (n <= 0) return dst; + size_t i = 0; + for (; i + 1 < (size_t)n && src[i]; i++) dst[i] = src[i]; + dst[i] = '\0'; + return dst; +} // SYSTEMTIME stub typedef struct _SYSTEMTIME { @@ -250,6 +256,7 @@ typedef struct _WIN32_FILE_ATTRIBUTE_DATA { #define FILE_FLAG_SEQUENTIAL_SCAN 0x08000000 #define FILE_SHARE_DELETE 4 #define FILE_SHARE_WRITE 2 +typedef int GET_FILEEX_INFO_LEVELS; #define GetFileExInfoStandard 0 #define INVALID_FILE_ATTRIBUTES ((DWORD)-1) @@ -266,270 +273,8 @@ typedef struct _WIN32_FILE_ATTRIBUTE_DATA { // Virtual games database (needed by all platforms) #include "virtual_games.h" -// FindFirstFile/FindNextFile/FindClose; POSIX implementation for non-Windows -#ifndef _WIN32 - -struct FindFileHandle { - DIR* dir; - char dirPath[512]; - char pattern[256]; // filename glob pattern (e.g. "*.xtf") - // Virtual game injection - int vgIndices[VGAMES_MAX]; // indices into g_vgames - int vgCount; // number of virtual entries for this directory - int vgPos; // current position in virtual list - char vgDrive[4]; // drive letter for this search (if applicable) - char vgCategory[32]; // category for this search (if applicable) -}; -inline HANDLE FindFirstFile(const char* path, WIN32_FIND_DATA* fd) { - if (!path || !fd) return INVALID_HANDLE_VALUE; - - // Translate Xbox-style paths (e.g. "Q:\Fonts\*.xtf" -> "Data/Fonts/*.xtf"). - char localPath[512]; - const char* colon = strchr(path, ':'); - if (colon && (colon[1] == '\\' || colon[1] == '/')) { - int driveLen = (int)(colon - path); - if (driveLen == 1) { - const char* prefix = XboxFS_DriveToPrefix(path[0]); - if (prefix) { - snprintf(localPath, sizeof(localPath), "%s/%s", prefix, colon + 2); - } else { - // Unknown drive (F:, G:, R:, ...) -- no analog on desktop. - // Pass through as-is so opendir below fails naturally. - strncpy(localPath, path, sizeof(localPath) - 1); - localPath[sizeof(localPath) - 1] = '\0'; - } - } else { - strncpy(localPath, path, sizeof(localPath) - 1); - localPath[sizeof(localPath) - 1] = '\0'; - } - } else { - strncpy(localPath, path, sizeof(localPath) - 1); - localPath[sizeof(localPath) - 1] = '\0'; - } - // Convert backslashes - for (char* p = localPath; *p; p++) { if (*p == '\\') *p = '/'; } - - // Find last / to split dir from pattern - char* lastSlash = strrchr(localPath, '/'); - char dirBuf[512], patBuf[256]; - if (lastSlash) { - size_t dirLen = lastSlash - localPath; - memcpy(dirBuf, localPath, dirLen); - dirBuf[dirLen] = '\0'; - strncpy(patBuf, lastSlash + 1, sizeof(patBuf) - 1); - patBuf[sizeof(patBuf) - 1] = '\0'; - } else { - strcpy(dirBuf, "."); - strncpy(patBuf, localPath, sizeof(patBuf) - 1); - patBuf[sizeof(patBuf) - 1] = '\0'; - } - - DIR* dir = opendir(dirBuf); - if (!dir) { - // Directory doesn't exist; but it might be a virtual game folder - // Check if dirBuf matches a virtual game: xboxfs/{drive}/{cat}/{name} - int vgIdx = VGames_MatchFolder(dirBuf); - if (vgIdx >= 0) { - // Virtual folder; check what file pattern is being requested - if (fnmatch("*.xbe", patBuf, FNM_CASEFOLD) == 0 || - fnmatch("default.xbe", patBuf, FNM_CASEFOLD) == 0) { - // Return a fake default.xbe - memset(fd, 0, sizeof(*fd)); - strcpy(fd->cFileName, "default.xbe"); - fd->nFileSizeLow = 4096; // fake size - FindFileHandle* ffh = new FindFileHandle; - ffh->dir = NULL; // no real dir - strncpy(ffh->dirPath, dirBuf, sizeof(ffh->dirPath) - 1); - strncpy(ffh->pattern, patBuf, sizeof(ffh->pattern) - 1); - ffh->vgCount = 0; ffh->vgPos = 0; - ffh->vgDrive[0] = 0; ffh->vgCategory[0] = 0; - return (HANDLE)ffh; - } - if (fnmatch("default.uixshortcut", patBuf, FNM_CASEFOLD) == 0 || - fnmatch("*.*", patBuf, FNM_CASEFOLD) == 0 || - strcmp(patBuf, "*") == 0) { - memset(fd, 0, sizeof(*fd)); - strcpy(fd->cFileName, "default.uixshortcut"); - fd->nFileSizeLow = 256; - FindFileHandle* ffh = new FindFileHandle; - ffh->dir = NULL; - strncpy(ffh->dirPath, dirBuf, sizeof(ffh->dirPath) - 1); - strncpy(ffh->pattern, patBuf, sizeof(ffh->pattern) - 1); - ffh->vgCount = 0; ffh->vgPos = 0; - ffh->vgDrive[0] = 0; ffh->vgCategory[0] = 0; - return (HANDLE)ffh; - } - } - return INVALID_HANDLE_VALUE; - } - - FindFileHandle* ffh = new FindFileHandle; - ffh->dir = dir; - strncpy(ffh->dirPath, dirBuf, sizeof(ffh->dirPath) - 1); - ffh->dirPath[sizeof(ffh->dirPath) - 1] = '\0'; - // Windows compat: "*.*" matches all files including those without dots - if (strcmp(patBuf, "*.*") == 0) - strcpy(patBuf, "*"); - strncpy(ffh->pattern, patBuf, sizeof(ffh->pattern) - 1); - ffh->pattern[sizeof(ffh->pattern) - 1] = '\0'; - - // Inject virtual games via shared VGames_ParseDir. - ffh->vgCount = 0; - ffh->vgPos = 0; - ffh->vgDrive[0] = 0; - ffh->vgCategory[0] = 0; - { - char driveBuf[4] = {0}; - char catBuf[32] = {0}; - if (VGames_ParseDir(dirBuf, driveBuf, catBuf, sizeof(catBuf))) { - strncpy(ffh->vgDrive, driveBuf, sizeof(ffh->vgDrive) - 1); - strncpy(ffh->vgCategory, catBuf, sizeof(ffh->vgCategory) - 1); - ffh->vgCount = VGames_GetForDirectory(driveBuf, catBuf, ffh->vgIndices, VGAMES_MAX); - } - } - - // Find first matching entry (real filesystem) - struct dirent* entry; - while ((entry = readdir(ffh->dir)) != NULL) { - if (fnmatch(ffh->pattern, entry->d_name, FNM_CASEFOLD) == 0) { - memset(fd, 0, sizeof(*fd)); - strncpy(fd->cFileName, entry->d_name, sizeof(fd->cFileName) - 1); - char fullPath[512]; - snprintf(fullPath, sizeof(fullPath), "%s/%s", ffh->dirPath, entry->d_name); - struct stat st; - if (stat(fullPath, &st) == 0) { - if (S_ISDIR(st.st_mode)) fd->dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY; - fd->nFileSizeLow = (DWORD)st.st_size; - } - return (HANDLE)ffh; - } - } - - // No real entries; try virtual game entries - while (ffh->vgPos < ffh->vgCount) { - int idx = ffh->vgIndices[ffh->vgPos++]; - VirtualGame& vg = g_vgames.games[idx]; - if (!vg.valid) continue; - if (fnmatch(ffh->pattern, vg.name, FNM_CASEFOLD) != 0) continue; - memset(fd, 0, sizeof(*fd)); - strncpy(fd->cFileName, vg.name, sizeof(fd->cFileName) - 1); - fd->dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY; - return (HANDLE)ffh; - } - - // No match found at all - closedir(ffh->dir); - delete ffh; - return INVALID_HANDLE_VALUE; -} -inline BOOL FindNextFile(HANDLE h, WIN32_FIND_DATA* fd) { - if (!h || h == INVALID_HANDLE_VALUE || !fd) return FALSE; - FindFileHandle* ffh = (FindFileHandle*)h; - - // First exhaust real directory entries (if we have a real dir) - if (!ffh->dir) goto virtual_entries; - struct dirent* entry; - while ((entry = readdir(ffh->dir)) != NULL) { - if (fnmatch(ffh->pattern, entry->d_name, FNM_CASEFOLD) == 0) { - memset(fd, 0, sizeof(*fd)); - strncpy(fd->cFileName, entry->d_name, sizeof(fd->cFileName) - 1); - char fullPath[512]; - snprintf(fullPath, sizeof(fullPath), "%s/%s", ffh->dirPath, entry->d_name); - struct stat st; - if (stat(fullPath, &st) == 0) { - if (S_ISDIR(st.st_mode)) fd->dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY; - fd->nFileSizeLow = (DWORD)st.st_size; - } - return TRUE; - } - } - - // Then serve virtual game entries - virtual_entries: - while (ffh->vgPos < ffh->vgCount) { - int idx = ffh->vgIndices[ffh->vgPos++]; - VirtualGame& vg = g_vgames.games[idx]; - if (!vg.valid) continue; - // Skip if a real folder with this name already exists - char checkPath[512]; - snprintf(checkPath, sizeof(checkPath), "%s/%s", ffh->dirPath, vg.name); - struct stat cst; - if (stat(checkPath, &cst) == 0) continue; // real folder takes priority - // Check against glob pattern - if (fnmatch(ffh->pattern, vg.name, FNM_CASEFOLD) != 0) continue; - memset(fd, 0, sizeof(*fd)); - strncpy(fd->cFileName, vg.name, sizeof(fd->cFileName) - 1); - fd->dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY; - return TRUE; - } - - return FALSE; -} -inline BOOL FindClose(HANDLE h) { - if (h && h != INVALID_HANDLE_VALUE) { - FindFileHandle* ffh = (FindFileHandle*)h; - if (ffh->dir) closedir(ffh->dir); - delete ffh; - } - return TRUE; -} -#endif // !_WIN32 - -// Simple Xbox path translation for stubs below (full version in xboxfs.h). -// Converts "C:\..." -> "Configs/...", "Q:\..." -> "Data/...", -// "E:\..." -> "Library/...". For unknown drives (F:, G:, R:, ...) returns -// an empty string so callers' stat/mkdir/fopen all fail predictably; do -// NOT pass through unchanged, since Preloader_Mkdirp would then create -// a literal "F:" directory on disk. -inline const char* _StubTranslatePath(const char* xboxPath) { - static char s_stubBuf[512]; - if (!xboxPath) return xboxPath; - const char* colon = strchr(xboxPath, ':'); - if (colon && (colon[1] == '\\' || colon[1] == '/')) { - int driveLen = (int)(colon - xboxPath); - if (driveLen == 1) { - const char* prefix = XboxFS_DriveToPrefix(xboxPath[0]); - if (prefix) { - snprintf(s_stubBuf, sizeof(s_stubBuf), "%s/%s", prefix, colon + 2); - for (char* p = s_stubBuf; *p; p++) { if (*p == '\\') *p = '/'; } - return s_stubBuf; - } - } - // Drive letter present but unrecognized -- fail safe. - s_stubBuf[0] = '\0'; - return s_stubBuf; - } - return xboxPath; -} - // Win32 API function stubs (only needed on non-Windows platforms) #ifndef _WIN32 -inline BOOL GetFileAttributesEx(const char* path, int level, void* info) { - (void)level; (void)info; - const char* p = path; - if (path && strchr(path, ':')) p = _StubTranslatePath(path); - struct stat st; - if (stat(p, &st) == 0) { - // Fill in basic WIN32_FILE_ATTRIBUTE_DATA if info is provided - if (info) { - WIN32_FILE_ATTRIBUTE_DATA* fad = (WIN32_FILE_ATTRIBUTE_DATA*)info; - memset(fad, 0, sizeof(*fad)); - fad->dwFileAttributes = S_ISDIR(st.st_mode) ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL; - fad->nFileSizeLow = (DWORD)(st.st_size & 0xFFFFFFFF); - fad->nFileSizeHigh = (DWORD)(st.st_size >> 32); - } - return TRUE; - } - return FALSE; -} -inline DWORD GetFileAttributes(const char* path) { - const char* p = path; - if (path && strchr(path, ':')) p = _StubTranslatePath(path); - struct stat st; - if (stat(p, &st) == 0) - return S_ISDIR(st.st_mode) ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL; - return INVALID_FILE_ATTRIBUTES; -} inline BOOL GetFileTime(HANDLE h, FILETIME* c, FILETIME* a, FILETIME* w) { (void)h; FILETIME ft = {0, 0}; @@ -543,11 +288,6 @@ inline BOOL CompareFileTime(const FILETIME* a, const FILETIME* b) { if (a->dwLowDateTime != b->dwLowDateTime) return a->dwLowDateTime > b->dwLowDateTime ? 1 : -1; return 0; } -inline BOOL RemoveDirectory(const char* path) { - const char* p = path; - if (path && strchr(path, ':')) p = _StubTranslatePath(path); - return rmdir(p) == 0; -} inline DWORD GetLastError() { return 0; } inline void GetLocalTime(SYSTEMTIME* st) { time_t now = time(NULL); @@ -696,7 +436,6 @@ typedef TCHAR* LPTSTR; #define _totlower tolower #define _tcstod strtod #define _itoa(val, buf, radix) snprintf(buf, 32, "%d", val) -#define lstrcpyn(dst, src, n) strncpy(dst, src, n) #define _tfopen fopen #define _ftprintf fprintf @@ -803,8 +542,8 @@ typedef struct { BYTE Signature[20]; } XCALCSIG_SIGNATURE; #ifndef _WIN32 -// File API stubs -// NOTE: CreateFileA is defined in xboxfs.h with path translation +// Handle-based file stubs. POSIX file HANDLEs are FILE*s, opened by +// TheseusCreateFile in xbox_file.cpp. inline BOOL ReadFile(HANDLE h, void* buf, DWORD nBytes, DWORD* nRead, void* ovlp) { size_t r = fread(buf, 1, nBytes, (FILE*)h); if (nRead) *nRead = (DWORD)r; @@ -831,6 +570,12 @@ inline DWORD GetFileSize(HANDLE h, DWORD* high) { #define OPEN_EXISTING 3 #define CREATE_ALWAYS 2 #define OPEN_ALWAYS 4 +#ifndef CREATE_NEW +#define CREATE_NEW 1 +#endif +#ifndef TRUNCATE_EXISTING +#define TRUNCATE_EXISTING 5 +#endif #define FILE_BEGIN 0 #define FILE_CURRENT 1 #define FILE_END 2 @@ -849,13 +594,6 @@ inline BOOL SetFileTime(HANDLE h, const FILETIME* c, const FILETIME* a, const FI return TRUE; } -// SetFileAttributes stub -inline BOOL SetFileAttributes(const char* path, DWORD attr) { - (void)path; (void)attr; - return TRUE; -} -#define SetFileAttributesA SetFileAttributes - // CharUpper/CharLower (Win32 string functions) inline LPTSTR CharUpper(LPTSTR p) { if ((uintptr_t)p < 256) return (LPTSTR)(uintptr_t)toupper((int)(uintptr_t)p); @@ -868,21 +606,8 @@ inline LPTSTR CharLower(LPTSTR p) { return p; } -// DeleteFile is provided by xboxfs.h with path translation -// CreateDirectory #include -inline BOOL CreateDirectory(const char* path, void* sa) { - (void)sa; - const char* p = path; - if (path && strchr(path, ':')) p = _StubTranslatePath(path); -#ifdef _WIN32 - return CreateDirectoryA(p, NULL); -#else - return mkdir(p, 0755) == 0 || errno == EEXIST; -#endif -} -#define CreateDirectoryA CreateDirectory // CreateThread stub (single-threaded on POSIX for now) typedef DWORD (*LPTHREAD_START_ROUTINE)(void*); @@ -1044,16 +769,6 @@ typedef union _ULARGE_INTEGER { unsigned long long QuadPart; } ULARGE_INTEGER; -// GetDiskFreeSpaceEx stub -inline BOOL GetDiskFreeSpaceEx(const char* path, ULARGE_INTEGER* lpFree, - ULARGE_INTEGER* lpTotal, ULARGE_INTEGER* lpTotalFree) { - (void)path; - if (lpFree) lpFree->QuadPart = 0; - if (lpTotal) lpTotal->QuadPart = 0; - if (lpTotalFree) lpTotalFree->QuadPart = 0; - return TRUE; -} -#define GetDiskFreeSpaceExA GetDiskFreeSpaceEx #endif // !_WIN32 @@ -1080,10 +795,8 @@ inline int WideCharToMultiByte(UINT codePage, DWORD flags, const wchar_t* src, i return len; } -// FindFirstFileA / FindNextFileA - same as FindFirstFile (ANSI build) +// ANSI build, so the A struct is the struct. typedef WIN32_FIND_DATA WIN32_FIND_DATAA; -#define FindFirstFileA FindFirstFile -#define FindNextFileA FindNextFile // __debugbreak #define __debugbreak() SDL_TriggerBreakpoint() @@ -1115,12 +828,6 @@ typedef WIN32_FIND_DATA WIN32_FIND_DATAA; // ------------------------------------------------------- #ifndef _WIN32 -// DeleteFileA stub -inline BOOL DeleteFileA(const char* path) { - const char* p = path; - if (path && strchr(path, ':')) p = _StubTranslatePath(path); - return remove(p) == 0; -} #endif // !_WIN32 // IN_ADDR struct (Windows already has this from winsock2.h) @@ -1156,8 +863,4 @@ inline int _uix_swprintf(wchar_t* buf, const wchar_t* fmt, ...) { #define swprintf _uix_swprintf #endif // !_WIN32 -// Xbox filesystem path translation; provides CreateFile/fopen/etc. -// macro redirection so engine code that calls Win32 file APIs works -// transparently on all platforms via xboxfs path mapping. -#include "xboxfs.h" diff --git a/theseus/desktop/sdl_main.cpp b/theseus/desktop/sdl_main.cpp index 7ca83f7..94ec2f5 100644 --- a/theseus/desktop/sdl_main.cpp +++ b/theseus/desktop/sdl_main.cpp @@ -56,8 +56,6 @@ bgfx::ShaderHandle theseus_bgfx_load_shader(const char* name) } char path[512]; snprintf(path, sizeof(path), "%s", AppPathf("Data/shaders/%s/%s.bin", sub, name)); - // fopen is macro-routed through xboxfs.h's translator and AppPathf is - // absolute, so this relies on XboxFS_IsHostPath to skip the drive remap. FILE* f = fopen(path, "rb"); if (!f) { fprintf(stderr, "bgfx: shader file not found: %s\n", path); @@ -175,27 +173,6 @@ volatile DWORD g_hangStartTime = 0; #define STBI_ONLY_BMP #include "stb_image.h" -// Platform code uses native file I/O; undo Xbox filesystem macros -// Dashboard engine files (config.cpp, Runner.cpp, xip.cpp, etc.) keep the macros -// so they think they're on an Xbox. We don't. -#undef fopen -#undef _tfopen -#ifdef _WIN32 -#undef CreateFile -#undef CreateFileA -#undef FindFirstFile -#undef FindFirstFileA -#undef FindNextFile -#undef FindNextFileA -#undef FindClose -#undef GetFileAttributes -#undef GetFileAttributesA -#undef GetFileAttributesEx -#undef GetFileAttributesExA -#undef RemoveDirectory -#undef RemoveDirectoryA -#endif - // Virtual games database VirtualGameDB g_vgames = {}; @@ -648,7 +625,7 @@ void SaveDesktopSettings() { fprintf(fp, "JellyfinUserId=%s\n", g_jellyfinUserId); fprintf(fp, "JellyfinClientId=%s\n", g_jellyfinClientId); fprintf(fp, "JellyfinUserName=%s\n", g_jellyfinUserName); - // Legacy Q:\System\config.ini sections (aliased to this file by xboxfs.h). + // Legacy Q:\System\config.ini sections (aliased to this file by xbox_path.h). fprintf(fp, "\n[Progressive]\n"); fprintf(fp, "Use 720p=%s\n", g_use720p); fprintf(fp, "Use Progressive=%s\n", g_useProgressive); diff --git a/theseus/desktop/std.h b/theseus/desktop/std.h index 5f0d49f..069b589 100755 --- a/theseus/desktop/std.h +++ b/theseus/desktop/std.h @@ -14,8 +14,6 @@ // D3D8 type stubs (no-op interfaces, math types, enums) #include "d3d8_sdl.h" -// Filesystem path translation (Q:\ -> ./Data/) -#include "xboxfs.h" // Standard C/C++ headers (already included by platform_shim.h, but // some source files expect these to come from std.h) @@ -118,19 +116,6 @@ typedef RGBQUAD* LPRGBQUAD; #define MAX_COMPUTERNAME_LENGTH 15 -// MoveFile stub (translates Xbox paths). DeleteFile / CreateFile / WriteFile / -// CreateDirectory and the thread / critical section / event stubs all live -// in platform_shim.h. -inline BOOL MoveFile(const char* src, const char* dst) { - const char* s = src; - const char* d = dst; - if (src && strchr(src, ':')) s = _StubTranslatePath(src); - char srcBuf[512]; - strncpy(srcBuf, s, sizeof(srcBuf) - 1); srcBuf[sizeof(srcBuf) - 1] = '\0'; - if (dst && strchr(dst, ':')) d = _StubTranslatePath(dst); - return rename(srcBuf, d) == 0; -} - // GetCurrentThreadId (platform_shim.h doesn't provide this one) inline DWORD GetCurrentThreadId() { return (DWORD)SDL_ThreadID(); } #else diff --git a/theseus/desktop/text.cpp b/theseus/desktop/text.cpp index bcc2274..b1a39a7 100755 --- a/theseus/desktop/text.cpp +++ b/theseus/desktop/text.cpp @@ -926,7 +926,7 @@ void InitFontTable() strcpy(szWild, g_sFontDir); strcpy(szWild + cchAppDir, "*.xtf"); - h = FindFirstFile(szWild, &fd); + h = TheseusFindFirstFile(szWild, &fd); if (h == INVALID_HANDLE_VALUE) { @@ -988,9 +988,9 @@ void InitFontTable() CloseHandle(hFont); } } - while (FindNextFile(h, &fd)); + while (TheseusFindNextFile(h, &fd)); - FindClose(h); + TheseusFindClose(h); } CFont* GetFont(const char* szFaceName) diff --git a/theseus/desktop/title_maker.cpp b/theseus/desktop/title_maker.cpp index a58aef8..8f5cd57 100644 --- a/theseus/desktop/title_maker.cpp +++ b/theseus/desktop/title_maker.cpp @@ -31,25 +31,6 @@ #include #endif -// Platform code uses native file I/O; undo Xbox filesystem macros. -#undef fopen -#undef _tfopen -#ifdef _WIN32 -#undef CreateFile -#undef CreateFileA -#undef FindFirstFile -#undef FindFirstFileA -#undef FindNextFile -#undef FindNextFileA -#undef FindClose -#undef GetFileAttributes -#undef GetFileAttributesA -#undef GetFileAttributesEx -#undef GetFileAttributesExA -#undef RemoveDirectory -#undef RemoveDirectoryA -#endif - bool g_titleMakerOpen = false; // ============================================================================ diff --git a/theseus/desktop/xap_editor.cpp b/theseus/desktop/xap_editor.cpp index 31b665a..33126f2 100644 --- a/theseus/desktop/xap_editor.cpp +++ b/theseus/desktop/xap_editor.cpp @@ -31,9 +31,6 @@ #include #endif -#undef fopen -#undef _tfopen - // Implemented in group.cpp extern "C" int CollectInlineNodes(CNode* pRoot, CInstance* pRootInst, void* outBuf, int maxEntries); extern "C" bool ReloadInlineNode(CNode* pInlineNode, const char* xapText); diff --git a/theseus/desktop/xbox_file.cpp b/theseus/desktop/xbox_file.cpp new file mode 100644 index 0000000..a13506a --- /dev/null +++ b/theseus/desktop/xbox_file.cpp @@ -0,0 +1,557 @@ +// xbox_file.cpp: the desktop side of the Theseus file calls in theseus.h. +// Every path in here is an Xbox path. It goes through xbox_path.h first, +// then hits the host. Host paths never come through here; code holding one +// calls the OS directly. +// +// Virtual games live here too: a folder that only exists in games.ini still +// lists, and its default.xbe opens as a synthesized header. + +#include "std.h" +#include "theseus.h" +#include "file_util.h" + +#include +#include +#include +#ifdef THESEUS_HOST_WIN +#include +#else +#include +#include +#include +#endif + +extern char g_qcowPath[512]; +FILE* XboxFS_FATXReadFile(const char* xboxPath); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +static bool IsWriteDisposition(DWORD access, DWORD disp) { + return (access & GENERIC_WRITE) || disp == CREATE_ALWAYS || disp == CREATE_NEW || + disp == OPEN_ALWAYS || disp == TRUNCATE_EXISTING; +} + +static void HostMkdir(const char* dir) { +#ifdef THESEUS_HOST_WIN + _mkdir(dir); +#else + mkdir(dir, 0755); +#endif +} + +// Creates every missing parent of a translated host path. +static void MakeParents(const char* hostPath) { + char dir[512]; + snprintf(dir, sizeof(dir), "%s", hostPath); + char* last = strrchr(dir, '/'); + if (!last) return; + *last = '\0'; + for (char* p = dir + 1; *p; p++) { + if (*p != '/') continue; + *p = '\0'; + HostMkdir(dir); + *p = '/'; + } + HostMkdir(dir); +} + +#ifdef THESEUS_HOST_WIN +static void ToBackslash(char* s) { + for (; *s; s++) if (*s == '/') *s = '\\'; +} +#endif + +// Minimal fake XBE for a virtual game: a 0x178-byte header then a 0x1EC-byte +// certificate carrying the title name (UTF-16) and title ID. Title +// enumeration only reads those two structures. +static size_t FillFakeXBE(uint8_t* buf, size_t bufSize, const char* titleName, unsigned long titleID) { + const size_t need = 0x178 + 0x1EC; + if (bufSize < need) return 0; + memset(buf, 0, need); + + buf[0] = 'X'; buf[1] = 'B'; buf[2] = 'E'; buf[3] = 'H'; + + uint32_t base = 0x00010000; + memcpy(buf + 0x104, &base, 4); + uint32_t hdrSize = (uint32_t)need; + memcpy(buf + 0x108, &hdrSize, 4); + uint32_t certAddr = base + 0x178; + memcpy(buf + 0x118, &certAddr, 4); + + uint8_t* cert = buf + 0x178; + uint32_t certSize = 0x1EC; + uint32_t tid = (uint32_t)titleID; + memcpy(cert + 0x00, &certSize, 4); + memcpy(cert + 0x08, &tid, 4); + + uint16_t* titleNameU = (uint16_t*)(cert + 0x0C); + for (int i = 0; i < 40 && titleName[i]; i++) + titleNameU[i] = (uint16_t)(unsigned char)titleName[i]; + + return need; +} + +// If hostPath is /default.xbe, hand back a fake one. +static HANDLE OpenVirtualXBE(const char* hostPath) { + char folder[512]; + snprintf(folder, sizeof(folder), "%s", hostPath); + for (char* p = folder; *p; p++) if (*p == '\\') *p = '/'; + char* last = strrchr(folder, '/'); + if (!last) return INVALID_HANDLE_VALUE; + const char* fileName = last + 1; + *last = '\0'; + if (strcasecmp(fileName, "default.xbe") != 0) return INVALID_HANDLE_VALUE; + + int vgIdx = VGames_MatchFolder(folder); + if (vgIdx < 0) return INVALID_HANDLE_VALUE; + const VirtualGame& vg = g_vgames.games[vgIdx]; + unsigned long tid = strtoul(vg.titleID, NULL, 16); + + uint8_t buf[0x178 + 0x1EC]; + size_t n = FillFakeXBE(buf, sizeof(buf), vg.name, tid); + if (!n) return INVALID_HANDLE_VALUE; + +#ifdef THESEUS_HOST_WIN + // Real HANDLEs and FILE*s aren't interchangeable here, so back it with a + // temp file the OS deletes on close. + char tempDir[MAX_PATH], tempFile[MAX_PATH]; + DWORD len = GetTempPathA(MAX_PATH, tempDir); + if (len == 0 || len > MAX_PATH) return INVALID_HANDLE_VALUE; + if (GetTempFileNameA(tempDir, "xbe", 0, tempFile) == 0) return INVALID_HANDLE_VALUE; + HANDLE h = CreateFileA(tempFile, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, NULL); + if (h == INVALID_HANDLE_VALUE) return INVALID_HANDLE_VALUE; + DWORD written = 0; + WriteFile(h, buf, (DWORD)n, &written, NULL); + SetFilePointer(h, 0, NULL, FILE_BEGIN); +#else + // POSIX handles are FILE*, so an anonymous tmpfile does it. + FILE* f = tmpfile(); + if (!f) return INVALID_HANDLE_VALUE; + fwrite(buf, 1, n, f); + fseek(f, 0, SEEK_SET); + HANDLE h = (HANDLE)f; +#endif + fprintf(stderr, "[xboxfs] Virtual XBE for '%s' (TID=%08lX)\n", vg.name, tid); + return h; +} + +// --------------------------------------------------------------------------- +// Files and folders +// --------------------------------------------------------------------------- + +HANDLE TheseusCreateFile(const TCHAR* szFileName, DWORD access, DWORD share, + LPSECURITY_ATTRIBUTES sa, DWORD disp, DWORD flags, HANDLE templ) +{ + if (!szFileName) return INVALID_HANDLE_VALUE; + char xboxPath[MAX_PATH]; + CleanFilePath(xboxPath, szFileName); + + bool writes = IsWriteDisposition(access, disp); + char host[512]; + if (writes) XboxPath_TranslateForWrite(xboxPath, host, sizeof(host)); + else XboxPath_Translate(xboxPath, host, sizeof(host)); + if (!host[0]) return INVALID_HANDLE_VALUE; + +#ifdef THESEUS_HOST_WIN + ToBackslash(host); + // Xbox flags desktop Windows can't honour: NO_BUFFERING wants sector + // aligned reads, OVERLAPPED wants async setup. + flags &= ~(FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED); + HANDLE h = CreateFileA(host, access, share, sa, disp, flags, templ); + if (h != INVALID_HANDLE_VALUE) return h; + return writes ? INVALID_HANDLE_VALUE : OpenVirtualXBE(host); +#else + (void)share; (void)sa; (void)flags; (void)templ; + const char* mode = "rb"; + if (access & GENERIC_WRITE) mode = (disp == CREATE_ALWAYS) ? "wb" : "r+b"; + if (writes) MakeParents(host); + FILE* f = fopen(host, mode); + if (!f && (disp == CREATE_ALWAYS || disp == OPEN_ALWAYS)) + f = fopen(host, "wb"); + if (f) return (HANDLE)f; + if (writes) return INVALID_HANDLE_VALUE; + + HANDLE fake = OpenVirtualXBE(host); + if (fake != INVALID_HANDLE_VALUE) return fake; + + // Save data can also come off a mounted qcow2 FATX image. + if (g_qcowPath[0] && (strstr(xboxPath, "UDATA") || strstr(xboxPath, "TDATA"))) { + f = XboxFS_FATXReadFile(xboxPath); + if (f) { + fprintf(stderr, "[xboxfs] Loaded from qcow2 FATX\n"); + return (HANDLE)f; + } + } + return INVALID_HANDLE_VALUE; +#endif +} + +bool TheseusCreateDirectory(LPCTSTR lpPathName, LPSECURITY_ATTRIBUTES) +{ + if (!lpPathName) return false; + char xboxPath[MAX_PATH]; + CleanFilePath(xboxPath, lpPathName); + char host[512]; + if (!XboxPath_TranslateForWrite(xboxPath, host, sizeof(host)) || !host[0]) return false; +#ifdef THESEUS_HOST_WIN + ToBackslash(host); + return CreateDirectoryA(host, NULL) != FALSE; +#else + return mkdir(host, 0755) == 0 || errno == EEXIST; +#endif +} + +BOOL TheseusDeleteFile(LPCSTR path) +{ + char host[512]; + if (!XboxPath_TranslateForWrite(path, host, sizeof(host)) || !host[0]) return FALSE; +#ifdef THESEUS_HOST_WIN + ToBackslash(host); + return DeleteFileA(host); +#else + return remove(host) == 0; +#endif +} + +BOOL TheseusRemoveDirectory(LPCSTR path) +{ + char host[512]; + if (!XboxPath_TranslateForWrite(path, host, sizeof(host)) || !host[0]) return FALSE; +#ifdef THESEUS_HOST_WIN + ToBackslash(host); + return RemoveDirectoryA(host); +#else + return rmdir(host) == 0; +#endif +} + +BOOL TheseusSetFileAttributes(LPCSTR path, DWORD attr) +{ + char host[512]; + if (!XboxPath_TranslateForWrite(path, host, sizeof(host)) || !host[0]) return FALSE; +#ifdef THESEUS_HOST_WIN + ToBackslash(host); + return SetFileAttributesA(host, attr); +#else + (void)attr; // FATX attribute bits have no POSIX meaning worth keeping + return TRUE; +#endif +} + +DWORD TheseusGetFileAttributes(LPCSTR path) +{ + char host[512]; + if (!XboxPath_Translate(path, host, sizeof(host)) || !host[0]) return INVALID_FILE_ATTRIBUTES; +#ifdef THESEUS_HOST_WIN + ToBackslash(host); + return GetFileAttributesA(host); +#else + struct stat st; + if (stat(host, &st) != 0) return INVALID_FILE_ATTRIBUTES; + return S_ISDIR(st.st_mode) ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL; +#endif +} + +BOOL TheseusGetFileAttributesEx(LPCSTR path, GET_FILEEX_INFO_LEVELS level, LPVOID info) +{ + char host[512]; + if (!XboxPath_Translate(path, host, sizeof(host)) || !host[0]) return FALSE; +#ifdef THESEUS_HOST_WIN + ToBackslash(host); + return GetFileAttributesExA(host, level, info); +#else + (void)level; + struct stat st; + if (stat(host, &st) != 0) return FALSE; + if (info) { + WIN32_FILE_ATTRIBUTE_DATA* fad = (WIN32_FILE_ATTRIBUTE_DATA*)info; + memset(fad, 0, sizeof(*fad)); + fad->dwFileAttributes = S_ISDIR(st.st_mode) ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL; + fad->nFileSizeLow = (DWORD)(st.st_size & 0xFFFFFFFF); + fad->nFileSizeHigh = (DWORD)((uint64_t)st.st_size >> 32); + } + return TRUE; +#endif +} + +BOOL TheseusGetDiskFreeSpaceEx(LPCSTR path, ULARGE_INTEGER* pFree, ULARGE_INTEGER* pTotal, ULARGE_INTEGER* pTotalFree) +{ + char host[512]; + if (!XboxPath_Translate(path, host, sizeof(host)) || !host[0]) return FALSE; +#ifdef THESEUS_HOST_WIN + ToBackslash(host); + return GetDiskFreeSpaceExA(host, pFree, pTotal, pTotalFree); +#else + // No real free-space query on POSIX yet; report empty like it always has. + if (pFree) pFree->QuadPart = 0; + if (pTotal) pTotal->QuadPart = 0; + if (pTotalFree) pTotalFree->QuadPart = 0; + return TRUE; +#endif +} + +BOOL TheseusMoveFile(LPCSTR src, LPCSTR dst, DWORD flags) +{ + char s[512], d[512]; + if (!XboxPath_TranslateForWrite(src, s, sizeof(s)) || !s[0]) return FALSE; + if (!XboxPath_TranslateForWrite(dst, d, sizeof(d)) || !d[0]) return FALSE; +#ifdef THESEUS_HOST_WIN + ToBackslash(s); + ToBackslash(d); + return MoveFileExA(s, d, flags); +#else + (void)flags; + return rename(s, d) == 0; +#endif +} + +BOOL TheseusCopyFile(LPCSTR src, LPCSTR dst, BOOL bFailIfExists) +{ + char s[512], d[512]; + if (!XboxPath_Translate(src, s, sizeof(s)) || !s[0]) return FALSE; + if (!XboxPath_TranslateForWrite(dst, d, sizeof(d)) || !d[0]) return FALSE; +#ifdef THESEUS_HOST_WIN + ToBackslash(s); + ToBackslash(d); + return CopyFileA(s, d, bFailIfExists); +#else + struct stat st; + if (bFailIfExists && stat(d, &st) == 0) return FALSE; + FILE* in = fopen(s, "rb"); + if (!in) return FALSE; + FILE* out = fopen(d, "wb"); + if (!out) { fclose(in); return FALSE; } + char buf[64 * 1024]; + size_t n; + bool ok = true; + while ((n = fread(buf, 1, sizeof(buf), in)) > 0) + if (fwrite(buf, 1, n, out) != n) { ok = false; break; } + fclose(in); + if (fclose(out) != 0) ok = false; + return ok; +#endif +} + +FILE* TheseusFOpen(const TCHAR* path, const TCHAR* mode) +{ + if (!path || !mode) return NULL; + // A bare relative name has no drive to map. + if (!strchr(path, ':')) return fopen(path, mode); + + bool writes = strpbrk(mode, "wa+") != NULL; + char host[512]; + if (writes) XboxPath_TranslateForWrite(path, host, sizeof(host)); + else XboxPath_Translate(path, host, sizeof(host)); + if (!host[0]) return NULL; + if (mode[0] == 'w' || mode[0] == 'a') MakeParents(host); + return fopen(host, mode); +} + +// --------------------------------------------------------------------------- +// Directory listing. Real entries first, then virtual games for the same +// folder, minus any that also exist on disk. +// --------------------------------------------------------------------------- + +namespace { + +const DWORD kFindMagic = 0x46464858; // 'XHFF' + +struct FindHandle { + DWORD magic; +#ifdef THESEUS_HOST_WIN + HANDLE real; // Win32 find handle, or INVALID_HANDLE_VALUE +#else + DIR* real; +#endif + char dirPath[512]; // translated host dir + char pattern[256]; // basename pattern from the call + int vgIndices[VGAMES_MAX]; + int vgCount; + int vgPos; +}; + +bool PatternMatch(const char* pattern, const char* name) { +#ifdef THESEUS_HOST_WIN + // '*' any run, '?' one char, case-insensitive. + while (*pattern && *name) { + if (*pattern == '*') { + pattern++; + if (!*pattern) return true; + for (; *name; name++) + if (PatternMatch(pattern, name)) return true; + return false; + } + if (*pattern != '?' && tolower((unsigned char)*pattern) != tolower((unsigned char)*name)) + return false; + pattern++; + name++; + } + while (*pattern == '*') pattern++; + return *pattern == 0 && *name == 0; +#else + return fnmatch(pattern, name, FNM_CASEFOLD) == 0; +#endif +} + +bool OnDisk(const FindHandle* ffh, const char* name) { + char full[768]; + snprintf(full, sizeof(full), "%s/%s", ffh->dirPath, name); + struct stat st; + return stat(full, &st) == 0; +} + +BOOL NextVirtual(FindHandle* ffh, WIN32_FIND_DATAA* fd) { + while (ffh->vgPos < ffh->vgCount) { + int idx = ffh->vgIndices[ffh->vgPos++]; + if (idx < 0 || idx >= VGAMES_MAX) continue; + const VirtualGame& vg = g_vgames.games[idx]; + if (!vg.valid) continue; + if (!PatternMatch(ffh->pattern, vg.name)) continue; + if (OnDisk(ffh, vg.name)) continue; // a real folder wins + memset(fd, 0, sizeof(*fd)); + strncpy(fd->cFileName, vg.name, sizeof(fd->cFileName) - 1); + fd->dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY; + return TRUE; + } + return FALSE; +} + +#ifndef THESEUS_HOST_WIN +BOOL NextReal(FindHandle* ffh, WIN32_FIND_DATAA* fd) { + struct dirent* entry; + while ((entry = readdir(ffh->real)) != NULL) { + if (!PatternMatch(ffh->pattern, entry->d_name)) continue; + memset(fd, 0, sizeof(*fd)); + strncpy(fd->cFileName, entry->d_name, sizeof(fd->cFileName) - 1); + char full[768]; + snprintf(full, sizeof(full), "%s/%s", ffh->dirPath, entry->d_name); + struct stat st; + if (stat(full, &st) == 0) { + if (S_ISDIR(st.st_mode)) fd->dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY; + fd->nFileSizeLow = (DWORD)st.st_size; + } + return TRUE; + } + return FALSE; +} + +// A folder that only exists in games.ini still answers for its default.xbe +// (or its shortcut), so title enumeration finds the game. +HANDLE FindInVirtualFolder(const char* dirPath, const char* pattern, WIN32_FIND_DATAA* fd) { + if (VGames_MatchFolder(dirPath) < 0) return INVALID_HANDLE_VALUE; + const char* name = NULL; + DWORD size = 0; + if (fnmatch("*.xbe", pattern, FNM_CASEFOLD) == 0 || fnmatch("default.xbe", pattern, FNM_CASEFOLD) == 0) { + name = "default.xbe"; + size = 4096; + } else if (fnmatch("default.uixshortcut", pattern, FNM_CASEFOLD) == 0 || + fnmatch("*.*", pattern, FNM_CASEFOLD) == 0 || strcmp(pattern, "*") == 0) { + name = "default.uixshortcut"; + size = 256; + } + if (!name) return INVALID_HANDLE_VALUE; + + FindHandle* ffh = (FindHandle*)calloc(1, sizeof(FindHandle)); + if (!ffh) return INVALID_HANDLE_VALUE; + ffh->magic = kFindMagic; + snprintf(ffh->dirPath, sizeof(ffh->dirPath), "%s", dirPath); + snprintf(ffh->pattern, sizeof(ffh->pattern), "%s", pattern); + memset(fd, 0, sizeof(*fd)); + strcpy(fd->cFileName, name); + fd->nFileSizeLow = size; + return (HANDLE)ffh; +} +#endif + +} // namespace + +HANDLE TheseusFindFirstFile(LPCSTR path, WIN32_FIND_DATAA* fd) +{ + if (!path || !fd) return INVALID_HANDLE_VALUE; + + // "Q:\Fonts\*.xtf" -> "/Fonts/*.xtf". A drive with no host analog + // has nothing to list. + char host[512]; + if (!XboxPath_Translate(path, host, sizeof(host)) || !host[0]) return INVALID_HANDLE_VALUE; + + char dirPath[512], pattern[256]; + const char* sep = strrchr(host, '/'); + if (sep) { + snprintf(dirPath, sizeof(dirPath), "%.*s", (int)(sep - host), host); + snprintf(pattern, sizeof(pattern), "%s", sep + 1); + } else { + strcpy(dirPath, "."); + snprintf(pattern, sizeof(pattern), "%s", host); + } + // "*.*" means everything on Win32, dotless names included. + if (strcmp(pattern, "*.*") == 0) strcpy(pattern, "*"); + + FindHandle* ffh = (FindHandle*)calloc(1, sizeof(FindHandle)); + if (!ffh) return INVALID_HANDLE_VALUE; + ffh->magic = kFindMagic; + snprintf(ffh->dirPath, sizeof(ffh->dirPath), "%s", dirPath); + snprintf(ffh->pattern, sizeof(ffh->pattern), "%s", pattern); + + char drive[4] = {0}, cat[32] = {0}; + if (VGames_ParseDir(dirPath, drive, cat, sizeof(cat))) + ffh->vgCount = VGames_GetForDirectory(drive, cat, ffh->vgIndices, VGAMES_MAX); + +#ifdef THESEUS_HOST_WIN + char winPath[512]; + snprintf(winPath, sizeof(winPath), "%s", host); + ToBackslash(winPath); + ffh->real = FindFirstFileA(winPath, fd); + if (ffh->real != INVALID_HANDLE_VALUE) return (HANDLE)ffh; +#else + ffh->real = opendir(dirPath); + if (!ffh->real) { + free(ffh); + return FindInVirtualFolder(dirPath, pattern, fd); + } + if (NextReal(ffh, fd)) return (HANDLE)ffh; + closedir(ffh->real); + ffh->real = NULL; +#endif + + if (NextVirtual(ffh, fd)) return (HANDLE)ffh; + free(ffh); + return INVALID_HANDLE_VALUE; +} + +BOOL TheseusFindNextFile(HANDLE h, WIN32_FIND_DATAA* fd) +{ + if (!h || h == INVALID_HANDLE_VALUE || !fd) return FALSE; + FindHandle* ffh = (FindHandle*)h; + if (ffh->magic != kFindMagic) return FALSE; +#ifdef THESEUS_HOST_WIN + if (ffh->real != INVALID_HANDLE_VALUE) { + if (FindNextFileA(ffh->real, fd)) return TRUE; + FindClose(ffh->real); + ffh->real = INVALID_HANDLE_VALUE; + } +#else + if (ffh->real) { + if (NextReal(ffh, fd)) return TRUE; + closedir(ffh->real); + ffh->real = NULL; + } +#endif + return NextVirtual(ffh, fd); +} + +BOOL TheseusFindClose(HANDLE h) +{ + if (!h || h == INVALID_HANDLE_VALUE) return FALSE; + FindHandle* ffh = (FindHandle*)h; + if (ffh->magic != kFindMagic) return FALSE; +#ifdef THESEUS_HOST_WIN + if (ffh->real != INVALID_HANDLE_VALUE) FindClose(ffh->real); +#else + if (ffh->real) closedir(ffh->real); +#endif + free(ffh); + return TRUE; +} diff --git a/theseus/desktop/xbox_path.h b/theseus/desktop/xbox_path.h new file mode 100644 index 0000000..82b30a9 --- /dev/null +++ b/theseus/desktop/xbox_path.h @@ -0,0 +1,165 @@ +// xbox_path.h: turns an Xbox path into a host path. The one translator on +// desktop; everything that takes an Xbox path comes through here. +// +// C: lands in Configs, Q: in Data, E: and X: in Library, all under the user +// data dir. F:, G:, R: and friends have no analog and come back empty, so +// XAP scripts that iterate drives just see nothing. +// +// Leaf header on purpose: platform_shim.h includes it before any of its own +// stubs, so it can't lean on the shim. + +#pragma once + +#include "platform.h" + +#include +#include +#include +#include + +#include "xboxfs_drive.h" // XboxFS_DriveToPrefix, AppPath +#include "path_ci.h" // Plat_ResolveCaseInsensitive + +extern int VGames_MatchFolder(const char*); +extern const char* VGames_GetIconPath(int); + +inline bool XboxPath_NoCaseN(const char* a, const char* b, size_t n) { + for (size_t i = 0; i < n; i++) { + if (tolower((unsigned char)a[i]) != tolower((unsigned char)b[i])) return false; + if (!a[i]) return true; + } + return true; +} + +inline bool XboxPath_NoCase(const char* a, const char* b) { + size_t na = strlen(a); + return na == strlen(b) && XboxPath_NoCaseN(a, b, na); +} + +inline void XboxPath_Slashes(char* s) { + for (; *s; s++) if (*s == '\\') *s = '/'; +} + +// Shared body. Writes never follow the virtual-game icon redirect: a +// DeleteFile on a missing folder icon would otherwise land on the real +// backing asset. +inline bool XboxPath_TranslateImpl(const char* xboxPath, char* out, size_t cch, + bool followIconRedirect) { + if (!out || !cch) return false; + out[0] = '\0'; + if (!xboxPath || !*xboxPath) return false; + + // Device-path form: "\Device\Harddisk0\PartitionN\..." Used by the XAP + // `launch()` builtin to address Xbox HDD partitions. Map the partition + // back to a drive letter. Only Partition1 (E) and Partition2 (C) exist + // on desktop; 6/7/8/9 = F/G/R/S are dead letters. + static const char kDevPrefix[] = "\\Device\\Harddisk0\\Partition"; + const size_t kDevPrefixLen = sizeof(kDevPrefix) - 1; + if (strncmp(xboxPath, kDevPrefix, kDevPrefixLen) == 0) { + const char* p = xboxPath + kDevPrefixLen; + int partNum = 0; + while (*p >= '0' && *p <= '9') { partNum = partNum * 10 + (*p - '0'); p++; } + if (*p == '\\' || *p == '/') p++; + char drive = 0; + switch (partNum) { + case 1: drive = 'E'; break; + case 2: drive = 'C'; break; + case 6: drive = 'F'; break; + case 7: drive = 'G'; break; + case 8: drive = 'R'; break; + case 9: drive = 'S'; break; + } + const char* prefix = drive ? XboxFS_DriveToPrefix(drive) : 0; + if (!prefix) return false; + snprintf(out, cch, "%s/%s", prefix, p); + XboxPath_Slashes(out); + Plat_ResolveCaseInsensitive(out, cch); + return true; + } + + // Drive letter pattern: "X:\" or "X:/" or "WORD:\". + const char* colon = strchr(xboxPath, ':'); + if (colon && (colon[1] == '\\' || colon[1] == '/')) { + int driveLen = (int)(colon - xboxPath); + if (driveLen > 0 && driveLen < 32) { + char drive[32]; + memcpy(drive, xboxPath, driveLen); + drive[driveLen] = '\0'; + + const char* rest = colon + 2; + const char* prefix = NULL; + + if (driveLen == 1) { + if (drive[0] == 'C' || drive[0] == 'c') { + // Strip "UIX Configs/" so the on-disk Configs/ is flat, + // without the UIX-Lite legacy folder name. + static const char kUix[] = "UIX Configs"; + size_t uixLen = sizeof(kUix) - 1; + if (strncmp(rest, kUix, uixLen) == 0 && + (rest[uixLen] == '\\' || rest[uixLen] == '/')) { + rest += uixLen + 1; + } + prefix = AppPath_Tree("Configs"); + } else { + prefix = XboxFS_DriveToPrefix(drive[0]); + } + } else if (XboxPath_NoCase(drive, "MUSIC")) { + // Legacy MUSIC: virtual drive, pointed at the bundled + // Library/Music fallback. desktop.ini's [Library] MusicRoot + // is consumed separately by CMediaCollection / DashMusic_Scan. + prefix = AppPath_Tree("Library/Music"); + } + + // F:, G:, R: and so on. Come back empty so stat/opendir/fopen/ + // mkdir all fail predictably. Passing it through would have + // mkdir create a literal "F:" directory. + if (!prefix) return false; + + snprintf(out, cch, "%s/%s", prefix, rest); + XboxPath_Slashes(out); + + // Engine settings lived in Q:\System\config.ini on Xbox. Desktop + // folds them into Configs/desktop.ini, so both names land on the + // one file. + char sysCfg[512]; + snprintf(sysCfg, sizeof(sysCfg), "%s/System/config.ini", AppPath_Tree("Data")); + if (XboxPath_NoCase(out, sysCfg)) + snprintf(out, cch, "%s", AppPath("Configs/desktop.ini")); + + // FATX doesn't care about case, Linux does. + Plat_ResolveCaseInsensitive(out, cch); + + // A missing folder icon falls through to the virtual games DB. + struct stat st; + if (followIconRedirect && stat(out, &st) != 0) { + const char* iconTail = strstr(out, "/icon.jpg"); + if (!iconTail) iconTail = strstr(out, "/icon.png"); + if (iconTail) { + char folder[512]; + snprintf(folder, sizeof(folder), "%.*s", (int)(iconTail - out), out); + int vgIdx = VGames_MatchFolder(folder); + if (vgIdx >= 0) { + const char* realIcon = VGames_GetIconPath(vgIdx); + if (realIcon) snprintf(out, cch, "%s", realIcon); + } + } + } + return true; + } + } + + // No drive letter: hand it back with forward slashes. + snprintf(out, cch, "%s", xboxPath); + XboxPath_Slashes(out); + return true; +} + +// Returns false, with out emptied, when the path has no host analog. +inline bool XboxPath_Translate(const char* xboxPath, char* out, size_t cch) { + return XboxPath_TranslateImpl(xboxPath, out, cch, true); +} + +// For anything that creates, deletes, renames or writes. +inline bool XboxPath_TranslateForWrite(const char* xboxPath, char* out, size_t cch) { + return XboxPath_TranslateImpl(xboxPath, out, cch, false); +} diff --git a/theseus/desktop/xboxfs.h b/theseus/desktop/xboxfs.h deleted file mode 100644 index d15142f..0000000 --- a/theseus/desktop/xboxfs.h +++ /dev/null @@ -1,745 +0,0 @@ -// Desktop drive letter to host path translation. C: lands in Configs, -// Q: in Data, E: in Library, all under the user data dir. F:, G:, R: and -// friends have no analog and come back empty, so XAP scripts that iterate -// drives just see nothing. -// -// Xbox build is unaffected: it uses real C:/Q:/E: drives via XTL. - -#pragma once - -#include -#include -#include -#include -#include -#ifndef _WIN32 -#include -#endif -#include -#include "virtual_games.h" - -#include "xboxfs_drive.h" // XboxFS_DriveToPrefix shared helper -#include "path_ci.h" // Plat_ResolveCaseInsensitive - -// Case-insensitive path resolution (Xbox FATX is case-insensitive). -// Walks each path component and matches against actual directory entries. -// Trailing wildcard components like "*.*" or "Track*.mp3" are not file -// names and shouldn't be case-mapped; peel them off, resolve the directory -// prefix, then re-attach the wildcard so callers like FindFirstFile keep -// working on case-sensitive Linux filesystems. -inline bool XboxFS_ResolveCaseInsensitive(char* resolvedPath, size_t maxLen) { - return Plat_ResolveCaseInsensitive(resolvedPath, maxLen); -} - -// Translate an Xbox-style path to a local path -// Returns a pointer to a static buffer (NOT thread-safe, but matches Xbox single-threaded model) -#ifdef _WIN32 -// On Windows a real path and an Xbox drive spec look identical: both are -// "C:\...". Everything AppPath hands back is now absolute, so without this a -// genuine C:\Users\...\Data\shaders\dx11\vs_ff.bin gets rewritten into the -// Configs folder and the whole scene silently loses its shaders. POSIX never -// hits this since host paths start with a slash. -// The dashboard's own trees. X: and Q: are ours outright; C: and E: are -// shared with Windows, so only the listed subtrees are virtual. Game -// categories match gameCats[] in virtual_games.h. -inline bool XboxFS_IsVirtualTree(const char* p) { - if (!p || !p[0] || p[1] != ':') return false; - char drive = (char)toupper((unsigned char)p[0]); - if (drive == 'Q' || drive == 'X') return true; - - static const char* kC[] = { "UIX Configs", "icons", "version", - "Discord Presence", 0 }; - static const char* kE[] = { "Games", "Applications", "Apps", "Homebrew", - "Emulators", "Dashboards", "ES-DE", "TDATA", - "UDATA", "Plex", "Jellyfin", "Xcloud", "Music", 0 }; - const char** list = (drive == 'C') ? kC : (drive == 'E') ? kE : 0; - if (!list) return false; - - const char* rest = p + 2; - while (*rest == '\\' || *rest == '/') rest++; - if (!*rest) return true; // bare "E:\" root listing is still ours - - for (int i = 0; list[i]; i++) { - size_t n = strlen(list[i]); - if (strncasecmp(rest, list[i], n) == 0 && - (rest[n] == '\0' || rest[n] == '\\' || rest[n] == '/')) - return true; - } - return false; -} - -inline bool XboxFS_IsHostPath(const char* p) { - // Already resolved through AppPath, so never translate it twice. - const char* roots[2] = { Plat_ShippedDir(), Plat_UserDataDir() }; - for (int i = 0; i < 2; i++) { - const char* r = roots[i]; - if (!r || !*r) continue; - size_t n = strlen(r), j = 0; - for (; j < n; j++) { - char a = p[j], b = r[j]; - if (a == '\\') a = '/'; - if (b == '\\') b = '/'; - if (tolower((unsigned char)a) != tolower((unsigned char)b)) break; - } - if (j == n && (p[n] == '\0' || p[n] == '/' || p[n] == '\\')) return true; - } - // Virtual regardless of what sits on the host disk, so E:\Games still - // works with a real E: volume mounted. - if (XboxFS_IsVirtualTree(p)) return false; - - // Anything else is a real Windows path. Identifiers the XAP hands us are - // always under one of the trees above, so an unlisted C: or E: subtree is - // a genuine install like C:\Program Files\Game\bin\game.exe. - return true; -} -#endif - -inline const char* XboxFS_TranslatePath(const char* xboxPath) { - static char s_buf[512]; - - if (!xboxPath || !*xboxPath) { - s_buf[0] = '\0'; - return s_buf; - } - -#ifdef _WIN32 - if (XboxFS_IsHostPath(xboxPath)) { - snprintf(s_buf, sizeof(s_buf), "%s", xboxPath); - return s_buf; - } -#endif - - // Device-path form: "\Device\Harddisk0\PartitionN\..." Used by the - // XAP `launch()` builtin to address Xbox HDD partitions. Map the - // partition number back to a drive letter so the rest of this - // function's drive-letter logic handles it. Only Partition1 (E) - // and Partition2 (C) actually exist on desktop; everything else - // (6/7/8/9 = F/G/R/S) is dead-letter. - { - static const char kDevPrefix[] = "\\Device\\Harddisk0\\Partition"; - const size_t kDevPrefixLen = sizeof(kDevPrefix) - 1; - if (strncmp(xboxPath, kDevPrefix, kDevPrefixLen) == 0) { - const char* p = xboxPath + kDevPrefixLen; - int partNum = 0; - while (*p >= '0' && *p <= '9') { partNum = partNum * 10 + (*p - '0'); p++; } - if (*p == '\\' || *p == '/') p++; - char drive = 0; - switch (partNum) { - case 1: drive = 'E'; break; - case 2: drive = 'C'; break; - case 6: drive = 'F'; break; - case 7: drive = 'G'; break; - case 8: drive = 'R'; break; - case 9: drive = 'S'; break; - default: drive = 0; break; - } - const char* prefix = drive ? XboxFS_DriveToPrefix(drive) : 0; - if (!prefix) { - s_buf[0] = '\0'; - return s_buf; - } - snprintf(s_buf, sizeof(s_buf), "%s/%s", prefix, p); - for (char* q = s_buf; *q; q++) if (*q == '\\') *q = '/'; - XboxFS_ResolveCaseInsensitive(s_buf, sizeof(s_buf)); - return s_buf; - } - } - - // Look for drive letter pattern: "X:\" or "X:/" or "WORD:\". - const char* colon = strchr(xboxPath, ':'); - if (colon && (colon[1] == '\\' || colon[1] == '/')) { - int driveLen = (int)(colon - xboxPath); - if (driveLen > 0 && driveLen < 32) { - char drive[32]; - memcpy(drive, xboxPath, driveLen); - drive[driveLen] = '\0'; - - // Skip ":\" or ":/" then route the rest of the path based on - // which Xbox drive this is. - const char* rest = colon + 2; - const char* prefix = NULL; - - if (driveLen == 1) { - if (drive[0] == 'C' || drive[0] == 'c') { - // Strip "UIX Configs/" if present so the on-disk Configs/ - // is a flat dir without the UIX-Lite legacy folder name. - static const char kUix[] = "UIX Configs"; - size_t uixLen = sizeof(kUix) - 1; - if (strncmp(rest, kUix, uixLen) == 0 && - (rest[uixLen] == '\\' || rest[uixLen] == '/')) { - rest += uixLen + 1; - } - prefix = AppPath_Tree("Configs"); - } else { - prefix = XboxFS_DriveToPrefix(drive[0]); - } - } else if (strcasecmp(drive, "MUSIC") == 0) { - // Legacy MUSIC: virtual drive. The dashboard mounts the - // user's music collection here; we point it at the bundled - // Library/Music fallback (the desktop's [Library] MusicRoot - // setting in desktop.ini is consumed separately by - // CMediaCollection / DashMusic_Scan). - prefix = AppPath_Tree("Library/Music"); - } - - if (!prefix) { - // F:, G:, H:, I:, R:, etc. No analog on desktop. Return - // an empty string so the caller's stat()/opendir()/fopen()/ - // mkdir() all fail predictably. Critically, do NOT fall - // through to the "pass through unchanged" branch, that - // would hand callers like Preloader_Mkdirp a literal - // "F:/Foo/Bar" path, and mkdir would happily create a - // directory named "F:" (which Finder helpfully renders as - // "F/", confusing everyone). - s_buf[0] = '\0'; - return s_buf; - } - - snprintf(s_buf, sizeof(s_buf), "%s/%s", prefix, rest); - - // Convert backslashes to forward slashes. - for (char* p = s_buf; *p; p++) { - if (*p == '\\') *p = '/'; - } - - // Legacy aliases. Xbox shipped engine-level settings in - // Q:\System\config.ini (XBE config: resolution, current skin) - // and a separate XAP-level config in C:\UIX Configs\config.ini - // (scene wiring). On desktop the engine-level settings are - // collapsed into Configs/desktop.ini alongside CRT, library - // roots, etc., so XAP code that calls - // CSettingsFile::Open("Q:\\System\\Config.ini") reads + writes - // through the same source-of-truth file. - char sysCfg[512]; - snprintf(sysCfg, sizeof(sysCfg), "%s/System/config.ini", AppPath_Tree("Data")); - if (strcasecmp(s_buf, sysCfg) == 0) { - strncpy(s_buf, AppPath("Configs/desktop.ini"), sizeof(s_buf) - 1); - s_buf[sizeof(s_buf) - 1] = '\0'; - } - - // Case-insensitive resolve (Xbox FATX doesn't care about case). - XboxFS_ResolveCaseInsensitive(s_buf, sizeof(s_buf)); - - // Virtual-game icon redirect: if the translated file doesn't - // exist and it looks like a game icon path, fall through to - // the virtual games DB. - { - struct stat _vst; - if (stat(s_buf, &_vst) != 0) { - const char* iconTail = strstr(s_buf, "/icon.jpg"); - if (!iconTail) iconTail = strstr(s_buf, "/icon.png"); - if (iconTail) { - char tmpBuf[512]; - strncpy(tmpBuf, s_buf, sizeof(tmpBuf) - 1); - tmpBuf[iconTail - s_buf] = 0; - extern int VGames_MatchFolder(const char*); - int vgIdx = VGames_MatchFolder(tmpBuf); - if (vgIdx >= 0) { - extern const char* VGames_GetIconPath(int); - const char* realIcon = VGames_GetIconPath(vgIdx); - if (realIcon) { - strncpy(s_buf, realIcon, sizeof(s_buf) - 1); - s_buf[sizeof(s_buf) - 1] = 0; - } - } - } - } - } - return s_buf; - } - } - - // No drive letter found - return path as-is (with backslash conversion) - strncpy(s_buf, xboxPath, sizeof(s_buf) - 1); - s_buf[sizeof(s_buf) - 1] = '\0'; - for (char* p = s_buf; *p; p++) { - if (*p == '\\') *p = '/'; - } - return s_buf; -} - -// ============================================================================ -// Shared helpers used by both Windows and POSIX paths -// ============================================================================ - -// Fills `buf` with a minimal fake XBE for a virtual game. Returns bytes -// written, or 0 if `buf` is too small. Layout: 0x178-byte header followed -// by a 0x1EC-byte certificate carrying the title name (UTF-16) and title -// ID. The dashboard's title enumeration only reads these two structures. -inline size_t XboxFS_FillFakeXBEBytes(uint8_t* buf, size_t bufSize, - const char* titleName, - unsigned long titleID) { - const size_t need = 0x178 + 0x1EC; - if (bufSize < need) return 0; - memset(buf, 0, need); - - // XBE magic - buf[0] = 'X'; buf[1] = 'B'; buf[2] = 'E'; buf[3] = 'H'; - - uint32_t base = 0x00010000; - memcpy(buf + 0x104, &base, 4); - - uint32_t hdrSize = (uint32_t)need; - memcpy(buf + 0x108, &hdrSize, 4); - - uint32_t certAddr = base + 0x178; - memcpy(buf + 0x118, &certAddr, 4); - - uint8_t* cert = buf + 0x178; - uint32_t certSize = 0x1EC; - memcpy(cert + 0x00, &certSize, 4); - memcpy(cert + 0x08, &titleID, 4); - - uint16_t* titleNameU = (uint16_t*)(cert + 0x0C); - for (int i = 0; i < 40 && titleName[i]; i++) - titleNameU[i] = (uint16_t)(unsigned char)titleName[i]; - - return need; -} - -// Windows-style wildcard match. '*' = any sequence, '?' = single char. -// Case-insensitive. Used to filter synthesized virtual-game names against -// the FindFirstFile pattern; real entries are matched by Win32 / readdir. -// directly. Recursion depth is bounded by the number of '*' tokens in the -// pattern, which is tiny in practice. -inline bool XboxFS_WildcardMatch(const char* pattern, const char* name) { - while (*pattern && *name) { - if (*pattern == '*') { - pattern++; - if (!*pattern) return true; - while (*name) { - if (XboxFS_WildcardMatch(pattern, name)) return true; - name++; - } - return false; - } - char p = (char)tolower((unsigned char)*pattern); - char n = (char)tolower((unsigned char)*name); - if (*pattern != '?' && p != n) return false; - pattern++; - name++; - } - while (*pattern == '*') pattern++; - return *pattern == 0 && *name == 0; -} - -#ifdef _WIN32 -// On Windows, translate Xbox paths then call real Win32 CreateFileA -// First, save a reference to the real function name -#undef CreateFile -#undef CreateFileA -// Re-declare the real Win32 function after the #undef dropped the macro. -extern "C" __declspec(dllimport) HANDLE __stdcall CreateFileA( - LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, - LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, - DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); - -inline void XboxFS_ToBackslash(char* buf, size_t bufSize, const char* src) { - strncpy(buf, src, bufSize - 1); - buf[bufSize - 1] = '\0'; - for (char* p = buf; *p; p++) { if (*p == '/') *p = '\\'; } -} - -// Forward decl. Definition lower down, alongside the Find* wrappers. -inline HANDLE XboxFS_CreateFakeXBE_Win32(const char* titleName, unsigned long titleID); - -inline HANDLE XboxFS_CreateFileA(LPCSTR name, DWORD access, DWORD share, - void* sa, DWORD disp, DWORD flags, HANDLE templ) { - const char* translated = XboxFS_TranslatePath(name); - // Convert forward slashes back to backslashes for Win32 APIs - char winPath[512]; - XboxFS_ToBackslash(winPath, sizeof(winPath), translated); - // Strip Xbox flags that cause issues on desktop Windows - // FILE_FLAG_NO_BUFFERING requires sector-aligned reads; FILE_FLAG_OVERLAPPED needs async I/O setup - flags &= ~(FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED); - HANDLE h = ::CreateFileA(winPath, access, share, (LPSECURITY_ATTRIBUTES)sa, disp, flags, templ); - if (h != INVALID_HANDLE_VALUE) return h; - - // Real open failed; check if this is the default.xbe inside a virtual - // game folder (matching the behavior the POSIX CreateFileA gives the - // dashboard's title enumeration). - char folderPath[512]; - strncpy(folderPath, translated, sizeof(folderPath) - 1); - folderPath[sizeof(folderPath) - 1] = '\0'; - // Both forward slashes (translated) and backslashes (Windows-native) - // can show up here; normalize to forward for VGames_MatchFolder, which - // expects xboxfs/// form. - for (char* p = folderPath; *p; p++) { if (*p == '\\') *p = '/'; } - char* lastSl = strrchr(folderPath, '/'); - if (lastSl) { - const char* fileName = lastSl + 1; - *lastSl = '\0'; - if (_stricmp(fileName, "default.xbe") == 0) { - int vgIdx = VGames_MatchFolder(folderPath); - if (vgIdx >= 0) { - unsigned long tid = strtoul(g_vgames.games[vgIdx].titleID, NULL, 16); - HANDLE fakeXbe = XboxFS_CreateFakeXBE_Win32(g_vgames.games[vgIdx].name, tid); - if (fakeXbe != INVALID_HANDLE_VALUE) { - fprintf(stderr, "[xboxfs] Virtual XBE for '%s' (TID=%08lX)\n", - g_vgames.games[vgIdx].name, tid); - return fakeXbe; - } - } - } - } - return INVALID_HANDLE_VALUE; -} -#define CreateFileA XboxFS_CreateFileA -#define CreateFile CreateFileA - -// Windows wrappers for FindFirstFile, FindNextFile, FindClose, GetFileAttributes, etc. -// These translate Xbox paths (E:\Games\*) to xboxfs paths before calling real Win32 APIs. -#undef FindFirstFile -#undef FindFirstFileA -#undef FindNextFile -#undef FindNextFileA -#undef FindClose -#undef GetFileAttributes -#undef GetFileAttributesA -#undef GetFileAttributesEx -#undef GetFileAttributesExA -#undef RemoveDirectory -#undef RemoveDirectoryA - -extern "C" __declspec(dllimport) HANDLE __stdcall FindFirstFileA( - LPCSTR lpFileName, LPWIN32_FIND_DATAA lpFindFileData); -extern "C" __declspec(dllimport) BOOL __stdcall FindNextFileA( - HANDLE hFindFile, LPWIN32_FIND_DATAA lpFindFileData); -extern "C" __declspec(dllimport) BOOL __stdcall FindClose(HANDLE hFindFile); -extern "C" __declspec(dllimport) DWORD __stdcall GetFileAttributesA(LPCSTR lpFileName); -extern "C" __declspec(dllimport) BOOL __stdcall GetFileAttributesExA( - LPCSTR lpFileName, GET_FILEEX_INFO_LEVELS fInfoLevelId, LPVOID lpFileInformation); -extern "C" __declspec(dllimport) BOOL __stdcall RemoveDirectoryA(LPCSTR lpPathName); -extern "C" __declspec(dllimport) DWORD __stdcall GetTempPathA(DWORD nBufferLength, LPSTR lpBuffer); -extern "C" __declspec(dllimport) UINT __stdcall GetTempFileNameA(LPCSTR lpPathName, LPCSTR lpPrefixString, UINT uUnique, LPSTR lpTempFileName); -extern "C" __declspec(dllimport) BOOL __stdcall WriteFile(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped); -extern "C" __declspec(dllimport) DWORD __stdcall SetFilePointer(HANDLE hFile, LONG lDistanceToMove, PLONG lpDistanceToMoveHigh, DWORD dwMoveMethod); - -// Windows fake XBE for virtual games -// On POSIX we hand back a tmpfile() FILE* cast to HANDLE; on Windows real -// HANDLEs and FILE*s aren't interchangeable, so create a temp file on disk -// flagged DELETE_ON_CLOSE, write the synthesized XBE bytes into it, rewind, -// and hand the OS HANDLE to the caller. The OS deletes the backing file -// when the caller closes the handle. -inline HANDLE XboxFS_CreateFakeXBE_Win32(const char* titleName, unsigned long titleID) { - char tempDir[260]; - char tempFile[260]; - DWORD n = ::GetTempPathA(260, tempDir); - if (n == 0 || n > 260) return INVALID_HANDLE_VALUE; - if (::GetTempFileNameA(tempDir, "xbe", 0, tempFile) == 0) - return INVALID_HANDLE_VALUE; - HANDLE h = ::CreateFileA(tempFile, - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ, - NULL, - CREATE_ALWAYS, - FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, - NULL); - if (h == INVALID_HANDLE_VALUE) return INVALID_HANDLE_VALUE; - - uint8_t buf[0x178 + 0x1EC]; - size_t bytes = XboxFS_FillFakeXBEBytes(buf, sizeof(buf), titleName, titleID); - if (bytes == 0) { ::FindClose(h); return INVALID_HANDLE_VALUE; } - DWORD written = 0; - ::WriteFile(h, buf, (DWORD)bytes, &written, NULL); - ::SetFilePointer(h, 0, NULL, 0 /* FILE_BEGIN */); - return h; -} - -// Windows wrapped Find handle -// Carries the real Win32 find handle plus the virtual-game cursor for the -// directory we're enumerating. XboxFS_FindFirstFileA always returns one of -// these; XboxFS_FindNextFileA / XboxFS_FindClose detect ours via the magic -// field and fall through to real Win32 if it doesn't match (defensive -- -// in practice every entry point goes through our macro overrides). -#define XBOXFS_FFH_MAGIC 0x46464858 /* 'XHFF' */ -struct XboxFSFindHandle { - DWORD magic; - HANDLE realHandle; // Win32 find handle, or INVALID_HANDLE_VALUE if no real dir - char dirPath[512]; // translated dir path with backslashes (for shadow checks) - char pattern[256]; // basename pattern from the original call - int vgIndices[VGAMES_MAX]; - int vgCount; - int vgPos; -}; - -// Inject virtual games for a translated dirPath. Shared with POSIX. -inline void XboxFS_PopulateVirtualGames(XboxFSFindHandle* ffh, const char* dirPath) { - ffh->vgCount = 0; - ffh->vgPos = 0; - - char drive[2] = {0}; - char cat[32] = {0}; - if (!VGames_ParseDir(dirPath, drive, cat, sizeof(cat))) return; - ffh->vgCount = VGames_GetForDirectory(drive, cat, ffh->vgIndices, VGAMES_MAX); -} - -// Returns the next virtual-game entry that (a) matches the pattern and -// (b) doesn't have a real folder of the same name in dirPath. Fills `fd` -// and returns TRUE on success; FALSE when virtual entries are exhausted. -inline BOOL XboxFS_NextVirtualEntry(XboxFSFindHandle* ffh, LPWIN32_FIND_DATAA fd) { - while (ffh->vgPos < ffh->vgCount) { - int idx = ffh->vgIndices[ffh->vgPos++]; - if (idx < 0 || idx >= VGAMES_MAX) continue; - VirtualGame& vg = g_vgames.games[idx]; - if (!vg.valid) continue; - if (!XboxFS_WildcardMatch(ffh->pattern, vg.name)) continue; - // Skip if a real folder with this name already exists alongside. - char shadow[768]; - snprintf(shadow, sizeof(shadow), "%s\\%s", ffh->dirPath, vg.name); - DWORD attrs = ::GetFileAttributesA(shadow); - if (attrs != INVALID_FILE_ATTRIBUTES) continue; - memset(fd, 0, sizeof(*fd)); - strncpy(fd->cFileName, vg.name, sizeof(fd->cFileName) - 1); - fd->dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY; - return TRUE; - } - return FALSE; -} - -inline HANDLE XboxFS_FindFirstFileA(LPCSTR path, LPWIN32_FIND_DATAA fd) { - if (!path || !fd) return INVALID_HANDLE_VALUE; - - // Translate Xbox drive paths (E:\Games\* -> xboxfs\E\Games\*) - char winPath[512]; - if (strchr(path, ':')) { - const char* translated = XboxFS_TranslatePath(path); - XboxFS_ToBackslash(winPath, sizeof(winPath), translated); - } else { - strncpy(winPath, path, sizeof(winPath) - 1); - winPath[sizeof(winPath) - 1] = '\0'; - } - - // Allocate the wrapper unconditionally. Even a pure pattern-not-found - // case may have virtual matches to inject. - XboxFSFindHandle* ffh = (XboxFSFindHandle*)calloc(1, sizeof(XboxFSFindHandle)); - if (!ffh) return INVALID_HANDLE_VALUE; - ffh->magic = XBOXFS_FFH_MAGIC; - ffh->realHandle = INVALID_HANDLE_VALUE; - - // Split dir + pattern at the last backslash (Win32 paths are backslashed). - const char* lastSep = strrchr(winPath, '\\'); - if (!lastSep) lastSep = strrchr(winPath, '/'); - if (lastSep) { - size_t dirLen = (size_t)(lastSep - winPath); - if (dirLen >= sizeof(ffh->dirPath)) dirLen = sizeof(ffh->dirPath) - 1; - memcpy(ffh->dirPath, winPath, dirLen); - ffh->dirPath[dirLen] = '\0'; - strncpy(ffh->pattern, lastSep + 1, sizeof(ffh->pattern) - 1); - } else { - strcpy(ffh->dirPath, "."); - strncpy(ffh->pattern, winPath, sizeof(ffh->pattern) - 1); - } - - // Win32 / POSIX FindFirstFile both treat "*.*" as "match everything". - // Our wildcard matcher is strict (literal '.' required), so collapse - // the convention here; otherwise virtual game names without a dot - // (most of them) get filtered out during injection. - if (strcmp(ffh->pattern, "*.*") == 0) - strcpy(ffh->pattern, "*"); - - // Populate virtual game list for this directory before any filesystem call. - XboxFS_PopulateVirtualGames(ffh, ffh->dirPath); - - // Now query the real filesystem. - ffh->realHandle = ::FindFirstFileA(winPath, fd); - if (ffh->realHandle != INVALID_HANDLE_VALUE) - return (HANDLE)ffh; - - // Real find failed; try virtual entries directly. - if (XboxFS_NextVirtualEntry(ffh, fd)) - return (HANDLE)ffh; - - free(ffh); - return INVALID_HANDLE_VALUE; -} - -inline BOOL XboxFS_FindNextFileA(HANDLE h, LPWIN32_FIND_DATAA fd) { - if (h == INVALID_HANDLE_VALUE || !fd) return FALSE; - XboxFSFindHandle* ffh = (XboxFSFindHandle*)h; - if (ffh->magic != XBOXFS_FFH_MAGIC) { - // Defensive; something passed a raw Win32 handle in. - return ::FindNextFileA(h, fd); - } - // Exhaust real entries first. - if (ffh->realHandle != INVALID_HANDLE_VALUE) { - if (::FindNextFileA(ffh->realHandle, fd)) - return TRUE; - // Real entries done; close the real handle so subsequent calls - // fall through to virtual entries directly. - ::FindClose(ffh->realHandle); - ffh->realHandle = INVALID_HANDLE_VALUE; - } - return XboxFS_NextVirtualEntry(ffh, fd); -} - -inline BOOL XboxFS_FindClose(HANDLE h) { - if (h == INVALID_HANDLE_VALUE) return FALSE; - XboxFSFindHandle* ffh = (XboxFSFindHandle*)h; - if (ffh->magic != XBOXFS_FFH_MAGIC) { - return ::FindClose(h); - } - BOOL ok = TRUE; - if (ffh->realHandle != INVALID_HANDLE_VALUE) - ok = ::FindClose(ffh->realHandle); - free(ffh); - return ok; -} - -inline DWORD XboxFS_GetFileAttributesA(LPCSTR path) { - if (path && strchr(path, ':')) { - const char* translated = XboxFS_TranslatePath(path); - char winPath[512]; - XboxFS_ToBackslash(winPath, sizeof(winPath), translated); - return ::GetFileAttributesA(winPath); - } - return ::GetFileAttributesA(path); -} - -inline BOOL XboxFS_GetFileAttributesExA(LPCSTR path, GET_FILEEX_INFO_LEVELS level, LPVOID info) { - if (path && strchr(path, ':')) { - const char* translated = XboxFS_TranslatePath(path); - char winPath[512]; - XboxFS_ToBackslash(winPath, sizeof(winPath), translated); - return ::GetFileAttributesExA(winPath, level, info); - } - return ::GetFileAttributesExA(path, level, info); -} - -inline BOOL XboxFS_RemoveDirectoryA(LPCSTR path) { - if (path && strchr(path, ':')) { - const char* translated = XboxFS_TranslatePath(path); - char winPath[512]; - XboxFS_ToBackslash(winPath, sizeof(winPath), translated); - return ::RemoveDirectoryA(winPath); - } - return ::RemoveDirectoryA(path); -} - -#define FindFirstFileA XboxFS_FindFirstFileA -#define FindFirstFile FindFirstFileA -#define FindNextFileA XboxFS_FindNextFileA -#define FindNextFile FindNextFileA -#define FindClose XboxFS_FindClose -#define GetFileAttributesA XboxFS_GetFileAttributesA -#define GetFileAttributes GetFileAttributesA -#define GetFileAttributesExA XboxFS_GetFileAttributesExA -#define GetFileAttributesEx GetFileAttributesExA -#define RemoveDirectoryA XboxFS_RemoveDirectoryA -#define RemoveDirectory RemoveDirectoryA -#else -// On non-Windows, use fopen-based implementation (HANDLE = FILE*) -#undef CreateFileA - -// Create an in-memory FILE* containing a minimal fake XBE header for virtual games. -// Wraps the shared XboxFS_FillFakeXBEBytes helper in a tmpfile(). -inline FILE* XboxFS_CreateFakeXBE(const char* titleName, unsigned long titleID) { - FILE* f = tmpfile(); - if (!f) return NULL; - uint8_t buf[0x178 + 0x1EC]; - size_t n = XboxFS_FillFakeXBEBytes(buf, sizeof(buf), titleName, titleID); - if (n == 0) { fclose(f); return NULL; } - fwrite(buf, 1, n, f); - fseek(f, 0, SEEK_SET); - return f; -} - -// Try to load a file from mounted qcow2 FATX when xboxfs host file is missing. -// Parses Xbox path like "E:\UDATA\4d530004\TitleImage.xbx", walks the FATX -// directory tree, reads file data, writes to tmpfile() and returns it. -inline FILE* XboxFS_TryFATXFallback(const char* xboxPath) { - // Only handle drive-letter paths - if (!xboxPath || strlen(xboxPath) < 3 || xboxPath[1] != ':') return nullptr; - - // The global HDD instance lives in desktop_nodes.cpp; go through this - // free function to avoid circular includes. - FILE* XboxFS_FATXReadFile(const char* xboxPath); - return XboxFS_FATXReadFile(xboxPath); -} - -inline HANDLE CreateFileA(LPCSTR name, DWORD access, DWORD share, - void* sa, DWORD disp, DWORD flags, HANDLE templ) { - // xboxfs host filesystem is the primary source for UIX Desktop data - const char* translated = XboxFS_TranslatePath(name); - const char* mode = (access & 0x40000000) ? "wb" : "rb"; // GENERIC_WRITE : GENERIC_READ - FILE* f = fopen(translated, mode); - if (!f) { - // Check if this is a virtual game file (e.g. default.xbe inside a virtual folder) - char folderPath[512]; - strncpy(folderPath, translated, sizeof(folderPath) - 1); - folderPath[sizeof(folderPath) - 1] = 0; - char* lastSl = strrchr(folderPath, '/'); - if (lastSl) { - const char* fileName = lastSl + 1; - *lastSl = 0; - extern int VGames_MatchFolder(const char*); - int vgIdx = VGames_MatchFolder(folderPath); - if (vgIdx >= 0) { - if (strcasecmp(fileName, "default.xbe") == 0) { - extern VirtualGameDB g_vgames; - unsigned long tid = strtoul(g_vgames.games[vgIdx].titleID, NULL, 16); - FILE* fakeXbe = XboxFS_CreateFakeXBE(g_vgames.games[vgIdx].name, tid); - if (fakeXbe) { - fprintf(stderr, "[xboxfs] Virtual XBE for '%s' (TID=%08lX)\n", - g_vgames.games[vgIdx].name, tid); - return (HANDLE)fakeXbe; - } - } - } - } - // Fallback: try qcow2 FATX but ONLY for save data paths (UDATA/TDATA) - // Don't interfere with xboxfs/VGames for game icons, configs, etc. - extern char g_qcowPath[512]; - if (g_qcowPath[0] && name && (strstr(name, "UDATA") || strstr(name, "TDATA"))) { - f = XboxFS_TryFATXFallback(name); - if (f) fprintf(stderr, "[xboxfs] Loaded from qcow2 FATX\n"); - } - // silently fail - not all lookups are expected to succeed - } - return f ? (HANDLE)f : INVALID_HANDLE_VALUE; -} - -// Map CreateFile to CreateFileA (ANSI mode, matching TCHAR=char) -#undef CreateFile -#define CreateFile CreateFileA -#endif - -// Override fopen/_tfopen calls that use Xbox paths -// settingsfile.cpp, runner.cpp, image.cpp all use fopen with raw Xbox paths like "C:\UIX Configs\cache.ini" -// Without this, macOS creates files with literal backslashes in their names -inline FILE* XboxFS_fopen(const char* path, const char* mode) { - if (path && strchr(path, ':')) { - const char* translated = XboxFS_TranslatePath(path); - // Ensure parent directories exist for write modes - if (mode[0] == 'w' || mode[0] == 'a') { - char dirBuf[512]; - strncpy(dirBuf, translated, sizeof(dirBuf) - 1); - dirBuf[sizeof(dirBuf) - 1] = '\0'; - char* lastSlash = strrchr(dirBuf, '/'); - if (lastSlash) { - *lastSlash = '\0'; - // Recursively create parent dirs - for (char* p = dirBuf; *p; p++) { - if (*p == '/') { - *p = '\0'; - #ifdef _WIN32 - _mkdir(dirBuf); -#else - mkdir(dirBuf, 0755); -#endif - *p = '/'; - } - } - #ifdef _WIN32 - _mkdir(dirBuf); -#else - mkdir(dirBuf, 0755); -#endif - } - } - return fopen(translated, mode); - } - return fopen(path, mode); -} -#define fopen XboxFS_fopen -#define _tfopen XboxFS_fopen diff --git a/theseus/desktop/xip.cpp b/theseus/desktop/xip.cpp index 8dff1ec..77202a8 100644 --- a/theseus/desktop/xip.cpp +++ b/theseus/desktop/xip.cpp @@ -264,10 +264,21 @@ void* FindObjectInXIP(const char* szURL, const char* szFilename, int nType/*=-1* char thePath[MAX_PATH]; Ansi(thePath, szBuf, MAX_PATH); - LPDIRECT3DTEXTURE8 lpTexture = NULL; - HRESULT hr = D3DXCreateTextureFromFileA(TheseusGetD3DDev(), thePath, &lpTexture); - if (hr == D3D_OK) - return lpTexture; + // The skin dir is an Xbox path, so it opens through the translator. + if (FILE* f = TheseusFOpen(thePath, "rb")) + { + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + BYTE* data = size > 0 ? (BYTE*)malloc(size) : NULL; + bool ok = data && fread(data, 1, size, f) == (size_t)size; + fclose(f); + LPDIRECT3DTEXTURE8 lpTexture = NULL; + HRESULT hr = ok ? D3DXCreateTextureFromFileInMemory(TheseusGetD3DDev(), data, (UINT)size, &lpTexture) : E_FAIL; + free(data); + if (hr == D3D_OK) + return lpTexture; + } } // Search loaded XIPs (newest to oldest) diff --git a/theseus/engine/locale_node.cpp b/theseus/engine/locale_node.cpp index 08ab515..234c540 100755 --- a/theseus/engine/locale_node.cpp +++ b/theseus/engine/locale_node.cpp @@ -56,7 +56,7 @@ static void LoadXlate(const CHAR* szXlateSection) DWORD datOut = 0; char tempPath[MAX_PATH]; sprintf(tempPath, "Q:\\Language\\%s.dat", szXlateSection); - g_hXlate = CreateFile(tempPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, NULL); + g_hXlate = TheseusCreateFile(tempPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, NULL); if (g_hXlate != INVALID_HANDLE_VALUE) { DWORD datSize = GetFileSize(g_hXlate, NULL); diff --git a/theseus/render/asset_loader.cpp b/theseus/render/asset_loader.cpp index 5d9dc55..d47474f 100644 --- a/theseus/render/asset_loader.cpp +++ b/theseus/render/asset_loader.cpp @@ -746,7 +746,7 @@ LPDIRECT3DTEXTURE8 LoadTextureFromXPR(const char *xprfile) if (xprfile == NULL) return NULL; - HANDLE hFile = CreateFile(xprfile, GENERIC_READ, FILE_SHARE_READ, NULL, + HANDLE hFile = TheseusCreateFile(xprfile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { @@ -939,7 +939,7 @@ HIMAGE MyLoadImage(const TCHAR *szURL) { return LoadTexture(szURL, 0, 0); } bool DecodeRAW(const TCHAR *szFileName, CTexture *pTexture) { - FILE *fp = _tfopen(szFileName, _T("rb")); + FILE *fp = TheseusFOpen(szFileName, _T("rb")); if (fp == NULL) return false; @@ -1114,7 +1114,7 @@ void PreloadSkinTextures() OutputDebugStringA("\n"); WIN32_FIND_DATAA findData; - HANDLE hFind = FindFirstFileA(searchPath, &findData); + HANDLE hFind = TheseusFindFirstFile(searchPath, &findData); if (hFind == INVALID_HANDLE_VALUE) { OutputDebugStringA("[Preload] No .xbx files found in skin directory.\n"); @@ -1145,9 +1145,9 @@ void PreloadSkinTextures() MultiByteToWideChar(CP_ACP, 0, findData.cFileName, -1, wideFileName, MAX_PATH); FindImage(wideFileName); - } while (FindNextFileA(hFind, &findData)); + } while (TheseusFindNextFile(hFind, &findData)); - FindClose(hFind); + TheseusFindClose(hFind); OutputDebugStringA("[Preload] Skin texture preload complete.\n"); } diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp old mode 100755 new mode 100644 index 954fc9e..ac824d2 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -850,9 +850,9 @@ void InitFontTable() #if defined(_XBOX) char szWildA [MAX_PATH]; Ansi(szWildA, szWild, MAX_PATH); - h = FindFirstFile(szWildA, &fd); + h = TheseusFindFirstFile(szWildA, &fd); #else - h = FindFirstFile(szWild, &fd); + h = TheseusFindFirstFile(szWild, &fd); #endif if (h == INVALID_HANDLE_VALUE) @@ -924,9 +924,9 @@ void InitFontTable() CloseHandle(hFont); } } - while (FindNextFile(h, &fd)); + while (TheseusFindNextFile(h, &fd)); - FindClose(h); + TheseusFindClose(h); } CFont* GetFont(const TCHAR* szFaceName) diff --git a/theseus/shared/audio_system.cpp b/theseus/shared/audio_system.cpp index 229b44f..c213331 100755 --- a/theseus/shared/audio_system.cpp +++ b/theseus/shared/audio_system.cpp @@ -710,7 +710,7 @@ CMP3Pump::~CMP3Pump() HRESULT CMP3Pump::Initialize(const char* szFilePath) { // Load entire MP3 into memory (file I/O from a thread is simpler this way) - HANDLE hFile = CreateFileA(szFilePath, GENERIC_READ, FILE_SHARE_READ, + HANDLE hFile = TheseusCreateFile(szFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) return E_FAIL; diff --git a/theseus/shared/file_ops.cpp b/theseus/shared/file_ops.cpp index be26f5a..a26b1ae 100755 --- a/theseus/shared/file_ops.cpp +++ b/theseus/shared/file_ops.cpp @@ -250,7 +250,7 @@ bool CGameCopier::CreateDirectory(const TCHAR* szDir) char szDir2 [MAX_PATH]; Ansi(szDir2, szDir, MAX_PATH); - return ::CreateDirectory(szDir2, NULL) != FALSE; + return TheseusCreateDirectory(szDir2, NULL) != FALSE; } bool CGameCopier::CopyDirectory(const TCHAR* szSrcPath, const TCHAR* szDestPath) @@ -265,7 +265,7 @@ bool CGameCopier::CopyDirectory(const TCHAR* szSrcPath, const TCHAR* szDestPath) WIN32_FIND_DATA fd; char szBufX [MAX_PATH]; Ansi(szBufX, szBuf, MAX_PATH); - HANDLE h = FindFirstFile(szBufX, &fd); + HANDLE h = TheseusFindFirstFile(szBufX, &fd); if (h != INVALID_HANDLE_VALUE) { do @@ -286,7 +286,7 @@ bool CGameCopier::CopyDirectory(const TCHAR* szSrcPath, const TCHAR* szDestPath) if (!CopyDirectory(szBuf, szBuf2)) { TRACE(_T("CopyDirectory(%s, %s) failed\n"), szBuf, szBuf2); - FindClose(h); + TheseusFindClose(h); m_internalError = true; return false; } @@ -296,14 +296,14 @@ bool CGameCopier::CopyDirectory(const TCHAR* szSrcPath, const TCHAR* szDestPath) if (!CopyFile(szBuf, szBuf2)) { TRACE(_T("CopyFile(%s, %s) failed\n"), szBuf, szBuf2); - FindClose(h); + TheseusFindClose(h); m_internalError = true; return false; } } } - while (FindNextFile(h, &fd)); - FindClose(h); + while (TheseusFindNextFile(h, &fd)); + TheseusFindClose(h); } return true; @@ -378,12 +378,8 @@ bool CGameCopier::DeleteFile(const TCHAR* szFile) { char szFileA [MAX_PATH]; Ansi(szFileA, szFile, MAX_PATH); - SetFileAttributes(szFileA, FILE_ATTRIBUTE_NORMAL); -#ifdef _XBOX - return ::DeleteFile(szFileA) != FALSE; -#else - return DeleteFile(szFileA) != FALSE; -#endif + TheseusSetFileAttributes(szFileA, FILE_ATTRIBUTE_NORMAL); + return TheseusDeleteFile(szFileA) != FALSE; } bool CGameCopier::DeleteDirectory(const TCHAR* szDir, bool RemoveSelf /*= true*/) @@ -394,7 +390,7 @@ bool CGameCopier::DeleteDirectory(const TCHAR* szDir, bool RemoveSelf /*= true*/ WIN32_FIND_DATA fd; char szBufX [MAX_PATH]; Ansi(szBufX, szBuf, MAX_PATH); - HANDLE h = FindFirstFile(szBufX, &fd); + HANDLE h = TheseusFindFirstFile(szBufX, &fd); if (h != INVALID_HANDLE_VALUE) { do @@ -411,7 +407,7 @@ bool CGameCopier::DeleteDirectory(const TCHAR* szDir, bool RemoveSelf /*= true*/ { if (!DeleteDirectory(szBuf)) { - FindClose(h); + TheseusFindClose(h); m_internalError = true; return false; } @@ -420,14 +416,14 @@ bool CGameCopier::DeleteDirectory(const TCHAR* szDir, bool RemoveSelf /*= true*/ { if (!DeleteFile(szBuf)) { - FindClose(h); + TheseusFindClose(h); m_internalError = true; return false; } } } - while (FindNextFile(h, &fd)); - FindClose(h); + while (TheseusFindNextFile(h, &fd)); + TheseusFindClose(h); } if (!RemoveSelf) @@ -437,7 +433,7 @@ bool CGameCopier::DeleteDirectory(const TCHAR* szDir, bool RemoveSelf /*= true*/ char szDirA [MAX_PATH]; Ansi(szDirA, szDir, MAX_PATH); - return ::RemoveDirectory(szDirA) != FALSE; + return TheseusRemoveDirectory(szDirA) != FALSE; } DWORD CGameCopier::ThreadProc() @@ -998,7 +994,7 @@ void CFolder::Refresh(const TCHAR* szPath) char szWildA [MAX_PATH]; Ansi(szWildA, szWild, MAX_PATH); - h = FindFirstFile(szWildA, &fd); + h = TheseusFindFirstFile(szWildA, &fd); if (h == INVALID_HANDLE_VALUE) { @@ -1071,9 +1067,9 @@ void CFolder::Refresh(const TCHAR* szPath) pFile->m_length = fd.nFileSizeLow; } } - while (FindNextFile(h, &fd)); + while (TheseusFindNextFile(h, &fd)); - FindClose(h); + TheseusFindClose(h); } static int __cdecl CmpName(const void* p1, const void* p2) diff --git a/theseus/shared/file_util.cpp b/theseus/shared/file_util.cpp index 6b3407d..e5cb22f 100755 --- a/theseus/shared/file_util.cpp +++ b/theseus/shared/file_util.cpp @@ -12,7 +12,7 @@ bool DoesFileExist(const TCHAR* szFileName) WIN32_FILE_ATTRIBUTE_DATA fad; Ansi(szFile, szFileName, MAX_PATH); - return GetFileAttributesEx(szFile, GetFileExInfoStandard, &fad) != 0; + return TheseusGetFileAttributesEx(szFile, GetFileExInfoStandard, &fad) != 0; } void MakePath(TCHAR* szBuf, const TCHAR* szDir, const TCHAR* szFile) @@ -216,7 +216,7 @@ int GetDirectoryBlocks(const TCHAR* szDirPath, int nBlockSize, bool bRecursive, char sszBuf [MAX_PATH]; Ansi(sszBuf, szBuf, MAX_PATH); - HANDLE hFind = FindFirstFile(sszBuf, &fd); + HANDLE hFind = TheseusFindFirstFile(sszBuf, &fd); if (hFind == INVALID_HANDLE_VALUE) return 0; @@ -241,8 +241,8 @@ int GetDirectoryBlocks(const TCHAR* szDirPath, int nBlockSize, bool bRecursive, nTotalBlocks += GetDirectoryBlocks(szBuf, nBlockSize, true, hCancelEvent); } } - while (FindNextFile(hFind, &fd)); - FindClose(hFind); + while (TheseusFindNextFile(hFind, &fd)); + TheseusFindClose(hFind); return bCancel ? -1 : nTotalBlocks; } diff --git a/theseus/shared/harddrive.cpp b/theseus/shared/harddrive.cpp index ec61c71..c3cb494 100755 --- a/theseus/shared/harddrive.cpp +++ b/theseus/shared/harddrive.cpp @@ -89,7 +89,7 @@ int CHardDrive::GetFreeSpace(const TCHAR *szDrive) char FreeSpace[MAX_PATH]; Ansi(FreeSpace, szDrive, countof(FreeSpace)); - if (GetDiskFreeSpaceEx(FreeSpace, + if (TheseusGetDiskFreeSpaceEx(FreeSpace, &lFreeBytesAvailable, &lTotalNumberOfBytes, &lTotalNumberOfFreeBytes) == 0) @@ -110,7 +110,7 @@ int CHardDrive::GetTotalSpace(const TCHAR *szDrive) char FreeSpace[MAX_PATH]; Ansi(FreeSpace, szDrive, countof(FreeSpace)); - if (GetDiskFreeSpaceEx(FreeSpace, + if (TheseusGetDiskFreeSpaceEx(FreeSpace, &lFreeBytesAvailable, &lTotalNumberOfBytes, &lTotalNumberOfFreeBytes) == 0) @@ -135,7 +135,7 @@ int CHardDrive::FileExists(const TCHAR *FileName) WIN32_FILE_ATTRIBUTE_DATA fad; Ansi(szFile, FileName, MAX_PATH); - int hr = GetFileAttributesEx(szFile, GetFileExInfoStandard, &fad); + int hr = TheseusGetFileAttributesEx(szFile, GetFileExInfoStandard, &fad); if (hr > 0) { @@ -150,7 +150,7 @@ void CHardDrive::MoveThisFile(const TCHAR *szSrcPath, const TCHAR *szDestPath) char NewFile[MAX_PATH]; Ansi(OriginalFile, szSrcPath, countof(OriginalFile)); Ansi(NewFile, szDestPath, countof(NewFile)); - MoveFileEx(OriginalFile, NewFile, MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING); + TheseusMoveFile(OriginalFile, NewFile, MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING); } void CHardDrive::MoveThisDirectory(const TCHAR *szSrcPath, const TCHAR *szDestPath) @@ -164,7 +164,7 @@ int CHardDrive::DeleteThisFile(const TCHAR *FileName) bool tempBool = false; char theFile[MAX_PATH]; Ansi(theFile, FileName, countof(theFile)); - int hr = DeleteFile(theFile); + int hr = TheseusDeleteFile(theFile); if (hr > 0) { @@ -182,7 +182,7 @@ int CHardDrive::DeleteThisDirectory(const TCHAR *szPath) sprintf(szBuf, "%s\\*.*", conversionString); WIN32_FIND_DATA fd; - HANDLE h = FindFirstFile(szBuf, &fd); + HANDLE h = TheseusFindFirstFile(szBuf, &fd); if (h != INVALID_HANDLE_VALUE) { do @@ -202,13 +202,13 @@ int CHardDrive::DeleteThisDirectory(const TCHAR *szPath) } else { - if (!DeleteFile(szBuf)) + if (!TheseusDeleteFile(szBuf)) return false; } - } while (FindNextFile(h, &fd)); - FindClose(h); + } while (TheseusFindNextFile(h, &fd)); + TheseusFindClose(h); } - int hr = RemoveDirectory(conversionString); + int hr = TheseusRemoveDirectory(conversionString); if (hr > 0) { @@ -223,7 +223,7 @@ void CHardDrive::CopyThisFile(const TCHAR *szSrcPath, const TCHAR *szDestPath) char destination[MAX_PATH]; Ansi(source, szSrcPath, countof(source)); Ansi(destination, szDestPath, countof(destination)); - CopyFile(source, destination, false); + TheseusCopyFile(source, destination, false); } void CHardDrive::CopyThisDirectory(const TCHAR *szSrcPath, const TCHAR *szDestPath) @@ -234,13 +234,13 @@ void CHardDrive::CopyThisDirectory(const TCHAR *szSrcPath, const TCHAR *szDestPa char theDestination[MAX_PATH]; Ansi(theDestination, szDestPath, countof(theDestination)); - CreateDirectory(theDestination, NULL); + TheseusCreateDirectory(theDestination, NULL); char szBuf[MAX_PATH]; sprintf(szBuf, "%s\\*.*", theSource); WIN32_FIND_DATA fd; - HANDLE h = FindFirstFile(szBuf, &fd); + HANDLE h = TheseusFindFirstFile(szBuf, &fd); if (h != INVALID_HANDLE_VALUE) { do @@ -265,10 +265,10 @@ void CHardDrive::CopyThisDirectory(const TCHAR *szSrcPath, const TCHAR *szDestPa } else { - CopyFile(szBuf, szBuf2, FALSE); + TheseusCopyFile(szBuf, szBuf2, FALSE); } - } while (FindNextFile(h, &fd)); - FindClose(h); + } while (TheseusFindNextFile(h, &fd)); + TheseusFindClose(h); } } @@ -277,7 +277,7 @@ int CHardDrive::CreateThisDirectory(const TCHAR *szDir) bool tempBool = false; char theFile[MAX_PATH]; Ansi(theFile, szDir, countof(theFile)); - int hr = CreateDirectory(theFile, NULL); + int hr = TheseusCreateDirectory(theFile, NULL); if (hr > 0) { @@ -291,7 +291,7 @@ int CHardDrive::RemoveThisDirectory(const TCHAR *szDir) bool tempBool = false; char theFile[MAX_PATH]; Ansi(theFile, szDir, countof(theFile)); - int hr = RemoveDirectory(theFile); + int hr = TheseusRemoveDirectory(theFile); if (hr > 0) { @@ -347,11 +347,11 @@ int CHardDrive::CopyGame(const TCHAR *szDestPath) char theDestination[MAX_PATH]; Ansi(theDestination, szDestPath, countof(theDestination)); - CreateDirectory(theDestination, NULL); + TheseusCreateDirectory(theDestination, NULL); char szBuf[MAX_PATH]; WIN32_FIND_DATA fd; - HANDLE h = FindFirstFile("D:\\*.*", &fd); + HANDLE h = TheseusFindFirstFile("D:\\*.*", &fd); if (h != INVALID_HANDLE_VALUE) { do @@ -376,10 +376,10 @@ int CHardDrive::CopyGame(const TCHAR *szDestPath) } else { - CopyFile(szBuf, szBuf2, FALSE); + TheseusCopyFile(szBuf, szBuf2, FALSE); } - } while (FindNextFile(h, &fd)); - FindClose(h); + } while (TheseusFindNextFile(h, &fd)); + TheseusFindClose(h); success = 1; } diff --git a/theseus/shared/music_collection.cpp b/theseus/shared/music_collection.cpp index 7c7461f..0a2726e 100755 --- a/theseus/shared/music_collection.cpp +++ b/theseus/shared/music_collection.cpp @@ -74,7 +74,7 @@ static void StripExtension(const char* src, char* dst, int maxLen) static int ProbeMP3Duration(const char* path) { - HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, + HANDLE hFile = TheseusCreateFile(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) return 0; @@ -127,7 +127,7 @@ static void ScanSoundtrackDir(const char* stPath, const char* dirName) _snprintf(searchPath, MAX_PATH_LEN, "%s\\*", stPath); WIN32_FIND_DATAA fd; - HANDLE hFind = FindFirstFileA(searchPath, &fd); + HANDLE hFind = TheseusFindFirstFile(searchPath, &fd); if (hFind == INVALID_HANDLE_VALUE) return; do @@ -142,8 +142,8 @@ static void ScanSoundtrackDir(const char* stPath, const char* dirName) song->duration = ProbeMP3Duration(song->path); st->songCount++; } - while (FindNextFileA(hFind, &fd)); - FindClose(hFind); + while (TheseusFindNextFile(hFind, &fd)); + TheseusFindClose(hFind); if (st->songCount > 0) g_soundtrackCount++; @@ -158,7 +158,7 @@ static void ScanMusicRoot() _snprintf(searchPath, MAX_PATH_LEN, "%s\\*", MUSIC_ROOT); WIN32_FIND_DATAA fd; - HANDLE hFind = FindFirstFileA(searchPath, &fd); + HANDLE hFind = TheseusFindFirstFile(searchPath, &fd); if (hFind == INVALID_HANDLE_VALUE) { OutputDebugStringA("[Music] No music directory found (this is OK)\n"); @@ -174,8 +174,8 @@ static void ScanMusicRoot() _snprintf(stPath, MAX_PATH_LEN, "%s\\%s", MUSIC_ROOT, fd.cFileName); ScanSoundtrackDir(stPath, fd.cFileName); } - while (FindNextFileA(hFind, &fd)); - FindClose(hFind); + while (TheseusFindNextFile(hFind, &fd)); + TheseusFindClose(hFind); if (g_soundtrackCount > 0) { diff --git a/theseus/shared/savegame_grid.cpp b/theseus/shared/savegame_grid.cpp index 13d81a7..2631534 100644 --- a/theseus/shared/savegame_grid.cpp +++ b/theseus/shared/savegame_grid.cpp @@ -363,7 +363,7 @@ static bool FindDLCFolderByIndex(const char* titleIDChar, int dlcIndex, _snprintf(searchPath, MAX_PATH, "E:\\TDATA\\%s\\$c\\*", titleIDChar); WIN32_FIND_DATAA findData; - HANDLE hFind = FindFirstFileA(searchPath, &findData); + HANDLE hFind = TheseusFindFirstFile(searchPath, &findData); if (hFind == INVALID_HANDLE_VALUE) return false; @@ -385,9 +385,9 @@ static bool FindDLCFolderByIndex(const char* titleIDChar, int dlcIndex, } currentIndex++; } - } while (FindNextFileA(hFind, &findData)); + } while (TheseusFindNextFile(hFind, &findData)); - FindClose(hFind); + TheseusFindClose(hFind); return found; } @@ -804,7 +804,7 @@ void CSavedGameGrid::RefreshDiskFreeSpace() #endif ULARGE_INTEGER availBytes, totalBytes, freeBytes; - if (!GetDiskFreeSpaceEx(path, &availBytes, &totalBytes, &freeBytes)) + if (!TheseusGetDiskFreeSpaceEx(path, &availBytes, &totalBytes, &freeBytes)) { // Treat query failures as a "full empty disk" so the UI shows // 0 free instead of stale values from the previous device. @@ -2306,7 +2306,7 @@ static int ScanDLContentCount(const char *titleID) _snprintf(dlcPath, MAX_PATH, "E:\\TDATA\\%s\\$c\\*", titleID); WIN32_FIND_DATAA findData; - HANDLE hFind = FindFirstFileA(dlcPath, &findData); + HANDLE hFind = TheseusFindFirstFile(dlcPath, &findData); if (hFind == INVALID_HANDLE_VALUE) return 0; @@ -2321,9 +2321,9 @@ static int ScanDLContentCount(const char *titleID) { dlcCount++; } - } while (FindNextFileA(hFind, &findData)); + } while (TheseusFindNextFile(hFind, &findData)); - FindClose(hFind); + TheseusFindClose(hFind); return dlcCount; } @@ -2492,10 +2492,10 @@ int CSavedGameGrid::IsDLContentPartial() titleIDChar, dlcFolder); WIN32_FIND_DATAA metaData; - HANDLE hMeta = FindFirstFileA(metaPath, &metaData); + HANDLE hMeta = TheseusFindFirstFile(metaPath, &metaData); if (hMeta == INVALID_HANDLE_VALUE) return 1; // No ContentMeta = partial download - FindClose(hMeta); + TheseusFindClose(hMeta); return 0; // ContentMeta exists = complete } diff --git a/theseus/shared/screen.cpp b/theseus/shared/screen.cpp index 7197906..05e557b 100755 --- a/theseus/shared/screen.cpp +++ b/theseus/shared/screen.cpp @@ -143,6 +143,6 @@ void CScreen::TakeScreenShot() sprintf( tempPath, "Q:\\Screenshots\\ScreenShot%d%d%d.bmp", theDate.getHours(), theDate.getMinutes(), theDate.getSeconds() ); - CreateDirectory( "Q:\\Screenshots", NULL ); + TheseusCreateDirectory( _T("Q:\\Screenshots"), NULL ); XGWriteSurfaceToFile( pFrontBuffer, tempPath ); } diff --git a/theseus/shared/settingsfile.cpp b/theseus/shared/settingsfile.cpp old mode 100755 new mode 100644 index 7464607..2020eea --- a/theseus/shared/settingsfile.cpp +++ b/theseus/shared/settingsfile.cpp @@ -157,7 +157,7 @@ bool CSettingsFile::Save() { // Write out Unicode file... - FILE* pFile = _tfopen(m_filePath, _T("w")); + FILE* pFile = TheseusFOpen(m_filePath, _T("w")); if (pFile == NULL) return false; diff --git a/theseus/shared/theseus.h b/theseus/shared/theseus.h index 0038ae0..6d5c570 100755 --- a/theseus/shared/theseus.h +++ b/theseus/shared/theseus.h @@ -695,6 +695,10 @@ class CDirPush +// Every file call that takes an Xbox path comes through here. The Xbox build +// hands it straight to XTL; the desktop build translates it to a host path +// first (desktop/xbox_file.cpp). Host paths never come through here. +#ifdef _XBOX inline HANDLE TheseusCreateFile(const TCHAR* szFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes = 0, HANDLE hTemplateFile = NULL) { // null path would crash inside CleanFilePath. Bail before we get there. @@ -702,38 +706,50 @@ inline HANDLE TheseusCreateFile(const TCHAR* szFileName, DWORD dwDesiredAccess, return INVALID_HANDLE_VALUE; FSCHAR sszFileName [MAX_PATH]; CleanFilePath(sszFileName, szFileName); -#ifdef _XBOX return CreateFile(sszFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); -#elif defined(THESEUS_HOST_WIN) - // Native Windows: use real Win32 CreateFileA so subsequent ReadFile/CloseHandle - // calls hit real kernel handles. The fopen path below would return a FILE* - // that the real Win32 APIs can't interpret. - return CreateFileA(sszFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); -#else - const char* mode = "rb"; - if (dwDesiredAccess & GENERIC_WRITE) mode = (dwCreationDisposition == CREATE_ALWAYS) ? "wb" : "r+b"; - FILE* f = fopen(sszFileName, mode); - if (!f && (dwCreationDisposition == CREATE_ALWAYS || dwCreationDisposition == OPEN_ALWAYS)) - f = fopen(sszFileName, "wb"); - return f ? (HANDLE)f : INVALID_HANDLE_VALUE; -#endif } inline bool TheseusCreateDirectory(LPCTSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL) { FSCHAR sszFileName [MAX_PATH]; CleanFilePath(sszFileName, lpPathName); -#ifdef _XBOX return CreateDirectory(sszFileName, lpSecurityAttributes) != FALSE; -#else - (void)lpSecurityAttributes; -#ifdef THESEUS_HOST_WIN - return CreateDirectoryA(sszFileName, NULL) != FALSE; -#else - return mkdir(sszFileName, 0755) == 0 || errno == EEXIST; +} + +#ifdef _UNICODE +// Same two calls for code that already holds a narrow path. +inline HANDLE TheseusCreateFile(const char* path, DWORD access, DWORD share, LPSECURITY_ATTRIBUTES sa, DWORD disp, DWORD flags = 0, HANDLE templ = NULL) { return CreateFile(path, access, share, sa, disp, flags, templ); } +inline bool TheseusCreateDirectory(const char* path, LPSECURITY_ATTRIBUTES sa = NULL) { return CreateDirectory(path, sa) != FALSE; } #endif + +inline BOOL TheseusDeleteFile(LPCSTR path) { return DeleteFile(path); } +inline BOOL TheseusRemoveDirectory(LPCSTR path) { return RemoveDirectory(path); } +inline BOOL TheseusSetFileAttributes(LPCSTR path, DWORD attr) { return SetFileAttributes(path, attr); } +inline DWORD TheseusGetFileAttributes(LPCSTR path) { return GetFileAttributes(path); } +inline BOOL TheseusGetFileAttributesEx(LPCSTR path, GET_FILEEX_INFO_LEVELS level, LPVOID info) { return GetFileAttributesEx(path, level, info); } +inline BOOL TheseusGetDiskFreeSpaceEx(LPCSTR path, ULARGE_INTEGER* pFree, ULARGE_INTEGER* pTotal, ULARGE_INTEGER* pTotalFree) { return GetDiskFreeSpaceEx(path, pFree, pTotal, pTotalFree); } +inline HANDLE TheseusFindFirstFile(LPCSTR path, WIN32_FIND_DATAA* fd) { return FindFirstFile(path, fd); } +inline BOOL TheseusFindNextFile(HANDLE h, WIN32_FIND_DATAA* fd) { return FindNextFile(h, fd); } +inline BOOL TheseusFindClose(HANDLE h) { return FindClose(h); } +inline BOOL TheseusMoveFile(LPCSTR src, LPCSTR dst, DWORD flags = 0) { return MoveFileEx(src, dst, flags); } +inline BOOL TheseusCopyFile(LPCSTR src, LPCSTR dst, BOOL bFailIfExists) { return CopyFile(src, dst, bFailIfExists); } +inline FILE* TheseusFOpen(const TCHAR* path, const TCHAR* mode) { return _tfopen(path, mode); } +#else +HANDLE TheseusCreateFile(const TCHAR* szFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes = 0, HANDLE hTemplateFile = NULL); +bool TheseusCreateDirectory(LPCTSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL); +BOOL TheseusDeleteFile(LPCSTR path); +BOOL TheseusRemoveDirectory(LPCSTR path); +BOOL TheseusSetFileAttributes(LPCSTR path, DWORD attr); +DWORD TheseusGetFileAttributes(LPCSTR path); +BOOL TheseusGetFileAttributesEx(LPCSTR path, GET_FILEEX_INFO_LEVELS level, LPVOID info); +BOOL TheseusGetDiskFreeSpaceEx(LPCSTR path, ULARGE_INTEGER* pFree, ULARGE_INTEGER* pTotal, ULARGE_INTEGER* pTotalFree); +HANDLE TheseusFindFirstFile(LPCSTR path, WIN32_FIND_DATAA* fd); +BOOL TheseusFindNextFile(HANDLE h, WIN32_FIND_DATAA* fd); +BOOL TheseusFindClose(HANDLE h); +BOOL TheseusMoveFile(LPCSTR src, LPCSTR dst, DWORD flags = 0); +BOOL TheseusCopyFile(LPCSTR src, LPCSTR dst, BOOL bFailIfExists); +FILE* TheseusFOpen(const TCHAR* path, const TCHAR* mode); #endif -} diff --git a/theseus/shared/title_collection.cpp b/theseus/shared/title_collection.cpp index 219740c..b2787b0 100755 --- a/theseus/shared/title_collection.cpp +++ b/theseus/shared/title_collection.cpp @@ -144,7 +144,7 @@ void CTitleArray::Update() MakePath(szFileName, GetUData(), _T("*.*")); FSCHAR szBuf [MAX_PATH]; Ansi(szBuf, szFileName, countof (szBuf)); - HANDLE hFind = FindFirstFile(szBuf, &fd); + HANDLE hFind = TheseusFindFirstFile(szBuf, &fd); if (hFind == INVALID_HANDLE_VALUE) { @@ -169,17 +169,17 @@ void CTitleArray::Update() AddTitle(szFileName); } - while (FindNextFile(hFind, &fd)); + while (TheseusFindNextFile(hFind, &fd)); if (ERROR_DEVICE_NOT_CONNECTED == GetLastError()) { - FindClose(hFind); + TheseusFindClose(hFind); DeleteAll(false); LeaveCriticalSection(&m_rootLock); return; } - FindClose(hFind); + TheseusFindClose(hFind); qsort(m_titles, m_titleCount, sizeof (CTitle), SortTitleCompare); @@ -465,7 +465,7 @@ int ScanSavedGames(const TCHAR* szRoot, const TCHAR* szTitleID, CSave* rgsaves / char szBuf [MAX_PATH]; Ansi(szBuf, szTitleDirWild, MAX_PATH); - hFind = FindFirstFile(szBuf, &fd); + hFind = TheseusFindFirstFile(szBuf, &fd); } if (hFind != INVALID_HANDLE_VALUE) @@ -494,8 +494,8 @@ int ScanSavedGames(const TCHAR* szRoot, const TCHAR* szTitleID, CSave* rgsaves / nSavedGameCount += 1; } - while (FindNextFile(hFind, &fd)); - FindClose(hFind); + while (TheseusFindNextFile(hFind, &fd)); + TheseusFindClose(hFind); } if (!bCancel) diff --git a/theseus/shared/xbe.cpp b/theseus/shared/xbe.cpp index dea5338..79adf2d 100755 --- a/theseus/shared/xbe.cpp +++ b/theseus/shared/xbe.cpp @@ -167,10 +167,10 @@ int CXBExecutable::ReadFile( const char * szFileName, const bool bGetTitleImage, // Copy path strcpy( m_szFileName, szFileName ); - if( GetFileAttributes( szFileName ) != -1 ) + if( TheseusGetFileAttributes( szFileName ) != -1 ) { // Open the local file - hFile = CreateFile( szFileName, + hFile = TheseusCreateFile( szFileName, GENERIC_READ, 0, NULL, @@ -390,7 +390,7 @@ int CXBExecutable::SaveFile( void ) } // Open the local file - hFile = ::CreateFile( m_szFileName, + hFile = TheseusCreateFile( m_szFileName, GENERIC_WRITE, 0, NULL, @@ -766,7 +766,7 @@ int CXBExecutable::ReadTitleName( void ) sprintf( m_szBuffer, "E:\\UDATA\\%08lx\\TitleMeta.xbx", m_ulTitleId ); // Open the local file - hTitleMetaFile = CreateFile( m_szBuffer, + hTitleMetaFile = TheseusCreateFile( m_szBuffer, GENERIC_READ, 0, NULL, From 26ccdef62302c97e545641ba22eae70ee2534959 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:31:00 -0400 Subject: [PATCH 004/167] Host paths: no stray dirs, logs in user data, MediaDB saves on Windows Configs/Library mkdirs used relative paths and dropped folders in the working directory. The hang dump and graphics log now land in the user data dir. MediaDB's cache rename replaces the old file on Windows, where rename() won't. --- theseus/desktop/jellyfin_client.cpp | 2 +- theseus/desktop/media_db.cpp | 5 +++++ theseus/desktop/playlist.cpp | 2 +- theseus/desktop/plex_client.cpp | 2 +- theseus/desktop/sdl_main.cpp | 16 ++++++++++------ theseus/desktop/tmdb.cpp | 2 +- theseus/desktop/virtual_games.h | 2 +- theseus/desktop/xcloud_client.cpp | 2 +- 8 files changed, 21 insertions(+), 12 deletions(-) diff --git a/theseus/desktop/jellyfin_client.cpp b/theseus/desktop/jellyfin_client.cpp index 4697cc9..f2a1574 100644 --- a/theseus/desktop/jellyfin_client.cpp +++ b/theseus/desktop/jellyfin_client.cpp @@ -573,7 +573,7 @@ std::string Jellyfin_StreamUrl(const std::string& itemId) static std::string ArtHostPath(const std::string& itemId) { - Mkdir_("Library"); + Mkdir_(AppPath_Tree("Library")); Mkdir_(AppPath_Tree("Library/Jellyfin")); return std::string(AppPath_Tree("Library/Jellyfin")) + "/" + itemId + ".jpg"; } diff --git a/theseus/desktop/media_db.cpp b/theseus/desktop/media_db.cpp index 5313011..c25e262 100644 --- a/theseus/desktop/media_db.cpp +++ b/theseus/desktop/media_db.cpp @@ -385,7 +385,12 @@ static void WriteCacheTo(const std::vector& movies, } fclose(f); +#ifdef THESEUS_HOST_WIN + // MS CRT rename() won't replace an existing file. + MoveFileExA(tmp.c_str(), path, MOVEFILE_REPLACE_EXISTING); +#else rename(tmp.c_str(), path); +#endif printf("[MediaDB] Saved cache: %u movies, %u shows\n", nMovies, nShows); } diff --git a/theseus/desktop/playlist.cpp b/theseus/desktop/playlist.cpp index 30d7322..8384d24 100644 --- a/theseus/desktop/playlist.cpp +++ b/theseus/desktop/playlist.cpp @@ -56,7 +56,7 @@ void Playlist_LoadAll() { } void Playlist_SaveAll() { - Plat_MkdirP("Configs"); + Plat_MkdirP(AppPath_Tree("Configs")); FILE* fp = fopen(kPath, "w"); if (!fp) return; for (size_t i = 0; i < s_playlists.size(); i++) { diff --git a/theseus/desktop/plex_client.cpp b/theseus/desktop/plex_client.cpp index 2769a38..2bf631e 100644 --- a/theseus/desktop/plex_client.cpp +++ b/theseus/desktop/plex_client.cpp @@ -639,7 +639,7 @@ bool Plex_Cache_GetEpisodes(const std::string& seasonRatingKey, // Host path: curl writes here. Lives at Library/Plex/.jpg next to binary. static std::string Plex_ArtCacheHostPath(const std::string& ratingKey) { - Mkdir_("Library"); + Mkdir_(AppPath_Tree("Library")); Mkdir_(AppPath_Tree("Library/Plex")); return std::string(AppPath_Tree("Library/Plex")) + "/" + ratingKey + ".jpg"; } diff --git a/theseus/desktop/sdl_main.cpp b/theseus/desktop/sdl_main.cpp index 94ec2f5..fd0a1ab 100644 --- a/theseus/desktop/sdl_main.cpp +++ b/theseus/desktop/sdl_main.cpp @@ -88,7 +88,7 @@ bgfx::ShaderHandle theseus_bgfx_load_shader(const char* name) #include #pragma comment(lib, "dbghelp.lib") -// Watchdog: dumps main thread stack to hang_dump.txt after 10s of detected hang +// Watchdog: dumps main thread stack to hang_dump.txt in the user data dir after 10s of detected hang static DWORD WINAPI HangWatchdog(LPVOID param) { DWORD mainThread = (DWORD)(uintptr_t)param; while (true) { @@ -98,7 +98,9 @@ static DWORD WINAPI HangWatchdog(LPVOID param) { if (!g_hangCheck) continue; if (GetTickCount() - g_hangStartTime < 10000) continue; - FILE* f = fopen("hang_dump.txt", "w"); + char dumpPath[512]; + snprintf(dumpPath, sizeof(dumpPath), "%s/hang_dump.txt", Plat_UserDataDir()); + FILE* f = fopen(dumpPath, "w"); if (!f) continue; fprintf(f, "=== HANG DETECTED (>10s) ===\n"); fprintf(f, "Main thread ID: %lu\n\n", mainThread); @@ -135,7 +137,7 @@ static DWORD WINAPI HangWatchdog(LPVOID param) { CloseHandle(hThread); } fclose(f); - fprintf(stderr, "[Watchdog] Hang dump written to hang_dump.txt\n"); + fprintf(stderr, "[Watchdog] Hang dump written to %s\n", dumpPath); g_hangCheck = false; } return 0; @@ -229,10 +231,12 @@ static float s_crtTime = 0.0f; #endif // `--graphics-debug` dumper. Writes GPU / driver / vsync / MSAA / display info -// to stderr and theseus_graphics.log next to the binary. +// to stderr and theseus_graphics.log in the user data dir. static void DumpGraphicsInfo() { - FILE* logFp = fopen("theseus_graphics.log", "w"); + char logPath[512]; + snprintf(logPath, sizeof(logPath), "%s/theseus_graphics.log", Plat_UserDataDir()); + FILE* logFp = fopen(logPath, "w"); auto write2 = [&logFp](const char* line) { fprintf(stderr, "%s", line); @@ -562,7 +566,7 @@ static void RereadLegacyFromDisk() { } void SaveDesktopSettings() { - Plat_MkdirP("Configs"); + Plat_MkdirP(AppPath_Tree("Configs")); RereadLegacyFromDisk(); FILE* fp = fopen(AppPath("Configs/desktop.ini"), "w"); if (!fp) return; diff --git a/theseus/desktop/tmdb.cpp b/theseus/desktop/tmdb.cpp index 78037d5..0dcf7e7 100644 --- a/theseus/desktop/tmdb.cpp +++ b/theseus/desktop/tmdb.cpp @@ -70,7 +70,7 @@ static inline int Mkdir(const char* path) static std::string CachePath(const char* prefix, const std::string& slug) { std::string p = std::string(AppPath_Tree("Library/TMDB")) + "/"; - Mkdir("Library"); + Mkdir(AppPath_Tree("Library")); Mkdir(p.c_str()); p += prefix; p += '_'; diff --git a/theseus/desktop/virtual_games.h b/theseus/desktop/virtual_games.h index 9fb2e41..1953f1d 100644 --- a/theseus/desktop/virtual_games.h +++ b/theseus/desktop/virtual_games.h @@ -117,7 +117,7 @@ inline void VGames_Reload() { } inline void VGames_Save() { - Plat_MkdirP("Configs"); + Plat_MkdirP(AppPath_Tree("Configs")); FILE* fp = fopen(VGAMES_INI, "w"); if (!fp) return; diff --git a/theseus/desktop/xcloud_client.cpp b/theseus/desktop/xcloud_client.cpp index 9f72ed3..b2d6165 100644 --- a/theseus/desktop/xcloud_client.cpp +++ b/theseus/desktop/xcloud_client.cpp @@ -889,7 +889,7 @@ static std::map*> s_artInFlight; // Host path curl writes to, next to the binary. static std::string Xcloud_ArtCacheHostPath(const std::string& titleId) { - Mkdir_("Library"); + Mkdir_(AppPath_Tree("Library")); Mkdir_(AppPath_Tree("Library/Xcloud")); return std::string(AppPath_Tree("Library/Xcloud")) + "/" + titleId + ".jpg"; } From d605a95df86d92551da8cf8e3ef50fe2a82d6c3e Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 24 Sep 2026 00:31:20 -0400 Subject: [PATCH 005/167] One frame profiler for both builds shared/frame_profiler replaces the loose g_* counters and hand-rolled averaging: fixed phases and counters, averaged over 250ms, read as one snapshot by the perf widget, the fps widget, the System page and the desktop --graphics-debug overlay. Xbox measures CPU time blocked on the GPU with a per-frame fence, only while the perf widget is on screen. Desktop pulls GPU time, draw count and transient buffer use from bgfx::getStats, and --graphics-debug writes a line every 5s to theseus_perf.log in the user data dir. Scene draws are counted apart from the HUD, and D3DX text mesh draws, world updates, state calls and repeats, disk queries and Inline load time are counted for the first time. Xbox state repeats are counted against a shadow copy only; every call still goes to the device. --- build/Makefile | 2 + theseus/desktop/dashapp.cpp | 8 +- theseus/desktop/mesh.cpp | 1 + theseus/desktop/sdl_main.cpp | 104 ++++++++++++----- theseus/desktop/text.cpp | 1 + theseus/desktop/xip.cpp | 1 + theseus/render/asset_loader.cpp | 5 +- theseus/render/scene_groups.cpp | 28 ++--- theseus/render/text.cpp | 7 +- theseus/render/widget_draw.cpp | 10 +- theseus/shared/frame_profiler.cpp | 186 ++++++++++++++++++++++++++++++ theseus/shared/frame_profiler.h | 85 ++++++++++++++ theseus/shared/theseus.h | 47 +++++++- theseus/xbox/fps_widget.cpp | 14 +-- theseus/xbox/main.cpp | 158 +++---------------------- theseus/xbox/overlay.cpp | 6 +- theseus/xbox/perf_widget.cpp | 82 ++++++------- theseus/xbox/xip_archive.cpp | 5 +- 18 files changed, 483 insertions(+), 267 deletions(-) create mode 100644 theseus/shared/frame_profiler.cpp create mode 100644 theseus/shared/frame_profiler.h mode change 100755 => 100644 theseus/xbox/main.cpp diff --git a/build/Makefile b/build/Makefile index 8bebb6a..a196f11 100644 --- a/build/Makefile +++ b/build/Makefile @@ -312,6 +312,7 @@ SRCS_SHARED = \ shared/timezone.cpp \ shared/title_collection.cpp \ shared/util.cpp \ + shared/frame_profiler.cpp \ shared/xap_vm.cpp \ shared/xbe.cpp @@ -784,6 +785,7 @@ DESKTOP_SRCS_ENGINE = \ $(THESEUS_SRC)/engine/xap_compile.cpp \ $(THESEUS_SRC)/render/node_system.cpp \ $(THESEUS_SRC)/shared/util.cpp \ + $(THESEUS_SRC)/shared/frame_profiler.cpp \ $(THESEUS_SRC)/engine/array.cpp \ $(THESEUS_SRC)/shared/string_util.cpp \ $(THESEUS_SRC)/engine/math_node.cpp \ diff --git a/theseus/desktop/dashapp.cpp b/theseus/desktop/dashapp.cpp index dd782d1..1753e87 100755 --- a/theseus/desktop/dashapp.cpp +++ b/theseus/desktop/dashapp.cpp @@ -104,8 +104,6 @@ DWORD g_dwMainThreadId = 0; // Desktop-only state (was in CDashApp class, now standalone globals). int g_nVertPerFrame = 0; int g_nTriPerFrame = 0; -int g_nodeVisitsThisFrame = 0; -int g_nodeSkipsThisFrame = 0; char* g_szAppTitle = NULL; D3DXMATRIX g_matView; @@ -495,6 +493,7 @@ struct COLORVERTEX void Draw() { + Frame_PhaseBegin(FP_SCENE_SETUP); // Setup projection transform if (g_bProjectionDirty) { @@ -714,6 +713,8 @@ void Draw() } // Draw the world + Frame_PhaseEnd(FP_SCENE_SETUP); + Frame_PhaseBegin(FP_SCENE_TREE); { // Clear active-keyboard tracker before scene render; visible CKeyboards // will re-set it via their Render() override. Hidden ones never run, so @@ -727,8 +728,11 @@ void Draw() g_pObject->Render(); TheseusPopWorld(); } + Frame_PhaseEnd(FP_SCENE_TREE); + Frame_PhaseBegin(FP_SCENE_END); TheseusEndScene(); + Frame_PhaseEnd(FP_SCENE_END); TheseusSetRenderState(D3DRS_SWATHWIDTH, D3DSWATH_OFF); TheseusPresent(); diff --git a/theseus/desktop/mesh.cpp b/theseus/desktop/mesh.cpp index f1db289..f2acda5 100644 --- a/theseus/desktop/mesh.cpp +++ b/theseus/desktop/mesh.cpp @@ -172,6 +172,7 @@ void CMesh::Render(bool bSetFVF) TheseusSetStreamSource(0, m_vertexBuffer, m_nVertexStride); TheseusSetIndices(m_indexBuffer, 0); + Frame_Count(FC_DRAW_SCENE); TheseusDrawIndexedPrimitive(m_primitiveType, 0, m_nVertexCount, 0, m_nFaceCount); g_nVertPerFrame += m_nVertexCount; diff --git a/theseus/desktop/sdl_main.cpp b/theseus/desktop/sdl_main.cpp index fd0a1ab..d5327ee 100644 --- a/theseus/desktop/sdl_main.cpp +++ b/theseus/desktop/sdl_main.cpp @@ -230,6 +230,37 @@ static float s_crtTime = 0.0f; // pair, so skip under BGFX to avoid the dup-symbol link error. #endif +// --graphics-debug also leaves a perf trail testers can attach to a report: +// one line every 5 seconds in theseus_perf.log in the user data dir. +static void WritePerfLogLine() +{ + static FILE* s_log = NULL; + static Uint32 s_last = 0; + Uint32 now = SDL_GetTicks(); + if (s_last && now - s_last < 5000) return; + s_last = now; + + if (!s_log) { + char path[512]; + snprintf(path, sizeof(path), "%s/theseus_perf.log", Plat_UserDataDir()); + s_log = fopen(path, "w"); + if (!s_log) return; + fprintf(s_log, "# sec fps frame_ms gpu_ms render_wait_incl_vsync events advance scene imgui ticks present " + "draws_scene draws_bgfx nodes world state repeats disk load_max\n"); + } + FrameSnapshot f; + Frame_Read(&f); + fprintf(s_log, "%u %.1f %.2f %.2f %.2f %.2f %.2f %.2f %.2f %.2f %.2f %.0f %.0f %.0f %.0f %.0f %.0f %.1f %.0f\n", + now / 1000, f.fps, f.frameMs, f.gpuMs, f.phaseMs[FP_GPU_WAIT], + f.phaseMs[FP_EVENTS], f.phaseMs[FP_ADVANCE], + f.phaseMs[FP_SCENE_SETUP] + f.phaseMs[FP_SCENE_TREE] + f.phaseMs[FP_SCENE_END], + f.phaseMs[FP_IMGUI], f.phaseMs[FP_TICKS], f.phaseMs[FP_PRESENT], + f.count[FC_DRAW_SCENE] + f.count[FC_DRAW_SUBSET], f.hostDraws, f.count[FC_NODE_VISITS], + f.count[FC_UPDATE_WORLD], f.count[FC_STATE_CALLS], f.count[FC_STATE_REPEATS], + f.count[FC_DISK_QUERIES], f.maxLoadMs); + fflush(s_log); +} + // `--graphics-debug` dumper. Writes GPU / driver / vsync / MSAA / display info // to stderr and theseus_graphics.log in the user data dir. static void DumpGraphicsInfo() @@ -327,7 +358,6 @@ static void ApplyEffectiveMute() void ApplyEffectiveMute_Public() { ApplyEffectiveMute(); } bool g_startMinimized = false; bool g_graphicsDebug = false; -extern double g_perfDrawMs, g_perfImguiMs, g_perfSwapMs, g_perfFrameMs, g_perfFps; float g_muteOverlayTimer = 0.0f; // seconds remaining to show the overlay toast // Instance lists for recreating GPU resources on device reset. @@ -984,12 +1014,28 @@ static void PreSwapOverlays() { ImGui::SetNextWindowBgAlpha(0.6f); ImGui::SetNextWindowPos(ImVec2(8, 30), ImGuiCond_FirstUseEver); if (ImGui::Begin("##perf", nullptr, fl)) { - ImGui::Text("FPS: %.1f", g_perfFps); - ImGui::Text("Frame: %.2f ms", g_perfFrameMs); + FrameSnapshot f; + Frame_Read(&f); + ImGui::Text("FPS: %.1f (%.2f ms)", f.fps, f.frameMs); + ImGui::Text("GPU: %.2f ms render wait %.2f ms (incl. vsync)", f.gpuMs, f.phaseMs[FP_GPU_WAIT]); ImGui::Separator(); - ImGui::Text("Draw: %.2f ms", g_perfDrawMs); - ImGui::Text("ImGui: %.2f ms", g_perfImguiMs); - ImGui::Text("Swap: %.2f ms", g_perfSwapMs); + ImGui::Text("Events: %.2f ms", f.phaseMs[FP_EVENTS]); + ImGui::Text("Advance: %.2f ms", f.phaseMs[FP_ADVANCE]); + ImGui::Text("Scene: %.2f (setup %.2f, tree %.2f)", + f.phaseMs[FP_SCENE_SETUP] + f.phaseMs[FP_SCENE_TREE] + f.phaseMs[FP_SCENE_END], + f.phaseMs[FP_SCENE_SETUP], f.phaseMs[FP_SCENE_TREE]); + ImGui::Text("ImGui: %.2f ms", f.phaseMs[FP_IMGUI]); + ImGui::Text("Ticks: %.2f ms", f.phaseMs[FP_TICKS]); + ImGui::Text("Present: %.2f ms", f.phaseMs[FP_PRESENT]); + ImGui::Separator(); + ImGui::Text("Draws: scene %.0f+%.0f bgfx %.0f", + f.count[FC_DRAW_SCENE], f.count[FC_DRAW_SUBSET], f.hostDraws); + ImGui::Text("Nodes: %.0f visit / %.0f skip", f.count[FC_NODE_VISITS], f.count[FC_NODE_SKIPS]); + ImGui::Text("World: %.0f State: %.0f / %.0f repeat", + f.count[FC_UPDATE_WORLD], f.count[FC_STATE_CALLS], f.count[FC_STATE_REPEATS]); + ImGui::Text("Transient: vb %.0f KB ib %.0f KB", f.transientVbKB, f.transientIbKB); + ImGui::Text("Disk: %.1f/frame Load: %.0f ms (max %.0f)", + f.count[FC_DISK_QUERIES], f.lastLoadMs, f.maxLoadMs); } ImGui::End(); } @@ -1029,14 +1075,6 @@ int g_windowResolution = 720; int g_windowMode = 0; bool g_displayChangeRequested = false; -// Per-pass timings populated each frame in the main loop. Read by the -// --graphics-debug overlay. Single-frame ms; FPS is EMA-smoothed. -double g_perfDrawMs = 0.0; -double g_perfImguiMs = 0.0; -double g_perfSwapMs = 0.0; -double g_perfFrameMs = 0.0; -double g_perfFps = 0.0; - // bgfx reset flags from g_vsyncMode + g_msaaSamples. bgfx has no adaptive // vsync, so only an explicit "off" (mode 2) disables it; the default and // "on" both vsync. Running without vsync spins the GPU flat out and paces @@ -1866,6 +1904,8 @@ int main(int argc, char* argv[]) { Uint64 fpsCapPrev = SDL_GetPerformanceCounter(); Uint64 perfFreq = SDL_GetPerformanceFrequency(); while (running) { + Frame_Begin(); + Frame_PhaseBegin(FP_EVENTS); SDL_Event event; while (SDL_PollEvent(&event)) { // Let ImGui handle events first @@ -2085,6 +2125,7 @@ int main(int argc, char* argv[]) { break; } } + Frame_PhaseEnd(FP_EVENTS); // All panels are floating ImGui windows in the main window now. { @@ -2189,7 +2230,10 @@ int main(int argc, char* argv[]) { ApplyEffectiveMute(); } - if (!g_mediaFullscreen && !g_xcloudFullscreen) Advance(); + if (!g_mediaFullscreen && !g_xcloudFullscreen) { + FramePhaseScope advance(FP_ADVANCE); + Advance(); + } // Same idea under bgfx: allocate / resize the offscreen FB to // window size, point view 0 at it so the scene draws there @@ -2206,7 +2250,6 @@ int main(int argc, char* argv[]) { } // Render 3D scene (viewport set to left portion; Present() skips swap when inspector is open) - Uint64 tDraw0 = SDL_GetPerformanceCounter(); if (g_mediaFullscreen) { extern void MediaUI_DrawFullscreenVideo(); MediaUI_DrawFullscreenVideo(); @@ -2215,7 +2258,6 @@ int main(int argc, char* argv[]) { } else { Draw(); } - Uint64 tDraw1 = SDL_GetPerformanceCounter(); // Force fill before ImGui so overlays render solid. Must go through // the wrapper, otherwise the wrapper cache stays at WIREFRAME and @@ -2229,9 +2271,9 @@ int main(int argc, char* argv[]) { } // Overlays (menu bar, inspector, Title Maker, etc.); always render - Uint64 tImgui0 = SDL_GetPerformanceCounter(); + Frame_PhaseBegin(FP_IMGUI); if (IDirect3DDevice8::s_preSwapCB) IDirect3DDevice8::s_preSwapCB(); - Uint64 tImgui1 = SDL_GetPerformanceCounter(); + Frame_PhaseEnd(FP_IMGUI); // FPS cap: sleep until budget elapsed. 0 = unlimited. if (g_fpsCap > 0) { @@ -2269,28 +2311,32 @@ int main(int argc, char* argv[]) { MilkdropWindow_Shutdown(); s_prevToggle = g_useMilkdropViz; + Frame_PhaseBegin(FP_TICKS); MilkdropWindow_Tick(); // Drive the xCloud->VGDB injection state machine (main-thread only). extern void XcloudVGames_Tick(); XcloudVGames_Tick(); + Frame_PhaseEnd(FP_TICKS); - // Swap - Uint64 tSwap0 = SDL_GetPerformanceCounter(); // bgfx owns presentation; its backend layer is the visible surface. // No GL drawable, so no SDL_GL_SwapWindow. + Frame_PhaseBegin(FP_PRESENT); bgfx::frame(); - Uint64 tSwap1 = SDL_GetPerformanceCounter(); + Frame_PhaseEnd(FP_PRESENT); if (g_graphicsDebug) { - double freq = (double)perfFreq; - g_perfDrawMs = (tDraw1 - tDraw0) * 1000.0 / freq; - g_perfImguiMs = (tImgui1 - tImgui0) * 1000.0 / freq; - g_perfSwapMs = (tSwap1 - tSwap0) * 1000.0 / freq; - g_perfFrameMs = (tSwap1 - tDraw0) * 1000.0 / freq; - double instFps = (g_perfFrameMs > 0.0) ? 1000.0 / g_perfFrameMs : 0.0; - g_perfFps = (g_perfFps == 0.0) ? instFps : 0.9 * g_perfFps + 0.1 * instFps; + Frame_RequestGpuTiming(); + const bgfx::Stats* st = bgfx::getStats(); + float gpuMs = 0.0f, waitMs = 0.0f; + if (st->gpuTimerFreq > 0) + gpuMs = (float)((double)(st->gpuTimeEnd - st->gpuTimeBegin) * 1000.0 / (double)st->gpuTimerFreq); + if (st->cpuTimerFreq > 0) + waitMs = (float)((double)st->waitRender * 1000.0 / (double)st->cpuTimerFreq); + Frame_AddHostStats(gpuMs, waitMs, (int)st->numDraw, st->transientVbUsed, st->transientIbUsed); } + Frame_End(); + if (g_graphicsDebug) WritePerfLogLine(); // (Old code re-applied Mix_Volume(-1, 0) every frame while muted. // Removed: rapid per-frame volume writes on playing channels were diff --git a/theseus/desktop/text.cpp b/theseus/desktop/text.cpp index b1a39a7..53b645c 100755 --- a/theseus/desktop/text.cpp +++ b/theseus/desktop/text.cpp @@ -1332,6 +1332,7 @@ void CTextNode::Render() // brightness, while vertex alpha controls clipping. TheseusSetVertexAlphaMul(TRUE); TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); + Frame_Count(FC_DRAW_SUBSET); m_pMesh->DrawSubset(0); TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); TheseusSetVertexAlphaMul(FALSE); diff --git a/theseus/desktop/xip.cpp b/theseus/desktop/xip.cpp index 77202a8..cd2da21 100644 --- a/theseus/desktop/xip.cpp +++ b/theseus/desktop/xip.cpp @@ -762,6 +762,7 @@ void CMeshRef::Render(bool bSetFVF/*=true*/) TheseusSetStreamSource(0, pMeshBuffer->m_pVertexBuffer, pMeshBuffer->m_nVertexStride); TheseusSetIndices(pMeshBuffer->m_pIndexBuffer, 0); + Frame_Count(FC_DRAW_SCENE); TheseusDrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, pMeshBuffer->m_nVertexCount, m_firstIndex, m_primitiveCount); } diff --git a/theseus/render/asset_loader.cpp b/theseus/render/asset_loader.cpp index d47474f..173ffec 100644 --- a/theseus/render/asset_loader.cpp +++ b/theseus/render/asset_loader.cpp @@ -1434,10 +1434,7 @@ void CMesh::Render(bool bSetFVF) TheseusSetStreamSource(0, m_vertexBuffer, m_nVertexStride); TheseusSetIndices(m_indexBuffer, 0); - extern int g_drawCallsThisFrame; - extern int g_drawCallsSceneFrame; - g_drawCallsThisFrame++; - g_drawCallsSceneFrame++; + Frame_Count(FC_DRAW_SCENE); TheseusDrawIndexedPrimitive(m_primitiveType, 0, m_nVertexCount, 0, m_nFaceCount); } diff --git a/theseus/render/scene_groups.cpp b/theseus/render/scene_groups.cpp index a47d397..bf752dd 100755 --- a/theseus/render/scene_groups.cpp +++ b/theseus/render/scene_groups.cpp @@ -134,8 +134,6 @@ float CGroup::GetRadius() void CGroup::Render() { - extern int g_nodeVisitsThisFrame; - extern int g_nodeSkipsThisFrame; int nChildCount = m_children.GetLength(); @@ -143,11 +141,11 @@ void CGroup::Render() for (i = 0; i < nChildCount; i += 1) { CNode* pNode = m_children.GetNode(i); - g_nodeVisitsThisFrame++; + Frame_Count(FC_NODE_VISITS); if (pNode->m_visible) pNode->Render(); else - g_nodeSkipsThisFrame++; + Frame_Count(FC_NODE_SKIPS); } } @@ -497,8 +495,7 @@ void CTransform::Render() // Whole subtree skipped due to zero alpha. Counts as one // skip event regardless of subtree size since we don't // descend to know how big it is. - extern int g_nodeSkipsThisFrame; - g_nodeSkipsThisFrame++; + Frame_Count(FC_NODE_SKIPS); } g_nEffectAlpha = nEffectAlphaSave; @@ -619,6 +616,8 @@ void CInline::FinishLoad() TRACE(_T("FinishLoad: (0x%08x) %s\n"), this, m_url); ASSERT(!m_bClassLoaded); + // Compile plus initialize/onLoad lands inside one frame; that's the hitch. + long long loadStart = Frame_Now(); if (m_class.LoadAbsURL(m_szAbsURL)) { CNode* pNode = m_class.CreateNode(); @@ -630,6 +629,7 @@ void CInline::FinishLoad() } m_bFirstLoad = false; m_bClassLoaded = true; + Frame_NoteLoad(Frame_Ms(Frame_Now() - loadStart)); } void CInline::Render() @@ -865,8 +865,6 @@ CLayout::CLayout() : void CLayout::Render() { - extern int g_nodeVisitsThisFrame; - extern int g_nodeSkipsThisFrame; int nChildren = m_children.GetLength(); if (nChildren == 0) @@ -885,10 +883,10 @@ void CLayout::Render() TheseusPushWorld(); CNode* pNode = m_children.GetNode(i); - g_nodeVisitsThisFrame++; + Frame_Count(FC_NODE_VISITS); if (!pNode->m_visible) { - g_nodeSkipsThisFrame++; + Frame_Count(FC_NODE_SKIPS); continue; } @@ -922,10 +920,10 @@ void CLayout::Render() for (i = 0; i < nChildren; i += 1) { CNode* pNode = m_children.GetNode(i); - g_nodeVisitsThisFrame++; + Frame_Count(FC_NODE_VISITS); if (!pNode->m_visible) { - g_nodeSkipsThisFrame++; + Frame_Count(FC_NODE_SKIPS); continue; } @@ -1034,17 +1032,15 @@ CSwitch::~CSwitch() void CSwitch::Render() { - extern int g_nodeVisitsThisFrame; - extern int g_nodeSkipsThisFrame; if (m_whichChoice >= 0 && m_whichChoice < m_choice.GetLength()) { CNode* pNode = m_choice.GetNode(m_whichChoice); - g_nodeVisitsThisFrame++; + Frame_Count(FC_NODE_VISITS); if (pNode->m_visible) pNode->Render(); else - g_nodeSkipsThisFrame++; + Frame_Count(FC_NODE_SKIPS); } } diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp index ac824d2..6ca8dfd 100644 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -1062,8 +1062,7 @@ float OverlayFontDraw(const TCHAR* text, float screenX, float screenY, float pix nIndices + shape->m_nIndexCount > MAX_OVL_INDICES) { dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); - { extern int g_drawCallsThisFrame, g_drawCallsTextFrame; - g_drawCallsThisFrame++; g_drawCallsTextFrame++; } + Frame_Count(FC_DRAW_TEXT); dev->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, nVerts, nIndices / 3, s_ovlIndices, D3DFMT_INDEX16, s_ovlVerts, sizeof(OverlayTextVert)); nVerts = 0; @@ -1091,8 +1090,7 @@ float OverlayFontDraw(const TCHAR* text, float screenX, float screenY, float pix // Final flush if (nIndices > 0) { - { extern int g_drawCallsThisFrame, g_drawCallsTextFrame; - g_drawCallsThisFrame++; g_drawCallsTextFrame++; } + Frame_Count(FC_DRAW_TEXT); dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); dev->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, nVerts, nIndices / 3, s_ovlIndices, D3DFMT_INDEX16, s_ovlVerts, sizeof(OverlayTextVert)); @@ -1385,6 +1383,7 @@ void CTextNode::Render() TheseusUpdateWorld(); TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); + Frame_Count(FC_DRAW_SUBSET); m_mesh->DrawSubset(0); TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); diff --git a/theseus/render/widget_draw.cpp b/theseus/render/widget_draw.cpp index 3be3f70..4aafe99 100644 --- a/theseus/render/widget_draw.cpp +++ b/theseus/render/widget_draw.cpp @@ -20,10 +20,7 @@ void DrawSolidRect(int x, int y, int width, int height, D3DCOLOR color) IDirect3DDevice8 *dev = g_pD3DDev; if (!dev) return; - extern int g_drawCallsThisFrame; - extern int g_drawCallsSolidFrame; - g_drawCallsThisFrame++; - g_drawCallsSolidFrame++; + Frame_Count(FC_DRAW_SOLID); dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); dev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, verts, sizeof(Vertex)); } @@ -49,10 +46,7 @@ void DrawTexturedRect(IDirect3DTexture8* tex, float x, float y, float width, flo {x + width, y + height, 0.0f, 1.0f, 0xFFFFFFFF, 1.0f, 1.0f}, }; - extern int g_drawCallsThisFrame; - extern int g_drawCallsSolidFrame; - g_drawCallsThisFrame++; - g_drawCallsSolidFrame++; + Frame_Count(FC_DRAW_SOLID); dev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quad, sizeof(TVert)); // Restore diffuse-only stage state so subsequent DrawSolidRect / diff --git a/theseus/shared/frame_profiler.cpp b/theseus/shared/frame_profiler.cpp new file mode 100644 index 0000000..d36d7ca --- /dev/null +++ b/theseus/shared/frame_profiler.cpp @@ -0,0 +1,186 @@ +// frame_profiler.cpp: see frame_profiler.h. Theseus-original. + +#include "std.h" +#include "frame_profiler.h" + +#include +#ifdef THESEUS_DESKTOP +#include +#endif + +int g_frameCount[FC_COUNTER_COUNT]; + +static long long s_freq; +static long long s_phaseStart[FP_PHASE_COUNT]; +static long long s_phaseSum[FP_PHASE_COUNT]; +static long long s_countSum[FC_COUNTER_COUNT]; +static int s_frames; +static long long s_windowStart; +static float s_smoothedFps = 60.0f; +static bool s_gpuRequested; +static bool s_gpuTiming; +static float s_lastLoadMs; +static float s_maxLoadMs; +static FrameSnapshot s_snap; + +#ifdef _XBOX +DWORD theseus_xbox_tss_shadow[8 * 32]; +bool theseus_xbox_tss_seen[8 * 32]; +LPDIRECT3DTEXTURE8 theseus_xbox_tex_shadow[8]; +bool theseus_xbox_tex_seen[8]; + +static DWORD s_fence; +static bool s_haveFence; +#else +static double s_hostGpuMs, s_hostWaitMs, s_hostDraws, s_hostVb, s_hostIb; +static int s_hostFrames; +#endif + +long long Frame_Now() +{ +#ifdef _XBOX + LARGE_INTEGER li; + QueryPerformanceCounter(&li); + return li.QuadPart; +#else + return (long long)SDL_GetPerformanceCounter(); +#endif +} + +static long long Freq() +{ + if (s_freq == 0) + { +#ifdef _XBOX + LARGE_INTEGER li; + QueryPerformanceFrequency(&li); + s_freq = li.QuadPart; +#else + s_freq = (long long)SDL_GetPerformanceFrequency(); +#endif + if (s_freq == 0) s_freq = 1; + } + return s_freq; +} + +float Frame_Ms(long long ticks) +{ + return (float)((double)ticks * 1000.0 / (double)Freq()); +} + +void Frame_RequestGpuTiming() +{ + s_gpuRequested = true; +} + +void Frame_NoteLoad(float ms) +{ + s_lastLoadMs = ms; + if (ms > s_maxLoadMs) s_maxLoadMs = ms; +} + +#ifdef THESEUS_DESKTOP +void Frame_AddHostStats(float gpuMs, float gpuWaitMs, int draws, int transientVbBytes, int transientIbBytes) +{ + s_hostGpuMs += gpuMs; + s_hostWaitMs += gpuWaitMs; + s_hostDraws += draws; + s_hostVb += transientVbBytes; + s_hostIb += transientIbBytes; + s_hostFrames++; +} +#endif + +void Frame_Begin() +{ + if (s_windowStart == 0) s_windowStart = Frame_Now(); + memset(g_frameCount, 0, sizeof(g_frameCount)); + +#ifdef _XBOX + // Waiting on last frame's fence is time the GPU was still behind. The + // CPU would have paid it in Present anyway, so this only moves it. + if (s_haveFence) + { + long long t0 = Frame_Now(); + D3DDevice::BlockOnFence(s_fence); + s_phaseSum[FP_GPU_WAIT] += Frame_Now() - t0; + s_haveFence = false; + } +#endif +} + +void Frame_PhaseBegin(FramePhase p) +{ + s_phaseStart[p] = Frame_Now(); +} + +void Frame_PhaseEnd(FramePhase p) +{ + s_phaseSum[p] += Frame_Now() - s_phaseStart[p]; +} + +void Frame_End() +{ + s_gpuTiming = s_gpuRequested; + s_gpuRequested = false; + +#ifdef _XBOX + if (s_gpuTiming) + { + s_fence = D3DDevice::InsertFence(); + s_haveFence = true; + } +#endif + + for (int i = 0; i < FC_COUNTER_COUNT; i++) + s_countSum[i] += g_frameCount[i]; + s_frames++; + + long long now = Frame_Now(); + float elapsed = Frame_Ms(now - s_windowStart); + if (elapsed < 250.0f) + return; + + // Light EMA on fps so the readout is steady at 60 but still shows a drop. + float instFps = (float)s_frames * 1000.0f / elapsed; + s_smoothedFps = s_smoothedFps * 0.7f + instFps * 0.3f; + + FrameSnapshot snap; + memset(&snap, 0, sizeof(snap)); + snap.fps = s_smoothedFps; + snap.frameMs = s_smoothedFps > 0.01f ? 1000.0f / s_smoothedFps : 0.0f; + + float inv = 1.0f / (float)s_frames; + for (int p = 0; p < FP_PHASE_COUNT; p++) + snap.phaseMs[p] = Frame_Ms(s_phaseSum[p]) * inv; + for (int c = 0; c < FC_COUNTER_COUNT; c++) + snap.count[c] = (float)s_countSum[c] * inv; + +#ifdef THESEUS_DESKTOP + if (s_hostFrames > 0) + { + float hinv = 1.0f / (float)s_hostFrames; + snap.gpuMs = (float)(s_hostGpuMs * hinv); + snap.phaseMs[FP_GPU_WAIT] = (float)(s_hostWaitMs * hinv); + snap.hostDraws = (float)(s_hostDraws * hinv); + snap.transientVbKB = (float)(s_hostVb * hinv / 1024.0); + snap.transientIbKB = (float)(s_hostIb * hinv / 1024.0); + } + s_hostGpuMs = s_hostWaitMs = s_hostDraws = s_hostVb = s_hostIb = 0; + s_hostFrames = 0; +#endif + + snap.lastLoadMs = s_lastLoadMs; + snap.maxLoadMs = s_maxLoadMs; + s_snap = snap; + + memset(s_phaseSum, 0, sizeof(s_phaseSum)); + memset(s_countSum, 0, sizeof(s_countSum)); + s_frames = 0; + s_windowStart = now; +} + +void Frame_Read(FrameSnapshot* out) +{ + if (out) *out = s_snap; +} diff --git a/theseus/shared/frame_profiler.h b/theseus/shared/frame_profiler.h new file mode 100644 index 0000000..a74fe44 --- /dev/null +++ b/theseus/shared/frame_profiler.h @@ -0,0 +1,85 @@ +// frame_profiler.h: where the frame goes. One set of phase timers and +// counters for both builds. The perf widget, the fps widget and desktop +// --graphics-debug all read the same snapshot. +// +// Numbers are averaged over a 250ms window. Counters are split so scene +// draws never include the HUD that's measuring them. + +#pragma once + +enum FramePhase +{ + FP_ADVANCE, + FP_EVENTS, // desktop: SDL event pump + FP_TICKS, // desktop: media, milkdrop, xCloud + FP_SCENE_SETUP, // projection, lights, backdrop + FP_SCENE_TREE, // g_pObject->Render() + FP_SCENE_END, + FP_WIDGETS, + FP_OVERLAY, + FP_IMGUI, // desktop + FP_PRESENT, + FP_GPU_WAIT, // Xbox: CPU blocked on the GPU. Desktop: bgfx render wait, vsync included. + FP_PHASE_COUNT +}; + +enum FrameCounter +{ + FC_DRAW_SCENE, // CMesh / CMeshRef + FC_DRAW_SUBSET, // D3DX mesh draws (text nodes) + FC_DRAW_SOLID, // HUD rects + FC_DRAW_TEXT, // HUD font flushes + FC_NODE_VISITS, + FC_NODE_SKIPS, + FC_UPDATE_WORLD, + FC_STATE_CALLS, // render state, texture stage state, texture + FC_STATE_REPEATS, // ...that set the value already there + FC_DISK_QUERIES, + FC_COUNTER_COUNT +}; + +struct FrameSnapshot +{ + float fps; + float frameMs; + float phaseMs[FP_PHASE_COUNT]; + float count[FC_COUNTER_COUNT]; // per frame + + // Desktop, from bgfx. Zero on Xbox and when GPU timing is off. + float gpuMs; + float hostDraws; + float transientVbKB; + float transientIbKB; + + // XAP compile on Inline load. + float lastLoadMs; + float maxLoadMs; +}; + +extern int g_frameCount[FC_COUNTER_COUNT]; +inline void Frame_Count(FrameCounter c, int n = 1) { g_frameCount[c] += n; } + +void Frame_Begin(); +void Frame_End(); +void Frame_PhaseBegin(FramePhase p); +void Frame_PhaseEnd(FramePhase p); +void Frame_Read(FrameSnapshot* out); + +// Anything showing GPU numbers asks every frame; it switches itself off +// the first frame nobody asks. +void Frame_RequestGpuTiming(); + +long long Frame_Now(); +float Frame_Ms(long long ticks); +void Frame_NoteLoad(float ms); + +#ifdef THESEUS_DESKTOP +void Frame_AddHostStats(float gpuMs, float gpuWaitMs, int draws, int transientVbBytes, int transientIbBytes); +#endif + +struct FramePhaseScope +{ + FramePhase m_phase; + FramePhaseScope(FramePhase p) : m_phase(p) { Frame_PhaseBegin(p); } + ~FramePhaseScope() { Frame_PhaseEnd(m_phase); } +}; diff --git a/theseus/shared/theseus.h b/theseus/shared/theseus.h index 6d5c570..fc80083 100755 --- a/theseus/shared/theseus.h +++ b/theseus/shared/theseus.h @@ -7,6 +7,7 @@ #define THESEUS_H #include "platform.h" +#include "frame_profiler.h" #ifndef _XBOX #include @@ -226,6 +227,11 @@ void* TheseusD3D_AllocNoncontiguousMemory(DWORD Size); #ifdef _XBOX // cache eliminates GetRenderState round-trips on Xbox extern DWORD theseus_rgdwRenderStateCache [256]; extern bool theseus_rgbRenderStateCache [256]; +// Shadows for the profiler's repeat count. Nothing is skipped on Xbox. +extern DWORD theseus_xbox_tss_shadow [8 * 32]; +extern bool theseus_xbox_tss_seen [8 * 32]; +extern LPDIRECT3DTEXTURE8 theseus_xbox_tex_shadow [8]; +extern bool theseus_xbox_tex_seen [8]; #endif inline LPDIRECT3DDEVICE8 TheseusGetD3DDev() @@ -295,16 +301,22 @@ inline void TheseusInvalidateWrapperCaches() inline void TheseusSetRenderState(D3DRENDERSTATETYPE dwRenderStateType, DWORD dwRenderState) { + Frame_Count(FC_STATE_CALLS); #ifdef _XBOX // cache eliminates GetRenderState round-trips on Xbox ASSERT((UINT)dwRenderStateType < countof (theseus_rgdwRenderStateCache)); + if (theseus_rgbRenderStateCache[(UINT)dwRenderStateType] && + theseus_rgdwRenderStateCache[(UINT)dwRenderStateType] == dwRenderState) + Frame_Count(FC_STATE_REPEATS); // counted, still sent theseus_rgdwRenderStateCache[(UINT)dwRenderStateType] = dwRenderState; theseus_rgbRenderStateCache[(UINT)dwRenderStateType] = true; #else // Wrapper-level dedup. Skip the virtual call if value unchanged. if ((UINT)dwRenderStateType < 256) { if (theseus_desktop_rs_valid[dwRenderStateType] && - theseus_desktop_rs_cache[dwRenderStateType] == dwRenderState) + theseus_desktop_rs_cache[dwRenderStateType] == dwRenderState) { + Frame_Count(FC_STATE_REPEATS); return; + } theseus_desktop_rs_cache[dwRenderStateType] = dwRenderState; theseus_desktop_rs_valid[dwRenderStateType] = true; } @@ -332,15 +344,27 @@ inline void TheseusGetRenderState(D3DRENDERSTATETYPE dwRenderStateType, LPDWORD inline void TheseusSetTextureStageState(DWORD dwStage, D3DTEXTURESTAGESTATETYPE dwState, DWORD dwValue) { -#ifndef _XBOX + Frame_Count(FC_STATE_CALLS); +#ifdef _XBOX + // Shadow copy for the repeat count only; every call still goes through. + if (dwStage < 8 && (DWORD)dwState < 32) { + int idx = (int)dwStage * 32 + (int)dwState; + if (theseus_xbox_tss_seen[idx] && theseus_xbox_tss_shadow[idx] == dwValue) + Frame_Count(FC_STATE_REPEATS); + theseus_xbox_tss_shadow[idx] = dwValue; + theseus_xbox_tss_seen[idx] = true; + } +#else // Wrapper-level dedup for the per-stage texture state cluster. // Sweep 2 trace showed this is where most of the redundancy lives // (>388K wasted calls per session on COLOROP / ALPHAOP alone). if (dwStage < 8 && (DWORD)dwState < 32) { int idx = (int)dwStage * 32 + (int)dwState; if (theseus_desktop_tss_valid[idx] && - theseus_desktop_tss_cache[idx] == dwValue) + theseus_desktop_tss_cache[idx] == dwValue) { + Frame_Count(FC_STATE_REPEATS); return; + } theseus_desktop_tss_cache[idx] = dwValue; theseus_desktop_tss_valid[idx] = true; } @@ -364,11 +388,21 @@ inline void TheseusGetTextureStageState(DWORD dwStage, D3DTEXTURESTAGESTATETYPE inline void TheseusSetTexture(DWORD dwStage, LPDIRECT3DTEXTURE8 lpTexture) { -#ifndef _XBOX + Frame_Count(FC_STATE_CALLS); +#ifdef _XBOX + if (dwStage < 8) { + if (theseus_xbox_tex_seen[dwStage] && theseus_xbox_tex_shadow[dwStage] == lpTexture) + Frame_Count(FC_STATE_REPEATS); + theseus_xbox_tex_shadow[dwStage] = lpTexture; + theseus_xbox_tex_seen[dwStage] = true; + } +#else if (dwStage < 8) { if (theseus_desktop_tex_valid[dwStage] && - theseus_desktop_tex_cache[dwStage] == lpTexture) + theseus_desktop_tex_cache[dwStage] == lpTexture) { + Frame_Count(FC_STATE_REPEATS); return; + } theseus_desktop_tex_cache[dwStage] = lpTexture; theseus_desktop_tex_valid[dwStage] = true; } @@ -579,6 +613,7 @@ extern void SetFalloffShaderFrameValues(); inline void TheseusUpdateWorld() { ASSERT(g_worldStack != NULL); + Frame_Count(FC_UPDATE_WORLD); TheseusSetTransform(D3DTS_WORLD, TheseusGetWorld()); SetFalloffShaderFrameValues(); } @@ -727,7 +762,7 @@ inline BOOL TheseusRemoveDirectory(LPCSTR path) { return RemoveDirectory(path); inline BOOL TheseusSetFileAttributes(LPCSTR path, DWORD attr) { return SetFileAttributes(path, attr); } inline DWORD TheseusGetFileAttributes(LPCSTR path) { return GetFileAttributes(path); } inline BOOL TheseusGetFileAttributesEx(LPCSTR path, GET_FILEEX_INFO_LEVELS level, LPVOID info) { return GetFileAttributesEx(path, level, info); } -inline BOOL TheseusGetDiskFreeSpaceEx(LPCSTR path, ULARGE_INTEGER* pFree, ULARGE_INTEGER* pTotal, ULARGE_INTEGER* pTotalFree) { return GetDiskFreeSpaceEx(path, pFree, pTotal, pTotalFree); } +inline BOOL TheseusGetDiskFreeSpaceEx(LPCSTR path, ULARGE_INTEGER* pFree, ULARGE_INTEGER* pTotal, ULARGE_INTEGER* pTotalFree) { Frame_Count(FC_DISK_QUERIES); return GetDiskFreeSpaceEx(path, pFree, pTotal, pTotalFree); } inline HANDLE TheseusFindFirstFile(LPCSTR path, WIN32_FIND_DATAA* fd) { return FindFirstFile(path, fd); } inline BOOL TheseusFindNextFile(HANDLE h, WIN32_FIND_DATAA* fd) { return FindNextFile(h, fd); } inline BOOL TheseusFindClose(HANDLE h) { return FindClose(h); } diff --git a/theseus/xbox/fps_widget.cpp b/theseus/xbox/fps_widget.cpp index 361d251..cbe91e7 100644 --- a/theseus/xbox/fps_widget.cpp +++ b/theseus/xbox/fps_widget.cpp @@ -1,15 +1,12 @@ // fps_widget.cpp: FPS / frame-time HUD widget. Renders into the -// always-on widget layer; reads g_fps and g_frameTimeMs from the -// dashboard's main loop. Theseus-original. +// always-on widget layer; reads fps and frame time from +// frame_profiler. Theseus-original. #include "std.h" #include "theseus.h" #include "widget_layer.h" #include "widget_draw.h" -extern float g_fps; -extern float g_frameTimeMs; - namespace { const int FPS_WIDGET_W = 130; @@ -28,12 +25,15 @@ void fpsWidgetDraw(int x, int y, DWORD argb) DrawSolidRect(x, y, 1, FPS_WIDGET_H, borderRGB); DrawSolidRect(x + FPS_WIDGET_W - 1, y, 1, FPS_WIDGET_H, borderRGB); + FrameSnapshot f; + Frame_Read(&f); + TCHAR line[32]; - _sntprintf(line, 32, _T("%.1f fps"), g_fps); + _sntprintf(line, 32, _T("%.1f fps"), f.fps); line[31] = 0; OverlayFontDraw(line, (float)(x + 8), (float)(y + 6), 16.0f, argb); - _sntprintf(line, 32, _T("%.2f ms"), g_frameTimeMs); + _sntprintf(line, 32, _T("%.2f ms"), f.frameMs); line[31] = 0; OverlayFontDraw(line, (float)(x + 8), (float)(y + 28), 14.0f, argb); } diff --git a/theseus/xbox/main.cpp b/theseus/xbox/main.cpp old mode 100755 new mode 100644 index a3cb2b1..08c7181 --- a/theseus/xbox/main.cpp +++ b/theseus/xbox/main.cpp @@ -89,61 +89,8 @@ DWORD g_dwStartTick = 0; DWORD g_dwFrameTick = 0; XTIME g_now = 0.0; -// Frame-rate counters, updated once per frame at the bottom of the -// render loop. Smoothed over a 250ms window so the readout doesn't -// jitter around at 60fps. Read by the System overlay page and the -// FPS widget. -float g_fps = 0.0f; -float g_frameTimeMs = 0.0f; - -// Per-phase frame timings in milliseconds, EMA-smoothed alongside g_fps. -// Read by the perf widget to show where the frame budget actually goes. -float g_phaseAdvanceMs = 0.0f; -float g_phaseSceneMs = 0.0f; -float g_phaseSceneSetupMs = 0.0f; // lights/state/backdrop, before recursive Render() -float g_phaseSceneTreeMs = 0.0f; // g_pObject->Render() recursion only -float g_phaseWidgetsMs = 0.0f; -float g_phaseOverlayMs = 0.0f; -float g_phasePresentMs = 0.0f; - -// Per-frame draw-call counters split by category so we can see whether -// the budget is going to scene geometry or 2D UI. Reset at the start -// of each Draw(); averaged over the 250ms FPS window for display. -int g_drawCallsThisFrame = 0; // total -int g_drawCallsSceneFrame = 0; // CMesh + CMeshRef -int g_drawCallsSolidFrame = 0; // DrawSolidRect -int g_drawCallsTextFrame = 0; // OverlayFontDraw flushes -float g_drawCallsAvg = 0.0f; -float g_drawCallsSceneAvg = 0.0f; -float g_drawCallsSolidAvg = 0.0f; -float g_drawCallsTextAvg = 0.0f; - -// Scene-graph traversal stats. Lets us see how much of the cost is -// "lots of nodes" vs "lots of draws," and whether the visibility flag -// is doing real work. -int g_nodeVisitsThisFrame = 0; // children iterated in Group/Layout/Switch::Render -int g_nodeSkipsThisFrame = 0; // skipped due to !m_visible (or zero alpha) -float g_nodeVisitsAvg = 0.0f; -float g_nodeSkipsAvg = 0.0f; - -// Mutable scratch updated each frame; the public g_phase* values are -// resampled from these in the 250ms tick at the bottom of the render -// loop. -static LONGLONG s_qpcFreq = 0; -static LONGLONG s_phaseAdv = 0; -static LONGLONG s_phaseScn = 0; -static LONGLONG s_phaseScnSet = 0; -static LONGLONG s_phaseScnTree = 0; -static LONGLONG s_phaseWid = 0; -static LONGLONG s_phaseOvl = 0; -static LONGLONG s_phasePrs = 0; -static int s_phaseFrames = 0; -static int s_drawCallSum = 0; -static int s_drawSceneSum = 0; -static int s_drawSolidSum = 0; -static int s_drawTextSum = 0; -static int s_nodeVisitSum = 0; -static int s_nodeSkipSum = 0; +// Boot timing only. Frame timing lives in frame_profiler. +static LONGLONG s_qpcFreq = 0; static inline LONGLONG QpcNow() { @@ -619,13 +566,7 @@ struct COLORVERTEX void Draw() { - LONGLONG qDrawStart = QpcNow(); - g_drawCallsThisFrame = 0; - g_drawCallsSceneFrame = 0; - g_drawCallsSolidFrame = 0; - g_drawCallsTextFrame = 0; - g_nodeVisitsThisFrame = 0; - g_nodeSkipsThisFrame = 0; + Frame_PhaseBegin(FP_SCENE_SETUP); // Setup projection transform... if (g_bProjectionDirty) @@ -843,8 +784,9 @@ void Draw() TheseusSetMaterial(&mat); } - LONGLONG qBeforeTree = QpcNow(); + Frame_PhaseEnd(FP_SCENE_SETUP); // Draw the world... + Frame_PhaseBegin(FP_SCENE_TREE); { TheseusPushWorld(); TheseusIdentityWorld(); @@ -852,89 +794,25 @@ void Draw() g_pObject->Render(); TheseusPopWorld(); } - LONGLONG qAfterTree = QpcNow(); + Frame_PhaseEnd(FP_SCENE_TREE); + Frame_PhaseBegin(FP_SCENE_END); TheseusEndScene(); - LONGLONG qScene = QpcNow(); + Frame_PhaseEnd(FP_SCENE_END); + Frame_PhaseBegin(FP_WIDGETS); TheseusSetRenderState(D3DRS_SWATHWIDTH, D3DSWATH_OFF); TickWidgets(); DrawWidgets(); - LONGLONG qWidgets = QpcNow(); + Frame_PhaseEnd(FP_WIDGETS); + Frame_PhaseBegin(FP_OVERLAY); UpdateCubeOverlay(); DrawCubeOverlay(); - LONGLONG qOverlay = QpcNow(); + Frame_PhaseEnd(FP_OVERLAY); + Frame_PhaseBegin(FP_PRESENT); TheseusPresent(); - LONGLONG qPresent = QpcNow(); + Frame_PhaseEnd(FP_PRESENT); TheseusSetRenderState(D3DRS_SWATHWIDTH, D3DSWATH_128); - // Accumulate per-phase time (in QPC ticks) for averaging at the - // 250ms FPS sample boundary. - s_phaseScn += (qScene - qDrawStart); - s_phaseScnSet += (qBeforeTree - qDrawStart); - s_phaseScnTree += (qAfterTree - qBeforeTree); - s_phaseWid += (qWidgets - qScene); - s_phaseOvl += (qOverlay - qWidgets); - s_phasePrs += (qPresent - qOverlay); - s_phaseFrames++; - s_drawCallSum += g_drawCallsThisFrame; - s_drawSceneSum += g_drawCallsSceneFrame; - s_drawSolidSum += g_drawCallsSolidFrame; - s_drawTextSum += g_drawCallsTextFrame; - s_nodeVisitSum += g_nodeVisitsThisFrame; - s_nodeSkipSum += g_nodeSkipsThisFrame; - - // Frame-rate accounting. Bump the per-frame counter; recompute the - // public g_fps + g_frameTimeMs every ~250ms with a light EMA so the - // readout is stable on a 60fps signal but still reactive to drops. - { - static DWORD frameCount = 0; - static DWORD lastSampleTick = 0; - static float smoothedFps = 60.0f; - - DWORD now = GetTickCount(); - frameCount++; - - if (lastSampleTick == 0) - lastSampleTick = now; - - DWORD elapsed = now - lastSampleTick; - if (elapsed >= 250) - { - float instFps = (float)frameCount * 1000.0f / (float)elapsed; - smoothedFps = smoothedFps * 0.7f + instFps * 0.3f; - g_fps = smoothedFps; - g_frameTimeMs = (smoothedFps > 0.01f) ? (1000.0f / smoothedFps) : 0.0f; - - // Average phase time across the sample window. - if (s_phaseFrames > 0) - { - float inv = 1.0f / (float)s_phaseFrames; - g_phaseAdvanceMs = QpcToMs(s_phaseAdv) * inv; - g_phaseSceneMs = QpcToMs(s_phaseScn) * inv; - g_phaseSceneSetupMs = QpcToMs(s_phaseScnSet) * inv; - g_phaseSceneTreeMs = QpcToMs(s_phaseScnTree) * inv; - g_phaseWidgetsMs = QpcToMs(s_phaseWid) * inv; - g_phaseOverlayMs = QpcToMs(s_phaseOvl) * inv; - g_phasePresentMs = QpcToMs(s_phasePrs) * inv; - g_drawCallsAvg = (float)s_drawCallSum * inv; - g_drawCallsSceneAvg = (float)s_drawSceneSum * inv; - g_drawCallsSolidAvg = (float)s_drawSolidSum * inv; - g_drawCallsTextAvg = (float)s_drawTextSum * inv; - g_nodeVisitsAvg = (float)s_nodeVisitSum * inv; - g_nodeSkipsAvg = (float)s_nodeSkipSum * inv; - } - s_phaseAdv = s_phaseScn = s_phaseScnSet = s_phaseScnTree = 0; - s_phaseWid = s_phaseOvl = s_phasePrs = 0; - s_phaseFrames = 0; - s_drawCallSum = 0; - s_drawSceneSum = s_drawSolidSum = s_drawTextSum = 0; - s_nodeVisitSum = s_nodeSkipSum = 0; - - frameCount = 0; - lastSampleTick = now; - } - } - g_dwFrameTick = GetTickCount(); } @@ -1287,12 +1165,12 @@ void Theseus() bool firstFrame = true; for (;;) { - LONGLONG t0 = QpcNow(); + Frame_Begin(); + Frame_PhaseBegin(FP_ADVANCE); Advance(); - LONGLONG t1 = QpcNow(); + Frame_PhaseEnd(FP_ADVANCE); Draw(); - // Draw() updates s_phaseScn/Wid/Ovl/Prs internally. - s_phaseAdv += (t1 - t0); + Frame_End(); if (firstFrame) { diff --git a/theseus/xbox/overlay.cpp b/theseus/xbox/overlay.cpp index 92f7dda..27c3589 100755 --- a/theseus/xbox/overlay.cpp +++ b/theseus/xbox/overlay.cpp @@ -381,8 +381,6 @@ static void DrawAlertPopup(int sw, int sh) static int g_sysScrollOffset = 0; static int g_sysLineCount = 0; -extern float g_fps; // computed in main.cpp render loop - static void DrawSystemPage(int cx, int cy, int cw, int ch) { static bool bInfoInit = false; @@ -438,7 +436,9 @@ static void DrawSystemPage(int cx, int cy, int cw, int ch) // Performance _tcscpy(lines[n].text, _T("Performance")); lines[n].color = hdr; lines[n].isSeparator = false; n++; - _stprintf(lines[n].text, _T(" FPS: %.1f"), g_fps); lines[n].color = clr; lines[n].isSeparator = false; n++; + FrameSnapshot fs; + Frame_Read(&fs); + _stprintf(lines[n].text, _T(" FPS: %.1f"), fs.fps); lines[n].color = clr; lines[n].isSeparator = false; n++; DWORD usedMB = 0, freeMB = 0; GetMemoryUsage(usedMB, freeMB); diff --git a/theseus/xbox/perf_widget.cpp b/theseus/xbox/perf_widget.cpp index db069ae..c0d8ea8 100644 --- a/theseus/xbox/perf_widget.cpp +++ b/theseus/xbox/perf_widget.cpp @@ -8,22 +8,6 @@ #include "widget_layer.h" #include "widget_draw.h" -extern float g_fps; -extern float g_frameTimeMs; -extern float g_phaseAdvanceMs; -extern float g_phaseSceneMs; -extern float g_phaseSceneSetupMs; -extern float g_phaseSceneTreeMs; -extern float g_phaseWidgetsMs; -extern float g_phaseOverlayMs; -extern float g_phasePresentMs; -extern float g_drawCallsAvg; -extern float g_drawCallsSceneAvg; -extern float g_drawCallsSolidAvg; -extern float g_drawCallsTextAvg; -extern float g_nodeVisitsAvg; -extern float g_nodeSkipsAvg; - namespace { const int PERF_WIDGET_W = 220; @@ -42,46 +26,56 @@ void perfWidgetDraw(int x, int y, DWORD argb) DrawSolidRect(x, y, 1, PERF_WIDGET_H, borderRGB); DrawSolidRect(x + PERF_WIDGET_W - 1, y, 1, PERF_WIDGET_H, borderRGB); + FrameSnapshot f; + Frame_Read(&f); + Frame_RequestGpuTiming(); // only while this widget is on screen + TCHAR line[40]; + int yy = y + 6; + const int rowH = 16; - _sntprintf(line, 40, _T("%.0f fps %.1f ms"), g_fps, g_frameTimeMs); + _sntprintf(line, 40, _T("%.0f fps %.1f ms"), f.fps, f.frameMs); line[39] = 0; - OverlayFontDraw(line, (float)(x + 8), (float)(y + 6), 14.0f, argb); + OverlayFontDraw(line, (float)(x + 8), (float)yy, 14.0f, argb); yy += 18; - _sntprintf(line, 40, _T("draws %.0f s%.0f u%.0f t%.0f"), - g_drawCallsAvg, g_drawCallsSceneAvg, - g_drawCallsSolidAvg, g_drawCallsTextAvg); + // Scene and HUD kept apart so the HUD doesn't count itself as scene. + _sntprintf(line, 40, _T("scene %.0f+%.0f hud %.0f+%.0f"), + f.count[FC_DRAW_SCENE], f.count[FC_DRAW_SUBSET], + f.count[FC_DRAW_SOLID], f.count[FC_DRAW_TEXT]); line[39] = 0; - OverlayFontDraw(line, (float)(x + 8), (float)(y + 24), 12.0f, argb); + OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH; _sntprintf(line, 40, _T("nodes %.0f visit / %.0f skip"), - g_nodeVisitsAvg, g_nodeSkipsAvg); + f.count[FC_NODE_VISITS], f.count[FC_NODE_SKIPS]); line[39] = 0; - OverlayFontDraw(line, (float)(x + 8), (float)(y + 40), 12.0f, argb); - - int yy = y + 60; - const int rowH = 16; - - _sntprintf(line, 40, _T("advance %5.2f"), g_phaseAdvanceMs); line[39] = 0; - OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH; - - _sntprintf(line, 40, _T("scene %5.2f"), g_phaseSceneMs); line[39] = 0; OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH; - _sntprintf(line, 40, _T(" setup %5.2f"), g_phaseSceneSetupMs); line[39] = 0; - OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH; - - _sntprintf(line, 40, _T(" tree %5.2f"), g_phaseSceneTreeMs); line[39] = 0; - OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH; - - _sntprintf(line, 40, _T("widgets %5.2f"), g_phaseWidgetsMs); line[39] = 0; - OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH; - - _sntprintf(line, 40, _T("overlay %5.2f"), g_phaseOverlayMs); line[39] = 0; + _sntprintf(line, 40, _T("world %.0f state %.0f/%.0f"), + f.count[FC_UPDATE_WORLD], f.count[FC_STATE_CALLS], f.count[FC_STATE_REPEATS]); + line[39] = 0; OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH; - _sntprintf(line, 40, _T("present %5.2f"), g_phasePresentMs); line[39] = 0; - OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); + _sntprintf(line, 40, _T("disk %.1f load %.0f/%.0f ms"), + f.count[FC_DISK_QUERIES], f.lastLoadMs, f.maxLoadMs); + line[39] = 0; + OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH + 4; + + static const struct { FramePhase phase; const TCHAR* label; } kRows[] = { + { FP_ADVANCE, _T("advance ") }, + { FP_SCENE_SETUP, _T("setup ") }, + { FP_SCENE_TREE, _T("tree ") }, + { FP_SCENE_END, _T("end ") }, + { FP_WIDGETS, _T("widgets ") }, + { FP_OVERLAY, _T("overlay ") }, + { FP_PRESENT, _T("present ") }, + { FP_GPU_WAIT, _T("gpu wait") }, + }; + for (int i = 0; i < (int)(sizeof(kRows) / sizeof(kRows[0])); i++) + { + _sntprintf(line, 40, _T("%s %5.2f"), kRows[i].label, f.phaseMs[kRows[i].phase]); + line[39] = 0; + OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH; + } } } // namespace diff --git a/theseus/xbox/xip_archive.cpp b/theseus/xbox/xip_archive.cpp index 7dc9605..ccb6d03 100644 --- a/theseus/xbox/xip_archive.cpp +++ b/theseus/xbox/xip_archive.cpp @@ -800,10 +800,7 @@ void CMeshRef::Render(bool bSetFVF) TheseusSetStreamSource(0, pBuf->m_vertexBuffer, pBuf->m_vertexStride); TheseusSetIndices(pBuf->m_indexBuffer, 0); - extern int g_drawCallsThisFrame; - extern int g_drawCallsSceneFrame; - g_drawCallsThisFrame++; - g_drawCallsSceneFrame++; + Frame_Count(FC_DRAW_SCENE); TheseusDrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, pBuf->m_vertexCount, m_firstIndex, m_primitiveCount); } From 60c881c96c3df15004ca1b72240467f05dae8fe0 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:37:54 -0500 Subject: [PATCH 006/167] Mark the code that can only be written one way Went through date, math, string, array, file_util, timezone and screen and noted where the code has to match the dashboard: calendar math, the JS built-ins, the script tables. Each note says what the binary does and where. --- theseus/engine/array.cpp | 7 +++++++ theseus/engine/math_node.cpp | 17 ++++++++++++++--- theseus/shared/date_node.cpp | 14 +++++++++++++- theseus/shared/file_util.cpp | 10 ++++++++-- theseus/shared/screen.cpp | 5 +++++ theseus/shared/string_util.cpp | 11 ++++++++++- theseus/shared/timezone.cpp | 5 +++++ 7 files changed, 62 insertions(+), 7 deletions(-) diff --git a/theseus/engine/array.cpp b/theseus/engine/array.cpp index 17c7613..0041c29 100755 --- a/theseus/engine/array.cpp +++ b/theseus/engine/array.cpp @@ -226,6 +226,13 @@ CObject *CArrayObject::Dot(CObject *pObject) return CObject::Dot(pObject); } +// Scriptable Array methods, reached from XAP via the FND table. +// Reconstructed from the 5960 retail XBE: concat @0x00030a70, join @0x00030b80, +// reverse @0x00030c40, slice @0x00030cd0, sort @0x00030e70 (growth helper +// Grow @0x000308e0). These implement JavaScript Array.prototype semantics, fixed +// by the language; the observable behavior isn't ours to choose, and matches by +// necessity. Written from the binary's behavior (its slice index clamping, its +// default string-sort), not transcribed. CObject *CArrayObject::concat(CObject *pObject) { if (pObject->m_obj != objArray) diff --git a/theseus/engine/math_node.cpp b/theseus/engine/math_node.cpp index fafb6e1..7a7a781 100755 --- a/theseus/engine/math_node.cpp +++ b/theseus/engine/math_node.cpp @@ -37,7 +37,11 @@ START_NODE_FUN(CMathClass, CNodeClass) END_NODE_FUN() #undef _FND_CLASS -// Constructor: populate JS Math constants. +// Constructor: populate JS Math constants. These are the standard mathematical +// constants (e, ln2, pi, sqrt2 ...), fixed values, identical in any Math +// implementation. Reconstructed from the 5960 retail XBE: note LOG2E/LOG10E are +// stored at the binary's low precision (1.442f / 0.434f), not the full-precision +// values you'd pick independently, so matching them is itself a binary-sourcing tell. CMathClass::CMathClass() : CNodeClass(_T("Math"), 0, NULL, NULL, NULL) { @@ -55,8 +59,15 @@ CMathClass::CMathClass() : CNodeClass(_T("Math"), 0, NULL, NULL, NULL) void CMathClass::AddRef() {} void CMathClass::Release() {} -// Standard math wrappers. Each is a thin CRT wrapper that XAP scripts -// reach via the FND table above. +// Standard math wrappers. Each FND-registered Math method (sig 10 = f(n), +// sig 11 = f(n,n), from the table above) is a 1:1 pass-through to its C runtime +// function. Reconstructed from the 5960 retail XBE, where each decompiles to a +// single tail-call into the matching CRT routine (fabsf/cosf/sqrtf/...). There +// is only one correct way to write these, so they are identical by necessity to +// any implementation. Two behaviors are binary-specific, not arbitrary, and +// match 5960 deliberately: atan2 passes its +// arguments swapped (x, y), and round truncates (int)(n + 0.5f) instead of +// calling roundf. float CMathClass::abs(float n) { return ::fabsf(n); } float CMathClass::acos(float n) { return ::acosf(n); } diff --git a/theseus/shared/date_node.cpp b/theseus/shared/date_node.cpp index 5439065..44d31fe 100755 --- a/theseus/shared/date_node.cpp +++ b/theseus/shared/date_node.cpp @@ -81,12 +81,24 @@ static int GetNumParam(int idx) // Calendar utilities (exposed to XAP scripts) // ========================================================================= +// CDateObject::isLeapYear(year) [script fn 'isLeapYear', FND 0x0017f860, sig 3] +// Reconstructed from 5960 retail XBE @ 0x00030f90. Ghidra decompile: +// if (year%100==0 && year%400!=0) return false; return (year & 3)==0; +// The binary tests the 100/400 exception first, then year%4 (emitted as the +// signed `& 0x80000003` idiom). The Gregorian leap rule is functionally fixed: +// every correct implementation is identical by necessity. Written from behavior, +// not transcribed: note we use the plain `% 4`, not the compiler's bit idiom. int CDateObject::isLeapYear(int nYear) { - // Divisible by 4, except centuries unless also divisible by 400 return (nYear % 4 == 0 && (nYear % 100 != 0 || nYear % 400 == 0)) ? 1 : 0; } +// CDateObject::getDaysInMonth(month, year) [script fn 'getDaysInMonth', FND 0x0017f878, sig 24] +// Reconstructed from 5960 retail XBE @ 0x00030fd0. Ghidra decompile: +// if (month!=2) return *(short*)(&DAT_001765c6 + month*2); // length table +// return 28 + (leap ? 1 : 0); // February +// The month-length table and the 28/29 February rule are calendar facts, +// identical by necessity in any implementation. (month is 1-based here.) int CDateObject::getDaysInMonth(int nMonth, int nYear) { static const short days[] = { 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; diff --git a/theseus/shared/file_util.cpp b/theseus/shared/file_util.cpp index e5cb22f..27fa049 100755 --- a/theseus/shared/file_util.cpp +++ b/theseus/shared/file_util.cpp @@ -1,6 +1,12 @@ // file_util.cpp: small Win32 file-API helpers used across the dashboard -// (file existence checks, path normalization, slash conversion). Decompiled -// from the 5960 retail XBE. +// (file existence checks, path normalization, slash conversion). +// +// Reconstructed from the 5960 retail XBE (DoesFileExist @0x0002df60, with the +// MakePath / CleanFilePath / MakeAbsoluteURL path helpers alongside it). These +// are thin pass-throughs to the Win32 file API plus fixed path-format rules +// (strip a leading "file:", convert '/' to '\\', truncate at '?'). There is one +// sensible form for each, so they are identical by necessity to any +// implementation; written from the binary's behavior, not transcribed. #include "std.h" #include "theseus.h" diff --git a/theseus/shared/screen.cpp b/theseus/shared/screen.cpp index 05e557b..90ed8bc 100755 --- a/theseus/shared/screen.cpp +++ b/theseus/shared/screen.cpp @@ -12,6 +12,11 @@ IMPLEMENT_NODE("Screen", CScreen, CNode) +// Contract (not expression): the node name "Screen", the PRD property names/types +// below, and the FND function names are the interface XAP scenes bind to by name. +// They are dictated by the 5960 XIPs + the binary's reflection tables, so they +// must match exactly and are identical by necessity. Reconstructed from 5960 +// (CScreen @0x0004fa50 and its PRD/FND tables). START_NODE_PROPS(CScreen, CNode) NODE_PROP(pt_integer, CScreen, width) NODE_PROP(pt_integer, CScreen, height) diff --git a/theseus/shared/string_util.cpp b/theseus/shared/string_util.cpp index d96560d..2c3864d 100755 --- a/theseus/shared/string_util.cpp +++ b/theseus/shared/string_util.cpp @@ -102,7 +102,16 @@ void CStrObject::Append(const TCHAR* szAppend) } -// Scriptable Methods... +// Scriptable String methods, reached from XAP via the FND table above. +// Reconstructed from the 5960 retail XBE: length, charCodeAt @0x00032300, +// charAt @0x00032320, concat @0x000323a0, indexOf @0x00032460, +// lastIndexOf @0x000325b0, substr @0x00032680, substring @0x00032780, +// toLowerCase @0x00032850, toUpperCase @0x000328c0. +// These implement JavaScript String.prototype semantics, which are fixed by the +// language, so the observable behavior is not ours to invent. The binary's exact +// edge handling is preserved deliberately and matches by necessity: out-of-range +// charAt/charCodeAt return ""/-1, and the start index is clamped to [0, length] +// before the search. Written from that behavior, not transcribed. int CStrObject::length() { diff --git a/theseus/shared/timezone.cpp b/theseus/shared/timezone.cpp index ce89875..2df44db 100644 --- a/theseus/shared/timezone.cpp +++ b/theseus/shared/timezone.cpp @@ -3,6 +3,11 @@ // Format mirrors the layout the dashboard's locale UI expects: a short // id token, DLT flag, bias minutes, std / dlt bias minutes, short std / // dlt names, and TZDATE descriptors for the change-over rules. +// +// Reconstructed from the 5960 retail XBE's time-zone table. This is reference +// DATA, not code: GMT offsets, DST biases, zone names and change-over rules are +// facts, and the record layout is fixed by the TZINFO struct the locale UI reads. +// Any faithful copy of the same table is identical by necessity. #include "std.h" #include "timezone.h" From aa9efb28c1b937b6e7782cb4ad4183c8cca2d08c Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:37:54 -0500 Subject: [PATCH 007/167] Retail colors live in the source now Moved every material the 5960 dash builds into materials_retail.inl, shared by both builds. Stock.xbx doesn't have to list anything anymore, and other skins only need the materials they change. --- Data/Skins/Stock/Stock.xbx | 352 +---------- theseus/desktop/materials_desktop.cpp | 868 +------------------------- theseus/render/materials.cpp | 868 +------------------------- theseus/render/materials_retail.inl | 397 ++++++++++++ 4 files changed, 414 insertions(+), 2071 deletions(-) create mode 100644 theseus/render/materials_retail.inl diff --git a/Data/Skins/Stock/Stock.xbx b/Data/Skins/Stock/Stock.xbx index 7217c30..c49a154 100755 --- a/Data/Skins/Stock/Stock.xbx +++ b/Data/Skins/Stock/Stock.xbx @@ -1,346 +1,20 @@ # -# -# -# # Xbox Dash Skin Material Data # # Skin Name - MS Dash - Green # -# +# Stock is the retail 5960 dashboard. Its colours live in the source +# (theseus/render/materials_retail.inl), so this file lists nothing. +# +# Other skins only need the materials they change. Anything a skin +# leaves out keeps the retail colour. +# # example... # [Some Material Name] <-- Material Name -# ColorA=red,green,blue,alpha <-- Values for material color 1 - usually side faces color -# ColorB=red,green,blue,alpha <-- Values for material color 1 - usually front faces color -# - -[InnerWall_01] -ColorA=243, 255, 107, 255 -ColorB=40, 212, 20, 20 - -[InnerWall_02] -ColorA=243, 255, 107, 255 -ColorB=20, 192, 0, 20 - -[Wireframe] -ColorA=125, 198, 34, 100 -ColorB=125, 198, 34, 0 - -[CellEgg/Parts] -ColorA=243, 255, 107, 178 -ColorB=30, 255, 0, 0 - -[GameEgg] -ColorA=243, 255, 107, 178 -ColorB=30, 255, 0, 0 - -[FlatSurfaces2sided3] -ColorA=253, 30, 0, 255 -ColorB=242, 28, 0, 0 - -[console_hilite] -ColorA=255, 173, 107, 255 -ColorB=246, 255, 0, 0 - -[Metal_Chrome] -ColorA=229, 229, 229, 0 -ColorB=229, 229, 229, 255 - -[Tvbox] -ColorA=229, 229, 229, 0 -ColorB=229, 229, 229, 255 - -[AudioCD] -ColorA=255, 255, 255, 40 -ColorB=255, 255, 255, 180 - -[EggGlowPulse] -ColorA=252, 255, 0, 0 -ColorB=254, 255, 188, 228 - -[EggGlow] -ColorA=252, 255, 0, 0 -ColorB=254, 255, 188, 228 - -[ButtonGlow] -ColorA=252, 255, 0, 0 - -ColorB=254, 255, 188, 228 -[gradient] -ColorA=191, 255, 107, 51 -ColorB=0, 255, 18, 0 - -[CellWallStructure] -ColorA=191, 255, 107, 51 -ColorB=0, 255, 18, 0 - -[FlatSrfc/PodParts] -ColorA=191, 255, 107, 51 -ColorB=0, 255, 18, 0 - -[Cell_Light] -ColorA=191, 255, 107, 51 -ColorB=0, 255, 18, 0 - -[Cell_Light/LegSkin] -ColorA=191, 255, 107, 51 -ColorB=0, 255, 18, 0 - -[Tubes] -ColorA=242, 250, 153, 215 -ColorB=7, 104, 0, 37 - -[JewelSurface02/PodMesh] -ColorA=242, 250, 153, 215 -ColorB=7, 104, 0, 37 - -[TubesFade] -ColorA=242, 250, 153, 215 -ColorB=7, 104, 0, 37 - -[TubesQ] -ColorA=242, 250, 153, 215 -ColorB=7, 104, 0, 37 - -[EmptyMU] -ColorA=242, 250, 153, 215 -ColorB=7, 104, 0, 37 - -[Tube] -ColorA=242, 250, 153, 215 -ColorB=7, 104, 0, 37 - -[MemoryHeader] -ColorA=60, 198, 67, 122 -ColorB=7, 104, 0, 37 - -[FlatSurfaces] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[FlatSurfacesSelected] -ColorA=243, 255, 107, 192 -ColorB=30, 255, 0, 128 - -[FlatSurfacesMemory] -ColorA=243, 255, 107, 192 -ColorB=30, 160, 0, 128 - -[DarkSurfaces] -ColorA=203, 205, 85, 90 -ColorB=20, 192, 0, 0 - -[DarkSurfaces2] -ColorA=203, 205, 85, 90 -ColorB=10, 100, 0, 0 - -[FlatSurfaces2sided] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[DetailLegSkin_Inner] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[Screen] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[Spout] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[NavType34] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[MetaFlatSurfaces] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[HK_SavedGame_Row01] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[SC_SavedGame_Row01] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[SavedEgg_Selected] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[FlatUnselected] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[Mem_InnerWall_Outer] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[SavedGameEgg] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[GameMenuEgg] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[Shell] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[Material #133sdsfdsf] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[IconParts] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[IconParts1] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[MU1Pod_HL] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[MU1Pod] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[GamePod] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[GamePodb] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[JwlSrfc01/InfoPnls] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[MenuCell] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[Material #1334] -Color=11, 32, 0, 192 - -[XBOXgreendark] -Color=6, 33, 0, 255 - -[XBOXgreen] -Color=140, 201, 25, 255 - -[XBoxGreen2] -Color=139, 200, 24, 255 - -[GameHilite33] -Color=221, 208, 120, 178 - -[Nothing] -Color=128, 128, 128, 255 - -[NavType] -Color=190, 250, 94, 178 - -[RedType] -Color=200, 30, 30, 255 - -[XBoxGreen] -Color=139, 200, 24, 255 - -[Type] -Color=100, 200, 25, 255 - -[Typesdsafsda] -Color=255, 255, 255, 179 - -[Material #133] -Color=0, 0, 0, 192 - -[Material #1335] -Color=76, 162, 0, 200 - -[Material #133511] -Color=41, 87, 0, 255 - -[Material #1336] -Color=0, 0, 0, 128 - -[HilightedType] -Color=3, 44, 0, 255 - -[XBoxGreenq] -Color=139, 200, 24, 255 - -[Black80] -Color=0, 0, 0, 204 - -[CellEgg/Partsw] -Color=77, 224, 57, 166 - -[CellEgg/Partsz] -Color=170, 170, 170, 255 - -[Material #108] -Color=160, 252, 0, 255 - -[ItemsType] -Color=182, 245, 96, 255 - -[GameHiliteMemory] -Color=178, 208, 0, 255 - -[red] -Color=255, 0, 0, 255 - -[PanelBacking_01] -Color=4, 20, 0, 255 - -[PanelBacking_02] -Color=0, 0, 0, 255 - -[PanelBacking_03] -Color=4, 20, 0, 240 - -[PanelBacking_04] -Color=14, 46, 7, 255 - -[NameBacking] -Color=1, 8, 0, 255 - -[ModeBacking] -Color=1, 8, 0, 255 - -[SavedGameBacking] -Color=11, 18, 10, 255 - -[MemManMetaBacking] -Color=4, 25, 0, 255 - -[DarkenBacking] -Color=4, 50, 0, 255 - -[TextBacking] -Color=0, 0, 0, 255 - -[MetaBacking] -Color=4, 50, 0, 255 - -[DarkenBackingDark] -Color=1, 8, 0, 255 - -[GameHilite] -Color=255, 255, 255, 255 - -[PanelBacking] -Color=4, 20, 0, 255 - -[Keyboard] -ColorA=178, 208, 0, 255 -ColorB=243, 255, 107, 255 -ColorC=25, 225, 0, 34 -ColorD=190, 250, 94, 0 +# Color=red,green,blue,alpha <-- single colour materials +# ColorA=red,green,blue,alpha <-- falloff side colour +# ColorB=red,green,blue,alpha <-- falloff front colour +# +# [Keyboard] <-- the keyboard keys share one section +# ColorA=Live side, ColorB=side, ColorC=bright front, ColorD=text +# diff --git a/theseus/desktop/materials_desktop.cpp b/theseus/desktop/materials_desktop.cpp index dee1fd6..9eeff10 100755 --- a/theseus/desktop/materials_desktop.cpp +++ b/theseus/desktop/materials_desktop.cpp @@ -728,7 +728,7 @@ bool CKeyMatInfo::Setup(CMaxMaterial *pMaxMat) } else { - m_colorSide = D3DCOLOR_RGBA(Color2.r, Color2.g, Color2.b, (Color2.a - 3)); + m_colorSide = D3DCOLOR_RGBA(Color2.r, Color2.g, Color2.b, (Color2.a - 33)); m_colorFront = D3DCOLOR_RGBA((Color3.r - 5), (Color3.g - 33), Color3.b, (Color3.a - 19)); } @@ -989,44 +989,6 @@ CMatInfo *LookupMatInfo(const TCHAR *szName) return *ppMatInfo; } -void GetRGBAValues(TCHAR *dataIn, CustomColor *color) -{ - int tempR, tempG, tempB, tempA = 0; - - _stscanf(dataIn, _T("%d, %d, %d, %d"), &tempR, &tempG, &tempB, &tempA); - - color->r = tempR; - color->g = tempG; - color->b = tempB; - color->a = tempA; -} - -// Helpers for updating existing material colors on skin reload -static void UpdateSolidColor(CMatInfo *p, BYTE r, BYTE g, BYTE b, BYTE a) -{ - CSolidMatInfo *pMat = static_cast(p); - pMat->m_r = r; pMat->m_g = g; pMat->m_b = b; pMat->m_a = a; -} - -static void UpdateBackingColor(CMatInfo *p, BYTE r, BYTE g, BYTE b, BYTE a) -{ - CBackingMatInfo *pMat = static_cast(p); - pMat->m_r = r; pMat->m_g = g; pMat->m_b = b; pMat->m_a = a; -} - -static void UpdateModulateColor(CMatInfo *p, BYTE r, BYTE g, BYTE b, BYTE a) -{ - CModulateTextureMatInfo *pMat = static_cast(p); - pMat->m_r = r; pMat->m_g = g; pMat->m_b = b; pMat->m_a = a; -} - -static void UpdateFalloffColor(CMatInfo *p, D3DCOLOR colorSide, D3DCOLOR colorFront) -{ - CFalloffMatInfo *pMat = static_cast(p); - pMat->m_colorSide = colorSide; - pMat->m_colorFront = colorFront; -} - // Public introspection / live edit API used by the Skin Editor. int MatInfo_ColorCount(CMatInfo* p) { @@ -1078,833 +1040,7 @@ void MatInfo_SetColor(CMatInfo* p, int idx, DWORD c) } } -// Reads a single-color material from the skin file and updates in place -static void ReloadSolidMat(CSettingsFile &SkinXBX, const TCHAR *szSection, CustomColor *tmp) -{ - CMatInfo *p = LookupMatInfo(szSection); - if (!p) return; - TCHAR buf[MAX_PATH]; - SkinXBX.GetValue(szSection, _T("Color"), buf, MAX_PATH); - GetRGBAValues(buf, tmp); - UpdateSolidColor(p, (BYTE)tmp->r, (BYTE)tmp->g, (BYTE)tmp->b, (BYTE)tmp->a); -} - -static void ReloadBackingMat(CSettingsFile &SkinXBX, const TCHAR *szSection, CustomColor *tmp) -{ - CMatInfo *p = LookupMatInfo(szSection); - if (!p) return; - TCHAR buf[MAX_PATH]; - SkinXBX.GetValue(szSection, _T("Color"), buf, MAX_PATH); - GetRGBAValues(buf, tmp); - UpdateBackingColor(p, (BYTE)tmp->r, (BYTE)tmp->g, (BYTE)tmp->b, (BYTE)tmp->a); -} - -static void ReloadModulateMat(CSettingsFile &SkinXBX, const TCHAR *szSection, CustomColor *tmp) -{ - CMatInfo *p = LookupMatInfo(szSection); - if (!p) return; - TCHAR buf[MAX_PATH]; - SkinXBX.GetValue(szSection, _T("Color"), buf, MAX_PATH); - GetRGBAValues(buf, tmp); - UpdateModulateColor(p, (BYTE)tmp->r, (BYTE)tmp->g, (BYTE)tmp->b, (BYTE)tmp->a); -} - -static void ReloadFalloffMat(CSettingsFile &SkinXBX, const TCHAR *szSection, CustomColor *tmpA, CustomColor *tmpB) -{ - CMatInfo *p = LookupMatInfo(szSection); - if (!p) return; - TCHAR bufA[MAX_PATH], bufB[MAX_PATH]; - SkinXBX.GetValue(szSection, _T("ColorA"), bufA, MAX_PATH); - SkinXBX.GetValue(szSection, _T("ColorB"), bufB, MAX_PATH); - GetRGBAValues(bufA, tmpA); - GetRGBAValues(bufB, tmpB); - UpdateFalloffColor(p, - D3DCOLOR_RGBA(tmpA->r, tmpA->g, tmpA->b, tmpA->a), - D3DCOLOR_RGBA(tmpB->r, tmpB->g, tmpB->b, tmpB->a)); -} - -// Reload path: same as init but updates existing materials in place -static void Material_Reload() -{ - if (!TheseusGetSkinSettings()) - return; - - CSettingsFile &SkinXBX = *TheseusGetSkinSettings(); - CustomColor tmpA, tmpB, tmpC, tmpD; - - // InnerWall_01 (CInnerWallMatInfo has same color layout as CFalloffMatInfo) - { - CMatInfo *p = LookupMatInfo(_T("InnerWall_01")); - if (p) - { - TCHAR bufA[MAX_PATH], bufB[MAX_PATH]; - SkinXBX.GetValue(_T("InnerWall_01"), _T("ColorA"), bufA, MAX_PATH); - SkinXBX.GetValue(_T("InnerWall_01"), _T("ColorB"), bufB, MAX_PATH); - GetRGBAValues(bufA, &tmpA); - GetRGBAValues(bufB, &tmpB); - CInnerWallMatInfo *pIW = static_cast(p); - pIW->m_colorSide = D3DCOLOR_RGBA(tmpA.r, tmpA.g, tmpA.b, tmpA.a); - pIW->m_colorFront = D3DCOLOR_RGBA(tmpB.r, tmpB.g, tmpB.b, tmpB.a); - } - } - - // InnerWall_02 - ReloadFalloffMat(SkinXBX, _T("InnerWall_02"), &tmpA, &tmpB); - - // Single-color solids - ReloadSolidMat(SkinXBX, _T("Material #1334"), &tmpA); - ReloadSolidMat(SkinXBX, _T("XBOXgreendark"), &tmpA); - ReloadSolidMat(SkinXBX, _T("XBOXgreen"), &tmpA); - ReloadSolidMat(SkinXBX, _T("XBoxGreen2"), &tmpA); - ReloadSolidMat(SkinXBX, _T("GameHilite33"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Nothing"), &tmpA); - ReloadSolidMat(SkinXBX, _T("NavType"), &tmpA); - ReloadSolidMat(SkinXBX, _T("RedType"), &tmpA); - ReloadSolidMat(SkinXBX, _T("XBoxGreen"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Type"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Typesdsafsda"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Material #133"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Material #1335"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Material #133511"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Material #1336"), &tmpA); - ReloadSolidMat(SkinXBX, _T("HilightedType"), &tmpA); - ReloadSolidMat(SkinXBX, _T("XBoxGreenq"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Black80"), &tmpA); - ReloadSolidMat(SkinXBX, _T("CellEgg/Partsw"), &tmpA); - ReloadSolidMat(SkinXBX, _T("CellEgg/Partsz"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Material #108"), &tmpA); - ReloadSolidMat(SkinXBX, _T("ItemsType"), &tmpA); - ReloadSolidMat(SkinXBX, _T("GameHiliteMemory"), &tmpA); - ReloadSolidMat(SkinXBX, _T("red"), &tmpA); - - // Falloff materials (ColorA/ColorB) - ReloadFalloffMat(SkinXBX, _T("FlatSurfaces"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("FlatSurfacesSelected"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("FlatSurfacesMemory"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("DarkSurfaces"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("DarkSurfaces2"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("FlatSurfaces2sided"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("DetailLegSkin_Inner"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Screen"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Spout"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("NavType34"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("MetaFlatSurfaces"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("SC_SavedGame_Row01"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("HK_SavedGame_Row01"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("SavedEgg_Selected"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("FlatUnselected"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Mem_InnerWall_Outer"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("SavedGameEgg"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("GameMenuEgg"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Shell"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Material #133sdsfdsf"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("IconParts"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("IconParts1"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("MU1Pod_HL"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("MU1Pod"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("GamePodb"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("GamePod"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("JwlSrfc01/InfoPnls"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("MenuCell"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Tubes"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("JewelSurface02/PodMesh"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("TubesFade"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("TubesQ"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("EmptyMU"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Tube"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("MemoryHeader"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("ButtonGlow"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("gradient"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("CellWallStructure"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("FlatSrfc/PodParts"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Cell_Light"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Cell_Light/LegSkin"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("CellEgg/Parts"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("GameEgg"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("FlatSurfaces2sided3"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("console_hilite"), &tmpA, &tmpB); - - // Wireframe (CFalloffTexInfo, same color fields as CFalloffMatInfo) - ReloadFalloffMat(SkinXBX, _T("Wireframe"), &tmpA, &tmpB); - - // Aniso materials (same color fields as CFalloffMatInfo) - ReloadFalloffMat(SkinXBX, _T("Metal_Chrome"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Tvbox"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("AudioCD"), &tmpA, &tmpB); - - // Backing materials - ReloadBackingMat(SkinXBX, _T("PanelBacking_01"), &tmpA); - ReloadBackingMat(SkinXBX, _T("PanelBacking_02"), &tmpA); - ReloadBackingMat(SkinXBX, _T("PanelBacking_03"), &tmpA); - ReloadBackingMat(SkinXBX, _T("PanelBacking_04"), &tmpA); - ReloadBackingMat(SkinXBX, _T("NameBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("ModeBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("SavedGameBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("MemManMetaBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("DarkenBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("TextBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("MetaBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("DarkenBackingDark"), &tmpA); - - // Modulate texture materials - ReloadModulateMat(SkinXBX, _T("GameHilite"), &tmpA); - ReloadModulateMat(SkinXBX, _T("PanelBacking"), &tmpA); - - // EggGlow (CEggGlowFadeMatInfo) - { - CMatInfo *p = LookupMatInfo(_T("EggGlow")); - if (p) - { - TCHAR bufA[MAX_PATH], bufB[MAX_PATH]; - SkinXBX.GetValue(_T("EggGlow"), _T("ColorA"), bufA, MAX_PATH); - SkinXBX.GetValue(_T("EggGlow"), _T("ColorB"), bufB, MAX_PATH); - GetRGBAValues(bufA, &tmpA); - GetRGBAValues(bufB, &tmpB); - CEggGlowFadeMatInfo *pEgg = static_cast(p); - pEgg->m_colorSide = D3DCOLOR_RGBA(tmpA.r, tmpA.g, tmpA.b, tmpA.a); - pEgg->Color1 = tmpB; - } - } - - // EggGlowPulse (CEggGlowPulseMatInfo) - { - CMatInfo *p = LookupMatInfo(_T("EggGlowPulse")); - if (p) - { - TCHAR bufA[MAX_PATH], bufB[MAX_PATH]; - SkinXBX.GetValue(_T("EggGlowPulse"), _T("ColorA"), bufA, MAX_PATH); - SkinXBX.GetValue(_T("EggGlowPulse"), _T("ColorB"), bufB, MAX_PATH); - GetRGBAValues(bufA, &tmpA); - GetRGBAValues(bufB, &tmpB); - CEggGlowPulseMatInfo *pPulse = static_cast(p); - pPulse->Color1 = tmpA; - pPulse->Color2 = tmpB; - } - } - - // Keyboard -> Key, BrightKey, KeyText (CKeyMatInfo) - { - TCHAR bufA[MAX_PATH], bufB[MAX_PATH]; - SkinXBX.GetValue(_T("Keyboard"), _T("ColorA"), bufA, MAX_PATH); - SkinXBX.GetValue(_T("Keyboard"), _T("ColorB"), bufB, MAX_PATH); - GetRGBAValues(bufA, &tmpA); - GetRGBAValues(bufB, &tmpB); - SkinXBX.GetValue(_T("Keyboard"), _T("ColorC"), bufA, MAX_PATH); - SkinXBX.GetValue(_T("Keyboard"), _T("ColorD"), bufB, MAX_PATH); - GetRGBAValues(bufA, &tmpC); - GetRGBAValues(bufB, &tmpD); - - const TCHAR *keyNames[] = { _T("Key"), _T("BrightKey"), _T("KeyText") }; - for (int i = 0; i < 3; i++) - { - CMatInfo *p = LookupMatInfo(keyNames[i]); - if (p) - { - CKeyMatInfo *pKey = static_cast(p); - pKey->Color1 = tmpA; - pKey->Color2 = tmpB; - pKey->Color3 = tmpC; - pKey->Color4 = tmpD; - } - } - } - - OutputDebugString(_T("[Material_Reload] All material colors updated\n")); -} - -void Material_Init(bool bReloadSkinXBX) -{ - if (bReloadSkinXBX) - { - Material_Reload(); - return; - } - - CustomColor *CustomColorA = new CustomColor; - CustomColor *CustomColorB = new CustomColor; - CustomColor *CustomColorC = new CustomColor; - CustomColor *CustomColorD = new CustomColor; - - TCHAR CurrentSkinFile[MAX_PATH]; - TCHAR WorkerString[MAX_PATH]; - TCHAR SkinString[MAX_PATH]; - TCHAR ColorA[MAX_PATH]; - TCHAR ColorB[MAX_PATH]; - - CSettingsFile &SkinXBX = *TheseusGetSkinSettings(); - - SkinXBX.GetValue(_T("InnerWall_01"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("InnerWall_01"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CInnerWallMatInfo(_T("InnerWall_01"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), - D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("InnerWall_02"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("InnerWall_02"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("InnerWall_02"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - new CMatInfo(_T("MetaInfo_Text"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("NamePanel_Text"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("GameNameText_01"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("GameNameText_02"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("GameNameText_03"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("GameIcon_01"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("GameIcon_03"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("Material #132"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("u2 info"), MATINFO_STANDARD_MATERIAL); - - SkinXBX.GetValue(_T("Material #1334"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Material #1334"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("XBOXgreendark"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("XBOXgreendark"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("XBOXgreen"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("XBOXgreen"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("XBoxGreen2"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("XBoxGreen2"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("GameHilite33"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("GameHilite33"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Nothing"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Nothing"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("NavType"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("NavType"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("RedType"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("RedType"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("XBoxGreen"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("XBoxGreen"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Type"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Type"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Typesdsafsda"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Typesdsafsda"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Material #133"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Material #133"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Material #1335"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Material #1335"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Material #133511"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Material #133511"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Material #1336"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Material #1336"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("HilightedType"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("HilightedType"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("XBoxGreenq"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("XBoxGreenq"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Black80"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Black80"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("CellEgg/Partsw"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("CellEgg/Partsw"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("CellEgg/Partsz"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("CellEgg/Partsz"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Material #108"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Material #108"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("ItemsType"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("ItemsType"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("GameHiliteMemory"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("GameHiliteMemory"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("red"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("red"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("FlatSurfaces"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatSurfaces"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatSurfaces"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("FlatSurfacesSelected"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatSurfacesSelected"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatSurfacesSelected"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("FlatSurfacesMemory"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatSurfacesMemory"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatSurfacesMemory"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("DarkSurfaces"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("DarkSurfaces"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("DarkSurfaces"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("DarkSurfaces2"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("DarkSurfaces2"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("DarkSurfaces2"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("FlatSurfaces2sided"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatSurfaces2sided"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatSurfaces2sided"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("DetailLegSkin_Inner"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("DetailLegSkin_Inner"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("DetailLegSkin_Inner"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Screen"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Screen"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Screen"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("Spout"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Spout"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Spout"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("NavType34"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("NavType34"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("NavType34"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("MetaFlatSurfaces"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("MetaFlatSurfaces"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("MetaFlatSurfaces"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("SC_SavedGame_Row01"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("SC_SavedGame_Row01"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("SC_SavedGame_Row01"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("HK_SavedGame_Row01"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("HK_SavedGame_Row01"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("HK_SavedGame_Row01"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("SavedEgg_Selected"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("SavedEgg_Selected"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("SavedEgg_Selected"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("FlatUnselected"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatUnselected"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatUnselected"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Mem_InnerWall_Outer"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Mem_InnerWall_Outer"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Mem_InnerWall_Outer"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("SavedGameEgg"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("SavedGameEgg"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("SavedGameEgg"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("GameMenuEgg"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("GameMenuEgg"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("GameMenuEgg"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Shell"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Shell"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Shell"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Material #133sdsfdsf"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Material #133sdsfdsf"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Material #133sdsfdsf"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("IconParts"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("IconParts"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("IconParts"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("IconParts1"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("IconParts1"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("IconParts1"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("MU1Pod_HL"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("MU1Pod_HL"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("MU1Pod_HL"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("MU1Pod"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("MU1Pod"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("MU1Pod"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("GamePodb"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("GamePodb"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("GamePodb"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("GamePod"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("GamePod"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("GamePod"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("JwlSrfc01/InfoPnls"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("JwlSrfc01/InfoPnls"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("JwlSrfc01/InfoPnls"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("MenuCell"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("MenuCell"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("MenuCell"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Wireframe"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Wireframe"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffTexInfo(_T("Wireframe"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Tubes"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Tubes"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Tubes"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("JewelSurface02/PodMesh"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("JewelSurface02/PodMesh"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("JewelSurface02/PodMesh"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("TubesFade"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("TubesFade"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("TubesFade"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("TubesQ"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("TubesQ"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("TubesQ"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("EmptyMU"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("EmptyMU"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("EmptyMU"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Tube"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Tube"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Tube"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("MemoryHeader"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("MemoryHeader"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("MemoryHeader"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("EggGlow"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("EggGlow"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CEggGlowFadeMatInfo(_T("EggGlow"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), *CustomColorB); - - SkinXBX.GetValue(_T("ButtonGlow"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("ButtonGlow"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("ButtonGlow"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("gradient"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("gradient"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("gradient"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("CellWallStructure"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("CellWallStructure"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("CellWallStructure"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("FlatSrfc/PodParts"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatSrfc/PodParts"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatSrfc/PodParts"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Cell_Light"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Cell_Light"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Cell_Light"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Cell_Light/LegSkin"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Cell_Light/LegSkin"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Cell_Light/LegSkin"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("CellEgg/Parts"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("CellEgg/Parts"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("CellEgg/Parts"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("GameEgg"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("GameEgg"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("GameEgg"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("FlatSurfaces2sided3"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatSurfaces2sided3"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatSurfaces2sided3"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("console_hilite"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("console_hilite"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("console_hilite"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Metal_Chrome"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Metal_Chrome"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CAnisoMatInfo(_T("Metal_Chrome"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Tvbox"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Tvbox"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CAnisoMatInfo(_T("Tvbox"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("AudioCD"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("AudioCD"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CAnisoMatInfo(_T("AudioCD"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("PanelBacking_01"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("PanelBacking_01"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("PanelBacking_02"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("PanelBacking_02"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("PanelBacking_03"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("PanelBacking_03"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("PanelBacking_04"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("PanelBacking_04"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("NameBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("NameBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("ModeBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("ModeBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("SavedGameBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("SavedGameBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("MemManMetaBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("MemManMetaBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("DarkenBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("DarkenBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a, MATINFO_CULL_NONE | MATINFO_RADIAL_EDGE_ALPHA); - - SkinXBX.GetValue(_T("TextBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("TextBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a, MATINFO_CULL_NONE | MATINFO_RADIAL_ALPHA); - - SkinXBX.GetValue(_T("MetaBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("MetaBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a, MATINFO_CULL_NONE | MATINFO_RADIAL_ALPHA); - - SkinXBX.GetValue(_T("DarkenBackingDark"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("DarkenBackingDark"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a, MATINFO_CULL_NONE | MATINFO_RADIAL_ALPHA); - - SkinXBX.GetValue(_T("GameHilite"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CModulateTextureMatInfo(_T("GameHilite"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a, MATINFO_CULL_NONE); - - new CMaskTextureMatInfo(_T("equalizer"), MATINFO_RADIAL_AV_ALPHA); - new CMaskTextureMatInfo(_T("MainMenuOrb"), MATINFO_RADIAL_EDGE_MAIN_ALPHA); - - SkinXBX.GetValue(_T("PanelBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CModulateTextureMatInfo(_T("PanelBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a, MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("EggGlowPulse"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("EggGlowPulse"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CEggGlowPulseMatInfo(_T("EggGlowPulse"), *CustomColorA, *CustomColorB); - - SkinXBX.GetValue(_T("Keyboard"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Keyboard"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - SkinXBX.GetValue(_T("Keyboard"), _T("ColorC"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Keyboard"), _T("ColorD"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorC); - GetRGBAValues(ColorB, CustomColorD); - new CKeyMatInfo(_T("Key"), false, false, *CustomColorA, *CustomColorB, *CustomColorC, *CustomColorD); - new CKeyMatInfo(_T("BrightKey"), true, false, *CustomColorA, *CustomColorB, *CustomColorC, *CustomColorD); - new CKeyMatInfo(_T("KeyText"), false, true, *CustomColorA, *CustomColorB, *CustomColorC, *CustomColorD); - - new CReflectMatInfo(_T("ReflectSurface")); - new CFreeSpaceMatInfo(_T("Material #10822")); - new CIconMatInfo(_T("TitleIcon"), 128, 128); - new CIconMatInfo(_T("TitleSoundtrackIcon"), 128, 128, false, true); - new CIconMatInfo(_T("SavedGameIcon"), 64, 64, true); - new CIconMatInfo(_T("SoundtrackIcon"), 64, 64, true, true); - new CIconMatInfo(_T("SelectedIcon"), 128, 128, false, false, true); - new CIconMatInfo(_T("TitleMenuIcon"), 128, 128, false, false, true); - // OrangeNavType - Solid (5960 binary: A=0xB2=178, not 255) - new CSolidMatInfo(_T("OrangeNavType"), 0xF9, 0x98, 0x19, 0xB2); - - // XboxLiveGameTitleOrbIcon - Same as OrangeNavType - new CSolidMatInfo(_T("XboxLiveGameTitleOrbIcon"), 0xF9, 0x98, 0x19, 0xFF); // same as OrangeNavType - - // MemoryHeaderHilite - Falloff (5960 binary: side=0xF0C7E800 front=0x82617200) - // Was incorrectly Solid with R/G swapped. Actual: RGBA(199,232,0,240) / RGBA(97,114,0,130) - new CFalloffMatInfo(_T("MemoryHeaderHilite"), D3DCOLOR_RGBA(199, 232, 0, 240), D3DCOLOR_RGBA(97, 114, 0, 130)); - - // grill grey - Falloff (5960 binary: side=0x00202020 front=0xFF404040) - new CFalloffMatInfo(_T("grill grey"), D3DCOLOR_RGBA(32, 32, 32, 0), D3DCOLOR_RGBA(64, 64, 64, 255)); - - // LiveChrome - Solid (9D6DC2) - new CSolidMatInfo(_T("LiveChrome"), 0x9D, 0x6D, 0xC2, 0xFF); // from 'livechrome' - - // grey - Falloff (5960 binary: side=0x00475345 front=0xFF566452) - new CFalloffMatInfo(_T("grey"), D3DCOLOR_RGBA(71, 83, 69, 0), D3DCOLOR_RGBA(86, 100, 82, 255)); - - // dark green panels - Solid - new CSolidMatInfo(_T("dark green panels"), 0x09, 0x29, 0x00, 0xFF); // from 'dark_green_panels' - - // dark green panels falloff - Falloff (5960 binary: side=0x00000000 front=0xFF052305) - new CFalloffMatInfo(_T("dark green panels falloff"), D3DCOLOR_RGBA(0, 0, 0, 0), D3DCOLOR_RGBA(5, 35, 5, 255)); - - // footer - Solid - new CSolidMatInfo(_T("footer"), 0x9D, 0x6D, 0xC2, 0xFF); // from 'footer' - - // live header - Solid - new CSolidMatInfo(_T("live header"), 0x04, 0x14, 0x00, 0xFF); // from 'live_header' - - // orangeEggGlow - Solid - new CSolidMatInfo(_T("orangeEggGlow"), 0xFE, 0xFF, 0xBC, 0xFF); // derived from 'eggglow_1' (E4FEFFBC flipped = FFBCFFFE) - - // MotdIcon - Solid (default white) - new CSolidMatInfo(_T("MotdIcon"), 0xFF, 0xFF, 0xFF, 0xFF); // fallback or default - - // highlight - Solid - new CSolidMatInfo(_T("highlight"), 0x9D, 0x6D, 0xC2, 0xFF); // from 'highlight' - - // button - Solid - new CSolidMatInfo(_T("button"), 0x9D, 0x6D, 0xC2, 0xFF); // from 'button' - - // white - Solid - new CSolidMatInfo(_T("white"), 0xFF, 0xFF, 0xFF, 0xFF); // from 'white' - - // solid green 1 - Standard - new CSolidMatInfo(_T("solid green 1"), 0x11, 0xFF, 0x22, 0xFF); // from 'solid_green_1' - - // solid green2 - Standard - new CSolidMatInfo(_T("solid green2"), 0x12, 0xC1, 0x0A, 0xFF); // from 'solid_green_2' - - // solid green 3 - Standard - new CSolidMatInfo(_T("solid green 3"), 0x0A, 0x75, 0x1C, 0xFF); // from 'solid_green_3' - - // solid green 4 - Standard - new CSolidMatInfo(_T("solid green 4"), 0x12, 0x37, 0x00, 0xFF); // from 'solid_green_4' - - // Black - new CSolidMatInfo(_T("Black"), 0x00, 0x00, 0x00, 0xFF); - - qsort(g_rgMatInfo, g_nMatInfoCount, sizeof(CMatInfo *), SortMatInfoCompare); - - delete CustomColorA; - delete CustomColorB; - delete CustomColorC; - delete CustomColorD; -} +#include "materials_retail.inl" IMPLEMENT_NODE("MaxMaterial", CMaxMaterial, CMaterial) diff --git a/theseus/render/materials.cpp b/theseus/render/materials.cpp index e7610a8..7974146 100755 --- a/theseus/render/materials.cpp +++ b/theseus/render/materials.cpp @@ -723,7 +723,7 @@ bool CKeyMatInfo::Setup(CMaxMaterial *pMaxMat) } else { - m_colorSide = D3DCOLOR_RGBA(Color2.r, Color2.g, Color2.b, (Color2.a - 3)); + m_colorSide = D3DCOLOR_RGBA(Color2.r, Color2.g, Color2.b, (Color2.a - 33)); m_colorFront = D3DCOLOR_RGBA((Color3.r - 5), (Color3.g - 33), Color3.b, (Color3.a - 19)); } @@ -983,44 +983,6 @@ CMatInfo *LookupMatInfo(const TCHAR *szName) return *ppMatInfo; } -void GetRGBAValues(TCHAR *dataIn, CustomColor *color) -{ - int tempR, tempG, tempB, tempA = 0; - - _stscanf(dataIn, _T("%d, %d, %d, %d"), &tempR, &tempG, &tempB, &tempA); - - color->r = tempR; - color->g = tempG; - color->b = tempB; - color->a = tempA; -} - -// Helpers for updating existing material colors on skin reload -static void UpdateSolidColor(CMatInfo *p, BYTE r, BYTE g, BYTE b, BYTE a) -{ - CSolidMatInfo *pMat = static_cast(p); - pMat->m_r = r; pMat->m_g = g; pMat->m_b = b; pMat->m_a = a; -} - -static void UpdateBackingColor(CMatInfo *p, BYTE r, BYTE g, BYTE b, BYTE a) -{ - CBackingMatInfo *pMat = static_cast(p); - pMat->m_r = r; pMat->m_g = g; pMat->m_b = b; pMat->m_a = a; -} - -static void UpdateModulateColor(CMatInfo *p, BYTE r, BYTE g, BYTE b, BYTE a) -{ - CModulateTextureMatInfo *pMat = static_cast(p); - pMat->m_r = r; pMat->m_g = g; pMat->m_b = b; pMat->m_a = a; -} - -static void UpdateFalloffColor(CMatInfo *p, D3DCOLOR colorSide, D3DCOLOR colorFront) -{ - CFalloffMatInfo *pMat = static_cast(p); - pMat->m_colorSide = colorSide; - pMat->m_colorFront = colorFront; -} - // Public introspection / live edit API used by the Skin Editor. Desktop only: // the dashboard never edits materials at runtime, and dynamic_cast here is the // only thing that would force RTTI into the Xbox build. @@ -1079,833 +1041,7 @@ void MatInfo_SetColor(CMatInfo* p, int idx, DWORD c) } #endif // !_XBOX -// Reads a single-color material from the skin file and updates in place -static void ReloadSolidMat(CSettingsFile &SkinXBX, const TCHAR *szSection, CustomColor *tmp) -{ - CMatInfo *p = LookupMatInfo(szSection); - if (!p) return; - TCHAR buf[MAX_PATH]; - SkinXBX.GetValue(szSection, _T("Color"), buf, MAX_PATH); - GetRGBAValues(buf, tmp); - UpdateSolidColor(p, (BYTE)tmp->r, (BYTE)tmp->g, (BYTE)tmp->b, (BYTE)tmp->a); -} - -static void ReloadBackingMat(CSettingsFile &SkinXBX, const TCHAR *szSection, CustomColor *tmp) -{ - CMatInfo *p = LookupMatInfo(szSection); - if (!p) return; - TCHAR buf[MAX_PATH]; - SkinXBX.GetValue(szSection, _T("Color"), buf, MAX_PATH); - GetRGBAValues(buf, tmp); - UpdateBackingColor(p, (BYTE)tmp->r, (BYTE)tmp->g, (BYTE)tmp->b, (BYTE)tmp->a); -} - -static void ReloadModulateMat(CSettingsFile &SkinXBX, const TCHAR *szSection, CustomColor *tmp) -{ - CMatInfo *p = LookupMatInfo(szSection); - if (!p) return; - TCHAR buf[MAX_PATH]; - SkinXBX.GetValue(szSection, _T("Color"), buf, MAX_PATH); - GetRGBAValues(buf, tmp); - UpdateModulateColor(p, (BYTE)tmp->r, (BYTE)tmp->g, (BYTE)tmp->b, (BYTE)tmp->a); -} - -static void ReloadFalloffMat(CSettingsFile &SkinXBX, const TCHAR *szSection, CustomColor *tmpA, CustomColor *tmpB) -{ - CMatInfo *p = LookupMatInfo(szSection); - if (!p) return; - TCHAR bufA[MAX_PATH], bufB[MAX_PATH]; - SkinXBX.GetValue(szSection, _T("ColorA"), bufA, MAX_PATH); - SkinXBX.GetValue(szSection, _T("ColorB"), bufB, MAX_PATH); - GetRGBAValues(bufA, tmpA); - GetRGBAValues(bufB, tmpB); - UpdateFalloffColor(p, - D3DCOLOR_RGBA(tmpA->r, tmpA->g, tmpA->b, tmpA->a), - D3DCOLOR_RGBA(tmpB->r, tmpB->g, tmpB->b, tmpB->a)); -} - -// Reload path: same as init but updates existing materials in place -static void Material_Reload() -{ - if (!TheseusGetSkinSettings()) - return; - - CSettingsFile &SkinXBX = *TheseusGetSkinSettings(); - CustomColor tmpA, tmpB, tmpC, tmpD; - - // InnerWall_01 (CInnerWallMatInfo has same color layout as CFalloffMatInfo) - { - CMatInfo *p = LookupMatInfo(_T("InnerWall_01")); - if (p) - { - TCHAR bufA[MAX_PATH], bufB[MAX_PATH]; - SkinXBX.GetValue(_T("InnerWall_01"), _T("ColorA"), bufA, MAX_PATH); - SkinXBX.GetValue(_T("InnerWall_01"), _T("ColorB"), bufB, MAX_PATH); - GetRGBAValues(bufA, &tmpA); - GetRGBAValues(bufB, &tmpB); - CInnerWallMatInfo *pIW = static_cast(p); - pIW->m_colorSide = D3DCOLOR_RGBA(tmpA.r, tmpA.g, tmpA.b, tmpA.a); - pIW->m_colorFront = D3DCOLOR_RGBA(tmpB.r, tmpB.g, tmpB.b, tmpB.a); - } - } - - // InnerWall_02 - ReloadFalloffMat(SkinXBX, _T("InnerWall_02"), &tmpA, &tmpB); - - // Single-color solids - ReloadSolidMat(SkinXBX, _T("Material #1334"), &tmpA); - ReloadSolidMat(SkinXBX, _T("XBOXgreendark"), &tmpA); - ReloadSolidMat(SkinXBX, _T("XBOXgreen"), &tmpA); - ReloadSolidMat(SkinXBX, _T("XBoxGreen2"), &tmpA); - ReloadSolidMat(SkinXBX, _T("GameHilite33"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Nothing"), &tmpA); - ReloadSolidMat(SkinXBX, _T("NavType"), &tmpA); - ReloadSolidMat(SkinXBX, _T("RedType"), &tmpA); - ReloadSolidMat(SkinXBX, _T("XBoxGreen"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Type"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Typesdsafsda"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Material #133"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Material #1335"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Material #133511"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Material #1336"), &tmpA); - ReloadSolidMat(SkinXBX, _T("HilightedType"), &tmpA); - ReloadSolidMat(SkinXBX, _T("XBoxGreenq"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Black80"), &tmpA); - ReloadSolidMat(SkinXBX, _T("CellEgg/Partsw"), &tmpA); - ReloadSolidMat(SkinXBX, _T("CellEgg/Partsz"), &tmpA); - ReloadSolidMat(SkinXBX, _T("Material #108"), &tmpA); - ReloadSolidMat(SkinXBX, _T("ItemsType"), &tmpA); - ReloadSolidMat(SkinXBX, _T("GameHiliteMemory"), &tmpA); - ReloadSolidMat(SkinXBX, _T("red"), &tmpA); - - // Falloff materials (ColorA/ColorB) - ReloadFalloffMat(SkinXBX, _T("FlatSurfaces"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("FlatSurfacesSelected"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("FlatSurfacesMemory"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("DarkSurfaces"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("DarkSurfaces2"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("FlatSurfaces2sided"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("DetailLegSkin_Inner"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Screen"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Spout"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("NavType34"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("MetaFlatSurfaces"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("SC_SavedGame_Row01"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("HK_SavedGame_Row01"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("SavedEgg_Selected"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("FlatUnselected"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Mem_InnerWall_Outer"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("SavedGameEgg"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("GameMenuEgg"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Shell"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Material #133sdsfdsf"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("IconParts"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("IconParts1"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("MU1Pod_HL"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("MU1Pod"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("GamePodb"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("GamePod"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("JwlSrfc01/InfoPnls"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("MenuCell"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Tubes"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("JewelSurface02/PodMesh"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("TubesFade"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("TubesQ"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("EmptyMU"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Tube"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("MemoryHeader"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("ButtonGlow"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("gradient"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("CellWallStructure"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("FlatSrfc/PodParts"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Cell_Light"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Cell_Light/LegSkin"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("CellEgg/Parts"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("GameEgg"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("FlatSurfaces2sided3"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("console_hilite"), &tmpA, &tmpB); - - // Wireframe (CFalloffTexInfo, same color fields as CFalloffMatInfo) - ReloadFalloffMat(SkinXBX, _T("Wireframe"), &tmpA, &tmpB); - - // Aniso materials (same color fields as CFalloffMatInfo) - ReloadFalloffMat(SkinXBX, _T("Metal_Chrome"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("Tvbox"), &tmpA, &tmpB); - ReloadFalloffMat(SkinXBX, _T("AudioCD"), &tmpA, &tmpB); - - // Backing materials - ReloadBackingMat(SkinXBX, _T("PanelBacking_01"), &tmpA); - ReloadBackingMat(SkinXBX, _T("PanelBacking_02"), &tmpA); - ReloadBackingMat(SkinXBX, _T("PanelBacking_03"), &tmpA); - ReloadBackingMat(SkinXBX, _T("PanelBacking_04"), &tmpA); - ReloadBackingMat(SkinXBX, _T("NameBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("ModeBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("SavedGameBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("MemManMetaBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("DarkenBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("TextBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("MetaBacking"), &tmpA); - ReloadBackingMat(SkinXBX, _T("DarkenBackingDark"), &tmpA); - - // Modulate texture materials - ReloadModulateMat(SkinXBX, _T("GameHilite"), &tmpA); - ReloadModulateMat(SkinXBX, _T("PanelBacking"), &tmpA); - - // EggGlow (CEggGlowFadeMatInfo) - { - CMatInfo *p = LookupMatInfo(_T("EggGlow")); - if (p) - { - TCHAR bufA[MAX_PATH], bufB[MAX_PATH]; - SkinXBX.GetValue(_T("EggGlow"), _T("ColorA"), bufA, MAX_PATH); - SkinXBX.GetValue(_T("EggGlow"), _T("ColorB"), bufB, MAX_PATH); - GetRGBAValues(bufA, &tmpA); - GetRGBAValues(bufB, &tmpB); - CEggGlowFadeMatInfo *pEgg = static_cast(p); - pEgg->m_colorSide = D3DCOLOR_RGBA(tmpA.r, tmpA.g, tmpA.b, tmpA.a); - pEgg->Color1 = tmpB; - } - } - - // EggGlowPulse (CEggGlowPulseMatInfo) - { - CMatInfo *p = LookupMatInfo(_T("EggGlowPulse")); - if (p) - { - TCHAR bufA[MAX_PATH], bufB[MAX_PATH]; - SkinXBX.GetValue(_T("EggGlowPulse"), _T("ColorA"), bufA, MAX_PATH); - SkinXBX.GetValue(_T("EggGlowPulse"), _T("ColorB"), bufB, MAX_PATH); - GetRGBAValues(bufA, &tmpA); - GetRGBAValues(bufB, &tmpB); - CEggGlowPulseMatInfo *pPulse = static_cast(p); - pPulse->Color1 = tmpA; - pPulse->Color2 = tmpB; - } - } - - // Keyboard -> Key, BrightKey, KeyText (CKeyMatInfo) - { - TCHAR bufA[MAX_PATH], bufB[MAX_PATH]; - SkinXBX.GetValue(_T("Keyboard"), _T("ColorA"), bufA, MAX_PATH); - SkinXBX.GetValue(_T("Keyboard"), _T("ColorB"), bufB, MAX_PATH); - GetRGBAValues(bufA, &tmpA); - GetRGBAValues(bufB, &tmpB); - SkinXBX.GetValue(_T("Keyboard"), _T("ColorC"), bufA, MAX_PATH); - SkinXBX.GetValue(_T("Keyboard"), _T("ColorD"), bufB, MAX_PATH); - GetRGBAValues(bufA, &tmpC); - GetRGBAValues(bufB, &tmpD); - - const TCHAR *keyNames[] = { _T("Key"), _T("BrightKey"), _T("KeyText") }; - for (int i = 0; i < 3; i++) - { - CMatInfo *p = LookupMatInfo(keyNames[i]); - if (p) - { - CKeyMatInfo *pKey = static_cast(p); - pKey->Color1 = tmpA; - pKey->Color2 = tmpB; - pKey->Color3 = tmpC; - pKey->Color4 = tmpD; - } - } - } - - OutputDebugString(_T("[Material_Reload] All material colors updated\n")); -} - -void Material_Init(bool bReloadSkinXBX) -{ - if (bReloadSkinXBX) - { - Material_Reload(); - return; - } - - CustomColor *CustomColorA = new CustomColor; - CustomColor *CustomColorB = new CustomColor; - CustomColor *CustomColorC = new CustomColor; - CustomColor *CustomColorD = new CustomColor; - - TCHAR CurrentSkinFile[MAX_PATH]; - TCHAR WorkerString[MAX_PATH]; - TCHAR SkinString[MAX_PATH]; - TCHAR ColorA[MAX_PATH]; - TCHAR ColorB[MAX_PATH]; - - CSettingsFile &SkinXBX = *TheseusGetSkinSettings(); - - SkinXBX.GetValue(_T("InnerWall_01"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("InnerWall_01"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CInnerWallMatInfo(_T("InnerWall_01"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), - D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("InnerWall_02"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("InnerWall_02"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("InnerWall_02"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - new CMatInfo(_T("MetaInfo_Text"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("NamePanel_Text"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("GameNameText_01"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("GameNameText_02"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("GameNameText_03"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("GameIcon_01"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("GameIcon_03"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("Material #132"), MATINFO_STANDARD_MATERIAL); - new CMatInfo(_T("u2 info"), MATINFO_STANDARD_MATERIAL); - - SkinXBX.GetValue(_T("Material #1334"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Material #1334"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("XBOXgreendark"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("XBOXgreendark"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("XBOXgreen"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("XBOXgreen"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("XBoxGreen2"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("XBoxGreen2"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("GameHilite33"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("GameHilite33"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Nothing"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Nothing"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("NavType"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("NavType"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("RedType"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("RedType"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("XBoxGreen"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("XBoxGreen"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Type"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Type"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Typesdsafsda"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Typesdsafsda"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Material #133"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Material #133"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Material #1335"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Material #1335"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Material #133511"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Material #133511"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Material #1336"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Material #1336"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("HilightedType"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("HilightedType"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("XBoxGreenq"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("XBoxGreenq"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Black80"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Black80"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("CellEgg/Partsw"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("CellEgg/Partsw"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("CellEgg/Partsz"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("CellEgg/Partsz"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("Material #108"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("Material #108"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("ItemsType"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("ItemsType"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("GameHiliteMemory"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("GameHiliteMemory"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("red"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CSolidMatInfo(_T("red"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("FlatSurfaces"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatSurfaces"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatSurfaces"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("FlatSurfacesSelected"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatSurfacesSelected"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatSurfacesSelected"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("FlatSurfacesMemory"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatSurfacesMemory"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatSurfacesMemory"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("DarkSurfaces"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("DarkSurfaces"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("DarkSurfaces"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("DarkSurfaces2"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("DarkSurfaces2"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("DarkSurfaces2"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("FlatSurfaces2sided"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatSurfaces2sided"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatSurfaces2sided"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("DetailLegSkin_Inner"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("DetailLegSkin_Inner"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("DetailLegSkin_Inner"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Screen"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Screen"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Screen"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("Spout"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Spout"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Spout"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("NavType34"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("NavType34"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("NavType34"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("MetaFlatSurfaces"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("MetaFlatSurfaces"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("MetaFlatSurfaces"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("SC_SavedGame_Row01"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("SC_SavedGame_Row01"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("SC_SavedGame_Row01"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("HK_SavedGame_Row01"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("HK_SavedGame_Row01"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("HK_SavedGame_Row01"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("SavedEgg_Selected"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("SavedEgg_Selected"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("SavedEgg_Selected"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("FlatUnselected"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatUnselected"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatUnselected"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Mem_InnerWall_Outer"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Mem_InnerWall_Outer"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Mem_InnerWall_Outer"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("SavedGameEgg"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("SavedGameEgg"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("SavedGameEgg"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("GameMenuEgg"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("GameMenuEgg"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("GameMenuEgg"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Shell"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Shell"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Shell"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Material #133sdsfdsf"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Material #133sdsfdsf"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Material #133sdsfdsf"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("IconParts"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("IconParts"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("IconParts"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("IconParts1"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("IconParts1"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("IconParts1"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("MU1Pod_HL"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("MU1Pod_HL"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("MU1Pod_HL"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("MU1Pod"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("MU1Pod"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("MU1Pod"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("GamePodb"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("GamePodb"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("GamePodb"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("GamePod"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("GamePod"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("GamePod"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("JwlSrfc01/InfoPnls"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("JwlSrfc01/InfoPnls"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("JwlSrfc01/InfoPnls"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("MenuCell"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("MenuCell"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("MenuCell"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Wireframe"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Wireframe"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffTexInfo(_T("Wireframe"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Tubes"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Tubes"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Tubes"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("JewelSurface02/PodMesh"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("JewelSurface02/PodMesh"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("JewelSurface02/PodMesh"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("TubesFade"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("TubesFade"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("TubesFade"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("TubesQ"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("TubesQ"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("TubesQ"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("EmptyMU"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("EmptyMU"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("EmptyMU"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Tube"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Tube"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Tube"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("MemoryHeader"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("MemoryHeader"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("MemoryHeader"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a), MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("EggGlow"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("EggGlow"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CEggGlowFadeMatInfo(_T("EggGlow"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), *CustomColorB); - - SkinXBX.GetValue(_T("ButtonGlow"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("ButtonGlow"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("ButtonGlow"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("gradient"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("gradient"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("gradient"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("CellWallStructure"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("CellWallStructure"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("CellWallStructure"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("FlatSrfc/PodParts"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatSrfc/PodParts"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatSrfc/PodParts"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Cell_Light"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Cell_Light"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Cell_Light"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Cell_Light/LegSkin"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Cell_Light/LegSkin"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("Cell_Light/LegSkin"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("CellEgg/Parts"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("CellEgg/Parts"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("CellEgg/Parts"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("GameEgg"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("GameEgg"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("GameEgg"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("FlatSurfaces2sided3"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("FlatSurfaces2sided3"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("FlatSurfaces2sided3"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("console_hilite"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("console_hilite"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CFalloffMatInfo(_T("console_hilite"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Metal_Chrome"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Metal_Chrome"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CAnisoMatInfo(_T("Metal_Chrome"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("Tvbox"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Tvbox"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CAnisoMatInfo(_T("Tvbox"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("AudioCD"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("AudioCD"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CAnisoMatInfo(_T("AudioCD"), D3DCOLOR_RGBA(CustomColorA->r, CustomColorA->g, CustomColorA->b, CustomColorA->a), D3DCOLOR_RGBA(CustomColorB->r, CustomColorB->g, CustomColorB->b, CustomColorB->a)); - - SkinXBX.GetValue(_T("PanelBacking_01"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("PanelBacking_01"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("PanelBacking_02"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("PanelBacking_02"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("PanelBacking_03"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("PanelBacking_03"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("PanelBacking_04"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("PanelBacking_04"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("NameBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("NameBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("ModeBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("ModeBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("SavedGameBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("SavedGameBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("MemManMetaBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("MemManMetaBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a); - - SkinXBX.GetValue(_T("DarkenBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("DarkenBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a, MATINFO_CULL_NONE | MATINFO_RADIAL_EDGE_ALPHA); - - SkinXBX.GetValue(_T("TextBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("TextBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a, MATINFO_CULL_NONE | MATINFO_RADIAL_ALPHA); - - SkinXBX.GetValue(_T("MetaBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("MetaBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a, MATINFO_CULL_NONE | MATINFO_RADIAL_ALPHA); - - SkinXBX.GetValue(_T("DarkenBackingDark"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CBackingMatInfo(_T("DarkenBackingDark"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a, MATINFO_CULL_NONE | MATINFO_RADIAL_ALPHA); - - SkinXBX.GetValue(_T("GameHilite"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CModulateTextureMatInfo(_T("GameHilite"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a, MATINFO_CULL_NONE); - - new CMaskTextureMatInfo(_T("equalizer"), MATINFO_RADIAL_AV_ALPHA); - new CMaskTextureMatInfo(_T("MainMenuOrb"), MATINFO_RADIAL_EDGE_MAIN_ALPHA); - - SkinXBX.GetValue(_T("PanelBacking"), _T("Color"), ColorA, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - new CModulateTextureMatInfo(_T("PanelBacking"), (BYTE)CustomColorA->r, (BYTE)CustomColorA->g, (BYTE)CustomColorA->b, (BYTE)CustomColorA->a, MATINFO_CULL_NONE); - - SkinXBX.GetValue(_T("EggGlowPulse"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("EggGlowPulse"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - new CEggGlowPulseMatInfo(_T("EggGlowPulse"), *CustomColorA, *CustomColorB); - - SkinXBX.GetValue(_T("Keyboard"), _T("ColorA"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Keyboard"), _T("ColorB"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorA); - GetRGBAValues(ColorB, CustomColorB); - SkinXBX.GetValue(_T("Keyboard"), _T("ColorC"), ColorA, MAX_PATH); - SkinXBX.GetValue(_T("Keyboard"), _T("ColorD"), ColorB, MAX_PATH); - GetRGBAValues(ColorA, CustomColorC); - GetRGBAValues(ColorB, CustomColorD); - new CKeyMatInfo(_T("Key"), false, false, *CustomColorA, *CustomColorB, *CustomColorC, *CustomColorD); - new CKeyMatInfo(_T("BrightKey"), true, false, *CustomColorA, *CustomColorB, *CustomColorC, *CustomColorD); - new CKeyMatInfo(_T("KeyText"), false, true, *CustomColorA, *CustomColorB, *CustomColorC, *CustomColorD); - - new CReflectMatInfo(_T("ReflectSurface")); - new CFreeSpaceMatInfo(_T("Material #10822")); - new CIconMatInfo(_T("TitleIcon"), 128, 128); - new CIconMatInfo(_T("TitleSoundtrackIcon"), 128, 128, false, true); - new CIconMatInfo(_T("SavedGameIcon"), 64, 64, true); - new CIconMatInfo(_T("SoundtrackIcon"), 64, 64, true, true); - new CIconMatInfo(_T("SelectedIcon"), 128, 128, false, false, true); - new CIconMatInfo(_T("TitleMenuIcon"), 128, 128, false, false, true); - // OrangeNavType - Solid (5960 binary: A=0xB2=178, not 255) - new CSolidMatInfo(_T("OrangeNavType"), 0xF9, 0x98, 0x19, 0xB2); - - // XboxLiveGameTitleOrbIcon - Same as OrangeNavType - new CSolidMatInfo(_T("XboxLiveGameTitleOrbIcon"), 0xF9, 0x98, 0x19, 0xFF); // same as OrangeNavType - - // MemoryHeaderHilite - Falloff (5960 binary: side=0xF0C7E800 front=0x82617200) - // Was incorrectly Solid with R/G swapped. Actual: RGBA(199,232,0,240) / RGBA(97,114,0,130) - new CFalloffMatInfo(_T("MemoryHeaderHilite"), D3DCOLOR_RGBA(199, 232, 0, 240), D3DCOLOR_RGBA(97, 114, 0, 130)); - - // grill grey - Falloff (5960 binary: side=0x00202020 front=0xFF404040) - new CFalloffMatInfo(_T("grill grey"), D3DCOLOR_RGBA(32, 32, 32, 0), D3DCOLOR_RGBA(64, 64, 64, 255)); - - // LiveChrome - Solid (9D6DC2) - new CSolidMatInfo(_T("LiveChrome"), 0x9D, 0x6D, 0xC2, 0xFF); // from 'livechrome' - - // grey - Falloff (5960 binary: side=0x00475345 front=0xFF566452) - new CFalloffMatInfo(_T("grey"), D3DCOLOR_RGBA(71, 83, 69, 0), D3DCOLOR_RGBA(86, 100, 82, 255)); - - // dark green panels - Solid - new CSolidMatInfo(_T("dark green panels"), 0x09, 0x29, 0x00, 0xFF); // from 'dark_green_panels' - - // dark green panels falloff - Falloff (5960 binary: side=0x00000000 front=0xFF052305) - new CFalloffMatInfo(_T("dark green panels falloff"), D3DCOLOR_RGBA(0, 0, 0, 0), D3DCOLOR_RGBA(5, 35, 5, 255)); - - // footer - Solid - new CSolidMatInfo(_T("footer"), 0x9D, 0x6D, 0xC2, 0xFF); // from 'footer' - - // live header - Solid - new CSolidMatInfo(_T("live header"), 0x04, 0x14, 0x00, 0xFF); // from 'live_header' - - // orangeEggGlow - Solid - new CSolidMatInfo(_T("orangeEggGlow"), 0xFE, 0xFF, 0xBC, 0xFF); // derived from 'eggglow_1' (E4FEFFBC flipped = FFBCFFFE) - - // MotdIcon - Solid (default white) - new CSolidMatInfo(_T("MotdIcon"), 0xFF, 0xFF, 0xFF, 0xFF); // fallback or default - - // highlight - Solid - new CSolidMatInfo(_T("highlight"), 0x9D, 0x6D, 0xC2, 0xFF); // from 'highlight' - - // button - Solid - new CSolidMatInfo(_T("button"), 0x9D, 0x6D, 0xC2, 0xFF); // from 'button' - - // white - Solid - new CSolidMatInfo(_T("white"), 0xFF, 0xFF, 0xFF, 0xFF); // from 'white' - - // solid green 1 - Standard - new CSolidMatInfo(_T("solid green 1"), 0x11, 0xFF, 0x22, 0xFF); // from 'solid_green_1' - - // solid green2 - Standard - new CSolidMatInfo(_T("solid green2"), 0x12, 0xC1, 0x0A, 0xFF); // from 'solid_green_2' - - // solid green 3 - Standard - new CSolidMatInfo(_T("solid green 3"), 0x0A, 0x75, 0x1C, 0xFF); // from 'solid_green_3' - - // solid green 4 - Standard - new CSolidMatInfo(_T("solid green 4"), 0x12, 0x37, 0x00, 0xFF); // from 'solid_green_4' - - // Black - new CSolidMatInfo(_T("Black"), 0x00, 0x00, 0x00, 0xFF); - - qsort(g_rgMatInfo, g_nMatInfoCount, sizeof(CMatInfo *), SortMatInfoCompare); - - delete CustomColorA; - delete CustomColorB; - delete CustomColorC; - delete CustomColorD; -} +#include "materials_retail.inl" IMPLEMENT_NODE("MaxMaterial", CMaxMaterial, CMaterial) diff --git a/theseus/render/materials_retail.inl b/theseus/render/materials_retail.inl new file mode 100644 index 0000000..7aa589c --- /dev/null +++ b/theseus/render/materials_retail.inl @@ -0,0 +1,397 @@ +// materials_retail.inl: every material the 5960 dashboard builds, with its +// retail colours. Included by render/materials.cpp and +// desktop/materials_desktop.cpp after their CMatInfo classes, so both builds +// bind the same names. A skin only overrides what it lists; the rest stays retail. + +enum MatKind +{ + MK_STANDARD, + MK_SOLID, + MK_BACKING, + MK_MODULATE, + MK_FALLOFF, + MK_FALLOFFTEX, + MK_INNERWALL, + MK_ANISO, + MK_EGGGLOW, + MK_EGGGLOWPULSE, + MK_KEY, + MK_ICON, + MK_MASK, + MK_REFLECT, + MK_FREESPACE, +}; + +struct RetailMat +{ + const TCHAR *szName; + MatKind kind; + DWORD dwFlags; + D3DCOLOR colorA; // solid colour, or falloff side + D3DCOLOR colorB; // falloff front + WORD nIconSize; + BYTE bFadeIn, bInXIP, bSelImg; + BYTE bBright, bText; +}; + +#define RGBA D3DCOLOR_RGBA + +// Same order as the 5960 Material_Init. EggGlow lists what its constructor is +// handed; the constructor shifts them one slot, same as retail. +static const RetailMat g_rgRetailMat[] = +{ + { _T("InnerWall_01"), MK_INNERWALL, 0, RGBA(243, 255, 107, 255), RGBA(40, 212, 20, 20), 0, 0, 0, 0, 0, 0 }, + { _T("InnerWall_02"), MK_FALLOFF, MATINFO_CULL_NONE, RGBA(243, 255, 107, 255), RGBA(20, 192, 0, 20), 0, 0, 0, 0, 0, 0 }, + { _T("MetaInfo_Text"), MK_STANDARD, MATINFO_STANDARD_MATERIAL, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("NamePanel_Text"), MK_STANDARD, MATINFO_STANDARD_MATERIAL, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("GameNameText_01"), MK_STANDARD, MATINFO_STANDARD_MATERIAL, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("GameNameText_02"), MK_STANDARD, MATINFO_STANDARD_MATERIAL, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("GameNameText_03"), MK_STANDARD, MATINFO_STANDARD_MATERIAL, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("GameIcon_01"), MK_STANDARD, MATINFO_STANDARD_MATERIAL, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("GameIcon_03"), MK_STANDARD, MATINFO_STANDARD_MATERIAL, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("Material #132"), MK_STANDARD, MATINFO_STANDARD_MATERIAL, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("u2 info"), MK_STANDARD, MATINFO_STANDARD_MATERIAL, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("Material #1334"), MK_SOLID, 0, RGBA(11, 32, 0, 192), 0, 0, 0, 0, 0, 0, 0 }, + { _T("XBOXgreendark"), MK_SOLID, 0, RGBA(6, 33, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("XBOXgreen"), MK_SOLID, 0, RGBA(140, 201, 25, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("XBoxGreen2"), MK_SOLID, 0, RGBA(139, 200, 24, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("GameHilite33"), MK_SOLID, 0, RGBA(221, 208, 120, 178), 0, 0, 0, 0, 0, 0, 0 }, + { _T("Nothing"), MK_SOLID, 0, RGBA(128, 128, 128, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("NavType"), MK_SOLID, 0, RGBA(190, 250, 94, 178), 0, 0, 0, 0, 0, 0, 0 }, + { _T("OrangeNavType"), MK_SOLID, 0, RGBA(249, 152, 25, 178), 0, 0, 0, 0, 0, 0, 0 }, + { _T("RedType"), MK_SOLID, 0, RGBA(200, 30, 30, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("XBoxGreen"), MK_SOLID, 0, RGBA(139, 200, 24, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("Type"), MK_SOLID, 0, RGBA(100, 200, 25, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("Typesdsafsda"), MK_SOLID, 0, RGBA(255, 255, 255, 179), 0, 0, 0, 0, 0, 0, 0 }, + { _T("Material #133"), MK_SOLID, 0, RGBA(0, 0, 0, 192), 0, 0, 0, 0, 0, 0, 0 }, + { _T("Material #1335"), MK_SOLID, 0, RGBA(76, 162, 0, 200), 0, 0, 0, 0, 0, 0, 0 }, + { _T("Material #133511"), MK_SOLID, 0, RGBA(41, 87, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("Material #1336"), MK_SOLID, 0, RGBA(0, 0, 0, 128), 0, 0, 0, 0, 0, 0, 0 }, + { _T("HilightedType"), MK_SOLID, 0, RGBA(3, 44, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("XBoxGreenq"), MK_SOLID, 0, RGBA(139, 200, 24, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("Black80"), MK_SOLID, 0, RGBA(0, 0, 0, 204), 0, 0, 0, 0, 0, 0, 0 }, + { _T("Black"), MK_SOLID, 0, RGBA(0, 0, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("CellEgg/Partsw"), MK_SOLID, MATINFO_CULL_NONE, RGBA(77, 224, 57, 166), 0, 0, 0, 0, 0, 0, 0 }, + { _T("CellEgg/Partsz"), MK_SOLID, MATINFO_CULL_NONE, RGBA(170, 170, 170, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("Material #108"), MK_SOLID, MATINFO_CULL_NONE, RGBA(160, 252, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("ItemsType"), MK_SOLID, MATINFO_CULL_NONE, RGBA(182, 245, 96, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("GameHiliteMemory"), MK_SOLID, 0, RGBA(178, 208, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("red"), MK_SOLID, 0, RGBA(255, 0, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("white"), MK_SOLID, 0, RGBA(255, 255, 255, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("solid green 1"), MK_SOLID, 0, RGBA(17, 255, 34, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("solid green2"), MK_SOLID, 0, RGBA(18, 193, 10, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("solid green 3"), MK_SOLID, 0, RGBA(10, 117, 28, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("solid green 4"), MK_SOLID, 0, RGBA(18, 55, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("dark green panels"), MK_SOLID, 0, RGBA(9, 41, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("FlatSurfaces"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("FlatSurfacesSelected"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(30, 255, 0, 128), 0, 0, 0, 0, 0, 0 }, + { _T("FlatSurfacesMemory"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(30, 160, 0, 128), 0, 0, 0, 0, 0, 0 }, + { _T("DarkSurfaces"), MK_FALLOFF, 0, RGBA(203, 205, 85, 90), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("DarkSurfaces2"), MK_FALLOFF, 0, RGBA(203, 205, 85, 90), RGBA(10, 100, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("FlatSurfaces2sided"), MK_FALLOFF, MATINFO_CULL_NONE, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("DetailLegSkin_Inner"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("Screen"), MK_FALLOFF, MATINFO_CULL_NONE, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("Spout"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("NavType34"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("MetaFlatSurfaces"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("SC_SavedGame_Row01"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("HK_SavedGame_Row01"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("SavedEgg_Selected"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("FlatUnselected"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("Mem_InnerWall_Outer"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("SavedGameEgg"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("GameMenuEgg"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("Shell"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("Material #133sdsfdsf"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("IconParts"), MK_FALLOFF, MATINFO_CULL_NONE, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("IconParts1"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("MU1Pod_HL"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("MU1Pod"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("GamePodb"), MK_FALLOFF, MATINFO_CULL_NONE, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("GamePod"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("JwlSrfc01/InfoPnls"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("MenuCell"), MK_FALLOFF, 0, RGBA(243, 255, 107, 192), RGBA(20, 192, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("dark green panels falloff"), MK_FALLOFF, 0, RGBA(0, 0, 0, 0), RGBA(5, 35, 5, 255), 0, 0, 0, 0, 0, 0 }, + { _T("grey"), MK_FALLOFF, 0, RGBA(71, 83, 69, 0), RGBA(86, 100, 82, 255), 0, 0, 0, 0, 0, 0 }, + { _T("grill grey"), MK_FALLOFF, 0, RGBA(32, 32, 32, 0), RGBA(64, 64, 64, 255), 0, 0, 0, 0, 0, 0 }, + { _T("Wireframe"), MK_FALLOFFTEX, 0, RGBA(125, 198, 34, 100), RGBA(125, 198, 34, 0), 0, 0, 0, 0, 0, 0 }, + { _T("Tubes"), MK_FALLOFF, MATINFO_CULL_NONE, RGBA(242, 250, 153, 215), RGBA(7, 104, 0, 37), 0, 0, 0, 0, 0, 0 }, + { _T("JewelSurface02/PodMesh"), MK_FALLOFF, 0, RGBA(242, 250, 153, 215), RGBA(7, 104, 0, 37), 0, 0, 0, 0, 0, 0 }, + { _T("TubesFade"), MK_FALLOFF, MATINFO_CULL_NONE, RGBA(242, 250, 153, 215), RGBA(7, 104, 0, 37), 0, 0, 0, 0, 0, 0 }, + { _T("TubesQ"), MK_FALLOFF, 0, RGBA(242, 250, 153, 215), RGBA(7, 104, 0, 37), 0, 0, 0, 0, 0, 0 }, + { _T("EmptyMU"), MK_FALLOFF, 0, RGBA(242, 250, 153, 215), RGBA(7, 104, 0, 37), 0, 0, 0, 0, 0, 0 }, + { _T("Tube"), MK_FALLOFF, 0, RGBA(242, 250, 153, 215), RGBA(7, 104, 0, 37), 0, 0, 0, 0, 0, 0 }, + { _T("MemoryHeader"), MK_FALLOFF, MATINFO_CULL_NONE, RGBA(60, 198, 67, 122), RGBA(7, 104, 0, 37), 0, 0, 0, 0, 0, 0 }, + { _T("MemoryHeaderHilite"), MK_FALLOFF, MATINFO_CULL_NONE, RGBA(199, 232, 0, 240), RGBA(97, 114, 0, 130), 0, 0, 0, 0, 0, 0 }, + { _T("EggGlow"), MK_EGGGLOW, 0, RGBA(252, 255, 0, 0), RGBA(254, 255, 188, 228), 0, 0, 0, 0, 0, 0 }, + { _T("ButtonGlow"), MK_FALLOFF, 0, RGBA(252, 255, 0, 0), RGBA(254, 255, 188, 228), 0, 0, 0, 0, 0, 0 }, + { _T("gradient"), MK_FALLOFF, 0, RGBA(191, 255, 107, 51), RGBA(0, 255, 18, 0), 0, 0, 0, 0, 0, 0 }, + { _T("CellWallStructure"), MK_FALLOFF, 0, RGBA(191, 255, 107, 51), RGBA(0, 255, 18, 0), 0, 0, 0, 0, 0, 0 }, + { _T("FlatSrfc/PodParts"), MK_FALLOFF, 0, RGBA(191, 255, 107, 51), RGBA(0, 255, 18, 0), 0, 0, 0, 0, 0, 0 }, + { _T("Cell_Light"), MK_FALLOFF, 0, RGBA(191, 255, 107, 51), RGBA(0, 255, 18, 0), 0, 0, 0, 0, 0, 0 }, + { _T("Cell_Light/LegSkin"), MK_FALLOFF, 0, RGBA(191, 255, 107, 51), RGBA(0, 255, 18, 0), 0, 0, 0, 0, 0, 0 }, + { _T("CellEgg/Parts"), MK_FALLOFF, 0, RGBA(243, 255, 107, 178), RGBA(30, 255, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("GameEgg"), MK_FALLOFF, 0, RGBA(243, 255, 107, 178), RGBA(30, 255, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("FlatSurfaces2sided3"), MK_FALLOFF, MATINFO_CULL_NONE, RGBA(253, 30, 0, 255), RGBA(242, 28, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("console_hilite"), MK_FALLOFF, 0, RGBA(255, 173, 107, 255), RGBA(246, 255, 0, 0), 0, 0, 0, 0, 0, 0 }, + { _T("TitleIcon"), MK_ICON, 0, 0, 0, 128, 0, 0, 0, 0, 0 }, + { _T("TitleSoundtrackIcon"), MK_ICON, 0, 0, 0, 128, 0, 1, 0, 0, 0 }, + { _T("SavedGameIcon"), MK_ICON, 0, 0, 0, 64, 1, 0, 0, 0, 0 }, + { _T("SoundtrackIcon"), MK_ICON, 0, 0, 0, 64, 1, 1, 0, 0, 0 }, + { _T("MotdIcon"), MK_ICON, 0, 0, 0, 64, 1, 0, 0, 0, 0 }, + { _T("XboxLiveGameTitleOrbIcon"), MK_ICON, 0, 0, 0, 128, 0, 1, 0, 0, 0 }, + { _T("XboxLiveAccountIcon"), MK_ICON, 0, 0, 0, 64, 1, 1, 0, 0, 0 }, + { _T("SelectedIcon"), MK_ICON, 0, 0, 0, 128, 0, 0, 1, 0, 0 }, + { _T("Key"), MK_KEY, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("BrightKey"), MK_KEY, 0, 0, 0, 0, 0, 0, 0, 1, 0 }, + { _T("KeyText"), MK_KEY, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, + { _T("EggGlowPulse"), MK_EGGGLOWPULSE, 0, RGBA(178, 208, 0, 0), RGBA(255, 255, 128, 255), 0, 0, 0, 0, 0, 0 }, + { _T("Metal_Chrome"), MK_ANISO, 0, RGBA(229, 229, 229, 0), RGBA(229, 229, 229, 255), 0, 0, 0, 0, 0, 0 }, + { _T("Tvbox"), MK_ANISO, 0, RGBA(229, 229, 229, 0), RGBA(229, 229, 229, 255), 0, 0, 0, 0, 0, 0 }, + { _T("PanelBacking_01"), MK_BACKING, 0, RGBA(4, 20, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("PanelBacking_02"), MK_BACKING, 0, RGBA(0, 0, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("PanelBacking_03"), MK_BACKING, 0, RGBA(4, 20, 0, 240), 0, 0, 0, 0, 0, 0, 0 }, + { _T("PanelBacking_04"), MK_BACKING, 0, RGBA(14, 46, 7, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("DarkenBacking"), MK_BACKING, MATINFO_CULL_NONE | MATINFO_RADIAL_EDGE_ALPHA, RGBA(4, 50, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("TextBacking"), MK_BACKING, MATINFO_CULL_NONE | MATINFO_RADIAL_ALPHA, RGBA(0, 0, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("button"), MK_MODULATE, MATINFO_CULL_NONE, RGBA(157, 109, 194, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("image"), MK_MODULATE, MATINFO_CULL_NONE, RGBA(4, 20, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("live header"), MK_MODULATE, MATINFO_CULL_NONE, RGBA(4, 20, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("highlight"), MK_MODULATE, MATINFO_CULL_NONE, RGBA(157, 109, 194, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("footer"), MK_MODULATE, MATINFO_CULL_NONE, RGBA(157, 109, 194, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("LiveChrome"), MK_MODULATE, MATINFO_CULL_NONE, RGBA(157, 109, 194, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("orangeEggGlow"), MK_SOLID, 0, RGBA(254, 255, 188, 255), 0, 0, 0, 0, 0, 0, 0 }, // textured in 5960, not traced yet + { _T("GameHilite"), MK_MODULATE, MATINFO_CULL_NONE, RGBA(255, 255, 255, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("equalizer"), MK_MASK, MATINFO_RADIAL_AV_ALPHA, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("MainMenuOrb"), MK_MASK, MATINFO_RADIAL_EDGE_MAIN_ALPHA, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("ReflectSurface"), MK_REFLECT, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { _T("PanelBacking"), MK_MODULATE, MATINFO_CULL_NONE, RGBA(4, 20, 0, 255), 0, 0, 0, 0, 0, 0, 0 }, + { _T("Material #10822"), MK_FREESPACE, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, +}; + +// The Keyboard section, shared by Key, BrightKey and KeyText: Live side, side, +// bright front, text. +static const D3DCOLOR g_rgRetailKey[4] = +{ + RGBA(178, 208, 0, 255), + RGBA(243, 255, 107, 255), + RGBA(25, 225, 0, 34), + RGBA(190, 250, 94, 0), +}; + +#undef RGBA + +#define MAT_R(c) ((BYTE)((c) >> 16)) +#define MAT_G(c) ((BYTE)((c) >> 8)) +#define MAT_B(c) ((BYTE)(c)) +#define MAT_A(c) ((BYTE)((c) >> 24)) + +static CustomColor ToCustomColor(D3DCOLOR c) +{ + CustomColor cc; + cc.r = MAT_R(c); + cc.g = MAT_G(c); + cc.b = MAT_B(c); + cc.a = MAT_A(c); + return cc; +} + +// "r, g, b, a" from the skin; leaves *pColor alone if the skin doesn't have it +static bool SkinColor(CSettingsFile *pSkin, const TCHAR *szSection, const TCHAR *szKey, D3DCOLOR *pColor) +{ + TCHAR buf[MAX_PATH]; + if (pSkin == NULL || !pSkin->GetValue(szSection, szKey, buf, MAX_PATH)) + return false; + + int r, g, b, a = 0; + if (_stscanf(buf, _T("%d, %d, %d, %d"), &r, &g, &b, &a) < 3) + return false; + + *pColor = D3DCOLOR_RGBA(r, g, b, a); + return true; +} + +static void ResolveColors(CSettingsFile *pSkin, const RetailMat &m, D3DCOLOR *pA, D3DCOLOR *pB) +{ + *pA = m.colorA; + *pB = m.colorB; + + switch (m.kind) + { + case MK_SOLID: + case MK_BACKING: + case MK_MODULATE: + SkinColor(pSkin, m.szName, _T("Color"), pA); + break; + + case MK_FALLOFF: + case MK_FALLOFFTEX: + case MK_INNERWALL: + case MK_ANISO: + case MK_EGGGLOW: + case MK_EGGGLOWPULSE: + SkinColor(pSkin, m.szName, _T("ColorA"), pA); + SkinColor(pSkin, m.szName, _T("ColorB"), pB); + break; + + default: + break; + } +} + +static void ResolveKeyColors(CSettingsFile *pSkin, CustomColor rgColor[4]) +{ + static const TCHAR *rgszKey[4] = { _T("ColorA"), _T("ColorB"), _T("ColorC"), _T("ColorD") }; + for (int i = 0; i < 4; i++) + { + D3DCOLOR c = g_rgRetailKey[i]; + SkinColor(pSkin, _T("Keyboard"), rgszKey[i], &c); + rgColor[i] = ToCustomColor(c); + } +} + +static void CreateMat(const RetailMat &m, D3DCOLOR a, D3DCOLOR b, const CustomColor rgKey[4]) +{ + switch (m.kind) + { + case MK_STANDARD: new CMatInfo(m.szName, m.dwFlags); break; + case MK_SOLID: new CSolidMatInfo(m.szName, MAT_R(a), MAT_G(a), MAT_B(a), MAT_A(a), m.dwFlags); break; + case MK_BACKING: new CBackingMatInfo(m.szName, MAT_R(a), MAT_G(a), MAT_B(a), MAT_A(a), m.dwFlags); break; + case MK_MODULATE: new CModulateTextureMatInfo(m.szName, MAT_R(a), MAT_G(a), MAT_B(a), MAT_A(a), m.dwFlags); break; + case MK_FALLOFF: new CFalloffMatInfo(m.szName, a, b, m.dwFlags); break; + case MK_FALLOFFTEX: new CFalloffTexInfo(m.szName, a, b, m.dwFlags); break; + case MK_INNERWALL: new CInnerWallMatInfo(m.szName, a, b, m.dwFlags); break; + case MK_ANISO: new CAnisoMatInfo(m.szName, a, b, m.dwFlags); break; + case MK_EGGGLOW: new CEggGlowFadeMatInfo(m.szName, a, ToCustomColor(b), m.dwFlags); break; + case MK_EGGGLOWPULSE: new CEggGlowPulseMatInfo(m.szName, ToCustomColor(a), ToCustomColor(b)); break; + case MK_KEY: new CKeyMatInfo(m.szName, m.bBright != 0, m.bText != 0, rgKey[0], rgKey[1], rgKey[2], rgKey[3]); break; + case MK_ICON: new CIconMatInfo(m.szName, m.nIconSize, m.nIconSize, m.bFadeIn != 0, m.bInXIP != 0, m.bSelImg != 0); break; + case MK_MASK: new CMaskTextureMatInfo(m.szName, m.dwFlags); break; + case MK_REFLECT: new CReflectMatInfo(m.szName); break; + case MK_FREESPACE: new CFreeSpaceMatInfo(m.szName); break; + } +} + +// Skin reload: recolour in place, same colours CreateMat would have used +static void RecolourMat(CMatInfo *p, const RetailMat &m, D3DCOLOR a, D3DCOLOR b, const CustomColor rgKey[4]) +{ + switch (m.kind) + { + case MK_SOLID: + { + CSolidMatInfo *s = static_cast(p); + s->m_r = MAT_R(a); s->m_g = MAT_G(a); s->m_b = MAT_B(a); s->m_a = MAT_A(a); + } + break; + + case MK_BACKING: + { + CBackingMatInfo *s = static_cast(p); + s->m_r = MAT_R(a); s->m_g = MAT_G(a); s->m_b = MAT_B(a); s->m_a = MAT_A(a); + } + break; + + case MK_MODULATE: + { + CModulateTextureMatInfo *s = static_cast(p); + s->m_r = MAT_R(a); s->m_g = MAT_G(a); s->m_b = MAT_B(a); s->m_a = MAT_A(a); + } + break; + + case MK_FALLOFF: + static_cast(p)->m_colorSide = a; + static_cast(p)->m_colorFront = b; + break; + + case MK_FALLOFFTEX: + static_cast(p)->m_colorSide = a; + static_cast(p)->m_colorFront = b; + break; + + case MK_INNERWALL: + static_cast(p)->m_colorSide = a; + static_cast(p)->m_colorFront = b; + break; + + case MK_ANISO: + static_cast(p)->m_colorSide = a; + static_cast(p)->m_colorFront = b; + break; + + case MK_EGGGLOW: + { + // mirrors the shifted constructor: a lands in front, b in the flags + CEggGlowFadeMatInfo *e = static_cast(p); + e->m_colorSide = m.dwFlags; + e->m_colorFront = a; + e->m_flags = b; + e->Color1 = ToCustomColor(b); + } + break; + + case MK_EGGGLOWPULSE: + static_cast(p)->Color1 = ToCustomColor(a); + static_cast(p)->Color2 = ToCustomColor(b); + break; + + case MK_KEY: + { + CKeyMatInfo *k = static_cast(p); + k->Color1 = rgKey[0]; + k->Color2 = rgKey[1]; + k->Color3 = rgKey[2]; + k->Color4 = rgKey[3]; + } + break; + + default: + break; + } +} + +static void Material_Reload() +{ + CSettingsFile *pSkin = TheseusGetSkinSettings(); + CustomColor rgKey[4]; + ResolveKeyColors(pSkin, rgKey); + + for (int i = 0; i < (int)(sizeof(g_rgRetailMat) / sizeof(g_rgRetailMat[0])); i++) + { + const RetailMat &m = g_rgRetailMat[i]; + CMatInfo *p = LookupMatInfo(m.szName); + if (p == NULL) + continue; + + D3DCOLOR a, b; + ResolveColors(pSkin, m, &a, &b); + RecolourMat(p, m, a, b, rgKey); + } +} + +void Material_Init(bool bReloadSkinXBX) +{ + if (bReloadSkinXBX) + { + Material_Reload(); + return; + } + + CSettingsFile *pSkin = TheseusGetSkinSettings(); + CustomColor rgKey[4]; + ResolveKeyColors(pSkin, rgKey); + + for (int i = 0; i < (int)(sizeof(g_rgRetailMat) / sizeof(g_rgRetailMat[0])); i++) + { + const RetailMat &m = g_rgRetailMat[i]; + D3DCOLOR a, b; + ResolveColors(pSkin, m, &a, &b); + CreateMat(m, a, b, rgKey); + } + + qsort(g_rgMatInfo, g_nMatInfoCount, sizeof(CMatInfo *), SortMatInfoCompare); +} + +#undef MAT_R +#undef MAT_G +#undef MAT_B +#undef MAT_A From d62e7aa3c2a685b37e062fe2ea2fe409fd11b76a Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:37:55 -0500 Subject: [PATCH 008/167] Xbox hardening pass, plus the OXDK makefile A lot of bounds checks were ASSERTs, which are gone in retail. Fixed the ones that matter: XIP and XPR loading, the skin path from Config.ini, FormatMemoryUnit on the hard drive slot, save metadata parsing, the script stack and settings writes per key. The panic screen now says what to do next. Booted and launched games on hardware once; not everything is covered. --- .gitignore | 3 + build/Makefile.xbox | 217 +++++++++++++++++++++++++++++++ build/xbe_identity.py | 71 ++++++++++ build/xbox-sources.mk | 90 +++++++++++++ theseus/desktop/xip.cpp | 55 ++++++-- theseus/render/text.cpp | 14 ++ theseus/shared/savegame_grid.cpp | 16 ++- theseus/shared/settingsfile.cpp | 4 +- theseus/shared/settingsfile.h | 7 + theseus/shared/xap_vm.cpp | 7 +- theseus/xbox/disc_management.cpp | 2 +- theseus/xbox/main.cpp | 29 ++++- theseus/xbox/memory_monitor.cpp | 14 +- theseus/xbox/panic_screen.cpp | 20 ++- theseus/xbox/panic_screen.h | 5 +- theseus/xbox/title_scanner.cpp | 8 ++ theseus/xbox/xip_archive.cpp | 165 ++++++++++++++++++++--- 17 files changed, 674 insertions(+), 53 deletions(-) create mode 100644 build/Makefile.xbox create mode 100644 build/xbe_identity.py create mode 100644 build/xbox-sources.mk diff --git a/.gitignore b/.gitignore index b76b5f0..9d678c9 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,6 @@ theseus/third-party/bgfx/.build/ # xiptool writes .xip next to the source folder on create. The packed # artifact belongs in Data/Xips/, not here. xips-source/*.xip + +# Machine-local build settings (paths to retail XBEs) +build/local.mk diff --git a/build/Makefile.xbox b/build/Makefile.xbox new file mode 100644 index 0000000..95a5177 --- /dev/null +++ b/build/Makefile.xbox @@ -0,0 +1,217 @@ +# Theseus, original Xbox build. +# +# The toolchain lives in OXDK. This file says what Theseus is made of and +# where its own quirks are; everything about how to drive clang, lld-link and +# cxbe for an Xbox target comes from $(OXDK_DIR)/oxdk.mk. Fixes to OXDK's +# flags reach Theseus by updating OXDK, not by hand-editing a second copy. +# +# Not invoked directly, normally. `make debug` / `make retail` in build/ +# delegate here. Driving it by hand works too: +# +# make -f Makefile.xbox CONFIG=retail +# +# Requires: clang + lld-link on PATH, OXDK cloned, and an XDK tree at +# $(XDK_DIR) (headers in include/, libs in lib/). cxbe builds itself. + +THESEUS_SRC ?= ../theseus +BUILDS_ROOT ?= $(HOME)/builds/theseus +CONFIG ?= debug + +# ------------------------------------------------------------------ +# OXDK +# ------------------------------------------------------------------ +# Prefer a vendored copy so a fresh clone needs nothing extra, then a +# sibling checkout, then the conventional ~/OXDK. +OXDK_DIR ?= $(firstword $(wildcard $(THESEUS_SRC)/third-party/oxdk/oxdk.mk \ + ../OXDK/oxdk.mk \ + $(HOME)/OXDK/oxdk.mk)) +OXDK_DIR := $(patsubst %/oxdk.mk,%,$(OXDK_DIR)) + +ifeq ($(strip $(OXDK_DIR)),) +$(error OXDK not found. Clone it next to Theseus or into $$HOME: \ +git clone https://github.com/MrMilenko/OXDK ~/OXDK \ +-- or set OXDK_DIR=/path/to/OXDK. cxbe builds itself from there.) +endif + +# OXDK reads headers from $(XDK_DIR)/include and libs from $(XDK_DIR)/lib, +# which is the layout theseus/xdk already has. +XDK_DIR ?= $(THESEUS_SRC)/xdk + +ifeq ($(wildcard $(XDK_DIR)/include),) +$(error No XDK at $(XDK_DIR). theseus/xdk is a gitignored symlink to a local \ +XDK tree with include/ and lib/ -- point it at yours, or pass XDK_DIR=/path) +endif + +# ------------------------------------------------------------------ +# Config +# ------------------------------------------------------------------ +ifeq ($(CONFIG),retail) + OUTPUT_DIR := $(BUILDS_ROOT)/xbox-retail + XBE_MODE := RETAIL + # Release libs. no debug asserts, no XBDM dependency. + XGRAPHICS_LIB := xgraphics.lib + XAPILIB := xapilib.lib + XNET_LIB := xnet.lib + XONLINE_LIB := xonline.lib + CRT_LIB := libcmt.lib + CRTCPP_LIB := libcpmt.lib + XBDM_LIB := + OXDK_OPT := -O0 +else + OUTPUT_DIR := $(BUILDS_ROOT)/xbox-debug + XBE_MODE := DEBUG + # Debug libs, except dsound: debug dsound asserts without a debugger attached. + XGRAPHICS_LIB := xgraphicsd.lib + XAPILIB := xapilibd.lib + XNET_LIB := xnetd.lib + XONLINE_LIB := xonlined.lib + CRT_LIB := libcmtd.lib + CRTCPP_LIB := libcpmtd.lib + XBDM_LIB := xbdm.lib + OXDK_OPT := -O0 -g +endif + +# OXDK defaults to -O2, which is right for a 733MHz console but wrong here: +# clang's optimizer miscompiles this codebase for Xbox and the retail XBE +# crashes on hardware. Both configs stay at -O0 until that is tracked down. +# Set OXDK_OPT on the command line to experiment. + +XBE_TITLE := theseus +XBE_NAME := Theseus + +# ------------------------------------------------------------------ +# Sources +# ------------------------------------------------------------------ +include xbox-sources.mk + +# d3d_compat.cpp lives beside this Makefile, not under theseus/. +SRCS := $(addprefix $(THESEUS_SRC)/,$(ALL_SRCS)) d3d_compat.cpp + +# theseuslib: the private-API helpers and the xiso/xip parsers, built as a +# static lib so outside consumers can link it on its own. +THESEUSLIB_DIR := ../theseuslib +THESEUSLIB_SRCS := theseuslib.c xiso.c xip_archive.c +THESEUSLIB_OBJS := $(addprefix $(OUTPUT_DIR)/theseuslib/,$(THESEUSLIB_SRCS:.c=.obj)) +THESEUSLIB_DEPS := $(THESEUSLIB_OBJS:.obj=.d) +THESEUSLIB_LIB := $(OUTPUT_DIR)/theseuslib.lib + +# ------------------------------------------------------------------ +# Theseus-specific flags, layered on top of OXDK's +# ------------------------------------------------------------------ +# -MMD -MP: OXDK does not track header deps, and we want incremental builds. +# xdk_compat.h: Theseus's own shim. It maps the public XDK headers onto the +# NT types this code expects, which is why no private XDK tree is required. +THESEUS_XBOX_FLAGS = \ + -MMD -MP \ + -D_UNICODE \ + -DUNICODE \ + -D_LIGHTS \ + -D_MAC_CROSS_COMPILE \ + -include xdk_compat.h \ + -I. \ + -iquote $(THESEUS_SRC)/xbox \ + -iquote $(THESEUS_SRC) \ + -iquote $(THESEUS_SRC)/engine \ + -iquote $(THESEUS_SRC)/shared \ + -iquote $(THESEUS_SRC)/render \ + -I$(THESEUS_SRC)/xbox/compat + +# Machine-local settings, never committed. Set DASH_IDENTITY_XBE there to a +# retail dashboard XBE and the built XBE takes its certificate identity (see +# xbe_identity.py). Without it the XBE keeps cxbe's defaults. +-include local.mk + +# Experiment switches from the command line, e.g. EXTRA_DEFINES=-DTHESEUS_PUSHBUFFER_KB=2048 +THESEUS_XBOX_FLAGS += $(EXTRA_DEFINES) + +CFLAGS += $(THESEUS_XBOX_FLAGS) +# OXDK leaves RTTI on because libcmt can support it. Theseus does not use +# dynamic_cast or typeid and the dashboard has no room for the tables. +CXXFLAGS += $(THESEUS_XBOX_FLAGS) -fno-rtti + +# Dashboard-only imports, on top of the kernel set OXDK already declares. +LDFLAGS += \ + /map:$(OUTPUT_DIR)/$(XBE_TITLE).map \ + /alternatename:__imp__KeDelayExecutionThread=__imp__KeDelayExecutionThread@12 \ + /alternatename:_ExReadWriteRefurbInfo=_ExReadWriteRefurbInfo@12 \ + /alternatename:_MU_CreateDeviceObject=_MU_CreateDeviceObject@12 \ + /alternatename:_MU_CloseDeviceObject=_MU_CloseDeviceObject@8 \ + /alternatename:_XcHMAC=_XcHMAC@28 \ + /alternatename:___CxxFrameHandler3=___CxxFrameHandler \ + /alternatename:_XapiDeleteCachePartition=_XapiDeleteCachePartition@4 \ + /alternatename:?vswprintf@@YAHPA_WIPB_WPAD@Z=_vswprintf \ + /alternatename:_mp3dec_init@4=_mp3dec_init \ + /alternatename:_mp3dec_decode_frame@20=_mp3dec_decode_frame \ + /alternatename:_mp3_decode_full@20=_mp3_decode_full + +# Full lib list, not OXDK's default: the config split above picks debug or +# release variants, and xapilib.lib follows $(XAPILIB) deliberately so the +# debug build still resolves the retail-only entry points. +OXDK_LIBS := \ + xboxkrnl.lib \ + $(XAPILIB) \ + xapilib.lib \ + d3d8.lib \ + d3dx8.lib \ + $(XGRAPHICS_LIB) \ + dsound.lib \ + $(XNET_LIB) \ + $(XONLINE_LIB) \ + $(CRT_LIB) \ + $(CRTCPP_LIB) \ + $(XBDM_LIB) + +LIBS += $(THESEUSLIB_LIB) + +# ------------------------------------------------------------------ +# OXDK supplies the rules from here down +# ------------------------------------------------------------------ +include $(OXDK_DIR)/oxdk.mk + +# The XBE cannot link until theseuslib.lib exists. Prerequisite-only rule: +# OXDK owns the recipe. +$(OUTPUT_DIR)/$(XBE_TITLE).exe: $(THESEUSLIB_LIB) + +$(THESEUSLIB_LIB): $(THESEUSLIB_OBJS) + $(LLD_LINK) /lib /nologo /out:$@ $^ + +# theseuslib's own headers are included with angle brackets, so they need a +# real -I. The -iquote paths above deliberately do not cover them, which is +# what keeps its from resolving to theseus/engine's. +$(OUTPUT_DIR)/theseuslib/%.obj: $(THESEUSLIB_DIR)/src/%.c | $(OUTPUT_DIR)/theseuslib + $(CLANG) $(OXDK_CFLAGS) $(CFLAGS) -I$(THESEUSLIB_DIR)/include -o $@ $< + +$(OUTPUT_DIR)/theseuslib: + @mkdir -p $@ + +# minimp3 compiles in isolation: no XDK headers, no xdk_compat.h force-include, +# and no stdcall default. It only wants the CRT headers. Overriding both flag +# variables for this one object reproduces that, and it is why the +# _mp3dec_*@N alternatenames above exist -- its symbols stay cdecl while every +# caller is stdcall. +MINIMP3_OBJ := $(call oxdk_obj,$(THESEUS_SRC)/engine/minimp3_decode.cpp) +$(MINIMP3_OBJ): OXDK_CXXFLAGS := $(OXDK_TARGET_FLAGS) -c $(OXDK_OPT) +$(MINIMP3_OBJ): CXXFLAGS := -MMD -MP -fno-rtti \ + -I$(THESEUS_SRC)/engine -I$(XDK_DIR)/include -Wno-typedef-redefinition + +# Experiment: HOT_O2=1 builds only the per-draw render files at -O2 and leaves +# the rest at -O0. If it boots and holds, the -O2 crash lives elsewhere; use a +# separate BUILDS_ROOT so it never mixes with the -O0 objects. +ifdef HOT_O2 +HOT_O2_OBJS := $(foreach f,render/text.cpp render/widget_draw.cpp render/asset_loader.cpp \ + render/shape_render.cpp xbox/xip_archive.cpp,$(call oxdk_obj,$(THESEUS_SRC)/$(f))) +$(HOT_O2_OBJS): OXDK_OPT := -O2 +endif + +# Present as the Xbox Dashboard: same title ID, version and keys as the +# retail XBE in DASH_IDENTITY_XBE. Re-applied whenever default.xbe is rebuilt. +ifneq ($(strip $(DASH_IDENTITY_XBE)),) +IDENTITY_STAMP := $(OUTPUT_DIR)/.identity-applied +all: $(IDENTITY_STAMP) +$(IDENTITY_STAMP): $(OUTPUT_DIR)/default.xbe $(DASH_IDENTITY_XBE) xbe_identity.py + python3 xbe_identity.py --from "$(DASH_IDENTITY_XBE)" --to $< + @touch $@ +endif + +-include $(THESEUSLIB_DEPS) +-include $(OBJS:.obj=.d) diff --git a/build/xbe_identity.py b/build/xbe_identity.py new file mode 100644 index 0000000..5875470 --- /dev/null +++ b/build/xbe_identity.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Give a built XBE the certificate identity of a retail dashboard XBE. + +The online library logs on as the running XBE's certificate, and T:\\ / U:\\ +follow its title ID, so a replacement dashboard has to present the same +identity as the one it replaces. This copies every identity field from the +retail XBE you point it at into the target, in place. No keys live here; +they're read from your own retail XBE at build time. Keys are never printed. +""" + +import argparse +import struct +import sys + +# Certificate field offsets (see cxbe's Xbe.h). +FIELDS = [ + ("title id", 0x008, 4), + ("title name", 0x00C, 0x50), + ("alternate title ids", 0x05C, 0x40), + ("allowed media", 0x09C, 4), + ("game region", 0x0A0, 4), + ("game ratings", 0x0A4, 4), + ("disk number", 0x0A8, 4), + ("version", 0x0AC, 4), + ("lan key", 0x0B0, 16), + ("signature key", 0x0C0, 16), + ("alternate signature keys", 0x0D0, 0x100), +] +CERT_MIN = 0x1D0 + + +def cert_offset(buf, what): + if buf[:4] != b"XBEH": + sys.exit(f"{what}: not an XBE") + base, = struct.unpack_from(" {describe(dst, do)}, keys copied") + + +if __name__ == "__main__": + main() diff --git a/build/xbox-sources.mk b/build/xbox-sources.mk new file mode 100644 index 0000000..3f64544 --- /dev/null +++ b/build/xbox-sources.mk @@ -0,0 +1,90 @@ +# Xbox source lists. Shared by build/Makefile and build/Makefile.xbox so the +# two never drift. Paths are relative to $(THESEUS_SRC). + +SRCS_ENGINE = \ + engine/array.cpp \ + engine/debug.cpp \ + engine/dotfield_node.cpp \ + engine/locale_node.cpp \ + engine/math_node.cpp \ + engine/settings_nodes.cpp \ + engine/xap_compile.cpp \ + engine/minimp3_decode.cpp + +# Shared sources compiled for BOTH Xbox and Desktop +SRCS_SHARED = \ + shared/activefile.cpp \ + shared/date_node.cpp \ + shared/file_ops.cpp \ + shared/file_util.cpp \ + shared/keyboard_node.cpp \ + shared/music_collection.cpp \ + shared/ntiosvc.cpp \ + shared/overlay_alert.cpp \ + shared/recovery_node.cpp \ + shared/savegame_grid.cpp \ + shared/screen.cpp \ + shared/settingsfile.cpp \ + shared/string_util.cpp \ + shared/timezone.cpp \ + shared/title_collection.cpp \ + shared/util.cpp \ + shared/frame_profiler.cpp \ + shared/xap_vm.cpp \ + shared/xbe.cpp + +# Xbox-only shared files. Desktop provides replacements or stubs. +SRCS_SHARED_XBOX_ONLY = \ + shared/audio_system.cpp \ + shared/discord.cpp \ + shared/harddrive.cpp + +SRCS_RENDER = \ + render/animation_nodes.cpp \ + render/asset_loader.cpp \ + render/autosmooth.cpp \ + render/camera.cpp \ + render/hud_node.cpp \ + render/materials.cpp \ + render/node_system.cpp \ + render/scene_groups.cpp \ + render/shape_render.cpp \ + render/starfield.cpp \ + render/text.cpp \ + render/tmap_system.cpp \ + render/widget_draw.cpp \ + render/widget_layer.cpp + +SRCS_XBOX = \ + xbox/config.cpp \ + xbox/disc_management.cpp \ + xbox/dsound_manager.cpp \ + xbox/fps_widget.cpp \ + xbox/ftp_widget.cpp \ + xbox/iso_widget.cpp \ + xbox/input.cpp \ + xbox/live_accounts.cpp \ + xbox/main.cpp \ + xbox/memory_monitor.cpp \ + xbox/memutil.cpp \ + xbox/modchip.cpp \ + xbox/network.cpp \ + xbox/overlay.cpp \ + xbox/panic_screen.cpp \ + xbox/perf_widget.cpp \ + xbox/theseus_launcher.cpp \ + xbox/title_scanner.cpp \ + xbox/widget_config.cpp \ + xbox/xbox_live.cpp \ + xbox/xip_archive.cpp + +SRCS_TOOLBOX = \ + toolbox/drive.cpp \ + toolbox/driveManager.cpp \ + toolbox/fileSystem.cpp \ + toolbox/ftpServer.cpp \ + toolbox/socketUtility.cpp \ + toolbox/stringUtility.cpp \ + toolbox/utils.cpp + +ALL_SRCS = $(SRCS_ENGINE) $(SRCS_SHARED) $(SRCS_SHARED_XBOX_ONLY) $(SRCS_RENDER) $(SRCS_XBOX) $(SRCS_TOOLBOX) diff --git a/theseus/desktop/xip.cpp b/theseus/desktop/xip.cpp index cd2da21..eb1bef2 100644 --- a/theseus/desktop/xip.cpp +++ b/theseus/desktop/xip.cpp @@ -210,7 +210,12 @@ CXipFile* LoadXIP(const char* szURL, bool bSync, bool isSkin ) } } - ASSERT(c_nXipFileCount < countof(c_rgXipFile)); + if (c_nXipFileCount >= (int)countof(c_rgXipFile)) + { + TRACE("\001LoadXIP: XIP pool full (%d)\n", (int)countof(c_rgXipFile)); + return NULL; + } + CXipFile* pXipFile = &c_rgXipFile[c_nXipFileCount]; if (!pXipFile->Open(szBuf)) @@ -392,6 +397,20 @@ DWORD CALLBACK CXipFile::StartLoadThread(LPVOID pvContext) return 0; } +// Read returns the byte count, or -1 when the file is short or unreadable, so +// "did I get all of it" has to be asked directly. +static bool XipReadExact(CFileBuffer& file, void* pv, int cb) +{ + return cb >= 0 && file.Read(pv, cb) == cb; +} + +// Every real archive ends exactly where dataStart + dataSize says it does. +static bool XipSizeMatches(CFileBuffer& file, const XIPHEADER& hdr) +{ + DWORD cbFile = GetFileSize(file.m_hFile, NULL); + return cbFile != (DWORD)-1 && cbFile == hdr.m_dwDataStart + hdr.m_dwDataSize; +} + bool CXipFile::Load() { char szDirPath [MAX_PATH]; @@ -410,26 +429,36 @@ bool CXipFile::Load() m_file.SetFile(m_szXipFileName, hFile); - if (!m_file.Read(&m_header, sizeof (XIPHEADER))) + if (!XipReadExact(m_file, &m_header, sizeof (XIPHEADER))) return false; if (m_header.m_dwMagic != XIP_MAGIC) return false; + // The tables sit between the header and the data section, so the name pool + // is the remainder. A corrupt dataStart makes that negative, and a real + // archive ends exactly where its header says it does. + const int cbTables = (int)(m_header.m_wFileCount * sizeof (FILEDATA)) + + (int)(m_header.m_wNameCount * sizeof (FILENAME)); + const int cbNames = (int)m_header.m_dwDataStart - (int)sizeof (XIPHEADER) - cbTables; + + if (cbNames <= 0 || !XipSizeMatches(m_file, m_header)) + { + TRACE("\001CXipFile::Load (%s) header does not describe this file\n", m_szXipFileName); + return false; + } + m_filedata = new FILEDATA [m_header.m_wFileCount]; - if (!m_file.Read(m_filedata, m_header.m_wFileCount * sizeof (FILEDATA))) + if (!XipReadExact(m_file, m_filedata, (int)(m_header.m_wFileCount * sizeof (FILEDATA)))) return false; m_directory = new FILENAME [m_header.m_wNameCount]; - if (!m_file.Read(m_directory, m_header.m_wNameCount * sizeof (FILENAME))) + if (!XipReadExact(m_file, m_directory, (int)(m_header.m_wNameCount * sizeof (FILENAME)))) return false; - { - int cbNames = m_header.m_dwDataStart - (sizeof (XIPHEADER) + m_header.m_wFileCount * sizeof (FILEDATA) + m_header.m_wNameCount * sizeof (FILENAME)); - m_names = new char [cbNames]; - if (!m_file.Read(m_names, cbNames)) - return false; - } + m_names = new char [cbNames]; + if (!XipReadExact(m_file, m_names, cbNames)) + return false; CreateObjects(); @@ -655,6 +684,12 @@ void CXipFile::ReadVertexBuffer(int nFileIndex, int nVertexBuffer) m_file.Read(&nVertexCount, sizeof (int)); m_file.Read(&fvf, sizeof (DWORD)); + if (nVertexCount <= 0 || m_filedata[nFileIndex].m_dwSize < 8) + { + TRACE("\001ReadVertexBuffer: bad vertex count %d\n", nVertexCount); + return; + } + pMeshBuffer->m_nVertexStride = (m_filedata[nFileIndex].m_dwSize - 8) / nVertexCount; TheseusCreateVertexBuffer(m_filedata[nFileIndex].m_dwSize - 8, D3DUSAGE_DYNAMIC, fvf, D3DPOOL_DEFAULT, &pMeshBuffer->m_pVertexBuffer); diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp index 6ca8dfd..40bf1fd 100644 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -436,6 +436,8 @@ void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, VERIFY(LoadGlyph(nGlyphIndex)); CGlyphShape* pGlyphShape = m_rgGlyphObjects[nGlyphIndex].m_glyphShape; + if (!pGlyphShape) + continue; pGlyphShape->m_nGlyphIndex = nGlyphIndex; if (nVerts + pGlyphShape->m_nVertexCount > 65535) @@ -669,6 +671,18 @@ void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, } CGlyphShape* pGlyphShape = m_rgGlyphObjects[nGlyphIndex].m_glyphShape; + if (!pGlyphShape) + continue; + + // The mesh was sized by the counting pass. If this pass ever walks + // the string differently it would write straight past the buffers, + // so stop at the edge and drop the rest of the line. + if (nCurVertex + pGlyphShape->m_nVertexCount > nVerts || + nCurIndex + pGlyphShape->m_nIndexCount > nFaces * 3) + { + nChars = ich - 1; + break; + } int j; for (j = 0; j < pGlyphShape->m_nIndexCount; j += 1) diff --git a/theseus/shared/savegame_grid.cpp b/theseus/shared/savegame_grid.cpp index 2631534..79880f1 100644 --- a/theseus/shared/savegame_grid.cpp +++ b/theseus/shared/savegame_grid.cpp @@ -1792,7 +1792,7 @@ bool ContentMetaParse(const TCHAR *wszMetaPath, TCHAR *outName, int outLen) bool inSection = false; bool matchLang = false; - for (size_t i = 0; i < bytesRead - 2; i += 2) + for (size_t i = 0; i + 1 < bytesRead; i += 2) { if (buf[i + 1] == 0 && buf[i] >= 32 && buf[i] <= 126) { @@ -1802,18 +1802,22 @@ bool ContentMetaParse(const TCHAR *wszMetaPath, TCHAR *outName, int outLen) size_t len = i - start; char asciiBuf[256] = {}; - for (size_t j = 0; j < len && j < sizeof(asciiBuf) - 1; j += 2) - asciiBuf[j / 2] = buf[start + j]; + size_t nOut = 0; + for (size_t j = 0; j < len && nOut < sizeof(asciiBuf) - 1; j += 2) + asciiBuf[nOut++] = buf[start + j]; - asciiBuf[len / 2] = '\0'; + asciiBuf[nOut] = '\0'; const char *str = asciiBuf; if (str[0] == '[' && str[strlen(str) - 1] == ']') { // Normalize and compare char sectionLang[32] = {}; - strncpy(sectionLang, str + 1, strlen(str) - 2); // strip brackets - sectionLang[strlen(str) - 2] = '\0'; + size_t cbLang = strlen(str) - 2; // strip brackets + if (cbLang > sizeof(sectionLang) - 1) + cbLang = sizeof(sectionLang) - 1; + strncpy(sectionLang, str + 1, cbLang); + sectionLang[cbLang] = '\0'; _strlwr(sectionLang); matchLang = (_stricmp(langCodeA, sectionLang) == 0 || strcmp(sectionLang, "default") == 0); diff --git a/theseus/shared/settingsfile.cpp b/theseus/shared/settingsfile.cpp index 2020eea..60a5da8 100644 --- a/theseus/shared/settingsfile.cpp +++ b/theseus/shared/settingsfile.cpp @@ -31,6 +31,7 @@ CSettingsFile::CSettingsFile() m_filePath = NULL; m_sections = NULL; m_dirty = false; + m_batching = false; #ifdef _UNICODE m_unicode = false; #endif @@ -326,7 +327,8 @@ void CSettingsFile::SetValue(const TCHAR* szSection, const TCHAR* szName, const if (pSection->SetValue(szName, szValue)) { m_dirty = true; - Save(); + if (!m_batching) + Save(); } } diff --git a/theseus/shared/settingsfile.h b/theseus/shared/settingsfile.h index 1dbb171..ec0d270 100755 --- a/theseus/shared/settingsfile.h +++ b/theseus/shared/settingsfile.h @@ -62,9 +62,16 @@ class CSettingsFile void SetValue(const TCHAR* szSection, const TCHAR* szName, const TCHAR* szValue); CSettingsFileSection* FindSection(const TCHAR* szSection, bool bCreate = false); + // SetValue writes the whole file every time it changes a key, which is + // fine for a setting the user just toggled and ruinous for a loop. Bracket + // a run of writes with these and call Save yourself at the end. + void BeginBatch() { m_batching = true; } + void EndBatch() { m_batching = false; } + TCHAR* m_filePath; CSettingsFileSection* m_sections; bool m_dirty; + bool m_batching; protected: diff --git a/theseus/shared/xap_vm.cpp b/theseus/shared/xap_vm.cpp index e4a0352..bed9a95 100644 --- a/theseus/shared/xap_vm.cpp +++ b/theseus/shared/xap_vm.cpp @@ -952,6 +952,12 @@ bool CRunner::Step(CObject** ppRetObj) int nFrameSize = FetchInt(); LOCALTRACE(_T("opFrame: %d\n"), nFrameSize); + if (nFrameSize < 0 || m_sp + (UINT)nFrameSize > countof(m_stack)) + { + Error(_T("script stack overflow")); + return false; + } + #ifdef _DEBUG // Make sure everything on the stack has been cleaned up! { @@ -961,7 +967,6 @@ bool CRunner::Step(CObject** ppRetObj) #endif m_sp += nFrameSize; m_spBase = m_sp; - ASSERT(m_sp < sizeof (m_stack) / sizeof (CObject*)); } break; diff --git a/theseus/xbox/disc_management.cpp b/theseus/xbox/disc_management.cpp index 092625d..bbe5181 100755 --- a/theseus/xbox/disc_management.cpp +++ b/theseus/xbox/disc_management.cpp @@ -372,7 +372,7 @@ int GetDiscType() hDevice = CreateFileA("cdrom0:", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - if (hDevice != NULL) + if (hDevice != INVALID_HANDLE_VALUE && hDevice != NULL) { BOOL fReturn; DWORD cbReturned; diff --git a/theseus/xbox/main.cpp b/theseus/xbox/main.cpp index 08c7181..7f4a53d 100644 --- a/theseus/xbox/main.cpp +++ b/theseus/xbox/main.cpp @@ -858,8 +858,14 @@ static void InitSkin() if (pSkinXBX->GetValue(_T("Dashboard Settings"), _T("Current Skin"), CurrentSkinFile, MAX_PATH)) { pSkinXBX->Close(); - _stprintf(SkinString, _T("Q:\\Skins\\%s\\"), CurrentSkinFile); - _stprintf(WorkerString, _T("%s%s.xbx"), SkinString, CurrentSkinFile); + + // CurrentSkinFile comes straight out of Config.ini and can be as + // long as the buffer itself, so both of these have to be bounded. + // _sntprintf leaves the string unterminated when it truncates. + _sntprintf(SkinString, countof(SkinString) - 1, _T("Q:\\Skins\\%s\\"), CurrentSkinFile); + SkinString[countof(SkinString) - 1] = 0; + _sntprintf(WorkerString, countof(WorkerString) - 1, _T("%s%s.xbx"), SkinString, CurrentSkinFile); + WorkerString[countof(WorkerString) - 1] = 0; if (pSkinXBX->Open(WorkerString)) { @@ -1073,7 +1079,8 @@ bool InitApp() GetStartupClassFile(szFileToLoad); BootMark("GetStartupClassFile"); if (!LoadXIP(_T("Q:\\Xips\\default.xip"), true)) - TheseusPanic(_T("Q:\\Xips\\default.xip is missing or unreadable.")); + TheseusPanic(_T("Q:\\Xips\\default.xip is missing or unreadable."), + _T("Copy default.xip back into Q:\\Xips over FTP, then restart.")); BootMark("LoadXIP default.xip (sync)"); Class_Init(); BootMark("Class_Init"); @@ -1088,14 +1095,22 @@ bool InitApp() g_bParseError ? _T("Parse error in %s") : _T("Cannot open %s"), szFileToLoad); - TheseusPanic(reason); + TheseusPanic(reason, + _T("Replace the file named above over FTP, then restart.")); } BootMark("g_pClass->Load(startup XAP)"); g_pObject = (CInstance *)g_pClass->CreateNode(); + if (g_pObject == NULL) + TheseusPanic(_T("The startup scene could not be created."), + _T("Replace Q:\\Xips over FTP, then restart.")); BootMark("CreateNode"); - VERIFYHR(D3DXCreateMatrixStack(0, &g_worldStack)); + // VERIFYHR still runs the call in retail but throws the result away, and a + // null matrix stack takes the first frame down with no explanation. + if (FAILED(D3DXCreateMatrixStack(0, &g_worldStack)) || g_worldStack == NULL) + TheseusPanic(_T("Out of memory setting up the renderer."), + _T("Restart the console. If it repeats, reinstall Theseus.")); BootMark("D3DXCreateMatrixStack"); CallFunction(g_pObject, _T("initialize")); @@ -1207,7 +1222,9 @@ LONG XdashUnhandledExceptionFilter(LPEXCEPTION_POINTERS pEx) { // Drop into panic mode so the user gets a recovery screen with FTP // info and a crash log on disk, instead of a silent reboot. - TheseusPanic(_T("Unhandled exception in dashboard."), pEx); + TheseusPanic(_T("Unhandled exception in dashboard."), + _T("Restart the console. If it repeats, send Q:\\Logs\\panic-*.txt to us."), + pEx); // TheseusPanic doesn't return, but if D3D was unavailable it will // have rebooted on its own. This line is the unreachable safety net. diff --git a/theseus/xbox/memory_monitor.cpp b/theseus/xbox/memory_monitor.cpp index 751bc0b..97789dc 100755 --- a/theseus/xbox/memory_monitor.cpp +++ b/theseus/xbox/memory_monitor.cpp @@ -421,6 +421,9 @@ int CMemoryMonitor::HaveDeviceBottom(int nDevice) bool CMemoryMonitor::GetTotalAndFreeBlocks(int devUnit, int *pnTotalBlocks, int *pnFreeBlocks) { + if (devUnit < Dev1Unit1 || devUnit > Dev4Unit2) + return false; + char szPath[4]; if (devUnit == Dev0) @@ -860,7 +863,8 @@ float CMemoryMonitor::GetFreeTotalRatio(int devUnit) if (!GetTotalAndFreeBlocks(devUnit, &nTotalBlocks, &nFreeBlocks)) return 0.0f; - ASSERT(nTotalBlocks != 0); + if (nTotalBlocks == 0) + return 0.0f; static float nLast[9]; float nReturn = (float)nFreeBlocks / nTotalBlocks; @@ -892,6 +896,14 @@ void CMemoryMonitor::SetMUName(int devUnit, const TCHAR *szName) void CMemoryMonitor::FormatMemoryUnit(int devUnit) { + // Scripts reach this directly, and Dev0 is a real selection that sits one + // past the end of the mount arrays. Only memory units can be formatted. + if (devUnit < Dev1Unit1 || devUnit > Dev4Unit2) + { + TRACE(_T("\001FormatMemoryUnit: %d is not a memory unit\n"), devUnit); + return; + } + if (m_rgchMount[devUnit] != 0) { XUnmountMU(devUnit / 2, devUnit & 1); diff --git a/theseus/xbox/panic_screen.cpp b/theseus/xbox/panic_screen.cpp index 4908bf9..ea93f4e 100644 --- a/theseus/xbox/panic_screen.cpp +++ b/theseus/xbox/panic_screen.cpp @@ -208,9 +208,11 @@ void RebootCold() } // namespace -void TheseusPanic(const TCHAR* reason, LPEXCEPTION_POINTERS pEx) +void TheseusPanic(const TCHAR* reason, const TCHAR* nextStep, LPEXCEPTION_POINTERS pEx) { if (!reason) reason = _T("(unknown)"); + if (!nextStep) + nextStep = _T("Restart the console. If it keeps happening, reinstall Theseus."); OutputDebugString(_T("[Panic] entering panic mode: ")); OutputDebugString(reason); @@ -287,9 +289,9 @@ void TheseusPanic(const TCHAR* reason, LPEXCEPTION_POINTERS pEx) (BYTE)(220 * pulse), (BYTE)(60 * pulse)); - DrawCenteredText(_T("THESEUS PANIC MODE"), screenW, + DrawCenteredText(_T("Something broke."), screenW, screenH * 0.075f, screenH * 0.058f, titleColor); - DrawCenteredText(_T("Dashboard init failed -- recovery only"), screenW, + DrawCenteredText(_T("Theseus could not finish starting up."), screenW, screenH * 0.146f, screenH * 0.029f, D3DCOLOR_ARGB(255, 120, 160, 120)); @@ -297,13 +299,19 @@ void TheseusPanic(const TCHAR* reason, LPEXCEPTION_POINTERS pEx) const float bodyH = screenH * 0.033f; // ~16 at 480 const float lineH = screenH * 0.029f; + // What to do about it. + float y = screenH * 0.222f; + OverlayFontDraw(_T("Next step:"), marginX, y, labelH, D3DCOLOR_ARGB(255, 255, 220, 120)); + OverlayFontDraw(nextStep, marginX, y + lineH * 1.3f, bodyH, + D3DCOLOR_ARGB(255, 255, 255, 255)); + // Reason panel. - float y = screenH * 0.229f; + y = screenH * 0.330f; OverlayFontDraw(_T("Reason:"), marginX, y, labelH, D3DCOLOR_ARGB(255, 100, 200, 100)); OverlayFontDraw(reason, marginX, y + lineH * 1.3f, bodyH, D3DCOLOR_ARGB(255, 240, 240, 240)); // Network panel. - y = screenH * 0.354f; + y = screenH * 0.438f; char ipStr[16] = "0.0.0.0"; bool netUp = net::isReady(); if (netUp) IpToString(net::getAddress(), ipStr); @@ -328,7 +336,7 @@ void TheseusPanic(const TCHAR* reason, LPEXCEPTION_POINTERS pEx) D3DCOLOR_ARGB(255, 200, 200, 200)); // Expected XIPs panel. - y = screenH * 0.521f; + y = screenH * 0.605f; OverlayFontDraw(_T("Expected XIPs:"), marginX, y, labelH, D3DCOLOR_ARGB(255, 100, 200, 100)); char fullPath[64]; for (size_t i = 0; i < sizeof(kExpectedXips) / sizeof(kExpectedXips[0]); i++) { diff --git a/theseus/xbox/panic_screen.h b/theseus/xbox/panic_screen.h index a09fbe0..7794c30 100644 --- a/theseus/xbox/panic_screen.h +++ b/theseus/xbox/panic_screen.h @@ -16,7 +16,10 @@ // over the network without reflashing. // // `reason` is shown to the user verbatim and dumped to Q:\Logs\panic-*.txt. +// `nextStep` is the one thing we want the person in front of the TV to go do +// about it; leave it NULL for the generic restart-then-reinstall advice. // `pEx` is optional; when set its EXCEPTION_RECORD + register CONTEXT are // also written to the log so post-mortem stack walks have something to work // with. -void TheseusPanic(const TCHAR* reason, LPEXCEPTION_POINTERS pEx = NULL); +void TheseusPanic(const TCHAR* reason, const TCHAR* nextStep = NULL, + LPEXCEPTION_POINTERS pEx = NULL); diff --git a/theseus/xbox/title_scanner.cpp b/theseus/xbox/title_scanner.cpp index 4efe802..c5bf747 100644 --- a/theseus/xbox/title_scanner.cpp +++ b/theseus/xbox/title_scanner.cpp @@ -421,6 +421,11 @@ void CTitleScanner::Rebuild() CSettingsFile icons; bool iconsOk = icons.Open(kIconsFilePath); + + // One write per key would rewrite both files once per title. Hold them + // in memory and let the explicit Save calls at the end do the work. + cache.BeginBatch(); + icons.BeginBatch(); if (!iconsOk) OutputDebugStringA("[TitleScanner] open Icons.ini failed; will create new\n"); @@ -457,6 +462,9 @@ void CTitleScanner::Rebuild() totalTitles += nTitles; } + cache.EndBatch(); + icons.EndBatch(); + if (!cache.Save()) OutputDebugStringA("[TitleScanner] save cache.ini FAILED\n"); cache.Close(); diff --git a/theseus/xbox/xip_archive.cpp b/theseus/xbox/xip_archive.cpp index ccb6d03..2693baf 100644 --- a/theseus/xbox/xip_archive.cpp +++ b/theseus/xbox/xip_archive.cpp @@ -205,7 +205,13 @@ CXipFile* LoadXIP(const TCHAR* szURL, bool bSync, bool isSkin) } } - ASSERT(c_nXipFileCount < countof(c_rgXipFile)); + if (c_nXipFileCount >= (int)countof(c_rgXipFile)) + { + TRACE(_T("\001LoadXIP: XIP pool full (%d), cannot load %s\n"), + (int)countof(c_rgXipFile), szBuf); + return NULL; + } + CXipFile* pXip = &c_rgXipFile[c_nXipFileCount]; if (!pXip->Open(szBuf)) @@ -216,7 +222,13 @@ CXipFile* LoadXIP(const TCHAR* szURL, bool bSync, bool isSkin) // Load synchronously or spawn a background thread if (bSync) { - pXip->Load(); + // A caller that waited for the load wants to know it failed. The slot + // stays spent: whoever asked for a sync XIP is about to give up anyway. + if (!pXip->Load()) + { + TRACE(_T("\001LoadXIP: %s failed to load\n"), szBuf); + return NULL; + } } else { @@ -271,9 +283,12 @@ void* FindObjectInXIP(const TCHAR* szURL, const TCHAR* szFilename, int nType) // Skin directory override: check for loose texture files first if (nType == XIP_TYPE_TEXTURE && TheseusGetSkinDir() != NULL && szFilename != NULL) { + // Skin dir plus an archive-supplied filename, either of which can be + // long enough on its own. TCHAR szSkinPath[MAX_PATH]; - _tcscpy(szSkinPath, TheseusGetSkinDir()); - _tcscat(szSkinPath, szFilename); + _sntprintf(szSkinPath, countof(szSkinPath) - 1, _T("%s%s"), + TheseusGetSkinDir(), szFilename); + szSkinPath[countof(szSkinPath) - 1] = 0; char ansiPath[MAX_PATH]; Ansi(ansiPath, szSkinPath, MAX_PATH); @@ -383,6 +398,38 @@ DWORD CALLBACK CXipFile::StartLoadThread(LPVOID pvContext) return 0; } +// Read returns the byte count, or -1 when the file is short or unreadable. +// Callers want "did I get all of it", so ask that question directly. +static bool ReadExact(CFileBuffer& file, void* pv, int cb) +{ + return cb >= 0 && file.Read(pv, cb) == cb; +} + +// CreateObjects walks the data section in order with no seeking, so anything +// we decline to store still has to come off the stream or every entry after +// it reads from the wrong place. +static void DiscardBytes(CFileBuffer& file, int cb) +{ + BYTE rgb[512]; + while (cb > 0) + { + int n = cb < (int)sizeof(rgb) ? cb : (int)sizeof(rgb); + if (file.Read(rgb, n) != n) + return; + cb -= n; + } +} + +// A XIP that fails to load is never read again, so let go of its handle. +static void CloseXipHandle(CFileBuffer& file) +{ + if (file.m_handle != INVALID_HANDLE_VALUE) + { + CloseHandle(file.m_handle); + file.m_handle = INVALID_HANDLE_VALUE; + } +} + bool CXipFile::Load() { #ifdef _DEBUG @@ -405,36 +452,57 @@ bool CXipFile::Load() m_file.SetFile(m_fileName, hFile); // Read and validate header - if (!m_file.Read(&m_header, sizeof(XIPHEADER))) + if (!ReadExact(m_file, &m_header, sizeof(XIPHEADER))) + { + CloseXipHandle(m_file); return false; + } if (m_header.m_magic != XIP_MAGIC) { #ifdef _DEBUG TRACE(_T("\001CXipFile::Open (%s.xip) not a valid XIP file!\n"), szDirPath); #endif + CloseXipHandle(m_file); + return false; + } + + // The tables sit between the header and the data section, so the name pool + // is whatever is left over. A corrupt dataStart makes that negative, and + // every real archive ends exactly where the header says it does. + const int cbTables = (int)(m_header.m_fileCount * sizeof(FILEDATA)) + + (int)(m_header.m_nameCount * sizeof(FILENAME)); + const int cbNames = (int)m_header.m_dataStart - (int)sizeof(XIPHEADER) - cbTables; + + if (cbNames <= 0 + || GetFileSize(m_file.m_handle, NULL) != m_header.m_dataStart + m_header.m_dataSize) + { + TRACE(_T("\001CXipFile::Load (%s) header does not describe this file\n"), m_fileName); + CloseXipHandle(m_file); return false; } // Read file table, directory, and name pool m_filedata = new FILEDATA[m_header.m_fileCount]; - if (!m_file.Read(m_filedata, m_header.m_fileCount * sizeof(FILEDATA))) + if (!ReadExact(m_file, m_filedata, (int)(m_header.m_fileCount * sizeof(FILEDATA)))) + { + CloseXipHandle(m_file); return false; + } m_directory = new FILENAME[m_header.m_nameCount]; - if (!m_file.Read(m_directory, m_header.m_nameCount * sizeof(FILENAME))) + if (!ReadExact(m_file, m_directory, (int)(m_header.m_nameCount * sizeof(FILENAME)))) + { + CloseXipHandle(m_file); return false; - - int cbNames = m_header.m_dataStart - - sizeof(XIPHEADER) - - m_header.m_fileCount * sizeof(FILEDATA) - - m_header.m_nameCount * sizeof(FILENAME); + } m_names = new char[cbNames]; - if (!m_file.Read(m_names, cbNames)) + if (!ReadExact(m_file, m_names, cbNames)) + { + CloseXipHandle(m_file); return false; - - ASSERT(GetFileSize(m_file.m_handle, NULL) == m_header.m_dataStart + m_header.m_dataSize); + } CreateObjects(); m_loaded = true; @@ -447,8 +515,22 @@ bool CXipFile::Load() static LPDIRECT3DTEXTURE8 ReadTextureFromXPR(CFileBuffer& file, int nBytes) { + // Everything below indexes pbRaw using sizes the archive supplies, so the + // entry has to be big enough to hold a header and a texture struct before + // any of them can be trusted. + if (nBytes < (int)(sizeof(XPR_HEADER) + sizeof(IDirect3DTexture8))) + { + TRACE(_T("\001ReadTextureFromXPR: entry too small (%d bytes)\n"), nBytes); + DiscardBytes(file, nBytes); + return NULL; + } + BYTE* pbRaw = new BYTE[nBytes]; - file.Read(pbRaw, nBytes); + if (!ReadExact(file, pbRaw, nBytes)) + { + delete[] pbRaw; + return NULL; + } const XPR_HEADER* pxpr = (const XPR_HEADER*)pbRaw; if (pxpr->dwMagic != XPR_MAGIC_VALUE) @@ -458,7 +540,25 @@ static LPDIRECT3DTEXTURE8 ReadTextureFromXPR(CFileBuffer& file, int nBytes) return NULL; } + // dwHeaderSize is where the pixels start and dwTotalSize is where they + // end; both have to land inside the bytes we actually read. + if (pxpr->dwHeaderSize < sizeof(XPR_HEADER) + || pxpr->dwTotalSize < pxpr->dwHeaderSize + || pxpr->dwTotalSize > (DWORD)nBytes) + { + TRACE(_T("\001ReadTextureFromXPR: header describes %d..%d of %d bytes\n"), + (int)pxpr->dwHeaderSize, (int)pxpr->dwTotalSize, nBytes); + delete[] pbRaw; + return NULL; + } + int cbData = pxpr->dwTotalSize - pxpr->dwHeaderSize; + if (cbData <= 0) + { + TRACE(_T("\001ReadTextureFromXPR: no pixel data\n")); + delete[] pbRaw; + return NULL; + } // Allocate the D3D texture header in system memory IDirect3DTexture8* pTex = (IDirect3DTexture8*)TheseusD3D_AllocNoncontiguousMemory(sizeof(D3DBaseTexture)); @@ -513,10 +613,19 @@ void CXipFile::CreateObjects() case XIP_TYPE_MESH_REFERENCE: { + // Top byte of the offset, so the archive can name any slot up + // to 255 whether or not we have that many. + const UINT iBuffer = m_filedata[i].m_dataOffset >> 24; + if (iBuffer >= MAX_MESHBUFFER) + { + TRACE(_T("\001CreateObjects: mesh ref %d out of range in %s\n"), + (int)iBuffer, m_fileName); + break; + } + CMeshRef* pRef = new CMeshRef; pRef->m_xipFile = this; - pRef->m_meshBufferIndex = m_filedata[i].m_dataOffset >> 24; - ASSERT(pRef->m_meshBufferIndex < MAX_MESHBUFFER); + pRef->m_meshBufferIndex = iBuffer; pRef->m_firstIndex = m_filedata[i].m_dataOffset & 0x00ffffff; pRef->m_primitiveCount = m_filedata[i].m_size; m_objects[i] = pRef; @@ -524,13 +633,23 @@ void CXipFile::CreateObjects() break; case XIP_TYPE_INDEXBUFFER: - ASSERT(m_indexBufferCount < MAX_MESHBUFFER); + if (m_indexBufferCount >= MAX_MESHBUFFER) + { + TRACE(_T("\001CreateObjects: too many index buffers in %s\n"), m_fileName); + DiscardBytes(m_file, m_filedata[i].m_size); + break; + } ReadIndexBuffer(i, m_vertexBufferCount); m_indexBufferCount++; break; case XIP_TYPE_VERTEXBUFFER: - ASSERT(m_vertexBufferCount < MAX_MESHBUFFER); + if (m_vertexBufferCount >= MAX_MESHBUFFER) + { + TRACE(_T("\001CreateObjects: too many vertex buffers in %s\n"), m_fileName); + DiscardBytes(m_file, m_filedata[i].m_size); + break; + } ReadVertexBuffer(i, m_vertexBufferCount); m_vertexBufferCount++; break; @@ -640,6 +759,12 @@ void CXipFile::ReadVertexBuffer(int nFileIndex, int nBuffer) m_file.Read(&fvf, sizeof(DWORD)); int cbVerts = m_filedata[nFileIndex].m_size - 8; + if (nVerts <= 0 || cbVerts <= 0) + { + TRACE(_T("\001ReadVertexBuffer: bad vertex count %d in %s\n"), nVerts, m_fileName); + DiscardBytes(m_file, cbVerts > 0 ? cbVerts : 0); + return; + } pBuf->m_vertexStride = cbVerts / nVerts; TheseusCreateVertexBuffer(cbVerts, D3DUSAGE_DYNAMIC, fvf, D3DPOOL_DEFAULT, &pBuf->m_vertexBuffer); From 412d83314edbab1809a9a45fb8670d425671aa6f Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:37:55 -0500 Subject: [PATCH 009/167] ES-DE and media library scanning fixes ES-DE can be pointed straight at an es_systems.xml (AppImage installs keep theirs inside the image), and the empty-scan message says which problem you hit. Movies and episodes dropped loose in a folder show up now instead of being skipped. --- theseus/desktop/esde.cpp | 46 +++++++++++++++- theseus/desktop/esde.h | 4 ++ theseus/desktop/media_db.cpp | 95 +++++++++++++++++++++++++++++---- theseus/desktop/title_maker.cpp | 11 +++- 4 files changed, 141 insertions(+), 15 deletions(-) diff --git a/theseus/desktop/esde.cpp b/theseus/desktop/esde.cpp index e1bd2d3..7b8c872 100644 --- a/theseus/desktop/esde.cpp +++ b/theseus/desktop/esde.cpp @@ -131,29 +131,66 @@ const char* Esde_RomRoot(const char* esdeRoot) return s_root; } +static bool g_esdeFoundSystemsXml = false; + // Custom systems win, same order ES-DE uses, then the copy inside the install. +// An AppImage keeps its copy inside the mount, so nothing is on disk for us to +// read; those users point us straight at an es_systems.xml instead. static bool FindSystemsXml(const char* esdeRoot, char* out, size_t outSz) { struct stat st; + + // Pointed straight at the file. + size_t rl = strlen(esdeRoot); + if (rl > 15 && strcasecmp(esdeRoot + rl - 15, "es_systems.xml") == 0 && + stat(esdeRoot, &st) == 0) { + snprintf(out, outSz, "%s", esdeRoot); + return true; + } + snprintf(out, outSz, "%s/custom_systems/es_systems.xml", esdeRoot); + fprintf(stderr, "[ES-DE] probe %s\n", out); if (stat(out, &st) == 0) return true; - static const char* kBundled[] = { + char home[1024]; + snprintf(home, sizeof(home), "%s", PlatformHomeDir()); + + char userLocal[1024], homeEsde[1024]; + snprintf(userLocal, sizeof(userLocal), + "%s/.local/share/es-de/resources/systems/linux/es_systems.xml", home); + snprintf(homeEsde, sizeof(homeEsde), + "%s/ES-DE/resources/systems/linux/es_systems.xml", home); + + const char* kBundled[] = { "/Applications/ES-DE.app/Contents/Resources/resources/systems/macos/es_systems.xml", "/usr/share/es-de/resources/systems/linux/es_systems.xml", + "/usr/local/share/es-de/resources/systems/linux/es_systems.xml", + "/usr/share/emulationstation/resources/systems/linux/es_systems.xml", "/app/share/es-de/resources/systems/linux/es_systems.xml", "/var/lib/flatpak/app/org.es_de.frontend/current/active/files/share/es-de/resources/systems/linux/es_systems.xml", + "/var/lib/snapd/snap/es-de/current/usr/share/es-de/resources/systems/linux/es_systems.xml", + userLocal, + homeEsde, "C:/Program Files/ES-DE/resources/systems/windows/es_systems.xml", }; for (size_t i = 0; i < sizeof(kBundled) / sizeof(kBundled[0]); i++) { + fprintf(stderr, "[ES-DE] probe %s\n", kBundled[i]); if (stat(kBundled[i], &st) == 0) { snprintf(out, outSz, "%s", kBundled[i]); return true; } } + out[0] = '\0'; return false; } +// True when we could not find a systems definition at all, which is a +// different failure from finding one and matching no roms. +bool Esde_LastScanFoundSystemsXml(void) +{ + return g_esdeFoundSystemsXml; +} + // One directory level. cb gets (name, isDir) and stops the walk on false. template static void ForEachEntry(const char* dir, F cb) @@ -244,7 +281,12 @@ int Esde_ScanSystems(const char* esdeRoot, EsdeSystem* out, int maxOut) if (!esdeRoot || !*esdeRoot || !out || maxOut <= 0) return 0; char sysXml[700]; - if (!FindSystemsXml(esdeRoot, sysXml, sizeof(sysXml))) return 0; + g_esdeFoundSystemsXml = FindSystemsXml(esdeRoot, sysXml, sizeof(sysXml)); + if (!g_esdeFoundSystemsXml) { + fprintf(stderr, "[ES-DE] no es_systems.xml found for root %s\n", esdeRoot); + return 0; + } + fprintf(stderr, "[ES-DE] systems from %s\n", sysXml); FILE* fp = fopen(sysXml, "rb"); if (!fp) return 0; diff --git a/theseus/desktop/esde.h b/theseus/desktop/esde.h index d8e057a..d1ba5b1 100644 --- a/theseus/desktop/esde.h +++ b/theseus/desktop/esde.h @@ -26,6 +26,10 @@ struct EsdeGame { // Systems whose rom folder exists and holds at least one matching file. int Esde_ScanSystems(const char* esdeRoot, EsdeSystem* out, int maxOut); +// False when the last scan never located an es_systems.xml, which is a +// different failure from locating one and matching no roms. +bool Esde_LastScanFoundSystemsXml(void); + // Games for one system, subfolders included. Names come from gamelist.xml // where it has them, the filename otherwise. int Esde_ScanGames(const char* esdeRoot, const EsdeSystem* sys, EsdeGame* out, int maxOut); diff --git a/theseus/desktop/media_db.cpp b/theseus/desktop/media_db.cpp index c25e262..5484fa4 100644 --- a/theseus/desktop/media_db.cpp +++ b/theseus/desktop/media_db.cpp @@ -182,22 +182,40 @@ static std::string ParseEpisodeTitle(const std::string& filename, int& seasonOut return rest; } -static std::string FindFirstVideoFile(const std::string& dir) +// First video at or below dir. depth 1 is this level only; 2 also covers a +// Season folder or similar sitting under the title folder. +static std::string FindFirstVideoFile(const std::string& dir, int depth = 1) { DIR* d = opendir(dir.c_str()); if (!d) return ""; + std::vector subdirs; + std::string out; struct dirent* e; while ((e = readdir(d))) { if (e->d_name[0] == '.') continue; if (IsVideoExt(e->d_name)) { - std::string out = dir + "/" + e->d_name; - closedir(d); - return out; + out = dir + "/" + e->d_name; + break; + } + if (depth > 1) + { + std::string sub = dir + "/" + e->d_name; + struct stat st; + if (stat(sub.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) + subdirs.push_back(sub); } } closedir(d); + if (!out.empty()) return out; + + // Files win over folders, so only descend once this level came up empty. + for (size_t i = 0; i < subdirs.size(); i++) + { + out = FindFirstVideoFile(subdirs[i], depth - 1); + if (!out.empty()) return out; + } return ""; } @@ -215,13 +233,22 @@ static void ScanMoviesInto(std::vector& out) if (e->d_name[0] == '.') continue; std::string folder = std::string(kMoviesRoot) + "/" + e->d_name; struct stat st; - if (stat(folder.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) continue; - - std::string videoPath = FindFirstVideoFile(folder); + if (stat(folder.c_str(), &st) != 0) continue; + + std::string videoPath; + std::string title = e->d_name; + if (S_ISDIR(st.st_mode)) { + videoPath = FindFirstVideoFile(folder, 2); + } else if (IsVideoExt(e->d_name)) { + // Movies dropped straight in the root, no folder per title. + videoPath = folder; + size_t dot = title.find_last_of('.'); + if (dot != std::string::npos) title.erase(dot); + } if (videoPath.empty()) continue; MovieRec m; - m.title = e->d_name; + m.title = title; m.path = videoPath; m.year = ParseYearFromTitle(m.title); out.push_back(m); @@ -244,8 +271,8 @@ static void ScanShowSeasons(ShowRec& show) std::string seasonDir = show.path + "/" + e->d_name; struct stat st; if (stat(seasonDir.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) continue; - // Only "Season N" folders. - if (strncmp(e->d_name, "Season", 6) != 0) continue; + if (strncasecmp(e->d_name, "Season", 6) != 0 && + strncasecmp(e->d_name, "Specials", 8) != 0) continue; SeasonRec s; s.name = e->d_name; @@ -276,6 +303,28 @@ static void ScanShowSeasons(ShowRec& show) show.seasons.push_back(s); } closedir(d); + + // Episodes sitting straight in the show folder, no Season dirs. Common + // enough that dropping the show entirely just looks like nothing loaded. + if (show.seasons.empty()) + { + SeasonRec s; + s.name = "Season 1"; + DIR* fd = opendir(show.path.c_str()); + if (!fd) return; + struct dirent* fe; + while ((fe = readdir(fd))) + { + if (fe->d_name[0] == '.') continue; + if (!IsVideoExt(fe->d_name)) continue; + EpisodeRec ep; + ep.path = show.path + "/" + fe->d_name; + ep.title = ParseEpisodeTitle(fe->d_name, ep.season, ep.episode); + s.episodes.push_back(ep); + } + closedir(fd); + if (!s.episodes.empty()) show.seasons.push_back(s); + } } static void ScanShowsInto(std::vector& out) @@ -289,9 +338,33 @@ static void ScanShowsInto(std::vector& out) if (e->d_name[0] == '.') continue; std::string folder = std::string(kTvRoot) + "/" + e->d_name; struct stat st; - if (stat(folder.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) continue; + if (stat(folder.c_str(), &st) != 0) continue; ShowRec sh; + if (!S_ISDIR(st.st_mode)) + { + // A loose episode in the root. Its own one episode show, so it + // is at least reachable instead of invisible. + if (!IsVideoExt(e->d_name)) continue; + std::string title = e->d_name; + size_t dot = title.find_last_of('.'); + if (dot != std::string::npos) title.erase(dot); + + SeasonRec s; + s.name = "Season 1"; + EpisodeRec ep; + ep.path = folder; + ep.title = ParseEpisodeTitle(e->d_name, ep.season, ep.episode); + s.episodes.push_back(ep); + + sh.title = title; + sh.path = kTvRoot; + sh.seasons.push_back(s); + out.push_back(sh); + BumpProgress((int)out.size()); + continue; + } + sh.title = e->d_name; sh.path = folder; ScanShowSeasons(sh); diff --git a/theseus/desktop/title_maker.cpp b/theseus/desktop/title_maker.cpp index 8f5cd57..9602ad5 100644 --- a/theseus/desktop/title_maker.cpp +++ b/theseus/desktop/title_maker.cpp @@ -2197,8 +2197,15 @@ void RenderTitleMaker() { if (strcmp(s_esScannedFor, s_esdePath) != 0) EsdeRescan(s_esdePath); if (s_esSysCount <= 0) { - ImGui::TextDisabled("Nothing found. ES-DE keeps its roms in %s", - Esde_RomRoot(s_esdePath)); + if (!Esde_LastScanFoundSystemsXml()) { + ImGui::TextDisabled("No es_systems.xml under %s.", s_esdePath); + ImGui::TextDisabled("An ES-DE AppImage keeps its copy inside the"); + ImGui::TextDisabled("image, so point this at an es_systems.xml or at"); + ImGui::TextDisabled("a folder holding custom_systems/es_systems.xml."); + } else { + ImGui::TextDisabled("Systems loaded but no roms matched. ES-DE keeps"); + ImGui::TextDisabled("its roms in %s", Esde_RomRoot(s_esdePath)); + } } else { ImGui::Text("%d system%s, %d game%s.", s_esSysCount, s_esSysCount == 1 ? "" : "s", From 9b789c28ee0a182c0923470ef729d1e676a3f184 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:37:55 -0500 Subject: [PATCH 010/167] Skip repeated render states on Xbox, deeper profiler The Xbox now drops render and texture stage states that didn't change, and forgets them on Reset and in the panic screen. Overlays set their full 2D state themselves since they can't count on leftovers anymore. The profiler splits the tree into material, world and draw and lists the heaviest meshes. Needs a hardware test. --- theseus/desktop/sdl_main.cpp | 2 ++ theseus/render/shape_render.cpp | 12 +++++++- theseus/render/text.cpp | 4 +++ theseus/render/widget_draw.cpp | 18 +++++++----- theseus/render/widget_layer.cpp | 28 +++++++++--------- theseus/shared/frame_profiler.cpp | 47 +++++++++++++++++++++++++++++++ theseus/shared/frame_profiler.h | 20 +++++++++++++ theseus/shared/theseus.h | 40 ++++++++++++++++++++++---- theseus/xbox/config.cpp | 2 ++ theseus/xbox/main.cpp | 7 +++++ theseus/xbox/overlay.cpp | 42 +++++++++++++-------------- theseus/xbox/panic_screen.cpp | 33 ++++++++++++---------- theseus/xbox/perf_widget.cpp | 23 ++++++++++++++- 13 files changed, 213 insertions(+), 65 deletions(-) diff --git a/theseus/desktop/sdl_main.cpp b/theseus/desktop/sdl_main.cpp index d5327ee..a9e6d52 100644 --- a/theseus/desktop/sdl_main.cpp +++ b/theseus/desktop/sdl_main.cpp @@ -1024,6 +1024,8 @@ static void PreSwapOverlays() { ImGui::Text("Scene: %.2f (setup %.2f, tree %.2f)", f.phaseMs[FP_SCENE_SETUP] + f.phaseMs[FP_SCENE_TREE] + f.phaseMs[FP_SCENE_END], f.phaseMs[FP_SCENE_SETUP], f.phaseMs[FP_SCENE_TREE]); + ImGui::Text(" material %.2f world %.2f draw %.2f", + f.phaseMs[FP_TREE_MATERIAL], f.phaseMs[FP_TREE_WORLD], f.phaseMs[FP_TREE_DRAW]); ImGui::Text("ImGui: %.2f ms", f.phaseMs[FP_IMGUI]); ImGui::Text("Ticks: %.2f ms", f.phaseMs[FP_TICKS]); ImGui::Text("Present: %.2f ms", f.phaseMs[FP_PRESENT]); diff --git a/theseus/render/shape_render.cpp b/theseus/render/shape_render.cpp index 4d6a861..26bbec1 100644 --- a/theseus/render/shape_render.cpp +++ b/theseus/render/shape_render.cpp @@ -65,9 +65,18 @@ void CShape::Render() } if (m_appearance != NULL) + { + FramePhaseScope material(FP_TREE_MATERIAL); m_appearance->Render(); + } - m_geometry->Render(); + { + FramePhaseScope draw(FP_TREE_DRAW); + int trisBefore = g_frameCount[FC_TRIANGLES]; + m_geometry->Render(); + if (pMeshNode) + Frame_TallyMesh(this, pMeshNode->m_url, g_frameCount[FC_TRIANGLES] - trisBefore); + } g_pRenderMeshNode = NULL; @@ -903,6 +912,7 @@ void SetReflectShaderFrameValues() void SetFalloffShaderFrameValues() { + FramePhaseScope world(FP_TREE_WORLD); D3DXMATRIX mat, worldView; D3DXMatrixMultiply(&worldView, TheseusGetWorld(), &g_matView); diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp index 40bf1fd..9acba46 100644 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -1077,8 +1077,10 @@ float OverlayFontDraw(const TCHAR* text, float screenX, float screenY, float pix { dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); Frame_Count(FC_DRAW_TEXT); + Frame_PhaseBegin(FP_UP_DRAW); dev->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, nVerts, nIndices / 3, s_ovlIndices, D3DFMT_INDEX16, s_ovlVerts, sizeof(OverlayTextVert)); + Frame_PhaseEnd(FP_UP_DRAW); nVerts = 0; nIndices = 0; } @@ -1106,8 +1108,10 @@ float OverlayFontDraw(const TCHAR* text, float screenX, float screenY, float pix { Frame_Count(FC_DRAW_TEXT); dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); + Frame_PhaseBegin(FP_UP_DRAW); dev->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, nVerts, nIndices / 3, s_ovlIndices, D3DFMT_INDEX16, s_ovlVerts, sizeof(OverlayTextVert)); + Frame_PhaseEnd(FP_UP_DRAW); } return curX * scale; diff --git a/theseus/render/widget_draw.cpp b/theseus/render/widget_draw.cpp index 4aafe99..b04af3a 100644 --- a/theseus/render/widget_draw.cpp +++ b/theseus/render/widget_draw.cpp @@ -22,7 +22,9 @@ void DrawSolidRect(int x, int y, int width, int height, D3DCOLOR color) if (!dev) return; Frame_Count(FC_DRAW_SOLID); dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); + Frame_PhaseBegin(FP_UP_DRAW); dev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, verts, sizeof(Vertex)); + Frame_PhaseEnd(FP_UP_DRAW); } void DrawTexturedRect(IDirect3DTexture8* tex, float x, float y, float width, float height) @@ -33,10 +35,10 @@ void DrawTexturedRect(IDirect3DTexture8* tex, float x, float y, float width, flo struct TVert { float x, y, z, rhw; DWORD color; float u, v; }; - dev->SetTexture(0, tex); - dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); - dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); - dev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); + TheseusSetTexture(0, tex); + TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); + TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); + TheseusSetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1); TVert quad[4] = { @@ -47,12 +49,14 @@ void DrawTexturedRect(IDirect3DTexture8* tex, float x, float y, float width, flo }; Frame_Count(FC_DRAW_SOLID); + Frame_PhaseBegin(FP_UP_DRAW); dev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quad, sizeof(TVert)); + Frame_PhaseEnd(FP_UP_DRAW); // Restore diffuse-only stage state so subsequent DrawSolidRect / // text draws keep working without each having to re-establish it. - dev->SetTexture(0, NULL); - dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); - dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); + TheseusSetTexture(0, NULL); + TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); } diff --git a/theseus/render/widget_layer.cpp b/theseus/render/widget_layer.cpp index c73e2e5..88c8cf6 100644 --- a/theseus/render/widget_layer.cpp +++ b/theseus/render/widget_layer.cpp @@ -71,26 +71,26 @@ void DrawWidgets() // does on entry, since both layers want the same diffuse-only // alpha-blended setup. IDirect3DDevice8* dev = g_pD3DDev; - dev->SetRenderState(D3DRS_ZENABLE, FALSE); + TheseusSetRenderState(D3DRS_ZENABLE, FALSE); #ifdef _XBOX // LIGHTING + COLORVERTEX are fixed-function lighting hints; no GL // effect via the desktop shim, kept for the Xbox path. - dev->SetRenderState(D3DRS_LIGHTING, FALSE); + TheseusSetRenderState(D3DRS_LIGHTING, FALSE); #endif - dev->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); - dev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); - dev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); - dev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); + TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); + TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); + TheseusSetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); + TheseusSetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); #ifdef _XBOX - dev->SetRenderState(D3DRS_COLORVERTEX, TRUE); + TheseusSetRenderState(D3DRS_COLORVERTEX, TRUE); #endif - dev->SetTexture(0, NULL); - dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); - dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); - dev->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); - dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); - dev->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); - dev->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); + TheseusSetTexture(0, NULL); + TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); + TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); + TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); + TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); const int sw = (int)g_nViewWidth; diff --git a/theseus/shared/frame_profiler.cpp b/theseus/shared/frame_profiler.cpp index d36d7ca..a3b0559 100644 --- a/theseus/shared/frame_profiler.cpp +++ b/theseus/shared/frame_profiler.cpp @@ -23,7 +23,11 @@ static float s_lastLoadMs; static float s_maxLoadMs; static FrameSnapshot s_snap; +struct MeshTally { const void* key; TCHAR label[24]; long long tris; }; +static MeshTally s_mesh[512]; // open addressing on the shape pointer + #ifdef _XBOX +bool theseus_xbox_rs_sent[256]; DWORD theseus_xbox_tss_shadow[8 * 32]; bool theseus_xbox_tss_seen[8 * 32]; LPDIRECT3DTEXTURE8 theseus_xbox_tex_shadow[8]; @@ -79,6 +83,33 @@ void Frame_NoteLoad(float ms) if (ms > s_maxLoadMs) s_maxLoadMs = ms; } +void Frame_TallyMesh(const void* key, const TCHAR* label, int triangles) +{ + if (!key || triangles <= 0) return; + unsigned int h = ((unsigned int)(size_t)key >> 4) & 511; + for (int probe = 0; probe < 512; probe++, h = (h + 1) & 511) + { + MeshTally& t = s_mesh[h]; + if (t.key == key) { t.tris += triangles; return; } + if (t.key == 0) + { + t.key = key; + t.tris = triangles; + int i = 0; + // keep the tail of the url; the file name is the useful part + if (label) + { + int n = 0; + while (label[n]) n++; + const TCHAR* src = label + (n > 23 ? n - 23 : 0); + for (; src[i] && i < 23; i++) t.label[i] = src[i]; + } + t.label[i] = 0; + return; + } + } +} + #ifdef THESEUS_DESKTOP void Frame_AddHostStats(float gpuMs, float gpuWaitMs, int draws, int transientVbBytes, int transientIbBytes) { @@ -172,6 +203,22 @@ void Frame_End() snap.lastLoadMs = s_lastLoadMs; snap.maxLoadMs = s_maxLoadMs; + + for (int i = 0; i < 512; i++) + { + const MeshTally& t = s_mesh[i]; + if (!t.key) continue; + float per = (float)t.tris * inv; + for (int r = 0; r < 3; r++) + { + if (per <= snap.topMesh[r].tris) continue; + for (int k = 2; k > r; k--) snap.topMesh[k] = snap.topMesh[k - 1]; + memcpy(snap.topMesh[r].label, t.label, sizeof(t.label)); + snap.topMesh[r].tris = per; + break; + } + } + memset(s_mesh, 0, sizeof(s_mesh)); s_snap = snap; memset(s_phaseSum, 0, sizeof(s_phaseSum)); diff --git a/theseus/shared/frame_profiler.h b/theseus/shared/frame_profiler.h index a74fe44..f156374 100644 --- a/theseus/shared/frame_profiler.h +++ b/theseus/shared/frame_profiler.h @@ -14,8 +14,14 @@ enum FramePhase FP_TICKS, // desktop: media, milkdrop, xCloud FP_SCENE_SETUP, // projection, lights, backdrop FP_SCENE_TREE, // g_pObject->Render() + FP_TREE_MATERIAL, // inside the tree: appearance / material setup per shape + FP_TREE_WORLD, // inside the tree: falloff shader constants per world change + FP_TREE_DRAW, // inside the tree: geometry render per shape + FP_DIP, // the device DrawIndexedPrimitive call alone, anywhere FP_SCENE_END, FP_WIDGETS, + FP_WIDGET_TICK, // inside widgets: TickWidgets + FP_UP_DRAW, // HUD / overlay DrawPrimitiveUP calls alone; the rest is text building FP_OVERLAY, FP_IMGUI, // desktop FP_PRESENT, @@ -35,9 +41,17 @@ enum FrameCounter FC_STATE_CALLS, // render state, texture stage state, texture FC_STATE_REPEATS, // ...that set the value already there FC_DISK_QUERIES, + FC_TRIANGLES, // primitives submitted through TheseusDrawIndexedPrimitive + FC_INDICES, // ...and the indices that implies FC_COUNTER_COUNT }; +struct FrameMeshTop +{ + TCHAR label[24]; + float tris; // per frame +}; + struct FrameSnapshot { float fps; @@ -54,6 +68,9 @@ struct FrameSnapshot // XAP compile on Inline load. float lastLoadMs; float maxLoadMs; + + // The three shapes submitting the most triangles. + FrameMeshTop topMesh[3]; }; extern int g_frameCount[FC_COUNTER_COUNT]; @@ -73,6 +90,9 @@ long long Frame_Now(); float Frame_Ms(long long ticks); void Frame_NoteLoad(float ms); +// Which shapes carry the triangles. key identifies the shape, label names it. +void Frame_TallyMesh(const void* key, const TCHAR* label, int triangles); + #ifdef THESEUS_DESKTOP void Frame_AddHostStats(float gpuMs, float gpuWaitMs, int draws, int transientVbBytes, int transientIbBytes); #endif diff --git a/theseus/shared/theseus.h b/theseus/shared/theseus.h index fc80083..4c5afd8 100755 --- a/theseus/shared/theseus.h +++ b/theseus/shared/theseus.h @@ -227,7 +227,10 @@ void* TheseusD3D_AllocNoncontiguousMemory(DWORD Size); #ifdef _XBOX // cache eliminates GetRenderState round-trips on Xbox extern DWORD theseus_rgdwRenderStateCache [256]; extern bool theseus_rgbRenderStateCache [256]; -// Shadows for the profiler's repeat count. Nothing is skipped on Xbox. +// What the device was last sent. Render state and texture stage state skip +// repeats; textures are only counted, since a freed texture's address can +// come back as a different texture. +extern bool theseus_xbox_rs_sent [256]; extern DWORD theseus_xbox_tss_shadow [8 * 32]; extern bool theseus_xbox_tss_seen [8 * 32]; extern LPDIRECT3DTEXTURE8 theseus_xbox_tex_shadow [8]; @@ -302,13 +305,21 @@ inline void TheseusInvalidateWrapperCaches() inline void TheseusSetRenderState(D3DRENDERSTATETYPE dwRenderStateType, DWORD dwRenderState) { Frame_Count(FC_STATE_CALLS); +#ifdef THESEUS_EXPERIMENT_NO_EDGEAA + // Measurement only: what does edge antialiasing cost the GPU? + if (dwRenderStateType == D3DRS_EDGEANTIALIAS) + dwRenderState = FALSE; +#endif #ifdef _XBOX // cache eliminates GetRenderState round-trips on Xbox ASSERT((UINT)dwRenderStateType < countof (theseus_rgdwRenderStateCache)); - if (theseus_rgbRenderStateCache[(UINT)dwRenderStateType] && - theseus_rgdwRenderStateCache[(UINT)dwRenderStateType] == dwRenderState) - Frame_Count(FC_STATE_REPEATS); // counted, still sent + if (theseus_xbox_rs_sent[(UINT)dwRenderStateType] && + theseus_rgdwRenderStateCache[(UINT)dwRenderStateType] == dwRenderState) { + Frame_Count(FC_STATE_REPEATS); + return; + } theseus_rgdwRenderStateCache[(UINT)dwRenderStateType] = dwRenderState; theseus_rgbRenderStateCache[(UINT)dwRenderStateType] = true; + theseus_xbox_rs_sent[(UINT)dwRenderStateType] = true; #else // Wrapper-level dedup. Skip the virtual call if value unchanged. if ((UINT)dwRenderStateType < 256) { @@ -346,11 +357,12 @@ inline void TheseusSetTextureStageState(DWORD dwStage, D3DTEXTURESTAGESTATETYPE { Frame_Count(FC_STATE_CALLS); #ifdef _XBOX - // Shadow copy for the repeat count only; every call still goes through. if (dwStage < 8 && (DWORD)dwState < 32) { int idx = (int)dwStage * 32 + (int)dwState; - if (theseus_xbox_tss_seen[idx] && theseus_xbox_tss_shadow[idx] == dwValue) + if (theseus_xbox_tss_seen[idx] && theseus_xbox_tss_shadow[idx] == dwValue) { Frame_Count(FC_STATE_REPEATS); + return; + } theseus_xbox_tss_shadow[idx] = dwValue; theseus_xbox_tss_seen[idx] = true; } @@ -410,6 +422,16 @@ inline void TheseusSetTexture(DWORD dwStage, LPDIRECT3DTEXTURE8 lpTexture) VERIFYHR(TheseusGetD3DDev()->SetTexture(dwStage, lpTexture)); } +// The device forgets everything on Reset; the skip caches have to forget too. +inline void TheseusInvalidateStateCache() +{ +#ifdef _XBOX + memset(theseus_xbox_rs_sent, 0, sizeof(theseus_xbox_rs_sent)); + memset(theseus_xbox_tss_seen, 0, sizeof(theseus_xbox_tss_seen)); + memset(theseus_xbox_tex_seen, 0, sizeof(theseus_xbox_tex_seen)); +#endif +} + inline void TheseusSetMaterial(D3DMATERIAL8* lpMaterial) { #ifndef _XBOX @@ -559,7 +581,13 @@ inline void TheseusSetIndices(IDirect3DIndexBuffer8* pIndexData, UINT BaseVertex inline void TheseusDrawIndexedPrimitive(D3DPRIMITIVETYPE Type, UINT MinIndex, UINT NumVertices, UINT StartIndex, UINT PrimitiveCount) { + // On Xbox the runtime copies every index into the push buffer, so + // cost follows indices, not draws. + Frame_Count(FC_TRIANGLES, (int)PrimitiveCount); + Frame_Count(FC_INDICES, (int)(Type == D3DPT_TRIANGLELIST ? PrimitiveCount * 3 : PrimitiveCount + 2)); + Frame_PhaseBegin(FP_DIP); VERIFYHR(TheseusGetD3DDev()->DrawIndexedPrimitive(Type, MinIndex, NumVertices, StartIndex, PrimitiveCount)); + Frame_PhaseEnd(FP_DIP); } extern int __cdecl NewFailed(size_t nBytes); diff --git a/theseus/xbox/config.cpp b/theseus/xbox/config.cpp index 9137b34..2400df5 100755 --- a/theseus/xbox/config.cpp +++ b/theseus/xbox/config.cpp @@ -260,6 +260,7 @@ void CConfig::SetVideoMode(int nVideoMode) g_bStretchWidescreen = bWideScreen; D3D__AvInfo = 0; TheseusGetD3DDev()->Reset(&g_pp); + TheseusInvalidateStateCache(); } g_bProjectionDirty = true; @@ -302,6 +303,7 @@ void CConfig::Set480Support(int bEnable) { D3D__AvInfo = 0; TheseusGetD3DDev()->Reset(&g_pp); + TheseusInvalidateStateCache(); } } } diff --git a/theseus/xbox/main.cpp b/theseus/xbox/main.cpp index 7f4a53d..112a50b 100644 --- a/theseus/xbox/main.cpp +++ b/theseus/xbox/main.cpp @@ -369,6 +369,11 @@ void XboxInitRenderState() bool InitD3D() { +#ifdef THESEUS_PUSHBUFFER_KB + // Experiment: a bigger push buffer so the per-frame index stream doesn't + // stall the CPU waiting for the GPU to free space. Must precede CreateDevice. + Direct3D_SetPushBufferSize(THESEUS_PUSHBUFFER_KB * 1024, 64 * 1024); +#endif g_pD3D = Direct3DCreate8(D3D_SDK_VERSION); if (g_pD3D == NULL) { @@ -801,7 +806,9 @@ void Draw() Frame_PhaseEnd(FP_SCENE_END); Frame_PhaseBegin(FP_WIDGETS); TheseusSetRenderState(D3DRS_SWATHWIDTH, D3DSWATH_OFF); + Frame_PhaseBegin(FP_WIDGET_TICK); TickWidgets(); + Frame_PhaseEnd(FP_WIDGET_TICK); DrawWidgets(); Frame_PhaseEnd(FP_WIDGETS); Frame_PhaseBegin(FP_OVERLAY); diff --git a/theseus/xbox/overlay.cpp b/theseus/xbox/overlay.cpp index 27c3589..b67b45b 100755 --- a/theseus/xbox/overlay.cpp +++ b/theseus/xbox/overlay.cpp @@ -131,10 +131,10 @@ static void DrawOverlayImage(IDirect3DTexture8 *tex, float x, float y, float wid IDirect3DDevice8 *dev = g_pD3DDev; // Switch to textured mode - dev->SetTexture(0, tex); - dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); - dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); - dev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); + TheseusSetTexture(0, tex); + TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); + TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); + TheseusSetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1); OverlayVertex quad[4] = { @@ -146,9 +146,9 @@ static void DrawOverlayImage(IDirect3DTexture8 *tex, float x, float y, float wid dev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, quad, sizeof(OverlayVertex)); // Restore diffuse-only state - dev->SetTexture(0, NULL); - dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); - dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); + TheseusSetTexture(0, NULL); + TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); } @@ -2650,20 +2650,20 @@ void DrawCubeOverlay() IDirect3DDevice8 *dev = g_pD3DDev; // Full D3D state setup. Override any leftover scene state. - dev->SetRenderState(D3DRS_ZENABLE, FALSE); - dev->SetRenderState(D3DRS_LIGHTING, FALSE); - dev->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); - dev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); - dev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); - dev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); - dev->SetRenderState(D3DRS_COLORVERTEX, TRUE); - dev->SetTexture(0, NULL); - dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); - dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); - dev->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); - dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); - dev->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); - dev->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); + TheseusSetRenderState(D3DRS_ZENABLE, FALSE); + TheseusSetRenderState(D3DRS_LIGHTING, FALSE); + TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); + TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); + TheseusSetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); + TheseusSetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); + TheseusSetRenderState(D3DRS_COLORVERTEX, TRUE); + TheseusSetTexture(0, NULL); + TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); + TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); + TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); + TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); const int sw = g_nViewWidth; diff --git a/theseus/xbox/panic_screen.cpp b/theseus/xbox/panic_screen.cpp index ea93f4e..893272b 100644 --- a/theseus/xbox/panic_screen.cpp +++ b/theseus/xbox/panic_screen.cpp @@ -152,21 +152,24 @@ void WritePanicLog(const TCHAR* reason, LPEXCEPTION_POINTERS pEx) // and the diffuse-coloured glyphs render invisible. void Set2DOverlayState(IDirect3DDevice8* dev) { - dev->SetRenderState(D3DRS_ZENABLE, FALSE); - dev->SetRenderState(D3DRS_LIGHTING, FALSE); - dev->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); - dev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); - dev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); - dev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); - dev->SetRenderState(D3DRS_ALPHATESTENABLE, FALSE); - dev->SetRenderState(D3DRS_COLORVERTEX, TRUE); - dev->SetTexture(0, NULL); - dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); - dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); - dev->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); - dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); - dev->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); - dev->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); + // A crash can land between a cache update and the device call, so + // nothing the skip caches remember is trusted here. + TheseusInvalidateStateCache(); + TheseusSetRenderState(D3DRS_ZENABLE, FALSE); + TheseusSetRenderState(D3DRS_LIGHTING, FALSE); + TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); + TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); + TheseusSetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); + TheseusSetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); + TheseusSetRenderState(D3DRS_ALPHATESTENABLE, FALSE); + TheseusSetRenderState(D3DRS_COLORVERTEX, TRUE); + TheseusSetTexture(0, NULL); + TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); + TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); + TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); + TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); } diff --git a/theseus/xbox/perf_widget.cpp b/theseus/xbox/perf_widget.cpp index c0d8ea8..71bd710 100644 --- a/theseus/xbox/perf_widget.cpp +++ b/theseus/xbox/perf_widget.cpp @@ -11,7 +11,7 @@ namespace { const int PERF_WIDGET_W = 220; -const int PERF_WIDGET_H = 240; +const int PERF_WIDGET_H = 400; void perfWidgetDraw(int x, int y, DWORD argb) { @@ -55,6 +55,11 @@ void perfWidgetDraw(int x, int y, DWORD argb) line[39] = 0; OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH; + _sntprintf(line, 40, _T("tris %.0fk idx %.0fk"), + f.count[FC_TRIANGLES] / 1000.0f, f.count[FC_INDICES] / 1000.0f); + line[39] = 0; + OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH; + _sntprintf(line, 40, _T("disk %.1f load %.0f/%.0f ms"), f.count[FC_DISK_QUERIES], f.lastLoadMs, f.maxLoadMs); line[39] = 0; @@ -64,8 +69,14 @@ void perfWidgetDraw(int x, int y, DWORD argb) { FP_ADVANCE, _T("advance ") }, { FP_SCENE_SETUP, _T("setup ") }, { FP_SCENE_TREE, _T("tree ") }, + { FP_TREE_MATERIAL, _T(" matl ") }, + { FP_TREE_WORLD, _T(" world ") }, + { FP_TREE_DRAW, _T(" draw ") }, + { FP_DIP, _T(" dip ") }, { FP_SCENE_END, _T("end ") }, { FP_WIDGETS, _T("widgets ") }, + { FP_WIDGET_TICK, _T(" tick ") }, + { FP_UP_DRAW, _T(" updraw ") }, { FP_OVERLAY, _T("overlay ") }, { FP_PRESENT, _T("present ") }, { FP_GPU_WAIT, _T("gpu wait") }, @@ -76,6 +87,16 @@ void perfWidgetDraw(int x, int y, DWORD argb) line[39] = 0; OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH; } + + // Heaviest shapes by triangles. + yy += 4; + for (int i = 0; i < 3; i++) + { + if (f.topMesh[i].tris <= 0.0f) break; + _sntprintf(line, 40, _T("%.0fk %s"), f.topMesh[i].tris / 1000.0f, f.topMesh[i].label); + line[39] = 0; + OverlayFontDraw(line, (float)(x + 8), (float)yy, 12.0f, argb); yy += rowH; + } } } // namespace From f632ee70b9e6a304f6ef7723f8b0328574224a8f Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:37:55 -0500 Subject: [PATCH 011/167] Fixed a mesh buffer overflow when loading XIPs Index buffers go into their vertex buffer's slot, but the check only looked at the index count. On desktop the check was an ASSERT that just warned and kept writing. Both readers skip the extra entry now, and check the mesh index before drawing. --- theseus/desktop/xip.cpp | 38 +++++++++++++++++++++++++++++++----- theseus/xbox/xip_archive.cpp | 9 ++++++--- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/theseus/desktop/xip.cpp b/theseus/desktop/xip.cpp index eb1bef2..7713424 100644 --- a/theseus/desktop/xip.cpp +++ b/theseus/desktop/xip.cpp @@ -404,6 +404,20 @@ static bool XipReadExact(CFileBuffer& file, void* pv, int cb) return cb >= 0 && file.Read(pv, cb) == cb; } +// CreateObjects reads entries back to back with no seeking, so an entry we +// refuse still has to be read past or every entry after it lands wrong. +static void XipDiscard(CFileBuffer& file, int cb) +{ + BYTE scratch[4096]; + while (cb > 0) + { + int n = cb < (int)sizeof(scratch) ? cb : (int)sizeof(scratch); + if (file.Read(scratch, n) != n) + return; + cb -= n; + } +} + // Every real archive ends exactly where dataStart + dataSize says it does. static bool XipSizeMatches(CFileBuffer& file, const XIPHEADER& hdr) { @@ -507,7 +521,6 @@ void CXipFile::CreateObjects() CMeshRef* pMeshRef = new CMeshRef; pMeshRef->m_xipFile = this; pMeshRef->m_meshBufferIndex = m_filedata[i].m_dwDataOffset >> 24; - ASSERT(pMeshRef->m_meshBufferIndex < MAX_MESHBUFFER); pMeshRef->m_firstIndex = m_filedata[i].m_dwDataOffset & 0x00ffffff; pMeshRef->m_primitiveCount = m_filedata[i].m_dwSize; m_objects[i] = pMeshRef; @@ -520,13 +533,25 @@ void CXipFile::CreateObjects() break; case XIP_TYPE_INDEXBUFFER: - ASSERT(m_nIndexBufferCount < MAX_MESHBUFFER); + // The index buffer lands in its vertex buffer's slot, so both + // counts have to fit. Desktop ASSERT only warns. + if (m_nIndexBufferCount >= MAX_MESHBUFFER || m_nVertexBufferCount >= MAX_MESHBUFFER) + { + TRACE("\001CreateObjects: too many index buffers\n"); + XipDiscard(m_file, m_filedata[i].m_dwSize); + break; + } ReadIndexBuffer(i, m_nVertexBufferCount); m_nIndexBufferCount += 1; break; case XIP_TYPE_VERTEXBUFFER: - ASSERT(m_nVertexBufferCount < MAX_MESHBUFFER); + if (m_nVertexBufferCount >= MAX_MESHBUFFER) + { + TRACE("\001CreateObjects: too many vertex buffers\n"); + XipDiscard(m_file, m_filedata[i].m_dwSize); + break; + } ReadVertexBuffer(i, m_nVertexBufferCount); m_nVertexBufferCount += 1; break; @@ -773,7 +798,9 @@ bool CXipFile::IsReloading() const void CMeshRef::Render(bool bSetFVF/*=true*/) { - ASSERT(m_meshBufferIndex < m_xipFile->m_nVertexBufferCount); + // The index comes from the archive's top byte, so it can name any of 256. + if (m_meshBufferIndex >= m_xipFile->m_nVertexBufferCount) + return; CMeshBuffer* pMeshBuffer = &m_xipFile->m_rgMeshBuffer[m_meshBufferIndex]; m_xipFile->m_cacheTime = TheseusGetNow(); @@ -803,6 +830,7 @@ void CMeshRef::Render(bool bSetFVF/*=true*/) DWORD CMeshRef::GetFVF() const { - ASSERT(m_meshBufferIndex < m_xipFile->m_nVertexBufferCount); + if (m_meshBufferIndex >= m_xipFile->m_nVertexBufferCount) + return 0; return m_xipFile->m_rgMeshBuffer[m_meshBufferIndex].m_fvf; } diff --git a/theseus/xbox/xip_archive.cpp b/theseus/xbox/xip_archive.cpp index 2693baf..bfbd9c4 100644 --- a/theseus/xbox/xip_archive.cpp +++ b/theseus/xbox/xip_archive.cpp @@ -633,7 +633,8 @@ void CXipFile::CreateObjects() break; case XIP_TYPE_INDEXBUFFER: - if (m_indexBufferCount >= MAX_MESHBUFFER) + // Written into its vertex buffer's slot, so that count has to fit too. + if (m_indexBufferCount >= MAX_MESHBUFFER || m_vertexBufferCount >= MAX_MESHBUFFER) { TRACE(_T("\001CreateObjects: too many index buffers in %s\n"), m_fileName); DiscardBytes(m_file, m_filedata[i].m_size); @@ -898,7 +899,8 @@ bool CXipFile::IsReloading() const void CMeshRef::Render(bool bSetFVF) { - ASSERT(m_meshBufferIndex < m_xipFile->m_vertexBufferCount); + if (m_meshBufferIndex >= m_xipFile->m_vertexBufferCount) + return; CMeshBuffer* pBuf = &m_xipFile->m_meshBuffers[m_meshBufferIndex]; m_xipFile->m_cacheTime = TheseusGetNow(); @@ -931,6 +933,7 @@ void CMeshRef::Render(bool bSetFVF) DWORD CMeshRef::GetFVF() const { - ASSERT(m_meshBufferIndex < m_xipFile->m_vertexBufferCount); + if (m_meshBufferIndex >= m_xipFile->m_vertexBufferCount) + return 0; return m_xipFile->m_meshBuffers[m_meshBufferIndex].m_fvf; } From bb795a99765da39d6388457a7e8957407aaa1c05 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:37:55 -0500 Subject: [PATCH 012/167] Fixed ES-DE ignoring a direct path to es_systems.xml It compared 15 characters against a 14 character name, so it never matched. --- theseus/desktop/esde.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/theseus/desktop/esde.cpp b/theseus/desktop/esde.cpp index 7b8c872..ccb6a1f 100644 --- a/theseus/desktop/esde.cpp +++ b/theseus/desktop/esde.cpp @@ -142,7 +142,8 @@ static bool FindSystemsXml(const char* esdeRoot, char* out, size_t outSz) // Pointed straight at the file. size_t rl = strlen(esdeRoot); - if (rl > 15 && strcasecmp(esdeRoot + rl - 15, "es_systems.xml") == 0 && + const size_t kName = sizeof("es_systems.xml") - 1; + if (rl >= kName && strcasecmp(esdeRoot + rl - kName, "es_systems.xml") == 0 && stat(esdeRoot, &st) == 0) { snprintf(out, outSz, "%s", esdeRoot); return true; From c4b74b498773a0833efec9a75ef8349e6d067272 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:37:55 -0500 Subject: [PATCH 013/167] Fixed a race in the art download queues Plex, Jellyfin and xCloud read the in-flight map outside its lock while finished downloads were erasing from it on other threads. --- theseus/desktop/jellyfin_client.cpp | 7 +++++-- theseus/desktop/plex_client.cpp | 7 +++++-- theseus/desktop/xcloud_client.cpp | 7 +++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/theseus/desktop/jellyfin_client.cpp b/theseus/desktop/jellyfin_client.cpp index f2a1574..290e07e 100644 --- a/theseus/desktop/jellyfin_client.cpp +++ b/theseus/desktop/jellyfin_client.cpp @@ -613,10 +613,13 @@ static void ArtWorker(std::string itemId, std::string tag, void Jellyfin_QueueArtDownload(const std::string& itemId, const std::string& tag) { if (itemId.empty()) return; + // Workers erase from the map on other threads, so it is only touched under the lock. + std::atomic* flag; { std::lock_guard lk(s_artMtx); if (s_artInFlight.count(itemId)) return; - s_artInFlight[itemId] = new std::atomic(false); + flag = new std::atomic(false); + s_artInFlight[itemId] = flag; } - std::thread(ArtWorker, itemId, tag, s_artInFlight[itemId]).detach(); + std::thread(ArtWorker, itemId, tag, flag).detach(); } diff --git a/theseus/desktop/plex_client.cpp b/theseus/desktop/plex_client.cpp index 2bf631e..f479cf4 100644 --- a/theseus/desktop/plex_client.cpp +++ b/theseus/desktop/plex_client.cpp @@ -670,10 +670,13 @@ static void ArtWorker(std::string ratingKey, std::string url, void Plex_QueueArtDownload(const std::string& ratingKey, const std::string& url) { if (ratingKey.empty() || url.empty()) return; + // Workers erase from the map on other threads, so it is only touched under the lock. + std::atomic* flag; { std::lock_guard lk(s_artMtx); if (s_artInFlight.count(ratingKey)) return; - s_artInFlight[ratingKey] = new std::atomic(false); + flag = new std::atomic(false); + s_artInFlight[ratingKey] = flag; } - std::thread(ArtWorker, ratingKey, url, s_artInFlight[ratingKey]).detach(); + std::thread(ArtWorker, ratingKey, url, flag).detach(); } diff --git a/theseus/desktop/xcloud_client.cpp b/theseus/desktop/xcloud_client.cpp index b2d6165..6e510db 100644 --- a/theseus/desktop/xcloud_client.cpp +++ b/theseus/desktop/xcloud_client.cpp @@ -918,12 +918,15 @@ static void ArtWorker(std::string titleId, std::string url, std::atomic* f void Xcloud_QueueArtDownload(const std::string& titleId, const std::string& url) { if (titleId.empty() || url.empty()) return; + // Workers erase from the map on other threads, so it is only touched under the lock. + std::atomic* flag; { std::lock_guard lk(s_artMtx); if (s_artInFlight.count(titleId)) return; - s_artInFlight[titleId] = new std::atomic(false); + flag = new std::atomic(false); + s_artInFlight[titleId] = flag; } - std::thread(ArtWorker, titleId, url, s_artInFlight[titleId]).detach(); + std::thread(ArtWorker, titleId, url, flag).detach(); } void Xcloud_StartStreamSession(const std::string& type, const std::string& id) From 0f1b2f5fdc4c42b86401a7e511a8cad03d5a8f88 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:37:55 -0500 Subject: [PATCH 014/167] Steadier frame timing on Windows Advance() used GetTickCount in a float. On Windows that's system uptime in 15.6 ms steps, so animation got choppier the longer the PC had been up. It uses the SDL performance counter in a double now, same as mac and Linux. Still needs a run on Windows. --- theseus/desktop/dashapp.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/theseus/desktop/dashapp.cpp b/theseus/desktop/dashapp.cpp index 1753e87..c55f7b8 100755 --- a/theseus/desktop/dashapp.cpp +++ b/theseus/desktop/dashapp.cpp @@ -826,7 +826,9 @@ void CleanupApp() void Advance() { - XTIME now = (float)GetTickCount() / 1000.0f; + // Seconds since SDL started, same base on every host. GetTickCount on + // Windows is system uptime at 15.6 ms steps, and a float of that drifts. + XTIME now = (double)SDL_GetPerformanceCounter() / (double)SDL_GetPerformanceFrequency(); float nDelta = (float)(now - g_now); if (nDelta == 0.0f) nDelta = 0.001f; From e1ea19827d04f0e8bd75c1b75bdd91ba4821d652 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:41:37 -0500 Subject: [PATCH 015/167] Fixed loose TV episodes sharing one library key Every episode dropped straight in the TV root got the root folder as its show path, so they all collided when TMDB data carried over on a rescan. Each one now keys on its own file. --- theseus/desktop/media_db.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/theseus/desktop/media_db.cpp b/theseus/desktop/media_db.cpp index 5484fa4..2c21785 100644 --- a/theseus/desktop/media_db.cpp +++ b/theseus/desktop/media_db.cpp @@ -357,8 +357,9 @@ static void ScanShowsInto(std::vector& out) ep.title = ParseEpisodeTitle(e->d_name, ep.season, ep.episode); s.episodes.push_back(ep); + // Keyed on the file itself so every loose show stays its own entry. sh.title = title; - sh.path = kTvRoot; + sh.path = folder; sh.seasons.push_back(s); out.push_back(sh); BumpProgress((int)out.size()); From 135064433dcaa24a41dce785940b8748a9b88128 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:42:33 -0500 Subject: [PATCH 016/167] Sorted episodes in show folders without season dirs Episodes sitting straight in a show folder came out in whatever order the disk listed them. Now they sort by season and episode from the filename (S01E02, s1e2, 1x02), and anything unnumbered follows in natural name order. --- theseus/desktop/media_db.cpp | 78 ++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/theseus/desktop/media_db.cpp b/theseus/desktop/media_db.cpp index 2c21785..ef340f8 100644 --- a/theseus/desktop/media_db.cpp +++ b/theseus/desktop/media_db.cpp @@ -27,6 +27,8 @@ #include #include #include +#include +#include #define MEDIADB_MAGIC "TMDB" #define MEDIADB_VERSION 2u // v2: + overview, voteAverage, tmdbId on movies/shows @@ -182,6 +184,81 @@ static std::string ParseEpisodeTitle(const std::string& filename, int& seasonOut return rest; } +// Season and episode out of a bare filename, for ordering only. Takes S01E02, +// s1e2 and 1x02 anywhere in the name, as long as it starts on a word break. +static bool ParseSeasonEpisode(const char* name, int& season, int& episode) +{ + for (const char* p = name; *p; p++) + { + if (p != name && isalnum((unsigned char)p[-1])) continue; + const char* q = p; + bool sForm = (*q == 'S' || *q == 's'); + if (sForm) q++; + const char* d = q; + int s = 0; + while (isdigit((unsigned char)*q)) s = s * 10 + (*q++ - '0'); + int sDigits = (int)(q - d); + if (sDigits < 1 || sDigits > 2) continue; + if (sForm ? (*q != 'E' && *q != 'e') : (*q != 'x' && *q != 'X')) continue; + q++; + d = q; + int e = 0; + while (isdigit((unsigned char)*q)) e = e * 10 + (*q++ - '0'); + int eDigits = (int)(q - d); + if (eDigits < 1 || eDigits > 3 || (!sForm && eDigits < 2)) continue; + season = s; episode = e; + return true; + } + return false; +} + +// Filename compare where digit runs count as numbers, so "Ep 2" lands before "Ep 10". +static int NaturalCompare(const char* a, const char* b) +{ + while (*a && *b) + { + if (isdigit((unsigned char)*a) && isdigit((unsigned char)*b)) + { + while (*a == '0') a++; + while (*b == '0') b++; + const char* na = a; const char* nb = b; + while (isdigit((unsigned char)*a)) a++; + while (isdigit((unsigned char)*b)) b++; + size_t la = a - na, lb = b - nb; + if (la != lb) return la < lb ? -1 : 1; + int c = strncmp(na, nb, la); + if (c) return c; + continue; + } + int ca = tolower((unsigned char)*a), cb = tolower((unsigned char)*b); + if (ca != cb) return ca < cb ? -1 : 1; + a++; b++; + } + return (*a != 0) - (*b != 0); +} + +static const char* BaseName(const std::string& path) +{ + size_t slash = path.find_last_of('/'); + return path.c_str() + (slash == std::string::npos ? 0 : slash + 1); +} + +// Numbered episodes in (season, episode) order, anything unnumbered after them by name. +static void SortEpisodes(std::vector& eps) +{ + std::stable_sort(eps.begin(), eps.end(), [](const EpisodeRec& x, const EpisodeRec& y) { + const char* nx = BaseName(x.path); + const char* ny = BaseName(y.path); + int sx = 0, ex = 0, sy = 0, ey = 0; + bool hx = ParseSeasonEpisode(nx, sx, ex); + bool hy = ParseSeasonEpisode(ny, sy, ey); + if (hx != hy) return hx; + if (hx && sx != sy) return sx < sy; + if (hx && ex != ey) return ex < ey; + return NaturalCompare(nx, ny) < 0; + }); +} + // First video at or below dir. depth 1 is this level only; 2 also covers a // Season folder or similar sitting under the title folder. static std::string FindFirstVideoFile(const std::string& dir, int depth = 1) @@ -323,6 +400,7 @@ static void ScanShowSeasons(ShowRec& show) s.episodes.push_back(ep); } closedir(fd); + SortEpisodes(s.episodes); if (!s.episodes.empty()) show.seasons.push_back(s); } } From 20aadeb1bf5b29eb6224832b300d7e0e1ce17492 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:43:48 -0500 Subject: [PATCH 017/167] Kept xCloud and ES-DE titles across a games list reload Reloading games.ini wiped the injected xCloud and ES-DE rows, and they stayed gone until a restart. The reload now carries them over since they never live in the file. --- theseus/desktop/virtual_games.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/theseus/desktop/virtual_games.h b/theseus/desktop/virtual_games.h index 1953f1d..2b5b127 100644 --- a/theseus/desktop/virtual_games.h +++ b/theseus/desktop/virtual_games.h @@ -110,10 +110,28 @@ inline void VGames_Load() { g_vgames.count, g_vgames.generation); } +inline bool VGames_IsLive(const VirtualGame& g) { + return g.valid && (g.transient || VGames_IsStreaming(g)); +} + +// Injected rows (xCloud, ES-DE) aren't in games.ini, so carry them across. inline void VGames_Reload() { + int nLive = 0; + for (int i = 0; i < g_vgames.count; i++) + if (VGames_IsLive(g_vgames.games[i])) nLive++; + VirtualGame* live = nLive ? (VirtualGame*)malloc(nLive * sizeof(VirtualGame)) : NULL; + if (!live) nLive = 0; + for (int i = 0, j = 0; i < g_vgames.count && j < nLive; i++) + if (VGames_IsLive(g_vgames.games[i])) live[j++] = g_vgames.games[i]; + g_vgames.loaded = false; g_vgames.count = 0; VGames_Load(); + + for (int j = 0; j < nLive && g_vgames.count < VGAMES_MAX; j++) + g_vgames.games[g_vgames.count++] = live[j]; + free(live); + g_vgames.generation++; } inline void VGames_Save() { From 15deeccec5d87a535bb07eac7ebf6ec7610f2f6a Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:45:44 -0500 Subject: [PATCH 018/167] Stopped ES-DE resyncs from eating games list slots A resync only marked the old ES-DE rows dead and appended the whole library again, so every rescan burned another library's worth of slots. Same with an xCloud refresh. Dead rows now get compacted away, and Title Maker drops its cached rows and selections when that happens. --- theseus/desktop/esde_vgames.cpp | 3 ++- theseus/desktop/title_maker.cpp | 9 ++++++++- theseus/desktop/virtual_games.h | 18 ++++++++++++++++++ theseus/desktop/xcloud_vgames.cpp | 2 +- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/theseus/desktop/esde_vgames.cpp b/theseus/desktop/esde_vgames.cpp index 36cafc4..ec5bff4 100644 --- a/theseus/desktop/esde_vgames.cpp +++ b/theseus/desktop/esde_vgames.cpp @@ -65,7 +65,8 @@ static void ClearEsdeEntries() if (g.valid && g.transient && strcasecmp(g.category, ESDE_CATEGORY) == 0) g.valid = false; } - g_vgames.generation++; + // Every resync re-adds the whole library, so hand the old slots back. + VGames_Compact(); } void EsdeVGames_Clear() diff --git a/theseus/desktop/title_maker.cpp b/theseus/desktop/title_maker.cpp index 9602ad5..8112b39 100644 --- a/theseus/desktop/title_maker.cpp +++ b/theseus/desktop/title_maker.cpp @@ -770,7 +770,10 @@ void RenderTitleMaker() { }); }; - if (s_needsScan) { DoScan(); s_needsScan = false; } + // Rows moved under us (ES-DE resync, xCloud refresh), so the cached indices are stale. + static unsigned int s_scanLayout = 0; + if (s_scanLayout != g_vgames.layout) s_needsScan = true; + if (s_needsScan) { DoScan(); s_needsScan = false; s_scanLayout = g_vgames.layout; } // Header ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "UIX Title Maker"); @@ -1435,6 +1438,8 @@ void RenderTitleMaker() { s_stIconBrowserInit = true; } + static unsigned int s_stLayout = 0; + if (s_stLayout != g_vgames.layout) { s_stSelectedVi = -1; s_stLayout = g_vgames.layout; } if (s_stSelectedVi >= 0 && (s_stSelectedVi >= g_vgames.count || !g_vgames.games[s_stSelectedVi].valid)) s_stSelectedVi = -1; @@ -1784,6 +1789,8 @@ void RenderTitleMaker() { } // Selection may have been removed; clamp. + static unsigned int s_raLayout = 0; + if (s_raLayout != g_vgames.layout) { s_raSelectedVi = -1; s_raLayout = g_vgames.layout; } if (s_raSelectedVi >= 0 && (s_raSelectedVi >= g_vgames.count || !g_vgames.games[s_raSelectedVi].valid)) s_raSelectedVi = -1; diff --git a/theseus/desktop/virtual_games.h b/theseus/desktop/virtual_games.h index 2b5b127..a677cb9 100644 --- a/theseus/desktop/virtual_games.h +++ b/theseus/desktop/virtual_games.h @@ -42,6 +42,9 @@ struct VirtualGameDB { // Bumps on every mutation so consumers can cache filtered slices keyed on // it and auto refresh when games.ini changes. unsigned int generation; + // Bumps when a compact moves rows to new indices, so anything holding on + // to an index knows to drop it. + unsigned int layout; }; extern VirtualGameDB g_vgames; @@ -114,6 +117,21 @@ inline bool VGames_IsLive(const VirtualGame& g) { return g.valid && (g.transient || VGames_IsStreaming(g)); } +// Close the gaps left by invalid rows so their slots can be used again. +// Shifts indices, same as a reload does. +inline void VGames_Compact() { + int w = 0; + for (int r = 0; r < g_vgames.count; r++) { + if (!g_vgames.games[r].valid) continue; + if (w != r) g_vgames.games[w] = g_vgames.games[r]; + w++; + } + g_vgames.generation++; + if (w == g_vgames.count) return; + g_vgames.count = w; + g_vgames.layout++; +} + // Injected rows (xCloud, ES-DE) aren't in games.ini, so carry them across. inline void VGames_Reload() { int nLive = 0; diff --git a/theseus/desktop/xcloud_vgames.cpp b/theseus/desktop/xcloud_vgames.cpp index ffb5f1b..d21d198 100644 --- a/theseus/desktop/xcloud_vgames.cpp +++ b/theseus/desktop/xcloud_vgames.cpp @@ -26,7 +26,7 @@ static void ClearStreamingEntries() if (g_vgames.games[i].valid && VGames_IsStreaming(g_vgames.games[i])) g_vgames.games[i].valid = false; } - g_vgames.generation++; + VGames_Compact(); } static void Inject() From 25ba48e43cb9d6c24cb4868b15f6f1fcfa39ad46 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:47:35 -0500 Subject: [PATCH 019/167] Fixed Plex reading its server list without the lock Opening a show or season read the server list straight off while a sync worker could be swapping it out after a sign in. It copies the address under the lock now. --- theseus/desktop/plex_client.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/theseus/desktop/plex_client.cpp b/theseus/desktop/plex_client.cpp index f479cf4..267ea19 100644 --- a/theseus/desktop/plex_client.cpp +++ b/theseus/desktop/plex_client.cpp @@ -595,6 +595,13 @@ std::vector Plex_Cache_GetItems(const std::string& sectionKey) return it->second; } +// A server refresh swaps s_servers on a worker, so copy under the lock. +static std::string FirstServerUri() +{ + std::lock_guard lk(g_mtx); + return s_servers.empty() ? std::string() : s_servers.front().uri; +} + bool Plex_Cache_GetSeasons(const std::string& showRatingKey, std::vector& out) { @@ -607,9 +614,8 @@ bool Plex_Cache_GetSeasons(const std::string& showRatingKey, } } // Not cached, fire a fetch; SeasonsWorker promotes the result. - if (!s_servers.empty()) { - Plex_FetchSeasons(s_servers.front().uri, showRatingKey); - } + std::string uri = FirstServerUri(); + if (!uri.empty()) Plex_FetchSeasons(uri, showRatingKey); return false; } @@ -624,9 +630,8 @@ bool Plex_Cache_GetEpisodes(const std::string& seasonRatingKey, return true; } } - if (!s_servers.empty()) { - Plex_FetchEpisodes(s_servers.front().uri, seasonRatingKey); - } + std::string uri = FirstServerUri(); + if (!uri.empty()) Plex_FetchEpisodes(uri, seasonRatingKey); return false; } From 74c3a716f8626e4403f649760374af2d0660be35 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:47:51 -0500 Subject: [PATCH 020/167] Stopped TMDB checkpoints from writing the media cache over each other The lookup workers each save a checkpoint every 10 titles, all through the same temp file. Two at once could interleave and rename a scrambled cache into place. Saves take turns now. --- theseus/desktop/media_db.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/theseus/desktop/media_db.cpp b/theseus/desktop/media_db.cpp index ef340f8..946c82b 100644 --- a/theseus/desktop/media_db.cpp +++ b/theseus/desktop/media_db.cpp @@ -483,6 +483,9 @@ static void WriteCacheTo(const std::vector& movies, { // Write atomically: temp file + rename so a kill mid-write never // leaves the cache truncated. + // TMDB workers checkpoint in parallel and share the one temp file, so take turns. + static std::mutex s_writeMutex; + std::lock_guard writeLock(s_writeMutex); std::string tmp = std::string(path) + ".tmp"; FILE* f = fopen(tmp.c_str(), "wb"); if (!f) return; From 56452d8152f62cb0242a34e6c73b6974c087e8f4 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:47:59 -0500 Subject: [PATCH 021/167] Count frame time from launch, not boot The last change put the clock on the performance counter, which on mac counts from boot. Scripts time the screensaver against zero, so it fired on the first frame before the menu existed and threw an array error. Windows had the same problem before, since GetTickCount is uptime there too. --- theseus/desktop/dashapp.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/theseus/desktop/dashapp.cpp b/theseus/desktop/dashapp.cpp index c55f7b8..885d47b 100755 --- a/theseus/desktop/dashapp.cpp +++ b/theseus/desktop/dashapp.cpp @@ -826,9 +826,10 @@ void CleanupApp() void Advance() { - // Seconds since SDL started, same base on every host. GetTickCount on - // Windows is system uptime at 15.6 ms steps, and a float of that drifts. - XTIME now = (double)SDL_GetPerformanceCounter() / (double)SDL_GetPerformanceFrequency(); + // Seconds since launch, on every host. Scripts time things like the + // screensaver against 0, so an uptime base fires them on the first frame. + static const Uint64 s_clockStart = SDL_GetPerformanceCounter(); + XTIME now = (double)(SDL_GetPerformanceCounter() - s_clockStart) / (double)SDL_GetPerformanceFrequency(); float nDelta = (float)(now - g_now); if (nDelta == 0.0f) nDelta = 0.001f; From f10f35213c981665efe791095641606414bf2f41 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:47:59 -0500 Subject: [PATCH 022/167] CHECK, a bounds check that survives retail ASSERT is nothing in retail and only a warning on desktop, and a lot of code leaned on it anyway. Switched the ones that touch real memory: long numbers in scripts overran a stack buffer in the compiler, big language files ran off the translation table, scripts could write junk parental control values into EEPROM, and XIP reloads trusted the buffer counts. --- theseus/desktop/locale.cpp | 3 ++- theseus/desktop/std.h | 9 +++++++++ theseus/desktop/xip.cpp | 6 ++++-- theseus/engine/locale_node.cpp | 3 ++- theseus/engine/xap_compile.cpp | 18 +++++++++++++++--- theseus/xbox/config.cpp | 13 +++++++++---- theseus/xbox/std.h | 11 +++++++++++ theseus/xbox/xip_archive.cpp | 6 ++++-- 8 files changed, 56 insertions(+), 13 deletions(-) diff --git a/theseus/desktop/locale.cpp b/theseus/desktop/locale.cpp index ce957ac..a4773f9 100755 --- a/theseus/desktop/locale.cpp +++ b/theseus/desktop/locale.cpp @@ -217,7 +217,8 @@ static void LoadXlate(const char* szXlateSection) // TRACE("XLATE: \"%s\" = \"%s\"\n", pchKey, pchValue); - ASSERT(nXlate < MAX_XLATE); // need to increase MAX_XLATE! + if (!CHECK(nXlate < MAX_XLATE)) + return; // the rest of the file is dropped; raise MAX_XLATE rgxlate[nXlate].m_szKey = (char*)pchKey; rgxlate[nXlate].m_szValue = (char*)pchValue; nXlate += 1; diff --git a/theseus/desktop/std.h b/theseus/desktop/std.h index 069b589..be971d0 100755 --- a/theseus/desktop/std.h +++ b/theseus/desktop/std.h @@ -95,6 +95,15 @@ EXTERN_C void Alert(const char* szMsg, ...); #define TRACE Trace #define ALERT Alert +// CHECK survives every build, unlike ASSERT. Use it where failing means bad memory. +#define CHECK(f) TheseusCheck(!!(f), #f, __FILE__, __LINE__) +inline bool TheseusCheck(bool ok, const char* expr, const char* file, int line) +{ + if (!ok) + fprintf(stderr, "[CHECK] %s:%d: %s\n", file, line, expr); + return ok; +} + // Profiling stubs (Xbox profiling removed) #define START_PROFILE() #define END_PROFILE() diff --git a/theseus/desktop/xip.cpp b/theseus/desktop/xip.cpp index 7713424..bb84653 100644 --- a/theseus/desktop/xip.cpp +++ b/theseus/desktop/xip.cpp @@ -743,14 +743,16 @@ void CXipFile::ReloadMeshBuffers() switch (m_filedata[i].m_dwType) { case XIP_TYPE_INDEXBUFFER: - ASSERT(nIndexBuffer < m_nIndexBufferCount); + if (!CHECK(nIndexBuffer < m_nIndexBufferCount)) + break; m_file.Seek(m_header.m_dwDataStart + m_filedata[i].m_dwDataOffset); ReadIndexBuffer(i, nIndexBuffer); nIndexBuffer += 1; break; case XIP_TYPE_VERTEXBUFFER: - ASSERT(nVertexBuffer < m_nVertexBufferCount); + if (!CHECK(nVertexBuffer < m_nVertexBufferCount)) + break; m_file.Seek(m_header.m_dwDataStart + m_filedata[i].m_dwDataOffset); ReadVertexBuffer(i, nVertexBuffer); nVertexBuffer += 1; diff --git a/theseus/engine/locale_node.cpp b/theseus/engine/locale_node.cpp index 234c540..6307106 100755 --- a/theseus/engine/locale_node.cpp +++ b/theseus/engine/locale_node.cpp @@ -189,7 +189,8 @@ static void LoadXlate(const CHAR* szXlateSection) // TRACE(_T("XLATE: \"%s\" = \"%s\"\n"), pchKey, pchValue); - ASSERT(nXlate < MAX_XLATE); // need to increase MAX_XLATE! + if (!CHECK(nXlate < MAX_XLATE)) + return; // the rest of the file is dropped; raise MAX_XLATE rgxlate[nXlate].m_szKey = (TCHAR*)pchKey; rgxlate[nXlate].m_szValue = (TCHAR*)pchValue; nXlate += 1; diff --git a/theseus/engine/xap_compile.cpp b/theseus/engine/xap_compile.cpp index 419e555..e6e8d25 100755 --- a/theseus/engine/xap_compile.cpp +++ b/theseus/engine/xap_compile.cpp @@ -327,7 +327,11 @@ const TCHAR* ParseInteger(const TCHAR* pch, int& i) pch = Token(pch, pchToken, cchToken); TCHAR szBuf[64]; - ASSERT(cchToken < countof(szBuf)); + if (!CHECK(cchToken < countof(szBuf))) + { + SyntaxError(_T("Number is too long")); + cchToken = countof(szBuf) - 1; + } _tcsncpy(szBuf, pchToken, cchToken); szBuf[cchToken] = 0; i = _ttoi(szBuf); @@ -342,7 +346,11 @@ const TCHAR* ParseNumber(const TCHAR* pch, float& n) pch = Token(pch, pchToken, cchToken); TCHAR szBuf[64]; - ASSERT(cchToken < countof(szBuf)); + if (!CHECK(cchToken < countof(szBuf))) + { + SyntaxError(_T("Number is too long")); + cchToken = countof(szBuf) - 1; + } _tcsncpy(szBuf, pchToken, cchToken); szBuf[cchToken] = 0; n = (float)_tcstod(szBuf, NULL); @@ -712,7 +720,11 @@ const TCHAR* CFunctionCompiler::ParseTerm(const TCHAR* pch) { // Numeric literal TCHAR szBuf[256]; // copy the token because _tcstod stupidly copies the whole string - ASSERT(cchToken < countof(szBuf)); + if (!CHECK(cchToken < countof(szBuf))) + { + SyntaxError(_T("Number is too long")); + cchToken = countof(szBuf) - 1; + } CopyChars(szBuf, pchToken, cchToken); szBuf[cchToken] = 0; float n = (float)_tcstod(szBuf, NULL); diff --git a/theseus/xbox/config.cpp b/theseus/xbox/config.cpp index 2400df5..816a7a5 100755 --- a/theseus/xbox/config.cpp +++ b/theseus/xbox/config.cpp @@ -580,13 +580,16 @@ int CConfig::GetGamePCFlags() { DWORD dwFlags = 0, dwType; VERIFY(!XQueryValue(XC_PARENTAL_CONTROL_GAMES, &dwType, &dwFlags, 4, NULL)); - ASSERT(dwFlags <= 6); + if (!CHECK(dwFlags <= 6)) + dwFlags = 0; return (int)(6 - dwFlags); // stored inverted in EEPROM } void CConfig::SetGamePCFlags(int nFlags) { - ASSERT(nFlags <= 6); + // Scripts call this and the value goes straight into EEPROM. + if (!CHECK(nFlags >= 0 && nFlags <= 6)) + return; nFlags = 6 - nFlags; VERIFY(!XSetValue(XC_PARENTAL_CONTROL_GAMES, REG_DWORD, (DWORD *)&nFlags, 4)); } @@ -595,13 +598,15 @@ int CConfig::GetMoviePCFlags() { DWORD dwFlags = 0, dwType; VERIFY(!XQueryValue(XC_PARENTAL_CONTROL_MOVIES, &dwType, &dwFlags, 4, NULL)); - ASSERT(dwFlags <= 7); + if (!CHECK(dwFlags <= 7)) + dwFlags = 0; return (int)(7 - dwFlags); } void CConfig::SetMoviePCFlags(int nFlags) { - ASSERT(nFlags <= 7); + if (!CHECK(nFlags >= 0 && nFlags <= 7)) + return; nFlags = 7 - nFlags; VERIFY(!XSetValue(XC_PARENTAL_CONTROL_MOVIES, REG_DWORD, (DWORD *)&nFlags, 4)); } diff --git a/theseus/xbox/std.h b/theseus/xbox/std.h index 2768d64..73f8df4 100755 --- a/theseus/xbox/std.h +++ b/theseus/xbox/std.h @@ -152,6 +152,15 @@ extern void Ansi(char* sz, const TCHAR* wsz, int nMaxChars); EXTERN_C bool AssertFailed(const TCHAR* szFile, int nLine, HRESULT hr); EXTERN_C void Trace(const TCHAR* szMsg, ...); +// CHECK survives retail, unlike ASSERT. Use it where failing means bad memory. +#define CHECK(f) TheseusCheck(!!(f), #f, __FILE__, __LINE__) +inline bool TheseusCheck(bool ok, const char* expr, const char* file, int line) +{ + if (!ok) + Trace(_T("\001CHECK failed %hs:%d: %hs\n"), file, line, expr); + return ok; +} + #else // Release (Xbox) and all desktop builds: no-op asserts, silent trace #define ASSERT(f) ((void)0) @@ -163,6 +172,8 @@ EXTERN_C void Trace(const TCHAR* szMsg, ...); inline void Trace(const TCHAR* szMsg, ...) { } +#define CHECK(f) (!!(f)) + #endif EXTERN_C void Alert(const TCHAR* szMsg, ...); diff --git a/theseus/xbox/xip_archive.cpp b/theseus/xbox/xip_archive.cpp index bfbd9c4..4408828 100644 --- a/theseus/xbox/xip_archive.cpp +++ b/theseus/xbox/xip_archive.cpp @@ -812,13 +812,15 @@ void CXipFile::ReloadMeshBuffers() { if (m_filedata[i].m_type == XIP_TYPE_INDEXBUFFER) { - ASSERT(nIB < m_indexBufferCount); + if (!CHECK(nIB < m_indexBufferCount)) + continue; m_file.Seek(m_header.m_dataStart + m_filedata[i].m_dataOffset); ReadIndexBuffer(i, nIB++); } else if (m_filedata[i].m_type == XIP_TYPE_VERTEXBUFFER) { - ASSERT(nVB < m_vertexBufferCount); + if (!CHECK(nVB < m_vertexBufferCount)) + continue; m_file.Seek(m_header.m_dataStart + m_filedata[i].m_dataOffset); ReadVertexBuffer(i, nVB++); } From fb04f2d72cba4eb71ccd26e474b3991594b19910 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:48:26 -0500 Subject: [PATCH 023/167] Locked media library reads against a running scan The library scene's getters and the cache load touched the movie and show lists with no lock while a refresh thread could be swapping them, so browsing during a rescan could read freed strings. They take the library lock now, and a cache that fails to load halfway leaves the old list alone. --- theseus/desktop/media_db.cpp | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/theseus/desktop/media_db.cpp b/theseus/desktop/media_db.cpp index 946c82b..e606309 100644 --- a/theseus/desktop/media_db.cpp +++ b/theseus/desktop/media_db.cpp @@ -566,8 +566,10 @@ static bool LoadCache() fclose(f); return false; } - g_movies.clear(); - g_shows.clear(); + // Parsed into locals and swapped in under the lock, since a scan thread + // may be publishing at the same time. + std::vector movies; + std::vector shows; uint32_t nMovies; if (fread(&nMovies, 4, 1, f) != 1) { fclose(f); return false; } @@ -578,7 +580,7 @@ static bool LoadCache() if (!ReadStr(f, m.title) || !ReadStr(f, m.path) || fread(&y, 2, 1, f) != 1) { - fclose(f); g_movies.clear(); return false; + fclose(f); return false; } m.year = y; uint16_t vote = 0; @@ -587,11 +589,11 @@ static bool LoadCache() fread(&vote, 2, 1, f) != 1 || fread(&tid, 4, 1, f) != 1) { - fclose(f); g_movies.clear(); return false; + fclose(f); return false; } m.voteAverage = vote / 10.0f; m.tmdbId = tid; - g_movies.push_back(m); + movies.push_back(m); } uint32_t nShows; @@ -632,10 +634,15 @@ static bool LoadCache() } sh.seasons.push_back(sn); } - g_shows.push_back(sh); + shows.push_back(sh); } fclose(f); + { + std::lock_guard lock(g_dbMutex); + g_movies.swap(movies); + g_shows.swap(shows); + } printf("[MediaDB] Loaded cache: %u movies, %u shows\n", nMovies, nShows); return true; } @@ -671,9 +678,11 @@ class CMediaCollection : public CNode int m_curSelKind; // 0 = movie, 1 = show int m_curSelIdx; - int GetMovieCount() { return (int)g_movies.size(); } + // A scan thread swaps these vectors out, so every read takes g_dbMutex. + int GetMovieCount() { std::lock_guard lock(g_dbMutex); return (int)g_movies.size(); } CStrObject* GetMovieTitle(int i) { + std::lock_guard lock(g_dbMutex); if (i < 0 || i >= (int)g_movies.size()) return new CStrObject; // Strip trailing " (YYYY)" for cleaner display. std::string t = g_movies[i].title; @@ -683,6 +692,7 @@ class CMediaCollection : public CNode } CStrObject* GetMovieMeta(int i) { + std::lock_guard lock(g_dbMutex); if (i < 0 || i >= (int)g_movies.size()) return new CStrObject; char buf[64]; if (g_movies[i].year > 0) @@ -693,13 +703,15 @@ class CMediaCollection : public CNode } CStrObject* GetMoviePath(int i) { + std::lock_guard lock(g_dbMutex); if (i < 0 || i >= (int)g_movies.size()) return new CStrObject; return new CStrObject(_T(g_movies[i].path.c_str())); } - int GetShowCount() { return (int)g_shows.size(); } + int GetShowCount() { std::lock_guard lock(g_dbMutex); return (int)g_shows.size(); } CStrObject* GetShowTitle(int i) { + std::lock_guard lock(g_dbMutex); if (i < 0 || i >= (int)g_shows.size()) return new CStrObject; std::string t = g_shows[i].title; size_t p = t.rfind(" ("); @@ -708,6 +720,7 @@ class CMediaCollection : public CNode } CStrObject* GetShowMeta(int i) { + std::lock_guard lock(g_dbMutex); if (i < 0 || i >= (int)g_shows.size()) return new CStrObject; int totalEps = 0; for (size_t s = 0; s < g_shows[i].seasons.size(); s++) @@ -727,11 +740,13 @@ class CMediaCollection : public CNode } int GetSeasonCount() { + std::lock_guard lock(g_dbMutex); if (m_curShowIdx < 0 || m_curShowIdx >= (int)g_shows.size()) return 0; return (int)g_shows[m_curShowIdx].seasons.size(); } CStrObject* GetSeasonName(int sIdx) { + std::lock_guard lock(g_dbMutex); if (m_curShowIdx < 0 || m_curShowIdx >= (int)g_shows.size()) return new CStrObject; if (sIdx < 0 || sIdx >= (int)g_shows[m_curShowIdx].seasons.size()) return new CStrObject; return new CStrObject(_T(g_shows[m_curShowIdx].seasons[sIdx].name.c_str())); @@ -740,12 +755,14 @@ class CMediaCollection : public CNode void SetCurrentSeason(int i) { m_curSeasonIdx = i; } int GetEpisodeCount() { + std::lock_guard lock(g_dbMutex); if (m_curShowIdx < 0 || m_curShowIdx >= (int)g_shows.size()) return 0; if (m_curSeasonIdx < 0 || m_curSeasonIdx >= (int)g_shows[m_curShowIdx].seasons.size()) return 0; return (int)g_shows[m_curShowIdx].seasons[m_curSeasonIdx].episodes.size(); } CStrObject* GetEpisodeTitle(int epIdx) { + std::lock_guard lock(g_dbMutex); if (m_curShowIdx < 0 || m_curShowIdx >= (int)g_shows.size()) return new CStrObject; if (m_curSeasonIdx < 0 || m_curSeasonIdx >= (int)g_shows[m_curShowIdx].seasons.size()) return new CStrObject; const std::vector& eps = g_shows[m_curShowIdx].seasons[m_curSeasonIdx].episodes; @@ -754,6 +771,7 @@ class CMediaCollection : public CNode } CStrObject* GetEpisodePath(int epIdx) { + std::lock_guard lock(g_dbMutex); if (m_curShowIdx < 0 || m_curShowIdx >= (int)g_shows.size()) return new CStrObject; if (m_curSeasonIdx < 0 || m_curSeasonIdx >= (int)g_shows[m_curShowIdx].seasons.size()) return new CStrObject; const std::vector& eps = g_shows[m_curShowIdx].seasons[m_curSeasonIdx].episodes; From 9f29cef28bb61a89973e6b36164673942d0d4e83 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:48:49 -0500 Subject: [PATCH 024/167] Held the library lock while Playlist Maker lists media Hitting Refresh Library in Playlist Maker kicks a scan that swaps the movie and show lists, while the window kept reading titles and paths out of them every frame. The listing holds the library lock now. --- theseus/desktop/playlist_maker.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/theseus/desktop/playlist_maker.cpp b/theseus/desktop/playlist_maker.cpp index cb75bd1..98bbe8f 100644 --- a/theseus/desktop/playlist_maker.cpp +++ b/theseus/desktop/playlist_maker.cpp @@ -130,9 +130,8 @@ static void DrawItemsPane() { ImGui::Spacing(); ImGui::Separator(); - // No outer lock: count getters self-lock; iterators race with scans - // the same way menu_bar.cpp's media counts do. Scans are explicit - // refresh actions, not background sweeps, so the race is benign. + // Counts self-lock. The listing below holds the DB lock, since a scan + // swaps the vectors out from under the C string getters. int movieCount = MediaDB_GetMovieCount(); int showCount = MediaDB_GetShowCount(); @@ -150,6 +149,7 @@ static void DrawItemsPane() { return; } + MediaDB_Lock(); int movieAvail = 0; for (int i = 0; i < movieCount; i++) if (!inPlaylist.count(MediaDB_GetMoviePathC(i))) movieAvail++; @@ -234,6 +234,7 @@ static void DrawItemsPane() { ImGui::PopID(); } } + MediaDB_Unlock(); } void RenderPlaylistMaker() { From 25aab0fb2517ec317fe9ff785a2f7576d7d6b1f0 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:49:52 -0500 Subject: [PATCH 025/167] Added a small host layer for desktop file calls The Xbox file functions now reach the OS through host.h, with one version for mac and Linux and one for Windows. Folder listing works the same everywhere, so Windows gets the games.ini-only folders it was missing. Needs a Windows test. --- build/Makefile | 15 ++- theseus/desktop/host.h | 42 ++++++ theseus/desktop/host_posix.cpp | 105 +++++++++++++++ theseus/desktop/host_win.cpp | 157 ++++++++++++++++++++++ theseus/desktop/media_db.cpp | 8 +- theseus/desktop/platform_shim.h | 1 + theseus/desktop/xbox_file.cpp | 227 ++++++++++---------------------- 7 files changed, 392 insertions(+), 163 deletions(-) create mode 100644 theseus/desktop/host.h create mode 100644 theseus/desktop/host_posix.cpp create mode 100644 theseus/desktop/host_win.cpp diff --git a/build/Makefile b/build/Makefile index a196f11..5774312 100644 --- a/build/Makefile +++ b/build/Makefile @@ -791,7 +791,16 @@ DESKTOP_SRCS_ENGINE = \ $(THESEUS_SRC)/engine/math_node.cpp \ $(THESEUS_SRC)/shared/date_node.cpp +# The host layer: one per OS, picked by host. host_win.cpp is the only +# desktop file that sees . +ifeq ($(filter Darwin Linux,$(UNAME_S)),) + DESKTOP_SRCS_HOST = $(THESEUS_SRC)/desktop/host_win.cpp +else + DESKTOP_SRCS_HOST = $(THESEUS_SRC)/desktop/host_posix.cpp +endif + DESKTOP_SRCS_FILE = \ + $(DESKTOP_SRCS_HOST) \ $(THESEUS_SRC)/desktop/xbox_file.cpp \ $(THESEUS_SRC)/desktop/xip.cpp \ $(THESEUS_SRC)/shared/file_util.cpp \ @@ -1302,8 +1311,10 @@ endif WIN64_SRCS = $(filter-out \ $(THESEUS_SRC)/desktop/imgui/imgui_impl_opengl3.cpp \ - $(THESEUS_SRC)/desktop/imgui_impl_bgfx.cpp, $(DESKTOP_ALL_SRCS)) -WIN64_SRCS += $(THESEUS_SRC)/desktop/imgui_impl_bgfx.cpp + $(THESEUS_SRC)/desktop/imgui_impl_bgfx.cpp \ + $(DESKTOP_SRCS_HOST), $(DESKTOP_ALL_SRCS)) +WIN64_SRCS += $(THESEUS_SRC)/desktop/imgui_impl_bgfx.cpp \ + $(THESEUS_SRC)/desktop/host_win.cpp # Same source list as desktop. the compiler reads the same files. WIN64_OBJS = $(patsubst $(THESEUS_SRC)/%.cpp,$(WIN64_DIR)/%.o,$(WIN64_SRCS)) diff --git a/theseus/desktop/host.h b/theseus/desktop/host.h new file mode 100644 index 0000000..1c7f8b2 --- /dev/null +++ b/theseus/desktop/host.h @@ -0,0 +1,42 @@ +// host.h: the narrow door from the desktop build to the host OS. Plain +// types only, so it never drags an OS header into Xbox-shaped code. +// host_posix.cpp (mac, Linux) and host_win.cpp implement it. +// +// Every path here is a host path with '/' separators. Xbox paths go +// through xbox_path.h first. + +#pragma once + +#include +#include +#include + +enum { + HOST_ATTR_DIR = 0x01, + HOST_ATTR_HIDDEN = 0x02, + HOST_ATTR_SYSTEM = 0x04, + HOST_ATTR_READONLY = 0x08, +}; + +struct HostFileInfo { + uint32_t attrs; // HOST_ATTR_* + uint64_t size; + int64_t mtime; // seconds since 1970, UTC +}; + +// Files and folders. Host_MakeDir is true when the folder exists afterwards. +bool Host_MakeDir(const char* path); +bool Host_RemoveDir(const char* path); +bool Host_RemoveFile(const char* path); +bool Host_Stat(const char* path, HostFileInfo* out); +bool Host_StatOpenFile(FILE* f, HostFileInfo* out); +bool Host_SetAttrs(const char* path, uint32_t attrs); +bool Host_RenameReplace(const char* from, const char* to); +bool Host_CopyFile(const char* from, const char* to, bool failIfExists); +bool Host_DiskSpace(const char* path, uint64_t* freeBytes, uint64_t* totalBytes); + +// Folder listing. Hands back every entry, "." and ".." included. +struct HostDir; +HostDir* Host_OpenDir(const char* path); +bool Host_ReadDir(HostDir* dir, char* name, size_t nameSize, HostFileInfo* info); +void Host_CloseDir(HostDir* dir); diff --git a/theseus/desktop/host_posix.cpp b/theseus/desktop/host_posix.cpp new file mode 100644 index 0000000..084ab03 --- /dev/null +++ b/theseus/desktop/host_posix.cpp @@ -0,0 +1,105 @@ +// host_posix.cpp: host.h for macOS and Linux. + +#include "host.h" + +#include +#include +#include +#include +#include +#include +#include + +static void FillInfo(const struct stat& st, HostFileInfo* out) { + out->attrs = S_ISDIR(st.st_mode) ? HOST_ATTR_DIR : 0; + out->size = (uint64_t)st.st_size; + out->mtime = (int64_t)st.st_mtime; +} + +bool Host_MakeDir(const char* path) { + return mkdir(path, 0755) == 0 || errno == EEXIST; +} + +bool Host_RemoveDir(const char* path) { return rmdir(path) == 0; } +bool Host_RemoveFile(const char* path) { return remove(path) == 0; } + +bool Host_Stat(const char* path, HostFileInfo* out) { + struct stat st; + if (stat(path, &st) != 0) return false; + if (out) FillInfo(st, out); + return true; +} + +bool Host_StatOpenFile(FILE* f, HostFileInfo* out) { + struct stat st; + if (!f || fstat(fileno(f), &st) != 0) return false; + if (out) FillInfo(st, out); + return true; +} + +// FATX attribute bits have no POSIX meaning worth keeping. +bool Host_SetAttrs(const char*, uint32_t) { return true; } + +bool Host_RenameReplace(const char* from, const char* to) { + return rename(from, to) == 0; +} + +bool Host_CopyFile(const char* from, const char* to, bool failIfExists) { + struct stat st; + if (failIfExists && stat(to, &st) == 0) return false; + FILE* in = fopen(from, "rb"); + if (!in) return false; + FILE* out = fopen(to, "wb"); + if (!out) { fclose(in); return false; } + char buf[64 * 1024]; + size_t n; + bool ok = true; + while ((n = fread(buf, 1, sizeof(buf), in)) > 0) + if (fwrite(buf, 1, n, out) != n) { ok = false; break; } + fclose(in); + if (fclose(out) != 0) ok = false; + return ok; +} + +bool Host_DiskSpace(const char* path, uint64_t* freeBytes, uint64_t* totalBytes) { + struct statvfs vfs; + if (statvfs(path, &vfs) != 0) return false; + if (freeBytes) *freeBytes = (uint64_t)vfs.f_bavail * vfs.f_frsize; + if (totalBytes) *totalBytes = (uint64_t)vfs.f_blocks * vfs.f_frsize; + return true; +} + +struct HostDir { + DIR* d; + char path[512]; +}; + +HostDir* Host_OpenDir(const char* path) { + DIR* d = opendir(path); + if (!d) return NULL; + HostDir* dir = (HostDir*)calloc(1, sizeof(HostDir)); + if (!dir) { closedir(d); return NULL; } + dir->d = d; + snprintf(dir->path, sizeof(dir->path), "%s", path); + return dir; +} + +bool Host_ReadDir(HostDir* dir, char* name, size_t nameSize, HostFileInfo* info) { + if (!dir) return false; + struct dirent* e = readdir(dir->d); + if (!e) return false; + snprintf(name, nameSize, "%s", e->d_name); + if (info) { + memset(info, 0, sizeof(*info)); + char full[768]; + snprintf(full, sizeof(full), "%s/%s", dir->path, e->d_name); + Host_Stat(full, info); + } + return true; +} + +void Host_CloseDir(HostDir* dir) { + if (!dir) return; + closedir(dir->d); + free(dir); +} diff --git a/theseus/desktop/host_win.cpp b/theseus/desktop/host_win.cpp new file mode 100644 index 0000000..8c6432a --- /dev/null +++ b/theseus/desktop/host_win.cpp @@ -0,0 +1,157 @@ +// host_win.cpp: host.h for Windows. The one place the desktop build talks +// to kernel32 for files. Never include std.h or platform_shim.h here. + +#include "host.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +#include +#include +#include + +// Win32 takes '/' almost everywhere, but not everywhere. Hand it '\'. +static void ToWin(char* dst, size_t size, const char* src) { + snprintf(dst, size, "%s", src); + for (char* p = dst; *p; p++) if (*p == '/') *p = '\\'; +} + +// 100ns ticks since 1601 to seconds since 1970. +static int64_t ToUnix(const FILETIME& ft) { + uint64_t t = ((uint64_t)ft.dwHighDateTime << 32) | ft.dwLowDateTime; + return (int64_t)(t / 10000000ULL) - 11644473600LL; +} + +static uint32_t ToHostAttrs(DWORD a) { + uint32_t r = 0; + if (a & FILE_ATTRIBUTE_DIRECTORY) r |= HOST_ATTR_DIR; + if (a & FILE_ATTRIBUTE_HIDDEN) r |= HOST_ATTR_HIDDEN; + if (a & FILE_ATTRIBUTE_SYSTEM) r |= HOST_ATTR_SYSTEM; + if (a & FILE_ATTRIBUTE_READONLY) r |= HOST_ATTR_READONLY; + return r; +} + +bool Host_MakeDir(const char* path) { + char p[MAX_PATH]; + ToWin(p, sizeof(p), path); + return CreateDirectoryA(p, NULL) || GetLastError() == ERROR_ALREADY_EXISTS; +} + +bool Host_RemoveDir(const char* path) { + char p[MAX_PATH]; + ToWin(p, sizeof(p), path); + return RemoveDirectoryA(p) != FALSE; +} + +bool Host_RemoveFile(const char* path) { + char p[MAX_PATH]; + ToWin(p, sizeof(p), path); + return DeleteFileA(p) != FALSE; +} + +bool Host_Stat(const char* path, HostFileInfo* out) { + char p[MAX_PATH]; + ToWin(p, sizeof(p), path); + WIN32_FILE_ATTRIBUTE_DATA fad; + if (!GetFileAttributesExA(p, GetFileExInfoStandard, &fad)) return false; + if (out) { + out->attrs = ToHostAttrs(fad.dwFileAttributes); + out->size = ((uint64_t)fad.nFileSizeHigh << 32) | fad.nFileSizeLow; + out->mtime = ToUnix(fad.ftLastWriteTime); + } + return true; +} + +bool Host_StatOpenFile(FILE* f, HostFileInfo* out) { + struct _stat64 st; + if (!f || _fstat64(_fileno(f), &st) != 0) return false; + if (out) { + out->attrs = (st.st_mode & _S_IFDIR) ? HOST_ATTR_DIR : 0; + out->size = (uint64_t)st.st_size; + out->mtime = (int64_t)st.st_mtime; + } + return true; +} + +bool Host_SetAttrs(const char* path, uint32_t attrs) { + char p[MAX_PATH]; + ToWin(p, sizeof(p), path); + DWORD a = 0; + if (attrs & HOST_ATTR_HIDDEN) a |= FILE_ATTRIBUTE_HIDDEN; + if (attrs & HOST_ATTR_SYSTEM) a |= FILE_ATTRIBUTE_SYSTEM; + if (attrs & HOST_ATTR_READONLY) a |= FILE_ATTRIBUTE_READONLY; + return SetFileAttributesA(p, a ? a : FILE_ATTRIBUTE_NORMAL) != FALSE; +} + +// The CRT's rename() won't replace an existing file. +bool Host_RenameReplace(const char* from, const char* to) { + char f[MAX_PATH], t[MAX_PATH]; + ToWin(f, sizeof(f), from); + ToWin(t, sizeof(t), to); + return MoveFileExA(f, t, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED) != FALSE; +} + +bool Host_CopyFile(const char* from, const char* to, bool failIfExists) { + char f[MAX_PATH], t[MAX_PATH]; + ToWin(f, sizeof(f), from); + ToWin(t, sizeof(t), to); + return CopyFileA(f, t, failIfExists) != FALSE; +} + +bool Host_DiskSpace(const char* path, uint64_t* freeBytes, uint64_t* totalBytes) { + char p[MAX_PATH]; + ToWin(p, sizeof(p), path); + ULARGE_INTEGER avail, total; + if (!GetDiskFreeSpaceExA(p, &avail, &total, NULL)) return false; + if (freeBytes) *freeBytes = avail.QuadPart; + if (totalBytes) *totalBytes = total.QuadPart; + return true; +} + +struct HostDir { + HANDLE find; + WIN32_FIND_DATAA first; + bool haveFirst; +}; + +HostDir* Host_OpenDir(const char* path) { + char p[MAX_PATH]; + char pattern[MAX_PATH + 4]; + ToWin(p, sizeof(p), path); + snprintf(pattern, sizeof(pattern), "%s\\*", p); + HostDir* dir = (HostDir*)calloc(1, sizeof(HostDir)); + if (!dir) return NULL; + dir->find = FindFirstFileA(pattern, &dir->first); + if (dir->find == INVALID_HANDLE_VALUE) { free(dir); return NULL; } + dir->haveFirst = true; + return dir; +} + +bool Host_ReadDir(HostDir* dir, char* name, size_t nameSize, HostFileInfo* info) { + if (!dir) return false; + WIN32_FIND_DATAA fd; + if (dir->haveFirst) { + fd = dir->first; + dir->haveFirst = false; + } else if (!FindNextFileA(dir->find, &fd)) { + return false; + } + snprintf(name, nameSize, "%s", fd.cFileName); + if (info) { + info->attrs = ToHostAttrs(fd.dwFileAttributes); + info->size = ((uint64_t)fd.nFileSizeHigh << 32) | fd.nFileSizeLow; + info->mtime = ToUnix(fd.ftLastWriteTime); + } + return true; +} + +void Host_CloseDir(HostDir* dir) { + if (!dir) return; + FindClose(dir->find); + free(dir); +} diff --git a/theseus/desktop/media_db.cpp b/theseus/desktop/media_db.cpp index 5484fa4..1fef47a 100644 --- a/theseus/desktop/media_db.cpp +++ b/theseus/desktop/media_db.cpp @@ -12,6 +12,7 @@ #include "tmdb.h" #include "playlist.h" #include "media_db.h" +#include "host.h" // ============================================================================ // CMediaCollection - filesystem-backed library of movies + TV shows. @@ -458,12 +459,7 @@ static void WriteCacheTo(const std::vector& movies, } fclose(f); -#ifdef THESEUS_HOST_WIN - // MS CRT rename() won't replace an existing file. - MoveFileExA(tmp.c_str(), path, MOVEFILE_REPLACE_EXISTING); -#else - rename(tmp.c_str(), path); -#endif + Host_RenameReplace(tmp.c_str(), path); printf("[MediaDB] Saved cache: %u movies, %u shows\n", nMovies, nShows); } diff --git a/theseus/desktop/platform_shim.h b/theseus/desktop/platform_shim.h index 671e132..1cb774b 100644 --- a/theseus/desktop/platform_shim.h +++ b/theseus/desktop/platform_shim.h @@ -247,6 +247,7 @@ typedef struct _WIN32_FILE_ATTRIBUTE_DATA { } WIN32_FILE_ATTRIBUTE_DATA; // File attribute constants +#define FILE_ATTRIBUTE_READONLY 0x01 #define FILE_ATTRIBUTE_DIRECTORY 0x10 #define FILE_ATTRIBUTE_HIDDEN 0x02 #define FILE_ATTRIBUTE_SYSTEM 0x04 diff --git a/theseus/desktop/xbox_file.cpp b/theseus/desktop/xbox_file.cpp index a13506a..952c85d 100644 --- a/theseus/desktop/xbox_file.cpp +++ b/theseus/desktop/xbox_file.cpp @@ -9,17 +9,9 @@ #include "std.h" #include "theseus.h" #include "file_util.h" +#include "host.h" -#include #include -#include -#ifdef THESEUS_HOST_WIN -#include -#else -#include -#include -#include -#endif extern char g_qcowPath[512]; FILE* XboxFS_FATXReadFile(const char* xboxPath); @@ -33,14 +25,6 @@ static bool IsWriteDisposition(DWORD access, DWORD disp) { disp == OPEN_ALWAYS || disp == TRUNCATE_EXISTING; } -static void HostMkdir(const char* dir) { -#ifdef THESEUS_HOST_WIN - _mkdir(dir); -#else - mkdir(dir, 0755); -#endif -} - // Creates every missing parent of a translated host path. static void MakeParents(const char* hostPath) { char dir[512]; @@ -51,10 +35,10 @@ static void MakeParents(const char* hostPath) { for (char* p = dir + 1; *p; p++) { if (*p != '/') continue; *p = '\0'; - HostMkdir(dir); + Host_MakeDir(dir); *p = '/'; } - HostMkdir(dir); + Host_MakeDir(dir); } #ifdef THESEUS_HOST_WIN @@ -63,6 +47,23 @@ static void ToBackslash(char* s) { } #endif +static DWORD ToFileAttributes(uint32_t attrs) { + DWORD a = 0; + if (attrs & HOST_ATTR_DIR) a |= FILE_ATTRIBUTE_DIRECTORY; + if (attrs & HOST_ATTR_HIDDEN) a |= FILE_ATTRIBUTE_HIDDEN; + if (attrs & HOST_ATTR_SYSTEM) a |= FILE_ATTRIBUTE_SYSTEM; + if (attrs & HOST_ATTR_READONLY) a |= FILE_ATTRIBUTE_READONLY; + return a ? a : FILE_ATTRIBUTE_NORMAL; +} + +static FILETIME ToFileTime(int64_t unixSeconds) { + uint64_t t = (uint64_t)(unixSeconds + 11644473600LL) * 10000000ULL; + FILETIME ft; + ft.dwLowDateTime = (DWORD)(t & 0xFFFFFFFF); + ft.dwHighDateTime = (DWORD)(t >> 32); + return ft; +} + // Minimal fake XBE for a virtual game: a 0x178-byte header then a 0x1EC-byte // certificate carrying the title name (UTF-16) and title ID. Title // enumeration only reads those two structures. @@ -196,116 +197,82 @@ bool TheseusCreateDirectory(LPCTSTR lpPathName, LPSECURITY_ATTRIBUTES) CleanFilePath(xboxPath, lpPathName); char host[512]; if (!XboxPath_TranslateForWrite(xboxPath, host, sizeof(host)) || !host[0]) return false; -#ifdef THESEUS_HOST_WIN - ToBackslash(host); - return CreateDirectoryA(host, NULL) != FALSE; -#else - return mkdir(host, 0755) == 0 || errno == EEXIST; -#endif + return Host_MakeDir(host); } BOOL TheseusDeleteFile(LPCSTR path) { char host[512]; if (!XboxPath_TranslateForWrite(path, host, sizeof(host)) || !host[0]) return FALSE; -#ifdef THESEUS_HOST_WIN - ToBackslash(host); - return DeleteFileA(host); -#else - return remove(host) == 0; -#endif + return Host_RemoveFile(host); } BOOL TheseusRemoveDirectory(LPCSTR path) { char host[512]; if (!XboxPath_TranslateForWrite(path, host, sizeof(host)) || !host[0]) return FALSE; -#ifdef THESEUS_HOST_WIN - ToBackslash(host); - return RemoveDirectoryA(host); -#else - return rmdir(host) == 0; -#endif + return Host_RemoveDir(host); } BOOL TheseusSetFileAttributes(LPCSTR path, DWORD attr) { char host[512]; if (!XboxPath_TranslateForWrite(path, host, sizeof(host)) || !host[0]) return FALSE; -#ifdef THESEUS_HOST_WIN - ToBackslash(host); - return SetFileAttributesA(host, attr); -#else - (void)attr; // FATX attribute bits have no POSIX meaning worth keeping - return TRUE; -#endif + uint32_t a = 0; + if (attr & FILE_ATTRIBUTE_HIDDEN) a |= HOST_ATTR_HIDDEN; + if (attr & FILE_ATTRIBUTE_SYSTEM) a |= HOST_ATTR_SYSTEM; + if (attr & FILE_ATTRIBUTE_READONLY) a |= HOST_ATTR_READONLY; + return Host_SetAttrs(host, a); } DWORD TheseusGetFileAttributes(LPCSTR path) { char host[512]; if (!XboxPath_Translate(path, host, sizeof(host)) || !host[0]) return INVALID_FILE_ATTRIBUTES; -#ifdef THESEUS_HOST_WIN - ToBackslash(host); - return GetFileAttributesA(host); -#else - struct stat st; - if (stat(host, &st) != 0) return INVALID_FILE_ATTRIBUTES; - return S_ISDIR(st.st_mode) ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL; -#endif + HostFileInfo info; + if (!Host_Stat(host, &info)) return INVALID_FILE_ATTRIBUTES; + return ToFileAttributes(info.attrs); } -BOOL TheseusGetFileAttributesEx(LPCSTR path, GET_FILEEX_INFO_LEVELS level, LPVOID info) +BOOL TheseusGetFileAttributesEx(LPCSTR path, GET_FILEEX_INFO_LEVELS level, LPVOID out) { + (void)level; char host[512]; if (!XboxPath_Translate(path, host, sizeof(host)) || !host[0]) return FALSE; -#ifdef THESEUS_HOST_WIN - ToBackslash(host); - return GetFileAttributesExA(host, level, info); -#else - (void)level; - struct stat st; - if (stat(host, &st) != 0) return FALSE; - if (info) { - WIN32_FILE_ATTRIBUTE_DATA* fad = (WIN32_FILE_ATTRIBUTE_DATA*)info; + HostFileInfo info; + if (!Host_Stat(host, &info)) return FALSE; + if (out) { + WIN32_FILE_ATTRIBUTE_DATA* fad = (WIN32_FILE_ATTRIBUTE_DATA*)out; memset(fad, 0, sizeof(*fad)); - fad->dwFileAttributes = S_ISDIR(st.st_mode) ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL; - fad->nFileSizeLow = (DWORD)(st.st_size & 0xFFFFFFFF); - fad->nFileSizeHigh = (DWORD)((uint64_t)st.st_size >> 32); + fad->dwFileAttributes = ToFileAttributes(info.attrs); + fad->nFileSizeLow = (DWORD)(info.size & 0xFFFFFFFF); + fad->nFileSizeHigh = (DWORD)(info.size >> 32); + fad->ftLastWriteTime = ToFileTime(info.mtime); + fad->ftCreationTime = fad->ftLastAccessTime = fad->ftLastWriteTime; } return TRUE; -#endif } BOOL TheseusGetDiskFreeSpaceEx(LPCSTR path, ULARGE_INTEGER* pFree, ULARGE_INTEGER* pTotal, ULARGE_INTEGER* pTotalFree) { char host[512]; if (!XboxPath_Translate(path, host, sizeof(host)) || !host[0]) return FALSE; -#ifdef THESEUS_HOST_WIN - ToBackslash(host); - return GetDiskFreeSpaceExA(host, pFree, pTotal, pTotalFree); -#else - // No real free-space query on POSIX yet; report empty like it always has. - if (pFree) pFree->QuadPart = 0; - if (pTotal) pTotal->QuadPart = 0; - if (pTotalFree) pTotalFree->QuadPart = 0; + uint64_t freeBytes = 0, totalBytes = 0; + if (!Host_DiskSpace(host, &freeBytes, &totalBytes)) return FALSE; + if (pFree) pFree->QuadPart = freeBytes; + if (pTotal) pTotal->QuadPart = totalBytes; + if (pTotalFree) pTotalFree->QuadPart = freeBytes; return TRUE; -#endif } +// Always replaces, like the MOVEFILE_REPLACE_EXISTING every caller passes. BOOL TheseusMoveFile(LPCSTR src, LPCSTR dst, DWORD flags) { + (void)flags; char s[512], d[512]; if (!XboxPath_TranslateForWrite(src, s, sizeof(s)) || !s[0]) return FALSE; if (!XboxPath_TranslateForWrite(dst, d, sizeof(d)) || !d[0]) return FALSE; -#ifdef THESEUS_HOST_WIN - ToBackslash(s); - ToBackslash(d); - return MoveFileExA(s, d, flags); -#else - (void)flags; - return rename(s, d) == 0; -#endif + return Host_RenameReplace(s, d); } BOOL TheseusCopyFile(LPCSTR src, LPCSTR dst, BOOL bFailIfExists) @@ -313,26 +280,7 @@ BOOL TheseusCopyFile(LPCSTR src, LPCSTR dst, BOOL bFailIfExists) char s[512], d[512]; if (!XboxPath_Translate(src, s, sizeof(s)) || !s[0]) return FALSE; if (!XboxPath_TranslateForWrite(dst, d, sizeof(d)) || !d[0]) return FALSE; -#ifdef THESEUS_HOST_WIN - ToBackslash(s); - ToBackslash(d); - return CopyFileA(s, d, bFailIfExists); -#else - struct stat st; - if (bFailIfExists && stat(d, &st) == 0) return FALSE; - FILE* in = fopen(s, "rb"); - if (!in) return FALSE; - FILE* out = fopen(d, "wb"); - if (!out) { fclose(in); return FALSE; } - char buf[64 * 1024]; - size_t n; - bool ok = true; - while ((n = fread(buf, 1, sizeof(buf), in)) > 0) - if (fwrite(buf, 1, n, out) != n) { ok = false; break; } - fclose(in); - if (fclose(out) != 0) ok = false; - return ok; -#endif + return Host_CopyFile(s, d, bFailIfExists != FALSE); } FILE* TheseusFOpen(const TCHAR* path, const TCHAR* mode) @@ -361,11 +309,7 @@ const DWORD kFindMagic = 0x46464858; // 'XHFF' struct FindHandle { DWORD magic; -#ifdef THESEUS_HOST_WIN - HANDLE real; // Win32 find handle, or INVALID_HANDLE_VALUE -#else - DIR* real; -#endif + HostDir* real; // NULL once the real entries run out char dirPath[512]; // translated host dir char pattern[256]; // basename pattern from the call int vgIndices[VGAMES_MAX]; @@ -373,9 +317,9 @@ struct FindHandle { int vgPos; }; +// '*' any run, '?' one char, case-insensitive. Win32 rules, so every host +// matches the same names. bool PatternMatch(const char* pattern, const char* name) { -#ifdef THESEUS_HOST_WIN - // '*' any run, '?' one char, case-insensitive. while (*pattern && *name) { if (*pattern == '*') { pattern++; @@ -391,16 +335,12 @@ bool PatternMatch(const char* pattern, const char* name) { } while (*pattern == '*') pattern++; return *pattern == 0 && *name == 0; -#else - return fnmatch(pattern, name, FNM_CASEFOLD) == 0; -#endif } bool OnDisk(const FindHandle* ffh, const char* name) { char full[768]; snprintf(full, sizeof(full), "%s/%s", ffh->dirPath, name); - struct stat st; - return stat(full, &st) == 0; + return Host_Stat(full, NULL); } BOOL NextVirtual(FindHandle* ffh, WIN32_FIND_DATAA* fd) { @@ -419,20 +359,18 @@ BOOL NextVirtual(FindHandle* ffh, WIN32_FIND_DATAA* fd) { return FALSE; } -#ifndef THESEUS_HOST_WIN BOOL NextReal(FindHandle* ffh, WIN32_FIND_DATAA* fd) { - struct dirent* entry; - while ((entry = readdir(ffh->real)) != NULL) { - if (!PatternMatch(ffh->pattern, entry->d_name)) continue; + char name[260]; + HostFileInfo info; + while (Host_ReadDir(ffh->real, name, sizeof(name), &info)) { + if (!PatternMatch(ffh->pattern, name)) continue; memset(fd, 0, sizeof(*fd)); - strncpy(fd->cFileName, entry->d_name, sizeof(fd->cFileName) - 1); - char full[768]; - snprintf(full, sizeof(full), "%s/%s", ffh->dirPath, entry->d_name); - struct stat st; - if (stat(full, &st) == 0) { - if (S_ISDIR(st.st_mode)) fd->dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY; - fd->nFileSizeLow = (DWORD)st.st_size; - } + strncpy(fd->cFileName, name, sizeof(fd->cFileName) - 1); + fd->dwFileAttributes = ToFileAttributes(info.attrs); + fd->nFileSizeLow = (DWORD)(info.size & 0xFFFFFFFF); + fd->nFileSizeHigh = (DWORD)(info.size >> 32); + fd->ftLastWriteTime = ToFileTime(info.mtime); + fd->ftCreationTime = fd->ftLastAccessTime = fd->ftLastWriteTime; return TRUE; } return FALSE; @@ -444,11 +382,11 @@ HANDLE FindInVirtualFolder(const char* dirPath, const char* pattern, WIN32_FIND_ if (VGames_MatchFolder(dirPath) < 0) return INVALID_HANDLE_VALUE; const char* name = NULL; DWORD size = 0; - if (fnmatch("*.xbe", pattern, FNM_CASEFOLD) == 0 || fnmatch("default.xbe", pattern, FNM_CASEFOLD) == 0) { + if (PatternMatch("*.xbe", pattern) || PatternMatch("default.xbe", pattern)) { name = "default.xbe"; size = 4096; - } else if (fnmatch("default.uixshortcut", pattern, FNM_CASEFOLD) == 0 || - fnmatch("*.*", pattern, FNM_CASEFOLD) == 0 || strcmp(pattern, "*") == 0) { + } else if (PatternMatch("default.uixshortcut", pattern) || + PatternMatch("*.*", pattern) || strcmp(pattern, "*") == 0) { name = "default.uixshortcut"; size = 256; } @@ -464,7 +402,6 @@ HANDLE FindInVirtualFolder(const char* dirPath, const char* pattern, WIN32_FIND_ fd->nFileSizeLow = size; return (HANDLE)ffh; } -#endif } // namespace @@ -499,22 +436,14 @@ HANDLE TheseusFindFirstFile(LPCSTR path, WIN32_FIND_DATAA* fd) if (VGames_ParseDir(dirPath, drive, cat, sizeof(cat))) ffh->vgCount = VGames_GetForDirectory(drive, cat, ffh->vgIndices, VGAMES_MAX); -#ifdef THESEUS_HOST_WIN - char winPath[512]; - snprintf(winPath, sizeof(winPath), "%s", host); - ToBackslash(winPath); - ffh->real = FindFirstFileA(winPath, fd); - if (ffh->real != INVALID_HANDLE_VALUE) return (HANDLE)ffh; -#else - ffh->real = opendir(dirPath); + ffh->real = Host_OpenDir(dirPath); if (!ffh->real) { free(ffh); return FindInVirtualFolder(dirPath, pattern, fd); } if (NextReal(ffh, fd)) return (HANDLE)ffh; - closedir(ffh->real); + Host_CloseDir(ffh->real); ffh->real = NULL; -#endif if (NextVirtual(ffh, fd)) return (HANDLE)ffh; free(ffh); @@ -526,19 +455,11 @@ BOOL TheseusFindNextFile(HANDLE h, WIN32_FIND_DATAA* fd) if (!h || h == INVALID_HANDLE_VALUE || !fd) return FALSE; FindHandle* ffh = (FindHandle*)h; if (ffh->magic != kFindMagic) return FALSE; -#ifdef THESEUS_HOST_WIN - if (ffh->real != INVALID_HANDLE_VALUE) { - if (FindNextFileA(ffh->real, fd)) return TRUE; - FindClose(ffh->real); - ffh->real = INVALID_HANDLE_VALUE; - } -#else if (ffh->real) { if (NextReal(ffh, fd)) return TRUE; - closedir(ffh->real); + Host_CloseDir(ffh->real); ffh->real = NULL; } -#endif return NextVirtual(ffh, fd); } @@ -547,11 +468,7 @@ BOOL TheseusFindClose(HANDLE h) if (!h || h == INVALID_HANDLE_VALUE) return FALSE; FindHandle* ffh = (FindHandle*)h; if (ffh->magic != kFindMagic) return FALSE; -#ifdef THESEUS_HOST_WIN - if (ffh->real != INVALID_HANDLE_VALUE) FindClose(ffh->real); -#else - if (ffh->real) closedir(ffh->real); -#endif + if (ffh->real) Host_CloseDir(ffh->real); free(ffh); return TRUE; } From 89db5b8b08de44552f723e14071a87563f4bacbd Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:51:47 -0500 Subject: [PATCH 026/167] Downloads land under a temp name and rename into place Http_GetToFile wrote straight to the final path, so the art loader could pick up half a jpg and a failed fetch wiped whatever was there. It now writes beside the target and renames on success. Needs a Windows test. --- theseus/desktop/http_util.cpp | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/theseus/desktop/http_util.cpp b/theseus/desktop/http_util.cpp index 1a8b9cf..928a97c 100644 --- a/theseus/desktop/http_util.cpp +++ b/theseus/desktop/http_util.cpp @@ -1,7 +1,12 @@ #include "http_util.h" +#include "platform.h" #include #include #include +#include +#ifdef THESEUS_HOST_WIN +#include +#endif static std::once_flag s_initOnce; static void EnsureGlobalInit() { @@ -101,17 +106,28 @@ std::string Http_GetToString(const std::string& url) { bool Http_GetToFile(const std::string& url, const std::string& outPath) { EnsureGlobalInit(); - FILE* fp = fopen(outPath.c_str(), "wb"); + // Download beside the target and rename on success, so a reader never + // sees half a file and a failed fetch never clobbers a good one. + static std::atomic s_partId{0}; + std::string part = outPath + ".part" + std::to_string(s_partId.fetch_add(1)); + FILE* fp = fopen(part.c_str(), "wb"); if (!fp) return false; CURL* h = curl_easy_init(); - if (!h) { fclose(fp); remove(outPath.c_str()); return false; } + if (!h) { fclose(fp); remove(part.c_str()); return false; } ApplyCommonOpts(h, url.c_str()); curl_easy_setopt(h, CURLOPT_FAILONERROR, 1L); curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, WriteToFile); curl_easy_setopt(h, CURLOPT_WRITEDATA, fp); CURLcode rc = curl_easy_perform(h); curl_easy_cleanup(h); - fclose(fp); - if (rc != CURLE_OK) { remove(outPath.c_str()); return false; } + bool wrote = (fclose(fp) == 0); + if (rc != CURLE_OK || !wrote) { remove(part.c_str()); return false; } +#ifdef THESEUS_HOST_WIN + // MS CRT rename() won't replace an existing file. + bool moved = MoveFileExA(part.c_str(), outPath.c_str(), MOVEFILE_REPLACE_EXISTING) != 0; +#else + bool moved = rename(part.c_str(), outPath.c_str()) == 0; +#endif + if (!moved) { remove(part.c_str()); return false; } return true; } From 49a25f4e8ae4082acc708be7d37440bcf1238009 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:52:32 -0500 Subject: [PATCH 027/167] Checked server ids before using them as art filenames Plex, Jellyfin and xCloud ids came straight off the server into cache paths, so an id with a slash or .. in it could write outside the art folder. Anything that isn't a plain filename is skipped now, including xCloud rows whose id becomes an icon name. --- theseus/desktop/http_util.cpp | 10 ++++++++++ theseus/desktop/http_util.h | 4 ++++ theseus/desktop/jellyfin_client.cpp | 4 +++- theseus/desktop/plex_client.cpp | 4 +++- theseus/desktop/xcloud_client.cpp | 4 +++- theseus/desktop/xcloud_vgames.cpp | 6 ++++-- 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/theseus/desktop/http_util.cpp b/theseus/desktop/http_util.cpp index 928a97c..eabeaa3 100644 --- a/theseus/desktop/http_util.cpp +++ b/theseus/desktop/http_util.cpp @@ -131,3 +131,13 @@ bool Http_GetToFile(const std::string& url, const std::string& outPath) { if (!moved) { remove(part.c_str()); return false; } return true; } + +bool Http_IsSafeFileId(const std::string& id) { + if (id.empty() || id.size() > 128 || id[0] == '.') return false; + for (char c : id) { + bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.'; + if (!ok) return false; + } + return true; +} diff --git a/theseus/desktop/http_util.h b/theseus/desktop/http_util.h index 4ed0417..c252ace 100644 --- a/theseus/desktop/http_util.h +++ b/theseus/desktop/http_util.h @@ -18,6 +18,10 @@ struct HttpResponse { std::string Http_GetToString(const std::string& url); bool Http_GetToFile(const std::string& url, const std::string& outPath); +// True when a server-supplied id is fine as a bare filename: letters, digits, +// '-', '_' and inner dots only, 1 to 128 chars, never "." or "..". +bool Http_IsSafeFileId(const std::string& id); + // Header + status-aware GET/POST. Non-2xx is NOT a failure; caller decides. HttpResponse Http_Get(const std::string& url, const HttpHeaders& headers = {}); HttpResponse Http_Post(const std::string& url, diff --git a/theseus/desktop/jellyfin_client.cpp b/theseus/desktop/jellyfin_client.cpp index 290e07e..d3ac9f9 100644 --- a/theseus/desktop/jellyfin_client.cpp +++ b/theseus/desktop/jellyfin_client.cpp @@ -580,6 +580,7 @@ static std::string ArtHostPath(const std::string& itemId) std::string Jellyfin_ArtCachePath(const std::string& itemId) { + if (!Http_IsSafeFileId(itemId)) return ""; return "E:\\Jellyfin\\" + itemId + ".jpg"; } @@ -612,7 +613,8 @@ static void ArtWorker(std::string itemId, std::string tag, void Jellyfin_QueueArtDownload(const std::string& itemId, const std::string& tag) { - if (itemId.empty()) return; + // The id comes off the server and ends up in a file path. + if (!Http_IsSafeFileId(itemId)) return; // Workers erase from the map on other threads, so it is only touched under the lock. std::atomic* flag; { diff --git a/theseus/desktop/plex_client.cpp b/theseus/desktop/plex_client.cpp index 267ea19..6396ff0 100644 --- a/theseus/desktop/plex_client.cpp +++ b/theseus/desktop/plex_client.cpp @@ -652,6 +652,7 @@ static std::string Plex_ArtCacheHostPath(const std::string& ratingKey) // XBox-style path for the XAP. xboxfs routes E:\ -> Library/. std::string Plex_ArtCachePath(const std::string& ratingKey) { + if (!Http_IsSafeFileId(ratingKey)) return ""; return "E:\\Plex\\" + ratingKey + ".jpg"; } @@ -674,7 +675,8 @@ static void ArtWorker(std::string ratingKey, std::string url, void Plex_QueueArtDownload(const std::string& ratingKey, const std::string& url) { - if (ratingKey.empty() || url.empty()) return; + // The key comes off the server and ends up in a file path. + if (!Http_IsSafeFileId(ratingKey) || url.empty()) return; // Workers erase from the map on other threads, so it is only touched under the lock. std::atomic* flag; { diff --git a/theseus/desktop/xcloud_client.cpp b/theseus/desktop/xcloud_client.cpp index 6e510db..b7804b5 100644 --- a/theseus/desktop/xcloud_client.cpp +++ b/theseus/desktop/xcloud_client.cpp @@ -897,6 +897,7 @@ static std::string Xcloud_ArtCacheHostPath(const std::string& titleId) // Xbox-style path for the XAP; xboxfs maps E:\ onto Library/. std::string Xcloud_ArtCachePath(const std::string& titleId) { + if (!Http_IsSafeFileId(titleId)) return ""; return "E:\\Xcloud\\" + titleId + ".jpg"; } @@ -917,7 +918,8 @@ static void ArtWorker(std::string titleId, std::string url, std::atomic* f void Xcloud_QueueArtDownload(const std::string& titleId, const std::string& url) { - if (titleId.empty() || url.empty()) return; + // The id comes off the server and ends up in a file path. + if (!Http_IsSafeFileId(titleId) || url.empty()) return; // Workers erase from the map on other threads, so it is only touched under the lock. std::atomic* flag; { diff --git a/theseus/desktop/xcloud_vgames.cpp b/theseus/desktop/xcloud_vgames.cpp index d21d198..56fd1b1 100644 --- a/theseus/desktop/xcloud_vgames.cpp +++ b/theseus/desktop/xcloud_vgames.cpp @@ -9,6 +9,7 @@ // threaded. Re-runnable: prior streaming entries are cleared before a refresh. #include "xcloud_client.h" +#include "http_util.h" #include "virtual_games.h" #include "xcloud_vgames.h" @@ -38,7 +39,8 @@ static void Inject() int nGames = 0; for (size_t i = 0; i < games.size(); i++) { const XcloudGame& g = games[i]; - if (!g.playable || g.titleId.empty()) continue; + // The id doubles as the icon filename, so it has to be a safe one. + if (!g.playable || !Http_IsSafeFileId(g.titleId)) continue; std::string launch = "xcloud://" + g.titleId; if (VGames_Add(g.name.c_str(), g.titleId.c_str(), launch.c_str(), "E", "Cloud Gaming") < 0) break; // DB full @@ -50,7 +52,7 @@ static void Inject() int nConsoles = 0; for (size_t i = 0; i < consoles.size(); i++) { const XcloudConsole& c = consoles[i]; - if (c.serverId.empty()) continue; + if (!Http_IsSafeFileId(c.serverId)) continue; std::string launch = "xhome://" + c.serverId; if (VGames_Add(c.name.c_str(), c.serverId.c_str(), launch.c_str(), "E", "Your Xbox") < 0) break; From 413d1428ebd52c88a7a5e4ce516052e008a151c8 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:53:25 -0500 Subject: [PATCH 028/167] Stopped failed TMDB lookups from sticking as no match A lookup that got no real answer (network down, bad key) came back as not found, and the library saved that as a permanent miss. Now only real search responses get cached, and a failed one stays unlooked up so the next scan tries again. Old cached error pages are ignored too. --- theseus/desktop/media_db.cpp | 5 +++-- theseus/desktop/tmdb.cpp | 29 +++++++++++++++++++++-------- theseus/desktop/tmdb.h | 2 ++ 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/theseus/desktop/media_db.cpp b/theseus/desktop/media_db.cpp index e606309..c60607a 100644 --- a/theseus/desktop/media_db.cpp +++ b/theseus/desktop/media_db.cpp @@ -1000,7 +1000,8 @@ static void EnrichMoviesTmdb(std::vector& movies, std::vector m.tmdbId = tm.tmdbId; hits.fetch_add(1); } else { - m.tmdbId = -1; + // A failed lookup stays 0 so the next scan asks again. + if (!tm.failed) m.tmdbId = -1; misses.fetch_add(1); } } @@ -1067,7 +1068,7 @@ static void EnrichShowsTmdb(std::vector& shows, std::vector& s.tmdbId = ts.tmdbId; hits.fetch_add(1); } else { - s.tmdbId = -1; + if (!ts.failed) s.tmdbId = -1; misses.fetch_add(1); } } diff --git a/theseus/desktop/tmdb.cpp b/theseus/desktop/tmdb.cpp index 0dcf7e7..0b5cbce 100644 --- a/theseus/desktop/tmdb.cpp +++ b/theseus/desktop/tmdb.cpp @@ -151,6 +151,13 @@ static std::string GetFirstResult(const std::string& json) return items.empty() ? "" : items[0]; } +// A real search answer always carries "results", even when it's empty. +// Anything else is an error page and must not be cached. +static bool IsSearchResponse(const std::string& body) +{ + return !body.empty() && Json_FindArray(body, "results") != std::string::npos; +} + // First 4 chars after a YYYY-MM-DD field as int. static int YearFromDate(const std::string& dateStr) { @@ -199,7 +206,7 @@ static TmdbMovie DoMovieLookup(const std::string& title, int year, const std::st // Try cache first. std::string body = ReadFile(cachePath); - if (!body.empty()) + if (IsSearchResponse(body)) return ParseMovieJson(body); if (g_tmdbKey[0] == 0) { @@ -216,8 +223,11 @@ static TmdbMovie DoMovieLookup(const std::string& title, int year, const std::st } url += "&include_adult=false"; body = HttpGet(url); - if (!body.empty()) WriteFile(cachePath, body); (void)key; + if (!IsSearchResponse(body)) { + TmdbMovie m{}; m.ready = true; m.failed = true; return m; + } + WriteFile(cachePath, body); return ParseMovieJson(body); } @@ -226,7 +236,7 @@ static TmdbShow DoShowLookup(const std::string& title, const std::string& key) std::string cachePath = CachePath("show", ShowKey(title.c_str())); std::string body = ReadFile(cachePath); - if (!body.empty()) + if (IsSearchResponse(body)) return ParseShowJson(body); if (g_tmdbKey[0] == 0) { @@ -238,8 +248,11 @@ static TmdbShow DoShowLookup(const std::string& title, const std::string& key) url += UrlEncode(title.c_str()); url += "&include_adult=false"; body = HttpGet(url); - if (!body.empty()) WriteFile(cachePath, body); (void)key; + if (!IsSearchResponse(body)) { + TmdbShow s{}; s.ready = true; s.failed = true; return s; + } + WriteFile(cachePath, body); return ParseShowJson(body); } @@ -268,7 +281,7 @@ TmdbMovie TMDB_LookupMovie(const char* title, int year) if (it != s_movieResults.end()) return it->second; } TmdbMovie m = DoMovieLookup(title, year, key); - { + if (!m.failed) { std::lock_guard lock(g_resultMutex); s_movieResults[key] = m; } @@ -286,7 +299,7 @@ TmdbShow TMDB_LookupShow(const char* title) if (it != s_showResults.end()) return it->second; } TmdbShow s = DoShowLookup(title, key); - { + if (!s.failed) { std::lock_guard lock(g_resultMutex); s_showResults[key] = s; } @@ -326,7 +339,7 @@ void TMDB_QueueMovieLookup(const char* title, int year) fprintf(stderr, "[TMDB] worker done for %s: ready=%d found=%d overview=%zu\n", key.c_str(), (int)m.ready, (int)m.found, m.overview.size()); std::lock_guard lock(g_resultMutex); - s_movieResults[key] = m; + if (!m.failed) s_movieResults[key] = m; auto it = s_inFlight.find(key); if (it != s_inFlight.end()) { delete it->second; s_inFlight.erase(it); } }).detach(); @@ -363,7 +376,7 @@ void TMDB_QueueShowLookup(const char* title) fprintf(stderr, "[TMDB] worker done for %s: ready=%d found=%d overview=%zu\n", key.c_str(), (int)s.ready, (int)s.found, s.overview.size()); std::lock_guard lock(g_resultMutex); - s_showResults[key] = s; + if (!s.failed) s_showResults[key] = s; auto it = s_inFlight.find(key); if (it != s_inFlight.end()) { delete it->second; s_inFlight.erase(it); } }).detach(); diff --git a/theseus/desktop/tmdb.h b/theseus/desktop/tmdb.h index cd27e01..41fba93 100644 --- a/theseus/desktop/tmdb.h +++ b/theseus/desktop/tmdb.h @@ -17,6 +17,7 @@ struct TmdbMovie { bool ready; // true once fetch resolved (cache or net) bool found; // true if TMDB had a hit + bool failed; // no usable answer (network, bad key), so try again later std::string title; // canonical title from TMDB std::string overview; // plot synopsis int year; // release year (0 = unknown) @@ -28,6 +29,7 @@ struct TmdbShow { bool ready; bool found; + bool failed; std::string title; std::string overview; int year; // first air year From 8b0d285709c39c38af40d1b22b5bd755dcf2cb28 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:54:02 -0500 Subject: [PATCH 029/167] Rebuilt the radial alpha mask from the 5960 binary Wrote GenerateRadialAlphaMask fresh from the disassembly at 0x56110, with the per-pixel math, constants and the four callers noted above it. Output matches what we had pixel for pixel on all four masks. --- theseus/desktop/materials_desktop.cpp | 77 ++++++++++++++------------- theseus/render/materials.cpp | 77 ++++++++++++++------------- 2 files changed, 82 insertions(+), 72 deletions(-) diff --git a/theseus/desktop/materials_desktop.cpp b/theseus/desktop/materials_desktop.cpp index 9eeff10..9ba7d0a 100755 --- a/theseus/desktop/materials_desktop.cpp +++ b/theseus/desktop/materials_desktop.cpp @@ -111,50 +111,55 @@ class CMaxMaterial : public CMaterial +// GenerateRadialAlphaMask [no script binding; the four Get*AlphaMask build on it] +// Reconstructed from 5960 retail XBE @ 0x00056110 (pixels in EAX, scale in +// XMM0, pitch/width/height/factor/max on the stack, RET 0x14). Per pixel: +// d = sqrt(dx*dx + dy*dy) dx, dy run -1..1 across the drawn block +// a = clamp((1 - clamp(d, 0, 1)) * factor, 0, max) +// pixel = (BYTE)(int)(a * 255) << 24 alpha only, color stays 0 +// Only the top-left width*scale by height*scale block is drawn; the rest of +// each row and every row under it is cleared. 1.0 / 0.0 / 255.0 are the +// binary's constants at 0x2bed8 / 0x2bee4 / 0x2bf38. Callers pass (factor, +// max, scale): 0x562d0 (1, 1, 1), 0x563a0 (1, 1, 0.6875), 0x56470 (10, 0.8, 1), +// 0x56540 (3, 1, 1). A round falloff has one sensible shape; this follows the +// binary's clamps and integer halving rather than any other version of it. void GenerateRadialAlphaMask(BYTE *pbPels, int nPitch, int nWidth, int nHeight, float nFactor, float nMax, float nScale = 1.0f) { - LPDWORD pdwPels = (LPDWORD)pbPels; - int nRadialWidth = (int)((float)nWidth * nScale); - int nRadialHeight = (int)((float)nHeight * nScale); - int x, y; + const int cxDraw = (int)(nWidth * nScale); + const int cyDraw = (int)(nHeight * nScale); + const int halfX = cxDraw / 2; + const int halfY = cyDraw / 2; + const int rowBytes = nPitch & ~3; - for (y = 0; y < nRadialHeight; y += 1) + for (int y = 0; y < nHeight; y++) { - for (x = 0; x < nRadialWidth; x += 1) - { - float cx = (float)(x - nRadialWidth / 2) / (nRadialWidth / 2); - float cy = (float)(y - nRadialHeight / 2) / (nRadialHeight / 2); - float d = sqrtf(cx * cx + cy * cy); - - if (d < 0.0f) - d = 0.0f; - else if (d > 1.0f) - d = 1.0f; - - float a = 1.0f - d; - - a = a * nFactor; - - if (a < 0.0f) - a = 0.0f; - else if (a > nMax) - a = nMax; + DWORD *pRow = (DWORD *)(pbPels + y * rowBytes); + int x = 0; - pdwPels[x] = ((BYTE)(a * 255.0f)) << 24; - } - - for (; x < nWidth; x += 1) + if (y < cyDraw) { - pdwPels[x] = 0; + float dy = (float)(y - halfY) / (float)halfY; + for (; x < cxDraw; x++) + { + float dx = (float)(x - halfX) / (float)halfX; + float d = sqrtf(dx * dx + dy * dy); + if (d < 0.0f) + d = 0.0f; + else if (d > 1.0f) + d = 1.0f; + + float a = (1.0f - d) * nFactor; + if (a < 0.0f) + a = 0.0f; + else if (a > nMax) + a = nMax; + + pRow[x] = (DWORD)(BYTE)(int)(a * 255.0f) << 24; + } } - pdwPels += (nPitch >> 2); - } - - for (; y < nHeight; y += 1) - { - memset(pdwPels, 0, nWidth << 2); - pdwPels += (nPitch >> 2); + for (; x < nWidth; x++) + pRow[x] = 0; } } diff --git a/theseus/render/materials.cpp b/theseus/render/materials.cpp index 7974146..3ba2120 100755 --- a/theseus/render/materials.cpp +++ b/theseus/render/materials.cpp @@ -107,50 +107,55 @@ class CMaxMaterial : public CMaterial +// GenerateRadialAlphaMask [no script binding; the four Get*AlphaMask build on it] +// Reconstructed from 5960 retail XBE @ 0x00056110 (pixels in EAX, scale in +// XMM0, pitch/width/height/factor/max on the stack, RET 0x14). Per pixel: +// d = sqrt(dx*dx + dy*dy) dx, dy run -1..1 across the drawn block +// a = clamp((1 - clamp(d, 0, 1)) * factor, 0, max) +// pixel = (BYTE)(int)(a * 255) << 24 alpha only, color stays 0 +// Only the top-left width*scale by height*scale block is drawn; the rest of +// each row and every row under it is cleared. 1.0 / 0.0 / 255.0 are the +// binary's constants at 0x2bed8 / 0x2bee4 / 0x2bf38. Callers pass (factor, +// max, scale): 0x562d0 (1, 1, 1), 0x563a0 (1, 1, 0.6875), 0x56470 (10, 0.8, 1), +// 0x56540 (3, 1, 1). A round falloff has one sensible shape; this follows the +// binary's clamps and integer halving rather than any other version of it. void GenerateRadialAlphaMask(BYTE *pbPels, int nPitch, int nWidth, int nHeight, float nFactor, float nMax, float nScale = 1.0f) { - LPDWORD pdwPels = (LPDWORD)pbPels; - int nRadialWidth = (int)((float)nWidth * nScale); - int nRadialHeight = (int)((float)nHeight * nScale); - int x, y; + const int cxDraw = (int)(nWidth * nScale); + const int cyDraw = (int)(nHeight * nScale); + const int halfX = cxDraw / 2; + const int halfY = cyDraw / 2; + const int rowBytes = nPitch & ~3; - for (y = 0; y < nRadialHeight; y += 1) + for (int y = 0; y < nHeight; y++) { - for (x = 0; x < nRadialWidth; x += 1) - { - float cx = (float)(x - nRadialWidth / 2) / (nRadialWidth / 2); - float cy = (float)(y - nRadialHeight / 2) / (nRadialHeight / 2); - float d = sqrtf(cx * cx + cy * cy); - - if (d < 0.0f) - d = 0.0f; - else if (d > 1.0f) - d = 1.0f; - - float a = 1.0f - d; - - a = a * nFactor; - - if (a < 0.0f) - a = 0.0f; - else if (a > nMax) - a = nMax; + DWORD *pRow = (DWORD *)(pbPels + y * rowBytes); + int x = 0; - pdwPels[x] = ((BYTE)(a * 255.0f)) << 24; - } - - for (; x < nWidth; x += 1) + if (y < cyDraw) { - pdwPels[x] = 0; + float dy = (float)(y - halfY) / (float)halfY; + for (; x < cxDraw; x++) + { + float dx = (float)(x - halfX) / (float)halfX; + float d = sqrtf(dx * dx + dy * dy); + if (d < 0.0f) + d = 0.0f; + else if (d > 1.0f) + d = 1.0f; + + float a = (1.0f - d) * nFactor; + if (a < 0.0f) + a = 0.0f; + else if (a > nMax) + a = nMax; + + pRow[x] = (DWORD)(BYTE)(int)(a * 255.0f) << 24; + } } - pdwPels += (nPitch >> 2); - } - - for (; y < nHeight; y += 1) - { - memset(pdwPels, 0, nWidth << 2); - pdwPels += (nPitch >> 2); + for (; x < nWidth; x++) + pRow[x] = 0; } } From 9b429457fabc0b41a9277a2706a63f0beb06557d Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:54:23 -0500 Subject: [PATCH 030/167] Kept a stale Plex sync from clashing with a fresh sign in Signing out cleared the running flag while the sync worker kept going. Signing back in then froze the main thread joining it, and the old run could still mark the library ready with the old account's data. Sign out now bumps a generation, so the old run finishes quietly and publishes nothing, and the next frame starts a clean sync. --- theseus/desktop/plex_client.cpp | 55 +++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/theseus/desktop/plex_client.cpp b/theseus/desktop/plex_client.cpp index 6396ff0..8112016 100644 --- a/theseus/desktop/plex_client.cpp +++ b/theseus/desktop/plex_client.cpp @@ -80,6 +80,9 @@ static std::atomic s_syncRunning{false}; static std::atomic s_syncProgress{0}; // 0..1000 static std::string s_syncPhase; static std::thread s_syncThread; +// Bumped by sign out. A worker from an older generation finishes on its own +// but publishes nothing, and owns s_syncRunning until it does. +static std::atomic s_syncGen{0}; static std::vector s_cacheLibs; static std::map> s_cacheItems; // sectionKey -> items static std::map> s_cacheSeasons; // showRk -> seasons @@ -127,8 +130,8 @@ void Plex_SignOut() s_cacheItems.clear(); s_cacheSeasons.clear(); s_cacheEpisodes.clear(); + s_syncGen++; s_syncReady = false; - s_syncRunning = false; s_syncProgress = 0; s_syncPhase.clear(); } @@ -243,6 +246,7 @@ static std::string PickConnection(const std::string& serverJson) static void ServersWorker() { + const unsigned gen = s_syncGen; HttpResponse r = Http_Get( "https://plex.tv/api/v2/resources?includeHttps=1&includeRelay=1", PlexHeaders(true, true)); @@ -266,13 +270,15 @@ static void ServersWorker() fprintf(stderr, "[Plex] resources fetch failed (status %ld)\n", r.status); } + size_t n = servers.size(); { std::lock_guard lk(g_mtx); + if (gen != s_syncGen) return; // signed out meanwhile s_servers = std::move(servers); s_activeServerName = s_servers.empty() ? std::string() : s_servers[0].name; } s_serversReady = true; - fprintf(stderr, "[Plex] discovered %zu server(s)\n", s_servers.size()); + fprintf(stderr, "[Plex] discovered %zu server(s)\n", n); } std::string Plex_GetActiveServerName() @@ -440,20 +446,34 @@ std::string Plex_ResolveStreamUrl(const std::string& serverUri, // episodes stay lazy (the fanout is too wide to walk on every sign-in). // ============================================================================ -static void SetSyncPhase(const std::string& phase, int progressOf1000) +static void SetSyncPhase(unsigned gen, const std::string& phase, int progressOf1000) { std::lock_guard lk(g_mtx); + if (gen != s_syncGen) return; s_syncPhase = phase; s_syncProgress = progressOf1000; } +// Marks the sync done, unless a sign out made this run stale. +static void FinishSync(unsigned gen, const char* phase) +{ + SetSyncPhase(gen, phase, 1000); + { + std::lock_guard lk(g_mtx); + if (gen == s_syncGen) s_syncReady = true; + } + s_syncRunning = false; +} + static void SyncWorker() { + const unsigned gen = s_syncGen; s_syncReady = false; - SetSyncPhase("Finding Plex server", 0); + SetSyncPhase(gen, "Finding Plex server", 0); Plex_FetchServers(); while (!Plex_ServersReady()) { + if (gen != s_syncGen) { s_syncRunning = false; return; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } @@ -462,16 +482,14 @@ static void SyncWorker() // Signed in but nothing came back from plex.tv. Distinct from "signed // in and the server has no libraries", and the UI shows both as 0. fprintf(stderr, "[Plex] no servers returned for this account/token\n"); - SetSyncPhase("No Plex servers reachable", 1000); - s_syncReady = true; - s_syncRunning = false; + FinishSync(gen, "No Plex servers reachable"); return; } // Ask each server in turn rather than betting on the first. An account // can list a server that's simply unreachable from here (someone else's // share, a LAN address while you're remote, a machine that's off), and // taking servers[0] on faith renders as "0 libraries" with no complaint. - SetSyncPhase("Loading library list", 50); + SetSyncPhase(gen, "Loading library list", 50); std::string serverUri; HttpResponse r; std::vector libs; @@ -486,7 +504,7 @@ static void SyncWorker() serverUri = servers[si].uri; { std::lock_guard lk(g_mtx); - s_activeServerName = servers[si].name; + if (gen == s_syncGen) s_activeServerName = servers[si].name; } fprintf(stderr, "[Plex] using %s (%s)\n", servers[si].name.c_str(), serverUri.c_str()); @@ -495,9 +513,7 @@ static void SyncWorker() if (serverUri.empty()) { fprintf(stderr, "[Plex] none of the %zu server(s) answered\n", servers.size()); - SetSyncPhase("No Plex servers reachable", 1000); - s_syncReady = true; - s_syncRunning = false; + FinishSync(gen, "No Plex servers reachable"); return; } @@ -521,7 +537,7 @@ static void SyncWorker() char phaseBuf[128]; snprintf(phaseBuf, sizeof(phaseBuf), "Loading %s (%d/%d)", lib.title.c_str(), i + 1, libCount); - SetSyncPhase(phaseBuf, 100 + (i * 900) / (libCount > 0 ? libCount : 1)); + SetSyncPhase(gen, phaseBuf, 100 + (i * 900) / (libCount > 0 ? libCount : 1)); std::string url = serverUri + "/library/sections/" + lib.sectionKey + "/all"; HttpResponse ir = Http_Get(url, PlexHeaders(true, true)); @@ -550,16 +566,17 @@ static void SyncWorker() } // Publish atomically. Readers see old snapshot or whole new one. + size_t nLibs = libs.size(); { std::lock_guard lk(g_mtx); - s_cacheLibs = std::move(libs); - s_cacheItems = std::move(itemsByKey); + if (gen == s_syncGen) { + s_cacheLibs = std::move(libs); + s_cacheItems = std::move(itemsByKey); + } } - SetSyncPhase("Plex library ready", 1000); - s_syncReady = true; - s_syncRunning = false; - fprintf(stderr, "[Plex] sync complete (%zu libraries)\n", s_cacheLibs.size()); + FinishSync(gen, "Plex library ready"); + fprintf(stderr, "[Plex] sync complete (%zu libraries)\n", nLibs); } void Plex_StartSync() From 80101bd4bbefafb15b32b0b8ad8067ee3f3639cc Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:54:41 -0500 Subject: [PATCH 031/167] Same stale sync fix for Jellyfin Jellyfin sign out had the same hole as Plex. A running sync could freeze the next sign in and mark the library ready with stale data. It uses the same generation check now. --- theseus/desktop/jellyfin_client.cpp | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/theseus/desktop/jellyfin_client.cpp b/theseus/desktop/jellyfin_client.cpp index d3ac9f9..b1b1503 100644 --- a/theseus/desktop/jellyfin_client.cpp +++ b/theseus/desktop/jellyfin_client.cpp @@ -85,6 +85,9 @@ static std::atomic s_syncRunning{false}; static std::atomic s_syncProgress{0}; static std::string s_syncPhase; static std::thread s_syncThread; +// Bumped by sign out. A worker from an older generation finishes on its own +// but publishes nothing, and owns s_syncRunning until it does. +static std::atomic s_syncGen{0}; static std::vector s_cacheLibs; static std::map> s_cacheItems; static std::map> s_cacheSeasons; @@ -136,8 +139,8 @@ void Jellyfin_SignOut() s_cacheItems.clear(); s_cacheSeasons.clear(); s_cacheEpisodes.clear(); + s_syncGen++; s_syncReady = false; - s_syncRunning = false; s_syncProgress = 0; s_syncPhase.clear(); } @@ -348,18 +351,20 @@ static std::vector FetchItemsForLibrary(const std::string& library // Sync worker (mirrors Plex_StartSync) // ============================================================================ -static void SetSyncPhase(const std::string& phase, int progressOf1000) +static void SetSyncPhase(unsigned gen, const std::string& phase, int progressOf1000) { std::lock_guard lk(g_mtx); + if (gen != s_syncGen) return; s_syncPhase = phase; s_syncProgress = progressOf1000; } static void SyncWorker() { + const unsigned gen = s_syncGen; s_syncReady = false; - SetSyncPhase("Loading library list", 0); + SetSyncPhase(gen, "Loading library list", 0); char url[1024]; snprintf(url, sizeof(url), "%s/Users/%s/Views", g_jellyfinUrl, g_jellyfinUserId); HttpResponse r = Http_Get(url, JellyfinHeaders(g_jellyfinToken)); @@ -384,20 +389,22 @@ static void SyncWorker() char phaseBuf[128]; snprintf(phaseBuf, sizeof(phaseBuf), "Loading %s (%d/%d)", lib.name.c_str(), i + 1, libCount); - SetSyncPhase(phaseBuf, 100 + (i * 900) / (libCount > 0 ? libCount : 1)); + SetSyncPhase(gen, phaseBuf, 100 + (i * 900) / (libCount > 0 ? libCount : 1)); itemsById[lib.id] = FetchItemsForLibrary(lib.id); } + size_t nLibs = libs.size(); + SetSyncPhase(gen, "Jellyfin library ready", 1000); { std::lock_guard lk(g_mtx); - s_cacheLibs = std::move(libs); - s_cacheItems = std::move(itemsById); + if (gen == s_syncGen) { + s_cacheLibs = std::move(libs); + s_cacheItems = std::move(itemsById); + s_syncReady = true; + } } - - SetSyncPhase("Jellyfin library ready", 1000); - s_syncReady = true; s_syncRunning = false; - fprintf(stderr, "[Jellyfin] sync complete (%zu libraries)\n", s_cacheLibs.size()); + fprintf(stderr, "[Jellyfin] sync complete (%zu libraries)\n", nLibs); } void Jellyfin_StartSync() From 18db23d0f27d479bff324bd88aeda493d1457506 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:55:21 -0500 Subject: [PATCH 032/167] Plex browsing and playback use the server the sync reached The sync tries each server until one answers, but seasons, episodes and playback still went to the first one listed. With an unreachable server ahead of yours, shows opened empty and nothing played. They all follow the server the sync settled on now. --- theseus/desktop/desktop_nodes.cpp | 7 +++---- theseus/desktop/plex_client.cpp | 26 ++++++++++++++++---------- theseus/desktop/plex_client.h | 2 ++ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/theseus/desktop/desktop_nodes.cpp b/theseus/desktop/desktop_nodes.cpp index f16d8a6..acbfbb9 100644 --- a/theseus/desktop/desktop_nodes.cpp +++ b/theseus/desktop/desktop_nodes.cpp @@ -3337,10 +3337,8 @@ class CPlexLibrary : public CNode { if (!Plex_HasToken()) return; Plex_StartSync(); - if (m_serverUri.empty() && Plex_ServersReady()) { - std::vector srv = Plex_GetServers(); - if (!srv.empty()) m_serverUri = srv[0].uri; - } + // Playback goes to the server the sync settled on, which isn't always the first listed. + if (Plex_SyncReady()) m_serverUri = Plex_GetActiveServerUri(); } int GetCount() @@ -3575,6 +3573,7 @@ class CPlexLibrary : public CNode const std::string& title, const std::string& subtitle) { + if (m_serverUri.empty()) m_serverUri = Plex_GetActiveServerUri(); if (m_serverUri.empty() || ratingKey.empty()) return; std::string url = Plex_ResolveStreamUrl(m_serverUri, ratingKey); if (url.empty()) { diff --git a/theseus/desktop/plex_client.cpp b/theseus/desktop/plex_client.cpp index 8112016..627e46b 100644 --- a/theseus/desktop/plex_client.cpp +++ b/theseus/desktop/plex_client.cpp @@ -64,6 +64,7 @@ static std::atomic s_serversReady{false}; static std::vector s_servers; static std::thread s_serversThread; static std::string s_activeServerName; +static std::string s_activeServerUri; // TV drill. One worker thread per season/episode fetch; results land in the // pre-stage cache below. @@ -126,6 +127,7 @@ void Plex_SignOut() std::lock_guard lk(g_mtx); s_servers.clear(); s_serversReady = false; s_activeServerName.clear(); + s_activeServerUri.clear(); s_cacheLibs.clear(); s_cacheItems.clear(); s_cacheSeasons.clear(); @@ -276,6 +278,7 @@ static void ServersWorker() if (gen != s_syncGen) return; // signed out meanwhile s_servers = std::move(servers); s_activeServerName = s_servers.empty() ? std::string() : s_servers[0].name; + s_activeServerUri = s_servers.empty() ? std::string() : s_servers[0].uri; } s_serversReady = true; fprintf(stderr, "[Plex] discovered %zu server(s)\n", n); @@ -295,6 +298,12 @@ void Plex_FetchServers() s_serversThread = std::thread(ServersWorker); } +std::string Plex_GetActiveServerUri() +{ + std::lock_guard lk(g_mtx); + return s_activeServerUri; +} + std::vector Plex_GetServers() { std::lock_guard lk(g_mtx); @@ -504,7 +513,10 @@ static void SyncWorker() serverUri = servers[si].uri; { std::lock_guard lk(g_mtx); - if (gen == s_syncGen) s_activeServerName = servers[si].name; + if (gen == s_syncGen) { + s_activeServerName = servers[si].name; + s_activeServerUri = servers[si].uri; + } } fprintf(stderr, "[Plex] using %s (%s)\n", servers[si].name.c_str(), serverUri.c_str()); @@ -612,13 +624,6 @@ std::vector Plex_Cache_GetItems(const std::string& sectionKey) return it->second; } -// A server refresh swaps s_servers on a worker, so copy under the lock. -static std::string FirstServerUri() -{ - std::lock_guard lk(g_mtx); - return s_servers.empty() ? std::string() : s_servers.front().uri; -} - bool Plex_Cache_GetSeasons(const std::string& showRatingKey, std::vector& out) { @@ -631,7 +636,8 @@ bool Plex_Cache_GetSeasons(const std::string& showRatingKey, } } // Not cached, fire a fetch; SeasonsWorker promotes the result. - std::string uri = FirstServerUri(); + // Same server the library came from, not just the first one listed. + std::string uri = Plex_GetActiveServerUri(); if (!uri.empty()) Plex_FetchSeasons(uri, showRatingKey); return false; } @@ -647,7 +653,7 @@ bool Plex_Cache_GetEpisodes(const std::string& seasonRatingKey, return true; } } - std::string uri = FirstServerUri(); + std::string uri = Plex_GetActiveServerUri(); if (!uri.empty()) Plex_FetchEpisodes(uri, seasonRatingKey); return false; } diff --git a/theseus/desktop/plex_client.h b/theseus/desktop/plex_client.h index ece505c..e3be6e0 100644 --- a/theseus/desktop/plex_client.h +++ b/theseus/desktop/plex_client.h @@ -60,6 +60,8 @@ void Plex_FetchServers(); std::vector Plex_GetServers(); bool Plex_ServersReady(); std::string Plex_GetActiveServerName(); +// Address of the server the sync actually reached. Empty until one answers. +std::string Plex_GetActiveServerUri(); // TV drill. Results land in the pre-stage cache (Plex_Cache_GetSeasons/Episodes). void Plex_FetchSeasons(const std::string& serverUri, From 065a421a3897dc4a00f02c419f4a67b1a370e206 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:55:45 -0500 Subject: [PATCH 033/167] Stopped Plex season and episode fetches from stalling frames The Plex scene asks for seasons and episodes every frame. Each miss joined the fetch already running, so the main thread sat on a network request and then fired the same fetch again. A miss while one is running now just returns, and the next frame picks up the result. --- theseus/desktop/plex_client.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/theseus/desktop/plex_client.cpp b/theseus/desktop/plex_client.cpp index 627e46b..8d0384a 100644 --- a/theseus/desktop/plex_client.cpp +++ b/theseus/desktop/plex_client.cpp @@ -70,6 +70,10 @@ static std::string s_activeServerUri; // pre-stage cache below. static std::thread s_seasonsThread; static std::thread s_episodesThread; +// The scene polls the cache every frame, so a fetch already running means +// "ask again next frame", never "wait for it here". +static std::atomic s_seasonsBusy{false}; +static std::atomic s_episodesBusy{false}; // Art download queue static std::mutex s_artMtx; @@ -360,12 +364,14 @@ static void SeasonsWorker(std::string serverUri, std::string showRk) s_cacheSeasons[showRk] = std::move(seasons); } fprintf(stderr, "[Plex] %zu season(s) under show %s\n", n, showRk.c_str()); + s_seasonsBusy = false; } void Plex_FetchSeasons(const std::string& serverUri, const std::string& showRatingKey) { Plex_Init(); - JoinIf(s_seasonsThread); + if (s_seasonsBusy.exchange(true)) return; + JoinIf(s_seasonsThread); // already finished, so this is instant s_seasonsThread = std::thread(SeasonsWorker, serverUri, showRatingKey); } @@ -401,11 +407,13 @@ static void EpisodesWorker(std::string serverUri, std::string seasonRk) s_cacheEpisodes[seasonRk] = std::move(episodes); } fprintf(stderr, "[Plex] %zu episode(s) under season %s\n", n, seasonRk.c_str()); + s_episodesBusy = false; } void Plex_FetchEpisodes(const std::string& serverUri, const std::string& seasonRatingKey) { Plex_Init(); + if (s_episodesBusy.exchange(true)) return; JoinIf(s_episodesThread); s_episodesThread = std::thread(EpisodesWorker, serverUri, seasonRatingKey); } From b2dd8732370d037096be279026ad8e6cdfcfe5ec Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:01:00 -0500 Subject: [PATCH 034/167] Stopped the Windows build reaching kernel32 from dashboard code platform_shim.h pulled in windows.h on Windows, so the dashboard called the real ReadFile, GetTickCount and Sleep there instead of the shim. The shim is now the same on every host, file handles are FILE*s everywhere, and the real Win32 bits from sdl_main, launch and skin_editor moved into host_win_app.cpp. Needs a Windows test. --- build/Makefile | 9 +- theseus/desktop/d3d8_sdl.h | 27 +-- theseus/desktop/desktop_nodes.cpp | 142 +++--------- theseus/desktop/host.h | 30 +++ theseus/desktop/host_posix.cpp | 58 +++++ theseus/desktop/host_posix_app.cpp | 137 ++++++++++++ theseus/desktop/host_win.cpp | 37 ++++ theseus/desktop/host_win_app.cpp | 195 +++++++++++++++++ theseus/desktop/launch.cpp | 133 +----------- theseus/desktop/media_db.cpp | 103 +++++---- theseus/desktop/platform_shim.h | 338 +++++++++++------------------ theseus/desktop/sdl_main.cpp | 276 ++--------------------- theseus/desktop/skin_editor.cpp | 42 +--- theseus/desktop/std.h | 19 +- theseus/desktop/xbox_file.cpp | 35 +-- theseus/shared/platform.h | 2 + 16 files changed, 713 insertions(+), 870 deletions(-) create mode 100644 theseus/desktop/host_posix_app.cpp create mode 100644 theseus/desktop/host_win_app.cpp diff --git a/build/Makefile b/build/Makefile index 5774312..1fb1ef5 100644 --- a/build/Makefile +++ b/build/Makefile @@ -793,10 +793,12 @@ DESKTOP_SRCS_ENGINE = \ # The host layer: one per OS, picked by host. host_win.cpp is the only # desktop file that sees . +HOST_WIN_SRCS = $(THESEUS_SRC)/desktop/host_win.cpp $(THESEUS_SRC)/desktop/host_win_app.cpp +HOST_POSIX_SRCS = $(THESEUS_SRC)/desktop/host_posix.cpp $(THESEUS_SRC)/desktop/host_posix_app.cpp ifeq ($(filter Darwin Linux,$(UNAME_S)),) - DESKTOP_SRCS_HOST = $(THESEUS_SRC)/desktop/host_win.cpp + DESKTOP_SRCS_HOST = $(HOST_WIN_SRCS) else - DESKTOP_SRCS_HOST = $(THESEUS_SRC)/desktop/host_posix.cpp + DESKTOP_SRCS_HOST = $(HOST_POSIX_SRCS) endif DESKTOP_SRCS_FILE = \ @@ -1313,8 +1315,7 @@ WIN64_SRCS = $(filter-out \ $(THESEUS_SRC)/desktop/imgui/imgui_impl_opengl3.cpp \ $(THESEUS_SRC)/desktop/imgui_impl_bgfx.cpp \ $(DESKTOP_SRCS_HOST), $(DESKTOP_ALL_SRCS)) -WIN64_SRCS += $(THESEUS_SRC)/desktop/imgui_impl_bgfx.cpp \ - $(THESEUS_SRC)/desktop/host_win.cpp +WIN64_SRCS += $(THESEUS_SRC)/desktop/imgui_impl_bgfx.cpp $(HOST_WIN_SRCS) # Same source list as desktop. the compiler reads the same files. WIN64_OBJS = $(patsubst $(THESEUS_SRC)/%.cpp,$(WIN64_DIR)/%.o,$(WIN64_SRCS)) diff --git a/theseus/desktop/d3d8_sdl.h b/theseus/desktop/d3d8_sdl.h index 7862e2b..1f41b7a 100644 --- a/theseus/desktop/d3d8_sdl.h +++ b/theseus/desktop/d3d8_sdl.h @@ -10,20 +10,12 @@ extern "C" unsigned char* stbi_load_from_memory(const unsigned char*, int, int*, int*, int*, int); extern "C" void stbi_image_free(void*); -// GL headers, pulled only for base typedefs (GLuint / GLint) that a few -// inert leftover state structs below still reference. No GL is called under -// bgfx; on Windows supplies the typedefs (GLEW is not needed). -#ifdef __APPLE__ - #define GL_SILENCE_DEPRECATION - #include -#elif defined(_WIN32) - #include - #include -#else - #define GL_GLEXT_PROTOTYPES - #include - #include -#endif +// A few inert leftover state structs below still hold GL-typed fields. +// No GL is called under bgfx, so the base typedefs are all they need. +typedef unsigned int GLuint; +typedef int GLint; +typedef unsigned int GLenum; +typedef unsigned short GLushort; // Ensure D3D vertex constructors are available (vertex8.h, hud.cpp) #ifndef DIRECT3D_VERSION @@ -1653,13 +1645,6 @@ class IDirect3DDevice8 { if (m_tss0MagFilter == D3DTEXF_POINT) f |= BGFX_SAMPLER_MAG_POINT; return f; } - static GLenum D3DFilterToGL(DWORD d3dFilter, bool isMag) { - switch (d3dFilter) { - case 1: return GL_NEAREST; // D3DTEXF_POINT - case 2: return GL_LINEAR; // D3DTEXF_LINEAR - default: return isMag ? GL_LINEAR : GL_LINEAR; // ANISOTROPIC/QUINCUNX/GAUSSIAN → linear - } - } HRESULT SetTextureStageState(DWORD stage, D3DTEXTURESTAGESTATETYPE type, DWORD value) { // Sweep 2 trace counter. if (stage < 8 && (DWORD)type < 32) { diff --git a/theseus/desktop/desktop_nodes.cpp b/theseus/desktop/desktop_nodes.cpp index f16d8a6..53ef8f3 100644 --- a/theseus/desktop/desktop_nodes.cpp +++ b/theseus/desktop/desktop_nodes.cpp @@ -6,14 +6,7 @@ #include "app_paths.h" #include "std.h" -#ifdef _WIN32 -#include -#ifndef F_OK -#define F_OK 0 -#endif -// Windows wingdi.h defines GetFreeSpace as a macro; conflicts with our method -#undef GetFreeSpace -#endif +#include "host.h" #include "dashapp.h" #include "node.h" #include "runner.h" @@ -1346,13 +1339,6 @@ END_NODE_FUN() #include "media_player.h" #include "panel_shared.h" // for DashAudio_MuteAll/UnmuteAll #include -#ifdef _WIN32 -#include -#include -#else -#include -#include -#endif #define XBOX_BLOCK_SIZE 16384 #define SAVED_GAME_MAX_TITLES 256 @@ -1756,32 +1742,16 @@ static bool ParseXboxMetaValue(const char* xboxPath, const char* key, char* outB static uint64_t GetDirectorySize(const char* dirPath) { uint64_t total = 0; -#ifdef _WIN32 - char searchBuf[1024]; - snprintf(searchBuf, sizeof(searchBuf), "%s\\*", dirPath); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind == -1) return 0; - do { - if (fd.name[0] == '.') continue; - if (!(fd.attrib & _A_SUBDIR)) - total += fd.size; - } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); -#else - DIR* dir = opendir(dirPath); + HostDir* dir = Host_OpenDir(dirPath); if (!dir) return 0; - struct dirent* ent; - while ((ent = readdir(dir)) != NULL) { - if (ent->d_name[0] == '.') continue; - char fullPath[1024]; - snprintf(fullPath, sizeof(fullPath), "%s/%s", dirPath, ent->d_name); - struct stat st; - if (stat(fullPath, &st) == 0 && S_ISREG(st.st_mode)) - total += st.st_size; - } - closedir(dir); -#endif + char name[260]; + HostFileInfo info; + while (Host_ReadDir(dir, name, sizeof(name), &info)) { + if (name[0] == '.') continue; + if (!(info.attrs & HOST_ATTR_DIR)) + total += info.size; + } + Host_CloseDir(dir); return total; } @@ -1844,100 +1814,50 @@ static void EnumerateTitles() title.saveCount = 0; title.totalSizeBytes = 0; // Enumerate saves within this title -#ifdef _WIN32 - char savSearchBuf[1024]; - snprintf(savSearchBuf, sizeof(savSearchBuf), "%s\\*", titlePath); - struct _finddata_t savFd; - intptr_t hSavFind = _findfirst(savSearchBuf, &savFd); - if (hSavFind != -1) { - do { - if (savFd.name[0] == '.') continue; - if (!(savFd.attrib & _A_SUBDIR)) continue; - if (title.saveCount >= SAVED_GAME_MAX_SAVES) break; - char savePath[1024]; - snprintf(savePath, sizeof(savePath), "%s/%s", titlePath, savFd.name); - struct stat saveSt; - if (stat(savePath, &saveSt) != 0) continue; - DesktopSavedGame& save = title.saves[title.saveCount]; - memset(&save, 0, sizeof(save)); - strncpy(save.folderName, savFd.name, sizeof(save.folderName) - 1); - char xboxSaveMetaPath[256]; - snprintf(xboxSaveMetaPath, sizeof(xboxSaveMetaPath), "E:\\UDATA\\%s\\%s\\SaveMeta.xbx", entName, savFd.name); - if (!ParseXboxMetaValue(xboxSaveMetaPath, "Name", save.saveName, sizeof(save.saveName))) - strncpy(save.saveName, savFd.name, sizeof(save.saveName) - 1); - save.sizeBytes = GetDirectorySize(savePath); - save.modTime = saveSt.st_mtime; - title.totalSizeBytes += save.sizeBytes; - title.saveCount++; - } while (_findnext(hSavFind, &savFd) == 0); - _findclose(hSavFind); - } -#else - DIR* titleDir = opendir(titlePath); + HostDir* titleDir = Host_OpenDir(titlePath); if (titleDir) { - struct dirent* saveEnt; - while ((saveEnt = readdir(titleDir)) != NULL && title.saveCount < SAVED_GAME_MAX_SAVES) { - if (saveEnt->d_name[0] == '.') continue; + char saveName[260]; + HostFileInfo saveInfo; + while (title.saveCount < SAVED_GAME_MAX_SAVES && + Host_ReadDir(titleDir, saveName, sizeof(saveName), &saveInfo)) { + if (saveName[0] == '.') continue; + if (!(saveInfo.attrs & HOST_ATTR_DIR)) continue; char savePath[1024]; - snprintf(savePath, sizeof(savePath), "%s/%s", titlePath, saveEnt->d_name); - struct stat saveSt; - if (stat(savePath, &saveSt) != 0 || !S_ISDIR(saveSt.st_mode)) continue; + snprintf(savePath, sizeof(savePath), "%s/%s", titlePath, saveName); DesktopSavedGame& save = title.saves[title.saveCount]; memset(&save, 0, sizeof(save)); - strncpy(save.folderName, saveEnt->d_name, sizeof(save.folderName) - 1); + strncpy(save.folderName, saveName, sizeof(save.folderName) - 1); char xboxSaveMetaPath[256]; - snprintf(xboxSaveMetaPath, sizeof(xboxSaveMetaPath), "E:\\UDATA\\%s\\%s\\SaveMeta.xbx", entName, saveEnt->d_name); + snprintf(xboxSaveMetaPath, sizeof(xboxSaveMetaPath), "E:\\UDATA\\%s\\%s\\SaveMeta.xbx", entName, saveName); if (!ParseXboxMetaValue(xboxSaveMetaPath, "Name", save.saveName, sizeof(save.saveName))) - strncpy(save.saveName, saveEnt->d_name, sizeof(save.saveName) - 1); + strncpy(save.saveName, saveName, sizeof(save.saveName) - 1); save.sizeBytes = GetDirectorySize(savePath); - save.modTime = saveSt.st_mtime; + save.modTime = (time_t)saveInfo.mtime; title.totalSizeBytes += save.sizeBytes; title.saveCount++; } - closedir(titleDir); + Host_CloseDir(titleDir); } -#endif title.totalSizeBytes += GetDirectorySize(titlePath); s_titleCount++; }; -#ifdef _WIN32 - char searchBuf[1024]; - snprintf(searchBuf, sizeof(searchBuf), "%s\\*", udataPath); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind == -1) { fprintf(stderr, "[SavedGameGrid] Cannot open %s\n", udataPath); return; } - do { ProcessTitleEntry(fd.name); } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); -#else - DIR* dir = opendir(udataPath); + HostDir* dir = Host_OpenDir(udataPath); if (!dir) { fprintf(stderr, "[SavedGameGrid] Cannot open %s\n", udataPath); return; } - struct dirent* ent; - while ((ent = readdir(dir)) != NULL) ProcessTitleEntry(ent->d_name); - closedir(dir); -#endif + char entry[260]; + while (Host_ReadDir(dir, entry, sizeof(entry), NULL)) ProcessTitleEntry(entry); + Host_CloseDir(dir); // Sort alphabetically for (int i = 0; i < s_titleCount - 1; i++) for (int j = i + 1; j < s_titleCount; j++) if (strcasecmp(s_titles[i].titleName, s_titles[j].titleName) > 0) { DesktopTitle tmp = s_titles[i]; s_titles[i] = s_titles[j]; s_titles[j] = tmp; } -#ifdef _WIN32 - { - ULARGE_INTEGER freeBytesAvail, totalBytes; - if (GetDiskFreeSpaceExA(udataPath, &freeBytesAvail, &totalBytes, NULL)) { - s_totalBlocks = (int)(totalBytes.QuadPart / XBOX_BLOCK_SIZE); - s_freeBlocks = (int)(freeBytesAvail.QuadPart / XBOX_BLOCK_SIZE); - } else - { s_totalBlocks = 50000; s_freeBlocks = 50000; } - } -#else - struct statvfs vfs; - if (statvfs(udataPath, &vfs) == 0) { - s_totalBlocks = (int)((uint64_t)vfs.f_blocks * vfs.f_frsize / XBOX_BLOCK_SIZE); - s_freeBlocks = (int)((uint64_t)vfs.f_bavail * vfs.f_frsize / XBOX_BLOCK_SIZE); + uint64_t freeBytes = 0, totalBytes = 0; + if (Host_DiskSpace(udataPath, &freeBytes, &totalBytes)) { + s_totalBlocks = (int)(totalBytes / XBOX_BLOCK_SIZE); + s_freeBlocks = (int)(freeBytes / XBOX_BLOCK_SIZE); } else { s_totalBlocks = 50000; s_freeBlocks = 50000; } -#endif fprintf(stderr, "[SavedGameGrid] Enumerated %d titles from %s\n", s_titleCount, udataPath); } diff --git a/theseus/desktop/host.h b/theseus/desktop/host.h index 1c7f8b2..d7d36c1 100644 --- a/theseus/desktop/host.h +++ b/theseus/desktop/host.h @@ -35,8 +35,38 @@ bool Host_RenameReplace(const char* from, const char* to); bool Host_CopyFile(const char* from, const char* to, bool failIfExists); bool Host_DiskSpace(const char* path, uint64_t* freeBytes, uint64_t* totalBytes); +// An empty read/write file that's deleted when closed. +FILE* Host_TempFile(); + +// 64-bit seek on an open file. Returns the new position, or -1. +int64_t Host_Seek(FILE* f, int64_t offset, int whence); + // Folder listing. Hands back every entry, "." and ".." included. struct HostDir; HostDir* Host_OpenDir(const char* path); bool Host_ReadDir(HostDir* dir, char* name, size_t nameSize, HostFileInfo* info); void Host_CloseDir(HostDir* dir); + +// The machine itself. +bool Host_MemoryStatus(uint64_t* totalBytes, uint64_t* availBytes); +uint32_t Host_LocalIPv4(uint32_t* netmask); // network order, 0 when there's none + +// Process plumbing for main() and the launcher. host_win_app.cpp and +// host_posix_app.cpp. +struct SDL_Window; + +// Windows only: attach to the parent shell's console, or with none (an +// Explorer launch) send stdout and stderr to logPath. No-op elsewhere. +void Host_ConsoleOrLog(const char* logPath); +// Ask the OS for game-like scheduling. No-op where there's nothing to ask. +void Host_BoostProcess(); +void Host_InstallCrashHandler(); +// What bgfx needs to draw into the SDL window. ndt is the display +// connection (X11/Wayland), NULL elsewhere. +bool Host_NativeWindow(SDL_Window* window, void** nwh, void** ndt, bool* wayland); +// Fire and forget: a URL goes to the OS handler, anything else runs as a +// shell command from workdir (may be NULL). err gets a line on failure. +bool Host_Spawn(const char* spec, bool isUrl, bool isSteamUrl, const char* workdir, + char* err, size_t errSize); +// Native "open file" dialog. False on cancel or when there isn't one. +bool Host_PickFile(const char* title, char* out, size_t outSize); diff --git a/theseus/desktop/host_posix.cpp b/theseus/desktop/host_posix.cpp index 084ab03..9a174c1 100644 --- a/theseus/desktop/host_posix.cpp +++ b/theseus/desktop/host_posix.cpp @@ -2,13 +2,23 @@ #include "host.h" +#include #include #include +#include +#include #include #include +#include #include #include #include +#ifdef __APPLE__ +#include +#include +#else +#include +#endif static void FillInfo(const struct stat& st, HostFileInfo* out) { out->attrs = S_ISDIR(st.st_mode) ? HOST_ATTR_DIR : 0; @@ -69,6 +79,13 @@ bool Host_DiskSpace(const char* path, uint64_t* freeBytes, uint64_t* totalBytes) return true; } +FILE* Host_TempFile() { return tmpfile(); } + +int64_t Host_Seek(FILE* f, int64_t offset, int whence) { + if (!f || fseeko(f, (off_t)offset, whence) != 0) return -1; + return (int64_t)ftello(f); +} + struct HostDir { DIR* d; char path[512]; @@ -103,3 +120,44 @@ void Host_CloseDir(HostDir* dir) { closedir(dir->d); free(dir); } + +bool Host_MemoryStatus(uint64_t* totalBytes, uint64_t* availBytes) { + uint64_t total = 0, avail = 0; +#ifdef __APPLE__ + int mib[2] = { CTL_HW, HW_MEMSIZE }; + size_t len = sizeof(total); + sysctl(mib, 2, &total, &len, NULL, 0); + mach_msg_type_number_t count = HOST_VM_INFO_COUNT; + vm_statistics_data_t vmstat; + if (host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmstat, &count) == KERN_SUCCESS) + avail = (uint64_t)vmstat.free_count * vm_page_size; +#else + struct sysinfo si; + if (sysinfo(&si) == 0) { + total = (uint64_t)si.totalram * si.mem_unit; + avail = (uint64_t)si.freeram * si.mem_unit; + } +#endif + if (totalBytes) *totalBytes = total; + if (availBytes) *availBytes = avail; + return total != 0; +} + +// First non-loopback IPv4 interface. +uint32_t Host_LocalIPv4(uint32_t* netmask) { + uint32_t addr = 0, mask = 0; + struct ifaddrs* ifa = NULL; + if (getifaddrs(&ifa) == 0) { + for (struct ifaddrs* cur = ifa; cur; cur = cur->ifa_next) { + if (!cur->ifa_addr || cur->ifa_addr->sa_family != AF_INET) continue; + uint32_t a = ((struct sockaddr_in*)cur->ifa_addr)->sin_addr.s_addr; + if ((a & 0xFF) == 127) continue; + addr = a; + mask = cur->ifa_netmask ? ((struct sockaddr_in*)cur->ifa_netmask)->sin_addr.s_addr : 0; + break; + } + freeifaddrs(ifa); + } + if (netmask) *netmask = mask; + return addr; +} diff --git a/theseus/desktop/host_posix_app.cpp b/theseus/desktop/host_posix_app.cpp new file mode 100644 index 0000000..ce9a506 --- /dev/null +++ b/theseus/desktop/host_posix_app.cpp @@ -0,0 +1,137 @@ +// host_posix_app.cpp: the process half of host.h for macOS and Linux. + +#include "host.h" + +#include +#include + +#include +#include +#include +#include +#include +#if defined(__APPLE__) || defined(__GLIBC__) +#include +#define HAS_BACKTRACE 1 +#endif + +void Host_ConsoleOrLog(const char*) {} +void Host_BoostProcess() {} + +static void CrashHandler(int sig) { + fprintf(stderr, "\n[CRASH] Signal %d (%s)\n", sig, sig == SIGSEGV ? "SIGSEGV" : sig == SIGABRT ? "SIGABRT" : "OTHER"); + fflush(stderr); +#ifdef HAS_BACKTRACE + void* bt[64]; + int n = backtrace(bt, 64); + backtrace_symbols_fd(bt, n, 2); +#endif + _exit(128 + sig); +} + +void Host_InstallCrashHandler() { + signal(SIGSEGV, CrashHandler); + signal(SIGBUS, CrashHandler); + signal(SIGABRT, CrashHandler); +} + +bool Host_NativeWindow(SDL_Window* window, void** nwh, void** ndt, bool* wayland) { + *nwh = NULL; + *ndt = NULL; + *wayland = false; + SDL_SysWMinfo wmi; + SDL_VERSION(&wmi.version); + if (!SDL_GetWindowWMInfo(window, &wmi)) { + fprintf(stderr, "SDL_GetWindowWMInfo failed: %s\n", SDL_GetError()); + return false; + } +#ifdef __APPLE__ + *nwh = wmi.info.cocoa.window; +#else + // Branch on SDL's actual subsystem. Feeding x11 lanes to bgfx on a + // Wayland session hands RADV a NULL Display and it crashes in + // vkGetPhysicalDeviceSurfaceSupportKHR. + if (wmi.subsystem == SDL_SYSWM_WAYLAND) { + *ndt = wmi.info.wl.display; + *nwh = wmi.info.wl.surface; + *wayland = true; + } else { + *ndt = wmi.info.x11.display; + *nwh = (void*)(uintptr_t)wmi.info.x11.window; + } +#endif + return true; +} + +// Put back the LD_LIBRARY_PATH AppRun stashed, so a spawned /bin/sh doesn't +// load the AppImage's bundled readline. No-op outside an AppImage. +static void RestoreHostLibraryPath() { + const char* host = getenv("THESEUS_HOST_LD_LIBRARY_PATH"); + if (!host) return; + if (*host) setenv("LD_LIBRARY_PATH", host, 1); + else unsetenv("LD_LIBRARY_PATH"); +} + +// Runs in the forked child and never returns on success. +static void ExecLaunch(const char* spec, bool isUrl, bool isSteamUrl) { + if (isSteamUrl) { +#ifdef __APPLE__ + // macOS: system handler works fine for steam:// + execlp("open", "open", spec, (char*)NULL); +#else + // Linux: bypass xdg-open/KIO; steam binary takes the URL directly. + execlp("steam", "steam", spec, (char*)NULL); +#endif + } else if (isUrl) { +#ifdef __APPLE__ + execlp("open", "open", spec, (char*)NULL); +#else + execlp("xdg-open", "xdg-open", spec, (char*)NULL); +#endif + } else { + // Raw command line: shell parses paths, args, scripts, etc. + execl("/bin/sh", "sh", "-c", spec, (char*)NULL); + } +} + +bool Host_Spawn(const char* spec, bool isUrl, bool isSteamUrl, const char* workdir, + char* err, size_t errSize) { + pid_t pid = fork(); + if (pid == 0) { + if (workdir) (void)chdir(workdir); + RestoreHostLibraryPath(); + ExecLaunch(spec, isUrl, isSteamUrl); + _exit(127); + } + if (pid < 0) { + snprintf(err, errSize, "fork() failed: %s", strerror(errno)); + return false; + } + // Parent: don't wait. Fire-and-forget. + return true; +} + +bool Host_PickFile(const char* title, char* out, size_t outSize) { + char cmd[512]; +#if defined(__APPLE__) + snprintf(cmd, sizeof(cmd), + "osascript -e 'POSIX path of (choose file with prompt \"%s\")' 2>/dev/null", title); +#else + snprintf(cmd, sizeof(cmd), "zenity --file-selection --title=\"%s\" 2>/dev/null", title); +#endif + FILE* p = popen(cmd, "r"); + if (!p) return false; + size_t n = 0; + char buf[2048]; + out[0] = '\0'; + while (fgets(buf, sizeof(buf), p)) { + size_t len = strlen(buf); + if (n + len >= outSize) len = outSize - 1 - n; + memcpy(out + n, buf, len); + n += len; + out[n] = '\0'; + } + pclose(p); + while (n && (out[n - 1] == '\n' || out[n - 1] == '\r')) out[--n] = '\0'; + return n > 0; +} diff --git a/theseus/desktop/host_win.cpp b/theseus/desktop/host_win.cpp index 8c6432a..2954bed 100644 --- a/theseus/desktop/host_win.cpp +++ b/theseus/desktop/host_win.cpp @@ -9,6 +9,7 @@ #ifndef NOMINMAX #define NOMINMAX #endif +#include #include #include @@ -113,6 +114,21 @@ bool Host_DiskSpace(const char* path, uint64_t* freeBytes, uint64_t* totalBytes) return true; } +// The CRT's tmpfile() wants the drive root, which a normal user can't write. +// "D" is the CRT's delete-on-close flag. +FILE* Host_TempFile() { + char dir[MAX_PATH], path[MAX_PATH]; + DWORD len = GetTempPathA(MAX_PATH, dir); + if (len == 0 || len > MAX_PATH) return NULL; + if (GetTempFileNameA(dir, "ths", 0, path) == 0) return NULL; + return fopen(path, "w+bD"); +} + +int64_t Host_Seek(FILE* f, int64_t offset, int whence) { + if (!f || _fseeki64(f, offset, whence) != 0) return -1; + return _ftelli64(f); +} + struct HostDir { HANDLE find; WIN32_FIND_DATAA first; @@ -155,3 +171,24 @@ void Host_CloseDir(HostDir* dir) { FindClose(dir->find); free(dir); } + +bool Host_MemoryStatus(uint64_t* totalBytes, uint64_t* availBytes) { + MEMORYSTATUSEX ms; + ms.dwLength = sizeof(ms); + if (!GlobalMemoryStatusEx(&ms)) return false; + if (totalBytes) *totalBytes = ms.ullTotalPhys; + if (availBytes) *availBytes = ms.ullAvailPhys; + return true; +} + +// Whatever the host name resolves to first. No netmask from this route. +uint32_t Host_LocalIPv4(uint32_t* netmask) { + uint32_t addr = 0; + char hostname[256]; + if (gethostname(hostname, sizeof(hostname)) == 0) { + struct hostent* he = gethostbyname(hostname); + if (he && he->h_addr_list[0]) memcpy(&addr, he->h_addr_list[0], 4); + } + if (netmask) *netmask = 0; + return addr; +} diff --git a/theseus/desktop/host_win_app.cpp b/theseus/desktop/host_win_app.cpp new file mode 100644 index 0000000..3f7c867 --- /dev/null +++ b/theseus/desktop/host_win_app.cpp @@ -0,0 +1,195 @@ +// host_win_app.cpp: the process half of host.h for Windows. Like +// host_win.cpp, this may see and must never see std.h. + +#include "host.h" + +#include +#include + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +void Host_ConsoleOrLog(const char* logPath) { + // /SUBSYSTEM:WINDOWS so explorer launches don't pop a console, but + // attach to a parent shell when one exists so stdout/stderr show up. + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + freopen("CONOUT$", "w", stdout); + freopen("CONOUT$", "w", stderr); + freopen("CONIN$", "r", stdin); + return; + } + // Launched from Explorer, so there's no console and every fprintf goes + // nowhere. Tee to a log the user can actually send back. + char dir[MAX_PATH]; + snprintf(dir, sizeof(dir), "%s", logPath); + char* slash = strrchr(dir, '/'); + char* back = strrchr(dir, '\\'); + if (back > slash) slash = back; + if (slash) { + *slash = '\0'; + Host_MakeDir(dir); + } + if (freopen(logPath, "w", stdout)) { + setvbuf(stdout, NULL, _IOLBF, 0); + _dup2(_fileno(stdout), _fileno(stderr)); + } +} + +void Host_BoostProcess() { + //Milenko-Testing: I don't run windows, i googled this. don't know if it will work as intended. + SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS); + DwmEnableMMCSS(TRUE); + { + DWORD taskIdx = 0; + AvSetMmThreadCharacteristicsW(L"Games", &taskIdx); + } + typedef BOOL (WINAPI *SetProcessInfoFn)(HANDLE, PROCESS_INFORMATION_CLASS, LPVOID, DWORD); + HMODULE hKernel = GetModuleHandleA("kernel32.dll"); + if (hKernel) { + SetProcessInfoFn pSetProcessInformation = + (SetProcessInfoFn)GetProcAddress(hKernel, "SetProcessInformation"); + if (pSetProcessInformation) { + PROCESS_POWER_THROTTLING_STATE pt = {0}; + pt.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION; + pt.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED; + pt.StateMask = 0; // 0 == opt out of throttling + pSetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &pt, sizeof(pt)); + } + } +} + +static LONG WINAPI WindowsCrashHandler(EXCEPTION_POINTERS* ep) { + fprintf(stderr, "\n[CRASH] Exception 0x%08lX at address 0x%p\n", + ep->ExceptionRecord->ExceptionCode, ep->ExceptionRecord->ExceptionAddress); + if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2) { + fprintf(stderr, "[CRASH] %s address 0x%p\n", + ep->ExceptionRecord->ExceptionInformation[0] ? "Writing" : "Reading", + (void*)ep->ExceptionRecord->ExceptionInformation[1]); + } + // Walk the stack + HANDLE process = GetCurrentProcess(); + SymInitialize(process, NULL, TRUE); + STACKFRAME64 frame = {}; + CONTEXT* ctx = ep->ContextRecord; + frame.AddrPC.Offset = ctx->Rip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = ctx->Rbp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = ctx->Rsp; + frame.AddrStack.Mode = AddrModeFlat; + fprintf(stderr, "[CRASH] Stack trace:\n"); + for (int i = 0; i < 32; i++) { + if (!StackWalk64(IMAGE_FILE_MACHINE_AMD64, process, GetCurrentThread(), &frame, ctx, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL)) + break; + char symBuf[sizeof(SYMBOL_INFO) + 256]; + SYMBOL_INFO* sym = (SYMBOL_INFO*)symBuf; + sym->SizeOfStruct = sizeof(SYMBOL_INFO); + sym->MaxNameLen = 255; + DWORD64 disp = 0; + if (SymFromAddr(process, frame.AddrPC.Offset, &disp, sym)) + fprintf(stderr, " [%d] %s + 0x%llx\n", i, sym->Name, (unsigned long long)disp); + else + fprintf(stderr, " [%d] 0x%p\n", i, (void*)frame.AddrPC.Offset); + } + fflush(stderr); + return EXCEPTION_EXECUTE_HANDLER; +} + +static void AbortHandler(int sig) { + fprintf(stderr, "\n[CRASH] Signal %d (%s)\n", sig, sig == SIGABRT ? "SIGABRT" : "OTHER"); + fflush(stderr); + exit(128 + sig); +} + +void Host_InstallCrashHandler() { + SetUnhandledExceptionFilter(WindowsCrashHandler); + signal(SIGABRT, AbortHandler); +} + +bool Host_NativeWindow(SDL_Window* window, void** nwh, void** ndt, bool* wayland) { + *nwh = NULL; + *ndt = NULL; + *wayland = false; + SDL_SysWMinfo wmi; + SDL_VERSION(&wmi.version); + if (!SDL_GetWindowWMInfo(window, &wmi)) { + fprintf(stderr, "SDL_GetWindowWMInfo failed: %s\n", SDL_GetError()); + return false; + } + *nwh = wmi.info.win.window; + return true; +} + +// URLs go through ShellExecute. Raw commands run through cmd /C; no /S, so +// cmd preserves outer quotes for the 2-quote/exists case and strips +// first/last otherwise. +bool Host_Spawn(const char* spec, bool isUrl, bool isSteamUrl, const char* workdir, + char* err, size_t errSize) { + (void)isSteamUrl; + if (isUrl) { + HINSTANCE rc = ShellExecuteA(NULL, "open", spec, NULL, workdir, SW_SHOWNORMAL); + if ((INT_PTR)rc <= 32) { + snprintf(err, errSize, "ShellExecute failed (code %ld): %s", (long)(INT_PTR)rc, spec); + return false; + } + return true; + } + + STARTUPINFOA si = {}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi = {}; + char cmd[2048]; + // Quoted .exe spec skips cmd entirely. CreateProcessA with + // lpApplicationName=NULL parses the first quoted token as the exe with + // no shell quote-mangling. cmd's strip-first-and-last rule eats our + // outer wrapper otherwise. .bat / .cmd / etc. stay on the cmd path + // because they need a shell to interpret. + bool directExe = false; + if (spec[0] == '"') { + const char* exeEnd = strchr(spec + 1, '"'); + if (exeEnd && exeEnd - spec >= 5 && _strnicmp(exeEnd - 4, ".exe", 4) == 0) + directExe = true; + } + if (directExe) { + strncpy(cmd, spec, sizeof(cmd) - 1); + cmd[sizeof(cmd) - 1] = 0; + } else { + snprintf(cmd, sizeof(cmd), "cmd /C \"%s\"", spec); + } + if (!CreateProcessA(NULL, cmd, NULL, NULL, FALSE, DETACHED_PROCESS, NULL, workdir, &si, &pi)) { + DWORD e = GetLastError(); + snprintf(err, errSize, "CreateProcess failed (error %lu): %s", e, spec); + return false; + } + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return true; +} + +bool Host_PickFile(const char* title, char* out, size_t outSize) { + OPENFILENAMEA ofn = {}; + char path[MAX_PATH] = {}; + ofn.lStructSize = sizeof(ofn); + ofn.lpstrFile = path; + ofn.nMaxFile = MAX_PATH; + ofn.lpstrTitle = title; + ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR; + if (!GetOpenFileNameA(&ofn)) return false; + snprintf(out, outSize, "%s", path); + return true; +} diff --git a/theseus/desktop/launch.cpp b/theseus/desktop/launch.cpp index 70c99ae..cc0e414 100644 --- a/theseus/desktop/launch.cpp +++ b/theseus/desktop/launch.cpp @@ -11,17 +11,7 @@ #include -#ifdef _WIN32 -#include -#include -#else -#include -#include -#include -#include -#include -#include -#endif +#include "host.h" #include "virtual_games.h" #include "stb_image.h" @@ -47,47 +37,6 @@ static bool IsSteamUrl(const char* s) return strncmp(s, "steam://", 8) == 0; } -#ifndef _WIN32 -// Put back the LD_LIBRARY_PATH AppRun stashed, so a spawned /bin/sh doesn't -// load the AppImage's bundled readline. No-op outside an AppImage. -static void RestoreHostLibraryPath() -{ - const char* host = getenv("THESEUS_HOST_LD_LIBRARY_PATH"); - if (!host) return; - if (*host) setenv("LD_LIBRARY_PATH", host, 1); - else unsetenv("LD_LIBRARY_PATH"); -} - -// Build the argv for execlp/execl based on dispatch rules. Caller forks -// first; this runs in the child and never returns on success. -static void ExecLaunch(const char* spec) -{ - if (IsSteamUrl(spec)) - { -#ifdef __APPLE__ - // macOS: system handler works fine for steam:// - execlp("open", "open", spec, (char*)NULL); -#else - // Linux: bypass xdg-open/KIO; steam binary takes the URL directly. - execlp("steam", "steam", spec, (char*)NULL); -#endif - } - else if (IsUrl(spec)) - { -#ifdef __APPLE__ - execlp("open", "open", spec, (char*)NULL); -#else - execlp("xdg-open", "xdg-open", spec, (char*)NULL); -#endif - } - else - { - // Raw command line: shell parses paths, args, scripts, etc. - execl("/bin/sh", "sh", "-c", spec, (char*)NULL); - } -} -#endif - // Last-launch diagnostic captured by Launch_DoSpawn so Title Maker's // "Test Launch" button can surface it as a status toast instead of users // having to dig in stderr. @@ -112,13 +61,8 @@ static void TrimOuterQuotes(char* s) static bool IsExistingFile(const char* path) { -#ifdef _WIN32 - DWORD attr = GetFileAttributesA(path); - return attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY); -#else - struct stat st; - return stat(path, &st) == 0 && S_ISREG(st.st_mode); -#endif + HostFileInfo info; + return Host_Stat(path, &info) && !(info.attrs & HOST_ATTR_DIR); } // Parent dir of the program being spawned. Tries quoted path, then whole @@ -177,78 +121,11 @@ static bool Launch_DoSpawn(const char* expanded) char workdir[1024]; bool haveWorkdir = DeriveWorkDir(spec, workdir, sizeof(workdir)); -#ifdef _WIN32 - // URLs go through ShellExecute. Raw commands run through cmd /C; no - // /S, so cmd preserves outer quotes for the 2-quote/exists case and - // strips first/last otherwise. - if (IsUrl(spec)) - { - HINSTANCE rc = ShellExecuteA(NULL, "open", spec, NULL, - haveWorkdir ? workdir : NULL, SW_SHOWNORMAL); - if ((INT_PTR)rc <= 32) { - snprintf(g_launchLastResult, sizeof(g_launchLastResult), - "ShellExecute failed (code %ld): %s", (long)(INT_PTR)rc, spec); - fprintf(stderr, "[launch] %s\n", g_launchLastResult); - return false; - } - } - else - { - STARTUPINFOA si = {}; - si.cb = sizeof(si); - PROCESS_INFORMATION pi = {}; - char cmd[2048]; - // Quoted .exe spec skips cmd entirely. CreateProcessA with - // lpApplicationName=NULL parses the first quoted token as the - // exe with no shell quote-mangling. cmd's strip-first-and-last - // rule eats our outer wrapper otherwise. .bat / .cmd / etc. - // stay on the cmd path because they need a shell to interpret. - bool directExe = false; - if (spec[0] == '"') { - const char* exeEnd = strchr(spec + 1, '"'); - if (exeEnd && exeEnd - spec >= 5 && - strncasecmp(exeEnd - 4, ".exe", 4) == 0) { - directExe = true; - } - } - if (directExe) { - strncpy(cmd, spec, sizeof(cmd) - 1); - cmd[sizeof(cmd) - 1] = 0; - } else { - snprintf(cmd, sizeof(cmd), "cmd /C \"%s\"", spec); - } - if (!CreateProcessA(NULL, cmd, NULL, NULL, FALSE, - DETACHED_PROCESS, NULL, - haveWorkdir ? workdir : NULL, - &si, &pi)) - { - DWORD err = GetLastError(); - snprintf(g_launchLastResult, sizeof(g_launchLastResult), - "CreateProcess failed (error %lu): %s", err, spec); - fprintf(stderr, "[launch] %s\n", g_launchLastResult); - return false; - } - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - } -#else - pid_t pid = fork(); - if (pid == 0) - { - if (haveWorkdir) (void)chdir(workdir); - RestoreHostLibraryPath(); - ExecLaunch(spec); - _exit(127); - } - else if (pid < 0) - { - snprintf(g_launchLastResult, sizeof(g_launchLastResult), - "fork() failed: %s", strerror(errno)); + if (!Host_Spawn(spec, IsUrl(spec), IsSteamUrl(spec), haveWorkdir ? workdir : NULL, + g_launchLastResult, sizeof(g_launchLastResult))) { fprintf(stderr, "[launch] %s\n", g_launchLastResult); return false; } - // Parent: don't wait. Fire-and-forget. -#endif snprintf(g_launchLastResult, sizeof(g_launchLastResult), "Launched: %s", spec); diff --git a/theseus/desktop/media_db.cpp b/theseus/desktop/media_db.cpp index 1fef47a..f9b853d 100644 --- a/theseus/desktop/media_db.cpp +++ b/theseus/desktop/media_db.cpp @@ -20,7 +20,6 @@ // a stateful query API to XAP (SetCurrentShow / GetSeasonName / etc.). // ============================================================================ -#include #include #include #include @@ -187,28 +186,28 @@ static std::string ParseEpisodeTitle(const std::string& filename, int& seasonOut // Season folder or similar sitting under the title folder. static std::string FindFirstVideoFile(const std::string& dir, int depth = 1) { - DIR* d = opendir(dir.c_str()); + HostDir* d = Host_OpenDir(dir.c_str()); if (!d) return ""; std::vector subdirs; std::string out; - struct dirent* e; - while ((e = readdir(d))) + char eName[260]; + while (Host_ReadDir(d, eName, sizeof(eName), NULL)) { - if (e->d_name[0] == '.') continue; - if (IsVideoExt(e->d_name)) + if (eName[0] == '.') continue; + if (IsVideoExt(eName)) { - out = dir + "/" + e->d_name; + out = dir + "/" + eName; break; } if (depth > 1) { - std::string sub = dir + "/" + e->d_name; + std::string sub = dir + "/" + eName; struct stat st; if (stat(sub.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) subdirs.push_back(sub); } } - closedir(d); + Host_CloseDir(d); if (!out.empty()) return out; // Files win over folders, so only descend once this level came up empty. @@ -226,21 +225,21 @@ static std::string FindFirstVideoFile(const std::string& dir, int depth = 1) static void ScanMoviesInto(std::vector& out) { out.clear(); - DIR* d = opendir(kMoviesRoot); + HostDir* d = Host_OpenDir(kMoviesRoot); if (!d) return; - struct dirent* e; - while ((e = readdir(d))) + char eName[260]; + while (Host_ReadDir(d, eName, sizeof(eName), NULL)) { - if (e->d_name[0] == '.') continue; - std::string folder = std::string(kMoviesRoot) + "/" + e->d_name; + if (eName[0] == '.') continue; + std::string folder = std::string(kMoviesRoot) + "/" + eName; struct stat st; if (stat(folder.c_str(), &st) != 0) continue; std::string videoPath; - std::string title = e->d_name; + std::string title = eName; if (S_ISDIR(st.st_mode)) { videoPath = FindFirstVideoFile(folder, 2); - } else if (IsVideoExt(e->d_name)) { + } else if (IsVideoExt(eName)) { // Movies dropped straight in the root, no folder per title. videoPath = folder; size_t dot = title.find_last_of('.'); @@ -257,40 +256,40 @@ static void ScanMoviesInto(std::vector& out) BumpProgress(n); if ((n % 50) == 0) printf("[MediaDB] Movies: %d found\n", n); } - closedir(d); + Host_CloseDir(d); printf("[MediaDB] Movie scan done: %d total\n", (int)out.size()); } static void ScanShowSeasons(ShowRec& show) { - DIR* d = opendir(show.path.c_str()); + HostDir* d = Host_OpenDir(show.path.c_str()); if (!d) return; - struct dirent* e; - while ((e = readdir(d))) + char eName[260]; + while (Host_ReadDir(d, eName, sizeof(eName), NULL)) { - if (e->d_name[0] == '.') continue; - std::string seasonDir = show.path + "/" + e->d_name; + if (eName[0] == '.') continue; + std::string seasonDir = show.path + "/" + eName; struct stat st; if (stat(seasonDir.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) continue; - if (strncasecmp(e->d_name, "Season", 6) != 0 && - strncasecmp(e->d_name, "Specials", 8) != 0) continue; + if (strncasecmp(eName, "Season", 6) != 0 && + strncasecmp(eName, "Specials", 8) != 0) continue; SeasonRec s; - s.name = e->d_name; + s.name = eName; - DIR* sd = opendir(seasonDir.c_str()); + HostDir* sd = Host_OpenDir(seasonDir.c_str()); if (!sd) continue; - struct dirent* se; - while ((se = readdir(sd))) + char seName[260]; + while (Host_ReadDir(sd, seName, sizeof(seName), NULL)) { - if (se->d_name[0] == '.') continue; - if (!IsVideoExt(se->d_name)) continue; + if (seName[0] == '.') continue; + if (!IsVideoExt(seName)) continue; EpisodeRec ep; - ep.path = seasonDir + "/" + se->d_name; - ep.title = ParseEpisodeTitle(se->d_name, ep.season, ep.episode); + ep.path = seasonDir + "/" + seName; + ep.title = ParseEpisodeTitle(seName, ep.season, ep.episode); s.episodes.push_back(ep); } - closedir(sd); + Host_CloseDir(sd); // Sort episodes by episode number. for (size_t i = 1; i < s.episodes.size(); i++) @@ -303,7 +302,7 @@ static void ScanShowSeasons(ShowRec& show) show.seasons.push_back(s); } - closedir(d); + Host_CloseDir(d); // Episodes sitting straight in the show folder, no Season dirs. Common // enough that dropping the show entirely just looks like nothing loaded. @@ -311,19 +310,19 @@ static void ScanShowSeasons(ShowRec& show) { SeasonRec s; s.name = "Season 1"; - DIR* fd = opendir(show.path.c_str()); + HostDir* fd = Host_OpenDir(show.path.c_str()); if (!fd) return; - struct dirent* fe; - while ((fe = readdir(fd))) + char feName[260]; + while (Host_ReadDir(fd, feName, sizeof(feName), NULL)) { - if (fe->d_name[0] == '.') continue; - if (!IsVideoExt(fe->d_name)) continue; + if (feName[0] == '.') continue; + if (!IsVideoExt(feName)) continue; EpisodeRec ep; - ep.path = show.path + "/" + fe->d_name; - ep.title = ParseEpisodeTitle(fe->d_name, ep.season, ep.episode); + ep.path = show.path + "/" + feName; + ep.title = ParseEpisodeTitle(feName, ep.season, ep.episode); s.episodes.push_back(ep); } - closedir(fd); + Host_CloseDir(fd); if (!s.episodes.empty()) show.seasons.push_back(s); } } @@ -331,13 +330,13 @@ static void ScanShowSeasons(ShowRec& show) static void ScanShowsInto(std::vector& out) { out.clear(); - DIR* d = opendir(kTvRoot); + HostDir* d = Host_OpenDir(kTvRoot); if (!d) return; - struct dirent* e; - while ((e = readdir(d))) + char eName[260]; + while (Host_ReadDir(d, eName, sizeof(eName), NULL)) { - if (e->d_name[0] == '.') continue; - std::string folder = std::string(kTvRoot) + "/" + e->d_name; + if (eName[0] == '.') continue; + std::string folder = std::string(kTvRoot) + "/" + eName; struct stat st; if (stat(folder.c_str(), &st) != 0) continue; @@ -346,8 +345,8 @@ static void ScanShowsInto(std::vector& out) { // A loose episode in the root. Its own one episode show, so it // is at least reachable instead of invisible. - if (!IsVideoExt(e->d_name)) continue; - std::string title = e->d_name; + if (!IsVideoExt(eName)) continue; + std::string title = eName; size_t dot = title.find_last_of('.'); if (dot != std::string::npos) title.erase(dot); @@ -355,7 +354,7 @@ static void ScanShowsInto(std::vector& out) s.name = "Season 1"; EpisodeRec ep; ep.path = folder; - ep.title = ParseEpisodeTitle(e->d_name, ep.season, ep.episode); + ep.title = ParseEpisodeTitle(eName, ep.season, ep.episode); s.episodes.push_back(ep); sh.title = title; @@ -366,7 +365,7 @@ static void ScanShowsInto(std::vector& out) continue; } - sh.title = e->d_name; + sh.title = eName; sh.path = folder; ScanShowSeasons(sh); if (!sh.seasons.empty()) { @@ -376,7 +375,7 @@ static void ScanShowsInto(std::vector& out) if ((n % 10) == 0) printf("[MediaDB] Shows: %d found\n", n); } } - closedir(d); + Host_CloseDir(d); printf("[MediaDB] Show scan done: %d total\n", (int)out.size()); } diff --git a/theseus/desktop/platform_shim.h b/theseus/desktop/platform_shim.h index 1cb774b..991f115 100644 --- a/theseus/desktop/platform_shim.h +++ b/theseus/desktop/platform_shim.h @@ -1,11 +1,15 @@ -// platform_shim.h: desktop platform abstraction layer. Replaces Xbox -// XDK and D3D8 types with SDL + cross-platform equivalents (HANDLE, -// HRESULT, FILETIME, the WINAPI calling convention, kernel stubs). -// Pulled in early by std.h so the rest of the dashboard can keep -// using Win32 / Xbox idioms unmodified. +// platform_shim.h: the Xbox-shaped world the dashboard code lives in on +// desktop. Win32 and XDK names (HANDLE, ReadFile, GetTickCount, FILETIME) +// backed by SDL, the C runtime and host.h. +// +// This file is the same on every host and never includes an OS header. +// Real Win32 lives in host_win.cpp, so dashboard code can't reach kernel32 +// by accident. #pragma once +#include "platform.h" +#include "host.h" #include "xboxfs_drive.h" // XboxFS_DriveToPrefix shared helper, used by stubs below #include "xbox_path.h" // the one Xbox path translator, used by stubs below @@ -20,39 +24,8 @@ #include #include #include -#ifdef _WIN32 -#include -#include -#include -#include -#include -#define strcasecmp _stricmp -#define strncasecmp _strnicmp -#define access _access -#ifndef F_OK -#define F_OK 0 -#endif -#ifndef S_ISDIR -#define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR) -#endif -#ifndef S_ISREG -#define S_ISREG(m) (((m) & _S_IFMT) == _S_IFREG) -#endif -#else -#include -#include -#include -#include -#include -#include -#ifdef __APPLE__ -#include -#include -#elif defined(__linux__) -#include -#endif -#endif #include +#include #else #include #include @@ -64,25 +37,26 @@ #include #include #include +#include +#endif +// All three toolchains ship these. Not : on mingw that's bx's +// compat header, which pulls in . Folder listing is host.h's. +#include +#include +#include +#ifndef S_ISDIR +#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) +#endif +#ifndef S_ISREG +#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) +#endif +#ifndef F_OK +#define F_OK 0 #endif // ------------------------------------------------------- // Windows base types // ------------------------------------------------------- -#ifdef _WIN32 -// On actual Windows, include for real Win32 API -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -// D3D types not provided by windows.h -typedef DWORD D3DCOLOR; -typedef float D3DVALUE; -#else -// Non-Windows: define Windows types ourselves typedef uint8_t BYTE; typedef uint16_t WORD; typedef uint32_t DWORD; @@ -125,10 +99,7 @@ typedef struct _FILETIME { DWORD dwLowDateTime; DWORD dwHighDateTime; } FILETIME, *PFILETIME, *LPFILETIME; -#endif // _WIN32 -#ifndef _WIN32 -// Non-Windows stubs for Windows API types and constants #define CONST const #define MB_OK 0 #define CALLBACK @@ -183,7 +154,7 @@ inline DWORD GetTimeZoneInformation(TIME_ZONE_INFORMATION* tz) { return TIME_ZONE_ID_UNKNOWN; } -// MEMORYSTATUS - reports real host memory (was fake 64MB Xbox values) +// MEMORYSTATUS: real host memory, not the Xbox's 64MB. typedef struct _MEMORYSTATUS { DWORD dwLength; DWORD dwMemoryLoad; @@ -197,28 +168,10 @@ typedef struct _MEMORYSTATUS { inline void GlobalMemoryStatus(MEMORYSTATUS* p) { memset(p, 0, sizeof(*p)); p->dwLength = sizeof(*p); -#ifdef __APPLE__ - // sysctl for total physical memory - int mib[2] = { CTL_HW, HW_MEMSIZE }; - uint64_t memsize = 0; - size_t len = sizeof(memsize); - if (sysctl(mib, 2, &memsize, &len, NULL, 0) == 0) - p->dwTotalPhys = (SIZE_T)memsize; - // vm_statistics for free pages - mach_msg_type_number_t count = HOST_VM_INFO_COUNT; - vm_statistics_data_t vmstat; - if (host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmstat, &count) == KERN_SUCCESS) - p->dwAvailPhys = (SIZE_T)vmstat.free_count * vm_page_size; -#elif defined(__linux__) - struct sysinfo si; - if (sysinfo(&si) == 0) { - p->dwTotalPhys = (SIZE_T)si.totalram * si.mem_unit; - p->dwAvailPhys = (SIZE_T)si.freeram * si.mem_unit; - } -#else - p->dwTotalPhys = 4ULL * 1024 * 1024 * 1024; // fallback 4GB - p->dwAvailPhys = 2ULL * 1024 * 1024 * 1024; -#endif + uint64_t total = 0, avail = 0; + Host_MemoryStatus(&total, &avail); + p->dwTotalPhys = (SIZE_T)total; + p->dwAvailPhys = (SIZE_T)avail; } // RECT stub @@ -267,18 +220,50 @@ typedef int GET_FILEEX_INFO_LEVELS; #endif #define INVALID_HANDLE_VALUE ((HANDLE)(intptr_t)-1) -#endif // !_WIN32 // --- Inline function stubs (must come after TRUE/FALSE/INVALID_HANDLE_VALUE) --- // Virtual games database (needed by all platforms) #include "virtual_games.h" -// Win32 API function stubs (only needed on non-Windows platforms) -#ifndef _WIN32 +// Seconds since 1970 to FILETIME's 100ns ticks since 1601, and back. +inline FILETIME Shim_UnixToFileTime(int64_t t) { + unsigned long long ull = (unsigned long long)(t + 11644473600LL) * 10000000ULL; + FILETIME ft; + ft.dwLowDateTime = (DWORD)(ull & 0xFFFFFFFF); + ft.dwHighDateTime = (DWORD)(ull >> 32); + return ft; +} +inline int64_t Shim_FileTimeToUnix(const FILETIME* ft) { + unsigned long long ull = ((unsigned long long)ft->dwHighDateTime << 32) | ft->dwLowDateTime; + return (int64_t)(ull / 10000000ULL) - 11644473600LL; +} +// Days since 1970 for a proleptic Gregorian date, no time zone involved. +inline int64_t Shim_DaysFromCivil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int64_t era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = (unsigned)(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + (int64_t)doe - 719468; +} +// Local time minus UTC, in seconds, at the given moment. +inline int64_t Shim_UtcOffset(time_t t) { + struct tm lt = *localtime(&t); + struct tm gt = *gmtime(&t); + int64_t l = Shim_DaysFromCivil(lt.tm_year + 1900, lt.tm_mon + 1, lt.tm_mday) * 86400 + + lt.tm_hour * 3600 + lt.tm_min * 60 + lt.tm_sec; + int64_t g = Shim_DaysFromCivil(gt.tm_year + 1900, gt.tm_mon + 1, gt.tm_mday) * 86400 + + gt.tm_hour * 3600 + gt.tm_min * 60 + gt.tm_sec; + return l - g; +} + +// File HANDLEs are FILE*s on every host, opened by TheseusCreateFile. inline BOOL GetFileTime(HANDLE h, FILETIME* c, FILETIME* a, FILETIME* w) { - (void)h; FILETIME ft = {0, 0}; + HostFileInfo info; + if (h && h != INVALID_HANDLE_VALUE && Host_StatOpenFile((FILE*)h, &info)) + ft = Shim_UnixToFileTime(info.mtime); if (c) *c = ft; if (a) *a = ft; if (w) *w = ft; @@ -290,67 +275,59 @@ inline BOOL CompareFileTime(const FILETIME* a, const FILETIME* b) { return 0; } inline DWORD GetLastError() { return 0; } +inline void Shim_TmToSystemTime(const struct tm* t, int ms, SYSTEMTIME* st) { + st->wYear = (WORD)(t->tm_year + 1900); + st->wMonth = (WORD)(t->tm_mon + 1); + st->wDayOfWeek = (WORD)t->tm_wday; + st->wDay = (WORD)t->tm_mday; + st->wHour = (WORD)t->tm_hour; + st->wMinute = (WORD)t->tm_min; + st->wSecond = (WORD)t->tm_sec; + st->wMilliseconds = (WORD)ms; +} inline void GetLocalTime(SYSTEMTIME* st) { + if (!st) return; time_t now = time(NULL); struct tm* lt = localtime(&now); - if (lt && st) { - st->wYear = (WORD)(lt->tm_year + 1900); - st->wMonth = (WORD)(lt->tm_mon + 1); - st->wDayOfWeek = (WORD)lt->tm_wday; - st->wDay = (WORD)lt->tm_mday; - st->wHour = (WORD)lt->tm_hour; - st->wMinute = (WORD)lt->tm_min; - st->wSecond = (WORD)lt->tm_sec; - st->wMilliseconds = 0; - } else if (st) { memset(st, 0, sizeof(*st)); } + if (lt) Shim_TmToSystemTime(lt, 0, st); + else memset(st, 0, sizeof(*st)); } inline void GetSystemTime(SYSTEMTIME* st) { + if (!st) return; time_t now = time(NULL); struct tm* gm = gmtime(&now); - if (gm && st) { - st->wYear = (WORD)(gm->tm_year + 1900); - st->wMonth = (WORD)(gm->tm_mon + 1); - st->wDayOfWeek = (WORD)gm->tm_wday; - st->wDay = (WORD)gm->tm_mday; - st->wHour = (WORD)gm->tm_hour; - st->wMinute = (WORD)gm->tm_min; - st->wSecond = (WORD)gm->tm_sec; - st->wMilliseconds = 0; - } else if (st) { memset(st, 0, sizeof(*st)); } + if (gm) Shim_TmToSystemTime(gm, 0, st); + else memset(st, 0, sizeof(*st)); } +// SYSTEMTIME is UTC here, same as Win32. inline BOOL SystemTimeToFileTime(const SYSTEMTIME* st, FILETIME* ft) { - // Approximate: store as Unix timestamp * 10,000,000 + Windows epoch offset - struct tm t = {}; - t.tm_year = st->wYear - 1900; t.tm_mon = st->wMonth - 1; t.tm_mday = st->wDay; - t.tm_hour = st->wHour; t.tm_min = st->wMinute; t.tm_sec = st->wSecond; - time_t tt = mktime(&t); - unsigned long long ull = (unsigned long long)tt * 10000000ULL + 116444736000000000ULL; + int64_t t = Shim_DaysFromCivil(st->wYear, st->wMonth, st->wDay) * 86400 + + st->wHour * 3600 + st->wMinute * 60 + st->wSecond; + unsigned long long ull = (unsigned long long)(t + 11644473600LL) * 10000000ULL + + (unsigned long long)st->wMilliseconds * 10000ULL; ft->dwLowDateTime = (DWORD)(ull & 0xFFFFFFFF); ft->dwHighDateTime = (DWORD)(ull >> 32); return TRUE; } inline BOOL FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) { unsigned long long ull = ((unsigned long long)ft->dwHighDateTime << 32) | ft->dwLowDateTime; - time_t tt = (time_t)((ull - 116444736000000000ULL) / 10000000ULL); + time_t tt = (time_t)Shim_FileTimeToUnix(ft); struct tm* gm = gmtime(&tt); - if (gm && st) { - st->wYear = (WORD)(gm->tm_year + 1900); st->wMonth = (WORD)(gm->tm_mon + 1); - st->wDayOfWeek = (WORD)gm->tm_wday; st->wDay = (WORD)gm->tm_mday; - st->wHour = (WORD)gm->tm_hour; st->wMinute = (WORD)gm->tm_min; - st->wSecond = (WORD)gm->tm_sec; st->wMilliseconds = 0; - } else if (st) { memset(st, 0, sizeof(*st)); } - return TRUE; + if (gm && st) Shim_TmToSystemTime(gm, (int)((ull / 10000ULL) % 1000), st); + else if (st) memset(st, 0, sizeof(*st)); + return gm != NULL; } inline BOOL FileTimeToLocalFileTime(const FILETIME* ftUtc, FILETIME* ftLocal) { - // On desktop, just copy (timezone offset not critical for preview) - *ftLocal = *ftUtc; + unsigned long long ull = ((unsigned long long)ftUtc->dwHighDateTime << 32) | ftUtc->dwLowDateTime; + int64_t off = Shim_UtcOffset((time_t)Shim_FileTimeToUnix(ftUtc)); + ull += (unsigned long long)(off * 10000000LL); + ftLocal->dwLowDateTime = (DWORD)(ull & 0xFFFFFFFF); + ftLocal->dwHighDateTime = (DWORD)(ull >> 32); return TRUE; } #define MAX_PATH 260 -#endif // !_WIN32 -#ifndef _WIN32 // HRESULT values #define S_OK ((HRESULT)0) #define S_FALSE ((HRESULT)1) @@ -371,10 +348,19 @@ typedef LONG NTSTATUS; typedef struct _GUID { uint32_t Data1; uint16_t Data2, Data3; uint8_t Data4[8]; } GUID, IID, CLSID; typedef const GUID& REFIID; typedef const GUID& REFCLSID; +// mingw already has these as macros for its own headers. +#ifndef STDMETHODCALLTYPE #define STDMETHODCALLTYPE +#endif +#ifndef __stdcall #define __stdcall +#endif +#ifndef __cdecl #define __cdecl +#endif +#ifndef WINAPI #define WINAPI +#endif class IUnknown { public: @@ -386,18 +372,7 @@ class IUnknown { inline long InterlockedIncrement(long* p) { return __sync_add_and_fetch(p, 1); } inline long InterlockedDecrement(long* p) { return __sync_sub_and_fetch(p, 1); } -#else -// Windows: NTSTATUS not always in windows.h -typedef LONG NTSTATUS; -#ifndef NT_SUCCESS -#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0) -#endif -#ifndef STATUS_SUCCESS -#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) -#endif -#endif // !_WIN32 -#ifndef _WIN32 // ------------------------------------------------------- // TCHAR - use char (ANSI), same as Xbox // ------------------------------------------------------- @@ -436,7 +411,6 @@ typedef TCHAR* LPTSTR; #define _totupper toupper #define _totlower tolower #define _tcstod strtod -#define _itoa(val, buf, radix) snprintf(buf, 32, "%d", val) #define _tfopen fopen #define _ftprintf fprintf @@ -448,7 +422,9 @@ inline char* _tcsupr(char* s) { for (char* p = s; *p; p++) *p = toupper((unsigne inline void Unicode(char* wsz, const char* sz, int nMaxChars) { strncpy(wsz, sz, nMaxChars); } inline void Ansi(char* sz, const char* wsz, int nMaxChars) { strncpy(sz, wsz, nMaxChars); } -// Windows string functions mapped to POSIX +// Microsoft C runtime names. mingw links that runtime, so it has them. +#ifndef THESEUS_CRT_MS +#define _itoa(val, buf, radix) snprintf(buf, 32, "%d", val) #define _strdup strdup #define _stricmp strcasecmp #define _strnicmp strncasecmp @@ -457,6 +433,7 @@ inline void Ansi(char* sz, const char* wsz, int nMaxChars) { strncpy(sz, wsz, nM #define _fileno fileno #define stricmp strcasecmp #define strnicmp strncasecmp +#endif // Memory macros #define CopyMemory(dst, src, len) memcpy(dst, src, len) @@ -464,16 +441,6 @@ inline void Ansi(char* sz, const char* wsz, int nMaxChars) { strncpy(sz, wsz, nM #define FillMemory(dst, len, val) memset(dst, val, len) #define MoveMemory(dst, src, len) memmove(dst, src, len) #define CopyChars(dest, src, count) memcpy(dest, src, (count) * sizeof(TCHAR)) -#else -// Windows: TCHAR mappings come from -#include -typedef TCHAR FSCHAR; -#define _FS(s) _T(s) -// Unicode/Ansi conversion stubs (we use char everywhere) -inline void Unicode(char* wsz, const char* sz, int nMaxChars) { strncpy(wsz, sz, nMaxChars); } -inline void Ansi(char* sz, const char* wsz, int nMaxChars) { strncpy(sz, wsz, nMaxChars); } -#define CopyChars(dest, src, count) memcpy(dest, src, (count) * sizeof(TCHAR)) -#endif // !_WIN32 // ------------------------------------------------------- // Xbox kernel string @@ -500,34 +467,13 @@ extern bool g_desktopRestartRequested; -// Network - host IP query (real implementation, not a stub) +// Network: the host's own address. typedef struct { DWORD ina, inaMask, inaGateway, inaDnsPrimary, inaDnsSecondary; } XNetConfigStatus; inline int XNetGetConfigStatus(XNetConfigStatus* p) { memset(p, 0, sizeof(*p)); -#ifdef _WIN32 - // Use Winsock to get local IP - char hostname[256]; - if (gethostname(hostname, sizeof(hostname)) == 0) { - struct hostent* he = gethostbyname(hostname); - if (he && he->h_addr_list[0]) { - memcpy(&p->ina, he->h_addr_list[0], 4); - } - } -#else - // Get real local IP via getifaddrs (POSIX) - struct ifaddrs* ifa = NULL; - if (getifaddrs(&ifa) == 0) { - for (struct ifaddrs* cur = ifa; cur; cur = cur->ifa_next) { - if (!cur->ifa_addr || cur->ifa_addr->sa_family != AF_INET) continue; - DWORD addr = ((struct sockaddr_in*)cur->ifa_addr)->sin_addr.s_addr; - if ((addr & 0xFF) == 127) continue; - p->ina = addr; - p->inaMask = cur->ifa_netmask ? ((struct sockaddr_in*)cur->ifa_netmask)->sin_addr.s_addr : 0; - break; - } - freeifaddrs(ifa); - } -#endif + uint32_t mask = 0; + p->ina = Host_LocalIPv4(&mask); + p->inaMask = mask; if (p->ina == 0) p->ina = 0x0100007f; // fallback 127.0.0.1 return 0; } @@ -542,9 +488,8 @@ typedef struct { BYTE Signature[20]; } XCALCSIG_SIGNATURE; -#ifndef _WIN32 -// Handle-based file stubs. POSIX file HANDLEs are FILE*s, opened by -// TheseusCreateFile in xbox_file.cpp. +// Handle-based file calls. File HANDLEs are FILE*s on every host, opened +// by TheseusCreateFile in xbox_file.cpp. inline BOOL ReadFile(HANDLE h, void* buf, DWORD nBytes, DWORD* nRead, void* ovlp) { size_t r = fread(buf, 1, nBytes, (FILE*)h); if (nRead) *nRead = (DWORD)r; @@ -554,16 +499,18 @@ inline BOOL ReadFile(HANDLE h, void* buf, DWORD nBytes, DWORD* nRead, void* ovlp inline BOOL CloseHandle(HANDLE h) { if (h && h != INVALID_HANDLE_VALUE) fclose((FILE*)h); return TRUE; } inline DWORD SetFilePointer(HANDLE h, LONG dist, LONG* distHigh, DWORD method) { int origin = (method == 0) ? SEEK_SET : (method == 1) ? SEEK_CUR : SEEK_END; - fseek((FILE*)h, dist, origin); - return (DWORD)ftell((FILE*)h); + int64_t off = distHigh ? (int64_t)(((uint64_t)(uint32_t)*distHigh << 32) | (uint32_t)dist) : (int64_t)dist; + int64_t pos = Host_Seek((FILE*)h, off, origin); + if (pos < 0) return 0xFFFFFFFF; + if (distHigh) *distHigh = (LONG)(pos >> 32); + return (DWORD)(pos & 0xFFFFFFFF); } inline DWORD GetFileSize(HANDLE h, DWORD* high) { - long pos = ftell((FILE*)h); - fseek((FILE*)h, 0, SEEK_END); - long size = ftell((FILE*)h); - fseek((FILE*)h, pos, SEEK_SET); - if (high) *high = 0; - return (DWORD)size; + HostFileInfo info; + fflush((FILE*)h); + if (!Host_StatOpenFile((FILE*)h, &info)) info.size = 0; + if (high) *high = (DWORD)(info.size >> 32); + return (DWORD)(info.size & 0xFFFFFFFF); } #define GENERIC_READ 0x80000000 #define GENERIC_WRITE 0x40000000 @@ -608,8 +555,6 @@ inline LPTSTR CharLower(LPTSTR p) { } -#include - // CreateThread stub (single-threaded on POSIX for now) typedef DWORD (*LPTHREAD_START_ROUTINE)(void*); inline HANDLE CreateThread(void* sa, DWORD stackSize, LPTHREAD_START_ROUTINE startAddr, void* param, DWORD flags, DWORD* tid) { @@ -647,7 +592,6 @@ inline void Sleep(DWORD ms) { SDL_Delay(ms); } // Debug output inline void OutputDebugStringA(const char* s) { fprintf(stderr, "%s", s); } #define OutputDebugString OutputDebugStringA -#endif // !_WIN32 // ------------------------------------------------------- // Macros @@ -659,13 +603,10 @@ inline void OutputDebugStringA(const char* s) { fprintf(stderr, "%s", s); } #define ARRAYSIZE(a) (sizeof(a) / sizeof((a)[0])) #endif -#ifndef _WIN32 -// SetRect stub (also in Image.h for non-Windows) #ifndef SetRect #define SetRect(pRect, nLeft, nTop, nRight, nBottom) \ do { (pRect)->left = (nLeft); (pRect)->top = (nTop); (pRect)->right = (nRight); (pRect)->bottom = (nBottom); } while(0) #endif -#endif // !_WIN32 // XGWriteSurfaceToFile stub (used by screen.cpp screenshot) inline HRESULT XGWriteSurfaceToFile(void* pSurface, const char* path) { (void)pSurface; (void)path; return 0; } @@ -691,7 +632,6 @@ typedef void* LPDIRECTSOUNDBUFFER; typedef void* LPDIRECTSOUND; #endif -#ifndef _WIN32 // OVERLAPPED stub (async I/O - not used on desktop) typedef struct _OVERLAPPED { DWORD Internal; @@ -718,7 +658,6 @@ typedef struct tagSIZE { LONG cx, cy; } SIZE; // COLORREF typedef DWORD COLORREF; -#endif // !_WIN32 // D3DBACKBUFFER_TYPE_MONO #define D3DBACKBUFFER_TYPE_MONO 0 @@ -758,7 +697,6 @@ typedef struct { DWORD Common; DWORD Data; void (*Register)(void*); } D3DResourc inline void D3D_CopyContiguousMemoryToVideo(void* p) { (void)p; } inline void* D3D_AllocContiguousMemory(DWORD size, DWORD align) { return malloc(size); } -#ifndef _WIN32 // LARGE_INTEGER typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; }; @@ -770,12 +708,7 @@ typedef union _ULARGE_INTEGER { unsigned long long QuadPart; } ULARGE_INTEGER; -#endif // !_WIN32 - - - -#ifndef _WIN32 -// LARGE_INTEGER / LONGLONG +// LONGLONG typedef long long LONGLONG; typedef unsigned long long ULONGLONG; @@ -799,9 +732,10 @@ inline int WideCharToMultiByte(UINT codePage, DWORD flags, const wchar_t* src, i // ANSI build, so the A struct is the struct. typedef WIN32_FIND_DATA WIN32_FIND_DATAA; -// __debugbreak +// __debugbreak is a compiler intrinsic under mingw. +#ifndef THESEUS_CRT_MS #define __debugbreak() SDL_TriggerBreakpoint() -#endif // !_WIN32 +#endif #ifndef ERROR_SUCCESS #define ERROR_SUCCESS 0 @@ -815,30 +749,22 @@ typedef WIN32_FIND_DATA WIN32_FIND_DATAA; #define XC_GAME_REGION_JAPAN 2 #define XC_GAME_REGION_RESTOFWORLD 3 -// (TIME_ZONE_INFORMATION moved earlier, before timezone stubs) // ------------------------------------------------------- // Missing constants for Locale.cpp / Xbox.cpp // ------------------------------------------------------- -#ifndef _WIN32 #define GENERIC_ALL 0x10000000 -#endif // ------------------------------------------------------- // Xbox.cpp stubs // ------------------------------------------------------- -#ifndef _WIN32 -#endif // !_WIN32 - -// IN_ADDR struct (Windows already has this from winsock2.h) -#ifndef _WIN32 +// IN_ADDR typedef struct in_addr_stub { DWORD s_addr; in_addr_stub() : s_addr(0) {} in_addr_stub(DWORD a) : s_addr(a) {} } IN_ADDR; -#endif // Overload for IN_ADDR parameter inline void XNetInAddrToString(IN_ADDR addr, char* pStr, int len) { @@ -851,7 +777,7 @@ inline void XNetInAddrToString(IN_ADDR addr, char* pStr, int len) { -#ifndef _WIN32 +#ifndef THESEUS_CRT_MS // swprintf - macOS/Linux swprintf requires a size param, but Xbox code doesn't pass one // Use a wrapper function instead of a recursive macro inline int _uix_swprintf(wchar_t* buf, const wchar_t* fmt, ...) { @@ -862,6 +788,6 @@ inline int _uix_swprintf(wchar_t* buf, const wchar_t* fmt, ...) { return r; } #define swprintf _uix_swprintf -#endif // !_WIN32 +#endif diff --git a/theseus/desktop/sdl_main.cpp b/theseus/desktop/sdl_main.cpp index a9e6d52..9b23c5d 100644 --- a/theseus/desktop/sdl_main.cpp +++ b/theseus/desktop/sdl_main.cpp @@ -28,9 +28,8 @@ #include "display.h" #include "esde.h" #include "esde_vgames.h" -#include -#include +#include "host.h" #include #include #include @@ -40,9 +39,9 @@ // Loads Data/shaders//.bin. Picks subdir from the active renderer. bgfx::ShaderHandle theseus_bgfx_load_shader(const char* name) { -#if defined(_WIN32) +#if defined(THESEUS_HOST_WIN) const char* sub = "dx11"; -#elif defined(__APPLE__) +#elif defined(THESEUS_HOST_MAC) const char* sub = "metal"; #else const char* sub = "spirv"; @@ -72,94 +71,7 @@ bgfx::ShaderHandle theseus_bgfx_load_shader(const char* name) bgfx::setName(sh, name); return sh; } -#ifdef __APPLE__ -#include // _NSGetExecutablePath -#endif -#ifdef __linux__ -#include -#endif - -#ifdef _WIN32 -#include -#include -#include -#include -#include -#include -#pragma comment(lib, "dbghelp.lib") - -// Watchdog: dumps main thread stack to hang_dump.txt in the user data dir after 10s of detected hang -static DWORD WINAPI HangWatchdog(LPVOID param) { - DWORD mainThread = (DWORD)(uintptr_t)param; - while (true) { - Sleep(2000); - extern volatile bool g_hangCheck; - extern volatile DWORD g_hangStartTime; - if (!g_hangCheck) continue; - if (GetTickCount() - g_hangStartTime < 10000) continue; - - char dumpPath[512]; - snprintf(dumpPath, sizeof(dumpPath), "%s/hang_dump.txt", Plat_UserDataDir()); - FILE* f = fopen(dumpPath, "w"); - if (!f) continue; - fprintf(f, "=== HANG DETECTED (>10s) ===\n"); - fprintf(f, "Main thread ID: %lu\n\n", mainThread); - - HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, mainThread); - if (hThread) { - SuspendThread(hThread); - CONTEXT ctx = {}; - ctx.ContextFlags = CONTEXT_FULL; - GetThreadContext(hThread, &ctx); - - SymInitialize(GetCurrentProcess(), NULL, TRUE); - STACKFRAME64 sf = {}; - sf.AddrPC.Offset = ctx.Rip; sf.AddrPC.Mode = AddrModeFlat; - sf.AddrFrame.Offset = ctx.Rbp; sf.AddrFrame.Mode = AddrModeFlat; - sf.AddrStack.Offset = ctx.Rsp; sf.AddrStack.Mode = AddrModeFlat; - - fprintf(f, "Call stack:\n"); - for (int i = 0; i < 50; i++) { - if (!StackWalk64(IMAGE_FILE_MACHINE_AMD64, GetCurrentProcess(), hThread, &sf, &ctx, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL)) - break; - char symBuf[sizeof(SYMBOL_INFO) + 256]; - SYMBOL_INFO* sym = (SYMBOL_INFO*)symBuf; - sym->SizeOfStruct = sizeof(SYMBOL_INFO); - sym->MaxNameLen = 255; - DWORD64 disp = 0; - if (SymFromAddr(GetCurrentProcess(), sf.AddrPC.Offset, &disp, sym)) - fprintf(f, " [%d] %s + 0x%llx\n", i, sym->Name, (unsigned long long)disp); - else - fprintf(f, " [%d] 0x%llx\n", i, (unsigned long long)sf.AddrPC.Offset); - } - SymCleanup(GetCurrentProcess()); - ResumeThread(hThread); - CloseHandle(hThread); - } - fclose(f); - fprintf(stderr, "[Watchdog] Hang dump written to %s\n", dumpPath); - g_hangCheck = false; - } - return 0; -} -volatile bool g_hangCheck = false; -volatile DWORD g_hangStartTime = 0; -#define strcasecmp _stricmp -#define rmdir _rmdir -#ifndef S_ISDIR -#define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR) -#endif -#ifndef S_ISREG -#define S_ISREG(m) (((m) & _S_IFMT) == _S_IFREG) -#endif -#else #include -#include -#if defined(__APPLE__) || defined(__GLIBC__) -#include -#define HAS_BACKTRACE 1 -#endif -#endif // Dear ImGui #include "imgui.h" @@ -225,11 +137,6 @@ struct CRTDefaults { } g_crtDefaults; static float s_crtTime = 0.0f; -#ifdef _WIN32 -// Force discrete GPU on Optimus / switchable laptops. bgfx exports its own -// pair, so skip under BGFX to avoid the dup-symbol link error. -#endif - // --graphics-debug also leaves a perf trail testers can attach to a report: // one line every 5 seconds in theseus_perf.log in the user data dir. static void WritePerfLogLine() @@ -324,14 +231,6 @@ int g_argc = 0; char** g_argv = NULL; // Game process tracking; hide dashboard while game runs, soft restart on exit -#ifdef _WIN32 -static DWORD s_gamePID = 0; // PID of launched game (0 = none) -#else -#include -#include -extern char **environ; -static pid_t s_gamePID = 0; // PID of launched game (0 = none) -#endif static bool s_gameRunning = false; // true while waiting for game to exit static bool s_softRestartPending = false; // reinit after game exits @@ -1293,65 +1192,6 @@ int g_mediaPlaybackExited = 0; // ReloadSceneFromEditor is now in xap_editor.cpp -static void crash_handler(int sig) { - fprintf(stderr, "\n[CRASH] Signal %d (%s)\n", sig, sig == SIGSEGV ? "SIGSEGV" : sig == SIGABRT ? "SIGABRT" : "OTHER"); - fflush(stderr); -#ifdef HAS_BACKTRACE - void* bt[64]; - int n = backtrace(bt, 64); - backtrace_symbols_fd(bt, n, 2); -#endif -#ifndef _WIN32 - _exit(128 + sig); -#else - exit(128 + sig); -#endif -} - -#ifdef _WIN32 -#include -#include -#pragma comment(lib, "dbghelp.lib") -static LONG WINAPI WindowsCrashHandler(EXCEPTION_POINTERS* ep) { - fprintf(stderr, "\n[CRASH] Exception 0x%08lX at address 0x%p\n", - ep->ExceptionRecord->ExceptionCode, ep->ExceptionRecord->ExceptionAddress); - if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2) { - fprintf(stderr, "[CRASH] %s address 0x%p\n", - ep->ExceptionRecord->ExceptionInformation[0] ? "Writing" : "Reading", - (void*)ep->ExceptionRecord->ExceptionInformation[1]); - } - // Walk the stack - HANDLE process = GetCurrentProcess(); - SymInitialize(process, NULL, TRUE); - STACKFRAME64 frame = {}; - CONTEXT* ctx = ep->ContextRecord; - frame.AddrPC.Offset = ctx->Rip; - frame.AddrPC.Mode = AddrModeFlat; - frame.AddrFrame.Offset = ctx->Rbp; - frame.AddrFrame.Mode = AddrModeFlat; - frame.AddrStack.Offset = ctx->Rsp; - frame.AddrStack.Mode = AddrModeFlat; - fprintf(stderr, "[CRASH] Stack trace:\n"); - for (int i = 0; i < 32; i++) { - if (!StackWalk64(IMAGE_FILE_MACHINE_AMD64, process, GetCurrentThread(), &frame, ctx, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL)) - break; - char symBuf[sizeof(SYMBOL_INFO) + 256]; - SYMBOL_INFO* sym = (SYMBOL_INFO*)symBuf; - sym->SizeOfStruct = sizeof(SYMBOL_INFO); - sym->MaxNameLen = 255; - DWORD64 disp = 0; - if (SymFromAddr(process, frame.AddrPC.Offset, &disp, sym)) - fprintf(stderr, " [%d] %s + 0x%llx\n", i, sym->Name, (unsigned long long)disp); - else - fprintf(stderr, " [%d] 0x%p\n", i, (void*)frame.AddrPC.Offset); - } - fflush(stderr); - return EXCEPTION_EXECUTE_HANDLER; -} -#endif - - - // Inspector panel (DrawSelectionHighlight, RenderNodeTree, RenderInspectorPanel, // etc.) is now in inspector.cpp // RenderXAPEditor is now in xap_editor.cpp @@ -1363,76 +1203,17 @@ int main(int argc, char* argv[]) { g_argv = argv; s_execPath = argv[0]; -#ifdef _WIN32 - // /SUBSYSTEM:WINDOWS so explorer launches don't pop a console, but - // attach to a parent shell when one exists so stdout/stderr show up. - if (AttachConsole(ATTACH_PARENT_PROCESS)) { - freopen("CONOUT$", "w", stdout); - freopen("CONOUT$", "w", stderr); - freopen("CONIN$", "r", stdin); - } else { - // Launched from Explorer, so there's no console and every fprintf - // below goes nowhere. Tee to a log the user can actually send back. + { char logPath[1024]; - Plat_MkdirP(Plat_UserDataDir()); snprintf(logPath, sizeof(logPath), "%s/theseus.log", Plat_UserDataDir()); - if (freopen(logPath, "w", stdout)) { - setvbuf(stdout, NULL, _IOLBF, 0); - _dup2(_fileno(stdout), _fileno(stderr)); - } + Host_ConsoleOrLog(logPath); } + Host_BoostProcess(); - //Milenko-Testing: I don't run windows, i googled this. don't know if it will work as intended. - SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS); - DwmEnableMMCSS(TRUE); - { - DWORD taskIdx = 0; - AvSetMmThreadCharacteristicsW(L"Games", &taskIdx); - } - typedef BOOL (WINAPI *SetProcessInfoFn)(HANDLE, PROCESS_INFORMATION_CLASS, LPVOID, DWORD); - HMODULE hKernel = GetModuleHandleA("kernel32.dll"); - if (hKernel) { - SetProcessInfoFn pSetProcessInformation = - (SetProcessInfoFn)GetProcAddress(hKernel, "SetProcessInformation"); - if (pSetProcessInformation) { - PROCESS_POWER_THROTTLING_STATE pt = {0}; - pt.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION; - pt.ControlMask = PROCESS_POWER_THROTTLING_EXECUTION_SPEED; - pt.StateMask = 0; // 0 == opt out of throttling - pSetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &pt, sizeof(pt)); - } - } -#endif // Change working directory to the executable's directory so that // relative paths (xboxfs/, etc.) work when launched from Finder/Explorer. - { - char exeDir[1024]; -#ifdef _WIN32 - // GetModuleFileName + strip filename - DWORD len = GetModuleFileNameA(NULL, exeDir, sizeof(exeDir)); - if (len > 0) { - char* last = strrchr(exeDir, '\\'); - if (last) *last = '\0'; - _chdir(exeDir); - } -#elif defined(__APPLE__) - uint32_t sz = sizeof(exeDir); - if (_NSGetExecutablePath(exeDir, &sz) == 0) { - char* last = strrchr(exeDir, '/'); - if (last) *last = '\0'; - chdir(exeDir); - } -#else - ssize_t len = readlink("/proc/self/exe", exeDir, sizeof(exeDir) - 1); - if (len > 0) { - exeDir[len] = '\0'; - char* last = strrchr(exeDir, '/'); - if (last) *last = '\0'; - chdir(exeDir); - } -#endif - } + chdir(Plat_ExeDir()); // Before anything reads a config. AppPaths_Init(); @@ -1463,13 +1244,7 @@ int main(int argc, char* argv[]) { #if !defined(ASAN_ACTIVE) // ASan installs its own SIGSEGV/SIGBUS handlers; ours would eat them. // Skip our handler entirely on asan builds. -# ifdef _WIN32 - SetUnhandledExceptionFilter(WindowsCrashHandler); -# else - signal(SIGSEGV, crash_handler); - signal(SIGBUS, crash_handler); -# endif - signal(SIGABRT, crash_handler); + Host_InstallCrashHandler(); #endif #undef ASAN_ACTIVE // Clean up @@ -1530,31 +1305,16 @@ int main(int argc, char* argv[]) { // bgfx::init. nwh = NSWindow* on macOS, HWND on Win32, Window on X11. { - SDL_SysWMinfo wmi; - SDL_VERSION(&wmi.version); - if (!SDL_GetWindowWMInfo(g_pSDLWindow, &wmi)) { - fprintf(stderr, "SDL_GetWindowWMInfo failed: %s\n", SDL_GetError()); - } else { + void* nwh = NULL; + void* ndt = NULL; + bool wayland = false; + if (Host_NativeWindow(g_pSDLWindow, &nwh, &ndt, &wayland)) { bgfx::PlatformData pd; std::memset(&pd, 0, sizeof(pd)); -# if defined(__APPLE__) - pd.nwh = wmi.info.cocoa.window; -# elif defined(_WIN32) - pd.nwh = wmi.info.win.window; -# else - // Branch on SDL's actual subsystem. Feeding x11 lanes to - // bgfx on a Wayland session hands RADV a NULL Display and - // it crashes in vkGetPhysicalDeviceSurfaceSupportKHR. - if (wmi.subsystem == SDL_SYSWM_WAYLAND) { - pd.ndt = wmi.info.wl.display; - pd.nwh = wmi.info.wl.surface; - pd.type = bgfx::NativeWindowHandleType::Wayland; - } else { - pd.ndt = wmi.info.x11.display; - pd.nwh = (void*)(uintptr_t)wmi.info.x11.window; - pd.type = bgfx::NativeWindowHandleType::Default; // X11 - } -# endif + pd.nwh = nwh; + pd.ndt = ndt; + pd.type = wayland ? bgfx::NativeWindowHandleType::Wayland + : bgfx::NativeWindowHandleType::Default; bgfx::Init bgfxInit; bgfxInit.platformData = pd; // Pixels, not points. On a HiDPI display the drawable is larger @@ -2161,13 +1921,9 @@ int main(int argc, char* argv[]) { newArgv[g_argc + 1] = NULL; } -#ifdef _WIN32 // Show window again if it was hidden (game launch) if (g_pSDLWindow) SDL_ShowWindow(g_pSDLWindow); - _execv(g_argv[0], newArgv); -#else execv(g_argv[0], newArgv); -#endif // fallthrough if execv fails fprintf(stderr, "[Desktop] execv failed: %s\n", strerror(errno)); } diff --git a/theseus/desktop/skin_editor.cpp b/theseus/desktop/skin_editor.cpp index 89592db..4fdd16d 100644 --- a/theseus/desktop/skin_editor.cpp +++ b/theseus/desktop/skin_editor.cpp @@ -8,6 +8,7 @@ #include "skin_assets.h" #include "skin_editor.h" #include "imgui.h" +#include "host.h" #include #include @@ -27,45 +28,10 @@ extern void ReloadSkin(); static bool IsStockReadOnly(); // Native OS file picker. Returns empty string on cancel / unsupported. -#if defined(_WIN32) -# include -# include -#endif - static std::string PickFile() { -#if defined(__APPLE__) - FILE* p = popen( - "osascript -e 'POSIX path of (choose file with prompt \"Select a replacement asset\")' 2>/dev/null", - "r"); - if (!p) return ""; - char buf[2048]; std::string out; - while (fgets(buf, sizeof(buf), p)) out += buf; - pclose(p); - while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) - out.pop_back(); - return out; -#elif defined(_WIN32) - OPENFILENAMEA ofn = {}; - char path[MAX_PATH] = {}; - ofn.lStructSize = sizeof(ofn); - ofn.lpstrFile = path; - ofn.nMaxFile = MAX_PATH; - ofn.lpstrTitle = "Select a replacement asset"; - ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR; - if (GetOpenFileNameA(&ofn)) return std::string(path); - return ""; -#elif defined(__linux__) - FILE* p = popen("zenity --file-selection --title=\"Select a replacement asset\" 2>/dev/null", "r"); - if (!p) return ""; - char buf[2048]; std::string out; - while (fgets(buf, sizeof(buf), p)) out += buf; - pclose(p); - while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) - out.pop_back(); - return out; -#else - return ""; -#endif + char path[2048]; + if (!Host_PickFile("Select a replacement asset", path, sizeof(path))) return ""; + return std::string(path); } // Copy the chosen file into the current skin folder as `slotName`. Backs diff --git a/theseus/desktop/std.h b/theseus/desktop/std.h index 069b589..e2c0b51 100755 --- a/theseus/desktop/std.h +++ b/theseus/desktop/std.h @@ -20,17 +20,9 @@ #include #include -// MSVC: DWORD is 'unsigned long', uint32_t is 'unsigned int'. Both are 32-bit -// but MSVC treats them as incompatible pointer types. Win32 APIs (ReadFile, -// WriteFile, CreateThread, etc.) expect DWORD*. This cast macro bridges the gap -// at API boundaries so src/ files can use uint32_t consistently. -// Both MSVC and mingw GCC treat DWORD/uint32_t as distinct types when -// they target Windows headers, so the cast is needed for any _WIN32 build. -#ifdef _WIN32 -#define LPDW(p) reinterpret_cast(p) -#else +// DWORD is uint32_t on every desktop host, so uint32_t* passes straight +// through to ReadFile and friends. #define LPDW(p) (p) -#endif @@ -103,7 +95,6 @@ EXTERN_C void Alert(const char* szMsg, ...); // Types not provided by platform_shim.h: GDI/RGB color helpers and // the device-notify stub used by Win32 device hot-plug callbacks. -#ifndef _WIN32 typedef void* HDEVNOTIFY; typedef struct tagRGBQUAD { @@ -118,12 +109,6 @@ typedef RGBQUAD* LPRGBQUAD; // GetCurrentThreadId (platform_shim.h doesn't provide this one) inline DWORD GetCurrentThreadId() { return (DWORD)SDL_ThreadID(); } -#else -// Windows: RGBQUAD comes from windows.h, but LPRGBQUAD isn't always defined -#ifndef LPRGBQUAD -typedef RGBQUAD* LPRGBQUAD; -#endif -#endif // !_WIN32 // D3D vertex types #include "vertex8.h" diff --git a/theseus/desktop/xbox_file.cpp b/theseus/desktop/xbox_file.cpp index 952c85d..16d5537 100644 --- a/theseus/desktop/xbox_file.cpp +++ b/theseus/desktop/xbox_file.cpp @@ -41,12 +41,6 @@ static void MakeParents(const char* hostPath) { Host_MakeDir(dir); } -#ifdef THESEUS_HOST_WIN -static void ToBackslash(char* s) { - for (; *s; s++) if (*s == '/') *s = '\\'; -} -#endif - static DWORD ToFileAttributes(uint32_t attrs) { DWORD a = 0; if (attrs & HOST_ATTR_DIR) a |= FILE_ATTRIBUTE_DIRECTORY; @@ -114,27 +108,12 @@ static HANDLE OpenVirtualXBE(const char* hostPath) { size_t n = FillFakeXBE(buf, sizeof(buf), vg.name, tid); if (!n) return INVALID_HANDLE_VALUE; -#ifdef THESEUS_HOST_WIN - // Real HANDLEs and FILE*s aren't interchangeable here, so back it with a - // temp file the OS deletes on close. - char tempDir[MAX_PATH], tempFile[MAX_PATH]; - DWORD len = GetTempPathA(MAX_PATH, tempDir); - if (len == 0 || len > MAX_PATH) return INVALID_HANDLE_VALUE; - if (GetTempFileNameA(tempDir, "xbe", 0, tempFile) == 0) return INVALID_HANDLE_VALUE; - HANDLE h = CreateFileA(tempFile, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, - CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, NULL); - if (h == INVALID_HANDLE_VALUE) return INVALID_HANDLE_VALUE; - DWORD written = 0; - WriteFile(h, buf, (DWORD)n, &written, NULL); - SetFilePointer(h, 0, NULL, FILE_BEGIN); -#else - // POSIX handles are FILE*, so an anonymous tmpfile does it. - FILE* f = tmpfile(); + // File handles are FILE*s, so a temp file gone on close does it. + FILE* f = Host_TempFile(); if (!f) return INVALID_HANDLE_VALUE; fwrite(buf, 1, n, f); fseek(f, 0, SEEK_SET); HANDLE h = (HANDLE)f; -#endif fprintf(stderr, "[xboxfs] Virtual XBE for '%s' (TID=%08lX)\n", vg.name, tid); return h; } @@ -156,15 +135,6 @@ HANDLE TheseusCreateFile(const TCHAR* szFileName, DWORD access, DWORD share, else XboxPath_Translate(xboxPath, host, sizeof(host)); if (!host[0]) return INVALID_HANDLE_VALUE; -#ifdef THESEUS_HOST_WIN - ToBackslash(host); - // Xbox flags desktop Windows can't honour: NO_BUFFERING wants sector - // aligned reads, OVERLAPPED wants async setup. - flags &= ~(FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED); - HANDLE h = CreateFileA(host, access, share, sa, disp, flags, templ); - if (h != INVALID_HANDLE_VALUE) return h; - return writes ? INVALID_HANDLE_VALUE : OpenVirtualXBE(host); -#else (void)share; (void)sa; (void)flags; (void)templ; const char* mode = "rb"; if (access & GENERIC_WRITE) mode = (disp == CREATE_ALWAYS) ? "wb" : "r+b"; @@ -187,7 +157,6 @@ HANDLE TheseusCreateFile(const TCHAR* szFileName, DWORD access, DWORD share, } } return INVALID_HANDLE_VALUE; -#endif } bool TheseusCreateDirectory(LPCTSTR lpPathName, LPSECURITY_ATTRIBUTES) diff --git a/theseus/shared/platform.h b/theseus/shared/platform.h index 538ea8b..7b426ee 100644 --- a/theseus/shared/platform.h +++ b/theseus/shared/platform.h @@ -19,6 +19,8 @@ # endif # if defined(_WIN32) # define THESEUS_HOST_WIN 1 + // mingw links the Microsoft C runtime: _stricmp and friends are real. +# define THESEUS_CRT_MS 1 # elif defined(__APPLE__) # define THESEUS_HOST_MAC 1 # elif defined(__linux__) From b9b96dc7769f297040656100fd33bcf0b2310eb4 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:06:03 -0500 Subject: [PATCH 035/167] Kept windows.h inside the host layer and taught lint to check The registry read, exe path, folder opener and CD TOC moved into the host files, and the folder scans in app_paths, retroarch and hdd_browser now use host.h. lint-platform fails on windows.h outside host_win, on _WIN32 outside platform.h and the host files, and on host tests in Xbox-shaped code. --- build/Makefile | 38 ++++++- theseus/desktop/app_paths.cpp | 132 ++++++------------------ theseus/desktop/audio_sdl.cpp | 7 +- theseus/desktop/cdaudio.cpp | 129 ++--------------------- theseus/desktop/client_util.h | 15 +-- theseus/desktop/esde.cpp | 5 +- theseus/desktop/hdd_browser.cpp | 58 +++-------- theseus/desktop/host.h | 9 ++ theseus/desktop/host_posix_app.cpp | 36 +++++++ theseus/desktop/host_win_app.cpp | 62 +++++++++++ theseus/desktop/icon_compress.cpp | 5 +- theseus/desktop/image.cpp | 2 +- theseus/desktop/imgui_impl_bgfx.cpp | 3 +- theseus/desktop/launchers/retroarch.cpp | 123 ++++++++-------------- theseus/desktop/launchers/steam.cpp | 25 ++--- theseus/desktop/menu_bar.cpp | 2 +- theseus/desktop/path_ci.h | 11 +- theseus/desktop/path_template.cpp | 3 +- theseus/desktop/platform_env.h | 9 +- theseus/desktop/plex_client.cpp | 3 +- theseus/desktop/preloader.cpp | 8 +- theseus/desktop/title_maker.cpp | 14 +-- theseus/desktop/tmdb.cpp | 10 +- theseus/desktop/udata_synth.cpp | 9 +- theseus/desktop/xap_editor.cpp | 4 +- theseus/desktop/xboxfs_drive.h | 14 +-- 26 files changed, 294 insertions(+), 442 deletions(-) diff --git a/build/Makefile b/build/Makefile index 1fb1ef5..e16c148 100644 --- a/build/Makefile +++ b/build/Makefile @@ -454,7 +454,21 @@ clean: # Platform flags live in shared/platform.h. The old desktop spellings stay # retired, and code the Xbox build compiles never reads _WIN32, which is -# true on the Xbox too. +# true on the Xbox too. On desktop only the host layer (desktop/host_*) +# talks to the OS by name: is host_win* only, _WIN32 is +# platform.h and host_* only, and the Xbox-shaped core never asks which +# host it's on. Vendored headers (stb, minimp3, imfilebrowser, imgui) are +# left alone. +LINT_DIRS = $(THESEUS_SRC)/desktop $(THESEUS_SRC)/shared $(THESEUS_SRC)/engine \ + $(THESEUS_SRC)/render $(THESEUS_SRC)/toolbox +LINT_VENDOR = --exclude-dir=third-party --exclude-dir=xdk --exclude-dir=imgui \ + --exclude='stb_*.h' --exclude='minimp3.h' --exclude='imfilebrowser.h' +LINT_SHAPED = $(THESEUS_SRC)/shared $(THESEUS_SRC)/engine $(THESEUS_SRC)/render \ + $(THESEUS_SRC)/toolbox $(THESEUS_SRC)/desktop/platform_shim.h \ + $(THESEUS_SRC)/desktop/std.h $(THESEUS_SRC)/desktop/d3d8_sdl.h \ + $(THESEUS_SRC)/desktop/xbox_file.cpp $(THESEUS_SRC)/desktop/xbox_path.h \ + $(THESEUS_SRC)/desktop/xboxfs_drive.h $(THESEUS_SRC)/desktop/host.h +LINT_HOSTTEST = THESEUS_HOST_(WIN|MAC|LINUX) .PHONY: lint-platform lint-platform: @bad=0; \ @@ -463,11 +477,25 @@ lint-platform: --exclude-dir=third-party --exclude-dir=xdk --exclude-dir=imgui; then \ echo "lint-platform: retired flag above. Use _XBOX or THESEUS_DESKTOP."; bad=1; \ fi; \ - if grep -rnw '_WIN32' $(THESEUS_SRC)/shared $(THESEUS_SRC)/engine $(THESEUS_SRC)/render \ - --include='*.h' --include='*.cpp' --include='*.c' \ - --exclude='stb_*.h' --exclude='platform.h'; then \ - echo "lint-platform: _WIN32 above in Xbox-built code. Use THESEUS_HOST_WIN."; bad=1; \ + if grep -rnE '^[[:space:]]*#[[:space:]]*include[[:space:]]*<(windows|winsock2|ws2tcpip|shellapi|commdlg|shlobj|dbghelp)[.]h>' \ + $(LINT_DIRS) --include='*.h' --include='*.cpp' --include='*.c' $(LINT_VENDOR) \ + | grep -v '/desktop/host_win[^/]*[.]cpp:'; then \ + echo "lint-platform: Windows header above. Only desktop/host_win*.cpp may include it."; bad=1; \ + fi; \ + if grep -rnw '_WIN32' $(LINT_DIRS) --include='*.h' --include='*.cpp' --include='*.c' \ + $(LINT_VENDOR) --exclude='platform.h' --exclude='host_*'; then \ + echo "lint-platform: _WIN32 above. It's true on the Xbox too; use THESEUS_HOST_WIN or host.h."; bad=1; \ + fi; \ + if grep -rnE '$(LINT_HOSTTEST)' $(LINT_SHAPED) \ + --include='*.h' --include='*.cpp' --include='*.c' $(LINT_VENDOR) --exclude='platform.h'; then \ + echo "lint-platform: host test above in Xbox-shaped code. Go through host.h."; bad=1; \ fi; \ + for f in $$(grep -rlE '$(LINT_HOSTTEST)' $(THESEUS_SRC)/desktop \ + --include='*.h' --include='*.cpp' $(LINT_VENDOR) --exclude='host_*'); do \ + awk '/^[ \t]*#[ \t]*include[ \t]*"(platform|std)[.]h"/ { ok = 1 } \ + /$(LINT_HOSTTEST)/ { if (!ok) { print FILENAME ":" FNR ": host test before platform.h"; exit 1 } exit 0 }' $$f \ + || { echo "lint-platform: include platform.h before testing THESEUS_HOST_*."; bad=1; }; \ + done; \ [ $$bad -eq 0 ] && echo "lint-platform: clean"; exit $$bad # ============================================================ diff --git a/theseus/desktop/app_paths.cpp b/theseus/desktop/app_paths.cpp index dc4f0bb..041e6e0 100644 --- a/theseus/desktop/app_paths.cpp +++ b/theseus/desktop/app_paths.cpp @@ -9,16 +9,8 @@ #include #include -#ifdef _WIN32 -#include -#include -#else -#include -#include -#endif -#ifdef __APPLE__ -#include -#endif +#include "platform.h" +#include "host.h" // The only thing that stays with the binary. Nobody hand edits a .bin, and // packagers want it read only. @@ -30,26 +22,14 @@ const char* Plat_ExeDir() { if (s_init) return s_dir; s_init = true; -#ifdef _WIN32 - DWORD len = GetModuleFileNameA(NULL, s_dir, sizeof(s_dir)); - if (len > 0) { - char* last = strrchr(s_dir, '\\'); - if (last) *last = '\0'; - } -#elif defined(__APPLE__) - uint32_t sz = sizeof(s_dir); - if (_NSGetExecutablePath(s_dir, &sz) == 0) { + if (Host_ExePath(s_dir, sizeof(s_dir))) { char* last = strrchr(s_dir, '/'); + char* back = strrchr(s_dir, '\\'); + if (back > last) last = back; if (last) *last = '\0'; + } else { + s_dir[0] = '\0'; } -#else - ssize_t len = readlink("/proc/self/exe", s_dir, sizeof(s_dir) - 1); - if (len > 0) { - s_dir[len] = '\0'; - char* last = strrchr(s_dir, '/'); - if (last) *last = '\0'; - } -#endif if (!s_dir[0]) strcpy(s_dir, "."); return s_dir; } @@ -60,13 +40,13 @@ const char* Plat_UserDataDir() { if (s_init) return s_dir; s_init = true; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN const char* appdata = getenv("APPDATA"); if (appdata && *appdata) snprintf(s_dir, sizeof(s_dir), "%s\\Theseus", appdata); else snprintf(s_dir, sizeof(s_dir), "%s\\AppData\\Roaming\\Theseus", PlatformHomeDir()); -#elif defined(__APPLE__) +#elif defined(THESEUS_HOST_MAC) snprintf(s_dir, sizeof(s_dir), "%s/Library/Application Support/Theseus", PlatformHomeDir()); #else const char* xdg = getenv("XDG_DATA_HOME"); @@ -79,7 +59,7 @@ const char* Plat_UserDataDir() { } const char* Plat_ShippedDir() { -#ifdef __APPLE__ +#ifdef THESEUS_HOST_MAC // Inside a .app the binary sits in Contents/MacOS and the payload in // Contents/Resources. Loose builds keep everything beside the binary. static char s_dir[1024] = {0}; @@ -130,36 +110,19 @@ static void CopyTree(const char* src, const char* dst) { struct stat st; if (stat(src, &st) != 0) return; Plat_MkdirP(dst); -#ifdef _WIN32 - char pattern[1024]; - snprintf(pattern, sizeof(pattern), "%s/*", src); - struct _finddata_t fd; - intptr_t h = _findfirst(pattern, &fd); - if (h == -1) return; - do { - if (!strcmp(fd.name, ".") || !strcmp(fd.name, "..")) continue; - char s[1024], d[1024]; - snprintf(s, sizeof(s), "%s/%s", src, fd.name); - snprintf(d, sizeof(d), "%s/%s", dst, fd.name); - if (fd.attrib & _A_SUBDIR) CopyTree(s, d); - else CopyFile(s, d); - } while (_findnext(h, &fd) == 0); - _findclose(h); -#else - DIR* dir = opendir(src); + HostDir* dir = Host_OpenDir(src); if (!dir) return; - struct dirent* e; - while ((e = readdir(dir)) != NULL) { - if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; + char name[260]; + HostFileInfo info; + while (Host_ReadDir(dir, name, sizeof(name), &info)) { + if (!strcmp(name, ".") || !strcmp(name, "..")) continue; char s[1024], d[1024]; - snprintf(s, sizeof(s), "%s/%s", src, e->d_name); - snprintf(d, sizeof(d), "%s/%s", dst, e->d_name); - struct stat es; - if (stat(s, &es) == 0 && S_ISDIR(es.st_mode)) CopyTree(s, d); - else CopyFile(s, d); + snprintf(s, sizeof(s), "%s/%s", src, name); + snprintf(d, sizeof(d), "%s/%s", dst, name); + if (info.attrs & HOST_ATTR_DIR) CopyTree(s, d); + else CopyFile(s, d); } - closedir(dir); -#endif + Host_CloseDir(dir); } // Copy one tree into the user dir if it isn't there yet. @@ -190,38 +153,19 @@ void AppPaths_Init() { snprintf(dataDir, sizeof(dataDir), "%s/Data", Plat_ShippedDir()); Plat_MkdirP(AppPath_Tree("Data")); -#ifdef _WIN32 - char pattern[1024]; - snprintf(pattern, sizeof(pattern), "%s/*", dataDir); - struct _finddata_t fd; - intptr_t h = _findfirst(pattern, &fd); - if (h == -1) return; - do { - if (!(fd.attrib & _A_SUBDIR)) continue; - if (!strcmp(fd.name, ".") || !strcmp(fd.name, "..")) continue; - if (!strcasecmp(fd.name, "shaders")) continue; - char rel[1024]; - snprintf(rel, sizeof(rel), "Data/%s", fd.name); - SeedTree(rel); - } while (_findnext(h, &fd) == 0); - _findclose(h); -#else - DIR* dir = opendir(dataDir); + HostDir* dir = Host_OpenDir(dataDir); if (!dir) return; - struct dirent* e; - while ((e = readdir(dir)) != NULL) { - if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; - if (!strcasecmp(e->d_name, "shaders")) continue; - char probe[1024]; - snprintf(probe, sizeof(probe), "%s/%s", dataDir, e->d_name); - struct stat es; - if (stat(probe, &es) != 0 || !S_ISDIR(es.st_mode)) continue; + char name[260]; + HostFileInfo info; + while (Host_ReadDir(dir, name, sizeof(name), &info)) { + if (!(info.attrs & HOST_ATTR_DIR)) continue; + if (!strcmp(name, ".") || !strcmp(name, "..")) continue; + if (!strcasecmp(name, "shaders")) continue; char rel[1024]; - snprintf(rel, sizeof(rel), "Data/%s", e->d_name); + snprintf(rel, sizeof(rel), "Data/%s", name); SeedTree(rel); } - closedir(dir); -#endif + Host_CloseDir(dir); } const char* AppPathf(const char* fmt, ...) { @@ -251,23 +195,7 @@ const char* AppPath_Tree(const char* tree) { void Plat_OpenInFileManager(const char* dir) { if (!dir || !*dir) return; -#ifdef _WIN32 - ShellExecuteA(NULL, "open", dir, NULL, NULL, SW_SHOWNORMAL); -#else - // Fork so a slow file manager can't stall us, and no shell is involved. - pid_t pid = fork(); - if (pid == 0) { -#ifdef __APPLE__ - execlp("open", "open", dir, (char*)NULL); -#else - // Under flatpak we want the host's file manager. - if (getenv("FLATPAK_ID")) - execlp("flatpak-spawn", "flatpak-spawn", "--host", "xdg-open", dir, (char*)NULL); - execlp("xdg-open", "xdg-open", dir, (char*)NULL); -#endif - _exit(127); - } -#endif + Host_OpenFolder(dir); } const char* AppPath(const char* logical) { diff --git a/theseus/desktop/audio_sdl.cpp b/theseus/desktop/audio_sdl.cpp index 19b4d09..38af4cf 100644 --- a/theseus/desktop/audio_sdl.cpp +++ b/theseus/desktop/audio_sdl.cpp @@ -3,6 +3,7 @@ // MP3, WAV, and XAP-script-driven audio cues. Intentionally does NOT // include std.h, to avoid #define new conflicts with STL. +#include "platform.h" #include "app_paths.h" #include #include @@ -14,7 +15,7 @@ #include #include #include -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN #include #include #ifndef S_ISDIR @@ -1092,7 +1093,7 @@ int DashMusic_Scan(const char* musicRoot) soundtrack.songs.push_back(BuildSong(full, StripExtension(songName))); }; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN char searchBuf[512]; snprintf(searchBuf, sizeof(searchBuf), "%s\\*", stPath.c_str()); struct _finddata_t fd; @@ -1162,7 +1163,7 @@ int DashMusic_Scan(const char* musicRoot) looseMusic.songs.push_back(BuildSong(full, StripExtension(fileName))); }; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN char searchBuf[512]; snprintf(searchBuf, sizeof(searchBuf), "%s\\*", musicRoot); struct _finddata_t fd; diff --git a/theseus/desktop/cdaudio.cpp b/theseus/desktop/cdaudio.cpp index 47a5be2..2188f2a 100644 --- a/theseus/desktop/cdaudio.cpp +++ b/theseus/desktop/cdaudio.cpp @@ -2,17 +2,17 @@ // // TOC detection: platform ioctls, no extra deps. // Linux : CDROMREADTOCHDR / CDROMREADTOCENTRY -// Windows: DeviceIoControl IOCTL_CDROM_READ_TOC +// Windows: IOCTL_CDROM_READ_TOC, in host_win_app.cpp // // Playback backends (first match wins): // UIX_CDAUDIO : libmpv audio-only handle -// Linux or Windows : CDROMREADAUDIO / IOCTL_CDROM_RAW_READ + SDL audio +// Linux : CDROMREADAUDIO + SDL audio // Anything else : stubs (detection still works) // -// The SDL ring-buffer backend is shared between Linux and Windows; -// only the reader-thread ioctl differs. #include "cdaudio.h" +#include "platform.h" +#include "host.h" #include #include #include @@ -97,83 +97,15 @@ static CdDiscType PlatformReadTOC(int* outCount, int* outDurations, int maxTrack static void PlatformPopulatePlaybackInfo() {} // already done in PlatformReadTOC -#elif defined(_WIN32) - -#define WIN32_LEAN_AND_MEAN -#include -#include -#include - -// RAW_READ_INFO / IOCTL_CDROM_RAW_READ: defined locally so we don't depend -// on mingw-w64 version shipping them. -struct CdRawReadInfo { - LARGE_INTEGER DiskOffset; // LBA * 2048 - ULONG SectorCount; - ULONG TrackMode; // 2 = CDDA -}; -#ifndef IOCTL_CDROM_RAW_READ -// CTL_CODE(0x2, 0xF, METHOD_OUT_DIRECT=2, FILE_READ_ACCESS=1) -#define IOCTL_CDROM_RAW_READ 0x0002403E -#endif - -static char s_cdDriveLetter = 0; -static int s_trackStartLBA[MAX_CDTRACKS] = {}; -static int s_leadOutLBA = 0; - -static HANDLE OpenCdromDevice() -{ - char path[] = "\\\\.\\D:"; - for (char c = 'D'; c <= 'H'; ++c) { - char root[4] = { c, ':', '\\', 0 }; - if (GetDriveTypeA(root) != DRIVE_CDROM) continue; - path[4] = c; - HANDLE h = CreateFileA(path, GENERIC_READ, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, OPEN_EXISTING, 0, nullptr); - if (h != INVALID_HANDLE_VALUE) { - s_cdDriveLetter = c; - return h; - } - } - return INVALID_HANDLE_VALUE; -} +#elif defined(THESEUS_HOST_WIN) static CdDiscType PlatformReadTOC(int* outCount, int* outDurations, int maxTracks) { if (outCount) *outCount = 0; - HANDLE h = OpenCdromDevice(); - if (h == INVALID_HANDLE_VALUE) return CD_NONE; - - CDROM_TOC toc; - DWORD br = 0; - BOOL ok = DeviceIoControl(h, IOCTL_CDROM_READ_TOC, - nullptr, 0, &toc, sizeof(toc), &br, nullptr); - CloseHandle(h); - if (!ok) return CD_NONE; - - int count = toc.LastTrack - toc.FirstTrack + 1; - if (outCount) *outCount = count; - bool allAudio = true; - for (int i = 0; i <= count; ++i) { // +1 to include lead-out - int ti = toc.FirstTrack - 1 + i; // index into TrackData - UCHAR* a = toc.TrackData[ti].Address; - int lba = ((int)a[1] * 60 + a[2]) * 75 + a[3] - 150; - if (lba < 0) lba = 0; - - if (i < count) { - if (toc.TrackData[ti].Control & 4) allAudio = false; - s_trackStartLBA[i] = lba; - if (outDurations && i + 1 <= count) { - UCHAR* b = toc.TrackData[ti + 1].Address; - int t0 = (int)a[1] * 60 + a[2]; - int t1 = (int)b[1] * 60 + b[2]; - outDurations[i] = (t1 > t0) ? (t1 - t0) : 0; - } - } else { - s_leadOutLBA = lba; - } - } + int count = Host_CdReadToc(outDurations, maxTracks, &allAudio); + if (count < 0) return CD_NONE; + if (outCount) *outCount = count; return allAudio ? CD_AUDIO : CD_DATA; } @@ -264,8 +196,8 @@ void CdAudio_Update() } } -// ---- B: SDL ring-buffer + platform raw-sector read (Linux + Windows) ------- -#elif defined(__linux__) || defined(_WIN32) +// ---- B: SDL ring-buffer + raw-sector read (Linux) ------------------------- +#elif defined(__linux__) #include @@ -350,47 +282,6 @@ static int SDLCALL ReaderFunc(void* arg) } close(fd); -#elif defined(_WIN32) - char devPath[16]; - snprintf(devPath, sizeof(devPath), "\\\\.\\%c:", s_cdDriveLetter ? s_cdDriveLetter : 'D'); - HANDLE hDev = CreateFileA(devPath, GENERIC_READ, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, OPEN_EXISTING, 0, nullptr); - if (hDev == INVALID_HANDLE_VALUE) { - fprintf(stderr, "[CdAudio] cannot open %s: error %lu\n", devPath, GetLastError()); - SDL_AtomicSet(&s_atomPlaying, 0); - return -1; - } - - while (!SDL_AtomicGet(&s_atomStop) && lba < ra.endLBA) { - if (SDL_AtomicGet(&s_atomPaused)) { SDL_Delay(20); continue; } - int n = CDDA_READ_BATCH; - if (lba + n > ra.endLBA) n = ra.endLBA - lba; - - CdRawReadInfo rri; - rri.DiskOffset.QuadPart = (LONGLONG)lba * 2048; - rri.SectorCount = n; - rri.TrackMode = 2; // CDDA - - DWORD br = 0; - if (!DeviceIoControl(hDev, IOCTL_CDROM_RAW_READ, - &rri, sizeof(rri), buf, n * CDDA_FRAME_BYTES, &br, nullptr)) { - fprintf(stderr, "[CdAudio] IOCTL_CDROM_RAW_READ at LBA %d: error %lu\n", lba, GetLastError()); - readOk = false; break; - } - - int bytes = n * CDDA_FRAME_BYTES; - SDL_LockMutex(s_mutex); - while (!SDL_AtomicGet(&s_atomStop) && (CDDA_RING_BYTES - s_ringFill) < bytes) - SDL_CondWait(s_cond, s_mutex); - if (!SDL_AtomicGet(&s_atomStop)) { - for (int i = 0; i < bytes; i++) { s_ring[s_ringWrite]=buf[i]; s_ringWrite=(s_ringWrite+1)%CDDA_RING_BYTES; } - s_ringFill += bytes; - } - SDL_UnlockMutex(s_mutex); - lba += n; - } - CloseHandle(hDev); #endif // platform read // ------------------------------------------------ diff --git a/theseus/desktop/client_util.h b/theseus/desktop/client_util.h index fa8c03d..2d5097e 100644 --- a/theseus/desktop/client_util.h +++ b/theseus/desktop/client_util.h @@ -7,22 +7,13 @@ #include #include #include -#ifdef _WIN32 -#include -#endif +#include "host.h" // Join a worker thread if it is running. inline void JoinIf(std::thread& t) { if (t.joinable()) t.join(); } -// Create a directory. POSIX mkdir takes (path, mode); the Windows CRT one -// takes just (path). -inline int Mkdir_(const char* path) { -#ifdef _WIN32 - return mkdir(path); -#else - return mkdir(path, 0755); -#endif -} +// Create a directory. 0 when it exists afterwards. +inline int Mkdir_(const char* path) { return Host_MakeDir(path) ? 0 : -1; } // Random RFC 4122 version-4 UUID as a 36-char string plus NUL. inline void GenerateUUID(char out[37]) { diff --git a/theseus/desktop/esde.cpp b/theseus/desktop/esde.cpp index ccb6a1f..dd423f7 100644 --- a/theseus/desktop/esde.cpp +++ b/theseus/desktop/esde.cpp @@ -1,3 +1,4 @@ +#include "platform.h" #include "esde.h" #include "platform_env.h" #include "app_paths.h" @@ -6,7 +7,7 @@ #include #include #include -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN #include #define strcasecmp _stricmp #else @@ -196,7 +197,7 @@ bool Esde_LastScanFoundSystemsXml(void) template static void ForEachEntry(const char* dir, F cb) { -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN char pattern[1024]; snprintf(pattern, sizeof(pattern), "%s/*", dir); struct _finddata_t fd; diff --git a/theseus/desktop/hdd_browser.cpp b/theseus/desktop/hdd_browser.cpp index 096a16e..e58e513 100644 --- a/theseus/desktop/hdd_browser.cpp +++ b/theseus/desktop/hdd_browser.cpp @@ -16,26 +16,13 @@ #include #include -#ifndef _WIN32 -#include +#include "platform.h" +#include "host.h" #include +#include +#ifndef THESEUS_HOST_WIN #include #include -#else -#include -#include -#include -#include -#include -// windows.h #defines CreateFile/CreateDirectory as A/W macros, which -// collides with FATXReader::CreateFile / FATXReader::CreateDirectory. -// Drop the macros so the class methods compile under their real names. -#ifdef CreateFile -#undef CreateFile -#endif -#ifdef CreateDirectory -#undef CreateDirectory -#endif #endif bool g_hddBrowserOpen = false; @@ -112,7 +99,7 @@ static const char* GetDefaultQcow2Path() { const char* home = getenv("HOME"); if (home) snprintf(path, sizeof(path), "%s/.local/share/xemu/xemu/xbox_hdd.qcow2", home); -#elif defined(_WIN32) +#elif defined(THESEUS_HOST_WIN) char* appdata = getenv("LOCALAPPDATA"); if (appdata) snprintf(path, sizeof(path), "%s\\xemu\\xemu\\xbox_hdd.qcow2", appdata); @@ -402,37 +389,18 @@ static int ImportFolderRecursive(FATXReader* part, uint32_t fatxDirCluster, } }; -#ifdef _WIN32 - char searchBuf[1024]; - snprintf(searchBuf, sizeof(searchBuf), "%s\\*", hostPath); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind == -1) { - fprintf(stderr, "[Import] _findfirst failed: '%s'\n", hostPath); - return 0; - } - do { - bool isDir = (fd.attrib & _A_SUBDIR) != 0; - bool isFile = !isDir; - ProcessEntry(fd.name, isDir, isFile); - } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); -#else - DIR* dir = opendir(hostPath); + HostDir* dir = Host_OpenDir(hostPath); if (!dir) { - fprintf(stderr, "[Import] opendir failed: '%s' (errno %d)\n", hostPath, errno); + fprintf(stderr, "[Import] cannot open folder: '%s'\n", hostPath); return 0; } - struct dirent* ent; - while ((ent = readdir(dir)) != nullptr) { - char fullPath[1024]; - snprintf(fullPath, sizeof(fullPath), "%s/%s", hostPath, ent->d_name); - struct stat st; - if (stat(fullPath, &st) != 0) continue; - ProcessEntry(ent->d_name, S_ISDIR(st.st_mode), S_ISREG(st.st_mode)); + char name[260]; + HostFileInfo info; + while (Host_ReadDir(dir, name, sizeof(name), &info)) { + bool isDir = (info.attrs & HOST_ATTR_DIR) != 0; + ProcessEntry(name, isDir, !isDir); } - closedir(dir); -#endif + Host_CloseDir(dir); return imported; } diff --git a/theseus/desktop/host.h b/theseus/desktop/host.h index d7d36c1..6d46fb2 100644 --- a/theseus/desktop/host.h +++ b/theseus/desktop/host.h @@ -70,3 +70,12 @@ bool Host_Spawn(const char* spec, bool isUrl, bool isSteamUrl, const char* workd char* err, size_t errSize); // Native "open file" dialog. False on cancel or when there isn't one. bool Host_PickFile(const char* title, char* out, size_t outSize); +// Full path of the running executable. +bool Host_ExePath(char* out, size_t outSize); +// Show a folder in the OS file manager. Doesn't wait. +void Host_OpenFolder(const char* dir); +// A string value under HKEY_CURRENT_USER. Always false off Windows. +bool Host_RegistryString(const char* subKey, const char* value, char* out, size_t outSize); +// Audio CD in the first optical drive: track count and per-track lengths +// in seconds. -1 when there's no readable disc. +int Host_CdReadToc(int* durations, int maxTracks, bool* allAudio); diff --git a/theseus/desktop/host_posix_app.cpp b/theseus/desktop/host_posix_app.cpp index ce9a506..7fb2768 100644 --- a/theseus/desktop/host_posix_app.cpp +++ b/theseus/desktop/host_posix_app.cpp @@ -10,6 +10,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif #if defined(__APPLE__) || defined(__GLIBC__) #include #define HAS_BACKTRACE 1 @@ -135,3 +138,36 @@ bool Host_PickFile(const char* title, char* out, size_t outSize) { while (n && (out[n - 1] == '\n' || out[n - 1] == '\r')) out[--n] = '\0'; return n > 0; } + +bool Host_ExePath(char* out, size_t outSize) { +#ifdef __APPLE__ + uint32_t sz = (uint32_t)outSize; + return _NSGetExecutablePath(out, &sz) == 0; +#else + ssize_t len = readlink("/proc/self/exe", out, outSize - 1); + if (len <= 0) return false; + out[len] = '\0'; + return true; +#endif +} + +void Host_OpenFolder(const char* dir) { + // Fork so a slow file manager can't stall us, and no shell is involved. + pid_t pid = fork(); + if (pid == 0) { +#ifdef __APPLE__ + execlp("open", "open", dir, (char*)NULL); +#else + // Under flatpak we want the host's file manager. + if (getenv("FLATPAK_ID")) + execlp("flatpak-spawn", "flatpak-spawn", "--host", "xdg-open", dir, (char*)NULL); + execlp("xdg-open", "xdg-open", dir, (char*)NULL); +#endif + _exit(127); + } +} + +bool Host_RegistryString(const char*, const char*, char*, size_t) { return false; } + +// Linux reads its TOC in cdaudio.cpp. +int Host_CdReadToc(int*, int, bool*) { return -1; } diff --git a/theseus/desktop/host_win_app.cpp b/theseus/desktop/host_win_app.cpp index 3f7c867..a05afb9 100644 --- a/theseus/desktop/host_win_app.cpp +++ b/theseus/desktop/host_win_app.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include @@ -193,3 +195,63 @@ bool Host_PickFile(const char* title, char* out, size_t outSize) { snprintf(out, outSize, "%s", path); return true; } + +bool Host_ExePath(char* out, size_t outSize) { + DWORD len = GetModuleFileNameA(NULL, out, (DWORD)outSize); + return len > 0 && len < outSize; +} + +void Host_OpenFolder(const char* dir) { + ShellExecuteA(NULL, "open", dir, NULL, NULL, SW_SHOWNORMAL); +} + +bool Host_RegistryString(const char* subKey, const char* value, char* out, size_t outSize) { + HKEY hKey; + if (RegOpenKeyExA(HKEY_CURRENT_USER, subKey, 0, KEY_READ, &hKey) != ERROR_SUCCESS) return false; + DWORD size = (DWORD)outSize; + DWORD type = 0; + bool ok = RegQueryValueExA(hKey, value, NULL, &type, (BYTE*)out, &size) == ERROR_SUCCESS && + (type == REG_SZ || type == REG_EXPAND_SZ); + RegCloseKey(hKey); + if (ok) out[outSize - 1] = '\0'; + return ok; +} + +static HANDLE OpenCdromDevice() { + char path[] = "\\\\.\\D:"; + for (char c = 'D'; c <= 'H'; ++c) { + char root[4] = { c, ':', '\\', 0 }; + if (GetDriveTypeA(root) != DRIVE_CDROM) continue; + path[4] = c; + HANDLE h = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, 0, nullptr); + if (h != INVALID_HANDLE_VALUE) return h; + } + return INVALID_HANDLE_VALUE; +} + +int Host_CdReadToc(int* durations, int maxTracks, bool* allAudio) { + *allAudio = true; + HANDLE h = OpenCdromDevice(); + if (h == INVALID_HANDLE_VALUE) return -1; + + CDROM_TOC toc; + DWORD br = 0; + BOOL ok = DeviceIoControl(h, IOCTL_CDROM_READ_TOC, nullptr, 0, &toc, sizeof(toc), &br, nullptr); + CloseHandle(h); + if (!ok) return -1; + + int count = toc.LastTrack - toc.FirstTrack + 1; + for (int i = 0; i < count; ++i) { + int ti = toc.FirstTrack - 1 + i; // index into TrackData + if (toc.TrackData[ti].Control & 4) *allAudio = false; + if (durations && i < maxTracks) { + UCHAR* a = toc.TrackData[ti].Address; + UCHAR* b = toc.TrackData[ti + 1].Address; + int t0 = (int)a[1] * 60 + a[2]; + int t1 = (int)b[1] * 60 + b[2]; + durations[i] = (t1 > t0) ? (t1 - t0) : 0; + } + } + return count; +} diff --git a/theseus/desktop/icon_compress.cpp b/theseus/desktop/icon_compress.cpp index 4e0523e..76b7b98 100644 --- a/theseus/desktop/icon_compress.cpp +++ b/theseus/desktop/icon_compress.cpp @@ -1,3 +1,4 @@ +#include "platform.h" #include "icon_compress.h" #include "virtual_games.h" // VGAMES_ICONS @@ -10,7 +11,7 @@ #include #include #include -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN #include #else #include @@ -127,7 +128,7 @@ IconCompressResult Icons_Compress(int maxDim, int quality) static char names[8192][256]; int count = 0; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN char pattern[1024]; snprintf(pattern, sizeof(pattern), "%s/*", dir); struct _finddata_t fd; diff --git a/theseus/desktop/image.cpp b/theseus/desktop/image.cpp index 813b4ac..095668d 100644 --- a/theseus/desktop/image.cpp +++ b/theseus/desktop/image.cpp @@ -547,7 +547,7 @@ void PreloadSkinTextures() FindImage(wideFileName); }; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN char searchBuf[512]; snprintf(searchBuf, sizeof(searchBuf), "%s\\*.xbx", mappedDir); struct _finddata_t fd; diff --git a/theseus/desktop/imgui_impl_bgfx.cpp b/theseus/desktop/imgui_impl_bgfx.cpp index 0e71542..417481e 100644 --- a/theseus/desktop/imgui_impl_bgfx.cpp +++ b/theseus/desktop/imgui_impl_bgfx.cpp @@ -11,6 +11,7 @@ // DisplayPos / DisplaySize. Each ImDrawCmd is a setScissor + draw of // (ElemCount) indices at (IdxOffset, VtxOffset). +#include "platform.h" #include "app_paths.h" #include "imgui_impl_bgfx.h" #include "imgui.h" @@ -38,7 +39,7 @@ static BgfxImguiBackend g_bd; // via a header; same pattern. static bgfx::ShaderHandle LoadShader(const char* name) { -#if defined(_WIN32) +#if defined(THESEUS_HOST_WIN) const char* sub = "dx11"; #elif defined(__APPLE__) const char* sub = "metal"; diff --git a/theseus/desktop/launchers/retroarch.cpp b/theseus/desktop/launchers/retroarch.cpp index b6dd943..cd122f8 100644 --- a/theseus/desktop/launchers/retroarch.cpp +++ b/theseus/desktop/launchers/retroarch.cpp @@ -17,17 +17,20 @@ #include #include -#ifdef _WIN32 -#include -#else -#include -#endif +#include "platform.h" +#include "host.h" extern char s_retroarchPath[512]; namespace { -#ifdef _WIN32 +// Case-insensitive "name ends with ext", like a Win32 *.ext glob. +bool HasExt(const char* name, const char* ext) { + size_t n = strlen(name), e = strlen(ext); + return n >= e && strcasecmp(name + n - e, ext) == 0; +} + +#ifdef THESEUS_HOST_WIN const char kPathSep = '\\'; const char* kRetroArchExe = "retroarch.exe"; #else @@ -88,7 +91,7 @@ bool ParseField(const char* query, const char* key, char* out, size_t outSize) { bool LooksAbsolute(const char* p) { if (!p || !*p) return false; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN if (p[0] && p[1] == ':' && (p[2] == '\\' || p[2] == '/')) return true; if (p[0] == '\\' && p[1] == '\\') return true; return false; @@ -185,7 +188,7 @@ int RetroArch_DiscoverInstall(const char* userOverride, if (userOverride && *userOverride) append(userOverride); -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN const char* candidates[] = { "C:\\RetroArch", "C:\\RetroArch-Win64", @@ -197,7 +200,7 @@ int RetroArch_DiscoverInstall(const char* userOverride, 0 }; for (int i = 0; candidates[i]; i++) append(candidates[i]); -#elif defined(__APPLE__) +#elif defined(THESEUS_HOST_MAC) const char* home = getenv("HOME"); if (home) { char buf[512]; @@ -252,7 +255,7 @@ bool RetroArch_DiscoverPlaylistsDir(const char* installRoot, snprintf(buf, sizeof(buf), "%s/Documents/RetroArch/playlists", home); if (DirExists(buf)) { strncpy(outDir, buf, outSize - 1); outDir[outSize - 1] = 0; return true; } } -#elif !defined(_WIN32) +#elif !defined(THESEUS_HOST_WIN) const char* home = getenv("HOME"); if (home) { snprintf(buf, sizeof(buf), "%s/.config/retroarch/playlists", home); @@ -270,9 +273,9 @@ bool RetroArch_FindCoreForSystem(const char* installRoot, const char* systemName if (outSize) outCoreFile[0] = '\0'; if (!installRoot || !*installRoot || !systemName || !*systemName) return false; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN const char* coreExt = ".dll"; -#elif defined(__APPLE__) +#elif defined(THESEUS_HOST_MAC) const char* coreExt = ".dylib"; #else const char* coreExt = ".so"; @@ -323,31 +326,17 @@ bool RetroArch_FindCoreForSystem(const char* installRoot, const char* systemName fclose(fp); }; -#ifdef _WIN32 - char glob[700]; - snprintf(glob, sizeof(glob), "%s\\*.info", infoDir); - WIN32_FIND_DATAA fd; - HANDLE h = FindFirstFileA(glob, &fd); - if (h != INVALID_HANDLE_VALUE) { - do { - if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue; - tryFile(fd.cFileName); - if (found) break; - } while (FindNextFileA(h, &fd)); - FindClose(h); - } -#else - DIR* d = opendir(infoDir); + HostDir* d = Host_OpenDir(infoDir); if (d) { - struct dirent* ent; - while ((ent = readdir(d)) && !found) { - size_t len = strlen(ent->d_name); - if (len < 5 || strcmp(ent->d_name + len - 5, ".info") != 0) continue; - tryFile(ent->d_name); + char name[260]; + HostFileInfo info; + while (!found && Host_ReadDir(d, name, sizeof(name), &info)) { + if (info.attrs & HOST_ATTR_DIR) continue; + if (!HasExt(name, ".info")) continue; + tryFile(name); } - closedir(d); + Host_CloseDir(d); } -#endif return found; } @@ -565,34 +554,19 @@ int RetroArch_WalkPlaylists(const char* playlistsDir, if (!playlistsDir || !*playlistsDir || !cb) return 0; int total = 0; -#ifdef _WIN32 - char glob[700]; - snprintf(glob, sizeof(glob), "%s\\*.lpl", playlistsDir); - WIN32_FIND_DATAA fd; - HANDLE h = FindFirstFileA(glob, &fd); - if (h != INVALID_HANDLE_VALUE) { - do { - if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue; - char filePath[900]; - snprintf(filePath, sizeof(filePath), "%s\\%s", playlistsDir, fd.cFileName); - total += WalkOnePlaylist(filePath, cb, userdata); - } while (FindNextFileA(h, &fd)); - FindClose(h); - } -#else - DIR* d = opendir(playlistsDir); + HostDir* d = Host_OpenDir(playlistsDir); if (d) { - struct dirent* ent; - while ((ent = readdir(d))) { - size_t len = strlen(ent->d_name); - if (len < 4 || strcmp(ent->d_name + len - 4, ".lpl") != 0) continue; + char name[260]; + HostFileInfo info; + while (Host_ReadDir(d, name, sizeof(name), &info)) { + if (info.attrs & HOST_ATTR_DIR) continue; + if (!HasExt(name, ".lpl")) continue; char filePath[900]; - snprintf(filePath, sizeof(filePath), "%s/%s", playlistsDir, ent->d_name); + snprintf(filePath, sizeof(filePath), "%s%c%s", playlistsDir, kPathSep, name); total += WalkOnePlaylist(filePath, cb, userdata); } - closedir(d); + Host_CloseDir(d); } -#endif // Also walk the recent-launches history under builtin/ which lives // outside the top-level .lpl glob. @@ -608,10 +582,10 @@ int RetroArch_EnumerateCores(const char* installRoot, char outCores[][256], int maxCores) { if (!installRoot || !*installRoot || !outCores || maxCores <= 0) return 0; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN const char* ext = ".dll"; char sep = '\\'; -#elif defined(__APPLE__) +#elif defined(THESEUS_HOST_MAC) const char* ext = ".dylib"; char sep = '/'; #else @@ -625,33 +599,18 @@ int RetroArch_EnumerateCores(const char* installRoot, int n = 0; size_t extLen = strlen(ext); -#ifdef _WIN32 - char glob[700]; - snprintf(glob, sizeof(glob), "%s\\*%s", dir, ext); - WIN32_FIND_DATAA fd; - HANDLE h = FindFirstFileA(glob, &fd); - if (h == INVALID_HANDLE_VALUE) return 0; - do { - if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue; - strncpy(outCores[n], fd.cFileName, 255); - outCores[n][255] = '\0'; - if (++n >= maxCores) break; - } while (FindNextFileA(h, &fd)); - FindClose(h); -#else - DIR* d = opendir(dir); + HostDir* d = Host_OpenDir(dir); if (!d) return 0; - struct dirent* ent; - while ((ent = readdir(d)) && n < maxCores) { - size_t len = strlen(ent->d_name); - if (len < extLen) continue; - if (strcmp(ent->d_name + len - extLen, ext) != 0) continue; - strncpy(outCores[n], ent->d_name, 255); + char name[260]; + HostFileInfo info; + while (n < maxCores && Host_ReadDir(d, name, sizeof(name), &info)) { + if (info.attrs & HOST_ATTR_DIR) continue; + if (!HasExt(name, ext)) continue; + strncpy(outCores[n], name, 255); outCores[n][255] = '\0'; n++; } - closedir(d); -#endif + Host_CloseDir(d); return n; } diff --git a/theseus/desktop/launchers/steam.cpp b/theseus/desktop/launchers/steam.cpp index b15276a..f6ab78b 100644 --- a/theseus/desktop/launchers/steam.cpp +++ b/theseus/desktop/launchers/steam.cpp @@ -21,9 +21,8 @@ #include #include -#ifdef _WIN32 -#include -#endif +#include "platform.h" +#include "host.h" namespace { @@ -53,24 +52,14 @@ bool DirExists(const char* path) { int CollectCandidateRoots(char outRoots[][512], int maxRoots) { int n = 0; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN // Windows registry: HKCU\Software\Valve\Steam\SteamPath is // what the Steam installer writes. Most reliable for non- // default installs (e.g. user picked D:\Steam). { - HKEY hKey; - if (RegOpenKeyExA(HKEY_CURRENT_USER, "Software\\Valve\\Steam", - 0, KEY_READ, &hKey) == ERROR_SUCCESS) { - char buf[512]; - DWORD bufSize = sizeof(buf); - DWORD type = 0; - if (RegQueryValueExA(hKey, "SteamPath", NULL, &type, - (BYTE*)buf, &bufSize) == ERROR_SUCCESS && - (type == REG_SZ || type == REG_EXPAND_SZ)) { - if (n < maxRoots) n = AppendUnique(outRoots, n, maxRoots, buf); - } - RegCloseKey(hKey); - } + char buf[512]; + if (Host_RegistryString("Software\\Valve\\Steam", "SteamPath", buf, sizeof(buf))) + n = AppendUnique(outRoots, n, maxRoots, buf); } // Default Windows install paths. { @@ -85,7 +74,7 @@ int CollectCandidateRoots(char outRoots[][512], int maxRoots) { n = AppendUnique(outRoots, n, maxRoots, path); } } -#elif defined(__APPLE__) +#elif defined(THESEUS_HOST_MAC) // macOS: official installer puts it under ~/Library/Application Support. const char* home = getenv("HOME"); if (home && *home) { diff --git a/theseus/desktop/menu_bar.cpp b/theseus/desktop/menu_bar.cpp index 76478ff..999a810 100644 --- a/theseus/desktop/menu_bar.cpp +++ b/theseus/desktop/menu_bar.cpp @@ -1266,7 +1266,7 @@ void RenderAboutWindow() { "macOS" #elif defined(__linux__) "Linux" -#elif defined(_WIN32) +#elif defined(THESEUS_HOST_WIN) "Windows" #else "Unknown" diff --git a/theseus/desktop/path_ci.h b/theseus/desktop/path_ci.h index dbafd8e..70af6f8 100644 --- a/theseus/desktop/path_ci.h +++ b/theseus/desktop/path_ci.h @@ -3,11 +3,12 @@ #pragma once +#include "platform.h" #include #include #include -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN #include #else #include @@ -40,7 +41,7 @@ inline bool Plat_ResolveCaseInsensitive(char* resolvedPath, size_t maxLen) { char rebuilt[1024]; rebuilt[0] = '\0'; char* walk = tempPath; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN if (tempPath[0] && tempPath[1] == ':') { snprintf(rebuilt, sizeof(rebuilt), "%c:", tempPath[0]); walk = tempPath + 2; @@ -57,14 +58,14 @@ inline bool Plat_ResolveCaseInsensitive(char* resolvedPath, size_t maxLen) { char* components[64]; int nComponents = 0; char* save = NULL; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN char* tok = strtok_s(walk, "/", &save); #else char* tok = strtok_r(walk, "/", &save); #endif while (tok && nComponents < 64) { components[nComponents++] = tok; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN tok = strtok_s(NULL, "/", &save); #else tok = strtok_r(NULL, "/", &save); @@ -89,7 +90,7 @@ inline bool Plat_ResolveCaseInsensitive(char* resolvedPath, size_t maxLen) { const char* dirToScan = rebuilt[0] ? rebuilt : "."; bool found = false; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN char searchBuf[1024]; snprintf(searchBuf, sizeof(searchBuf), "%s%s*", dirToScan, needSlash ? "/" : ""); struct _finddata_t fd; diff --git a/theseus/desktop/path_template.cpp b/theseus/desktop/path_template.cpp index 09b7286..0abe8af 100644 --- a/theseus/desktop/path_template.cpp +++ b/theseus/desktop/path_template.cpp @@ -11,6 +11,7 @@ // each module will call a Register-style API instead of editing // this table; for now we hardcode the existing dispatcher knobs. +#include "platform.h" #include "path_template.h" #include @@ -80,7 +81,7 @@ static const char* HomeDir() { if (!s_init) { s_init = true; const char* h = getenv("HOME"); -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN if (!h || !*h) h = getenv("USERPROFILE"); #endif if (h && *h) { diff --git a/theseus/desktop/platform_env.h b/theseus/desktop/platform_env.h index b595e6e..ad305db 100644 --- a/theseus/desktop/platform_env.h +++ b/theseus/desktop/platform_env.h @@ -1,10 +1,11 @@ // Home and user lookup that works the same on every desktop OS. Standalone -// on purpose (only libc), so lean files that already include can -// pull it without dragging the platform shim in. Both return a stable buffer, +// on purpose (only libc), so host files can pull it without dragging the +// platform shim in. Both return a stable buffer, // never NULL. #pragma once +#include "platform.h" #include #include @@ -14,7 +15,7 @@ inline const char* PlatformHomeDir() { if (!s_init) { s_init = true; const char* h = getenv("HOME"); -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN if (!h || !*h) h = getenv("USERPROFILE"); #endif if (h && *h) { @@ -33,7 +34,7 @@ inline const char* PlatformUserName() { if (!s_init) { s_init = true; const char* u; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN u = getenv("USERNAME"); #else u = getenv("USER"); diff --git a/theseus/desktop/plex_client.cpp b/theseus/desktop/plex_client.cpp index f479cf4..0553d9c 100644 --- a/theseus/desktop/plex_client.cpp +++ b/theseus/desktop/plex_client.cpp @@ -1,5 +1,6 @@ // plex_client.cpp: Plex Media Server HTTPS client. +#include "platform.h" #include "app_paths.h" #include "plex_client.h" #include "http_util.h" @@ -35,7 +36,7 @@ static HttpHeaders PlexHeaders(bool wantJson, bool withToken) h.push_back({ "X-Plex-Device-Name", "UIX Desktop" }); #if defined(__APPLE__) h.push_back({ "X-Plex-Platform", "macOS" }); -#elif defined(_WIN32) +#elif defined(THESEUS_HOST_WIN) h.push_back({ "X-Plex-Platform", "Windows" }); #else h.push_back({ "X-Plex-Platform", "Linux" }); diff --git a/theseus/desktop/preloader.cpp b/theseus/desktop/preloader.cpp index 1da1a90..7059fc3 100644 --- a/theseus/desktop/preloader.cpp +++ b/theseus/desktop/preloader.cpp @@ -15,7 +15,7 @@ #include #include -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN #include #include #else @@ -50,7 +50,7 @@ static void Preloader_Mkdirp(const char* path) { for (char* p = tmp + 1; *p; p++) { if (*p == '/') { *p = '\0'; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN _mkdir(tmp); #else mkdir(tmp, 0755); @@ -58,7 +58,7 @@ static void Preloader_Mkdirp(const char* path) { *p = '/'; } } -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN _mkdir(tmp); #else mkdir(tmp, 0755); @@ -233,7 +233,7 @@ int Preloader_ExtractXIP(const char* xipPath, const char* outDir) { static int Preloader_ExtractAllXIPs(const char* xipsDir, char statusBuf[], int statusBufSize) { int totalXips = 0, count = 0; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN // Windows: use _findfirst/_findnext char searchPath[1024]; snprintf(searchPath, sizeof(searchPath), "%s\\*.xip", xipsDir); diff --git a/theseus/desktop/title_maker.cpp b/theseus/desktop/title_maker.cpp index 9602ad5..deaacab 100644 --- a/theseus/desktop/title_maker.cpp +++ b/theseus/desktop/title_maker.cpp @@ -23,7 +23,7 @@ #include #include -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN #include #include #include @@ -40,7 +40,7 @@ bool g_titleMakerOpen = false; static void TM_EnsureDir(const char* path) { struct stat st; if (stat(path, &st) != 0) { -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN _mkdir(path); #else mkdir(path, 0755); @@ -532,7 +532,7 @@ static bool TM_DownloadSteamIcon(int appid, const char* titleID) { "sips -z 128 128 -s format jpeg \"%s\" --out \"%s\" >/dev/null 2>&1", tmpPath, iconPath); bool resizeOk = (system(cmd) == 0); -#elif defined(_WIN32) +#elif defined(THESEUS_HOST_WIN) bool resizeOk = TM_CopyFile(tmpPath, iconPath); #else char cmd[1024]; @@ -597,7 +597,7 @@ static void TM_ImportSteamLibrary(const char* steamPath, }; for (int d = 0; d < libCount && gameCount < 256; d++) { -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN char searchBuf[512]; snprintf(searchBuf, sizeof(searchBuf), "%s\\appmanifest_*", libDirs[d]); struct _finddata_t fd; @@ -809,7 +809,7 @@ void RenderTitleMaker() { "/Applications/xemu.app", "/opt/homebrew/bin/xemu", "/usr/local/bin/xemu", -#elif defined(_WIN32) +#elif defined(THESEUS_HOST_WIN) "C:\\Program Files\\xemu\\xemu.exe", "C:\\Program Files (x86)\\xemu\\xemu.exe", #else @@ -1362,7 +1362,7 @@ void RenderTitleMaker() { ImGui::SameLine(); if (ImGui::SmallButton("Find##st")) { const char* user = NULL; -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN user = getenv("USERNAME"); #else user = getenv("USER"); @@ -1372,7 +1372,7 @@ void RenderTitleMaker() { const char* tryRoots[] = { #ifdef __APPLE__ "/Users/%s/Library/Application Support/Steam", -#elif defined(_WIN32) +#elif defined(THESEUS_HOST_WIN) "C:\\Program Files (x86)\\Steam", "C:\\Program Files\\Steam", "D:\\Steam", diff --git a/theseus/desktop/tmdb.cpp b/theseus/desktop/tmdb.cpp index 0dcf7e7..690fdc0 100644 --- a/theseus/desktop/tmdb.cpp +++ b/theseus/desktop/tmdb.cpp @@ -4,6 +4,7 @@ #include "app_paths.h" #include "tmdb.h" +#include "host.h" #include #include @@ -56,15 +57,10 @@ static std::string ShowKey(const char* title) return Slugify(title); } -// Portable mkdir wrapper. POSIX mkdir takes (path, mode); MinGW's -// redeclares it as the MS-CRT one-arg variant. +// Make one folder. 0 when it exists afterwards. static inline int Mkdir(const char* path) { -#ifdef _WIN32 - return mkdir(path); -#else - return mkdir(path, 0755); -#endif + return Host_MakeDir(path) ? 0 : -1; } static std::string CachePath(const char* prefix, const std::string& slug) diff --git a/theseus/desktop/udata_synth.cpp b/theseus/desktop/udata_synth.cpp index b56742b..344ef64 100644 --- a/theseus/desktop/udata_synth.cpp +++ b/theseus/desktop/udata_synth.cpp @@ -13,13 +13,8 @@ #include #include #include -#ifdef _WIN32 -#include -#define UDS_MKDIR(p) _mkdir(p) -#else -#include -#define UDS_MKDIR(p) mkdir((p), 0755) -#endif +#include "host.h" +#define UDS_MKDIR(p) Host_MakeDir(p) namespace { diff --git a/theseus/desktop/xap_editor.cpp b/theseus/desktop/xap_editor.cpp index 33126f2..4850f02 100644 --- a/theseus/desktop/xap_editor.cpp +++ b/theseus/desktop/xap_editor.cpp @@ -20,7 +20,7 @@ #include #include -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN #include #include #define strcasecmp _stricmp @@ -174,7 +174,7 @@ void XapEditor_Cleanup() {} // ============================================================================ static void XapEditor_ScanDir(const char* dir, char fileList[][512], int* count, int maxFiles) { -#ifdef _WIN32 +#ifdef THESEUS_HOST_WIN char searchPath[512]; snprintf(searchPath, sizeof(searchPath), "%s\\*", dir); struct _finddata_t fd; diff --git a/theseus/desktop/xboxfs_drive.h b/theseus/desktop/xboxfs_drive.h index dd01188..946faba 100644 --- a/theseus/desktop/xboxfs_drive.h +++ b/theseus/desktop/xboxfs_drive.h @@ -9,13 +9,9 @@ #include "app_paths.h" +#include "host.h" + #include -#include -#ifdef _WIN32 -#include -#else -#include -#endif static inline const char* XboxFS_DriveToPrefix(char drive) { if (drive == 'C' || drive == 'c') return AppPath_Tree("Configs"); @@ -28,11 +24,7 @@ static inline const char* XboxFS_DriveToPrefix(char drive) { // Make one directory. True if it exists after. static inline bool Plat_Mkdir(const char* path) { if (!path || !*path) return false; -#ifdef _WIN32 - return _mkdir(path) == 0 || errno == EEXIST; -#else - return mkdir(path, 0755) == 0 || errno == EEXIST; -#endif + return Host_MakeDir(path); } // Make a directory and any missing parents. Accepts either slash style. From 581f829e15bd0878f54bd9cdd7b14066b9c140f3 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:06:47 -0500 Subject: [PATCH 036/167] Added a Windows import guard to the build make win64-import-guard compiles the Windows objects and fails if anything outside the host_win files calls kernel32's file, clock or sleep calls. It doesn't need to link, so it works without the mingw libs. --- build/Makefile | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/build/Makefile b/build/Makefile index e16c148..d80a8a6 100644 --- a/build/Makefile +++ b/build/Makefile @@ -1433,6 +1433,27 @@ $(WIN64_DIR)/%.o: $(THESEUS_SRC)/%.cpp @mkdir -p $(dir $@) $(WIN64_CXX) $(WIN64_CFLAGS) -c -o $@ $< +# Import guard: only the host_win objects may call kernel32's file, clock +# and sleep calls. Anything else that does got a real Win32 call where the +# shim was meant to answer. Compiles the objects, doesn't link, so it runs +# without the mingw libs. +WIN64_NM ?= x86_64-w64-mingw32-nm +WIN64_GUARD_SYMS = CreateFileA CreateFileW DeleteFileA DeleteFileW \ + CreateDirectoryA CreateDirectoryW GetDiskFreeSpaceExA GetDiskFreeSpaceExW \ + FindFirstFileA FindFirstFileW GetTickCount Sleep +WIN64_GUARD_OBJS = $(filter-out $(WIN64_DIR)/desktop/host_win%.o,$(WIN64_OBJS)) + +.PHONY: win64-import-guard +win64-import-guard: $(WIN64_GUARD_OBJS) + @bad=0; \ + for o in $(WIN64_GUARD_OBJS); do \ + hits=$$($(WIN64_NM) -u $$o | awk '{ print $$NF }' | sed 's/^__imp_//' | \ + grep -xF $(foreach s,$(WIN64_GUARD_SYMS),-e $(s)) | sort -u | tr '\n' ' '); \ + if [ -n "$$hits" ]; then echo "$$o: $$hits"; bad=1; fi; \ + done; \ + if [ $$bad -ne 0 ]; then echo "win64-import-guard: Win32 calls above belong in desktop/host_win*.cpp."; exit 1; fi; \ + echo "win64-import-guard: clean ($(words $(WIN64_GUARD_OBJS)) objects)" + desktop-win64-clean: projectm-win64-clean rm -rf $(WIN64_DIR) From 9eb2df00b6554c664916cdee2de18f1f60e1cfa8 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:08:13 -0500 Subject: [PATCH 037/167] Stopped building xbox/ files into the desktop modchip.cpp and network.cpp were compiled on desktop and picked up xbox/std.h. The desktop only needed the net:: stubs, which now live in desktop_stubs.cpp, and each std.h refuses to build for the wrong side. --- build/Makefile | 2 -- theseus/desktop/desktop_stubs.cpp | 15 +++++++++++++++ theseus/desktop/std.h | 3 +++ theseus/xbox/modchip.cpp | 9 --------- theseus/xbox/network.cpp | 25 +++---------------------- theseus/xbox/std.h | 19 ++++++------------- 6 files changed, 27 insertions(+), 46 deletions(-) diff --git a/build/Makefile b/build/Makefile index d80a8a6..b5909af 100644 --- a/build/Makefile +++ b/build/Makefile @@ -883,8 +883,6 @@ DESKTOP_SRCS_PLATFORM = \ $(THESEUS_SRC)/desktop/launchers/retroarch.cpp \ $(THESEUS_SRC)/desktop/title_sanitize.cpp \ $(THESEUS_SRC)/desktop/udata_synth.cpp \ - $(THESEUS_SRC)/xbox/modchip.cpp \ - $(THESEUS_SRC)/xbox/network.cpp \ $(THESEUS_SRC)/desktop/desktop_stubs.cpp \ $(THESEUS_SRC)/desktop/desktop_nodes.cpp \ $(THESEUS_SRC)/desktop/media_db.cpp \ diff --git a/theseus/desktop/desktop_stubs.cpp b/theseus/desktop/desktop_stubs.cpp index 42d1ed9..04c13e0 100644 --- a/theseus/desktop/desktop_stubs.cpp +++ b/theseus/desktop/desktop_stubs.cpp @@ -8,6 +8,7 @@ #include "runner.h" #include "titlecollection.h" #include "discmanager.h" +#include "network.h" // g_titles - from TitleCollection.cpp (removed) CTitleArray g_titles[9]; @@ -139,3 +140,17 @@ int CXBExecutable::ReadFile(const char* szFileName, const bool, const bool) { return 0; } +// net:: from xbox/network.cpp. No FTP UI on desktop today. +namespace net +{ + bool init() { return true; } + void shutdown() {} + void restart() {} + bool isReady() { return false; } + bool isFtpRunning() { return false; } + DWORD getAddress() { return 0; } + DWORD getSubnet() { return 0; } + DWORD getGateway() { return 0; } + DWORD getPrimaryDns() { return 0; } + DWORD getSecondaryDns(){ return 0; } +} diff --git a/theseus/desktop/std.h b/theseus/desktop/std.h index e2c0b51..a246a68 100755 --- a/theseus/desktop/std.h +++ b/theseus/desktop/std.h @@ -3,6 +3,9 @@ // xbox/std.h. #include "platform.h" +#ifdef _XBOX +#error "desktop/std.h is the desktop prelude. Xbox code includes xbox/std.h." +#endif #include "stdcfg.h" // Flag so toolbox/xboxinternals.h knows kernel types are already available diff --git a/theseus/xbox/modchip.cpp b/theseus/xbox/modchip.cpp index 74b62e5..a7fba93 100755 --- a/theseus/xbox/modchip.cpp +++ b/theseus/xbox/modchip.cpp @@ -4,7 +4,6 @@ #include "std.h" -#ifdef _XBOX #include "xkflash.h" typedef unsigned char uint8_t; typedef unsigned short uint16_t; @@ -88,11 +87,3 @@ const TCHAR* DetectModchip() return szModchip; } -#else // desktop: no LPC bus, no flash chip to probe. - -const TCHAR* DetectModchip() -{ - return _T("N/A (Desktop)"); -} - -#endif // _XBOX diff --git a/theseus/xbox/network.cpp b/theseus/xbox/network.cpp index 5f5592c..25c3ccf 100755 --- a/theseus/xbox/network.cpp +++ b/theseus/xbox/network.cpp @@ -1,11 +1,9 @@ -// network.cpp: net:: namespace implementation. The Xbox path drives -// the dashboard's FTP server and the real XNet stack; the desktop -// path stubs the namespace so callers (Discord relay, FTP widget, -// IP display) compile without a real network backend. +// network.cpp: net:: namespace implementation. Drives the dashboard's +// FTP server and the real XNet stack. The desktop's stubs live in +// desktop/desktop_stubs.cpp. #include "std.h" #include "network.h" -#ifdef _XBOX #include "toolbox/ftpServer.h" #include "toolbox/driveManager.h" #include @@ -149,20 +147,3 @@ namespace net } } -#else // desktop: no FTP UI today; stubs let callers link. - -namespace net -{ - bool init() { return true; } - void shutdown() {} - void restart() {} - bool isReady() { return false; } - bool isFtpRunning() { return false; } - DWORD getAddress() { return 0; } - DWORD getSubnet() { return 0; } - DWORD getGateway() { return 0; } - DWORD getPrimaryDns() { return 0; } - DWORD getSecondaryDns(){ return 0; } -} - -#endif // _XBOX diff --git a/theseus/xbox/std.h b/theseus/xbox/std.h index 2768d64..2fa5985 100755 --- a/theseus/xbox/std.h +++ b/theseus/xbox/std.h @@ -1,13 +1,12 @@ -// std.h: universal prelude header. Every .cpp file includes this -// first; sets up the type system, CRT headers, D3D types, and -// debug infrastructure for whichever platform we're building on. -// -// _XBOX : XDK kernel + XTL + D3D8 (original Xbox hardware) -// !_XBOX + _WIN32 : Win32 API + SDL + OpenGL (Windows desktop) -// !_XBOX + !_WIN32 : POSIX shims + SDL + OpenGL (macOS / Linux) +// std.h: the Xbox build's prelude. Every Xbox .cpp includes this first; +// it sets up the XDK kernel + XTL, CRT headers, D3D8 types and debug +// infrastructure. The desktop has its own in desktop/std.h. // Check Configuration Options #include "platform.h" +#ifdef THESEUS_DESKTOP +#error "xbox/std.h is the Xbox prelude. Desktop code includes desktop/std.h." +#endif #include "stdcfg.h" // Flag so toolbox/xboxinternals.h knows kernel types are already available @@ -62,12 +61,6 @@ typedef char FSCHAR; #include "vertex8.h" #endif -#else // !_XBOX -// --- Desktop: SDL platform shim + D3D8-to-OpenGL --- - -#include "platform_shim.h" -#include "d3d8_sdl.h" - #endif // _XBOX // ========================================================================= From 94dc83815266bc7e77de3dfa30e39e40861685bc Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:12:19 -0500 Subject: [PATCH 038/167] Rebuilt the script runner from the 5960 binary Wrote every body in xap_vm.cpp fresh from the retail runner code at 0x36f70 to 0x39b90, with the address and decompile above each one, and added the runner's layout, opcodes and value objects to docs/decomp/VM.md. Kept what Theseus adds: the bigger stack and its overflow checks, the null call check, the desktop launcher, log and alert output, and unary minus giving -0. Script results match what we had on every pure function in the 54 shipped XAPs and on a 47 case runner test, and a desktop boot logs the same. --- docs/decomp/VM.md | 122 +++ theseus/shared/xap_vm.cpp | 1765 +++++++++++++++++++++---------------- 2 files changed, 1118 insertions(+), 769 deletions(-) diff --git a/docs/decomp/VM.md b/docs/decomp/VM.md index 515e04f..842dc70 100644 --- a/docs/decomp/VM.md +++ b/docs/decomp/VM.md @@ -108,3 +108,125 @@ The tokenizer supports three comment styles: - `#` line comments - `//` line comments - `/* */` block comments + +## Runner Internals (5960 retail XBE) + +The runner code sits together at 0x36f70 to 0x39b90. Everything below is read from the retail binary. + +### CRunner Layout + +The runner is never heap allocated. `CallFunction` (0x371a0), `ExecuteScript` (0x36fb0) and `CMemberFunctionObject::Deref` (0x39810) each build one on the stack and tear it down with `ResetFunc`. + +| Offset | Field | Notes | +|--------|-------|-------| +| 0x000 | wakeup | double, sleep deadline | +| 0x008 | nop | byte offset into the op stream | +| 0x00c | ops | current op stream | +| 0x010 | sp | stack top index | +| 0x014 | stack | 100 object pointers | +| 0x1a4 | spFrame | first argument of the current call | +| 0x1a8 | spBase | top of the locals | +| 0x1ac | self | owner of the running function | +| 0x1b0 | this | object the script was started on | +| 0x1b4 | error | set by the first error, stops `Run` | +| 0x1b8 | line | last `opStatement` line | +| 0x1bc | context | saved frames, newest first | + +A saved frame (`PushContext` 0x37690, 0x1c bytes) holds the next link, ops, nop, spFrame, self, this, and the stack index under the callee and its arguments. `PopContext` (0x376e0) resolves the return value, restores the five fields (and `g_pThis`), releases every slot above the saved index, frees the frame and pushes the value. + +### Globals + +| Address | Use | +|---------|-----| +| 0x17ae9c | constructor argument count | +| 0x17aea0 | constructor argument array | +| 0x17aea4 | runner currently stepping | +| 0x17aea8 | `this` for member lookups | +| 0x17c058 | input enable flag (`EnableInput`) | +| 0x17fac8 | the `Math` object | +| 0x187fb0 | the camera | + +### Step (0x37800) + +One switch on the opcode byte. Operands follow inline: ints and floats are 4 bytes, a string is a 4 byte length followed by that many 16-bit characters, `opCall` takes a single byte. + +| Op | Operands | Behavior | +|----|----------|----------| +| opNull | | push null | +| opNew | int n, string class | `CreateNewObject` with the n values on the stack, release them, push the result. Unknown class: `Failed to create new object: "%s"` (name cut to 255) | +| opVar | string | push a new unresolved name object | +| opJump | uint | jump | +| opCond | uint | pop, resolve, `ToNum`; nonzero skips the operand, zero or null jumps | +| opCall | byte n | see below | +| opRet | | with a saved frame, `PopContext`; at the top, pop the result into the out pointer and stop | +| opStr / opNum | string / float | push a string / number | +| opAdd .. opXor | | `BinaryOperator` | +| opDot / opArray | | pop key and base, both must be non-null; `opArray` resolves the key fully; the base is resolved one step and asked `Dot(key)`. A null answer reports `Unknown member: %s` but still pushes | +| opAssign | | pop value (resolved) and target; null target is `cannot assign to null`; else `target->Assign(value)`, push value | +| opLocal | int i | slot `spFrame + i`; creates a `CLocalVariable` there on first use, pushes it with a new reference | +| opThis | | push self | +| opFrame | int n | `sp += n`, `spBase = sp`; no size check | +| opStatement | int line | store the line | +| opNeg | | pop and resolve; not a number is `expected a number`; pushes `0.0 - v` | +| opDrop | | pop and release | +| opEndFrame | int n | release n slots off the top, `spBase = sp` | +| opSleep | | pop and resolve; null is `illegal null reference`, not a number `expected a number`; wakeup = now + v | + +Any other byte stops the runner. + +`opCall` looks at the object under the n arguments: + +- type 8 (native function ref): call it, release it and the arguments, push the result. +- type 14 (bound method): AddRef it, `PushContext(n)`, `spFrame = sp - n`, self = its owner, ops = its code. +- otherwise it is a name. `ExecuteBuiltIn` gets the first try; if it handled the call the argument slots are cleared without a release, the name is released and the result pushed. Then `LookupFunction`; a hit pushes a frame and sets self and `g_pThis` to the owner. A miss is `unknown function: %s` with the name cut to 31 characters. + +There is no null check on the callee. + +### BinaryOperator (0x37fe0) + +Pops right then left, each resolved fully. + +1. Either null, or both type 5 (node): `==` and `!=` compare the pointers, anything else is `illegal null reference`. +2. Both strings and the op is not `+`: `wcscmp` (null text reads as empty), then `== != < > <= >=` push 1 or 0. Anything else is `type mismatch`. +3. Both numbers: `+ - * /`, the six comparisons, and `& | ^ << >>`. The bit ops convert both sides with `_ftol`, mask the shift count to 5 bits, and turn the result back into a float as unsigned (a negative int gets 4294967296.0 added, the constant at 0x2bed0). Any other op, including `%`, falls out of the switch and pushes the right operand unchanged. +4. Otherwise `+` concatenates the two `ToStr` forms and anything else is `type mismatch error`. + +The compiler never emits `%`, so the fallthrough is unreachable from script. + +### Errors (0x375e0) + +Only the first error of a run counts. The message is formatted into 512 characters, wrapped with or without the line number, and the error flag is set. Retail never displays it; the run just stops. + +### Built-ins (0x38620) + +Matched by length, then by text: `EnableInput` (1 arg, else `Invalid parameter`), `eval` (1 arg, else `Bad launch`), `launch` (1 or 2 args, else `Bad launch`), `DebugBreak` (does nothing), `alert` (1 arg, debug print, else `Bad alert`), `log` (1 arg, does nothing with it in retail, else `Bad log!`). + +`launch` with one argument copies the path, turns its last `/` into `\`, then cuts at the last `\` to get the directory. There is no check that a `\` was found. Both paths go through `CleanFilePath`, a lock at 0x17aeb0 is taken exclusive, and the title is launched through `XWriteTitleInfoAndRebootA` with launch data type -1. + +### Lookup + +`LookupMember` (0x399e0) sets `g_pThis`, then tries in order: a node property (type 5 objects only, wrapped in a `CProperty` whose ref count is set to 0), the object's own members, each class up the base chain, and then the same lookup on each parent. + +`LookupFunction` (0x389b0) tries `FindMemberFunction` on self, then `LookupMember` on each parent. A hit on a parent makes that parent the owner. A hit that is not a function (type 13) is returned as a raw pointer anyway. + +`CVarObject::Deref` (0x394e0) special-cases the names `Math` and `camera`, then looks the name up on self and, if self is not `this`, on `this`. A miss is `Unknown object: %s`; `Assign` (0x395b0) reports `Unknown variable: %s`. + +### Value Objects + +| Type | Class | Vtable | Notes | +|------|-------|--------|-------| +| 2 | CNumObject | 0x22c50 | `ToStr` (0x38fd0) prints `%f` into 20 characters, then trims trailing zeros and a trailing point | +| 4 | CVarObject | 0x22cf8 | owns a copy of the name | +| 7 | CNodeArrayObject | 0x22d30 | `length`, `add`, `remove` become function refs 0, 1, 2; a number indexes, out of range is `Array reference out of bounds` | +| 8 | CFunctionObject | 0x22cc0 | index plus a referenced owner | +| 9 | CMember | 0x22dd8 | variable slot on whatever `g_pThis` is | +| 10 | CMemberVarObject | 0x22da0 | variable slot on a referenced instance | +| 13 | CFunction | 0x22c18 | code size at +0x14, ops from +0x18 | +| 14 | CMemberFunctionObject | 0x22d68 | reading one without calling runs it on a fresh runner | +| 16 | CVec3Object | 0x22c88 | node class `Vector3`; `ToStr` is `%f %f %f` | +| | CLocalVariable | 0x22e48 | one reference to its value; `Deref` swaps the slot reference for a value reference | +| | CProperty | 0x22e10 | node plus PRD | + +`CProperty::Deref` (0x38b00) wraps integer, boolean and number fields as numbers, vec3 as a `Vector3`, node fields as the node itself, strings as a new string, and children / node array fields as a `CNodeArrayObject` over the field. Anything else is `Unknown property type`. + +`CProperty::Assign` (0x38d70) refuses null for everything but node fields (`Illegal null assignement`, spelled that way in the binary). Integers truncate, booleans test against 0.0, a vec3 field only takes a `Vector3`, and other types are ignored. diff --git a/theseus/shared/xap_vm.cpp b/theseus/shared/xap_vm.cpp index bed9a95..9d2e6b2 100644 --- a/theseus/shared/xap_vm.cpp +++ b/theseus/shared/xap_vm.cpp @@ -1,10 +1,9 @@ -// xap_vm.cpp: XAP script virtual machine. +// xap_vm.cpp: the XAP script runner and the value objects it pushes around. // -// Bytecode interpreter, runtime object system, and property binding. -// Stack-based architecture with reference-counted objects, late-bound -// variable lookup, and a context stack for nested function calls. -// Executes bytecode produced by xap_compile.cpp. Decompiled from the -// 5960 retail XBE; see docs/decomp/VM.md. +// One byte per opcode, operands inline, a fixed operand stack and a linked +// list of saved frames for nested calls. Everything on the stack is a +// ref-counted CObject. Bodies follow the 5960 retail XBE's runner code +// (0x36f70 to 0x39b90); docs/decomp/VM.md has the behavior in more detail. #include "std.h" #include "theseus.h" #include "node.h" @@ -12,15 +11,10 @@ #include "camera.h" #include "xap_compile.h" - - //#define LOCALTRACE TRACE #define LOCALTRACE 1 ? (void)0 : ::Trace -// ========================================================================= -// Globals -// ========================================================================= - +// Shared runner state. 5960 keeps the same four at 0x17aea0..0x17aea8. CObject* g_pThis; CRunner* g_pRunner; CObject** g_rgParam; @@ -32,11 +26,15 @@ extern CCamera theCamera; CObject* LookupMember(CObject* pThis, const TCHAR* pchName, int cchName); // ========================================================================= -// Section 1: Runtime object types +// Numbers and vectors // ========================================================================= -// CNumObject: numeric value (all XAP numbers are float). - +// CNumObject constructors [type 2, vtable 0x22c50] +// Reconstructed from 5960 retail XBE @ 0x00038fa0 (float form; the empty form +// only shows up inlined). Decompile: +// m_nRefCount = 1; m_obj = 2; m_value = xmm0; +// The string form has no standalone copy in the binary; CStrObject::ToNum +// @ 0x32200 does the same wcstod inline. Kept for the header contract. CNumObject::CNumObject() { m_obj = objNumber; @@ -55,20 +53,29 @@ CNumObject::CNumObject(const TCHAR* szValue) m_value = (float)_tcstod(szValue, NULL); } +// CNumObject::ToStr +// Reconstructed from 5960 retail XBE @ 0x00038fd0. Decompile: +// n = swprintf(buf20, L"%f", (double)m_value); p = buf + n - 1; +// while (*p == '0') p--; if (*p == '.') p--; p[1] = 0; +// return new CStrObject(buf); +// So 2.500000 prints as "2.5" and 3.000000 as "3". CStrObject* CNumObject::ToStr() { - TCHAR szBuf[20]; - TCHAR* pch = szBuf + _stprintf(szBuf, _T("%f"), m_value) - 1; - // Strip trailing zeros - while (*pch == '0') pch--; - if (*pch == '.') pch--; - pch++; - *pch = '\0'; - return new CStrObject(szBuf); -} + TCHAR szNum[20]; + int cch = _stprintf(szNum, _T("%f"), m_value); + + TCHAR* pchLast = &szNum[cch - 1]; + while (*pchLast == '0') + pchLast--; + if (*pchLast == '.') + pchLast--; + pchLast[1] = '\0'; -// CVec3Object: 3D vector for XAP scripts. + return new CStrObject(szNum); +} +// Script-side Vector3. 5960 registers it as node class "Vector3" with x/y/z +// number props (factory @ 0x39070, prop map @ 0x36fa0). class CVec3Object : public CObject { DECLARE_NODE(CVec3Object, CObject) @@ -88,10 +95,16 @@ START_NODE_PROPS(CVec3Object, CObject) NODE_PROP(pt_number, CVec3Object, z) END_NODE_PROPS() +// CVec3Object constructors [type 16, vtable 0x22c88] +// Reconstructed from 5960 retail XBE @ 0x00039070 (empty, via the factory) +// and @ 0x000390c0 (copy). Decompile: +// m_obj = 0x10; m_x = v[0]; m_y = v[1]; m_z = v[2]; (or all 0) CVec3Object::CVec3Object() { m_obj = objVec3; - m_x = m_y = m_z = 0.0f; + m_x = 0.0f; + m_y = 0.0f; + m_z = 0.0f; } CVec3Object::CVec3Object(const D3DXVECTOR3& v) @@ -102,15 +115,23 @@ CVec3Object::CVec3Object(const D3DXVECTOR3& v) m_z = v.z; } +// CVec3Object::ToStr +// Reconstructed from 5960 retail XBE @ 0x000390f0. Decompile: +// swprintf(buf60, L"%f %f %f", x, y, z); return new CStrObject(buf); +// No zero trimming here, unlike numbers. CStrObject* CVec3Object::ToStr() { - TCHAR szBuf[60]; - _stprintf(szBuf, _T("%f %f %f"), m_x, m_y, m_z); - return new CStrObject(szBuf); + TCHAR szVec[60]; + _stprintf(szVec, _T("%f %f %f"), m_x, m_y, m_z); + return new CStrObject(szVec); } -// CLocalVariable: stack frame local with ref-counted value. +// ========================================================================= +// Locals and late-bound names +// ========================================================================= +// A stack slot for a script local. opLocal creates one the first time a +// slot is touched; it holds one reference to whatever the local points at. class CLocalVariable : public CObject { public: @@ -121,7 +142,18 @@ class CLocalVariable : public CObject CObject* m_pValue; }; -CLocalVariable::CLocalVariable() { m_pValue = NULL; } +// CLocalVariable [vtable 0x22e48] +// Reconstructed from 5960 retail XBE @ 0x00037300 (ctor), 0x37340 (dtor), +// 0x373a0 (Assign), 0x373d0 (Deref). Decompile: +// ctor: m_value = NULL; +// dtor: if (m_value) m_value->Release(); +// Assign: if (m_value) m_value->Release(); m_value = p; if (p) p->AddRef(); +// Deref: v = m_value; if (v) v->AddRef(); this->Release(); return v; +// Deref trades the reference to the slot for one to its value. +CLocalVariable::CLocalVariable() +{ + m_pValue = NULL; +} CLocalVariable::~CLocalVariable() { @@ -134,21 +166,24 @@ void CLocalVariable::Assign(CObject* pObject) if (m_pValue != NULL) m_pValue->Release(); m_pValue = pObject; - if (m_pValue != NULL) - m_pValue->AddRef(); + if (pObject != NULL) + pObject->AddRef(); } CObject* CLocalVariable::Deref() { - CObject* pObject = m_pValue; - if (pObject != NULL) - pObject->AddRef(); + CObject* pValue = m_pValue; + if (pValue != NULL) + pValue->AddRef(); Release(); - return pObject; + return pValue; } -// CVarObject: late-bound variable name. - +// CVarObject constructor / destructor [type 4, vtable 0x22cf8] +// Reconstructed from 5960 retail XBE @ 0x00039400 and @ 0x000394c0. Decompile: +// m_length = cch; m_capacity = cch + 1; m_text = new WCHAR[cch + 1]; +// memcpy(m_text, pch, cch * 2); m_text[cch] = 0; +// dtor: free(m_text); CVarObject::CVarObject(const TCHAR* pch, int cch) { m_obj = objVariable; @@ -164,54 +199,75 @@ CVarObject::~CVarObject() delete[] m_text; } +// CVarObject::Deref +// Reconstructed from 5960 retail XBE @ 0x000394e0. Decompile: +// if (!wcscmp(m_text, L"Math")) obj = &g_Math; (0x17fac8) +// else if (!wcscmp(m_text, L"camera")) obj = &theCamera; (0x187fb0) +// else { obj = LookupMember(runner->m_self, m_text, len); +// if (!obj && runner->m_self != runner->m_thisRef) +// obj = LookupMember(runner->m_thisRef, m_text, len); +// if (!obj) { Error(L"Unknown object: %s", m_text); return 0; } } +// obj->AddRef(); r = obj->Deref(); this->Release(); return r; +// The self-then-this lookup is CRunner::LookupVariable, inlined there. CObject* CVarObject::Deref() { - CObject* pObject; + CObject* pFound; - // Built-in globals if (_tcscmp(m_text, _T("Math")) == 0) { extern CMathClass g_Math; - pObject = &g_Math; + pFound = &g_Math; } else if (_tcscmp(m_text, _T("camera")) == 0) { - pObject = &theCamera; + pFound = &theCamera; } else { - pObject = g_pRunner->LookupVariable(m_text, _tcslen(m_text)); - if (pObject == NULL) + pFound = g_pRunner->LookupVariable(m_text, _tcslen(m_text)); + if (pFound == NULL) { g_pRunner->Error(_T("Unknown object: %s"), m_text); return NULL; } - - if (pObject->m_obj == objMember) - { - LOCALTRACE(_T("member\n")); - } } - pObject->AddRef(); - pObject = pObject->Deref(); + pFound->AddRef(); + CObject* pValue = pFound->Deref(); Release(); - return pObject; + return pValue; } +// CVarObject::Assign +// Reconstructed from 5960 retail XBE @ 0x000395b0. Decompile: +// obj = (self lookup, then this lookup, as in Deref); +// if (!obj) { Error(L"Unknown variable: %s", m_text); return; } +// obj->Assign(value); void CVarObject::Assign(CObject* pNewObject) { - CObject* pObject = g_pRunner->LookupVariable(m_text, _tcslen(m_text)); - if (pObject == NULL) + CObject* pTarget = g_pRunner->LookupVariable(m_text, _tcslen(m_text)); + if (pTarget == NULL) { g_pRunner->Error(_T("Unknown variable: %s"), m_text); return; } - pObject->Assign(pNewObject); + pTarget->Assign(pNewObject); } -// CMemberVarObject: reference to an instance variable by index. +// ========================================================================= +// Class members, bound methods, native function refs +// ========================================================================= +// CMemberVarObject [type 10, vtable 0x22da0] +// Reconstructed from 5960 retail XBE @ 0x00039640 (ctor), 0x396c0 (dtor), +// 0x39720 (Deref), 0x39760 (Assign). Decompile: +// ctor: m_owner = inst; m_memberIndex = i; inst->AddRef(); +// dtor: m_owner->Release(); +// Deref: v = m_owner->m_vars[i] (NULL if i out of range); +// if (v) v->AddRef(); this->Release(); return v; +// Assign: old = m_owner->m_vars[i]; if (old) old->Release(); +// m_owner->m_vars[i] = p (if in range); if (p) p->AddRef(); +// The range checks are CNodeArray::GetNode/SetNode. Theseus adds the ASSERTs. CMemberVarObject::CMemberVarObject(CInstance* pObject, int nMember) { ASSERT(pObject != NULL); @@ -220,7 +276,7 @@ CMemberVarObject::CMemberVarObject(CInstance* pObject, int nMember) m_obj = objMemberVar; m_owner = pObject; m_memberIndex = nMember; - m_owner->AddRef(); + pObject->AddRef(); } CMemberVarObject::~CMemberVarObject() @@ -232,26 +288,34 @@ CMemberVarObject::~CMemberVarObject() CObject* CMemberVarObject::Deref() { ASSERT(m_memberIndex >= 0 && m_memberIndex < m_owner->m_vars.GetLength()); - CObject* pObj = m_owner->m_vars.GetNode(m_memberIndex); - if (pObj != NULL) - pObj->AddRef(); + + CObject* pValue = m_owner->m_vars.GetNode(m_memberIndex); + if (pValue != NULL) + pValue->AddRef(); Release(); - return pObj; + return pValue; } void CMemberVarObject::Assign(CObject* pNewObject) { ASSERT(m_memberIndex >= 0 && m_memberIndex < m_owner->m_vars.GetLength()); - CObject* pOldObj = m_owner->m_vars.GetNode(m_memberIndex); - if (pOldObj != NULL) - pOldObj->Release(); + + CObject* pOld = m_owner->m_vars.GetNode(m_memberIndex); + if (pOld != NULL) + pOld->Release(); m_owner->m_vars.SetNode(m_memberIndex, (CNode*)pNewObject); if (pNewObject != NULL) pNewObject->AddRef(); } -// CMemberFunctionObject: reference to a scripted member function. - +// CMemberFunctionObject [type 14, vtable 0x22d68] +// Reconstructed from 5960 retail XBE @ 0x000397b0 (ctor) and @ 0x00039810 +// (Deref). Decompile: +// ctor: m_owner = owner; m_function = fn; (no AddRef) +// Deref: CRunner r(m_owner); r.ResetFunc(); r.m_ops = m_function->m_rgop; +// ret = r.Run(); this->Release(); return ret; +// Reading a method without calling it runs it on a fresh runner. The owner +// is a plain CObject in Theseus so non-instance nodes can hand these out. CMemberFunctionObject::CMemberFunctionObject(CObject* pOwner, CFunction* pFunction) { m_obj = objMemberFunction; @@ -259,19 +323,25 @@ CMemberFunctionObject::CMemberFunctionObject(CObject* pOwner, CFunction* pFuncti m_function = pFunction; } -CMemberFunctionObject::~CMemberFunctionObject() {} +CMemberFunctionObject::~CMemberFunctionObject() +{ +} CObject* CMemberFunctionObject::Deref() { - CRunner runner(m_owner); // CObject*, no cast needed + CRunner runner(m_owner); runner.SetFunc(m_function); - CObject* pObject = runner.Run(); + CObject* pResult = runner.Run(); Release(); - return pObject; + return pResult; } -// CFunctionObject: reference to a native function. - +// CFunctionObject [type 8, vtable 0x22cc0] +// Reconstructed from 5960 retail XBE @ 0x00039920 (ctor), 0x39960 (dtor), +// 0x399c0 (Call). Decompile: +// ctor: m_obj = 8; m_owner = NULL; +// dtor: if (m_owner) m_owner->Release(); +// Call: return m_owner->Call(m_functionIndex, rgparam, nParam); CFunctionObject::CFunctionObject() { m_obj = objFunctionRef; @@ -289,8 +359,11 @@ CObject* CFunctionObject::Call(CObject** rgparam, int nParam) return m_owner->Call(m_functionIndex, rgparam, nParam); } -// CFunction: compiled bytecode. - +// CFunction [type 13, vtable 0x22c18] +// Reconstructed from 5960 retail XBE: the constructor is inlined in +// CCompiler::CreateFunction @ 0x34670 (m_obj = 0xd, m_codeSize = 0) and the +// destructor @ 0x39b00 does nothing of its own. m_name is Theseus's debug +// name for the function, so Theseus frees it here. CFunction::CFunction() { m_obj = objFunction; @@ -300,33 +373,41 @@ CFunction::CFunction() CFunction::~CFunction() { - delete [] m_name; + delete[] m_name; } -// CMember: named member variable (class scope). - +// CMember [type 9, vtable 0x22dd8] +// Reconstructed from 5960 retail XBE: ctor inlined in +// CClassCompiler::ParseMemberVar @ 0x36ad0 (m_obj = 9, m_memberIndex = -1), +// dtor @ 0x39b30, Deref @ 0x39b40, Assign @ 0x39b90. Decompile: +// Deref: v = g_pThis->m_vars[i]; if (v) v->AddRef(); Release(); return v; +// Assign: old = g_pThis->m_vars[i]; if (old) old->Release(); +// g_pThis->m_vars[i] = p; if (p) p->AddRef(); +// Same as CMemberVarObject but the instance is whatever g_pThis is right +// now (LookupMember sets it). Theseus adds the ASSERTs. CMember::CMember() { m_obj = objMember; m_memberIndex = -1; } -CMember::~CMember() {} +CMember::~CMember() +{ +} CObject* CMember::Deref() { ASSERT(g_pThis != NULL); ASSERT(g_pThis->m_obj == objInstance); - CInstance* pThis = (CInstance*)g_pThis; - - ASSERT(m_memberIndex >= 0 && m_memberIndex < pThis->m_vars.GetLength()); + CInstance* pInstance = (CInstance*)g_pThis; + ASSERT(m_memberIndex >= 0 && m_memberIndex < pInstance->m_vars.GetLength()); - CObject* pObj = pThis->m_vars.GetNode(m_memberIndex); - if (pObj != NULL) - pObj->AddRef(); + CObject* pValue = pInstance->m_vars.GetNode(m_memberIndex); + if (pValue != NULL) + pValue->AddRef(); Release(); - return pObj; + return pValue; } void CMember::Assign(CObject* pNewValue) @@ -334,103 +415,120 @@ void CMember::Assign(CObject* pNewValue) ASSERT(g_pThis != NULL); ASSERT(g_pThis->m_obj == objInstance); - CInstance* pThis = (CInstance*)g_pThis; + CInstance* pInstance = (CInstance*)g_pThis; + ASSERT(m_memberIndex >= 0 && m_memberIndex < pInstance->m_vars.GetLength()); - ASSERT(m_memberIndex >= 0 && m_memberIndex < pThis->m_vars.GetLength()); - - CObject* pObj = pThis->m_vars.GetNode(m_memberIndex); - if (pObj != NULL) - pObj->Release(); - pThis->m_vars.SetNode(m_memberIndex, (CNode*)pNewValue); + CObject* pOld = pInstance->m_vars.GetNode(m_memberIndex); + if (pOld != NULL) + pOld->Release(); + pInstance->m_vars.SetNode(m_memberIndex, (CNode*)pNewValue); if (pNewValue != NULL) pNewValue->AddRef(); } +// CObject::Assign +// Reconstructed from 5960 retail XBE @ 0x00039900 (shared by CObject and +// CNode). Decompile: +// Error(L"Cannot assign to objects"); void CObject::Assign(CObject* pObject) { g_pRunner->Error(_T("Cannot assign to objects")); } -// CNodeArrayObject: script-accessible wrapper for CNodeArray. +// ========================================================================= +// Node arrays as script arrays +// ========================================================================= +// CNodeArrayObject constructor [type 7, vtable 0x22d30] +// Reconstructed from 5960 retail XBE: inlined in CProperty::Deref @ 0x38b00. +// m_obj = 7; m_nodeArray = (CNodeArray*)(node + prop offset); CNodeArrayObject::CNodeArrayObject(CNodeArray* pNodeArray) { m_obj = objNodeArray; m_nodeArray = pNodeArray; } +// CNodeArrayObject::Call +// Reconstructed from 5960 retail XBE @ 0x000391a0. Decompile: +// 0: return new CNumObject((float)m_nodeArray->m_length); +// 1: p = rgparam[0]->Deref(); p->AddRef(); m_nodeArray->AddNode(p); +// 2: p = rgparam[0]->Deref(); m_nodeArray->RemoveNode(p); p->Release(); +// return NULL; +// add keeps both the Deref reference and an extra one, so the array owns one. CObject* CNodeArrayObject::Call(int nFunction, CObject** rgparam, int nParam) { - switch (nFunction) - { - case 0: // length + if (nFunction == 0) return new CNumObject((float)m_nodeArray->GetLength()); - case 1: // add - { - CObject* pObj = rgparam[0]->Deref(); - pObj->AddRef(); - m_nodeArray->AddNode((CNode*)pObj); - } - break; - - case 2: // remove - { - CObject* pObj = rgparam[0]->Deref(); - m_nodeArray->RemoveNode((CNode*)pObj); - pObj->Release(); - } - break; + if (nFunction == 1) + { + CObject* pAdd = rgparam[0]->Deref(); + pAdd->AddRef(); + m_nodeArray->AddNode((CNode*)pAdd); + } + else if (nFunction == 2) + { + CObject* pRemove = rgparam[0]->Deref(); + m_nodeArray->RemoveNode((CNode*)pRemove); + pRemove->Release(); } return NULL; } +// CNodeArrayObject::Dot +// Reconstructed from 5960 retail XBE @ 0x00039260. Decompile: +// if (key->m_obj != objVariable) { +// i = (int)key->m_value; +// if (i < 0 || i >= m_nodeArray->m_length) +// { Error(L"Array reference out of bounds"); return 0; } +// n = m_nodeArray->m_rgpNode[i]; n->AddRef(); return n; } +// "length"(6) / "add"(3) / "remove"(6) -> new CFunctionObject +// { m_functionIndex = 0 / 1 / 2, m_owner = this }; this->AddRef(); +// else return CObject::Dot(key); CObject* CNodeArrayObject::Dot(CObject* pObj) { - if (pObj->m_obj == objVariable) + if (pObj->m_obj != objVariable) { - CVarObject* pVar = (CVarObject*)pObj; - - int nFunc = -1; - if (pVar->m_length == 6 && _tcsncmp(pVar->m_text, _T("length"), pVar->m_length) == 0) - nFunc = 0; - else if (pVar->m_length == 3 && _tcsncmp(pVar->m_text, _T("add"), pVar->m_length) == 0) - nFunc = 1; - else if (pVar->m_length == 6 && _tcsncmp(pVar->m_text, _T("remove"), pVar->m_length) == 0) - nFunc = 2; - - if (nFunc >= 0) - { - CFunctionObject* pFun = new CFunctionObject; - pFun->m_functionIndex = nFunc; - pFun->m_owner = this; - AddRef(); - return pFun; - } - } - else - { - // Array indexing - int n = (int)((CNumObject*)pObj)->m_value; - if (n < 0 || n >= m_nodeArray->GetLength()) + int iItem = (int)((CNumObject*)pObj)->m_value; + if (iItem < 0 || iItem >= m_nodeArray->GetLength()) { g_pRunner->Error(_T("Array reference out of bounds")); return NULL; } - CNode* pNode = m_nodeArray->GetNode(n); - pNode->AddRef(); - return pNode; + CNode* pItem = m_nodeArray->GetNode(iItem); + pItem->AddRef(); + return pItem; } - return CObject::Dot(pObj); + CVarObject* pName = (CVarObject*)pObj; + int nFunction = -1; + if (pName->m_length == 6 && _tcsncmp(pName->m_text, _T("length"), 6) == 0) + nFunction = 0; + else if (pName->m_length == 3 && _tcsncmp(pName->m_text, _T("add"), 3) == 0) + nFunction = 1; + else if (pName->m_length == 6 && _tcsncmp(pName->m_text, _T("remove"), 6) == 0) + nFunction = 2; + + if (nFunction < 0) + return CObject::Dot(pObj); + + CFunctionObject* pRef = new CFunctionObject; + pRef->m_functionIndex = nFunction; + pRef->m_owner = this; + AddRef(); + return pRef; } // ========================================================================= -// Section 2: Property system. Bridges XAP scripts to C++ node properties. +// Node properties seen from script // ========================================================================= +// CProperty constructor / destructor [vtable 0x22e10] +// Reconstructed from 5960 retail XBE @ 0x00038a20 and @ 0x00038aa0. Decompile: +// ctor: m_node = node; m_pprd = prd; node->AddRef(); +// dtor: m_node->Release(); CProperty::CProperty(CObject* pNode, const PRD* pprd) { m_node = pNode; @@ -443,62 +541,72 @@ CProperty::~CProperty() m_node->Release(); } +// CProperty::Deref +// Reconstructed from 5960 retail XBE @ 0x00038b00. Reads the field at +// m_node + prd offset and wraps it by type: integer / boolean / number become +// a CNumObject, vec3 a CVec3Object, node is returned AddRef'd as is, string +// becomes a CStrObject, children and nodearray become a CNodeArrayObject over +// the field itself. Any other type: Error(L"Unknown property type"). The +// property reference is released either way. Theseus reads the field before +// the release so the value can't go away under it. CObject* CProperty::Deref() { - void* pvValue = (BYTE*)m_node + (int)m_pprd->pbOffset; + void* pvField = (BYTE*)m_node + (int)m_pprd->pbOffset; + CObject* pValue; switch (m_pprd->nType) { - case pt_string: - { - CStrObject* pStr = new CStrObject(*(TCHAR**)pvValue); - Release(); - return pStr; - } - case pt_number: - { - float n = *(float*)pvValue; - Release(); - return new CNumObject(n); - } case pt_integer: - { - int n = *(int*)pvValue; - Release(); - return new CNumObject((float)n); - } + pValue = new CNumObject((float)*(int*)pvField); + break; + case pt_boolean: - { - bool n = *(bool*)pvValue; - Release(); - return new CNumObject((float)n); - } - case pt_node: - { - CNode* pNode = *(CNode**)pvValue; - if (pNode != NULL) pNode->AddRef(); - Release(); - return pNode; - } - case pt_vec3: - { - D3DXVECTOR3 v = *(D3DXVECTOR3*)pvValue; - Release(); - return new CVec3Object(v); - } - case pt_nodearray: + pValue = new CNumObject((float)*(bool*)pvField); + break; + + case pt_number: + pValue = new CNumObject(*(float*)pvField); + break; + case pt_children: - { - CNodeArrayObject* pRet = new CNodeArrayObject((CNodeArray*)pvValue); - Release(); - return pRet; - } + case pt_nodearray: + pValue = new CNodeArrayObject((CNodeArray*)pvField); + break; + + case pt_vec3: + pValue = new CVec3Object(*(D3DXVECTOR3*)pvField); + break; + + case pt_node: + pValue = *(CNode**)pvField; + if (pValue != NULL) + pValue->AddRef(); + break; + + case pt_string: + pValue = new CStrObject(*(TCHAR**)pvField); + break; + + default: + g_pRunner->Error(_T("Unknown property type")); + return NULL; } - g_pRunner->Error(_T("Unknown property type")); - return NULL; + Release(); + return pValue; } +// CProperty::Assign +// Reconstructed from 5960 retail XBE @ 0x00038d70. Decompile: +// if (!value && type != pt_node) { Error(L"Illegal null assignement"); return; } +// integer: n = value->Deref()->ToNum(); SetProperty(prd, &(int)n->m_value); +// boolean: b = n->m_value != 0; SetProperty(prd, &b); +// number: SetProperty(prd, &n->m_value); (then n->Release()) +// vec3: v = value->Deref(); if (v->m_obj == objVec3) SetProperty(prd, &v->m_x); +// node: p = value ? value->Deref() : NULL; SetProperty(prd, &p); +// string: s = value->Deref()->ToStr(); SetProperty(prd, &s->m_text); s->Release(); +// "assignement" is the binary's spelling. Theseus passes the byte count to +// SetProperty and traces when a vec3 gets something else. void CProperty::Assign(CObject* pObject) { if (pObject == NULL && m_pprd->nType != pt_node) @@ -509,57 +617,64 @@ void CProperty::Assign(CObject* pObject) switch (m_pprd->nType) { - case pt_string: - { - CStrObject* pStr = pObject->Deref()->ToStr(); - const TCHAR* sz = pStr->GetSz(); - m_node->SetProperty(m_pprd, &sz, pStr->GetLength()); - pStr->Release(); - } - break; - case pt_number: + case pt_integer: { CNumObject* pNum = pObject->Deref()->ToNum(); - m_node->SetProperty(m_pprd, &pNum->m_value, sizeof(float)); + int nValue = (int)pNum->m_value; + m_node->SetProperty(m_pprd, &nValue, sizeof(int)); pNum->Release(); } break; - case pt_integer: + + case pt_boolean: { CNumObject* pNum = pObject->Deref()->ToNum(); - int n = (int)pNum->m_value; - m_node->SetProperty(m_pprd, &n, sizeof(int)); + bool bValue = pNum->m_value != 0.0f; + m_node->SetProperty(m_pprd, &bValue, sizeof(bool)); pNum->Release(); } break; - case pt_boolean: + + case pt_number: { CNumObject* pNum = pObject->Deref()->ToNum(); - bool b = pNum->m_value != 0.0f; - m_node->SetProperty(m_pprd, &b, sizeof(bool)); + m_node->SetProperty(m_pprd, &pNum->m_value, sizeof(float)); pNum->Release(); } break; + case pt_vec3: { CVec3Object* pVec = (CVec3Object*)pObject->Deref(); - if (pVec->m_obj != objVec3) - TRACE(_T("\001Expected a Vector3 object!\n")); - else + if (pVec->m_obj == objVec3) m_node->SetProperty(m_pprd, &pVec->m_x, sizeof(float) * 3); + else + TRACE(_T("\001Expected a Vector3 object!\n")); } break; + case pt_node: { - CNode* pNode = NULL; - if (pObject != NULL) - pNode = (CNode*)pObject->Deref(); + CNode* pNode = (pObject != NULL) ? (CNode*)pObject->Deref() : NULL; m_node->SetProperty(m_pprd, &pNode, sizeof(CNode*)); } break; + + case pt_string: + { + CStrObject* pStr = pObject->Deref()->ToStr(); + const TCHAR* szValue = pStr->GetSz(); + m_node->SetProperty(m_pprd, &szValue, pStr->GetLength()); + pStr->Release(); + } + break; } } +// CNode::Dot / CNode::ToStr [CNode vtable 0x219f8 slots 12 and 8] +// Reconstructed from 5960 retail XBE @ 0x00038f30 and @ 0x00038f40. Decompile: +// Dot: return CObject::Dot(key); +// ToStr: return new CStrObject(GetNodeClass()->m_className); CObject* CNode::Dot(CObject* pObj) { return CObject::Dot(pObj); @@ -571,49 +686,62 @@ CStrObject* CNode::ToStr() } // ========================================================================= -// Section 3: Helper functions +// Free helpers // ========================================================================= +// Dereference +// Reconstructed from 5960 retail XBE @ 0x000372e0. Decompile: +// while (p) { q = p->Deref(); if (q == p) break; p = q; } return p; +// Keeps following references until something answers with itself. CObject* Dereference(CObject* pObject) { - CObject* pLast = pObject; while (pObject != NULL) { - pObject = pLast->Deref(); - if (pObject == pLast) + CObject* pNext = pObject->Deref(); + if (pNext == pObject) break; - pLast = pObject; + pObject = pNext; } return pObject; } +// LookupClass +// Reconstructed from 5960 retail XBE: inlined at the top of CreateNewObject +// @ 0x37580 as g_classes->Lookup(name) (0x32ca0) plus an objClass check. CClass* LookupClass(const TCHAR* pchName, int cchName) { extern CNameSpace* g_classes; - CObject* pObject = g_classes->Lookup(pchName, cchName); - if (pObject == NULL || pObject->m_obj != objClass) - return NULL; - return (CClass*)pObject; + CObject* pFound = g_classes->Lookup(pchName, cchName); + if (pFound != NULL && pFound->m_obj == objClass) + return (CClass*)pFound; + return NULL; } +// CreateNewObject +// Reconstructed from 5960 retail XBE @ 0x00037580. Decompile: +// g_rgParam = rgparam; g_nParam = nParam; +// c = g_classes->Lookup(name); if (c && c->m_obj == objClass) return c->CreateNode(); +// nc = CNodeClass::FindByName(name); if (nc && (o = nc->CreateNode())) return o; +// return 0; +// The constructor args ride in g_rgParam / g_nParam for Date, Array and friends. CObject* CreateNewObject(const TCHAR* pchClassName, int cchClassName, CObject** rgparam, int nParam) { g_rgParam = rgparam; g_nParam = nParam; - // Check scripted classes first - CClass* pClass = LookupClass(pchClassName, cchClassName); - if (pClass != NULL) - return pClass->CreateNode(); + CClass* pScripted = LookupClass(pchClassName, cchClassName); + if (pScripted != NULL) + return pScripted->CreateNode(); - // Try built-in node types - CObject* pNode = NewNode(pchClassName, cchClassName); - if (pNode != NULL) - return pNode; - - return NULL; + return NewNode(pchClassName, cchClassName); } +// ExecuteScript +// Reconstructed from 5960 retail XBE @ 0x00036fb0. Decompile: +// CFunctionCompiler c; c.ParseBlock(sz); if (c.m_bError) return false; +// c.Write(opRet); f = c.CreateFunction(); +// CRunner r(obj); r.ResetFunc(); r.m_ops = f->m_rgop; r.Run(); +// delete f; return true; bool ExecuteScript(CObject* pObject, const TCHAR* szScript) { CFunctionCompiler compiler; @@ -622,19 +750,27 @@ bool ExecuteScript(CObject* pObject, const TCHAR* szScript) return false; compiler.Write(opRet); - CFunction* pFunction = compiler.CreateFunction(); + CFunction* pScript = compiler.CreateFunction(); CRunner runner(pObject); - runner.SetFunc(pFunction); + runner.SetFunc(pScript); runner.Run(); - delete pFunction; + delete pScript; return true; } +// CallFunction +// Reconstructed from 5960 retail XBE @ 0x000371a0. Decompile: +// g_pThis = obj; CRunner r(obj); r.ResetFunc(); +// if (!(r.m_ops = r.LookupFunction(name, wcslen(name), r.m_self))) return false; +// for (i = 0; i < n; i++) r.m_stack[r.m_sp++] = rgParam[i]; +// r.Run(); return true; +// The arguments go on the stack as is; the caller's references move with them. bool CallFunction(CObject* pObject, const TCHAR* szFunc, int nParam, CObject** rgParam) { g_pThis = pObject; + CRunner runner(pObject); if (!runner.SetFunc(szFunc)) return false; @@ -646,95 +782,96 @@ bool CallFunction(CObject* pObject, const TCHAR* szFunc, int nParam, CObject** r return true; } -// ========================================================================= -// Section 4: Member lookup. Resolves names through the scene hierarchy. -// ========================================================================= - +// FindMember [no binary symbol; LookupMember @ 0x399e0 does this inline] +// for (c = cls; c; c = c->m_baseClass) if ((m = c->GetMember(name))) return m; +// node_system.cpp uses it to find member functions too. CObject* FindMember(CNodeClass* pClass, const TCHAR* pchName, int cchName) { - for ( ; pClass != NULL; pClass = pClass->m_baseClass) + while (pClass != NULL) { - CObject* pObj = pClass->GetMember(pchName, cchName); - if (pObj != NULL) - return pObj; + CObject* pMember = pClass->GetMember(pchName, cchName); + if (pMember != NULL) + return pMember; + pClass = pClass->m_baseClass; } return NULL; } +// LookupMember +// Reconstructed from 5960 retail XBE @ 0x000399e0. Decompile: +// g_pThis = obj; +// if (obj->m_obj == objNode && (prd = obj->FindProp(name))) +// { p = new CProperty(obj, prd); p->m_nRefCount = 0; return p; } +// if ((m = obj->GetMember(name))) return m; +// for (c = obj->GetNodeClass(); c; c = c->m_baseClass) +// if ((m = c->GetMember(name))) return m; +// for (p = obj->m_parent; p; p = p->m_parent) +// if ((m = LookupMember(p, name))) return m; +// return 0; +// Order is: node property, own members, class chain, then each ancestor. +// The CProperty starts at zero refs; the caller's AddRef is the first one. CObject* LookupMember(CObject* pThis, const TCHAR* pchName, int cchName) { -#ifdef _DEBUG0 - TCHAR szBuf [256]; - int cch = cchName; - if (cch > 255) - cch = 255; - CopyChars(szBuf, pchName, cch); - szBuf[cch] = 0; - TRACE(_T("LookupMember: 0x%08x '%s'\n"), pThis, szBuf); -#endif - ASSERT(pThis != NULL); g_pThis = pThis; - // Check node properties if (pThis->m_obj == objNode) { CNode* pNode = (CNode*)pThis; const PRD* pprd = pNode->FindProp(pchName, cchName); if (pprd != NULL) { - CObject* pObj = new CProperty(pNode, pprd); - pObj->m_nRefCount = 0; - return pObj; + CObject* pProp = new CProperty(pNode, pprd); + pProp->m_nRefCount = 0; + return pProp; } } - // Check instance members - { - CObject* pObject = pThis->GetMember(pchName, cchName); - if (pObject != NULL) - return pObject; - } + CObject* pFound = pThis->GetMember(pchName, cchName); + if (pFound != NULL) + return pFound; - // Check class hierarchy - { - CObject* pObj = FindMember(pThis->GetNodeClass(), pchName, cchName); - if (pObj != NULL) - return pObj; - } + pFound = FindMember(pThis->GetNodeClass(), pchName, cchName); + if (pFound != NULL) + return pFound; - // Walk parent chain + for (CObject* pAncestor = pThis->m_parent; pAncestor != NULL; pAncestor = pAncestor->m_parent) { - for (CObject* pObject = pThis->m_parent; pObject != NULL; pObject = pObject->m_parent) - { - CObject* pObj = LookupMember(pObject, pchName, cchName); - if (pObj != NULL) - return pObj; - } + pFound = LookupMember(pAncestor, pchName, cchName); + if (pFound != NULL) + return pFound; } return NULL; } // ========================================================================= -// Section 5: CRunner, the bytecode interpreter. +// CRunner setup and the operand stack // ========================================================================= +// CRunner::CRunner / ~CRunner +// Reconstructed from 5960 retail XBE: the runner is built on the stack in +// CallFunction @ 0x371a0, ExecuteScript @ 0x36fb0 and +// CMemberFunctionObject::Deref @ 0x39810, always the same way: +// m_sp = m_nop = 0; m_ops = NULL; m_self = m_thisRef = obj; +// m_bError = false; m_nextContext = NULL; zero the stack; +// and torn down with ResetFunc. 5960's stack holds 100 entries; Theseus's +// holds 256 (see runner.h). CRunner::CRunner(CObject* pObject) { ASSERT(pObject != NULL); + m_wakeup = 0.0f; m_nop = 0; m_ops = NULL; m_sp = 0; - m_spBase = 0; m_spFrame = 0; + m_spBase = 0; m_self = pObject; m_thisRef = pObject; m_bError = false; m_nextContext = NULL; - m_wakeup = 0.0f; ZeroMemory(m_stack, sizeof(m_stack)); } @@ -743,6 +880,10 @@ CRunner::~CRunner() ResetFunc(); } +// CRunner::SetFunc [inlined at each runner site in 5960] +// By name: ResetFunc, then m_ops = LookupFunction(name) (see CallFunction). +// By function: ResetFunc, then m_ops = fn->m_rgop with nop/sp/frame at 0. +// Theseus also clears the line number and the sleep deadline here. bool CRunner::SetFunc(const TCHAR* szFunc) { ResetFunc(); @@ -753,39 +894,57 @@ bool CRunner::SetFunc(const TCHAR* szFunc) void CRunner::SetFunc(CFunction* pFunction) { ResetFunc(); + m_ops = pFunction->m_rgop; m_nop = 0; m_sp = 0; m_spFrame = 0; - m_ops = pFunction->m_rgop; m_nLine = 0; m_wakeup = 0.0f; } +// CRunner::ResetFunc +// Reconstructed from 5960 retail XBE @ 0x00037400. Decompile: +// while (m_sp) { p = m_stack[--m_sp]; m_stack[m_sp] = 0; if (p) p->Release(); } +// m_bError = 0; m_nop = 0; m_wakeup = 0.0; m_sp = 0; m_spFrame = 0; void CRunner::ResetFunc() { - while (m_sp > 0) + while (m_sp != 0) { m_sp--; - CObject* pObj = m_stack[m_sp]; + CObject* pSlot = m_stack[m_sp]; m_stack[m_sp] = NULL; - if (pObj != NULL) - pObj->Release(); + if (pSlot != NULL) + pSlot->Release(); } + m_bError = false; m_nop = 0; + m_wakeup = 0.0f; m_sp = 0; m_spFrame = 0; - m_wakeup = 0.0f; } -// Stack operations +// CRunner::Push family +// Reconstructed from 5960 retail XBE @ 0x00037450 / 0x374b0 (numbers), +// 0x37510 (string). Decompile: +// m_stack[m_sp++] = new CNumObject(n); +// if (cch == -1) cch = wcslen(sz); m_stack[m_sp++] = new CStrObject(sz, cch); +// The object form is inlined everywhere as m_stack[m_sp++] = p. Theseus +// refuses to run off the end of the stack. +void CRunner::Push(float nValue) +{ + Push(new CNumObject(nValue)); +} -void CRunner::Push(float nValue) { Push(new CNumObject(nValue)); } -void CRunner::Push(int nValue) { Push(new CNumObject((float)nValue)); } +void CRunner::Push(int nValue) +{ + Push(new CNumObject((float)nValue)); +} void CRunner::Push(const TCHAR* szValue, int nLen) { - if (nLen == -1) nLen = _tcslen(szValue); + if (nLen == -1) + nLen = _tcslen(szValue); Push(new CStrObject(szValue, nLen)); } @@ -800,31 +959,42 @@ void CRunner::Push(CObject* pObject) m_stack[m_sp++] = pObject; } +// CRunner::Pop [inlined in 5960 as p = m_stack[--m_sp]; m_stack[m_sp] = 0] CObject* CRunner::Pop() { ASSERT(m_sp > 0); m_sp--; - CObject* pObject = m_stack[m_sp]; + CObject* pTop = m_stack[m_sp]; m_stack[m_sp] = NULL; - return pObject; + return pTop; } +// CRunner::Error +// Reconstructed from 5960 retail XBE @ 0x000375e0. Decompile: +// if (m_bError) return; +// vswprintf(msg, 512, fmt, args); +// if (m_nLine == 0) swprintf(out, , msg); +// else swprintf(out, , m_nLine, msg); +// m_bError = 1; +// Only the first error of a run is reported; Run stops on the flag. Retail +// builds the text and shows it nowhere. Theseus traces it, dumps the stack +// in debug, and pops a message box on desktop. void CRunner::Error(const TCHAR* szFmt, ...) { if (m_bError) return; + TCHAR szDetail[512]; va_list args; va_start(args, szFmt); - - TCHAR szBuffer[512]; - _vsntprintf(szBuffer, countof(szBuffer), szFmt, args); + _vsntprintf(szDetail, countof(szDetail), szFmt, args); + va_end(args); TCHAR szMessage[1024]; - if (m_nLine == 0) - _stprintf(szMessage, _T("Runtime Error\n\n%s"), szBuffer); + if (m_nLine != 0) + _stprintf(szMessage, _T("Runtime Error\n\nLine: %d\n\n%s"), m_nLine, szDetail); else - _stprintf(szMessage, _T("Runtime Error\n\nLine: %d\n\n%s"), m_nLine, szBuffer); + _stprintf(szMessage, _T("Runtime Error\n\n%s"), szDetail); Trace(_T("\001\007%s\n"), szMessage); @@ -837,82 +1007,99 @@ void CRunner::Error(const TCHAR* szFmt, ...) TheseusMessageBox(szMessage, 0); #endif - va_end(args); m_bError = true; } -// Context stack for nested calls - +// CRunner::PushContext +// Reconstructed from 5960 retail XBE @ 0x00037690. Decompile: +// c = new RUNCONTEXT; c->m_ops = m_ops; c->m_nop = m_nop; +// c->m_spFrame = m_spFrame; c->m_self = m_self; c->m_thisRef = m_thisRef; +// c->m_sp = m_sp - nParam - 1; c->m_next = m_nextContext; m_nextContext = c; +// The saved sp sits under the callee and its arguments, so returning drops both. void CRunner::PushContext(UINT nParam) { - RUNCONTEXT* pCtx = new RUNCONTEXT; - pCtx->m_ops = m_ops; - pCtx->m_nop = m_nop; - pCtx->m_spFrame = m_spFrame; - pCtx->m_self = m_self; - pCtx->m_thisRef = m_thisRef; - pCtx->m_sp = m_sp - (nParam + 1); - pCtx->m_nextContext = m_nextContext; - m_nextContext = pCtx; + RUNCONTEXT* pSaved = new RUNCONTEXT; + pSaved->m_ops = m_ops; + pSaved->m_nop = m_nop; + pSaved->m_spFrame = m_spFrame; + pSaved->m_self = m_self; + pSaved->m_thisRef = m_thisRef; + pSaved->m_sp = m_sp - nParam - 1; + pSaved->m_nextContext = m_nextContext; + m_nextContext = pSaved; } +// CRunner::PopContext +// Reconstructed from 5960 retail XBE @ 0x000376e0. Decompile: +// c = m_nextContext; ret = Dereference(Pop()); +// m_ops/m_nop/m_spFrame/m_self = c->...; g_pThis = m_thisRef = c->m_thisRef; +// for (i = c->m_sp; i < m_sp; i++) if (m_stack[i]) { m_stack[i]->Release(); m_stack[i] = 0; } +// m_sp = c->m_sp; m_nextContext = c->m_next; delete c; m_stack[m_sp++] = ret; void CRunner::PopContext() { - RUNCONTEXT* pCtx = m_nextContext; - ASSERT(pCtx != NULL); + RUNCONTEXT* pSaved = m_nextContext; + ASSERT(pSaved != NULL); - CObject* pRetObject = Dereference(Pop()); + CObject* pResult = Dereference(Pop()); - m_ops = pCtx->m_ops; - m_nop = pCtx->m_nop; - m_spFrame = pCtx->m_spFrame; - m_self = pCtx->m_self; - m_thisRef = pCtx->m_thisRef; + m_ops = pSaved->m_ops; + m_nop = pSaved->m_nop; + m_spFrame = pSaved->m_spFrame; + m_self = pSaved->m_self; + m_thisRef = pSaved->m_thisRef; g_pThis = m_thisRef; - UINT i; - for (i = pCtx->m_sp; i < m_sp; i++) + for (UINT iSlot = pSaved->m_sp; iSlot < m_sp; iSlot++) { - if (m_stack[i] != NULL) + if (m_stack[iSlot] != NULL) { - m_stack[i]->Release(); - m_stack[i] = NULL; + m_stack[iSlot]->Release(); + m_stack[iSlot] = NULL; } } - m_sp = pCtx->m_sp; - m_nextContext = pCtx->m_nextContext; - delete pCtx; + m_sp = pSaved->m_sp; + m_nextContext = pSaved->m_nextContext; + delete pSaved; - Push(pRetObject); + Push(pResult); } -// Main execution loop - +// CRunner::Run +// Reconstructed from 5960 retail XBE @ 0x000377b0. Decompile: +// m_spBase = m_sp; +// while (!m_bError) if (!Step(&ret)) break; +// Theseus hands back whatever the outermost return left (NULL on error) and +// checks in debug that nothing above the stack top was left behind. CObject* CRunner::Run() { m_spBase = m_sp; - for (;;) - { - if (m_bError) return NULL; + while (!m_bError) + { #ifdef _DEBUG - // Make sure everything on the stack has been cleaned up! - { - for (UINT i = 0; i < 20; i += 1) - ASSERT(m_stack[m_sp + i] == NULL); - } + for (UINT i = 0; i < 20; i++) + ASSERT(m_stack[m_sp + i] == NULL); #endif - CObject* pRetObj = NULL; - if (!Step(&pRetObj)) - return pRetObj; + CObject* pResult = NULL; + if (!Step(&pResult)) + return pResult; } + + return NULL; } // ========================================================================= -// Section 6: Opcode interpreter +// One instruction // ========================================================================= +// CRunner::Step +// Reconstructed from 5960 retail XBE @ 0x00037800 (1880 bytes, one switch on +// the opcode byte). Sets g_pRunner, fetches the opcode and runs it; returns +// false to stop (outermost return, error, or an opcode it doesn't know). +// Each case is described next to its code below; the full write-up with the +// decompile excerpts is in docs/decomp/VM.md. Theseus adds the stack size +// check on opFrame and the null check on opCall. bool CRunner::Step(CObject** ppRetObj) { g_pRunner = this; @@ -920,467 +1107,485 @@ bool CRunner::Step(CObject** ppRetObj) BYTE op = m_ops[m_nop++]; switch (op) { - default: - ASSERT(FALSE); // Lost in space! - return NULL; - - case opSleep: - { - CObject* pObj = Dereference(Pop()); - if (pObj == NULL) { Error(_T("illegal null reference")); return false; } - if (pObj->m_obj != objNumber) { Error(_T("expected a number")); return false; } - m_wakeup = TheseusGetNow() + ((CNumObject*)pObj)->m_value; - pObj->Release(); - return true; - } - - case opStatement: - m_nLine = FetchInt(); - LOCALTRACE(_T("opStatement: line %d, sp %d\n"), m_nLine, m_sp); - break; - - case opDrop: - { - LOCALTRACE(_T("opDrop\n")); - CObject* pObject = Pop(); - if (pObject != NULL) pObject->Release(); - } - break; - - case opFrame: - { - int nFrameSize = FetchInt(); - LOCALTRACE(_T("opFrame: %d\n"), nFrameSize); - - if (nFrameSize < 0 || m_sp + (UINT)nFrameSize > countof(m_stack)) - { - Error(_T("script stack overflow")); - return false; - } - -#ifdef _DEBUG - // Make sure everything on the stack has been cleaned up! - { - for (UINT i = m_sp; i < m_sp + nFrameSize; i += 1) - ASSERT(m_stack[i] == NULL); - } -#endif - m_sp += nFrameSize; - m_spBase = m_sp; - } - break; - - case opEndFrame: - { - int nFrameSize = FetchInt(); - LOCALTRACE(_T("opEndFrame: %d\n"), nFrameSize); - while (nFrameSize-- > 0) - { - m_sp--; - if (m_stack[m_sp] != NULL) - { - m_stack[m_sp]->Release(); - m_stack[m_sp] = NULL; - } - } - m_spBase = m_sp; - } - break; - + // Push null. case opNull: Push((CObject*)NULL); - break; - - case opThis: - ASSERT(m_self != NULL); - m_self->AddRef(); - Push(m_self); - break; + return true; + // int nParam, string class. Build it with the arguments sitting on the + // stack, release them, and leave the new object in their place. case opNew: { - LOCALTRACE(_T("opNew\n")); int nParam = FetchInt(); - int cch; - const TCHAR* pch = FetchString(cch); + int cchClass; + const TCHAR* pchClass = FetchString(cchClass); CObject** rgparam = &m_stack[m_sp - nParam]; - CObject* pObj = CreateNewObject(pch, cch, rgparam, nParam); - if (pObj == NULL) + CObject* pNew = CreateNewObject(pchClass, cchClass, rgparam, nParam); + if (pNew == NULL) { - TCHAR szBuf[256]; - if (cch > 255) cch = 255; - CopyChars(szBuf, pch, cch); - szBuf[cch] = '\0'; - Error(_T("Failed to create new object: \"%s\""), szBuf); - return NULL; + TCHAR szClass[256]; + if (cchClass > 255) + cchClass = 255; + CopyChars(szClass, pchClass, cchClass); + szClass[cchClass] = '\0'; + Error(_T("Failed to create new object: \"%s\""), szClass); + return false; } for (int i = 0; i < nParam; i++) { - if (rgparam[i] != NULL) { rgparam[i]->Release(); rgparam[i] = NULL; } + if (rgparam[i] != NULL) + { + rgparam[i]->Release(); + rgparam[i] = NULL; + } } m_sp -= nParam; - Push(pObj); + Push(pNew); } - break; + return true; - case opAssign: + // string name. Push an unresolved name; it binds when dereferenced. + case opVar: { - LOCALTRACE(_T("opAssign\n")); - CObject* pRight = Dereference(Pop()); - CObject* pLeft = Pop(); - if (pLeft == NULL) { Error(_T("cannot assign to null")); return NULL; } - pLeft->Assign(pRight); - pLeft->Release(); - Push(pRight); + int cchName; + const TCHAR* pchName = FetchString(cchName); + Push(new CVarObject(pchName, cchName)); } - break; - - case opDot: - case opArray: - { - LOCALTRACE(_T("%d %s: \"%s\"\n"), m_nop - 1, op == opDot ? "opDot" : "opArray"); - - CObject* pRight = Pop(); - CObject* pLeft = Pop(); - - if (pRight == NULL || pLeft == NULL) { Error(_T("illegal null reference")); return NULL; } - - if (op == opArray) - pRight = Dereference(pRight); - - ASSERT(pRight->m_obj == objVariable || pRight->m_obj == objNumber || pRight->m_obj == objString); + return true; - CObject* pRealLeft = pLeft->Deref(); - if (pRealLeft == NULL) { Error(_T("illegal null reference")); return NULL; } + // uint target. + case opJump: + m_nop = FetchUInt(); + return true; - CObject* pDot = pRealLeft->Dot(pRight); - if (pDot == NULL) + // uint target. Pop a value; true (nonzero number) falls through, false + // or null jumps. + case opCond: + { + bool bTrue = false; + CObject* pTest = Dereference(Pop()); + if (pTest != NULL) { - if (pRight->m_obj == objVariable) - Error(_T("Unknown member: %s"), ((CVarObject*)pRight)->m_text); - else - { - CStrObject* pStr = pRight->ToStr(); - Error(_T("Unknown member: %s"), pStr->GetSz()); - pStr->Release(); - } + CNumObject* pNum = pTest->ToNum(); + bTrue = pNum->m_value != 0.0f; + pNum->Release(); + pTest->Release(); } - Push(pDot); - pRealLeft->Release(); - pRight->Release(); - } - break; - - case opNeg: - { - CObject* pObj = Dereference(Pop()); - if (pObj == NULL || pObj->m_obj != objNumber) { Error(_T("expected a number")); return NULL; } - Push(-((CNumObject*)pObj)->m_value); - pObj->Release(); + if (bTrue) + m_nop += sizeof(UINT); + else + m_nop = FetchUInt(); } - break; - - case opAdd: case opSub: case opMul: case opDiv: case opMod: - case opEQ: case opNE: case opLT: case opLE: case opGT: case opGE: - case opAnd: case opXor: case opOr: case opSHL: case opSHR: - if (!BinaryOperator(op)) - return NULL; - break; + return true; + // byte nParam. The callee sits under its arguments. A native function + // ref runs right here; a bound method or a named script function gets a + // new frame; a bare name tries the built-ins first, then the script + // function lookup. case opCall: { - LOCALTRACE(_T("opCall sp=%d\n"), m_sp); - int nParam = m_ops[m_nop++]; ASSERT(m_sp >= (UINT)(nParam + 1)); - CObject* pFun = (CVarObject*)m_stack[m_sp - (nParam + 1)]; + CObject** ppCallee = &m_stack[m_sp - nParam - 1]; CObject** rgparam = &m_stack[m_sp - nParam]; + CObject* pCallee = *ppCallee; - if (pFun == NULL) { Error(_T("call on null")); return NULL; } - - if (pFun->m_obj == objFunctionRef) + // Theseus: calling null is an error instead of a crash. + if (pCallee == NULL) { - m_stack[m_sp - (nParam + 1)] = NULL; + Error(_T("call on null")); + return false; + } - CFunctionObject* pFunction = (CFunctionObject*)pFun; - CObject* pRetObj = pFunction->Call(rgparam, nParam); - pFun->Release(); + if (pCallee->m_obj == objFunctionRef) + { + *ppCallee = NULL; + CObject* pResult = ((CFunctionObject*)pCallee)->Call(rgparam, nParam); + pCallee->Release(); for (int i = 0; i < nParam; i++) { - if (rgparam[i] != NULL) { rgparam[i]->Release(); rgparam[i] = NULL; } + if (rgparam[i] != NULL) + { + rgparam[i]->Release(); + rgparam[i] = NULL; + } } m_sp -= nParam + 1; - LOCALTRACE(_T("opCall(done) sp=%d\n"), m_sp); - Push(pRetObj); + Push(pResult); + return true; } - else if (pFun->m_obj == objMemberFunction) - { - CMemberFunctionObject* pFunction = (CMemberFunctionObject*)pFun; - pFunction->AddRef(); -#ifdef _DEBUG - LOCALTRACE(_T("Call2 %s\n"), pFunction->m_function->m_name); -#endif + if (pCallee->m_obj == objMemberFunction) + { + CMemberFunctionObject* pMethod = (CMemberFunctionObject*)pCallee; + pMethod->AddRef(); PushContext(nParam); - m_spFrame = m_sp - nParam; - - ASSERT(pFunction->m_owner != NULL); - m_self = pFunction->m_owner; - m_ops = pFunction->m_function->m_rgop; + ASSERT(pMethod->m_owner != NULL); + m_self = pMethod->m_owner; + m_ops = pMethod->m_function->m_rgop; m_nop = 0; + return true; } - else - { - CVarObject* pFunction = (CVarObject*)pFun; - int cch = pFunction->m_length; - const TCHAR* pch = pFunction->m_text; - CObject* pRetObj = NULL; - if (ExecuteBuiltIn(pch, cch, nParam, rgparam, pRetObj)) - { - for (int i = 0; i < nParam; i++) - rgparam[i] = NULL; - m_sp -= nParam + 1; - Push(pRetObj); - pFun->Release(); - } - else - { - CObject* pThis; - BYTE* pop = LookupFunction(pch, cch, pThis); - if (pop == NULL) - { - TCHAR szFunc[32]; - if (cch > countof(szFunc) - 1) cch = countof(szFunc) - 1; - _tcsncpy(szFunc, pch, cch); - szFunc[cch] = 0; - Error(_T("unknown function: %s"), szFunc); - return NULL; - } + CVarObject* pName = (CVarObject*)pCallee; + CObject* pResult = NULL; + if (ExecuteBuiltIn(pName->m_text, pName->m_length, nParam, rgparam, pResult)) + { + for (int i = 0; i < nParam; i++) + rgparam[i] = NULL; + m_sp -= nParam + 1; + Push(pResult); + pCallee->Release(); + return true; + } - PushContext(nParam); - m_self = pThis; - g_pThis = pThis; - m_spFrame = m_sp - nParam; - m_ops = pop; - m_nop = 0; - } + CObject* pOwner; + BYTE* pbCode = LookupFunction(pName->m_text, pName->m_length, pOwner); + if (pbCode == NULL) + { + TCHAR szName[32]; + int cchName = pName->m_length; + if (cchName > 31) + cchName = 31; + _tcsncpy(szName, pName->m_text, cchName); + szName[cchName] = '\0'; + Error(_T("unknown function: %s"), szName); + return false; } + + PushContext(nParam); + m_self = pOwner; + g_pThis = pOwner; + m_spFrame = m_sp - nParam; + m_ops = pbCode; + m_nop = 0; } - break; + return true; + // Return. Inside a call, restore the caller and leave the result on its + // stack. At the outermost level, hand back the top of stack and stop. case opRet: if (m_nextContext != NULL) { PopContext(); + return true; } - else + if (ppRetObj != NULL) + *ppRetObj = (m_sp != 0) ? Pop() : NULL; + return false; + + // string literal. + case opStr: { - if (ppRetObj != NULL) - *ppRetObj = m_sp > 0 ? Pop() : NULL; - return false; + int cchText; + const TCHAR* pchText = FetchString(cchText); + Push(pchText, cchText); } - break; + return true; - case opCond: + // float literal. + case opNum: + Push(FetchFloat()); + return true; + + case opAdd: case opSub: case opMul: case opDiv: case opMod: + case opEQ: case opNE: case opLT: case opGT: case opLE: case opGE: + case opSHL: case opSHR: case opAnd: case opOr: case opXor: + return BinaryOperator(op); + + // a.b pushes a then the name b; a[i] pushes a then i, which gets + // resolved first. Either way the left side is resolved one step and + // asked for the member. + case opDot: + case opArray: { - LOCALTRACE(_T("opCond\n")); - bool bCond = false; - CObject* pObj = Dereference(Pop()); - if (pObj != NULL) + CObject* pKey = Pop(); + CObject* pBase = Pop(); + if (pKey == NULL || pBase == NULL) { - CNumObject* pNum = pObj->ToNum(); - if (pNum->m_value != 0.0f) - bCond = true; - pNum->Release(); - pObj->Release(); + Error(_T("illegal null reference")); + return false; } - if (bCond) - m_nop += sizeof(UINT); - else - m_nop = FetchUInt(); + if (op == opArray) + pKey = Dereference(pKey); + + ASSERT(pKey->m_obj == objVariable || pKey->m_obj == objNumber || pKey->m_obj == objString); + + CObject* pTarget = pBase->Deref(); + if (pTarget == NULL) + { + Error(_T("illegal null reference")); + return false; + } + + CObject* pMember = pTarget->Dot(pKey); + if (pMember == NULL) + { + if (pKey->m_obj == objVariable) + { + Error(_T("Unknown member: %s"), ((CVarObject*)pKey)->m_text); + } + else + { + CStrObject* pKeyText = pKey->ToStr(); + Error(_T("Unknown member: %s"), pKeyText->GetSz()); + pKeyText->Release(); + } + } + + Push(pMember); + pTarget->Release(); + pKey->Release(); } - break; + return true; - case opJump: - LOCALTRACE(_T("opJump\n")); - m_nop = FetchUInt(); - break; + // Pop the value (resolved) and the target, assign, and leave the value. + case opAssign: + { + CObject* pValue = Dereference(Pop()); + CObject* pTarget = Pop(); + if (pTarget == NULL) + { + Error(_T("cannot assign to null")); + return false; + } + pTarget->Assign(pValue); + pTarget->Release(); + Push(pValue); + } + return true; - case opVar: + // int index. Push the local's slot, creating it on first use. + case opLocal: { - LOCALTRACE(_T("opVar\n")); - int cch; - const TCHAR* pch = FetchString(cch); + int iLocal = FetchInt(); + ASSERT(m_spFrame + iLocal < m_sp); + + CObject*& pSlot = m_stack[m_spFrame + iLocal]; + if (pSlot == NULL) + pSlot = new CLocalVariable; + pSlot->AddRef(); + Push(pSlot); + } + return true; -#ifdef _DEBUG + // Push the current self. + case opThis: + ASSERT(m_self != NULL); + m_self->AddRef(); + Push(m_self); + return true; + + // int count. Reserve the locals and mark the new base. + case opFrame: + { + int nLocals = FetchInt(); + + // Theseus: a frame that won't fit is an error, not a stack overrun. + if (nLocals < 0 || m_sp + (UINT)nLocals > countof(m_stack)) { - TCHAR szBuf [256]; - CopyChars(szBuf, pch, cch); - szBuf[cch] = '\0'; - LOCALTRACE(_T("opVar: \"%s\"\n"), szBuf); + Error(_T("script stack overflow")); + return false; } + +#ifdef _DEBUG + for (UINT i = m_sp; i < m_sp + nLocals; i++) + ASSERT(m_stack[i] == NULL); #endif + m_sp += nLocals; + m_spBase = m_sp; + } + return true; + + // int line. Remember it for error messages. + case opStatement: + m_nLine = FetchInt(); + return true; - Push(new CVarObject(pch, cch)); + // Negate a number. + case opNeg: + { + CObject* pValue = Dereference(Pop()); + if (pValue == NULL || pValue->m_obj != objNumber) + { + Error(_T("expected a number")); + return false; + } + // 5960 computes 0.0 - v here, which gives +0 for 0; Theseus keeps -v. + Push(-((CNumObject*)pValue)->m_value); + pValue->Release(); } - break; + return true; - case opLocal: + // Throw away the top of stack. + case opDrop: { - LOCALTRACE(_T("opLocal\n")); - int nLocal = FetchInt(); + CObject* pTop = Pop(); + if (pTop != NULL) + pTop->Release(); + } + return true; - ASSERT(m_spFrame + nLocal < m_sp); - CObject* pObject = m_stack[m_spFrame + nLocal]; - if (pObject == NULL) + // int count. Release that many locals off the top. + case opEndFrame: + { + int nLocals = FetchInt(); + for ( ; nLocals > 0; nLocals--) { - pObject = new CLocalVariable; - m_stack[m_spFrame + nLocal] = pObject; + m_sp--; + if (m_stack[m_sp] != NULL) + { + m_stack[m_sp]->Release(); + m_stack[m_sp] = NULL; + } } - pObject->AddRef(); - Push(pObject); + m_spBase = m_sp; } - break; + return true; - case opStr: + // Pop a number of seconds and set the wake time; the caller checks + // IsSleeping between steps. + case opSleep: { - int cch; - const TCHAR* pch = FetchString(cch); - Push(pch, cch); + CObject* pDelay = Dereference(Pop()); + if (pDelay == NULL) + { + Error(_T("illegal null reference")); + return false; + } + if (pDelay->m_obj != objNumber) + { + Error(_T("expected a number")); + return false; + } + m_wakeup = TheseusGetNow() + ((CNumObject*)pDelay)->m_value; + pDelay->Release(); } - break; - - case opNum: - Push(FetchFloat()); - break; + return true; } - return true; + // Lost in space. + ASSERT(FALSE); + return false; } // ========================================================================= -// Section 7: Binary operators +// Two-operand operators // ========================================================================= +// CRunner::BinaryOperator +// Reconstructed from 5960 retail XBE @ 0x00037fe0. Pops right then left, +// both fully resolved, and picks a path: +// - either is null, or both are nodes: only == and != work, comparing +// identity; anything else is "illegal null reference". +// - both strings and not +: wcscmp, then == != < <= > >= push 1 or 0; +// anything else is "type mismatch". +// - both numbers: arithmetic, comparisons (1 or 0), and the integer ops +// & | ^ << >> on the values truncated to integers. +// - otherwise + concatenates the two ToStr forms, and anything else is +// "type mismatch error". +// Full write-up in docs/decomp/VM.md. Theseus keeps its own integer +// conversion for the bit ops and adds a whole-number check in debug. bool CRunner::BinaryOperator(BYTE op) { - LOCALTRACE(_T("opOperator: %d\n"), op); - CObject* pRight = Dereference(Pop()); CObject* pLeft = Dereference(Pop()); - // Null comparison - if (pLeft == NULL || pRight == NULL || pLeft->m_obj == objNode && pRight->m_obj == objNode) + if (pLeft == NULL || pRight == NULL || (pLeft->m_obj == objNode && pRight->m_obj == objNode)) { - float fValue; - switch (op) + if (op == opEQ) + Push(pLeft == pRight ? 1.0f : 0.0f); + else if (op == opNE) + Push(pLeft != pRight ? 1.0f : 0.0f); + else { - default: Error(_T("illegal null reference")); return false; - case opEQ: fValue = (pLeft == pRight) ? 1.0f : 0.0f; Push(fValue); break; - case opNE: fValue = (pLeft != pRight) ? 1.0f : 0.0f; Push(fValue); break; + Error(_T("illegal null reference")); + return false; } - if (pRight != NULL) pRight->Release(); - if (pLeft != NULL) pLeft->Release(); + if (pRight != NULL) + pRight->Release(); + if (pLeft != NULL) + pLeft->Release(); return true; } - // String comparison (non-add operators) if (op != opAdd && pLeft->m_obj == objString && pRight->m_obj == objString) { - int nCmp = _tcscmp(((CStrObject*)pLeft)->GetSz(), ((CStrObject*)pRight)->GetSz()); - bool bValue; + int nOrder = _tcscmp(((CStrObject*)pLeft)->GetSz(), ((CStrObject*)pRight)->GetSz()); + bool bResult; switch (op) { - default: Error(_T("type mismatch")); return false; - case opEQ: bValue = nCmp == 0; break; - case opNE: bValue = nCmp != 0; break; - case opLT: bValue = nCmp < 0; break; - case opLE: bValue = nCmp <= 0; break; - case opGT: bValue = nCmp > 0; break; - case opGE: bValue = nCmp >= 0; break; + case opEQ: bResult = (nOrder == 0); break; + case opNE: bResult = (nOrder != 0); break; + case opLT: bResult = (nOrder < 0); break; + case opGT: bResult = (nOrder > 0); break; + case opLE: bResult = (nOrder <= 0); break; + case opGE: bResult = (nOrder >= 0); break; + default: + Error(_T("type mismatch")); + return false; } - Push(bValue); + Push(bResult); } else if (pLeft->m_obj == objNumber && pRight->m_obj == objNumber) { - // Numeric operations - float fL = ((CNumObject*)pLeft)->m_value; - float fR = ((CNumObject*)pRight)->m_value; - float fValue; + float a = ((CNumObject*)pLeft)->m_value; + float b = ((CNumObject*)pRight)->m_value; + float fResult; #ifdef _DEBUG - double intptr; -#endif - - switch (op) + // Theseus: bit ops on fractions are a type mismatch in debug builds. + if (op == opAnd || op == opOr || op == opXor) { - case opEQ: fValue = (fL == fR) ? 1.0f : 0.0f; break; - case opNE: fValue = (fL != fR) ? 1.0f : 0.0f; break; - case opLT: fValue = (fL < fR) ? 1.0f : 0.0f; break; - case opLE: fValue = (fL <= fR) ? 1.0f : 0.0f; break; - case opGT: fValue = (fL > fR) ? 1.0f : 0.0f; break; - case opGE: fValue = (fL >= fR) ? 1.0f : 0.0f; break; - case opAdd: fValue = fL + fR; break; - case opSub: fValue = fL - fR; break; - case opMul: fValue = fL * fR; break; - case opDiv: fValue = fL / fR; break; - - case opAnd: -#ifdef _DEBUG - if (modf(fL, &intptr) != 0 || modf(fR, &intptr) != 0) - { - Error(_T("type mismatch")); - return false; - } -#endif - fValue = (float)((ULONG)fL & (ULONG)fR); - break; - - case opXor: -#ifdef _DEBUG - if (modf(fL, &intptr) != 0 || modf(fR, &intptr) != 0) - { - Error(_T("type mismatch")); - return false; - } -#endif - fValue = (float)((ULONG)fL ^ (ULONG)fR); - break; - - case opOr: -#ifdef _DEBUG - if (modf(fL, &intptr) != 0 || modf(fR, &intptr) != 0) + double whole; + if (modf(a, &whole) != 0 || modf(b, &whole) != 0) { Error(_T("type mismatch")); return false; } + } #endif - fValue = (float)((ULONG)fL | (ULONG)fR); - break; - case opSHL: fValue = (float)((ULONG)fL << (ULONG)fR); break; - case opSHR: fValue = (float)((ULONG)fL >> (ULONG)fR); break; + switch (op) + { + case opAdd: fResult = a + b; break; + case opSub: fResult = a - b; break; + case opMul: fResult = a * b; break; + case opDiv: fResult = a / b; break; + + case opEQ: fResult = (a == b) ? 1.0f : 0.0f; break; + case opNE: fResult = (a != b) ? 1.0f : 0.0f; break; + case opLT: fResult = (a < b) ? 1.0f : 0.0f; break; + case opGT: fResult = (a > b) ? 1.0f : 0.0f; break; + case opLE: fResult = (a <= b) ? 1.0f : 0.0f; break; + case opGE: fResult = (a >= b) ? 1.0f : 0.0f; break; + + case opAnd: fResult = (float)((ULONG)a & (ULONG)b); break; + case opOr: fResult = (float)((ULONG)a | (ULONG)b); break; + case opXor: fResult = (float)((ULONG)a ^ (ULONG)b); break; + case opSHL: fResult = (float)((ULONG)a << (ULONG)b); break; + case opSHR: fResult = (float)((ULONG)a >> (ULONG)b); break; + + // opMod has no case in 5960 either; it falls out of the switch. } - Push(fValue); + Push(fResult); } else if (op == opAdd) { - // String concatenation - CStrObject* pStrL = pLeft->ToStr(); - CStrObject* pStrR = pRight->ToStr(); - Push(pStrL->concat(pStrR->GetSz())); - pStrL->Release(); - pStrR->Release(); + CStrObject* pLeftText = pLeft->ToStr(); + CStrObject* pRightText = pRight->ToStr(); + Push(pLeftText->concat(pRightText->GetSz())); + pLeftText->Release(); + pRightText->Release(); } else { @@ -1394,89 +1599,104 @@ bool CRunner::BinaryOperator(BYTE op) } // ========================================================================= -// Section 8: Built-in functions (EnableInput, eval, launch, alert, log) +// Built-in script functions // ========================================================================= +// CRunner::ExecuteBuiltIn +// Reconstructed from 5960 retail XBE @ 0x00038620. Matches the name by +// length then text, and returns TRUE when it handled the call: +// EnableInput(b) sets g_bInputEnable from the number else "Invalid parameter" +// eval(s) ExecuteScript(m_self, s) else "Bad launch" (sic) +// launch(p [,d]) d defaults to p up to its last \ or /, then +// CleanFilePath both and XWriteTitleInfoAndReboot else "Bad launch" +// DebugBreak() nothing in retail +// alert(s) debug print of s else "Bad alert" +// log(s) nothing in retail else "Bad log!" +// Theseus traces log(), routes alert() through Alert, launches through +// the desktop launcher on desktop, and breaks into the debugger in debug. BOOL CRunner::ExecuteBuiltIn(const TCHAR* pchName, int cchName, int nParam, CObject** rgParam, CObject*& pRetObj) { pRetObj = NULL; - if (cchName == 11 && _tcsncmp(pchName, _T("EnableInput"), cchName) == 0) + if (cchName == 11 && _tcsncmp(pchName, _T("EnableInput"), 11) == 0) { - if (nParam == 1) + if (nParam != 1) { - CObject* pObj = Dereference(rgParam[0]); - CNumObject* pNum = pObj->ToNum(); - g_bInputEnable = (bool)(pNum->m_value != 0.0f); - } - else Error(_T("Invalid parameter")); + return TRUE; + } + CNumObject* pOn = Dereference(rgParam[0])->ToNum(); + g_bInputEnable = pOn->m_value != 0.0f; return TRUE; } - if (cchName == 4 && _tcsncmp(pchName, _T("eval"), cchName) == 0) + if (cchName == 4 && _tcsncmp(pchName, _T("eval"), 4) == 0) { - if (nParam == 1) + if (nParam != 1) { - CObject* pObject = Dereference(rgParam[0]); - CStrObject* pStr = pObject->ToStr(); - ExecuteScript(m_self, pStr->GetSz()); - pStr->Release(); - pObject->Release(); - } - else Error(_T("Bad launch")); + return TRUE; + } + CObject* pArg = Dereference(rgParam[0]); + CStrObject* pScript = pArg->ToStr(); + ExecuteScript(m_self, pScript->GetSz()); + pScript->Release(); + pArg->Release(); return TRUE; } - if (cchName == 6 && _tcsncmp(pchName, _T("launch"), cchName) == 0) + if (cchName == 6 && _tcsncmp(pchName, _T("launch"), 6) == 0) { - if (nParam == 1 || nParam == 2) + if (nParam != 1 && nParam != 2) { - CObject* pObject1 = Dereference(rgParam[0]); - CStrObject* pStr1 = pObject1->ToStr(); - const TCHAR* sz1 = pStr1->GetSz(); + Error(_T("Bad launch")); + return TRUE; + } - TCHAR sz2[MAX_PATH]; - if (nParam == 2) - { - CObject* pObject2 = Dereference(rgParam[1]); - CStrObject* pStr2 = pObject2->ToStr(); - _tcscpy(sz2, pStr2->GetSz()); - pStr2->Release(); - pObject2->Release(); - } - else - { - _tcscpy(sz2, sz1); - TCHAR* pch = _tcsrchr(sz2, '/'); - if (pch != NULL) *pch = '\\'; - pch = _tcsrchr(sz2, '\\'); - ASSERT(pch != NULL); - *pch = 0; - } + CObject* pPathArg = Dereference(rgParam[0]); + CStrObject* pPath = pPathArg->ToStr(); + const TCHAR* szPath = pPath->GetSz(); - TRACE(_T("Launch title: %s, %s\n"), sz1, sz2); + TCHAR szDir[MAX_PATH]; + if (nParam == 2) + { + CObject* pDirArg = Dereference(rgParam[1]); + CStrObject* pDir = pDirArg->ToStr(); + _tcscpy(szDir, pDir->GetSz()); + pDir->Release(); + pDirArg->Release(); + } + else + { + _tcscpy(szDir, szPath); + TCHAR* pchSlash = _tcsrchr(szDir, '/'); + if (pchSlash != NULL) + *pchSlash = '\\'; + pchSlash = _tcsrchr(szDir, '\\'); + ASSERT(pchSlash != NULL); + *pchSlash = '\0'; + } + + TRACE(_T("Launch title: %s, %s\n"), szPath, szDir); #if defined(_XBOX) - FSCHAR ssz1[MAX_PATH]; - FSCHAR ssz2[MAX_PATH]; - CleanFilePath(ssz1, sz1); - CleanFilePath(ssz2, sz2); - XWriteTitleInfoAndReboot(ssz1, ssz2, LDT_NONE, 0, NULL); + FSCHAR szXbe[MAX_PATH]; + FSCHAR szXbeDir[MAX_PATH]; + CleanFilePath(szXbe, szPath); + CleanFilePath(szXbeDir, szDir); + XWriteTitleInfoAndReboot(szXbe, szXbeDir, LDT_NONE, 0, NULL); #else - extern void DesktopLaunchTitle(const char*); - DesktopLaunchTitle(sz2); + // Theseus: desktop hands the title to its own launcher. + extern void DesktopLaunchTitle(const char*); + DesktopLaunchTitle(szDir); #endif - pStr1->Release(); - pObject1->Release(); - } - else - Error(_T("Bad launch")); + + pPath->Release(); + pPathArg->Release(); return TRUE; } - if (cchName == 10 && _tcsncmp(pchName, _T("DebugBreak"), cchName) == 0) + if (cchName == 10 && _tcsncmp(pchName, _T("DebugBreak"), 10) == 0) { #ifdef _DEBUG _CrtDbgBreak(); @@ -1484,33 +1704,33 @@ BOOL CRunner::ExecuteBuiltIn(const TCHAR* pchName, int cchName, int nParam, CObj return TRUE; } - if (cchName == 5 && _tcsncmp(pchName, _T("alert"), cchName) == 0) + if (cchName == 5 && _tcsncmp(pchName, _T("alert"), 5) == 0) { - if (nParam == 1) + if (nParam != 1) { - CObject* pObject = Dereference(rgParam[0]); - CStrObject* pStr = pObject->ToStr(); - Alert(_T("%s"), pStr->GetSz()); - pStr->Release(); - pObject->Release(); - } - else Error(_T("Bad alert")); + return TRUE; + } + CObject* pArg = Dereference(rgParam[0]); + CStrObject* pText = pArg->ToStr(); + Alert(_T("%s"), pText->GetSz()); + pText->Release(); + pArg->Release(); return TRUE; } - if (cchName == 3 && _tcsncmp(pchName, _T("log"), cchName) == 0) + if (cchName == 3 && _tcsncmp(pchName, _T("log"), 3) == 0) { - if (nParam == 1) + if (nParam != 1) { - CObject* pObject = Dereference(rgParam[0]); - CStrObject* pStr = pObject->ToStr(); - TRACE(_T("%s\n"), pStr->GetSz()); - pStr->Release(); - pObject->Release(); - } - else Error(_T("Bad log!")); + return TRUE; + } + CObject* pArg = Dereference(rgParam[0]); + CStrObject* pText = pArg->ToStr(); + TRACE(_T("%s\n"), pText->GetSz()); + pText->Release(); + pArg->Release(); return TRUE; } @@ -1518,50 +1738,57 @@ BOOL CRunner::ExecuteBuiltIn(const TCHAR* pchName, int cchName, int nParam, CObj } // ========================================================================= -// Section 9: Function lookup. Resolves by name through scene hierarchy. +// Name lookup // ========================================================================= +// CRunner::LookupFunction +// Reconstructed from 5960 retail XBE @ 0x000389b0. Decompile: +// owner = m_self; +// if ((f = m_self->FindMemberFunction(name))) return f->m_rgop; +// for (p = m_self->m_parent; p; p = p->m_parent) +// if ((m = LookupMember(p, name))) { +// owner = p; return m->m_obj == objFunction ? m->m_rgop : (BYTE*)m; } +// owner = NULL; return NULL; +// A non-function hit comes back as a raw pointer, as in the binary; Theseus +// asserts on it. BYTE* CRunner::LookupFunction(const TCHAR* pchName, int cchName, CObject*& pOwner) { pOwner = m_self; - // Check self first - { - CFunction* pFunction = m_self->FindMemberFunction(pchName, cchName); - if (pFunction != NULL) - return pFunction->m_rgop; - } + CFunction* pOwn = m_self->FindMemberFunction(pchName, cchName); + if (pOwn != NULL) + return pOwn->m_rgop; - // Walk parent chain + for (CObject* pAncestor = m_self->m_parent; pAncestor != NULL; pAncestor = pAncestor->m_parent) { - for (CObject* pObject = m_self->m_parent; pObject != NULL; pObject = pObject->m_parent) - { - CObject* pObj = LookupMember(pObject, pchName, cchName); - if (pObj != NULL) - { - pOwner = pObject; - if (pObj->m_obj == objFunction) - return ((CFunction*)pObj)->m_rgop; + CObject* pFound = LookupMember(pAncestor, pchName, cchName); + if (pFound == NULL) + continue; - ASSERT(FALSE); - return (BYTE*)pObj; - } - } + pOwner = pAncestor; + if (pFound->m_obj == objFunction) + return ((CFunction*)pFound)->m_rgop; + + ASSERT(FALSE); + return (BYTE*)pFound; } pOwner = NULL; return NULL; } +// CRunner::LookupVariable [inlined in CVarObject::Deref / Assign] +// Try self, and if self isn't the script's this, try this too. CObject* CRunner::LookupVariable(const TCHAR* pchName, int cchName) { - CObject* pObject = LookupMember(m_self, pchName, cchName); - if (pObject != NULL || m_self == m_thisRef) - return pObject; - return LookupMember(m_thisRef, pchName, cchName); + CObject* pFound = LookupMember(m_self, pchName, cchName); + if (pFound == NULL && m_self != m_thisRef) + pFound = LookupMember(m_thisRef, pchName, cchName); + return pFound; } #ifdef _DEBUG +// Theseus: debug dump of the runner and its saved frames. void CRunner::DumpStack() { TRACE(_T("\nStack Trace\n{\n")); From 770198a3ccd9ba8a11ee2b7dd0ffca4fa3b289d8 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:12:32 -0500 Subject: [PATCH 039/167] Rebuilt the XAP compiler from the 5960 binary Wrote the scanner, value readers, script compiler and scene compiler fresh from the disassembly, with the address and behavior above each function, and wrote up what the binary does in docs/decomp/Compiler.md. Kept the long number CHECKs and the other Theseus touches. All 116 XAPs we ship compile to the same bytes as before, and truncated files, open strings and huge tokens fail the same way they did. --- docs/decomp/Compiler.md | 258 +++++ theseus/engine/xap_compile.cpp | 1872 +++++++++++++++++++------------- 2 files changed, 1346 insertions(+), 784 deletions(-) create mode 100644 docs/decomp/Compiler.md diff --git a/docs/decomp/Compiler.md b/docs/decomp/Compiler.md new file mode 100644 index 0000000..d59f982 --- /dev/null +++ b/docs/decomp/Compiler.md @@ -0,0 +1,258 @@ +# Compiler (xap_compile.cpp) + +How the 5960 dashboard turns XAP text into bytecode. The compiler is three +layers: a character scanner, a statement and expression compiler that emits +VM ops for script functions, and a scene compiler that emits node +construction ops for a class body. All text is UTF-16 in the retail XBE. +Opcode names are listed in VM.md. + +## Globals + +| Address | Meaning | +|---------|---------| +| 0x0017ae90 | current file name (shown in syntax errors) | +| 0x0017ae94 | current line number | +| 0x0017ac80 | parse error flag (byte) | +| 0x0017ac88 | buffer the `#file` directive copies a name into | +| 0x0017ae98 | class namespace list head (`Class_Init` / `Class_Exit`) | +| 0x000201c4, 0x00021b98 | empty strings returned on a hard parse failure | + +`CClass::ParseFile` (0x00033d00) sets file = name, line = 1, error = 0, +compiles the body, reports `"Stuff past expected end of file!"` if anything +is left, then clears file and line and returns `error == 0`. + +## Scanner + +| Function | Address | +|----------|---------| +| SyntaxError (free) | 0x00032cf0 | +| SkipWhite | 0x00032d70 | +| Token | 0x00032f70 | +| ParseBoolean | 0x00033150 | +| ParseInteger | 0x000331d0 | +| ParseNumber | 0x00033250 | +| ParseString | 0x000332d0 | +| ParseVec3 / ParseVec4 | 0x00033340 / 0x00033370 | +| ParseIntArray | 0x000333a0 | +| ParseVecArray | 0x00033490 | +| ExpandCString | 0x00033690 | + +**SyntaxError** formats the message into a 512 character buffer, wraps it +with the file name and line into a 1024 character report, and sets the error +flag. The retail build never displays the report. + +**SkipWhite(pch, mask)** eats every character up to and including space, +counting newlines. The mask enables `#` (1), `//` (2) and `/* */` (4) +comments; every caller passes 0xffffffff. A `#` line may be a directive: + +```c +// after '#' and any spaces/tabs +if (!wcsncmp(p, L"line", 4)) line = _wtol(p + 4 + blanks) - 1; +else if (!wcsncmp(p, L"file", 4)) { copy optional-quoted name to 0x17ac88; file = 0x17ac88; } +// then skip to end of line either way +``` + +**Token** returns the next token and its length, in one of four shapes: + +- quoted (`"` or `'`): the text between the quotes, `\` protects the next + character, the closing quote is consumed but not part of the token; +- path mode (third argument set, used by `DEF` and `USE`): everything up to + the next blank; +- identifier: starts with a letter or `_`, continues with letters, digits, `_`; +- otherwise a word of letters, digits, `.`, `-`, `_` if it starts with one of + those, or an operator run of anything else up to a blank or word character. + +**ParseInteger / ParseNumber** copy the token into a 64 character stack +buffer with no length check, then `_wtol` / `wcstod`. **ParseBoolean** uses +`wcsnicmp` against `true` then `false` over the token length. **ParseString** +copies the token unexpanded into a new buffer. + +**ParseIntArray / ParseVecArray** require `[`, remember the cursor and line, +and walk the list twice: once to count (entries split by an optional `,`, +each entry `nVecSize` numbers), then allocate `count * 4` bytes, rewind the +cursor and line, and read for real. ParseVecArray inlines the number read. +Neither loop checks for the end of the text, so an unterminated array never +finishes. + +**ExpandCString(out, max, tok, cch)** expands `\b \f \n \r \t`, `\xHH`, +`\uHHHH`, octal `\ooo` (and a bare `\0`), and passes any other escaped +character through. Errors: `"string constant is too long"`, +`"backslash at end of string"`, `"Invalid hex character escape sequence"`, +`"Invalid hex character"`, and the two Unicode equivalents. + +## Code buffer (CCompiler) + +| Function | Address | +|----------|---------| +| Write(pv, cb) | 0x00034440 | +| Write(BYTE) | 0x00034490 | +| WriteInteger | 0x000344c0 | +| WriteString | 0x000344f0 | +| GrowTo | 0x00034590 | +| SyntaxError (member) | 0x000345e0 | +| CreateFunction | 0x00034670 | +| ~CCompiler | 0x000346f0 | + +Layout: `+4` op count, `+8` op buffer, `+0xc` buffer size, `+0x10` error +byte. GrowTo rounds the request up to a 4 KB page, copies and frees the old +buffer. The writers are inlined at most call sites. WriteString writes the +character count as a dword, then the characters. CreateFunction allocates +`0x1c + ops` bytes, builds a CFunction (vtable 0x00022c18, type 13) and +copies the ops in after the header. The member SyntaxError also sets the +compiler's own error byte. + +## Script compiler (CFunctionCompiler) + +| Function | Address | +|----------|---------| +| ParseTerm | 0x00034710 | +| ParseOperator | 0x00034bb0 | +| ParseArray | 0x00034c20 | +| ParseCall | 0x00034c90 | +| ParseExpression(pch, prio) | 0x00034d80 | +| ParseIF | 0x00034ea0 | +| ParseWHILE | 0x00034fe0 | +| ParseDO | 0x00035170 | +| ParseFOR | 0x000352f0 | +| FixupBreaks | 0x00035510 | +| ParseBREAK | 0x00035540 | +| ParseCONTINUE | 0x00035610 | +| ParseRETURN | 0x000356c0 | +| ParseSLEEP | 0x00035760 | +| ParseBlock | 0x00035800 | +| ParseStatement | 0x00035960 | +| ParseLocalVar | 0x00035bf0 | + +Layout past the CCompiler base: 100 local slots of `{short len; ptr name}` +at `+0x14`, frame size at `+0x334`, break list at `+0x338`, loop depth at +`+0x33c`, loop top address at `+0x340`, behavior flag at `+0x344`. Nothing +checks the 100 slot limit. + +### Operators + +The table at 0x000200e8 holds 22 ten-byte entries `{ch1, ch2, ch3, priority, +opcode}`, scanned in order with the first match winning: + +| Text | Priority | Op | +|------|----------|----| +| `.` | 1 | 0x0d | +| `*` `/` | 2 | 0x0b 0x0c | +| `+` `-` | 3 | 0x09 0x0a | +| `<<` `>>` | 4 | 0x17 0x18 | +| `<=` `>=` `<` `>` | 5 | 0x14 0x15 0x12 0x13 | +| `==` `!=` | 6 | 0x10 0x11 | +| `&` | 7 | 0x1a | +| `^` | 8 | 0x1c | +| `\|` | 9 | 0x1b | +| `=` `+=` `-=` `*=` `/=` `%=` | 13 | 0x0e 0x20 0x21 0x22 0x23 0x24 | + +Because `+`, `-`, `*` and `/` come before their compound forms, `a += 1` +scans as `+` and the compound entries are never reached. There are no `&&` +or `||` entries; scripts use `&` and `|`. + +ParseExpression is precedence climbing from 14 down to 0, left associative. +Level 0 is ParseTerm; at level 1 a `(` after the operand is a call +(`opCall` + one byte argument count) and a `[` is an index (`opArray`). + +### Terms + +ParseTerm folds a run of unary `-`, `+` and `~` (`~` only reports +`"Sorry, the ~ operator has not been implemented yet!"`), then: + +- `( expr )`; +- a quoted string: `opStr` + expanded text (1024 character buffer); +- a token starting with `.`, `-` or a digit: copied into a 256 character + buffer (no length check), `wcstod`, a pending minus folded into the + constant, `opNum` + float; +- `true` / `false`: `opNum` 1.0 / 0.0; `this`: `opThis`; `null`: `opNull`; +- `new Name(args)`: the args, then `opNew` + count + name; +- a declared local: `opLocal` + slot; anything else: `opVar` + name. + +An unfolded minus emits `opNeg` after the term. + +### Statements + +ParseStatement leaves a `}` alone, sends `{` to ParseBlock, and otherwise +emits `opStatement` + the current line before dispatching on the keyword +(compared by length): `var`, `for`, `if`, `do`, `while`, `break`, `continue`, +`return`, and `sleep` in behaviors only. Anything else is an expression +statement compiled from the token start, then `opDrop` and a `;` that may be +missing at the end of the text. + +- **Block**: `opFrame` + local count (patched), statements, `opEndFrame` + + count; the frame size drops back afterwards. +- **if**: `opCond` + exit, then; `else` adds `opJump` over the else part. + Without `else` the cursor backs up to the start of the peeked token. +- **while**: top at the condition, `opCond` + exit, body, `opJump` top. +- **do**: steps one character past `do`, top at the body, then + `while ( cond ) ;` emits `opCond` + exit and `opJump` top. +- **for**: init (`var` is read from three characters past the `(`), top at + the condition, the step clause compiled once to find `)` and discarded, + body, step again + `opDrop`, `opJump` top. +- **break** pushes a forward `opJump` on the break list, patched to the loop + end by FixupBreaks; **continue** jumps to the loop top. Both report + `"unexpected 'break'"` / `"unexpected 'continue'"` outside a loop. +- **return** / **sleep**: optional value (`opNull` if none), then `opRet` / + `opSleep`. +- **var a = x, b;**: each name takes the next slot; an initializer emits + `opLocal` slot, the value, `opAssign`, `opDrop`. + +## Scene compiler (CClassCompiler) + +| Function | Address | +|----------|---------| +| CClass::ParseClassBody | 0x00033d60 | +| Compile (node list) | 0x00035e50 | +| ParseNode | 0x00035ec0 | +| ParseChildren | 0x000363c0 | +| ParseProps | 0x00036440 | +| ParseMemberVar | 0x00036ad0 | +| ParseFunction | 0x00036c30 | +| ParseClass | 0x00036e50 | +| Class_Init / Class_Exit | 0x00036f00 / 0x00036f30 | + +The target class sits at `+0x14`. ParseClassBody compiles a node list until +`}` or the end, appends a 0 byte, and stores the CFunction in the class. + +**ParseNode**, by first token: + +- `DEF name` (path token): new CMember (vtable 0x00022dd8) in the next + variable slot (class `+0x68`), `opDefNode` + slot, then the node; +- `USE name`: `opUseNode` + slot of an existing member, else + `"Undefined USE '%s'"` (name cut to 255 characters); +- `behavior {...}` and `function name(...) {...}`: compiled and set as class + members, a function named `behavior` is refused; +- `var`, `class`, `import Name, url` (url copied into a stack buffer with no + length check, the imported class loaded and defined by name); +- otherwise a script class or built-in node class by name (`"Unknown class: + %s"`): `opNewNode` + name, optional `{ props }` (`"Exected a '}'"`, sic), + `opEndNode`. + +One trailing `,` is consumed after every item. + +**ParseProps** reads names until `}`. Inline `behavior` / `function` +compile into the class instance function array (`+0x58`) and emit +`opFunction` + name + index. Other names go through the node class property +table (`"Unknown property: %s"`), and the 12-byte PRD `{offset, type, name}` +drives the encoding: + +| Type | Reader | Emitted | +|------|--------|---------| +| children (4), node array (11) | ParseChildren | `opInitArray` offset, nodes, `opEndArray` | +| node (9) | ParseNode | `opNewNodeProp` PRD, node | +| integer (1) | ParseInteger | `opInitProp` PRD, 4, value | +| boolean (2) | ParseBoolean | `opInitProp` PRD, 1, value | +| number (3) | ParseNumber | `opInitProp` PRD, 4, value | +| vec3 (5), color (8) | ParseVec3 | `opInitProp` PRD, 12, values | +| vec4 (6), quaternion (7) | ParseVec4 | `opInitProp` PRD, 16, values | +| string (10) | ParseString | `opInitProp` PRD, length, characters | +| int array (12) | ParseIntArray | `opInitProp` PRD, bytes, values | +| number / vec2 / vec3 / vec4 array (13 to 16) | ParseVecArray(1 to 4) | `opInitProp` PRD, bytes, values | + +**ParseChildren** takes one node or a `[ ... ]` list. **ParseMemberVar** +adds members and refuses initializers (`"Sorry, class member initializers +have not been implemented yet!"`). **ParseFunction** builds a +CFunctionCompiler on the stack, reads the parameter list into the local +slots (behaviors have none), requires a `{` body, and appends `opNull opRet` +before creating the function. **ParseClass** compiles a nested class body +into a new CClass and defines it in the class namespace. diff --git a/theseus/engine/xap_compile.cpp b/theseus/engine/xap_compile.cpp index e6e8d25..befbe91 100755 --- a/theseus/engine/xap_compile.cpp +++ b/theseus/engine/xap_compile.cpp @@ -1,10 +1,11 @@ -// xap_compile.cpp: XAP script compilation pipeline. +// xap_compile.cpp: the XAP compiler. // -// Tokenizer, scene graph parser, and bytecode compiler combined into a -// single translation unit. Pipeline shape: -// XAP text -> tokenizer -> scene parser -> bytecode compiler -> CFunction -// Decompiled from the 5960 retail XBE; see docs/decomp/VM.md for the -// binary-analysis notes on opcode set, operator table, and dispatch. +// One file holds the whole path from script text to bytecode: a character +// scanner (SkipWhite, Token), typed value readers for node properties, the +// statement and expression compiler (CFunctionCompiler) and the scene compiler +// (CClassCompiler) that turns node declarations into construction ops. +// Every body here follows the 5960 retail XBE; addresses and the decompile +// notes are in docs/decomp/Compiler.md. Opcode numbers live in runner.h. #include "std.h" #include "theseus.h" #include "xap_compile.h" @@ -12,298 +13,368 @@ #include "runner.h" -// ========================================================================= -// Global parse state -// ========================================================================= - +// Parse state shared by every compiler in flight. 5960 keeps the same four +// globals: file name @0x17ae90, line @0x17ae94, error flag @0x17ac80 and the +// #file name buffer @0x17ac88. int g_nLine; const TCHAR* g_szFileName; static TCHAR g_szFileNameBuf[MAX_PATH]; bool g_bParseError; +// The class namespace, 5960's list head @0x17ae98. CNameSpace* g_classes; +// Every hard failure hands back an empty string so callers stop cleanly. +// 5960 points at a shared L"" (0x201c4 / 0x21b98) the same way. static const TCHAR szParseError[] = _T(""); -#define IsAlNum(ch) (((ch) >= 'a' && (ch) <= 'z') || ((ch) >= 'A' && (ch) <= 'Z') || ((ch) >= '0' && (ch) <= '9') || (ch) == '.' || (ch) == '-' || (ch) == '_') +static inline bool IsLetter(TCHAR ch) +{ + return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); +} + +static inline bool IsDigit(TCHAR ch) +{ + return ch >= '0' && ch <= '9'; +} + +// Characters that make up a number or bare word: letters, digits, '.' and '-'. +// 5960 also lets '_' through here; Theseus never has, so a '_' ends the run. +static inline bool IsWordChar(TCHAR ch) +{ + return IsLetter(ch) || IsDigit(ch) || ch == '.' || ch == '-'; +} + +static inline bool IsIdentChar(TCHAR ch) +{ + return IsLetter(ch) || IsDigit(ch) || ch == '_'; +} + +static inline bool IsKeyword(const TCHAR* pchToken, int cchToken, const TCHAR* szKeyword, int cchKeyword) +{ + return cchToken == cchKeyword && _tcsncmp(pchToken, szKeyword, cchKeyword) == 0; +} -// ========================================================================= -// Section 1: Tokenizer -- character-level scanning -// ========================================================================= +// StartParse / EndParse +// 5960 has no separate functions for these; CClass::ParseFile @0x33d00 sets +// file = name, line = 1, error = 0 before compiling and clears file and line +// after. Theseus splits that into a pair so the line can start elsewhere. void StartParse(const TCHAR* pch, const TCHAR* szFileName, int nLine) { - g_szFileName = szFileName; - g_nLine = nLine; g_bParseError = false; + g_nLine = nLine; + g_szFileName = szFileName; } void EndParse() { - g_szFileName = NULL; g_nLine = 0; + g_szFileName = NULL; } + +// SyntaxError (free function) +// Reconstructed from 5960 retail XBE @ 0x00032cf0. Ghidra decompile: +// vswprintf(msg, 0x200, fmt, args); msg[511] = 0; +// swprintf(out, <"Syntax Error ... File: %s Line: %d ... %s">, file, line, msg); +// g_bParseError = 1; +// Retail builds the text and drops it; Theseus sends it to Trace so it shows up. void SyntaxError(const TCHAR* szMsg, ...) { + TCHAR szDetail[512]; va_list args; va_start(args, szMsg); + _vsntprintf(szDetail, countof(szDetail), szMsg, args); + va_end(args); - TCHAR szBuffer[512]; - _vsntprintf(szBuffer, countof(szBuffer), szMsg, args); - - TCHAR szMessage[1024]; - _stprintf(szMessage, _T("Syntax Error\n\nFile: %s\nLine: %d\n\n%s"), g_szFileName, g_nLine, szBuffer); - - Trace(_T("\007%s\n"), szMessage); + TCHAR szReport[1024]; + _stprintf(szReport, _T("Syntax Error\n\nFile: %s\nLine: %d\n\n%s"), g_szFileName, g_nLine, szDetail); + Trace(_T("\007%s\n"), szReport); - va_end(args); g_bParseError = true; } -// Skip whitespace and comments (supports #, //, /* */ styles) + +// SkipWhite +// Reconstructed from 5960 retail XBE @ 0x00032d70. Behavior: loop eating +// control characters and spaces (counting '\n' into the line), then, if the +// comment mask allows it, one comment: '#' or '//' runs to end of line, '/*' +// runs to the matching '*/' counting lines. A '#' line can be a directive: +// "#line N" sets the line so the next one reads N, "#file name" copies the +// name (optionally quoted) into the file buffer and makes it current. +// Returns at the first character that is neither blank nor a comment. const TCHAR* SkipWhite(const TCHAR* pch, COMMENT_TYPE ct) { for (;;) { - while (*pch != '\0' && *pch <= ' ') + for (; *pch != '\0' && *pch <= ' '; pch++) { if (*pch == '\n') g_nLine++; - pch++; } - if (((ct & pound) && *pch == '#') || ((ct & slashslash) && *pch == '/' && *(pch + 1) == '/')) + bool bPound = (ct & pound) && pch[0] == '#'; + bool bSlashSlash = (ct & slashslash) && pch[0] == '/' && pch[1] == '/'; + bool bSlashStar = (ct & slashstar) && pch[0] == '/' && pch[1] == '*'; + + if (bPound || bSlashSlash) { - // Handle #line and #file directives - if (*pch == '#') + if (bPound) { - pch++; - while (*pch == ' ' || *pch == '\t') + do pch++; + while (*pch == ' ' || *pch == '\t'); if (_tcsncmp(pch, _T("line"), 4) == 0) { - pch += 4; - while (*pch == ' ' || *pch == '\t') - pch++; + for (pch += 4; *pch == ' ' || *pch == '\t'; pch++) + ; g_nLine = _ttoi(pch) - 1; } else if (_tcsncmp(pch, _T("file"), 4) == 0) { - pch += 4; - while (*pch == ' ' || *pch == '\t') - pch++; - - TCHAR* pDest = g_szFileNameBuf; + for (pch += 4; *pch == ' ' || *pch == '\t'; pch++) + ; if (*pch == '"') pch++; - while (*pch != '\0' && *pch != '\r' && *pch != '\n' && *pch != '"') - *pDest++ = *pch++; - *pDest = 0; + + TCHAR* pchName = g_szFileNameBuf; + for (; *pch != '\0' && *pch != '\r' && *pch != '\n' && *pch != '"'; pch++) + *pchName++ = *pch; + *pchName = '\0'; g_szFileName = g_szFileNameBuf; } } - // Skip to end of line while (*pch != '\0' && *pch != '\r' && *pch != '\n') pch++; + continue; } - else if ((ct & slashstar) && *pch == '/' && *(pch + 1) == '*') + + if (bSlashStar) { - pch += 2; - while (*pch != '\0' && !(*pch == '*' && *(pch + 1) == '/')) + for (pch += 2; *pch != '\0'; pch++) { + if (pch[0] == '*' && pch[1] == '/') + { + pch += 2; + break; + } if (*pch == '\n') g_nLine++; - pch++; } - if (*pch != '\0') - pch += 2; - } - else - { - return pch; + continue; } + + return pch; } } -// Extract the next token -- handles quoted strings, identifiers, numbers, operators + +// Token +// Reconstructed from 5960 retail XBE @ 0x00032f70. Behavior: skip white, then +// one of four shapes. A '"' or '\'' opens a quoted token: the token is the +// text inside, '\\' protects the next character, and the closing quote is +// consumed. With bAllowPaths the token is everything up to the next blank. +// A letter or '_' starts an identifier (letters, digits, '_'). Otherwise a +// word character starts a number or bare word, and anything else starts an +// operator run that lasts until a blank or a word character. const TCHAR* Token(const TCHAR* pch, const TCHAR*& pchToken, int& cchToken, bool bAllowPaths, COMMENT_TYPE ct) { pch = SkipWhite(pch, ct); - BOOL bQuoted = FALSE; TCHAR chQuote = 0; if (*pch == '"' || *pch == '\'') - { - bQuoted = TRUE; - chQuote = *pch; - pch++; - } + chQuote = *pch++; pchToken = pch; - if (bQuoted) + if (chQuote != 0) { - while (*pch != '\0' && *pch != chQuote) + for (; *pch != '\0' && *pch != chQuote; pch++) { - if (*pch == '\\' && *(pch + 1) != '\0') + if (pch[0] == '\\' && pch[1] != '\0') pch++; - pch++; } } else if (bAllowPaths) { - while (*pch != '\0' && *pch > ' ') + while (*pch > ' ') pch++; } - else if ((*pch >= 'a' && *pch <= 'z' || *pch >= 'A' && *pch <= 'Z' || *pch == '_')) + else if (IsLetter(*pch) || *pch == '_') { - while (*pch >= 'a' && *pch <= 'z' || *pch >= 'A' && *pch <= 'Z' || *pch >= '0' && *pch <= '9' || *pch == '_') + while (IsIdentChar(*pch)) + pch++; + } + else if (IsWordChar(*pch)) + { + while (IsWordChar(*pch)) pch++; } else { - BOOL bAlNum = IsAlNum(*pch); - if (bAlNum) - { - while (IsAlNum(*pch)) - pch++; - } - else - { - while (*pch > ' ' && !IsAlNum(*pch)) - pch++; - } + while (*pch > ' ' && !IsWordChar(*pch)) + pch++; } cchToken = (int)(pch - pchToken); - if (bQuoted && *pch == chQuote) + if (chQuote != 0 && *pch == chQuote) pch++; return pch; } -// Expand C-style escape sequences in a string literal + +// Reads exactly cDigits hex digits for \x and \u. Returns false on a bad one. +static bool ReadHexEscape(const TCHAR*& pch, int& cch, int cDigits, TCHAR& chOut) +{ + int nValue = 0; + for (int i = 0; i < cDigits; i++) + { + TCHAR ch = *pch++; + cch--; + + int nDigit; + if (ch >= '0' && ch <= '9') + nDigit = ch - '0'; + else if (ch >= 'A' && ch <= 'F') + nDigit = ch - 'A' + 10; + else if (ch >= 'a' && ch <= 'f') + nDigit = ch - 'a' + 10; + else + return false; + + nValue = nValue * 16 + nDigit; + } + chOut = (TCHAR)nValue; + return true; +} + +// ExpandCString +// Reconstructed from 5960 retail XBE @ 0x00033690. Behavior: copy the token +// into the buffer turning escapes into characters. \b \f \n \r \t map to their +// control codes, \xHH takes two hex digits, \uHHHH four, \0 not followed by an +// octal digit is a NUL, other \ooo octal runs build one character, and any +// other escaped character stands for itself. Fails with -1 and a syntax error +// when the output fills, the string ends on a backslash, or a hex escape is +// short or bad. Returns the number of characters written. +// \u only exists in wide builds, where TCHAR can hold it (5960 is wide). int ExpandCString(TCHAR* szString, int cchMaxString, const TCHAR* pchToken, int cchToken) { TCHAR* pchOut = szString; + TCHAR* pchEnd = szString + cchMaxString; + while (cchToken > 0) { - if (pchOut >= szString + cchMaxString) + if (pchOut >= pchEnd) { SyntaxError(_T("string constant is too long")); return -1; } - if (*pchToken == '\\') + TCHAR ch = *pchToken++; + cchToken--; + + if (ch != '\\') + { + *pchOut++ = ch; + continue; + } + + if (cchToken == 0) + { + SyntaxError(_T("backslash at end of string")); + return -1; + } + + ch = *pchToken++; + cchToken--; + + switch (ch) { - pchToken++; - cchToken--; - if (cchToken == 0) + case 'b': + ch = '\b'; + break; + + case 'f': + ch = '\f'; + break; + + case 'n': + ch = '\n'; + break; + + case 'r': + ch = '\r'; + break; + + case 't': + ch = '\t'; + break; + + case 'x': + if (cchToken < 2) { - SyntaxError(_T("backslash at end of string")); + SyntaxError(_T("Invalid hex character escape sequence")); return -1; } + if (!ReadHexEscape(pchToken, cchToken, 2, ch)) + { + SyntaxError(_T("Invalid hex character")); + return -1; + } + break; - TCHAR ch = *pchToken++; - cchToken--; - switch (ch) +#ifdef _UNICODE + case 'u': + if (cchToken < 4) + { + SyntaxError(_T("Invalid Unicode character escape sequence")); + return -1; + } + if (!ReadHexEscape(pchToken, cchToken, 4, ch)) { - case 'b': *pchOut++ = '\b'; break; - case 'f': *pchOut++ = '\f'; break; - case 'n': *pchOut++ = '\n'; break; - case 'r': *pchOut++ = '\r'; break; - case 't': *pchOut++ = '\t'; break; + SyntaxError(_T("Invalid Unicode character")); + return -1; + } + break; +#endif - case 'x': - { - if (cchToken < 2) - { - SyntaxError(_T("Invalid hex character escape sequence")); - return -1; - } - ch = 0; - int i; - for (i = 0; i < 2; i++) - { - TCHAR chHex = *pchToken++; - cchToken--; - if (chHex >= '0' && chHex <= '9') ch = 16 * ch + chHex - '0'; - else if (chHex >= 'A' && chHex <= 'F') ch = 16 * ch + chHex - 'A' + 10; - else if (chHex >= 'a' && chHex <= 'f') ch = 16 * ch + chHex - 'a' + 10; - else { SyntaxError(_T("Invalid hex character")); return -1; } - } - *pchOut++ = ch; - } + default: + if (ch < '0' || ch > '7') break; -#ifdef _UNICODE - case 'u': - { - if (cchToken < 4) - { - SyntaxError(_T("Invalid Unicode character escape sequence")); - return -1; - } - ch = 0; - int i; - for (i = 0; i < 4; i++) - { - TCHAR chHex = *pchToken++; - cchToken--; - if (chHex >= '0' && chHex <= '9') ch = 16 * ch + chHex - '0'; - else if (chHex >= 'A' && chHex <= 'F') ch = 16 * ch + chHex - 'A' + 10; - else if (chHex >= 'a' && chHex <= 'f') ch = 16 * ch + chHex - 'a' + 10; - else { SyntaxError(_T("Invalid Unicode character")); return -1; } - } - *pchOut++ = ch; - } + if (ch == '0' && cchToken > 0 && (*pchToken < '0' || *pchToken > '7')) + { + ch = '\0'; break; -#endif + } - default: - if (ch >= '0' && ch <= '7') - { - if (ch == '0' && cchToken > 0 && (*pchToken < '0' || *pchToken > '7')) - { - *pchOut++ = '\0'; - } - else - { - ch -= '0'; - while (cchToken != 0 && *pchToken >= '0' && *pchToken <= '7') - { - ch = ch * 8 + *pchToken - '0'; - pchToken++; - cchToken--; - } - *pchOut++ = ch; - } - } - else - { - *pchOut++ = ch; - } - break; + { + int nValue = ch - '0'; + for (; cchToken != 0 && *pchToken >= '0' && *pchToken <= '7'; pchToken++, cchToken--) + nValue = nValue * 8 + (*pchToken - '0'); + ch = (TCHAR)nValue; } + break; } - else - { - *pchOut++ = *pchToken++; - cchToken--; - } + + *pchOut++ = ch; } return (int)(pchOut - szString); } -// ========================================================================= -// Value parsers -- extract typed values from token stream -// ========================================================================= +// ParseBoolean +// Reconstructed from 5960 retail XBE @ 0x00033150. Ghidra decompile: +// pch = Token(pch, &tok, &cch); +// if (!wcsnicmp(tok, L"true", cch)) b = 1; else if (!wcsnicmp(tok, L"false", cch)) b = 0; +// else SyntaxError(L"Invalid boolean value"); +// Case-insensitive prefix compare over the token length, same as the binary. const TCHAR* ParseBoolean(const TCHAR* pch, bool& b) { const TCHAR* pchToken; @@ -320,22 +391,33 @@ const TCHAR* ParseBoolean(const TCHAR* pch, bool& b) return pch; } -const TCHAR* ParseInteger(const TCHAR* pch, int& i) +// Copies a numeric token into a terminated buffer. 5960 copies blind into a +// 64 character stack buffer; Theseus clamps long tokens and flags them. +static void CopyNumberToken(TCHAR* szBuf, int cchBuf, const TCHAR* pchToken, int cchToken) { - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); - - TCHAR szBuf[64]; - if (!CHECK(cchToken < countof(szBuf))) + if (!CHECK(cchToken < cchBuf)) { SyntaxError(_T("Number is too long")); - cchToken = countof(szBuf) - 1; + cchToken = cchBuf - 1; } _tcsncpy(szBuf, pchToken, cchToken); - szBuf[cchToken] = 0; - i = _ttoi(szBuf); + szBuf[cchToken] = '\0'; +} + +// ParseInteger / ParseNumber +// Reconstructed from 5960 retail XBE @ 0x000331d0 / 0x00033250. Ghidra decompile: +// pch = Token(pch, &tok, &cch); wcsncpy(buf, tok, cch); buf[cch] = 0; // buf[64] +// *pi = _wtol(buf); or *pn = (float)wcstod(buf, NULL); +// The CHECK on the length is Theseus's bounds fix. +const TCHAR* ParseInteger(const TCHAR* pch, int& i) +{ + const TCHAR* pchToken; + int cchToken; + pch = Token(pch, pchToken, cchToken); + TCHAR szNumber[64]; + CopyNumberToken(szNumber, countof(szNumber), pchToken, cchToken); + i = _ttoi(szNumber); return pch; } @@ -345,19 +427,16 @@ const TCHAR* ParseNumber(const TCHAR* pch, float& n) int cchToken; pch = Token(pch, pchToken, cchToken); - TCHAR szBuf[64]; - if (!CHECK(cchToken < countof(szBuf))) - { - SyntaxError(_T("Number is too long")); - cchToken = countof(szBuf) - 1; - } - _tcsncpy(szBuf, pchToken, cchToken); - szBuf[cchToken] = 0; - n = (float)_tcstod(szBuf, NULL); - + TCHAR szNumber[64]; + CopyNumberToken(szNumber, countof(szNumber), pchToken, cchToken); + n = (float)_tcstod(szNumber, NULL); return pch; } +// ParseString +// Reconstructed from 5960 retail XBE @ 0x000332d0. Ghidra decompile: +// pch = Token(pch, &tok, &cch); sz = new WCHAR[cch + 1]; memcpy(sz, tok, cch * 2); sz[cch] = 0; +// No escape expansion: property strings are taken as written. const TCHAR* ParseString(const TCHAR* pch, TCHAR*& sz) { const TCHAR* pchToken; @@ -367,72 +446,75 @@ const TCHAR* ParseString(const TCHAR* pch, TCHAR*& sz) sz = new TCHAR[cchToken + 1]; CopyChars(sz, pchToken, cchToken); sz[cchToken] = '\0'; - return pch; } +// ParseVec3 / ParseVec4 +// Reconstructed from 5960 retail XBE @ 0x00033340 / 0x00033370. Ghidra decompile: +// ParseNumber(v + 0); ParseNumber(v + 1); ParseNumber(v + 2); [ParseNumber(v + 3);] const TCHAR* ParseVec3(const TCHAR* pch, float v[3]) { - pch = ParseNumber(pch, v[0]); - pch = ParseNumber(pch, v[1]); - pch = ParseNumber(pch, v[2]); + for (int i = 0; i < 3; i++) + pch = ParseNumber(pch, v[i]); return pch; } const TCHAR* ParseVec4(const TCHAR* pch, float v[4]) { - pch = ParseNumber(pch, v[0]); - pch = ParseNumber(pch, v[1]); - pch = ParseNumber(pch, v[2]); - pch = ParseNumber(pch, v[3]); + for (int i = 0; i < 4; i++) + pch = ParseNumber(pch, v[i]); return pch; } +// ParseIntArray / ParseVecArray +// Reconstructed from 5960 retail XBE @ 0x000333a0 / 0x00033490. Behavior: +// demand '[', then walk the list twice. The first walk only counts values (a +// group of nVecSize numbers per entry, entries split by an optional ','), +// stopping at ']'. Then it allocates count * 4 bytes, rewinds the cursor and +// the line counter to just after '[', and walks again storing each value. +// Returns past the ']'. A missing '[' is a syntax error and returns "". +// 5960 inlines the number read in ParseVecArray; Theseus calls ParseNumber so +// the long token CHECK covers arrays too. const TCHAR* ParseIntArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray) { pch = SkipWhite(pch); if (*pch != '[') { SyntaxError(_T("Expected '[' to start array")); - return _T(""); + return szParseError; } - pch++; - const TCHAR* pchStart = pch; - int nLineStart = g_nLine; - int nValues = 0; + const TCHAR* pchFirst = pch + 1; + const int nLineFirst = g_nLine; - // First pass: count values - for (;;) + int cValues = 0; + for (pch = SkipWhite(pchFirst); *pch != ']'; pch = SkipWhite(pch)) { + int nIgnored; + pch = ParseInteger(pch, nIgnored); + cValues++; + pch = SkipWhite(pch); - if (*pch == ']') break; - int n; - pch = ParseInteger(pch, n); - nValues++; - pch = SkipWhite(pch); - if (*pch == ']') break; - if (*pch == ',') pch++; + if (*pch == ']') + break; + if (*pch == ',') + pch++; } - // Second pass: read values - cbArray = nValues * sizeof(int); + cbArray = cValues * sizeof(int); pbArray = new BYTE[cbArray]; - BYTE* pb = pbArray; + int* pnOut = (int*)pbArray; - pch = pchStart; - g_nLine = nLineStart; - for (;;) + g_nLine = nLineFirst; + for (pch = SkipWhite(pchFirst); *pch != ']'; pch = SkipWhite(pch)) { + pch = ParseInteger(pch, *pnOut++); + pch = SkipWhite(pch); - if (*pch == ']') break; - int n; - pch = ParseInteger(pch, n); - *(int*)pb = n; - pb += sizeof(int); - pch = SkipWhite(pch); - if (*pch == ']') break; - if (*pch == ',') pch++; + if (*pch == ']') + break; + if (*pch == ',') + pch++; } return pch + 1; @@ -446,61 +528,52 @@ const TCHAR* ParseVecArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray, int n if (*pch != '[') { SyntaxError(_T("Expected '[' to start array")); - return _T(""); + return szParseError; } - pch++; - const TCHAR* pchStart = pch; - int nLineStart = g_nLine; - int nValues = 0; + const TCHAR* pchFirst = pch + 1; + const int nLineFirst = g_nLine; - // First pass: count - for (;;) + int cValues = 0; + for (pch = SkipWhite(pchFirst); *pch != ']'; pch = SkipWhite(pch)) { - pch = SkipWhite(pch); - if (*pch == ']') break; - int i; - for (i = 0; i < nVecSize; i++) + for (int i = 0; i < nVecSize; i++) { - float n; - pch = ParseNumber(pch, n); - nValues++; + float nIgnored; + pch = ParseNumber(pch, nIgnored); } + cValues += nVecSize; + pch = SkipWhite(pch); - if (*pch == ']') break; - if (*pch == ',') pch++; + if (*pch == ']') + break; + if (*pch == ',') + pch++; } - // Second pass: read - cbArray = nValues * sizeof(float); + cbArray = cValues * sizeof(float); pbArray = new BYTE[cbArray]; - BYTE* pb = pbArray; + float* pnOut = (float*)pbArray; - pch = pchStart; - g_nLine = nLineStart; - for (;;) + g_nLine = nLineFirst; + for (pch = SkipWhite(pchFirst); *pch != ']'; pch = SkipWhite(pch)) { + for (int i = 0; i < nVecSize; i++) + pch = ParseNumber(pch, *pnOut++); + pch = SkipWhite(pch); - if (*pch == ']') break; - int i; - for (i = 0; i < nVecSize; i++) - { - float n; - pch = ParseNumber(pch, n); - *(float*)pb = n; - pb += sizeof(float); - } - pch = SkipWhite(pch); - if (*pch == ']') break; - if (*pch == ',') pch++; + if (*pch == ']') + break; + if (*pch == ',') + pch++; } return pch + 1; } -// ========================================================================= -// CParser -- tokenizer wrapper with file context -// ========================================================================= + +// CParser is a Theseus helper with no counterpart in the 5960 binary. +// Kept as it was. CParser::CParser(const TCHAR* szFilePath, const TCHAR* pchFile, int cchFile) { @@ -542,15 +615,16 @@ void CParser::SyntaxError(const TCHAR* szMsg, ...) va_end(args); } -// ========================================================================= -// Section 2: Bytecode emitter -- CCompiler base class -// ========================================================================= +// CCompiler::CCompiler / ~CCompiler +// 5960 inlines the constructor (see the CFunctionCompiler set up in +// ParseFunction @0x36c30: ops count, buffer, size and error all zeroed). +// The destructor @0x000346f0 frees the op buffer. CCompiler::CCompiler() { + m_ops = NULL; m_nop = 0; m_opsSize = 0; - m_ops = NULL; m_bError = false; } @@ -559,50 +633,64 @@ CCompiler::~CCompiler() delete[] m_ops; } +// CCompiler::GrowTo +// Reconstructed from 5960 retail XBE @ 0x00034590. Ghidra decompile: +// cb = (n + 0xfff) & ~0xfff; p = new BYTE[cb]; memcpy(p, m_ops, m_nop); +// delete m_ops; m_opsSize = cb; m_ops = p; +// The buffer grows in whole 4 KB pages. void CCompiler::GrowTo(int nNewSize) { - int cb = ((nNewSize + 4095) & ~0xfff); - BYTE* ops = new BYTE[cb]; - CopyMemory(ops, m_ops, m_nop); + const int cbPage = 4096; + int cbNew = (nNewSize + cbPage - 1) / cbPage * cbPage; + + BYTE* pbNew = new BYTE[cbNew]; + CopyMemory(pbNew, m_ops, m_nop); delete[] m_ops; - m_ops = ops; - m_opsSize = cb; + + m_ops = pbNew; + m_opsSize = cbNew; } +// CCompiler::SyntaxError +// Reconstructed from 5960 retail XBE @ 0x000345e0. Same report as the free +// SyntaxError, and it also marks this compiler: m_bError = 1; g_bParseError = 1. +// Theseus sends the text to Trace. void CCompiler::SyntaxError(const TCHAR* szMsg, ...) { + TCHAR szDetail[512]; va_list args; va_start(args, szMsg); + _vsntprintf(szDetail, countof(szDetail), szMsg, args); + va_end(args); - TCHAR szBuffer[512]; - _vsntprintf(szBuffer, countof(szBuffer), szMsg, args); - - TCHAR szMessage[1024]; - _stprintf(szMessage, _T("Syntax Error\n\nFile: %s\nLine: %d\n\n%s"), g_szFileName, g_nLine, szBuffer); - - Trace(_T("\007%s\n"), szMessage); + TCHAR szReport[1024]; + _stprintf(szReport, _T("Syntax Error\n\nFile: %s\nLine: %d\n\n%s"), g_szFileName, g_nLine, szDetail); + Trace(_T("\007%s\n"), szReport); - va_end(args); m_bError = true; g_bParseError = true; } +// CCompiler::CreateFunction +// Reconstructed from 5960 retail XBE @ 0x00034670. Ghidra decompile: +// f = new(m_nop) CFunction; f->m_codeSize = m_nop; memcpy(f->m_rgop, m_ops, m_nop); +// One allocation holds the function header and its ops. CFunction* CCompiler::CreateFunction() { #pragma push_macro("new") #undef new CFunction* pFunction = new(m_nop) CFunction; #pragma pop_macro("new") + pFunction->m_codeSize = m_nop; CopyMemory(pFunction->m_rgop, m_ops, m_nop); return pFunction; } -// ========================================================================= -// Section 3: Expression and statement compiler -- CFunctionCompiler -// Compiles JS-like expressions and statements into bytecode opcodes -// ========================================================================= +// CFunctionCompiler::CFunctionCompiler +// Inlined in 5960 (ParseFunction @0x36c30): no locals, no pending breaks, not +// inside a loop, loop top at 0, behavior flag from the caller. CFunctionCompiler::CFunctionCompiler() { m_nFrameSize = 0; @@ -612,7 +700,9 @@ CFunctionCompiler::CFunctionCompiler() m_bBehavior = false; } -// Operator precedence table -- maps character sequences to opcodes +// Binary operator table, 22 entries of { ch1, ch2, ch3, priority, opcode }. +// Read straight out of the 5960 XBE @ 0x000200e8. Scanned in order and the +// first match wins, so the order is part of the language. static const DOPER rgdoper[] = { { '.', 0, 0, 1, opDot }, @@ -639,55 +729,69 @@ static const DOPER rgdoper[] = { '%', '=', 0, 13, opModAssign }, }; -#define LAST_PRI 14 +// ParseExpression always starts at priority 14 (0xe in every 5960 call). +static const int nLowestPriority = 14; +// CFunctionCompiler::ParseOperator +// Reconstructed from 5960 retail XBE @ 0x00034bb0. Ghidra decompile: +// *ppdoper = 0; pch = SkipWhite(pch); if (*pch == 0) return pch; +// for (i = 0; i < 22; i++) if (t[i].ch1 == pch[0] && (!t[i].ch2 || t[i].ch2 == pch[1])) { +// *ppdoper = &t[i]; return pch + (t[i].ch2 ? 2 : 1); } +// return pch; const TCHAR* CFunctionCompiler::ParseOperator(const TCHAR* pch, const DOPER*& pdoper) { pdoper = NULL; + pch = SkipWhite(pch); if (*pch == '\0') return pch; - for (int i = 0; i < sizeof(rgdoper) / sizeof(DOPER); i++) + for (const DOPER* p = rgdoper; p < rgdoper + countof(rgdoper); p++) { - if (rgdoper[i].m_ch1 == *pch && (rgdoper[i].m_ch2 == 0 || rgdoper[i].m_ch2 == *(pch + 1))) - { - pch++; - if (rgdoper[i].m_ch2 != 0) - pch++; - pdoper = &rgdoper[i]; - break; - } + if (p->m_ch1 != pch[0]) + continue; + if (p->m_ch2 != 0 && p->m_ch2 != pch[1]) + continue; + + pdoper = p; + return pch + (p->m_ch2 != 0 ? 2 : 1); } return pch; } -// Parse a primary expression (literal, variable, 'new', parenthesized expr) +// CFunctionCompiler::ParseTerm +// Reconstructed from 5960 retail XBE @ 0x00034710. Behavior: fold any run of +// unary '-', '+' and '~' ('~' only raises "not implemented"). Then either a +// parenthesized expression, or one token: a quoted string becomes opStr with +// its escapes expanded; a token starting with '.', '-' or a digit becomes +// opNum (a pending '-' is folded into the constant); true/false become opNum +// 1/0; this and null become opThis/opNull; "new Name(args)" becomes opNew +// with the arg count and class name; a declared local becomes opLocal with +// its slot; anything else is a late-bound opVar by name. A '-' that wasn't +// folded emits opNeg at the end. See docs/decomp/Compiler.md. const TCHAR* CFunctionCompiler::ParseTerm(const TCHAR* pch) { - bool bNeg = false; - bool bInvert = false; + bool bNegate = false; + bool bComplement = false; - // Handle unary +, -, ~ - for (;;) + for (;; pch++) { pch = SkipWhite(pch); - if (*pch == '-') bNeg = !bNeg; - else if (*pch == '+') ; - else if (*pch == '~') bInvert = !bInvert; - else break; - pch++; + if (*pch == '-') + bNegate = !bNegate; + else if (*pch == '~') + bComplement = !bComplement; + else if (*pch != '+') + break; } - if (bInvert) + if (bComplement) SyntaxError(_T("Sorry, the ~ operator has not been implemented yet!")); if (*pch == '(') { - // Parenthesized sub-expression - pch++; - pch = ParseExpression(pch); + pch = ParseExpression(pch + 1); pch = SkipWhite(pch); if (*pch != ')') { @@ -698,170 +802,192 @@ const TCHAR* CFunctionCompiler::ParseTerm(const TCHAR* pch) } else { - bool bQuoted = *pch == '"' || *pch == '\''; + const bool bString = (*pch == '"' || *pch == '\''); + const TCHAR* pchToken; int cchToken; pch = Token(pch, pchToken, cchToken); - if (cchToken == 0 && !bQuoted) - return pch; - - if (bQuoted) + if (bString) { - // String literal Write(opStr); - TCHAR rgchString[1024]; - int nLen = ExpandCString(rgchString, countof(rgchString), pchToken, cchToken); - if (nLen < 0) - return _T(""); - WriteString(rgchString, nLen); + TCHAR szText[1024]; + int cchText = ExpandCString(szText, countof(szText), pchToken, cchToken); + if (cchText < 0) + return szParseError; + WriteString(szText, cchText); + } + else if (cchToken == 0) + { + return pch; } - else if (*pchToken == '.' || *pchToken == '-' || *pchToken >= '0' && *pchToken <= '9') + else if (*pchToken == '.' || *pchToken == '-' || IsDigit(*pchToken)) { - // Numeric literal - TCHAR szBuf[256]; // copy the token because _tcstod stupidly copies the whole string - if (!CHECK(cchToken < countof(szBuf))) + // Theseus clamps over-long numbers; 5960 copies them blind. + TCHAR szNumber[256]; + if (!CHECK(cchToken < countof(szNumber))) { SyntaxError(_T("Number is too long")); - cchToken = countof(szBuf) - 1; + cchToken = countof(szNumber) - 1; + } + CopyChars(szNumber, pchToken, cchToken); + szNumber[cchToken] = '\0'; + + float nValue = (float)_tcstod(szNumber, NULL); + if (bNegate) + { + nValue = -nValue; + bNegate = false; } - CopyChars(szBuf, pchToken, cchToken); - szBuf[cchToken] = 0; - float n = (float)_tcstod(szBuf, NULL); - if (bNeg) { bNeg = false; n = -n; } Write(opNum); - WriteNumber(n); + WriteNumber(nValue); } - else if (cchToken == 4 && _tcsncmp(pchToken, _T("true"), 4) == 0) + else if (IsKeyword(pchToken, cchToken, _T("true"), 4)) { Write(opNum); WriteNumber(1.0f); } - else if (cchToken == 5 && _tcsncmp(pchToken, _T("false"), 5) == 0) + else if (IsKeyword(pchToken, cchToken, _T("false"), 5)) { Write(opNum); WriteNumber(0.0f); } - else if (cchToken == 4 && _tcsncmp(pchToken, _T("this"), 4) == 0) + else if (IsKeyword(pchToken, cchToken, _T("this"), 4)) { Write(opThis); } - else if (cchToken == 4 && _tcsncmp(pchToken, _T("null"), 4) == 0) + else if (IsKeyword(pchToken, cchToken, _T("null"), 4)) { Write(opNull); } - else if (cchToken == 3 && _tcsncmp(pchToken, _T("new"), 3) == 0) + else if (IsKeyword(pchToken, cchToken, _T("new"), 3)) { - // Object construction: new ClassName(args...) - pch = Token(pch, pchToken, cchToken); + const TCHAR* pchClass; + int cchClass; + pch = Token(pch, pchClass, cchClass); - int nParam = 0; + int cArgs = 0; pch = SkipWhite(pch); if (*pch == '(') { pch++; - if (*pch == ')') - { - pch++; - } - else + if (*pch != ')') { for (;;) { pch = ParseExpression(pch); - nParam++; + cArgs++; pch = SkipWhite(pch); - if (*pch == ')') { pch++; break; } - if (*pch != ',') { SyntaxError(_T("Missing ','")); return pch; } + if (*pch == ')') + break; + if (*pch != ',') + { + SyntaxError(_T("Missing ','")); + return pch; + } pch++; } } + pch++; } Write(opNew); - WriteInteger(nParam); - WriteString(pchToken, cchToken); + WriteInteger(cArgs); + WriteString(pchClass, cchClass); } else { - // Symbol -- check local variables first, then late-bind - int i; - for (i = 0; i < m_nFrameSize; i++) + int iLocal = 0; + while (iLocal < m_nFrameSize && + !(m_rgstLocal[iLocal].cchName == cchToken && _tcsncmp(pchToken, m_rgstLocal[iLocal].pchName, cchToken) == 0)) { - if (cchToken == m_rgstLocal[i].cchName && _tcsncmp(pchToken, m_rgstLocal[i].pchName, cchToken) == 0) - { - Write(opLocal); - WriteInteger(i); - break; - } + iLocal++; } - if (i == m_nFrameSize) + if (iLocal < m_nFrameSize) + { + Write(opLocal); + WriteInteger(iLocal); + } + else { - // Late-bound variable reference Write(opVar); WriteString(pchToken, cchToken); } } } - if (bNeg) + if (bNegate) Write(opNeg); return pch; } +// CFunctionCompiler::ParseArray +// Reconstructed from 5960 retail XBE @ 0x00034c20. Ghidra decompile: +// pch = ParseExpression(pch + 1); pch = SkipWhite(pch); +// if (*pch != ']') { SyntaxError(L"Missing ']'"); return pch; } +// Write(opArray); return pch + 1; const TCHAR* CFunctionCompiler::ParseArray(const TCHAR* pch) { ASSERT(*pch == '['); - pch++; - pch = ParseExpression(pch); - pch = SkipWhite(pch); + pch = SkipWhite(ParseExpression(pch + 1)); if (*pch != ']') { SyntaxError(_T("Missing ']'")); return pch; } - pch++; + Write(opArray); - return pch; + return pch + 1; } +// CFunctionCompiler::ParseCall +// Reconstructed from 5960 retail XBE @ 0x00034c90. Ghidra decompile: +// n = 0; pch = SkipWhite(pch + 1); +// if (*pch != ')') { ParseExpression; n = 1; while (*(pch = SkipWhite) != ')') { +// if (*pch != ',') { SyntaxError(L"Missing ,"); return pch; } ParseExpression(pch + 1); n++; } } +// Write(opCall); Write((BYTE)n); return pch + 1; +// The callee reference is already on the stack; the count is a single byte. const TCHAR* CFunctionCompiler::ParseCall(const TCHAR* pch) { - // we have evaluated the reference to the function already and are looking at the '(' - - int nArgs = 0; - ASSERT(*pch == '('); - pch++; - - pch = SkipWhite(pch); - if (*pch == ')') - { - pch++; - } - else + int cArgs = 0; + pch = SkipWhite(pch + 1); + if (*pch != ')') { for (;;) { - pch = ParseExpression(pch); - nArgs++; - pch = SkipWhite(pch); - if (*pch == ')') { pch++; break; } - if (*pch != ',') { SyntaxError(_T("Missing ,")); return pch; } + pch = SkipWhite(ParseExpression(pch)); + cArgs++; + if (*pch == ')') + break; + if (*pch != ',') + { + SyntaxError(_T("Missing ,")); + return pch; + } pch++; } } Write(opCall); - Write((BYTE)nArgs); - return pch; + Write((BYTE)cArgs); + return pch + 1; } -// Recursive descent expression parser with operator precedence +// CFunctionCompiler::ParseExp +// Reconstructed from 5960 retail XBE @ 0x00034d80 (the binary's +// ParseExpression(pch, prio)). Ghidra decompile: +// if (prio == 0) return ParseTerm(pch); +// pch = ParseExp(pch, prio - 1); +// for (;;) { pch = SkipWhite(pch); +// if (prio == 1) { if (*pch == '(') pch = ParseCall(pch); else if (*pch == '[') pch = ParseArray(pch); } +// p = ParseOperator(pch, &op); if (!op || op->pri != prio) return pch; +// pch = ParseExp(p, prio - 1); Write(op->op); } +// Left-associative precedence climbing; calls and indexing bind at level 1. const TCHAR* CFunctionCompiler::ParseExp(const TCHAR* pch, int nPrio) { if (nPrio == 0) @@ -881,231 +1007,272 @@ const TCHAR* CFunctionCompiler::ParseExp(const TCHAR* pch, int nPrio) pch = ParseArray(pch); } - const TCHAR* pchOper = pch; const DOPER* pdoper; - pch = ParseOperator(pch, pdoper); + const TCHAR* pchRight = ParseOperator(pch, pdoper); if (pdoper == NULL || pdoper->m_pri != nPrio) - return pchOper; + return pch; - pch = ParseExp(pch, nPrio - 1); + pch = ParseExp(pchRight, nPrio - 1); Write(pdoper->m_op); } } const TCHAR* CFunctionCompiler::ParseExpression(const TCHAR* pch) { - return ParseExp(pch, LAST_PRI); + return ParseExp(pch, nLowestPriority); } -// ========================================================================= -// Statement parsers -- if, while, for, do, break, continue, return, sleep -// ========================================================================= - +// CFunctionCompiler::ParseIF +// Reconstructed from 5960 retail XBE @ 0x00034ea0. Ghidra decompile: +// '(' expr ')' or SyntaxError(L"expected '('" / L"expected ')'") and return; +// Write(opCond); fix = m_nop; WriteInteger(0); pch = ParseStatement(pch); +// pch = Token(pch, &tok, &cch); +// if (cch == 4 && !wcsncmp(tok, L"else", 4)) { Write(opJump); fix2 = m_nop; WriteInteger(0); +// patch(fix) = m_nop; pch = ParseStatement(pch); patch(fix2) = m_nop; return pch; } +// patch(fix) = m_nop; return tok; +// With no else the cursor goes back to the start of the peeked token. const TCHAR* CFunctionCompiler::ParseIF(const TCHAR* pch) { pch = SkipWhite(pch); - if (*pch != '(') { SyntaxError(_T("expected '('")); return pch; } - pch++; - - pch = ParseExpression(pch); + if (*pch != '(') + { + SyntaxError(_T("expected '('")); + return pch; + } - pch = SkipWhite(pch); - if (*pch != ')') { SyntaxError(_T("expected ')'")); return pch; } - pch++; + pch = SkipWhite(ParseExpression(pch + 1)); + if (*pch != ')') + { + SyntaxError(_T("expected ')'")); + return pch; + } Write(opCond); - UINT nop = GetAddress(); + UINT nopSkipThen = GetAddress(); WriteInteger(0); - pch = ParseStatement(pch); + pch = ParseStatement(pch + 1); - // Check for else const TCHAR* pchToken; int cchToken; - pch = Token(pch, pchToken, cchToken); - - if (cchToken == 4 && _tcsncmp(pchToken, _T("else"), cchToken) == 0) - { - Write(opJump); - UINT nopElse = GetAddress(); - WriteInteger(0); - Fixup(nop, GetAddress()); - pch = ParseStatement(pch); - Fixup(nopElse, GetAddress()); - } - else + const TCHAR* pchAfter = Token(pch, pchToken, cchToken); + if (!IsKeyword(pchToken, cchToken, _T("else"), 4)) { - pch = pchToken; // back up - Fixup(nop, GetAddress()); + Fixup(nopSkipThen, GetAddress()); + return pchToken; } + Write(opJump); + UINT nopSkipElse = GetAddress(); + WriteInteger(0); + + Fixup(nopSkipThen, GetAddress()); + pch = ParseStatement(pchAfter); + Fixup(nopSkipElse, GetAddress()); return pch; } +// CFunctionCompiler::ParseWHILE +// Reconstructed from 5960 retail XBE @ 0x00034fe0. Behavior: save the break +// list and loop top, open a breakable scope, then '(' cond ')' with the loop +// top at the start of the condition. Emits opCond (forward fixup), the body, +// opJump back to the top, patches the exit and every break to here, and +// restores the outer loop state. A missing paren is a syntax error and "". const TCHAR* CFunctionCompiler::ParseWHILE(const TCHAR* pch) { - BREAK* pOldBreaks = m_breaks; + BREAK* pOuterBreaks = m_breaks; m_breaks = NULL; m_nBreakables++; pch = SkipWhite(pch); - if (*pch != '(') { SyntaxError(_T("expected '('")); return _T(""); } - pch++; + if (*pch != '(') + { + SyntaxError(_T("expected '('")); + return szParseError; + } - int nopOldTop = m_nopTopOfLoop; + int nopOuterTop = m_nopTopOfLoop; m_nopTopOfLoop = GetAddress(); - pch = ParseExpression(pch); - pch = SkipWhite(pch); - if (*pch != ')') { SyntaxError(_T("expected ')'")); return _T(""); } - pch++; + pch = SkipWhite(ParseExpression(pch + 1)); + if (*pch != ')') + { + SyntaxError(_T("expected ')'")); + return szParseError; + } Write(opCond); - UINT nopFixup = GetAddress(); + UINT nopExit = GetAddress(); WriteInteger(0); - pch = ParseStatement(pch); + pch = ParseStatement(pch + 1); Write(opJump); WriteInteger(m_nopTopOfLoop); - Fixup(nopFixup, GetAddress()); + Fixup(nopExit, GetAddress()); FixupBreaks(); - m_breaks = pOldBreaks; + m_breaks = pOuterBreaks; m_nBreakables--; - m_nopTopOfLoop = nopOldTop; - + m_nopTopOfLoop = nopOuterTop; return pch; } +// CFunctionCompiler::ParseDO +// Reconstructed from 5960 retail XBE @ 0x00035170. Behavior: same loop +// bookkeeping as while, with the top at the body. Steps one character past +// the "do" keyword, compiles the body, then demands while '(' cond ')' ';'. +// Emits opCond to the exit, then an unconditional opJump back to the top +// (the condition is checked once per pass that way), patches exit and breaks. const TCHAR* CFunctionCompiler::ParseDO(const TCHAR* pch) { - BREAK* pOldBreaks = m_breaks; + BREAK* pOuterBreaks = m_breaks; m_breaks = NULL; m_nBreakables++; - pch++; - - int nopOldTop = m_nopTopOfLoop; + int nopOuterTop = m_nopTopOfLoop; m_nopTopOfLoop = GetAddress(); - pch = ParseStatement(pch); + pch = ParseStatement(pch + 1); const TCHAR* pchToken; int cchToken; pch = Token(pch, pchToken, cchToken); - if (cchToken != 5 || _tcsncmp(pchToken, _T("while"), 5) != 0) + if (!IsKeyword(pchToken, cchToken, _T("while"), 5)) { SyntaxError(_T("expected 'while'")); - return _T(""); + return szParseError; } pch = SkipWhite(pch); - if (*pch != '(') { SyntaxError(_T("expected '('")); return _T(""); } - pch++; - - pch = ParseExpression(pch); + if (*pch != '(') + { + SyntaxError(_T("expected '('")); + return szParseError; + } - pch = SkipWhite(pch); - if (*pch != ')') { SyntaxError(_T("expected ')'")); return _T(""); } - pch++; + pch = SkipWhite(ParseExpression(pch + 1)); + if (*pch != ')') + { + SyntaxError(_T("expected ')'")); + return szParseError; + } - pch = SkipWhite(pch); - if (*pch != ';') { SyntaxError(_T("expected ';'")); return _T(""); } - pch++; + pch = SkipWhite(pch + 1); + if (*pch != ';') + { + SyntaxError(_T("expected ';'")); + return szParseError; + } Write(opCond); - UINT nopFixup = GetAddress(); + UINT nopExit = GetAddress(); WriteInteger(0); Write(opJump); WriteInteger(m_nopTopOfLoop); - Fixup(nopFixup, GetAddress()); + Fixup(nopExit, GetAddress()); FixupBreaks(); - m_breaks = pOldBreaks; + m_breaks = pOuterBreaks; m_nBreakables--; - m_nopTopOfLoop = nopOldTop; - - return pch; + m_nopTopOfLoop = nopOuterTop; + return pch + 1; } +// CFunctionCompiler::ParseFOR +// Reconstructed from 5960 retail XBE @ 0x000352f0. Behavior: '(' then the +// init clause: "var ..." (read from three characters past the paren, handled +// by ParseLocalVar which eats the ';') or an expression + opDrop + ';'. The +// loop top is the condition. The step clause is compiled once and thrown +// away just to find the ')', then compiled for real after the body, followed +// by opDrop and opJump back to the top. Exit and breaks are patched last. const TCHAR* CFunctionCompiler::ParseFOR(const TCHAR* pch) { - BREAK* pOldBreaks = m_breaks; + BREAK* pOuterBreaks = m_breaks; m_breaks = NULL; m_nBreakables++; pch = SkipWhite(pch); - if (*pch != '(') { SyntaxError(_T("expected '('")); return _T(""); } + if (*pch != '(') + { + SyntaxError(_T("expected '('")); + return szParseError; + } pch++; - // Initializer const TCHAR* pchToken; int cchToken; Token(pch, pchToken, cchToken); - if (cchToken == 3 && _tcsncmp(pchToken, _T("var"), cchToken) == 0) + if (IsKeyword(pchToken, cchToken, _T("var"), 3)) { - pch += 3; - pch = ParseLocalVar(pch); + pch = ParseLocalVar(pch + 3); } else { pch = ParseExpression(pch); Write(opDrop); pch = SkipWhite(pch); - if (*pch != ';') { SyntaxError(_T("expected ';'")); return _T(""); } + if (*pch != ';') + { + SyntaxError(_T("expected ';'")); + return szParseError; + } pch++; } - // Condition - int nopOldTop = m_nopTopOfLoop; + int nopOuterTop = m_nopTopOfLoop; m_nopTopOfLoop = GetAddress(); - pch = ParseExpression(pch); - pch = SkipWhite(pch); - if (*pch != ';') { SyntaxError(_T("expected ';'")); return _T(""); } - pch++; + pch = SkipWhite(ParseExpression(pch)); + if (*pch != ';') + { + SyntaxError(_T("expected ';'")); + return szParseError; + } - // Save third expression position, parse it, then rewind - int nopExp3 = GetAddress(); - const TCHAR* pchExp3 = pch; - pch = ParseExpression(pch); + const TCHAR* pchStep = pch + 1; + UINT nopStep = GetAddress(); + pch = ParseExpression(pchStep); Write(opDrop); - m_nop = nopExp3; + m_nop = nopStep; pch = SkipWhite(pch); - if (*pch != ')') { SyntaxError(_T("expected ')'")); return _T(""); } - pch++; + if (*pch != ')') + { + SyntaxError(_T("expected ')'")); + return szParseError; + } Write(opCond); - UINT nopFixup = GetAddress(); + UINT nopExit = GetAddress(); WriteInteger(0); - pch = ParseStatement(pch); + pch = ParseStatement(pch + 1); - // Generate the third expression code - ParseExpression(pchExp3); + ParseExpression(pchStep); Write(opDrop); - Write(opJump); WriteInteger(m_nopTopOfLoop); - Fixup(nopFixup, GetAddress()); + Fixup(nopExit, GetAddress()); FixupBreaks(); - m_breaks = pOldBreaks; + m_breaks = pOuterBreaks; m_nBreakables--; - m_nopTopOfLoop = nopOldTop; - + m_nopTopOfLoop = nopOuterTop; return pch; } +// CFunctionCompiler::FixupBreaks +// Reconstructed from 5960 retail XBE @ 0x00035510. Ghidra decompile: +// for (b = m_breaks; b; b = next) { next = b->next; *(int*)(m_ops + b->nop) = m_nop; delete b; } void CFunctionCompiler::FixupBreaks() { - for (BREAK* pBreak = m_breaks; pBreak != NULL; ) + BREAK* pBreak = m_breaks; + while (pBreak != NULL) { BREAK* pNext = pBreak->m_next; Fixup(pBreak->m_nop, GetAddress()); @@ -1114,104 +1281,155 @@ void CFunctionCompiler::FixupBreaks() } } +// CFunctionCompiler::ParseBREAK +// Reconstructed from 5960 retail XBE @ 0x00035540. Ghidra decompile: +// pch = SkipWhite(pch); if (*pch != ';') { SyntaxError(L"Missing ';'"); return L""; } +// if (!m_nBreakables) { SyntaxError(L"unexpected 'break'"); return L""; } +// Write(opJump); b = new BREAK; b->next = m_breaks; m_breaks = b; b->nop = m_nop; +// WriteInteger(0); return pch + 1; const TCHAR* CFunctionCompiler::ParseBREAK(const TCHAR* pch) { pch = SkipWhite(pch); - if (*pch != ';') { SyntaxError(_T("Missing ';'")); return _T(""); } - pch++; - - if (m_nBreakables == 0) { SyntaxError(_T("unexpected 'break'")); return _T(""); } + if (*pch != ';') + { + SyntaxError(_T("Missing ';'")); + return szParseError; + } + if (m_nBreakables == 0) + { + SyntaxError(_T("unexpected 'break'")); + return szParseError; + } Write(opJump); + BREAK* pBreak = new BREAK; + pBreak->m_nop = GetAddress(); pBreak->m_next = m_breaks; m_breaks = pBreak; - pBreak->m_nop = GetAddress(); - WriteInteger(0); - return pch; + WriteInteger(0); + return pch + 1; } +// CFunctionCompiler::ParseCONTINUE +// Reconstructed from 5960 retail XBE @ 0x00035610. Same checks as break +// (L"unexpected 'continue'"), then: Write(opJump); WriteInteger(m_nopTopOfLoop). const TCHAR* CFunctionCompiler::ParseCONTINUE(const TCHAR* pch) { pch = SkipWhite(pch); - if (*pch != ';') { SyntaxError(_T("Missing ';'")); return _T(""); } - pch++; - - if (m_nBreakables == 0) { SyntaxError(_T("unexpected 'continue'")); return _T(""); } + if (*pch != ';') + { + SyntaxError(_T("Missing ';'")); + return szParseError; + } + if (m_nBreakables == 0) + { + SyntaxError(_T("unexpected 'continue'")); + return szParseError; + } Write(opJump); WriteInteger(m_nopTopOfLoop); - - return pch; + return pch + 1; } -const TCHAR* CFunctionCompiler::ParseRETURN(const TCHAR* pch) +// CFunctionCompiler::ParseRETURN / ParseSLEEP +// Reconstructed from 5960 retail XBE @ 0x000356c0 / 0x00035760. Ghidra decompile: +// pch = SkipWhite(pch); +// if (*pch == ';') Write(opNull); +// else { pch = SkipWhite(ParseExpression(pch)); if (*pch != ';') { SyntaxError(L"Missing ';'"); return L""; } } +// Write(opRet or opSleep); return pch + 1; +// The two are the same shape and differ only in the closing opcode. +static const TCHAR* ParseValueThenOp(CFunctionCompiler* pCompiler, const TCHAR* pch, BYTE op) { pch = SkipWhite(pch); if (*pch == ';') - Write(opNull); + { + pCompiler->Write(opNull); + } else { - pch = ParseExpression(pch); - pch = SkipWhite(pch); - if (*pch != ';') { SyntaxError(_T("Missing ';'")); return _T(""); } + pch = SkipWhite(pCompiler->ParseExpression(pch)); + if (*pch != ';') + { + pCompiler->SyntaxError(_T("Missing ';'")); + return szParseError; + } } - pch++; - Write(opRet); - return pch; + + pCompiler->Write(op); + return pch + 1; +} + +const TCHAR* CFunctionCompiler::ParseRETURN(const TCHAR* pch) +{ + return ParseValueThenOp(this, pch, opRet); } const TCHAR* CFunctionCompiler::ParseSLEEP(const TCHAR* pch) { - pch = SkipWhite(pch); - if (*pch == ';') - Write(opNull); - else - { - pch = ParseExpression(pch); - pch = SkipWhite(pch); - if (*pch != ';') { SyntaxError(_T("Missing ';'")); return _T(""); } - } - pch++; - Write(opSleep); - return pch; + return ParseValueThenOp(this, pch, opSleep); } +// CFunctionCompiler::ParseBlock +// Reconstructed from 5960 retail XBE @ 0x00035800. Ghidra decompile: +// brace = (*pch == '{'); if (brace) pch++; +// base = m_nFrameSize; Write(opFrame); fix = m_nop; WriteInteger(0); +// while (*pch && !(brace && *pch == '}')) pch = ParseStatement(pch); +// if (brace) { if (*pch != '}') { SyntaxError(L"expected '}'"); return pch; } pch++; } +// patch(fix) = m_nFrameSize - base; Write(opEndFrame); WriteInteger(m_nFrameSize - base); +// m_nFrameSize = base; +// Locals declared inside the block live exactly as long as its frame. const TCHAR* CFunctionCompiler::ParseBlock(const TCHAR* pch) { - bool bBrace = (*pch == '{'); - if (bBrace) + const bool bBraced = (*pch == '{'); + if (bBraced) pch++; - int nFrameSize = m_nFrameSize; + const int nFrameBase = m_nFrameSize; Write(opFrame); - UINT nopFrameFixup = GetAddress(); + UINT nopFrameSize = GetAddress(); WriteInteger(0); - while (*pch != '\0' && (!bBrace || *pch != '}')) + while (*pch != '\0' && !(bBraced && *pch == '}')) pch = ParseStatement(pch); - if (bBrace) + if (bBraced) { - if (*pch != '}') { SyntaxError(_T("expected '}'")); return pch; } + if (*pch != '}') + { + SyntaxError(_T("expected '}'")); + return pch; + } pch++; } - Fixup(nopFrameFixup, m_nFrameSize - nFrameSize); + const int cLocals = m_nFrameSize - nFrameBase; + Fixup(nopFrameSize, cLocals); Write(opEndFrame); - WriteInteger(m_nFrameSize - nFrameSize); + WriteInteger(cLocals); - m_nFrameSize = nFrameSize; + m_nFrameSize = nFrameBase; return pch; } +// CFunctionCompiler::ParseStatement +// Reconstructed from 5960 retail XBE @ 0x00035960. Behavior: skip white; a +// '}' is left for the caller and a '{' opens a block. Otherwise read one +// token, emit opStatement with the current line, and dispatch on keyword +// (grouped by length in the binary): var, for, if, do, while, break, +// continue, return, and sleep inside behaviors only. Anything else is an +// expression statement compiled from the start of that token, followed by +// opDrop and a ';' (which may be missing at the very end of the text). const TCHAR* CFunctionCompiler::ParseStatement(const TCHAR* pch) { pch = SkipWhite(pch); - if (*pch == '}') return pch; - if (*pch == '{') return ParseBlock(pch); + if (*pch == '}') + return pch; + if (*pch == '{') + return ParseBlock(pch); const TCHAR* pchToken; int cchToken; @@ -1220,57 +1438,78 @@ const TCHAR* CFunctionCompiler::ParseStatement(const TCHAR* pch) Write(opStatement); WriteInteger(g_nLine); - if (cchToken == 3 && _tcsncmp(pchToken, _T("var"), cchToken) == 0) - return ParseLocalVar(pch); - if (cchToken == 2 && _tcsncmp(pchToken, _T("if"), cchToken) == 0) - return ParseIF(pch); - if (cchToken == 5 && _tcsncmp(pchToken, _T("while"), cchToken) == 0) - return ParseWHILE(pch); - if (cchToken == 3 && _tcsncmp(pchToken, _T("for"), cchToken) == 0) - return ParseFOR(pch); - if (cchToken == 2 && _tcsncmp(pchToken, _T("do"), cchToken) == 0) - return ParseDO(pch); - if (cchToken == 5 && _tcsncmp(pchToken, _T("break"), cchToken) == 0) - return ParseBREAK(pch); - if (cchToken == 8 && _tcsncmp(pchToken, _T("continue"), cchToken) == 0) - return ParseCONTINUE(pch); - if (cchToken == 6 && _tcsncmp(pchToken, _T("return"), cchToken) == 0) - return ParseRETURN(pch); - if (m_bBehavior && cchToken == 5 && _tcsncmp(pchToken, _T("sleep"), cchToken) == 0) - return ParseSLEEP(pch); - - // Expression statement + switch (cchToken) + { + case 2: + if (_tcsncmp(pchToken, _T("if"), 2) == 0) + return ParseIF(pch); + if (_tcsncmp(pchToken, _T("do"), 2) == 0) + return ParseDO(pch); + break; + + case 3: + if (_tcsncmp(pchToken, _T("var"), 3) == 0) + return ParseLocalVar(pch); + if (_tcsncmp(pchToken, _T("for"), 3) == 0) + return ParseFOR(pch); + break; + + case 5: + if (_tcsncmp(pchToken, _T("while"), 5) == 0) + return ParseWHILE(pch); + if (_tcsncmp(pchToken, _T("break"), 5) == 0) + return ParseBREAK(pch); + if (m_bBehavior && _tcsncmp(pchToken, _T("sleep"), 5) == 0) + return ParseSLEEP(pch); + break; + + case 6: + if (_tcsncmp(pchToken, _T("return"), 6) == 0) + return ParseRETURN(pch); + break; + + case 8: + if (_tcsncmp(pchToken, _T("continue"), 8) == 0) + return ParseCONTINUE(pch); + break; + } + pch = ParseExpression(pchToken); Write(opDrop); - if (*pch != 0) + if (*pch == '\0') + return pch; + if (*pch != ';') { - if (*pch != ';') { SyntaxError(_T("expected ';'")); return _T(""); } - pch++; + SyntaxError(_T("expected ';'")); + return szParseError; } - - return pch; + return pch + 1; } +// CFunctionCompiler::ParseLocalVar +// Reconstructed from 5960 retail XBE @ 0x00035bf0. Behavior: for each name in +// a ','-separated list, record it in the next local slot; with "= expr" emit +// opLocal slot, the expression, opAssign, opDrop. Ends at ';' (consumed) or +// raises "Expected a ';'" and leaves the cursor where it stopped. const TCHAR* CFunctionCompiler::ParseLocalVar(const TCHAR* pch) { for (;;) { - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); + const TCHAR* pchName; + int cchName; + pch = Token(pch, pchName, cchName); - m_rgstLocal[m_nFrameSize].pchName = pchToken; - m_rgstLocal[m_nFrameSize].cchName = (short)cchToken; - m_nFrameSize++; + const int iSlot = m_nFrameSize++; + m_rgstLocal[iSlot].pchName = pchName; + m_rgstLocal[iSlot].cchName = (short)cchName; pch = SkipWhite(pch); if (*pch == '=') { - pch++; Write(opLocal); - WriteInteger(m_nFrameSize - 1); - pch = ParseExpression(pch); + WriteInteger(iSlot); + pch = ParseExpression(pch + 1); Write(opAssign); Write(opDrop); pch = SkipWhite(pch); @@ -1282,87 +1521,125 @@ const TCHAR* CFunctionCompiler::ParseLocalVar(const TCHAR* pch) } if (*pch != ';') + { SyntaxError(_T("Expected a ';'")); - else - pch++; - - return pch; + return pch; + } + return pch + 1; } -// ========================================================================= -// Section 4: XAP scene graph parser -- CClassCompiler -// Parses VRML97-like node declarations, properties, and inline scripts -// ========================================================================= +// CClassCompiler::CClassCompiler +// Inlined in 5960 (CClass::ParseClassBody @0x33d60): a zeroed compiler +// pointed at the class being built. CClassCompiler::CClassCompiler(CClass* pClass) { ASSERT(pClass != NULL); m_targetClass = pClass; } +// CClassCompiler::Compile +// Reconstructed from 5960 retail XBE @ 0x00035e50. Ghidra decompile: +// pch = SkipWhite(pch); while (*pch && *pch != '}') { ParseNode(pch); pch = SkipWhite(...); } +// Write(0); return pch; +// The trailing 0 is opNull, the end marker of a class body. const TCHAR* CClassCompiler::Compile(const TCHAR* pch) { - for (;;) - { - pch = SkipWhite(pch); - if (*pch == '\0' || *pch == '}') - break; + for (pch = SkipWhite(pch); *pch != '\0' && *pch != '}'; pch = SkipWhite(pch)) pch = ParseNode(pch); - } - Write(0); + Write((BYTE)0); return pch; } +// Copies a name token into a terminated buffer for error text, cut to fit. +static void CopyNameForMessage(TCHAR* szName, int cchMax, const TCHAR* pchName, int cchName) +{ + if (cchName > cchMax - 1) + cchName = cchMax - 1; + CopyChars(szName, pchName, cchName); + szName[cchName] = '\0'; +} + +// Reports an unknown name by briefly terminating the token in place, the way +// 5960 does it for "Unknown class" and "Unknown property". +static void ReportUnknown(CCompiler* pCompiler, const TCHAR* szFormat, const TCHAR* pchName, int cchName) +{ + TCHAR* pchEnd = (TCHAR*)pchName + cchName; + TCHAR chSaved = *pchEnd; + *pchEnd = '\0'; + pCompiler->SyntaxError(szFormat, pchName); + *pchEnd = chSaved; +} + +// CClassCompiler::ParseNode +// Reconstructed from 5960 retail XBE @ 0x00035ec0. Behavior, by first token: +// DEF name node new CMember in the next variable slot, opDefNode slot, +// then the node itself. +// USE name opUseNode with the slot of an existing member, or +// "Undefined USE". +// behavior {...} compiled as a behavior and set as a class member. +// function n(..) compiled and set as member n ("behavior" is refused). +// var a, b; class member variables. +// class N {...} nested class, defined in the class namespace. +// import N, url loads another class file under name N. +// otherwise a node: script class or built-in node class by name, +// opNewNode name, optional { props }, opEndNode. +// One trailing ',' after the item is eaten. See docs/decomp/Compiler.md. +// Theseus additions: the import trace line, and the "Expected a '}'" text +// (5960 has a typo there). const TCHAR* CClassCompiler::ParseNode(const TCHAR* pch) { const TCHAR* pchToken; int cchToken; pch = Token(pch, pchToken, cchToken); - if (cchToken == 3 && _tcsncmp(pchToken, _T("DEF"), cchToken) == 0) + if (IsKeyword(pchToken, cchToken, _T("DEF"), 3)) { - pch = Token(pch, pchToken, cchToken, true); + const TCHAR* pchName; + int cchName; + pch = Token(pch, pchName, cchName, true); CMember* pMember = new CMember; pMember->m_memberIndex = m_targetClass->m_nVarCount++; - m_targetClass->AddMember(pchToken, cchToken, pMember); + m_targetClass->AddMember(pchName, cchName, pMember); Write(opDefNode); WriteInteger(pMember->m_memberIndex); pch = ParseNode(pch); } - else if (cchToken == 3 && _tcsncmp(pchToken, _T("USE"), cchToken) == 0) + else if (IsKeyword(pchToken, cchToken, _T("USE"), 3)) { - pch = Token(pch, pchToken, cchToken, true); + const TCHAR* pchName; + int cchName; + pch = Token(pch, pchName, cchName, true); - CMember* pMember = (CMember*)m_targetClass->GetMember(pchToken, cchToken); + CMember* pMember = (CMember*)m_targetClass->GetMember(pchName, cchName); if (pMember == NULL || pMember->m_obj != objMember) { - TCHAR szBuf[256]; - if (cchToken > 255) cchToken = 255; - CopyChars(szBuf, pchToken, cchToken); - szBuf[cchToken] = 0; - SyntaxError(_T("Undefined USE '%s'"), szBuf); + TCHAR szName[256]; + CopyNameForMessage(szName, countof(szName), pchName, cchName); + SyntaxError(_T("Undefined USE '%s'"), szName); return szParseError; } Write(opUseNode); WriteInteger(pMember->m_memberIndex); } - else if (cchToken == 8 && _tcsncmp(pchToken, _T("behavior"), cchToken) == 0) + else if (IsKeyword(pchToken, cchToken, _T("behavior"), 8)) { - CFunction* pFunction; - pch = ParseFunction(pch, pFunction, true); - if (pFunction != NULL) - m_targetClass->SetMember(pchToken, cchToken, pFunction); + CFunction* pBehavior; + pch = ParseFunction(pch, pBehavior, true); + if (pBehavior != NULL) + m_targetClass->SetMember(pchToken, cchToken, pBehavior); } - else if (cchToken == 8 && _tcsncmp(pchToken, _T("function"), cchToken) == 0) + else if (IsKeyword(pchToken, cchToken, _T("function"), 8)) { - pch = Token(pch, pchToken, cchToken); - - if (cchToken == 8 && _tcsncmp(_T("behavior"), pchToken, cchToken) == 0) + const TCHAR* pchName; + int cchName; + pch = Token(pch, pchName, cchName); + if (IsKeyword(pchName, cchName, _T("behavior"), 8)) { SyntaxError(_T("invalid function name")); return szParseError; @@ -1371,58 +1648,55 @@ const TCHAR* CClassCompiler::ParseNode(const TCHAR* pch) CFunction* pFunction; pch = ParseFunction(pch, pFunction); if (pFunction != NULL) - { - m_targetClass->SetMember(pchToken, cchToken, pFunction); - } + m_targetClass->SetMember(pchName, cchName, pFunction); } - else if (cchToken == 3 && _tcsncmp(pchToken, _T("var"), cchToken) == 0) + else if (IsKeyword(pchToken, cchToken, _T("var"), 3)) { pch = ParseMemberVar(pch); } - else if (cchToken == 5 && _tcsncmp(pchToken, _T("class"), cchToken) == 0) + else if (IsKeyword(pchToken, cchToken, _T("class"), 5)) { pch = ParseClass(pch); } - else if (cchToken == 6 && _tcsncmp(pchToken, _T("import"), cchToken) == 0) + else if (IsKeyword(pchToken, cchToken, _T("import"), 6)) { - pch = Token(pch, pchToken, cchToken); + const TCHAR* pchName; + int cchName; + pch = Token(pch, pchName, cchName); + pch = SkipWhite(pch); - if (*pch != ',') { SyntaxError(_T("Expected ','")); return szParseError; } - pch++; + if (*pch != ',') + { + SyntaxError(_T("Expected ','")); + return szParseError; + } const TCHAR* pchURL; int cchURL; - pch = Token(pch, pchURL, cchURL); + pch = Token(pch + 1, pchURL, cchURL); TCHAR szURL[MAX_PATH]; CopyChars(szURL, pchURL, cchURL); szURL[cchURL] = '\0'; - CClass* pClass = new CClass; - + CClass* pImported = new CClass; TRACE(_T("Importing "), szURL); - - if (!pClass->Load(szURL)) + if (!pImported->Load(szURL)) { - delete pClass; + delete pImported; return szParseError; } - g_classes->Define(pchToken, cchToken, (CNode*)pClass); + g_classes->Define(pchName, cchName, (CNode*)pImported); } else { - // Node instantiation CNodeClass* pNodeClass = LookupClass(pchToken, cchToken); if (pNodeClass == NULL) pNodeClass = CNodeClass::FindByName(pchToken, cchToken); - if (pNodeClass == NULL) { - TCHAR chSav = pchToken[cchToken]; - ((TCHAR*)pchToken)[cchToken] = '\0'; - SyntaxError(_T("Unknown class: %s"), pchToken); - ((TCHAR*)pchToken)[cchToken] = chSav; + ReportUnknown(this, _T("Unknown class: %s"), pchToken, cchToken); return szParseError; } @@ -1430,11 +1704,9 @@ const TCHAR* CClassCompiler::ParseNode(const TCHAR* pch) WriteString(pchToken, cchToken); pch = SkipWhite(pch); - if (*pch == '{') { - pch++; - pch = ParseProps(pch, pNodeClass); + pch = ParseProps(pch + 1, pNodeClass); if (*pch != '}') { SyntaxError(_T("Expected a '}'")); @@ -1449,159 +1721,193 @@ const TCHAR* CClassCompiler::ParseNode(const TCHAR* pch) pch = SkipWhite(pch); if (*pch == ',') pch++; - return pch; } +// CClassCompiler::ParseChildren +// Reconstructed from 5960 retail XBE @ 0x000363c0. Ghidra decompile: +// pch = SkipWhite(pch); if (*pch != '[') return ParseNode(pch); +// pch = SkipWhite(pch + 1); while (*pch && *pch != '}' && *pch != ']') { ParseNode; SkipWhite; } +// if (*pch != ']') { SyntaxError(...); return pch; } return pch + 1; +// A children value is either one node or a bracketed list of them. const TCHAR* CClassCompiler::ParseChildren(const TCHAR* pch) { pch = SkipWhite(pch); + if (*pch != '[') + return ParseNode(pch); - if (*pch == '[') - { - pch++; - pch = SkipWhite(pch); - - for (;;) - { - pch = SkipWhite(pch); - if (*pch == '\0' || *pch == '}' || *pch == ']') - break; - pch = ParseNode(pch); - } + for (pch = SkipWhite(pch + 1); *pch != '\0' && *pch != '}' && *pch != ']'; pch = SkipWhite(pch)) + pch = ParseNode(pch); - if (*pch == ']') - pch++; - else - SyntaxError(_T("Expected a ']'")); - } - else + if (*pch != ']') { - pch = ParseNode(pch); + SyntaxError(_T("Expected a ']'")); + return pch; } - - return pch; + return pch + 1; } +// CClassCompiler::ParseProps +// Reconstructed from 5960 retail XBE @ 0x00036440. Behavior: until '}' or the +// end of text, read a name. "behavior" and "function name" compile an inline +// function, append it to the class's instance functions and emit opFunction +// with the name and its index. Any other name must be a property of the node +// class or it's "Unknown property". children / node arrays emit opInitArray +// with the property offset, the child list, opEndArray. A node property emits +// opNewNodeProp, the PRD, and the node. Everything else is read by type +// (bool, int, float, string, vec3/color, vec4/quaternion, int and vector +// arrays) and emitted as opInitProp, the PRD, then the value: strings as +// length + characters, the rest as byte count + bytes. +// See docs/decomp/Compiler.md for the type to reader table. const TCHAR* CClassCompiler::ParseProps(const TCHAR* pch, CNodeClass* pNodeClass) { - for (;;) + for (pch = SkipWhite(pch); *pch != '\0' && *pch != '}'; pch = SkipWhite(pch)) { - pch = SkipWhite(pch); - if (*pch == '\0' || *pch == '}') - break; - - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); - - // Inline behavior - if (cchToken == 8 && _tcsncmp(_T("behavior"), pchToken, cchToken) == 0) - { - CFunction* pFunction; - pch = ParseFunction(pch, pFunction, true); - if (pFunction != NULL) - { - int nFunction = m_targetClass->m_instanceFunctions.GetLength(); - m_targetClass->m_instanceFunctions.AddNode((CNode*)pFunction); - Write(opFunction); - WriteString(pchToken, cchToken); - WriteInteger(nFunction); - } - continue; - } + const TCHAR* pchName; + int cchName; + pch = Token(pch, pchName, cchName); - // Inline function - if (cchToken == 8 && _tcsncmp(_T("function"), pchToken, cchToken) == 0) + const bool bBehavior = IsKeyword(pchName, cchName, _T("behavior"), 8); + if (bBehavior || IsKeyword(pchName, cchName, _T("function"), 8)) { - pch = Token(pch, pchToken, cchToken); - if (cchToken == 8 && _tcsncmp(_T("behavior"), pchToken, cchToken) == 0) + if (!bBehavior) { - SyntaxError(_T("invalid function name")); - return szParseError; + pch = Token(pch, pchName, cchName); + if (IsKeyword(pchName, cchName, _T("behavior"), 8)) + { + SyntaxError(_T("invalid function name")); + return szParseError; + } } CFunction* pFunction; - pch = ParseFunction(pch, pFunction); + pch = ParseFunction(pch, pFunction, bBehavior); if (pFunction != NULL) { - int nFunction = m_targetClass->m_instanceFunctions.GetLength(); + int iFunction = m_targetClass->m_instanceFunctions.GetLength(); m_targetClass->m_instanceFunctions.AddNode((CNode*)pFunction); Write(opFunction); - WriteString(pchToken, cchToken); - WriteInteger(nFunction); + WriteString(pchName, cchName); + WriteInteger(iFunction); } continue; } - // Property assignment - const PRD* pprd = pNodeClass->FindProp(pchToken, cchToken); - + const PRD* pprd = pNodeClass->FindProp(pchName, cchName); if (pprd == NULL) { - TCHAR chSav = pchToken[cchToken]; - ((TCHAR*)pchToken)[cchToken] = '\0'; - SyntaxError(_T("Unknown property: %s"), pchToken); - ((TCHAR*)pchToken)[cchToken] = chSav; + ReportUnknown(this, _T("Unknown property: %s"), pchName, cchName); return szParseError; } - if (pprd->nType == pt_children || pprd->nType == pt_nodearray) + switch (pprd->nType) { + case pt_children: + case pt_nodearray: Write(opInitArray); WriteInteger(PTR2INT(pprd->pbOffset)); pch = ParseChildren(pch); Write(opEndArray); - } - else if (pprd->nType == pt_node) - { + continue; + + case pt_node: Write(opNewNodeProp); Write(pprd, sizeof(PRD)); pch = ParseNode(pch); + continue; + + default: + break; } - else + + union { - union { bool b; int i; float n; float v[4]; TCHAR* s; } value; - BYTE* pbArray = NULL; - int cbValue = 0; + bool b; + int i; + float n; + float v[4]; + TCHAR* sz; + } value; + BYTE* pbArray = NULL; + int cbValue = 0; - switch (pprd->nType) - { - case pt_boolean: pch = ParseBoolean(pch, value.b); cbValue = sizeof(bool); break; - case pt_integer: pch = ParseInteger(pch, value.i); cbValue = sizeof(int); break; - case pt_number: pch = ParseNumber(pch, value.n); cbValue = sizeof(float); break; - case pt_string: pch = ParseString(pch, value.s); cbValue = _tcslen(value.s); break; - case pt_vec3: - case pt_color: pch = ParseVec3(pch, value.v); cbValue = sizeof(D3DXVECTOR3); break; - case pt_vec4: - case pt_quaternion: pch = ParseVec4(pch, value.v); cbValue = sizeof(D3DXVECTOR4); break; - case pt_intarray: pch = ParseIntArray(pch, pbArray, cbValue); break; - case pt_numarray: pch = ParseVecArray(pch, pbArray, cbValue, 1); break; - case pt_vec2array: pch = ParseVecArray(pch, pbArray, cbValue, 2); break; - case pt_vec3array: pch = ParseVecArray(pch, pbArray, cbValue, 3); break; - case pt_vec4array: pch = ParseVecArray(pch, pbArray, cbValue, 4); break; - } + switch (pprd->nType) + { + case pt_integer: + pch = ParseInteger(pch, value.i); + cbValue = sizeof(int); + break; - Write(opInitProp); - Write(pprd, sizeof(PRD)); + case pt_boolean: + pch = ParseBoolean(pch, value.b); + cbValue = sizeof(bool); + break; + + case pt_number: + pch = ParseNumber(pch, value.n); + cbValue = sizeof(float); + break; + + case pt_vec3: + case pt_color: + pch = ParseVec3(pch, value.v); + cbValue = 3 * sizeof(float); + break; + + case pt_vec4: + case pt_quaternion: + pch = ParseVec4(pch, value.v); + cbValue = 4 * sizeof(float); + break; + + case pt_string: + pch = ParseString(pch, value.sz); + cbValue = _tcslen(value.sz); + break; + + case pt_intarray: + pch = ParseIntArray(pch, pbArray, cbValue); + break; + + case pt_numarray: + pch = ParseVecArray(pch, pbArray, cbValue, 1); + break; + + case pt_vec2array: + pch = ParseVecArray(pch, pbArray, cbValue, 2); + break; + + case pt_vec3array: + pch = ParseVecArray(pch, pbArray, cbValue, 3); + break; + + case pt_vec4array: + pch = ParseVecArray(pch, pbArray, cbValue, 4); + break; + + default: + break; + } + + Write(opInitProp); + Write(pprd, sizeof(PRD)); - if (pprd->nType == pt_string) + if (pprd->nType == pt_string) + { + WriteString(value.sz, cbValue); + delete[] value.sz; + } + else + { + WriteInteger(cbValue); + if (pbArray != NULL) { - WriteString(value.s, cbValue); - delete[] value.s; + Write(pbArray, cbValue); + delete[] pbArray; } else { - WriteInteger(cbValue); - if (pbArray != NULL) - { - Write(pbArray, cbValue); - delete[] pbArray; - } - else - { - Write(&value, cbValue); - } + Write(&value, cbValue); } } } @@ -1609,23 +1915,26 @@ const TCHAR* CClassCompiler::ParseProps(const TCHAR* pch, CNodeClass* pNodeClass return pch; } +// CClassCompiler::ParseMemberVar +// Reconstructed from 5960 retail XBE @ 0x00036ad0. Behavior: for each name in +// a ','-separated list, add a CMember in the next variable slot. An '=' +// initializer is refused ("not implemented") and stops the parse. Ends at ';' +// or raises "Expected a ';'" leaving the cursor where it stopped. const TCHAR* CClassCompiler::ParseMemberVar(const TCHAR* pch) { for (;;) { - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); + const TCHAR* pchName; + int cchName; + pch = Token(pch, pchName, cchName); CMember* pMember = new CMember; pMember->m_memberIndex = m_targetClass->m_nVarCount++; - m_targetClass->AddMember(pchToken, cchToken, pMember); + m_targetClass->AddMember(pchName, cchName, pMember); pch = SkipWhite(pch); - if (*pch == '=') { - pch++; SyntaxError(_T("Sorry, class member initializers have not been implemented yet!")); return szParseError; } @@ -1636,48 +1945,47 @@ const TCHAR* CClassCompiler::ParseMemberVar(const TCHAR* pch) } if (*pch != ';') + { SyntaxError(_T("Expected a ';'")); - else - pch++; - - return pch; + return pch; + } + return pch + 1; } +// CClassCompiler::ParseFunction +// Reconstructed from 5960 retail XBE @ 0x00036c30. Behavior: set up a fresh +// CFunctionCompiler (behavior flag from the caller). A function needs a +// parameter list: '(' names split by ',' ')', each name becoming the next +// local; no '(' just returns with no function, a bad list is "expected ')'". +// Behaviors take no list. The body must start with '{' or there's no +// function. The block is compiled, opNull opRet is appended as the implicit +// "return null;" and the ops become the CFunction. const TCHAR* CClassCompiler::ParseFunction(const TCHAR* pch, CFunction*& pFunction, bool bBehavior) { - CFunctionCompiler compiler; - compiler.m_bBehavior = bBehavior; + CFunctionCompiler body; + body.m_bBehavior = bBehavior; pFunction = NULL; pch = SkipWhite(pch); if (!bBehavior) { - // Parse parameter list if (*pch != '(') - { return pch; - } - pch++; - for (;;) + for (pch = SkipWhite(pch + 1); *pch != '\0' && *pch != ')'; pch = SkipWhite(pch + 1)) { - pch = SkipWhite(pch); - if (*pch == '\0' || *pch == ')') - break; + const TCHAR* pchName; + int cchName; + pch = Token(pch, pchName, cchName); - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); - - compiler.m_rgstLocal[compiler.m_nFrameSize].pchName = pchToken; - compiler.m_rgstLocal[compiler.m_nFrameSize].cchName = (short)cchToken; - compiler.m_nFrameSize++; + ST& local = body.m_rgstLocal[body.m_nFrameSize++]; + local.pchName = pchName; + local.cchName = (short)cchName; pch = SkipWhite(pch); if (*pch != ',') break; - pch++; } if (*pch != ')') @@ -1688,66 +1996,62 @@ const TCHAR* CClassCompiler::ParseFunction(const TCHAR* pch, CFunction*& pFuncti pch++; } - // Parse function body - { - pch = SkipWhite(pch); - if (*pch != '{') - { - return pch; - } - pch = compiler.ParseBlock(pch); - } + pch = SkipWhite(pch); + if (*pch != '{') + return pch; - compiler.Write(opNull); - compiler.Write(opRet); + pch = body.ParseBlock(pch); + body.Write(opNull); + body.Write(opRet); - pFunction = compiler.CreateFunction(); + pFunction = body.CreateFunction(); return pch; } +// CClassCompiler::ParseClass +// Reconstructed from 5960 retail XBE @ 0x00036e50. Ghidra decompile: +// Token(pch, &name, &cch); pch = SkipWhite(...); if (*pch != '{') { SyntaxError; return L""; } +// c = new CClass; pch = c->ParseClassBody(pch + 1); +// if (*pch != '}') { SyntaxError; return L""; } g_classes->Define(name, cch, c); return pch + 1; +// The debug trace of the class name is Theseus's. const TCHAR* CClassCompiler::ParseClass(const TCHAR* pch) { - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); + const TCHAR* pchName; + int cchName; + pch = Token(pch, pchName, cchName); #ifdef _DEBUG { - TCHAR chSav = pchToken[cchToken]; - ((TCHAR*)pchToken)[cchToken] = '\0'; - TRACE(_T("class %s\n"), pchToken); - ((TCHAR*)pchToken)[cchToken] = chSav; + TCHAR szName[256]; + CopyNameForMessage(szName, countof(szName), pchName, cchName); + TRACE(_T("class %s\n"), szName); } #endif - // TODO: base class? interfaces? - pch = SkipWhite(pch); if (*pch != '{') { SyntaxError(_T("Expected '{'\n")); return szParseError; } - pch++; CClass* pClass = new CClass; - pch = pClass->ParseClassBody(pch); - + pch = pClass->ParseClassBody(pch + 1); if (*pch != '}') { SyntaxError(_T("Expected '}'")); return szParseError; } - pch++; - g_classes->Define(pchToken, cchToken, (CNode*)pClass); - return pch; + g_classes->Define(pchName, cchName, (CNode*)pClass); + return pch + 1; } -// ========================================================================= -// Class namespace init/cleanup -// ========================================================================= +// Class_Init / Class_Exit +// Reconstructed from 5960 retail XBE @ 0x00036f00 / 0x00036f30. Ghidra decompile: +// g_classes = new CNameSpace; // one word: the list head, zeroed +// delete g_classes; // walks the list releasing each class void Class_Init() { g_classes = new CNameSpace; From 761036065cca78b29ec769b78311b7244e648301 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:13:31 -0500 Subject: [PATCH 040/167] Fixed four script runner overruns Numbers from 1e12 up overran the 20 character buffer when turned into text, and Vector3 text could do the same, so both buffers now fit the biggest float. launch() wrote through NULL on a bare file name and copied script paths into MAX_PATH without a limit. The runner also printed a junk line number for an error before the first statement. --- theseus/shared/xap_vm.cpp | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/theseus/shared/xap_vm.cpp b/theseus/shared/xap_vm.cpp index 9d2e6b2..b9acc0d 100644 --- a/theseus/shared/xap_vm.cpp +++ b/theseus/shared/xap_vm.cpp @@ -58,10 +58,11 @@ CNumObject::CNumObject(const TCHAR* szValue) // n = swprintf(buf20, L"%f", (double)m_value); p = buf + n - 1; // while (*p == '0') p--; if (*p == '.') p--; p[1] = 0; // return new CStrObject(buf); -// So 2.500000 prints as "2.5" and 3.000000 as "3". +// So 2.500000 prints as "2.5" and 3.000000 as "3". The binary's 20 characters +// overflow from 1e12 up, so Theseus makes room for the biggest float. CStrObject* CNumObject::ToStr() { - TCHAR szNum[20]; + TCHAR szNum[64]; int cch = _stprintf(szNum, _T("%f"), m_value); TCHAR* pchLast = &szNum[cch - 1]; @@ -118,10 +119,10 @@ CVec3Object::CVec3Object(const D3DXVECTOR3& v) // CVec3Object::ToStr // Reconstructed from 5960 retail XBE @ 0x000390f0. Decompile: // swprintf(buf60, L"%f %f %f", x, y, z); return new CStrObject(buf); -// No zero trimming here, unlike numbers. +// No zero trimming here, unlike numbers. Theseus sizes it for three huge floats. CStrObject* CVec3Object::ToStr() { - TCHAR szVec[60]; + TCHAR szVec[160]; _stprintf(szVec, _T("%f %f %f"), m_x, m_y, m_z); return new CStrObject(szVec); } @@ -857,11 +858,13 @@ CObject* LookupMember(CObject* pThis, const TCHAR* pchName, int cchName) // m_sp = m_nop = 0; m_ops = NULL; m_self = m_thisRef = obj; // m_bError = false; m_nextContext = NULL; zero the stack; // and torn down with ResetFunc. 5960's stack holds 100 entries; Theseus's -// holds 256 (see runner.h). +// holds 256 (see runner.h). Theseus also starts the line at 0 so an error +// before the first statement doesn't print a junk line number. CRunner::CRunner(CObject* pObject) { ASSERT(pObject != NULL); + m_nLine = 0; m_wakeup = 0.0f; m_nop = 0; m_ops = NULL; @@ -1657,24 +1660,28 @@ BOOL CRunner::ExecuteBuiltIn(const TCHAR* pchName, int cchName, int nParam, CObj CStrObject* pPath = pPathArg->ToStr(); const TCHAR* szPath = pPath->GetSz(); + // Theseus: script paths longer than MAX_PATH get cut instead of overrunning. TCHAR szDir[MAX_PATH]; if (nParam == 2) { CObject* pDirArg = Dereference(rgParam[1]); CStrObject* pDir = pDirArg->ToStr(); - _tcscpy(szDir, pDir->GetSz()); + _tcsncpy(szDir, pDir->GetSz(), MAX_PATH - 1); + szDir[MAX_PATH - 1] = '\0'; pDir->Release(); pDirArg->Release(); } else { - _tcscpy(szDir, szPath); + _tcsncpy(szDir, szPath, MAX_PATH - 1); + szDir[MAX_PATH - 1] = '\0'; TCHAR* pchSlash = _tcsrchr(szDir, '/'); if (pchSlash != NULL) *pchSlash = '\\'; pchSlash = _tcsrchr(szDir, '\\'); - ASSERT(pchSlash != NULL); - *pchSlash = '\0'; + // Theseus: a bare file name keeps the whole path instead of writing through NULL. + if (CHECK(pchSlash != NULL)) + *pchSlash = '\0'; } TRACE(_T("Launch title: %s, %s\n"), szPath, szDir); From 8c5fb2079c97e433c7db83b424454c1cae1070b3 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:14:22 -0500 Subject: [PATCH 041/167] Stopped broken scripts from hanging or overrunning the compiler An array cut off before its ']' made the compiler spin forever, more than 100 locals or parameters wrote past the slot table, and a long import path overran its buffer. Each one is now a syntax error. The 5960 dash has all three, so this is on top of what the binary does. --- theseus/engine/xap_compile.cpp | 44 ++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/theseus/engine/xap_compile.cpp b/theseus/engine/xap_compile.cpp index befbe91..060c648 100755 --- a/theseus/engine/xap_compile.cpp +++ b/theseus/engine/xap_compile.cpp @@ -466,6 +466,16 @@ const TCHAR* ParseVec4(const TCHAR* pch, float v[4]) return pch; } +// The array readers stop at the end of the text instead of spinning on it +// forever. Theseus fix; 5960 has no such check. +static bool ArrayRanOut(const TCHAR* pch) +{ + if (*pch != '\0') + return false; + SyntaxError(_T("Expected ']' to end array")); + return true; +} + // ParseIntArray / ParseVecArray // Reconstructed from 5960 retail XBE @ 0x000333a0 / 0x00033490. Behavior: // demand '[', then walk the list twice. The first walk only counts values (a @@ -488,7 +498,7 @@ const TCHAR* ParseIntArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray) const int nLineFirst = g_nLine; int cValues = 0; - for (pch = SkipWhite(pchFirst); *pch != ']'; pch = SkipWhite(pch)) + for (pch = SkipWhite(pchFirst); *pch != ']' && !ArrayRanOut(pch); pch = SkipWhite(pch)) { int nIgnored; pch = ParseInteger(pch, nIgnored); @@ -501,12 +511,15 @@ const TCHAR* ParseIntArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray) pch++; } + if (*pch == '\0') + return pch; + cbArray = cValues * sizeof(int); pbArray = new BYTE[cbArray]; int* pnOut = (int*)pbArray; g_nLine = nLineFirst; - for (pch = SkipWhite(pchFirst); *pch != ']'; pch = SkipWhite(pch)) + for (pch = SkipWhite(pchFirst); *pch != ']' && !ArrayRanOut(pch); pch = SkipWhite(pch)) { pch = ParseInteger(pch, *pnOut++); @@ -535,7 +548,7 @@ const TCHAR* ParseVecArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray, int n const int nLineFirst = g_nLine; int cValues = 0; - for (pch = SkipWhite(pchFirst); *pch != ']'; pch = SkipWhite(pch)) + for (pch = SkipWhite(pchFirst); *pch != ']' && !ArrayRanOut(pch); pch = SkipWhite(pch)) { for (int i = 0; i < nVecSize; i++) { @@ -551,12 +564,15 @@ const TCHAR* ParseVecArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray, int n pch++; } + if (*pch == '\0') + return pch; + cbArray = cValues * sizeof(float); pbArray = new BYTE[cbArray]; float* pnOut = (float*)pbArray; g_nLine = nLineFirst; - for (pch = SkipWhite(pchFirst); *pch != ']'; pch = SkipWhite(pch)) + for (pch = SkipWhite(pchFirst); *pch != ']' && !ArrayRanOut(pch); pch = SkipWhite(pch)) { for (int i = 0; i < nVecSize; i++) pch = ParseNumber(pch, *pnOut++); @@ -1500,6 +1516,13 @@ const TCHAR* CFunctionCompiler::ParseLocalVar(const TCHAR* pch) int cchName; pch = Token(pch, pchName, cchName); + // Theseus fix: 5960 runs past the 100 local slots without noticing. + if (!CHECK(m_nFrameSize < (int)countof(m_rgstLocal))) + { + SyntaxError(_T("Too many local variables")); + return szParseError; + } + const int iSlot = m_nFrameSize++; m_rgstLocal[iSlot].pchName = pchName; m_rgstLocal[iSlot].cchName = (short)cchName; @@ -1675,7 +1698,13 @@ const TCHAR* CClassCompiler::ParseNode(const TCHAR* pch) int cchURL; pch = Token(pch + 1, pchURL, cchURL); + // Theseus fix: 5960 copies the url into its buffer without a length check. TCHAR szURL[MAX_PATH]; + if (!CHECK(cchURL < MAX_PATH)) + { + SyntaxError(_T("Import path is too long")); + return szParseError; + } CopyChars(szURL, pchURL, cchURL); szURL[cchURL] = '\0'; @@ -1979,6 +2008,13 @@ const TCHAR* CClassCompiler::ParseFunction(const TCHAR* pch, CFunction*& pFuncti int cchName; pch = Token(pch, pchName, cchName); + // Theseus fix: 5960 runs past the 100 local slots without noticing. + if (!CHECK(body.m_nFrameSize < (int)countof(body.m_rgstLocal))) + { + SyntaxError(_T("Too many parameters")); + return szParseError; + } + ST& local = body.m_rgstLocal[body.m_nFrameSize++]; local.pchName = pchName; local.cchName = (short)cchName; From e2ef74d1eb994166bde70b5fd772d5549cb470fb Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:14:43 -0500 Subject: [PATCH 042/167] Fixed two 64-bit bugs in the music visualizer surface Line() folded coordinates through a long, which is 64 bits on desktop, so any negative coordinate became a huge positive one and the line got dropped instead of clipped. The in-place fade flushed rows as unsigned long, copying twice each row and writing a row past the image. Both now use 32-bit types, same as the Xbox build always had. --- theseus/render/tmap_system.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/theseus/render/tmap_system.cpp b/theseus/render/tmap_system.cpp index 6098341..e164870 100644 --- a/theseus/render/tmap_system.cpp +++ b/theseus/render/tmap_system.cpp @@ -198,7 +198,8 @@ void CSurfx::Fade(char* inPix, long inBytesPerRow, long inX, long inY, DeltaFiel curBufRow = sTemp + ((curBufRowNum + y) % bufRows) * inBytesPerRow; for (x = 0; x < p; x += 1) { - ((unsigned long*)inPix)[x] = ((unsigned long*)curBufRow)[x]; + // DWORD, not unsigned long, which is 8 bytes on 64-bit desktop builds. + ((DWORD*)inPix)[x] = ((DWORD*)curBufRow)[x]; } inPix += inBytesPerRow; @@ -415,10 +416,11 @@ void CSurfx::Line(int sx, int sy, int ex, int ey, unsigned char color) long xmov, ymov, dx, dy, t, j, lw; long penExtents; - sx = (((long)(sx & 0x80000000)) >> 1) | (sx & 0x3FFFFFFF); - ex = (((long)(ex & 0x80000000)) >> 1) | (ex & 0x3FFFFFFF); - sy = (((long)(sy & 0x80000000)) >> 1) | (sy & 0x3FFFFFFF); - ey = (((long)(ey & 0x80000000)) >> 1) | (ey & 0x3FFFFFFF); + // int on purpose: through a 64-bit long a negative coordinate folds to a huge positive one. + sx = (((int)(sx & 0x80000000)) >> 1) | (sx & 0x3FFFFFFF); + ex = (((int)(ex & 0x80000000)) >> 1) | (ex & 0x3FFFFFFF); + sy = (((int)(sy & 0x80000000)) >> 1) | (sy & 0x3FFFFFFF); + ey = (((int)(ey & 0x80000000)) >> 1) | (ey & 0x3FFFFFFF); lw = mLineWidth; if (mLineWidth > 3) { From 9eba93038e043274e9eaaf7ed4dbdd3c2c5115b5 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:17:26 -0500 Subject: [PATCH 043/167] Dropped the compiler's debug-only lines and unused CParser The ASSERTs, TRACE lines and _DEBUG block never run in a retail build, and nothing ever used CParser, so they're gone. Fixup now checks its address with CHECK since it writes into the op buffer. The shipped XAPs still compile to the same bytes. --- theseus/engine/xap_compile.cpp | 64 +--------------------------------- theseus/engine/xap_compile.h | 36 ++----------------- 2 files changed, 4 insertions(+), 96 deletions(-) diff --git a/theseus/engine/xap_compile.cpp b/theseus/engine/xap_compile.cpp index 060c648..7771e0f 100755 --- a/theseus/engine/xap_compile.cpp +++ b/theseus/engine/xap_compile.cpp @@ -535,8 +535,6 @@ const TCHAR* ParseIntArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray) const TCHAR* ParseVecArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray, int nVecSize) { - ASSERT(nVecSize > 0 && nVecSize <= 4); - pch = SkipWhite(pch); if (*pch != '[') { @@ -588,50 +586,6 @@ const TCHAR* ParseVecArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray, int n } -// CParser is a Theseus helper with no counterpart in the 5960 binary. -// Kept as it was. - -CParser::CParser(const TCHAR* szFilePath, const TCHAR* pchFile, int cchFile) -{ - m_pchFile = pchFile; - m_cchFile = (cchFile == -1) ? _tcslen(pchFile) : cchFile; - m_filePath = szFilePath; - m_pch = m_pchFile; - m_nLine = 1; -} - -CParser::~CParser() -{ -} - -void CParser::SkipWhite() -{ - m_pch = ::SkipWhite(m_pch); -} - -bool CParser::Token(const TCHAR*& pchToken, int& cchToken, bool bAllowPaths) -{ - m_pch = ::Token(m_pch, pchToken, cchToken, bAllowPaths); - return cchToken != 0; -} - -void CParser::SyntaxError(const TCHAR* szMsg, ...) -{ - va_list args; - va_start(args, szMsg); - - TCHAR szBuffer[512]; - _vsntprintf(szBuffer, countof(szBuffer), szMsg, args); - - TCHAR szMessage[1024]; - _stprintf(szMessage, _T("Syntax Error\n\nFile: %s\nLine: %d\n\n%s"), m_filePath, m_nLine, szBuffer); - - Trace(_T("\007%s\n"), szMessage); - - va_end(args); -} - - // CCompiler::CCompiler / ~CCompiler // 5960 inlines the constructor (see the CFunctionCompiler set up in // ParseFunction @0x36c30: ops count, buffer, size and error all zeroed). @@ -946,8 +900,6 @@ const TCHAR* CFunctionCompiler::ParseTerm(const TCHAR* pch) // Write(opArray); return pch + 1; const TCHAR* CFunctionCompiler::ParseArray(const TCHAR* pch) { - ASSERT(*pch == '['); - pch = SkipWhite(ParseExpression(pch + 1)); if (*pch != ']') { @@ -968,8 +920,6 @@ const TCHAR* CFunctionCompiler::ParseArray(const TCHAR* pch) // The callee reference is already on the stack; the count is a single byte. const TCHAR* CFunctionCompiler::ParseCall(const TCHAR* pch) { - ASSERT(*pch == '('); - int cArgs = 0; pch = SkipWhite(pch + 1); if (*pch != ')') @@ -1557,7 +1507,6 @@ const TCHAR* CFunctionCompiler::ParseLocalVar(const TCHAR* pch) // pointed at the class being built. CClassCompiler::CClassCompiler(CClass* pClass) { - ASSERT(pClass != NULL); m_targetClass = pClass; } @@ -1609,8 +1558,7 @@ static void ReportUnknown(CCompiler* pCompiler, const TCHAR* szFormat, const TCH // otherwise a node: script class or built-in node class by name, // opNewNode name, optional { props }, opEndNode. // One trailing ',' after the item is eaten. See docs/decomp/Compiler.md. -// Theseus additions: the import trace line, and the "Expected a '}'" text -// (5960 has a typo there). +// Theseus spells "Expected a '}'" right; 5960 has a typo there. const TCHAR* CClassCompiler::ParseNode(const TCHAR* pch) { const TCHAR* pchToken; @@ -1709,7 +1657,6 @@ const TCHAR* CClassCompiler::ParseNode(const TCHAR* pch) szURL[cchURL] = '\0'; CClass* pImported = new CClass; - TRACE(_T("Importing "), szURL); if (!pImported->Load(szURL)) { delete pImported; @@ -2049,21 +1996,12 @@ const TCHAR* CClassCompiler::ParseFunction(const TCHAR* pch, CFunction*& pFuncti // Token(pch, &name, &cch); pch = SkipWhite(...); if (*pch != '{') { SyntaxError; return L""; } // c = new CClass; pch = c->ParseClassBody(pch + 1); // if (*pch != '}') { SyntaxError; return L""; } g_classes->Define(name, cch, c); return pch + 1; -// The debug trace of the class name is Theseus's. const TCHAR* CClassCompiler::ParseClass(const TCHAR* pch) { const TCHAR* pchName; int cchName; pch = Token(pch, pchName, cchName); -#ifdef _DEBUG - { - TCHAR szName[256]; - CopyNameForMessage(szName, countof(szName), pchName, cchName); - TRACE(_T("class %s\n"), szName); - } -#endif - pch = SkipWhite(pch); if (*pch != '{') { diff --git a/theseus/engine/xap_compile.h b/theseus/engine/xap_compile.h index 1880844..a70c8da 100755 --- a/theseus/engine/xap_compile.h +++ b/theseus/engine/xap_compile.h @@ -35,37 +35,6 @@ const TCHAR* ParseVecArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray, int n int ExpandCString(TCHAR* szString, int cchMaxString, const TCHAR* pchToken, int cchToken); -// Lightweight tokenizer state for the script source-text walkers. -// Each parser instance pins the start of the file, the file path -// (used in syntax error messages), the current cursor, and the -// current line number. -class CParser -{ -public: - CParser(const TCHAR* szFilePath, const TCHAR* pchFile, int cchFile = -1); - ~CParser(); - - void SkipWhite(); - bool Token(const TCHAR*& pchToken, int& cchToken, bool bAllowPaths = false); - void SyntaxError(const TCHAR* szMsg, ...); - bool AtEnd() const; - - const TCHAR* m_pchFile; - int m_cchFile; - - const TCHAR* m_filePath; - - const TCHAR* m_pch; - int m_nLine; -}; - -inline bool CParser::AtEnd() const -{ - return (m_pch >= m_pchFile + m_cchFile); -} - - - class CCompiler { public: @@ -87,10 +56,11 @@ class CCompiler inline UINT GetAddress() { return m_nop; } + // Patches a jump or frame size written earlier. 5960 stores straight in. inline void Fixup(UINT nAddress, UINT nValue) { - ASSERT(nAddress < m_nop); - CopyMemory(&m_ops[nAddress], &nValue, sizeof (UINT)); + if (CHECK(nAddress + sizeof (UINT) <= m_nop)) + CopyMemory(&m_ops[nAddress], &nValue, sizeof (UINT)); } CFunction* CreateFunction(); From 608aabe9d9bafdd30f252d1da18e0be3fede25a9 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:17:59 -0500 Subject: [PATCH 044/167] Moved the desktop folder scans onto the host layer Most desktop files still had one folder scan for Windows and another for mac and Linux. They all go through host.h now, along with the user data folder, the platform name and the default shader folder. Needs a Windows test. --- theseus/desktop/app_paths.cpp | 36 +---------- theseus/desktop/audio_sdl.cpp | 82 +++++-------------------- theseus/desktop/cdaudio.cpp | 6 +- theseus/desktop/esde.cpp | 47 +++----------- theseus/desktop/hdd_browser.cpp | 4 +- theseus/desktop/host.h | 16 +++++ theseus/desktop/host_posix.cpp | 22 +++++++ theseus/desktop/host_posix_app.cpp | 45 ++++++++++++++ theseus/desktop/host_win.cpp | 9 +++ theseus/desktop/host_win_app.cpp | 18 ++++++ theseus/desktop/icon_compress.cpp | 33 +++------- theseus/desktop/image.cpp | 22 ++----- theseus/desktop/imgui_impl_bgfx.cpp | 10 +-- theseus/desktop/launchers/retroarch.cpp | 4 +- theseus/desktop/menu_bar.cpp | 12 +--- theseus/desktop/path_ci.h | 64 +++++-------------- theseus/desktop/path_template.cpp | 27 ++------ theseus/desktop/platform_env.h | 13 ++-- theseus/desktop/plex_client.cpp | 10 +-- theseus/desktop/preloader.cpp | 82 +++++-------------------- theseus/desktop/sdl_main.cpp | 8 +-- theseus/desktop/title_maker.cpp | 54 ++++------------ theseus/desktop/xap_editor.cpp | 51 +++------------ 23 files changed, 224 insertions(+), 451 deletions(-) diff --git a/theseus/desktop/app_paths.cpp b/theseus/desktop/app_paths.cpp index 041e6e0..7d6a31b 100644 --- a/theseus/desktop/app_paths.cpp +++ b/theseus/desktop/app_paths.cpp @@ -9,7 +9,6 @@ #include #include -#include "platform.h" #include "host.h" // The only thing that stays with the binary. Nobody hand edits a .bin, and @@ -40,45 +39,14 @@ const char* Plat_UserDataDir() { if (s_init) return s_dir; s_init = true; -#ifdef THESEUS_HOST_WIN - const char* appdata = getenv("APPDATA"); - if (appdata && *appdata) - snprintf(s_dir, sizeof(s_dir), "%s\\Theseus", appdata); - else - snprintf(s_dir, sizeof(s_dir), "%s\\AppData\\Roaming\\Theseus", PlatformHomeDir()); -#elif defined(THESEUS_HOST_MAC) - snprintf(s_dir, sizeof(s_dir), "%s/Library/Application Support/Theseus", PlatformHomeDir()); -#else - const char* xdg = getenv("XDG_DATA_HOME"); - if (xdg && *xdg) - snprintf(s_dir, sizeof(s_dir), "%s/theseus", xdg); - else - snprintf(s_dir, sizeof(s_dir), "%s/.local/share/theseus", PlatformHomeDir()); -#endif + Host_UserDataDir(s_dir, sizeof(s_dir)); return s_dir; } const char* Plat_ShippedDir() { -#ifdef THESEUS_HOST_MAC - // Inside a .app the binary sits in Contents/MacOS and the payload in - // Contents/Resources. Loose builds keep everything beside the binary. static char s_dir[1024] = {0}; - static bool s_init = false; - if (!s_init) { - s_init = true; - const char* exe = Plat_ExeDir(); - size_t n = strlen(exe); - const char kTail[] = "/Contents/MacOS"; - size_t t = sizeof(kTail) - 1; - if (n > t && strcmp(exe + n - t, kTail) == 0) - snprintf(s_dir, sizeof(s_dir), "%.*s/Contents/Resources", (int)(n - t), exe); - else - snprintf(s_dir, sizeof(s_dir), "%s", exe); - } + if (!s_dir[0]) Host_ShippedDir(Plat_ExeDir(), s_dir, sizeof(s_dir)); return s_dir; -#else - return Plat_ExeDir(); -#endif } static bool IsUserOwned(const char* logical) { diff --git a/theseus/desktop/audio_sdl.cpp b/theseus/desktop/audio_sdl.cpp index 38af4cf..7a48046 100644 --- a/theseus/desktop/audio_sdl.cpp +++ b/theseus/desktop/audio_sdl.cpp @@ -3,7 +3,6 @@ // MP3, WAV, and XAP-script-driven audio cues. Intentionally does NOT // include std.h, to avoid #define new conflicts with STL. -#include "platform.h" #include "app_paths.h" #include #include @@ -15,16 +14,7 @@ #include #include #include -#ifdef THESEUS_HOST_WIN -#include -#include -#ifndef S_ISDIR -#define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR) -#endif -#else -#include -#include -#endif +#include "host.h" #include "audio_sdl.h" #include "stb_image.h" @@ -968,13 +958,9 @@ static bool IsM3UFile(const char* name) static long long FileMTime(const char* path) { - struct stat st; - if (stat(path, &st) != 0) return 0; -#ifdef __APPLE__ - return (long long)st.st_mtimespec.tv_sec; -#else - return (long long)st.st_mtime; -#endif + HostFileInfo info; + if (!Host_Stat(path, &info)) return 0; + return (long long)info.mtime; } static void ApplyDBToSong(const DBEntry& e, Song& s) @@ -1093,23 +1079,12 @@ int DashMusic_Scan(const char* musicRoot) soundtrack.songs.push_back(BuildSong(full, StripExtension(songName))); }; -#ifdef THESEUS_HOST_WIN - char searchBuf[512]; - snprintf(searchBuf, sizeof(searchBuf), "%s\\*", stPath.c_str()); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind != -1) { - do { ProcessSongFile(fd.name); } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); - } -#else - DIR* stDir = opendir(stPath.c_str()); + HostDir* stDir = Host_OpenDir(stPath.c_str()); if (stDir) { - struct dirent* songEntry; - while ((songEntry = readdir(stDir)) != NULL) ProcessSongFile(songEntry->d_name); - closedir(stDir); + char songName[260]; + while (Host_ReadDir(stDir, songName, sizeof(songName), NULL)) ProcessSongFile(songName); + Host_CloseDir(stDir); } -#endif if (!soundtrack.songs.empty()) { if (soundtrack.albumPath.empty() && !fallbackArt.empty()) @@ -1163,43 +1138,20 @@ int DashMusic_Scan(const char* musicRoot) looseMusic.songs.push_back(BuildSong(full, StripExtension(fileName))); }; -#ifdef THESEUS_HOST_WIN - char searchBuf[512]; - snprintf(searchBuf, sizeof(searchBuf), "%s\\*", musicRoot); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind == -1) { - fprintf(stderr, "[Audio] Music directory '%s' not found (this is OK if no music installed)\n", musicRoot); - return 0; - } - do { - std::string entryPath = std::string(musicRoot) + "/" + fd.name; - struct stat est; - if (stat(entryPath.c_str(), &est) == 0) { - if (S_ISDIR(est.st_mode)) ProcessSoundtrackDir(fd.name); - else if (IsM3UFile(fd.name)) ProcessM3UFile(fd.name); - else ProcessLooseFile(fd.name); - } - } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); -#else - DIR* rootDir = opendir(musicRoot); + HostDir* rootDir = Host_OpenDir(musicRoot); if (!rootDir) { fprintf(stderr, "[Audio] Music directory '%s' not found (this is OK if no music installed)\n", musicRoot); return 0; } - struct dirent* entry; - while ((entry = readdir(rootDir)) != NULL) { - if (entry->d_name[0] == '.') continue; - std::string entryPath = std::string(musicRoot) + "/" + entry->d_name; - struct stat est; - if (stat(entryPath.c_str(), &est) != 0) continue; - if (S_ISDIR(est.st_mode)) ProcessSoundtrackDir(entry->d_name); - else if (IsM3UFile(entry->d_name)) ProcessM3UFile(entry->d_name); - else ProcessLooseFile(entry->d_name); + char entryName[260]; + bool isDir; + while (Host_ReadDirType(rootDir, entryName, sizeof(entryName), &isDir)) { + if (entryName[0] == '.') continue; + if (isDir) ProcessSoundtrackDir(entryName); + else if (IsM3UFile(entryName)) ProcessM3UFile(entryName); + else ProcessLooseFile(entryName); } - closedir(rootDir); -#endif + Host_CloseDir(rootDir); if (!looseMusic.songs.empty()) { bool merged = false; diff --git a/theseus/desktop/cdaudio.cpp b/theseus/desktop/cdaudio.cpp index 2188f2a..fe2b31d 100644 --- a/theseus/desktop/cdaudio.cpp +++ b/theseus/desktop/cdaudio.cpp @@ -25,7 +25,7 @@ // Platform: disc detection + TOC + track LBAs // ============================================================================ -#if defined(__linux__) +#if defined(THESEUS_HOST_LINUX) #include #include @@ -197,7 +197,7 @@ void CdAudio_Update() } // ---- B: SDL ring-buffer + raw-sector read (Linux) ------------------------- -#elif defined(__linux__) +#elif defined(THESEUS_HOST_LINUX) #include @@ -246,7 +246,7 @@ static int SDLCALL ReaderFunc(void* arg) bool readOk = true; // ----- platform-specific device open + read ----- -#if defined(__linux__) +#if defined(THESEUS_HOST_LINUX) int fd = OpenDevice(); if (fd < 0) { fprintf(stderr, "[CdAudio] cannot open device\n"); diff --git a/theseus/desktop/esde.cpp b/theseus/desktop/esde.cpp index dd423f7..8fc682a 100644 --- a/theseus/desktop/esde.cpp +++ b/theseus/desktop/esde.cpp @@ -1,4 +1,3 @@ -#include "platform.h" #include "esde.h" #include "platform_env.h" #include "app_paths.h" @@ -7,13 +6,8 @@ #include #include #include -#ifdef THESEUS_HOST_WIN -#include -#define strcasecmp _stricmp -#else -#include #include -#endif +#include "host.h" // Lives here so the ES-DE scanner stays linkable on its own. sdl_main points // it at the saved value. @@ -197,40 +191,15 @@ bool Esde_LastScanFoundSystemsXml(void) template static void ForEachEntry(const char* dir, F cb) { -#ifdef THESEUS_HOST_WIN - char pattern[1024]; - snprintf(pattern, sizeof(pattern), "%s/*", dir); - struct _finddata_t fd; - intptr_t h = _findfirst(pattern, &fd); - if (h == -1) return; - do { - if (fd.name[0] == '.') continue; - if (!cb(fd.name, (fd.attrib & _A_SUBDIR) != 0)) break; - } while (_findnext(h, &fd) == 0); - _findclose(h); -#else - DIR* d = opendir(dir); + HostDir* d = Host_OpenDir(dir); if (!d) return; - struct dirent* e; - while ((e = readdir(d)) != NULL) { - if (e->d_name[0] == '.') continue; - // readdir already knows the type. stat() per entry costs a network - // round trip, which on an SMB rom library is the whole scan. - bool isDir; - if (e->d_type == DT_DIR) isDir = true; - else if (e->d_type == DT_REG) isDir = false; - else { - // Symlinks and anything readdir won't commit to still need a look. - char full[1024]; - snprintf(full, sizeof(full), "%s/%s", dir, e->d_name); - struct stat st; - if (stat(full, &st) != 0) continue; - isDir = S_ISDIR(st.st_mode) != 0; - } - if (!cb(e->d_name, isDir)) break; + char name[256]; + bool isDir; + while (Host_ReadDirType(d, name, sizeof(name), &isDir)) { + if (name[0] == '.') continue; + if (!cb(name, isDir)) break; } - closedir(d); -#endif + Host_CloseDir(d); } // A .app is a directory on macOS, so extension wins over file type. Anything diff --git a/theseus/desktop/hdd_browser.cpp b/theseus/desktop/hdd_browser.cpp index e58e513..087ac50 100644 --- a/theseus/desktop/hdd_browser.cpp +++ b/theseus/desktop/hdd_browser.cpp @@ -91,11 +91,11 @@ static const char* GetDefaultQcow2Path() { static char path[512] = ""; if (path[0]) return path; -#ifdef __APPLE__ +#ifdef THESEUS_HOST_MAC const char* home = getenv("HOME"); if (home) snprintf(path, sizeof(path), "%s/Library/Application Support/xemu/xemu/xbox_hdd.qcow2", home); -#elif defined(__linux__) +#elif defined(THESEUS_HOST_LINUX) const char* home = getenv("HOME"); if (home) snprintf(path, sizeof(path), "%s/.local/share/xemu/xemu/xbox_hdd.qcow2", home); diff --git a/theseus/desktop/host.h b/theseus/desktop/host.h index 6d46fb2..320394e 100644 --- a/theseus/desktop/host.h +++ b/theseus/desktop/host.h @@ -24,6 +24,9 @@ struct HostFileInfo { int64_t mtime; // seconds since 1970, UTC }; +// The separator native paths show the user. +extern const char Host_PathSep; + // Files and folders. Host_MakeDir is true when the folder exists afterwards. bool Host_MakeDir(const char* path); bool Host_RemoveDir(const char* path); @@ -45,6 +48,10 @@ int64_t Host_Seek(FILE* f, int64_t offset, int whence); struct HostDir; HostDir* Host_OpenDir(const char* path); bool Host_ReadDir(HostDir* dir, char* name, size_t nameSize, HostFileInfo* info); +// Name and folder-or-not only. Skips the per-entry stat when the OS already +// knows the type, which matters on a network share. Entries that can't be +// looked at are skipped. +bool Host_ReadDirType(HostDir* dir, char* name, size_t nameSize, bool* isDir); void Host_CloseDir(HostDir* dir); // The machine itself. @@ -79,3 +86,12 @@ bool Host_RegistryString(const char* subKey, const char* value, char* out, size_ // Audio CD in the first optical drive: track count and per-track lengths // in seconds. -1 when there's no readable disc. int Host_CdReadToc(int* durations, int maxTracks, bool* allAudio); + +// "macOS", "Windows" or "Linux", for people and servers that ask. +const char* Host_Name(); +// The bgfx shader folder to try when the renderer doesn't say. +const char* Host_DefaultShaderDir(); +// Where the user's Configs and Library live, and where the shipped Data +// sits relative to the executable's folder. +void Host_UserDataDir(char* out, size_t outSize); +void Host_ShippedDir(const char* exeDir, char* out, size_t outSize); diff --git a/theseus/desktop/host_posix.cpp b/theseus/desktop/host_posix.cpp index 9a174c1..f40f328 100644 --- a/theseus/desktop/host_posix.cpp +++ b/theseus/desktop/host_posix.cpp @@ -20,6 +20,8 @@ #include #endif +const char Host_PathSep = '/'; + static void FillInfo(const struct stat& st, HostFileInfo* out) { out->attrs = S_ISDIR(st.st_mode) ? HOST_ATTR_DIR : 0; out->size = (uint64_t)st.st_size; @@ -115,6 +117,26 @@ bool Host_ReadDir(HostDir* dir, char* name, size_t nameSize, HostFileInfo* info) return true; } +bool Host_ReadDirType(HostDir* dir, char* name, size_t nameSize, bool* isDir) { + if (!dir) return false; + struct dirent* e; + while ((e = readdir(dir->d)) != NULL) { + if (e->d_type == DT_DIR) *isDir = true; + else if (e->d_type == DT_REG) *isDir = false; + else { + // Symlinks and anything readdir won't commit to still need a look. + char full[768]; + snprintf(full, sizeof(full), "%s/%s", dir->path, e->d_name); + struct stat st; + if (stat(full, &st) != 0) continue; + *isDir = S_ISDIR(st.st_mode) != 0; + } + snprintf(name, nameSize, "%s", e->d_name); + return true; + } + return false; +} + void Host_CloseDir(HostDir* dir) { if (!dir) return; closedir(dir->d); diff --git a/theseus/desktop/host_posix_app.cpp b/theseus/desktop/host_posix_app.cpp index 7fb2768..ea28432 100644 --- a/theseus/desktop/host_posix_app.cpp +++ b/theseus/desktop/host_posix_app.cpp @@ -171,3 +171,48 @@ bool Host_RegistryString(const char*, const char*, char*, size_t) { return false // Linux reads its TOC in cdaudio.cpp. int Host_CdReadToc(int*, int, bool*) { return -1; } + +const char* Host_Name() { +#ifdef __APPLE__ + return "macOS"; +#else + return "Linux"; +#endif +} + +const char* Host_DefaultShaderDir() { +#ifdef __APPLE__ + return "metal"; +#else + return "spirv"; +#endif +} + +void Host_UserDataDir(char* out, size_t outSize) { + const char* home = getenv("HOME"); + if (!home) home = ""; +#ifdef __APPLE__ + snprintf(out, outSize, "%s/Library/Application Support/Theseus", home); +#else + const char* xdg = getenv("XDG_DATA_HOME"); + if (xdg && *xdg) + snprintf(out, outSize, "%s/theseus", xdg); + else + snprintf(out, outSize, "%s/.local/share/theseus", home); +#endif +} + +void Host_ShippedDir(const char* exeDir, char* out, size_t outSize) { +#ifdef __APPLE__ + // Inside a .app the binary sits in Contents/MacOS and the payload in + // Contents/Resources. Loose builds keep everything beside the binary. + size_t n = strlen(exeDir); + const char kTail[] = "/Contents/MacOS"; + size_t t = sizeof(kTail) - 1; + if (n > t && strcmp(exeDir + n - t, kTail) == 0) { + snprintf(out, outSize, "%.*s/Contents/Resources", (int)(n - t), exeDir); + return; + } +#endif + snprintf(out, outSize, "%s", exeDir); +} diff --git a/theseus/desktop/host_win.cpp b/theseus/desktop/host_win.cpp index 2954bed..26b12a2 100644 --- a/theseus/desktop/host_win.cpp +++ b/theseus/desktop/host_win.cpp @@ -16,6 +16,8 @@ #include #include +const char Host_PathSep = '\\'; + // Win32 takes '/' almost everywhere, but not everywhere. Hand it '\'. static void ToWin(char* dst, size_t size, const char* src) { snprintf(dst, size, "%s", src); @@ -166,6 +168,13 @@ bool Host_ReadDir(HostDir* dir, char* name, size_t nameSize, HostFileInfo* info) return true; } +bool Host_ReadDirType(HostDir* dir, char* name, size_t nameSize, bool* isDir) { + HostFileInfo info; + if (!Host_ReadDir(dir, name, nameSize, &info)) return false; + *isDir = (info.attrs & HOST_ATTR_DIR) != 0; + return true; +} + void Host_CloseDir(HostDir* dir) { if (!dir) return; FindClose(dir->find); diff --git a/theseus/desktop/host_win_app.cpp b/theseus/desktop/host_win_app.cpp index a05afb9..a2770a4 100644 --- a/theseus/desktop/host_win_app.cpp +++ b/theseus/desktop/host_win_app.cpp @@ -255,3 +255,21 @@ int Host_CdReadToc(int* durations, int maxTracks, bool* allAudio) { } return count; } + +const char* Host_Name() { return "Windows"; } +const char* Host_DefaultShaderDir() { return "dx11"; } + +void Host_UserDataDir(char* out, size_t outSize) { + const char* appdata = getenv("APPDATA"); + if (appdata && *appdata) { + snprintf(out, outSize, "%s\\Theseus", appdata); + return; + } + const char* home = getenv("HOME"); + if (!home || !*home) home = getenv("USERPROFILE"); + snprintf(out, outSize, "%s\\AppData\\Roaming\\Theseus", home ? home : ""); +} + +void Host_ShippedDir(const char* exeDir, char* out, size_t outSize) { + snprintf(out, outSize, "%s", exeDir); +} diff --git a/theseus/desktop/icon_compress.cpp b/theseus/desktop/icon_compress.cpp index 76b7b98..4151b72 100644 --- a/theseus/desktop/icon_compress.cpp +++ b/theseus/desktop/icon_compress.cpp @@ -1,4 +1,3 @@ -#include "platform.h" #include "icon_compress.h" #include "virtual_games.h" // VGAMES_ICONS @@ -11,11 +10,7 @@ #include #include #include -#ifdef THESEUS_HOST_WIN -#include -#else -#include -#endif +#include "host.h" static size_t FileSize(const char* path) { @@ -128,29 +123,15 @@ IconCompressResult Icons_Compress(int maxDim, int quality) static char names[8192][256]; int count = 0; -#ifdef THESEUS_HOST_WIN - char pattern[1024]; - snprintf(pattern, sizeof(pattern), "%s/*", dir); - struct _finddata_t fd; - intptr_t hnd = _findfirst(pattern, &fd); - if (hnd != -1) { - do { - if (fd.name[0] == '.') continue; - if (count < 8192) snprintf(names[count++], 256, "%s", fd.name); - } while (_findnext(hnd, &fd) == 0); - _findclose(hnd); - } -#else - DIR* d = opendir(dir); + HostDir* d = Host_OpenDir(dir); if (d) { - struct dirent* e; - while ((e = readdir(d)) != NULL) { - if (e->d_name[0] == '.') continue; - if (count < 8192) snprintf(names[count++], 256, "%s", e->d_name); + char name[256]; + while (Host_ReadDir(d, name, sizeof(name), NULL)) { + if (name[0] == '.') continue; + if (count < 8192) snprintf(names[count++], 256, "%s", name); } - closedir(d); + Host_CloseDir(d); } -#endif for (int i = 0; i < count; i++) { r.scanned++; diff --git a/theseus/desktop/image.cpp b/theseus/desktop/image.cpp index 095668d..bf5a04d 100644 --- a/theseus/desktop/image.cpp +++ b/theseus/desktop/image.cpp @@ -12,6 +12,7 @@ #include "file_util.h" #include "xbx_texture.h" #include "skin_assets.h" +#include "host.h" inline void D3D_FreeNoncontiguousMemory(void *pMemory) { free(pMemory); } @@ -547,27 +548,14 @@ void PreloadSkinTextures() FindImage(wideFileName); }; -#ifdef THESEUS_HOST_WIN - char searchBuf[512]; - snprintf(searchBuf, sizeof(searchBuf), "%s\\*.xbx", mappedDir); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind == -1) { - OutputDebugStringA("[Preload] Could not open skin directory\n"); - return; - } - do { ProcessXbxFile(fd.name); } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); -#else - DIR* dir = opendir(mappedDir); + HostDir* dir = Host_OpenDir(mappedDir); if (!dir) { OutputDebugStringA("[Preload] Could not open skin directory\n"); return; } - struct dirent* ent; - while ((ent = readdir(dir)) != NULL) ProcessXbxFile(ent->d_name); - closedir(dir); -#endif + char name[MAX_PATH]; + while (Host_ReadDir(dir, name, sizeof(name), NULL)) ProcessXbxFile(name); + Host_CloseDir(dir); }// ============================================================================ // Software Rasterization (FillRect / BitBlt) // ============================================================================ diff --git a/theseus/desktop/imgui_impl_bgfx.cpp b/theseus/desktop/imgui_impl_bgfx.cpp index 417481e..012e036 100644 --- a/theseus/desktop/imgui_impl_bgfx.cpp +++ b/theseus/desktop/imgui_impl_bgfx.cpp @@ -11,7 +11,7 @@ // DisplayPos / DisplaySize. Each ImDrawCmd is a setScissor + draw of // (ElemCount) indices at (IdxOffset, VtxOffset). -#include "platform.h" +#include "host.h" #include "app_paths.h" #include "imgui_impl_bgfx.h" #include "imgui.h" @@ -39,13 +39,7 @@ static BgfxImguiBackend g_bd; // via a header; same pattern. static bgfx::ShaderHandle LoadShader(const char* name) { -#if defined(THESEUS_HOST_WIN) - const char* sub = "dx11"; -#elif defined(__APPLE__) - const char* sub = "metal"; -#else - const char* sub = "spirv"; -#endif + const char* sub = Host_DefaultShaderDir(); switch (bgfx::getRendererType()) { case bgfx::RendererType::Direct3D11: sub = "dx11"; break; case bgfx::RendererType::Metal: sub = "metal"; break; diff --git a/theseus/desktop/launchers/retroarch.cpp b/theseus/desktop/launchers/retroarch.cpp index cd122f8..24a791f 100644 --- a/theseus/desktop/launchers/retroarch.cpp +++ b/theseus/desktop/launchers/retroarch.cpp @@ -135,7 +135,7 @@ bool Build(const char* spec, char* outCmd, size_t outSize) { snprintf(extra, sizeof(extra), "%s%s%s%s", fs, ovr[0] ? " --appendconfig \"" : "", ovr[0] ? ovr : "", ovr[0] ? "\"" : ""); -#ifdef __APPLE__ +#ifdef THESEUS_HOST_MAC // macOS: the cores dir is in ~/Library/Application Support/RetroArch, // but the binary is inside /Applications/RetroArch.app. Launch via // `open -na RetroArch --args ...` so we don't hardcode the bundle path. @@ -249,7 +249,7 @@ bool RetroArch_DiscoverPlaylistsDir(const char* installRoot, if (DirExists(buf)) { strncpy(outDir, buf, outSize - 1); outDir[outSize - 1] = 0; return true; } } -#ifdef __APPLE__ +#ifdef THESEUS_HOST_MAC const char* home = getenv("HOME"); if (home) { snprintf(buf, sizeof(buf), "%s/Documents/RetroArch/playlists", home); diff --git a/theseus/desktop/menu_bar.cpp b/theseus/desktop/menu_bar.cpp index 999a810..58fedd5 100644 --- a/theseus/desktop/menu_bar.cpp +++ b/theseus/desktop/menu_bar.cpp @@ -1261,17 +1261,7 @@ void RenderAboutWindow() { } ImGui::Text("Version: %s", s_version); - ImGui::Text("Platform: %s", -#ifdef __APPLE__ - "macOS" -#elif defined(__linux__) - "Linux" -#elif defined(THESEUS_HOST_WIN) - "Windows" -#else - "Unknown" -#endif - ); + ImGui::Text("Platform: %s", Host_Name()); ImGui::Spacing(); ImGui::Separator(); diff --git a/theseus/desktop/path_ci.h b/theseus/desktop/path_ci.h index 70af6f8..69de68e 100644 --- a/theseus/desktop/path_ci.h +++ b/theseus/desktop/path_ci.h @@ -3,17 +3,11 @@ #pragma once -#include "platform.h" +#include "host.h" #include #include -#include - -#ifdef THESEUS_HOST_WIN -#include -#else -#include #include -#endif +#include // Rewrites resolvedPath to the real on disk spelling. A trailing wildcard is // peeled off first so FindFirstFile style callers keep working. @@ -41,7 +35,7 @@ inline bool Plat_ResolveCaseInsensitive(char* resolvedPath, size_t maxLen) { char rebuilt[1024]; rebuilt[0] = '\0'; char* walk = tempPath; -#ifdef THESEUS_HOST_WIN + // A drive letter only turns up on Windows. if (tempPath[0] && tempPath[1] == ':') { snprintf(rebuilt, sizeof(rebuilt), "%c:", tempPath[0]); walk = tempPath + 2; @@ -50,26 +44,18 @@ inline bool Plat_ResolveCaseInsensitive(char* resolvedPath, size_t maxLen) { walk++; } } -#endif if (*walk == '/') { strncat(rebuilt, "/", sizeof(rebuilt) - strlen(rebuilt) - 1); walk++; } char* components[64]; int nComponents = 0; - char* save = NULL; -#ifdef THESEUS_HOST_WIN - char* tok = strtok_s(walk, "/", &save); -#else - char* tok = strtok_r(walk, "/", &save); -#endif - while (tok && nComponents < 64) { - components[nComponents++] = tok; -#ifdef THESEUS_HOST_WIN - tok = strtok_s(NULL, "/", &save); -#else - tok = strtok_r(NULL, "/", &save); -#endif + for (char* p = walk; *p && nComponents < 64; ) { + while (*p == '/') p++; + if (!*p) break; + components[nComponents++] = p; + while (*p && *p != '/') p++; + if (*p) *p++ = '\0'; } for (int i = 0; i < nComponents; i++) { @@ -90,41 +76,21 @@ inline bool Plat_ResolveCaseInsensitive(char* resolvedPath, size_t maxLen) { const char* dirToScan = rebuilt[0] ? rebuilt : "."; bool found = false; -#ifdef THESEUS_HOST_WIN - char searchBuf[1024]; - snprintf(searchBuf, sizeof(searchBuf), "%s%s*", dirToScan, needSlash ? "/" : ""); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind != -1) { - do { - if (_stricmp(fd.name, wanted) == 0) { - snprintf(candidate, sizeof(candidate), "%s%s%s", - rebuilt, needSlash ? "/" : "", fd.name); - strncpy(rebuilt, candidate, sizeof(rebuilt) - 1); - rebuilt[sizeof(rebuilt) - 1] = '\0'; - found = true; - break; - } - } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); - } -#else - DIR* dir = opendir(dirToScan); + HostDir* dir = Host_OpenDir(dirToScan); if (!dir) return false; - struct dirent* entry; - while ((entry = readdir(dir)) != NULL) { - if (strcasecmp(entry->d_name, wanted) == 0) { + char name[260]; + while (Host_ReadDir(dir, name, sizeof(name), NULL)) { + if (strcasecmp(name, wanted) == 0) { snprintf(candidate, sizeof(candidate), "%s%s%s", - rebuilt, needSlash ? "/" : "", entry->d_name); + rebuilt, needSlash ? "/" : "", name); strncpy(rebuilt, candidate, sizeof(rebuilt) - 1); rebuilt[sizeof(rebuilt) - 1] = '\0'; found = true; break; } } - closedir(dir); -#endif + Host_CloseDir(dir); if (!found) return false; } diff --git a/theseus/desktop/path_template.cpp b/theseus/desktop/path_template.cpp index 0abe8af..98da16c 100644 --- a/theseus/desktop/path_template.cpp +++ b/theseus/desktop/path_template.cpp @@ -11,8 +11,8 @@ // each module will call a Register-style API instead of editing // this table; for now we hardcode the existing dispatcher knobs. -#include "platform.h" #include "path_template.h" +#include "platform_env.h" #include #include @@ -71,28 +71,9 @@ static bool AppendStr(char* out, size_t outSize, size_t* pos, const char* s, siz } } // namespace -// Resolve the user's home directory at runtime. Cached on first call -// so repeated expansions don't re-query the environment. Empty string -// if the platform doesn't expose one (extremely unusual; fallback is -// to leave `~` literal so the spawn fails with a recognizable error). -static const char* HomeDir() { - static char s_home[512] = {0}; - static bool s_init = false; - if (!s_init) { - s_init = true; - const char* h = getenv("HOME"); -#ifdef THESEUS_HOST_WIN - if (!h || !*h) h = getenv("USERPROFILE"); -#endif - if (h && *h) { - size_t n = strlen(h); - if (n >= sizeof(s_home)) n = sizeof(s_home) - 1; - memcpy(s_home, h, n); - s_home[n] = '\0'; - } - } - return s_home; -} +// The user's home directory, or "" when there isn't one (then `~` stays +// literal so the spawn fails with a recognizable error). +static const char* HomeDir() { return PlatformHomeDir(); } int PathTemplate_Expand(const char* in, char* out, size_t outSize) { if (!in || !out || outSize == 0) return -1; diff --git a/theseus/desktop/platform_env.h b/theseus/desktop/platform_env.h index ad305db..c81176b 100644 --- a/theseus/desktop/platform_env.h +++ b/theseus/desktop/platform_env.h @@ -5,7 +5,6 @@ #pragma once -#include "platform.h" #include #include @@ -14,10 +13,9 @@ inline const char* PlatformHomeDir() { static bool s_init = false; if (!s_init) { s_init = true; + // USERPROFILE is Windows' HOME; nothing else sets it. const char* h = getenv("HOME"); -#ifdef THESEUS_HOST_WIN if (!h || !*h) h = getenv("USERPROFILE"); -#endif if (h && *h) { size_t n = strlen(h); if (n >= sizeof(s_home)) n = sizeof(s_home) - 1; @@ -33,13 +31,10 @@ inline const char* PlatformUserName() { static bool s_init = false; if (!s_init) { s_init = true; - const char* u; -#ifdef THESEUS_HOST_WIN - u = getenv("USERNAME"); -#else - u = getenv("USER"); + // Windows only sets USERNAME; POSIX sets USER or LOGNAME. + const char* u = getenv("USER"); if (!u || !*u) u = getenv("LOGNAME"); -#endif + if (!u || !*u) u = getenv("USERNAME"); if (u && *u) { size_t n = strlen(u); if (n >= sizeof(s_user)) n = sizeof(s_user) - 1; diff --git a/theseus/desktop/plex_client.cpp b/theseus/desktop/plex_client.cpp index 97b11bc..67f23dc 100644 --- a/theseus/desktop/plex_client.cpp +++ b/theseus/desktop/plex_client.cpp @@ -1,8 +1,8 @@ // plex_client.cpp: Plex Media Server HTTPS client. -#include "platform.h" #include "app_paths.h" #include "plex_client.h" +#include "host.h" #include "http_util.h" #include "json_util.h" #include "client_util.h" @@ -34,13 +34,7 @@ static HttpHeaders PlexHeaders(bool wantJson, bool withToken) h.push_back({ "X-Plex-Product", "UIX Desktop" }); h.push_back({ "X-Plex-Version", "0.3" }); h.push_back({ "X-Plex-Device-Name", "UIX Desktop" }); -#if defined(__APPLE__) - h.push_back({ "X-Plex-Platform", "macOS" }); -#elif defined(THESEUS_HOST_WIN) - h.push_back({ "X-Plex-Platform", "Windows" }); -#else - h.push_back({ "X-Plex-Platform", "Linux" }); -#endif + h.push_back({ "X-Plex-Platform", Host_Name() }); if (withToken && g_plexToken[0]) { h.push_back({ "X-Plex-Token", g_plexToken }); } diff --git a/theseus/desktop/preloader.cpp b/theseus/desktop/preloader.cpp index 7059fc3..65b5b1d 100644 --- a/theseus/desktop/preloader.cpp +++ b/theseus/desktop/preloader.cpp @@ -15,12 +15,7 @@ #include #include -#ifdef THESEUS_HOST_WIN -#include -#include -#else -#include -#endif +#include "host.h" #include @@ -50,19 +45,11 @@ static void Preloader_Mkdirp(const char* path) { for (char* p = tmp + 1; *p; p++) { if (*p == '/') { *p = '\0'; -#ifdef THESEUS_HOST_WIN - _mkdir(tmp); -#else - mkdir(tmp, 0755); -#endif + Host_MakeDir(tmp); *p = '/'; } } -#ifdef THESEUS_HOST_WIN - _mkdir(tmp); -#else - mkdir(tmp, 0755); -#endif + Host_MakeDir(tmp); } static const char* Preloader_FindName(WORD idx, XTOOL_FILENAME* filenames, int nameCount, @@ -233,66 +220,29 @@ int Preloader_ExtractXIP(const char* xipPath, const char* outDir) { static int Preloader_ExtractAllXIPs(const char* xipsDir, char statusBuf[], int statusBufSize) { int totalXips = 0, count = 0; -#ifdef THESEUS_HOST_WIN - // Windows: use _findfirst/_findnext - char searchPath[1024]; - snprintf(searchPath, sizeof(searchPath), "%s\\*.xip", xipsDir); - struct _finddata_t fd; - intptr_t hFind; - - // Count pass - hFind = _findfirst(searchPath, &fd); - if (hFind == -1) { - snprintf(statusBuf, statusBufSize, "Cannot open: %s", xipsDir); - return -1; - } - do { totalXips++; } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); - - // Extract pass - hFind = _findfirst(searchPath, &fd); - if (hFind != -1) { - do { - char xipPath[1024], outDir[1024]; - snprintf(xipPath, sizeof(xipPath), "%s\\%s", xipsDir, fd.name); - snprintf(outDir, sizeof(outDir), "%s\\%s", xipsDir, fd.name); - char* dot = strrchr(outDir + strlen(xipsDir) + 1, '.'); - if (dot) *dot = '\0'; - - snprintf(statusBuf, statusBufSize, "Extracting %s... (%d/%d)", fd.name, count + 1, totalXips); - fprintf(stdout, "[Extract] %s\n", fd.name); - Preloader_ExtractXIP(xipPath, outDir); - count++; - } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); - } -#else - // POSIX: use opendir/readdir - DIR* d = opendir(xipsDir); + char name[260]; + HostDir* d = Host_OpenDir(xipsDir); if (!d) { snprintf(statusBuf, statusBufSize, "Cannot open: %s", xipsDir); return -1; } + while (Host_ReadDir(d, name, sizeof(name), NULL)) + if (Preloader_EndsWith(name, ".xip")) totalXips++; + Host_CloseDir(d); - struct dirent* ent; - while ((ent = readdir(d)) != NULL) { - if (Preloader_EndsWith(ent->d_name, ".xip")) totalXips++; - } - rewinddir(d); - - while ((ent = readdir(d)) != NULL) { - if (!Preloader_EndsWith(ent->d_name, ".xip")) continue; + d = Host_OpenDir(xipsDir); + while (d && Host_ReadDir(d, name, sizeof(name), NULL)) { + if (!Preloader_EndsWith(name, ".xip")) continue; char xipPath[1024], outDir[1024]; - snprintf(xipPath, sizeof(xipPath), "%s/%s", xipsDir, ent->d_name); - snprintf(outDir, sizeof(outDir), "%s/%s", xipsDir, ent->d_name); + snprintf(xipPath, sizeof(xipPath), "%s%c%s", xipsDir, Host_PathSep, name); + snprintf(outDir, sizeof(outDir), "%s%c%s", xipsDir, Host_PathSep, name); char* dot = strrchr(outDir + strlen(xipsDir) + 1, '.'); if (dot) *dot = '\0'; - snprintf(statusBuf, statusBufSize, "Extracting %s... (%d/%d)", ent->d_name, count + 1, totalXips); - fprintf(stdout, "[Extract] %s\n", ent->d_name); + snprintf(statusBuf, statusBufSize, "Extracting %s... (%d/%d)", name, count + 1, totalXips); + fprintf(stdout, "[Extract] %s\n", name); Preloader_ExtractXIP(xipPath, outDir); count++; } - closedir(d); -#endif + Host_CloseDir(d); snprintf(statusBuf, statusBufSize, "Extracted %d XIP file(s)", count); return count; diff --git a/theseus/desktop/sdl_main.cpp b/theseus/desktop/sdl_main.cpp index 9b23c5d..825cd35 100644 --- a/theseus/desktop/sdl_main.cpp +++ b/theseus/desktop/sdl_main.cpp @@ -39,13 +39,7 @@ // Loads Data/shaders//.bin. Picks subdir from the active renderer. bgfx::ShaderHandle theseus_bgfx_load_shader(const char* name) { -#if defined(THESEUS_HOST_WIN) - const char* sub = "dx11"; -#elif defined(THESEUS_HOST_MAC) - const char* sub = "metal"; -#else - const char* sub = "spirv"; -#endif + const char* sub = Host_DefaultShaderDir(); switch (bgfx::getRendererType()) { case bgfx::RendererType::Direct3D11: sub = "dx11"; break; case bgfx::RendererType::Metal: sub = "metal"; break; diff --git a/theseus/desktop/title_maker.cpp b/theseus/desktop/title_maker.cpp index a93d499..d8ca548 100644 --- a/theseus/desktop/title_maker.cpp +++ b/theseus/desktop/title_maker.cpp @@ -23,13 +23,7 @@ #include #include -#ifdef THESEUS_HOST_WIN -#include -#include -#include -#else -#include -#endif +#include "host.h" bool g_titleMakerOpen = false; @@ -39,13 +33,8 @@ bool g_titleMakerOpen = false; static void TM_EnsureDir(const char* path) { struct stat st; - if (stat(path, &st) != 0) { -#ifdef THESEUS_HOST_WIN - _mkdir(path); -#else - mkdir(path, 0755); -#endif - } + if (stat(path, &st) != 0) + Host_MakeDir(path); } static void TM_SanitizeName(char* name, int maxLen) { @@ -526,7 +515,7 @@ static bool TM_DownloadSteamIcon(int appid, const char* titleID) { "https://cdn.akamai.steamstatic.com/steam/apps/%d/%s", appid, tryUrls[u]); if (Http_GetToFile(url, tmpPath) && stat(tmpPath, &ist) == 0 && ist.st_size > 1000) { -#ifdef __APPLE__ +#ifdef THESEUS_HOST_MAC char cmd[1024]; snprintf(cmd, sizeof(cmd), "sips -z 128 128 -s format jpeg \"%s\" --out \"%s\" >/dev/null 2>&1", @@ -597,24 +586,12 @@ static void TM_ImportSteamLibrary(const char* steamPath, }; for (int d = 0; d < libCount && gameCount < 256; d++) { -#ifdef THESEUS_HOST_WIN - char searchBuf[512]; - snprintf(searchBuf, sizeof(searchBuf), "%s\\appmanifest_*", libDirs[d]); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind != -1) { - do { ParseAppManifest(libDirs[d], fd.name); } while (_findnext(hFind, &fd) == 0 && gameCount < 256); - _findclose(hFind); - } -#else - DIR* dir = opendir(libDirs[d]); + HostDir* dir = Host_OpenDir(libDirs[d]); if (!dir) continue; - struct dirent* ent; - while ((ent = readdir(dir)) != NULL && gameCount < 256) { - ParseAppManifest(libDirs[d], ent->d_name); - } - closedir(dir); -#endif + char name[260]; + while (gameCount < 256 && Host_ReadDir(dir, name, sizeof(name), NULL)) + ParseAppManifest(libDirs[d], name); + Host_CloseDir(dir); } if (gameCount == 0) return; @@ -808,7 +785,7 @@ void RenderTitleMaker() { ImGui::SameLine(); if (ImGui::SmallButton("Find##xemu")) { const char* tryPaths[] = { -#ifdef __APPLE__ +#ifdef THESEUS_HOST_MAC "/Applications/xemu.app", "/opt/homebrew/bin/xemu", "/usr/local/bin/xemu", @@ -1364,16 +1341,11 @@ void RenderTitleMaker() { } ImGui::SameLine(); if (ImGui::SmallButton("Find##st")) { - const char* user = NULL; -#ifdef THESEUS_HOST_WIN - user = getenv("USERNAME"); -#else - user = getenv("USER"); -#endif - if (!user) user = "user"; + const char* user = PlatformUserName(); + if (!user || !*user) user = "user"; char buf[512]; const char* tryRoots[] = { -#ifdef __APPLE__ +#ifdef THESEUS_HOST_MAC "/Users/%s/Library/Application Support/Steam", #elif defined(THESEUS_HOST_WIN) "C:\\Program Files (x86)\\Steam", diff --git a/theseus/desktop/xap_editor.cpp b/theseus/desktop/xap_editor.cpp index 4850f02..8ea63f1 100644 --- a/theseus/desktop/xap_editor.cpp +++ b/theseus/desktop/xap_editor.cpp @@ -19,17 +19,7 @@ #include #include #include - -#ifdef THESEUS_HOST_WIN -#include -#include -#define strcasecmp _stricmp -#ifndef S_ISDIR -#define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR) -#endif -#else -#include -#endif +#include "host.h" // Implemented in group.cpp extern "C" int CollectInlineNodes(CNode* pRoot, CInstance* pRootInst, void* outBuf, int maxEntries); @@ -174,43 +164,22 @@ void XapEditor_Cleanup() {} // ============================================================================ static void XapEditor_ScanDir(const char* dir, char fileList[][512], int* count, int maxFiles) { -#ifdef THESEUS_HOST_WIN - char searchPath[512]; - snprintf(searchPath, sizeof(searchPath), "%s\\*", dir); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchPath, &fd); - if (hFind == -1) return; - do { - if (fd.name[0] == '.') continue; - if (*count >= maxFiles) break; - char fullpath[512]; - snprintf(fullpath, sizeof(fullpath), "%s\\%s", dir, fd.name); - if (fd.attrib & _A_SUBDIR) - XapEditor_ScanDir(fullpath, fileList, count, maxFiles); - else if (strlen(fd.name) > 4 && _stricmp(fd.name + strlen(fd.name) - 4, ".xap") == 0) { - strncpy(fileList[*count], fullpath, 511); - (*count)++; - } - } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); -#else - DIR* d = opendir(dir); + HostDir* d = Host_OpenDir(dir); if (!d) return; - struct dirent* entry; - while ((entry = readdir(d)) != NULL && *count < maxFiles) { - if (entry->d_name[0] == '.') continue; + char name[260]; + bool isDir; + while (*count < maxFiles && Host_ReadDirType(d, name, sizeof(name), &isDir)) { + if (name[0] == '.') continue; char fullpath[512]; - snprintf(fullpath, sizeof(fullpath), "%s/%s", dir, entry->d_name); - struct stat fst; - if (stat(fullpath, &fst) == 0 && S_ISDIR(fst.st_mode)) + snprintf(fullpath, sizeof(fullpath), "%s%c%s", dir, Host_PathSep, name); + if (isDir) XapEditor_ScanDir(fullpath, fileList, count, maxFiles); - else if (strlen(entry->d_name) > 4 && strcasecmp(entry->d_name + strlen(entry->d_name) - 4, ".xap") == 0) { + else if (strlen(name) > 4 && strcasecmp(name + strlen(name) - 4, ".xap") == 0) { strncpy(fileList[*count], fullpath, 511); (*count)++; } } - closedir(d); -#endif + Host_CloseDir(d); } // ============================================================================ From c2a214cc5f5b0f8ae170098bd88e4618f1ea1905 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:17:59 -0500 Subject: [PATCH 045/167] Tightened lint-platform to the host layer Host tests are now only allowed in the host files and platform.h, plus five files that keep per-OS install paths and the Linux CD code. __APPLE__ and __linux__ get the same check as _WIN32. --- build/Makefile | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/build/Makefile b/build/Makefile index b5909af..064b3ef 100644 --- a/build/Makefile +++ b/build/Makefile @@ -455,19 +455,16 @@ clean: # Platform flags live in shared/platform.h. The old desktop spellings stay # retired, and code the Xbox build compiles never reads _WIN32, which is # true on the Xbox too. On desktop only the host layer (desktop/host_*) -# talks to the OS by name: is host_win* only, _WIN32 is -# platform.h and host_* only, and the Xbox-shaped core never asks which -# host it's on. Vendored headers (stb, minimp3, imfilebrowser, imgui) are -# left alone. +# talks to the OS by name: is host_win* only, the compiler's OS +# macros are platform.h and host_* only, and THESEUS_HOST_* is too, apart +# from LINT_HOST_DATA. Those files keep per-OS data, not OS calls: default +# install paths for Steam, RetroArch and xemu, and the Linux CD ioctls. +# Vendored headers (stb, minimp3, imfilebrowser, imgui) are left alone. LINT_DIRS = $(THESEUS_SRC)/desktop $(THESEUS_SRC)/shared $(THESEUS_SRC)/engine \ $(THESEUS_SRC)/render $(THESEUS_SRC)/toolbox LINT_VENDOR = --exclude-dir=third-party --exclude-dir=xdk --exclude-dir=imgui \ --exclude='stb_*.h' --exclude='minimp3.h' --exclude='imfilebrowser.h' -LINT_SHAPED = $(THESEUS_SRC)/shared $(THESEUS_SRC)/engine $(THESEUS_SRC)/render \ - $(THESEUS_SRC)/toolbox $(THESEUS_SRC)/desktop/platform_shim.h \ - $(THESEUS_SRC)/desktop/std.h $(THESEUS_SRC)/desktop/d3d8_sdl.h \ - $(THESEUS_SRC)/desktop/xbox_file.cpp $(THESEUS_SRC)/desktop/xbox_path.h \ - $(THESEUS_SRC)/desktop/xboxfs_drive.h $(THESEUS_SRC)/desktop/host.h +LINT_HOST_DATA = title_maker.cpp retroarch.cpp steam.cpp hdd_browser.cpp cdaudio.cpp LINT_HOSTTEST = THESEUS_HOST_(WIN|MAC|LINUX) .PHONY: lint-platform lint-platform: @@ -482,13 +479,14 @@ lint-platform: | grep -v '/desktop/host_win[^/]*[.]cpp:'; then \ echo "lint-platform: Windows header above. Only desktop/host_win*.cpp may include it."; bad=1; \ fi; \ - if grep -rnw '_WIN32' $(LINT_DIRS) --include='*.h' --include='*.cpp' --include='*.c' \ + if grep -rnwE '_WIN32|__APPLE__|__linux__' $(LINT_DIRS) --include='*.h' --include='*.cpp' --include='*.c' \ $(LINT_VENDOR) --exclude='platform.h' --exclude='host_*'; then \ - echo "lint-platform: _WIN32 above. It's true on the Xbox too; use THESEUS_HOST_WIN or host.h."; bad=1; \ + echo "lint-platform: OS macro above. _WIN32 is true on the Xbox too; go through host.h."; bad=1; \ fi; \ - if grep -rnE '$(LINT_HOSTTEST)' $(LINT_SHAPED) \ - --include='*.h' --include='*.cpp' --include='*.c' $(LINT_VENDOR) --exclude='platform.h'; then \ - echo "lint-platform: host test above in Xbox-shaped code. Go through host.h."; bad=1; \ + if grep -rnE '$(LINT_HOSTTEST)' $(LINT_DIRS) \ + --include='*.h' --include='*.cpp' --include='*.c' $(LINT_VENDOR) --exclude='platform.h' \ + --exclude='host_*' $(foreach f,$(LINT_HOST_DATA),--exclude='$(f)'); then \ + echo "lint-platform: host test above. Go through host.h."; bad=1; \ fi; \ for f in $$(grep -rlE '$(LINT_HOSTTEST)' $(THESEUS_SRC)/desktop \ --include='*.h' --include='*.cpp' $(LINT_VENDOR) --exclude='host_*'); do \ From 73300754bcedbb880c72ca179819d93070158823 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:18:23 -0500 Subject: [PATCH 046/167] Rebuilt CSurfx from the 5960 binary Wrote the surface, both fade passes and line drawing fresh from 0x5f3f0, 0x5f430, 0x5f540 and 0x5fdf0, and wrote up the cell format and pen tables. Lines, fields and fades match byte for byte on every test case. The only change is pens 12 and wider now get the binary's round cap; the dash never draws wider than 1. --- docs/decomp/TmapSystem.md | 91 ++++++ theseus/render/tmap_system.cpp | 492 ++++++++++++++++----------------- 2 files changed, 325 insertions(+), 258 deletions(-) diff --git a/docs/decomp/TmapSystem.md b/docs/decomp/TmapSystem.md index ccf38fe..c00c53d 100644 --- a/docs/decomp/TmapSystem.md +++ b/docs/decomp/TmapSystem.md @@ -58,6 +58,97 @@ The rendering pipeline works as: 5. The 8-bit buffer is swizzled into the Xbox texture via the Swizzler API. 6. CPalette uploads a 256-entry hardware palette via D3DPalette. +## CSurfx (8-bit surface) + +A 0x24 byte object, allocated inline by the DynamicTexture update at `0x0005dde0`. + +| Offset | Field | +|--------|-------| +| 0x00 | width | +| 0x04 | height | +| 0x08 | pixels, `new[(height + 1) * width]` (one spare row) | +| 0x0c | bytes per row (= width) | +| 0x10 | line width, always 1 | +| 0x14..0x20 | clip rect `{0, 0, width, height}` | + +Constructor `0x0005f3f0` bumps a live count at `0x0017e75c`. The DynamicTexture destructor +(`0x0005dbc0`) frees the pixels, drops the count, and when it hits zero frees the shared fade +scratch buffer (`0x0017e764`, size at `0x0017e760`). + +### Delta cells + +A delta field is 3 bytes per pixel, read as a 32-bit load then masked. Bits 12..23 hold the +source x offset, bits 0..11 the source y offset, both in 1/32 pixel steps and biased by 63 +pixels (`0x7e0`). `0x7e07e0` means "this pixel pulls from itself". + +### Fade, out of place (`0x0005f430`) + +```c +src += -0x3f * srcPitch - 0x3f; +for (row...) { + for (x = 0; x < w; x++) { + code = *(uint *)grad; grad += 3; + p = src + x + ((code >> 12 & 0xfff) >> 5) + ((code & 0xfff) >> 5) * srcPitch; + fx = code >> 12 & 0x1f; fy = code & 0x1f; + dest[x] = ((p[pitch]*fy + p[0]*(32-fy))*(32-fx) + (p[pitch+1]*fy + p[1]*(32-fy))*fx) * 0x1f >> 0xf; + } + dest += destPitch; src += srcPitch; +} +``` + +Bilinear 5-bit weights total 1024, so `* 31 >> 15` is the blend times 31/32: every pass dims +the image a little, which is what makes trails fade out. + +### Fade, in place (`0x0005f540`) + +Takes the DeltaFieldData (`{negY rows, cells}`), the pixels, pitch, width and height. + +1. Grows the shared scratch buffer to `negY * pitch` if needed (free, then new). +2. Clamps the ring height to the image height. +3. Runs the out-of-place pass for the first `negY` rows into the scratch buffer. +4. For each later row: copies the oldest scratch row back into the image row `negY` above, + then computes the current row into that scratch slot (`slot = (slot + 1) % negY`). +5. Flushes the remaining scratch rows in order, `width >> 2` dwords per row. + +A pixel can pull from at most `negY` rows above, so a row only goes back into the image once +nothing still needs the old values. + +Quirks: the bilinear tap reads one row and one pixel past the source pixel, so the bottom-right +pixel reads one byte past the image (the spare row covers everything else). A y offset of more +than 63 rows up underflows its 12-bit field and borrows from the x field. + +## Line (`0x0005fdf0`) + +`__fastcall`, with the surface and color on the stack. + +- Each coordinate is folded as `((v >> 1 ^ v) & 0x3fffffff) ^ (v >> 1)`: low 30 bits kept, sign + copied into bits 30 and 31. +- Line width 4 and up is scaled by direction: with squared run lengths `dx2`, `dy2`, + `w = (lineWidth * (128 + 55 * minor / major) + 64) >> 7`, so diagonals get up to 1.43x. +- Clipping: the clip rect is inset by `w >> 1`. If the start is outside, the ends swap; if the + end is outside too, nothing is drawn. Step counts are trimmed so the walk stops one short of + the right and bottom edges. +- Width 1: integer Bresenham, one pixel per step, `x`-major when `dx >= dy`. +- Wider: first a round cap at the start, then per step a `w` pixel span across the minor axis + (`x`-major when `dx > dy`). Cap insets per row come from byte tables: + +| Width | Table | Insets | +|-------|-------|--------| +| 2 | 0x0002a364 | 0 0 | +| 3 | 0x0002a360 | 1 0 1 | +| 4 | 0x0002a358 | 1 0 0 1 | +| 5 | 0x0002a350 | 1 0 0 0 1 | +| 6 | 0x0002a348 | 1 0 0 0 0 1 | +| 7 | 0x0002a340 | 2 1 0 0 0 1 2 | +| 8 | 0x0002a334 | 2 1 0 0 0 0 1 2 | +| 9 | 0x0002a328 | 3 1 1 0 0 0 1 1 3 | +| 10 | 0x0002a31c | 3 1 1 0 0 0 0 1 1 3 | +| 11 | 0x0002a310 | 4 2 1 1 0 0 0 1 1 2 4 | + + From 12 up the inset is computed: `half - (int)fsqrt(half*half - k*k)` for `k = -half..`. + +Nothing in the 5960 dash changes the line width from 1, so only the 1 pixel path runs. + ## Palette Presets (10 styles) 0: Firestorm, 1: Aqua, 2: Purple & Blues, 3: Color Wheel, diff --git a/theseus/render/tmap_system.cpp b/theseus/render/tmap_system.cpp index e164870..6923c07 100644 --- a/theseus/render/tmap_system.cpp +++ b/theseus/render/tmap_system.cpp @@ -32,37 +32,40 @@ void fft_close (fft_state* state); // ===== CSurfx =============================================================== -// 8-bit palettized pixel surface. Used as the render target for all TMAP -// effects. Supports Bresenham line drawing and delta field warping. +// 8-bit palettized pixel surface. Every TMAP effect draws into one of these. int CSurfx::sTempRef = 0; long CSurfx::sTempSize = 0; char* CSurfx::sTemp = NULL; +// CSurfx::CSurfx(nWidth, nHeight) +// Reconstructed from 5960 retail XBE @ 0x0005f3f0 (inlined into +// CDynamicTexture's update at 0x0005dde0). Ghidra decompile: +// w = nWidth; h = nHeight; pels = new[(h + 1) * w]; +// lineWidth = 1; clip = {0, 0, w, h}; bytesPerRow = w; ++DAT_0017e75c; +// The spare row under the image catches the analyzer's peak dots at y == h. CSurfx::CSurfx(int nWidth, int nHeight) { m_nWidth = nWidth; m_nHeight = nHeight; - m_pels = new BYTE [nWidth * (nHeight + 1)]; - + m_pels = new BYTE [(nHeight + 1) * nWidth]; + mBytesPerRow = nWidth; mLineWidth = 1; - mClipRect.left = 0; mClipRect.top = 0; mClipRect.right = nWidth; mClipRect.bottom = nHeight; - - mBytesPerRow = nWidth; - - sTempRef += 1; + sTempRef++; } +// CSurfx::~CSurfx() +// Reconstructed from 5960 retail XBE, inlined into CDynamicTexture_dtor @ 0x0005dbc0: +// free(pels); if (--DAT_0017e75c == 0) { free(DAT_0017e764); DAT_0017e764 = 0; DAT_0017e760 = 0; } +// The last surface to go frees the shared fade scratch buffer. CSurfx::~CSurfx() { delete [] m_pels; - - sTempRef -= 1; - if (sTempRef == 0) + if (--sTempRef == 0) { delete [] sTemp; sTemp = NULL; @@ -70,138 +73,109 @@ CSurfx::~CSurfx() } } -void CSurfx::Fade(DeltaFieldData* inGrad) +// A delta field cell is 24 bits: source x offset in the top 12, source y in +// the bottom 12, both in 1/32 pixel steps and biased by 63 pixels (0x7E0). +#define DELTA_BIAS_PELS 63 +#define DELTA_FRAC_BITS 5 + +static inline unsigned long ReadDelta(const char* cell) { - Fade((char*)m_pels, m_nWidth, m_nWidth, m_nHeight, inGrad); + const unsigned char* b = (const unsigned char*)cell; + return b[0] | (b[1] << 8) | ((unsigned long)b[2] << 16); } -#define HALFCORD 0x003F -#define FIXED_BITS 5 - -// Source-to-source copy variant (separate source and dest buffers) -void CSurfx::Fade(const char* inSrce, long inBytesPerSrceRow, char* inDest, long inBytesPerDestRow, long inX, long inY, const char* inGrad) +// Bilinear pull for one destination pixel. base is the source pixel that a +// zero offset lands on, already shifted back by the 63 pixel bias. +static inline BYTE PullPel(const char* base, long bytesPerRow, unsigned long code) { - unsigned long p, x, y, u, v, u1, v1, P1, P2, P3, P4, codedDel, xGrad, yGrad; - const char* srceMap; + unsigned long dx = (code >> 12) & 0xFFF; + unsigned long dy = code & 0xFFF; + const unsigned char* p = (const unsigned char*)base + (dx >> DELTA_FRAC_BITS) + (dy >> DELTA_FRAC_BITS) * bytesPerRow; + unsigned long fx = dx & 31; + unsigned long fy = dy & 31; - inSrce = inSrce - HALFCORD * inBytesPerSrceRow - HALFCORD; + unsigned long left = p[0] * (32 - fy) + p[bytesPerRow] * fy; + unsigned long right = p[1] * (32 - fy) + p[bytesPerRow + 1] * fy; - for (y = 0; y < (unsigned)inY; y++) - { - for (x = 0; x < (unsigned)inX; x++) - { - codedDel = *((const unsigned long*) inGrad); - inGrad += 3; - xGrad = 0xFFF & (codedDel >> 12); - yGrad = 0xFFF & (codedDel); - - srceMap = inSrce + (xGrad >> FIXED_BITS) + (yGrad >> FIXED_BITS) * inBytesPerSrceRow + x; - - u = (yGrad & 0x1F); - v = (xGrad & 0x1F); - - P1 = ((unsigned char*) srceMap)[0]; - P2 = ((unsigned char*) srceMap)[1]; - - u1 = 0x20 - u; - P1 *= u1; - P2 *= u1; - - P3 = ((unsigned char*) srceMap)[inBytesPerSrceRow]; - P4 = ((unsigned char*) srceMap)[inBytesPerSrceRow + 1]; + // 32 * 32 weights sum to 1024; the * 31 >> 15 also dims each pass by 1/32. + return (BYTE)(((left * (32 - fx) + right * fx) * 31) >> 15); +} - v1 = 0x20 - v; - P3 *= u; - P4 *= u; +// CSurfx::Fade(inGrad) +// Reconstructed from the call in CImageFader_RenderDynamicTexture @ 0x0005e120: +// Fade(pels, width, width, height, field) width doubles as the row pitch. +void CSurfx::Fade(DeltaFieldData* inGrad) +{ + Fade((char*)m_pels, m_nWidth, m_nWidth, m_nHeight, inGrad); +} - // Bilinear interpolation - p = v * (P2 + P4) + v1 * (P1 + P3); +// CSurfx::Fade(inSrce, srcPitch, inDest, destPitch, inX, inY, inGrad) +// Reconstructed from 5960 retail XBE @ 0x0005f430. Ghidra decompile: +// src += -0x3f * srcPitch - 0x3f; +// for each row: for x < inX: code = *(uint*)grad; grad += 3; +// p = src + x + (code>>12 & 0xfff)>>5 + ((code & 0xfff)>>5) * srcPitch; +// dest[x] = ((p[pitch]*fy + p[0]*(32-fy))*(32-fx) + (p[pitch+1]*fy + p[1]*(32-fy))*fx) * 0x1f >> 0xf; +// dest += destPitch; src += srcPitch; +// Out-of-place pass: warps inY rows of source into a separate buffer. +void CSurfx::Fade(const char* inSrce, long inBytesPerSrceRow, char* inDest, long inBytesPerDestRow, long inX, long inY, const char* inGrad) +{ + const char* base = inSrce - DELTA_BIAS_PELS * inBytesPerSrceRow - DELTA_BIAS_PELS; - ((unsigned char*) inDest)[x] = (unsigned char)((31 * p) >> 15); - } + for (long row = 0; row < inY; row++) + { + for (long x = 0; x < inX; x++, inGrad += 3) + inDest[x] = (char)PullPel(base + x, inBytesPerSrceRow, ReadDelta(inGrad)); + base += inBytesPerSrceRow; inDest += inBytesPerDestRow; - inSrce += inBytesPerSrceRow; } } -// In-place variant using a trailing row buffer to avoid overwriting source data +// CSurfx::Fade(inPix, inBytesPerRow, inX, inY, inGrad) +// Reconstructed from 5960 retail XBE @ 0x0005f540. Warps the image in place. +// A pixel can pull from up to mNegYExtents rows above itself, so finished rows +// wait in a ring of that many rows (the shared DAT_0017e764 buffer, grown on +// demand) and only overwrite the image once nothing can read them anymore. +// Steps: first ring-full of rows goes through the out-of-place pass; each later +// row swaps the oldest ring row into the image and computes into its slot; the +// tail of the ring is flushed in order, inX / 4 dwords per row like the binary. +// See docs/decomp/TmapSystem.md. void CSurfx::Fade(char* inPix, long inBytesPerRow, long inX, long inY, DeltaFieldData* inGrad) { - unsigned long p, x, y, u, v, u1, v1, P1, P2, P3, P4, codedDel, xGrad, yGrad, curBufRowNum; - long bufRows; - char* srceMap, *curBufRow, *srce, *grad = inGrad->mField; - - bufRows = inGrad->mNegYExtents; - if (sTempSize < bufRows * inBytesPerRow) + long ringRows = inGrad->mNegYExtents; + if (sTempSize < ringRows * inBytesPerRow) { - sTempSize = bufRows * inBytesPerRow; + sTempSize = ringRows * inBytesPerRow; delete [] sTemp; sTemp = new char [sTempSize]; } + if (ringRows > inY) + ringRows = inY; - if (inY <= bufRows) - bufRows = inY; - Fade(inPix, inBytesPerRow, sTemp, inBytesPerRow, inX, bufRows, grad); - inY -= bufRows; - grad += 3 * inX * bufRows; - curBufRowNum = 0; + const char* grad = inGrad->mField; + Fade(inPix, inBytesPerRow, sTemp, inBytesPerRow, inX, ringRows, grad); + grad += 3 * inX * ringRows; - srce = inPix + bufRows * inBytesPerRow - HALFCORD * inBytesPerRow - HALFCORD; + const char* base = inPix + (ringRows - DELTA_BIAS_PELS) * inBytesPerRow - DELTA_BIAS_PELS; + long slot = 0; - for (y = 0; y < (unsigned)inY; y += 1) + for (long row = ringRows; row < inY; row++) { - curBufRow = sTemp + curBufRowNum * inBytesPerRow; - - for (x = 0; x < (unsigned)inX; x += 1) + char* ring = sTemp + slot * inBytesPerRow; + for (long x = 0; x < inX; x++, grad += 3) { - codedDel = *((const unsigned long*) grad); - grad += 3; - xGrad = 0xFFF & (codedDel >> 12); - yGrad = 0xFFF & (codedDel); - - inPix[x] = curBufRow[x]; - - srceMap = srce + (xGrad >> FIXED_BITS) + (yGrad >> FIXED_BITS) * inBytesPerRow + x; - - u = (yGrad & 0x1F); - v = (xGrad & 0x1F); - - P1 = ((unsigned char*)srceMap)[0]; - P2 = ((unsigned char*)srceMap)[1]; - - u1 = 0x20 - u; - P1 *= u1; - P2 *= u1; - - P3 = ((unsigned char*)srceMap)[inBytesPerRow]; - P4 = ((unsigned char*)srceMap)[inBytesPerRow + 1]; - - v1 = 0x20 - v; - P3 *= u; - P4 *= u; - - p = v * (P2 + P4) + v1 * (P1 + P3); - - curBufRow[x] = (char)((31 * p) >> 15); + inPix[x] = ring[x]; + ring[x] = (char)PullPel(base + x, inBytesPerRow, ReadDelta(grad)); } - curBufRowNum = (curBufRowNum + 1) % bufRows; + slot = (slot + 1) % ringRows; inPix += inBytesPerRow; - srce += inBytesPerRow; + base += inBytesPerRow; } - // Flush trailing buffer - p = inX >> 2; - for (y = 0; y < (unsigned)bufRows; y += 1) + for (long i = 0; i < ringRows; i++) { - curBufRow = sTemp + ((curBufRowNum + y) % bufRows) * inBytesPerRow; - for (x = 0; x < p; x += 1) - { - // DWORD, not unsigned long, which is 8 bytes on 64-bit desktop builds. - ((DWORD*)inPix)[x] = ((DWORD*)curBufRow)[x]; - } - + memcpy(inPix, sTemp + ((slot + i) % ringRows) * inBytesPerRow, (inX / 4) * 4); inPix += inBytesPerRow; } } @@ -379,192 +353,194 @@ void MakePalette(DWORD outPalette [256]) // ===== Line Drawing ========================================================= -// Bresenham line with variable-width pen and circle stamp for thick lines. - -#define __doXerr error_term += dy; \ - if (error_term >= dx) { \ - error_term -= dx; \ - basePtr += rowOffset; \ - ymov--; \ - } +// Bresenham stepping with an optional round pen. Nothing in the dash ever +// raises mLineWidth above 1, but the wide pen is there and kept working. -#define __doYerr error_term += dx; \ - if (error_term >= dy) { \ - error_term -= dy; \ - basePtr += xDirection; \ - xmov--; \ - } +// Pen insets per row for widths 2..11, from the byte tables at 0x0002a310..0x0002a364. +static const char* PenShape(int width) +{ + static const char w2[] = { 0, 0 }; + static const char w3[] = { 1, 0, 1 }; + static const char w4[] = { 1, 0, 0, 1 }; + static const char w5[] = { 1, 0, 0, 0, 1 }; + static const char w6[] = { 1, 0, 0, 0, 0, 1 }; + static const char w7[] = { 2, 1, 0, 0, 0, 1, 2 }; + static const char w8[] = { 2, 1, 0, 0, 0, 0, 1, 2 }; + static const char w9[] = { 3, 1, 1, 0, 0, 0, 1, 1, 3 }; + static const char w10[] = { 3, 1, 1, 0, 0, 0, 0, 1, 1, 3 }; + static const char w11[] = { 4, 2, 1, 1, 0, 0, 0, 1, 1, 2, 4 }; + static const char* const shapes[] = { w2, w3, w4, w5, w6, w7, w8, w9, w10, w11 }; + return shapes[width - 2]; +} -#define __circ(dia, a) switch ((dia)) { \ - case 2: a = "\0\0"; break; \ - case 3: a = "\1\0\1"; break; \ - case 4: a = "\1\0\0\1"; break; \ - case 5: a = "\1\0\0\0\1"; break; \ - case 6: a = "\1\0\0\0\0\1"; break; \ - case 7: a = "\2\1\0\0\0\1\2"; break; \ - case 8: a = "\2\1\0\0\0\0\1\2"; break; \ - case 9: a = "\3\1\1\0\0\0\1\1\3"; break; \ - case 10: a = "\3\1\1\0\0\0\0\1\1\3"; break; \ - case 11: a = "\4\2\1\1\0\0\0\1\1\2\4"; break;\ - case 12: a = "\4\2\1\1\0\0\0\0\1\1\2\4"; break;\ - } +// Keeps bits 0..29 and copies the sign into bits 30 and 31, so a wild +// coordinate keeps its side of the surface without overflowing the math below. +static inline int FoldCoord(int v) +{ + return (v & 0x3FFFFFFF) | ((v >> 1) & (int)0xC0000000); +} +// CSurfx::Line(sx, sy, ex, ey, color) +// Reconstructed from 5960 retail XBE @ 0x0005fdf0. Behavior: +// - Pens wider than 3 shrink toward 128/128 on axis lines and 183/128 on +// diagonals: w = (lineWidth * (128 + 55 * minor^2 / major^2) + 64) >> 7. +// - If the start sits outside the clip rect (inset by half the pen) the ends +// are swapped; if both do, nothing is drawn. Step counts are trimmed so the +// walk stops at the clip edge. +// - Width 1 plots single pixels. Wider pens stamp a round cap at the start +// (insets from the 0x2a310 tables below 12, halfW - sqrt(halfW^2 - k^2) at +// 12 and up) and then lay a w pixel span across the minor axis per step. +// The stepping itself is the textbook integer Bresenham walk; see +// docs/decomp/TmapSystem.md for the full breakdown. void CSurfx::Line(int sx, int sy, int ex, int ey, unsigned char color) { - long xDirection, rowOffset, error_term; - char* basePtr, *center; - long xmov, ymov, dx, dy, t, j, lw; - long penExtents; - - // int on purpose: through a 64-bit long a negative coordinate folds to a huge positive one. - sx = (((int)(sx & 0x80000000)) >> 1) | (sx & 0x3FFFFFFF); - ex = (((int)(ex & 0x80000000)) >> 1) | (ex & 0x3FFFFFFF); - sy = (((int)(sy & 0x80000000)) >> 1) | (sy & 0x3FFFFFFF); - ey = (((int)(ey & 0x80000000)) >> 1) | (ey & 0x3FFFFFFF); - - lw = mLineWidth; - if (mLineWidth > 3) { - dx = ex - sx; dx = dx * dx; - dy = ey - sy; dy = dy * dy; - if (dx > 0 && dx >= dy) - lw = 128 + 55 * dy / dx; - else if (dy > 0 && dy > dx) - lw = 128 + 55 * dx / dy; - - if (dx > 0 || dy > 0) - lw = (mLineWidth * lw + 64) >> 7; - } - penExtents = lw >> 1; - - // Clipping - if (sx < mClipRect.left + penExtents || sx >= mClipRect.right - penExtents || sy < mClipRect.top + penExtents || sy >= mClipRect.bottom - penExtents) { - if (ex < mClipRect.left + penExtents || ex >= mClipRect.right - penExtents || ey < mClipRect.top + penExtents || ey >= mClipRect.bottom - penExtents) - return; + sx = FoldCoord(sx); + sy = FoldCoord(sy); + ex = FoldCoord(ex); + ey = FoldCoord(ey); + + long w = mLineWidth; + if (mLineWidth > 3) + { + // 64 bit squares: the folded coordinates can reach 2^30. + long long dx2 = (long long)(ex - sx) * (ex - sx); + long long dy2 = (long long)(ey - sy) * (ey - sy); + if (dx2 > 0 && dx2 >= dy2) + w = (long)(128 + 55 * dy2 / dx2); + else if (dy2 > 0 && dy2 > dx2) + w = (long)(128 + 55 * dx2 / dy2); + if (dx2 > 0 || dy2 > 0) + w = (mLineWidth * w + 64) >> 7; + } + long half = w >> 1; + + long left = mClipRect.left + half; + long top = mClipRect.top + half; + long right = mClipRect.right - half; + long bottom = mClipRect.bottom - half; - t = ex; ex = sx; sx = t; - t = ey; ey = sy; sy = t; + if (sx < left || sx >= right || sy < top || sy >= bottom) + { + if (ex < left || ex >= right || ey < top || ey >= bottom) + return; + int t = sx; sx = ex; ex = t; + t = sy; sy = ey; ey = t; } - dx = ex - sx; - dy = ey - sy; + // Major/minor run lengths, step directions, and how many steps fit on screen. + long dx = ex - sx, dy = ey - sy; + long xStep, xLeft, rowStep, yLeft; - dx = ex - sx; - xmov = dx; - if (dx < 0) { - xmov = -dx; - if (sx - xmov < mClipRect.left + penExtents) - xmov = sx - (mClipRect.left + penExtents); - xDirection = -1; + if (dx < 0) + { dx = -dx; - } else if (dx > 0) { - if (sx + xmov >= mClipRect.right - penExtents) - xmov = mClipRect.right - penExtents - 1 - sx; - xDirection = 1; - } else { - xDirection = 0; - } - - ymov = dy; - if (dy < 0) { - ymov = -dy; - if (sy - ymov < mClipRect.top + penExtents) - ymov = sy - (mClipRect.top + penExtents); - rowOffset = -mBytesPerRow; - dy = -dy; - } else { - if (sy + ymov >= mClipRect.bottom - penExtents) - ymov = mClipRect.bottom - penExtents - sy - 1; - rowOffset = mBytesPerRow; + xStep = -1; + xLeft = (sx - dx < left) ? sx - left : dx; + } + else if (dx > 0) + { + xStep = 1; + xLeft = (sx + dx >= right) ? right - 1 - sx : dx; + } + else + { + xStep = 0; + xLeft = 0; } - basePtr = (char*)m_pels + sy * mBytesPerRow + sx * 1; - error_term = 0; + if (dy < 0) + { + dy = -dy; + rowStep = -mBytesPerRow; + yLeft = (sy - dy < top) ? sy - top : dy; + } + else + { + rowStep = mBytesPerRow; + yLeft = (sy + dy >= bottom) ? bottom - 1 - sy : dy; + } - long halfW; + unsigned char* p = m_pels + sy * mBytesPerRow + sx; + long err = 0; - if (lw > 1) + if (w > 1) { - long c_x, tw = mLineWidth; - halfW = (tw) >> 1; + long pen = mLineWidth; + long penHalf = pen >> 1; + const char* shape = pen < 12 ? PenShape(pen) : NULL; - if (tw < 12) - { - char* c_shape; - __circ(tw, c_shape) - for (j = 0; j < tw; j++) - { - c_x = c_shape[j]; - center = basePtr + (j - halfW) * mBytesPerRow; - int k; - for (k = c_x; k < tw - c_x; k++) - { - ((unsigned char*) center)[k - halfW] = color; - } - } - } - else + for (long row = 0; row < pen; row++) { - for (j = 0; j < tw; j++) - { - c_x = 0; - center = basePtr + (j - halfW) * mBytesPerRow; - for (int k = c_x; k < tw - c_x; k++) - { - ((unsigned char*) center)[k - halfW] = color; - } - } + long k = row - penHalf; + long inset = shape ? shape[row] : penHalf - (long)sqrt((double)(penHalf * penHalf - k * k)); + if (inset < pen - inset) + memset(p + k * mBytesPerRow + inset - penHalf, color, pen - 2 * inset); } - halfW = lw >> 1; - if (dx > dy) { - for (; xmov >= 0 && ymov >= 0; xmov--) + for (; xLeft >= 0 && yLeft >= 0; xLeft--) { - center = basePtr - halfW * mBytesPerRow; - for (j = 0; j < lw; j++) + unsigned char* q = p - half * mBytesPerRow; + for (long i = 0; i < w; i++, q += mBytesPerRow) + *q = color; + + p += xStep; + err += dy; + if (err >= dx) { - *((unsigned char*) center) = color; - center += mBytesPerRow; + err -= dx; + p += rowStep; + yLeft--; } - - basePtr += xDirection; - __doXerr } } else { - for (; ymov >= 0 && xmov >= 0; ymov--) + for (; yLeft >= 0 && xLeft >= 0; yLeft--) { - center = basePtr - (halfW) * 1; - for (j = 0; j < lw; j++) + memset(p - half, color, w); + + p += rowStep; + err += dx; + if (err >= dy) { - *((unsigned char*) center) = color; - center += 1; + err -= dy; + p += xStep; + xLeft--; } - basePtr += rowOffset; - __doYerr } } + return; } - else + + if (dx >= dy) { - if (dx >= dy) + for (; xLeft >= 0 && yLeft >= 0; xLeft--) { - for (; xmov >= 0 && ymov >= 0; xmov--) + *p = color; + p += xStep; + err += dy; + if (err >= dx) { - *((unsigned char*) basePtr) = color; - basePtr += xDirection; - __doXerr + err -= dx; + p += rowStep; + yLeft--; } } - else + } + else + { + for (; yLeft >= 0 && xLeft >= 0; yLeft--) { - for (; ymov >= 0 && xmov >= 0; ymov--) + *p = color; + p += rowStep; + err += dx; + if (err >= dy) { - *((unsigned char*) basePtr) = color; - basePtr += rowOffset; - __doYerr + err -= dy; + p += xStep; + xLeft--; } } } From 1c3b31c62ca6cb2d3cdbb05d9e830c4faadcabd2 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:18:26 -0500 Subject: [PATCH 047/167] Renamed the compiler's locals to match what they hold Gave the token, function, member and array locals names of their own. No behavior change; the shipped XAPs compile to the same bytes. --- theseus/engine/xap_compile.cpp | 450 ++++++++++++++++----------------- 1 file changed, 225 insertions(+), 225 deletions(-) diff --git a/theseus/engine/xap_compile.cpp b/theseus/engine/xap_compile.cpp index 7771e0f..adaa46f 100755 --- a/theseus/engine/xap_compile.cpp +++ b/theseus/engine/xap_compile.cpp @@ -26,7 +26,7 @@ CNameSpace* g_classes; // Every hard failure hands back an empty string so callers stop cleanly. // 5960 points at a shared L"" (0x201c4 / 0x21b98) the same way. -static const TCHAR szParseError[] = _T(""); +static const TCHAR szGiveUp[] = _T(""); static inline bool IsLetter(TCHAR ch) { @@ -50,9 +50,9 @@ static inline bool IsIdentChar(TCHAR ch) return IsLetter(ch) || IsDigit(ch) || ch == '_'; } -static inline bool IsKeyword(const TCHAR* pchToken, int cchToken, const TCHAR* szKeyword, int cchKeyword) +static inline bool IsKeyword(const TCHAR* pchTok, int cchTok, const TCHAR* szKeyword, int cchKeyword) { - return cchToken == cchKeyword && _tcsncmp(pchToken, szKeyword, cchKeyword) == 0; + return cchTok == cchKeyword && _tcsncmp(pchTok, szKeyword, cchKeyword) == 0; } @@ -180,7 +180,7 @@ const TCHAR* SkipWhite(const TCHAR* pch, COMMENT_TYPE ct) // A letter or '_' starts an identifier (letters, digits, '_'). Otherwise a // word character starts a number or bare word, and anything else starts an // operator run that lasts until a blank or a word character. -const TCHAR* Token(const TCHAR* pch, const TCHAR*& pchToken, int& cchToken, bool bAllowPaths, COMMENT_TYPE ct) +const TCHAR* Token(const TCHAR* pch, const TCHAR*& pchTok, int& cchTok, bool bAllowPaths, COMMENT_TYPE ct) { pch = SkipWhite(pch, ct); @@ -188,7 +188,7 @@ const TCHAR* Token(const TCHAR* pch, const TCHAR*& pchToken, int& cchToken, bool if (*pch == '"' || *pch == '\'') chQuote = *pch++; - pchToken = pch; + pchTok = pch; if (chQuote != 0) { @@ -219,7 +219,7 @@ const TCHAR* Token(const TCHAR* pch, const TCHAR*& pchToken, int& cchToken, bool pch++; } - cchToken = (int)(pch - pchToken); + cchTok = (int)(pch - pchTok); if (chQuote != 0 && *pch == chQuote) pch++; @@ -262,12 +262,12 @@ static bool ReadHexEscape(const TCHAR*& pch, int& cch, int cDigits, TCHAR& chOut // when the output fills, the string ends on a backslash, or a hex escape is // short or bad. Returns the number of characters written. // \u only exists in wide builds, where TCHAR can hold it (5960 is wide). -int ExpandCString(TCHAR* szString, int cchMaxString, const TCHAR* pchToken, int cchToken) +int ExpandCString(TCHAR* szOut, int cchOut, const TCHAR* pchTok, int cchTok) { - TCHAR* pchOut = szString; - TCHAR* pchEnd = szString + cchMaxString; + TCHAR* pchOut = szOut; + TCHAR* pchEnd = szOut + cchOut; - while (cchToken > 0) + while (cchTok > 0) { if (pchOut >= pchEnd) { @@ -275,8 +275,8 @@ int ExpandCString(TCHAR* szString, int cchMaxString, const TCHAR* pchToken, int return -1; } - TCHAR ch = *pchToken++; - cchToken--; + TCHAR ch = *pchTok++; + cchTok--; if (ch != '\\') { @@ -284,14 +284,14 @@ int ExpandCString(TCHAR* szString, int cchMaxString, const TCHAR* pchToken, int continue; } - if (cchToken == 0) + if (cchTok == 0) { SyntaxError(_T("backslash at end of string")); return -1; } - ch = *pchToken++; - cchToken--; + ch = *pchTok++; + cchTok--; switch (ch) { @@ -316,12 +316,12 @@ int ExpandCString(TCHAR* szString, int cchMaxString, const TCHAR* pchToken, int break; case 'x': - if (cchToken < 2) + if (cchTok < 2) { SyntaxError(_T("Invalid hex character escape sequence")); return -1; } - if (!ReadHexEscape(pchToken, cchToken, 2, ch)) + if (!ReadHexEscape(pchTok, cchTok, 2, ch)) { SyntaxError(_T("Invalid hex character")); return -1; @@ -330,12 +330,12 @@ int ExpandCString(TCHAR* szString, int cchMaxString, const TCHAR* pchToken, int #ifdef _UNICODE case 'u': - if (cchToken < 4) + if (cchTok < 4) { SyntaxError(_T("Invalid Unicode character escape sequence")); return -1; } - if (!ReadHexEscape(pchToken, cchToken, 4, ch)) + if (!ReadHexEscape(pchTok, cchTok, 4, ch)) { SyntaxError(_T("Invalid Unicode character")); return -1; @@ -347,7 +347,7 @@ int ExpandCString(TCHAR* szString, int cchMaxString, const TCHAR* pchToken, int if (ch < '0' || ch > '7') break; - if (ch == '0' && cchToken > 0 && (*pchToken < '0' || *pchToken > '7')) + if (ch == '0' && cchTok > 0 && (*pchTok < '0' || *pchTok > '7')) { ch = '\0'; break; @@ -355,8 +355,8 @@ int ExpandCString(TCHAR* szString, int cchMaxString, const TCHAR* pchToken, int { int nValue = ch - '0'; - for (; cchToken != 0 && *pchToken >= '0' && *pchToken <= '7'; pchToken++, cchToken--) - nValue = nValue * 8 + (*pchToken - '0'); + for (; cchTok != 0 && *pchTok >= '0' && *pchTok <= '7'; pchTok++, cchTok--) + nValue = nValue * 8 + (*pchTok - '0'); ch = (TCHAR)nValue; } break; @@ -365,7 +365,7 @@ int ExpandCString(TCHAR* szString, int cchMaxString, const TCHAR* pchToken, int *pchOut++ = ch; } - return (int)(pchOut - szString); + return (int)(pchOut - szOut); } @@ -377,13 +377,13 @@ int ExpandCString(TCHAR* szString, int cchMaxString, const TCHAR* pchToken, int // Case-insensitive prefix compare over the token length, same as the binary. const TCHAR* ParseBoolean(const TCHAR* pch, bool& b) { - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); + const TCHAR* pchTok; + int cchTok; + pch = Token(pch, pchTok, cchTok); - if (_tcsnicmp(pchToken, _T("true"), cchToken) == 0) + if (_tcsnicmp(pchTok, _T("true"), cchTok) == 0) b = true; - else if (_tcsnicmp(pchToken, _T("false"), cchToken) == 0) + else if (_tcsnicmp(pchTok, _T("false"), cchTok) == 0) b = false; else SyntaxError(_T("Invalid boolean value")); @@ -393,15 +393,15 @@ const TCHAR* ParseBoolean(const TCHAR* pch, bool& b) // Copies a numeric token into a terminated buffer. 5960 copies blind into a // 64 character stack buffer; Theseus clamps long tokens and flags them. -static void CopyNumberToken(TCHAR* szBuf, int cchBuf, const TCHAR* pchToken, int cchToken) +static void CopyNumberToken(TCHAR* szBuf, int cchBuf, const TCHAR* pchTok, int cchTok) { - if (!CHECK(cchToken < cchBuf)) + if (!CHECK(cchTok < cchBuf)) { SyntaxError(_T("Number is too long")); - cchToken = cchBuf - 1; + cchTok = cchBuf - 1; } - _tcsncpy(szBuf, pchToken, cchToken); - szBuf[cchToken] = '\0'; + _tcsncpy(szBuf, pchTok, cchTok); + szBuf[cchTok] = '\0'; } // ParseInteger / ParseNumber @@ -411,24 +411,24 @@ static void CopyNumberToken(TCHAR* szBuf, int cchBuf, const TCHAR* pchToken, int // The CHECK on the length is Theseus's bounds fix. const TCHAR* ParseInteger(const TCHAR* pch, int& i) { - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); + const TCHAR* pchTok; + int cchTok; + pch = Token(pch, pchTok, cchTok); TCHAR szNumber[64]; - CopyNumberToken(szNumber, countof(szNumber), pchToken, cchToken); + CopyNumberToken(szNumber, countof(szNumber), pchTok, cchTok); i = _ttoi(szNumber); return pch; } const TCHAR* ParseNumber(const TCHAR* pch, float& n) { - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); + const TCHAR* pchTok; + int cchTok; + pch = Token(pch, pchTok, cchTok); TCHAR szNumber[64]; - CopyNumberToken(szNumber, countof(szNumber), pchToken, cchToken); + CopyNumberToken(szNumber, countof(szNumber), pchTok, cchTok); n = (float)_tcstod(szNumber, NULL); return pch; } @@ -439,13 +439,13 @@ const TCHAR* ParseNumber(const TCHAR* pch, float& n) // No escape expansion: property strings are taken as written. const TCHAR* ParseString(const TCHAR* pch, TCHAR*& sz) { - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); + const TCHAR* pchTok; + int cchTok; + pch = Token(pch, pchTok, cchTok); - sz = new TCHAR[cchToken + 1]; - CopyChars(sz, pchToken, cchToken); - sz[cchToken] = '\0'; + sz = new TCHAR[cchTok + 1]; + CopyChars(sz, pchTok, cchTok); + sz[cchTok] = '\0'; return pch; } @@ -485,13 +485,13 @@ static bool ArrayRanOut(const TCHAR* pch) // Returns past the ']'. A missing '[' is a syntax error and returns "". // 5960 inlines the number read in ParseVecArray; Theseus calls ParseNumber so // the long token CHECK covers arrays too. -const TCHAR* ParseIntArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray) +const TCHAR* ParseIntArray(const TCHAR* pch, BYTE*& pbItems, int& cbArray) { pch = SkipWhite(pch); if (*pch != '[') { SyntaxError(_T("Expected '[' to start array")); - return szParseError; + return szGiveUp; } const TCHAR* pchFirst = pch + 1; @@ -515,8 +515,8 @@ const TCHAR* ParseIntArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray) return pch; cbArray = cValues * sizeof(int); - pbArray = new BYTE[cbArray]; - int* pnOut = (int*)pbArray; + pbItems = new BYTE[cbArray]; + int* pnOut = (int*)pbItems; g_nLine = nLineFirst; for (pch = SkipWhite(pchFirst); *pch != ']' && !ArrayRanOut(pch); pch = SkipWhite(pch)) @@ -533,13 +533,13 @@ const TCHAR* ParseIntArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray) return pch + 1; } -const TCHAR* ParseVecArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray, int nVecSize) +const TCHAR* ParseVecArray(const TCHAR* pch, BYTE*& pbItems, int& cbArray, int nVecSize) { pch = SkipWhite(pch); if (*pch != '[') { SyntaxError(_T("Expected '[' to start array")); - return szParseError; + return szGiveUp; } const TCHAR* pchFirst = pch + 1; @@ -566,8 +566,8 @@ const TCHAR* ParseVecArray(const TCHAR* pch, BYTE*& pbArray, int& cbArray, int n return pch; cbArray = cValues * sizeof(float); - pbArray = new BYTE[cbArray]; - float* pnOut = (float*)pbArray; + pbItems = new BYTE[cbArray]; + float* pnOut = (float*)pbItems; g_nLine = nLineFirst; for (pch = SkipWhite(pchFirst); *pch != ']' && !ArrayRanOut(pch); pch = SkipWhite(pch)) @@ -649,12 +649,12 @@ CFunction* CCompiler::CreateFunction() { #pragma push_macro("new") #undef new - CFunction* pFunction = new(m_nop) CFunction; + CFunction* pFn = new(m_nop) CFunction; #pragma pop_macro("new") - pFunction->m_codeSize = m_nop; - CopyMemory(pFunction->m_rgop, m_ops, m_nop); - return pFunction; + pFn->m_codeSize = m_nop; + CopyMemory(pFn->m_rgop, m_ops, m_nop); + return pFn; } @@ -708,9 +708,9 @@ static const int nLowestPriority = 14; // for (i = 0; i < 22; i++) if (t[i].ch1 == pch[0] && (!t[i].ch2 || t[i].ch2 == pch[1])) { // *ppdoper = &t[i]; return pch + (t[i].ch2 ? 2 : 1); } // return pch; -const TCHAR* CFunctionCompiler::ParseOperator(const TCHAR* pch, const DOPER*& pdoper) +const TCHAR* CFunctionCompiler::ParseOperator(const TCHAR* pch, const DOPER*& pOp) { - pdoper = NULL; + pOp = NULL; pch = SkipWhite(pch); if (*pch == '\0') @@ -723,7 +723,7 @@ const TCHAR* CFunctionCompiler::ParseOperator(const TCHAR* pch, const DOPER*& pd if (p->m_ch2 != 0 && p->m_ch2 != pch[1]) continue; - pdoper = p; + pOp = p; return pch + (p->m_ch2 != 0 ? 2 : 1); } @@ -774,34 +774,34 @@ const TCHAR* CFunctionCompiler::ParseTerm(const TCHAR* pch) { const bool bString = (*pch == '"' || *pch == '\''); - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); + const TCHAR* pchTok; + int cchTok; + pch = Token(pch, pchTok, cchTok); if (bString) { Write(opStr); TCHAR szText[1024]; - int cchText = ExpandCString(szText, countof(szText), pchToken, cchToken); + int cchText = ExpandCString(szText, countof(szText), pchTok, cchTok); if (cchText < 0) - return szParseError; + return szGiveUp; WriteString(szText, cchText); } - else if (cchToken == 0) + else if (cchTok == 0) { return pch; } - else if (*pchToken == '.' || *pchToken == '-' || IsDigit(*pchToken)) + else if (*pchTok == '.' || *pchTok == '-' || IsDigit(*pchTok)) { // Theseus clamps over-long numbers; 5960 copies them blind. TCHAR szNumber[256]; - if (!CHECK(cchToken < countof(szNumber))) + if (!CHECK(cchTok < countof(szNumber))) { SyntaxError(_T("Number is too long")); - cchToken = countof(szNumber) - 1; + cchTok = countof(szNumber) - 1; } - CopyChars(szNumber, pchToken, cchToken); - szNumber[cchToken] = '\0'; + CopyChars(szNumber, pchTok, cchTok); + szNumber[cchTok] = '\0'; float nValue = (float)_tcstod(szNumber, NULL); if (bNegate) @@ -812,25 +812,25 @@ const TCHAR* CFunctionCompiler::ParseTerm(const TCHAR* pch) Write(opNum); WriteNumber(nValue); } - else if (IsKeyword(pchToken, cchToken, _T("true"), 4)) + else if (IsKeyword(pchTok, cchTok, _T("true"), 4)) { Write(opNum); WriteNumber(1.0f); } - else if (IsKeyword(pchToken, cchToken, _T("false"), 5)) + else if (IsKeyword(pchTok, cchTok, _T("false"), 5)) { Write(opNum); WriteNumber(0.0f); } - else if (IsKeyword(pchToken, cchToken, _T("this"), 4)) + else if (IsKeyword(pchTok, cchTok, _T("this"), 4)) { Write(opThis); } - else if (IsKeyword(pchToken, cchToken, _T("null"), 4)) + else if (IsKeyword(pchTok, cchTok, _T("null"), 4)) { Write(opNull); } - else if (IsKeyword(pchToken, cchToken, _T("new"), 3)) + else if (IsKeyword(pchTok, cchTok, _T("new"), 3)) { const TCHAR* pchClass; int cchClass; @@ -869,7 +869,7 @@ const TCHAR* CFunctionCompiler::ParseTerm(const TCHAR* pch) { int iLocal = 0; while (iLocal < m_nFrameSize && - !(m_rgstLocal[iLocal].cchName == cchToken && _tcsncmp(pchToken, m_rgstLocal[iLocal].pchName, cchToken) == 0)) + !(m_rgstLocal[iLocal].cchName == cchTok && _tcsncmp(pchTok, m_rgstLocal[iLocal].pchName, cchTok) == 0)) { iLocal++; } @@ -882,7 +882,7 @@ const TCHAR* CFunctionCompiler::ParseTerm(const TCHAR* pch) else { Write(opVar); - WriteString(pchToken, cchToken); + WriteString(pchTok, cchTok); } } } @@ -973,13 +973,13 @@ const TCHAR* CFunctionCompiler::ParseExp(const TCHAR* pch, int nPrio) pch = ParseArray(pch); } - const DOPER* pdoper; - const TCHAR* pchRight = ParseOperator(pch, pdoper); - if (pdoper == NULL || pdoper->m_pri != nPrio) + const DOPER* pOp; + const TCHAR* pchRight = ParseOperator(pch, pOp); + if (pOp == NULL || pOp->m_pri != nPrio) return pch; pch = ParseExp(pchRight, nPrio - 1); - Write(pdoper->m_op); + Write(pOp->m_op); } } @@ -1019,13 +1019,13 @@ const TCHAR* CFunctionCompiler::ParseIF(const TCHAR* pch) pch = ParseStatement(pch + 1); - const TCHAR* pchToken; - int cchToken; - const TCHAR* pchAfter = Token(pch, pchToken, cchToken); - if (!IsKeyword(pchToken, cchToken, _T("else"), 4)) + const TCHAR* pchTok; + int cchTok; + const TCHAR* pchAfter = Token(pch, pchTok, cchTok); + if (!IsKeyword(pchTok, cchTok, _T("else"), 4)) { Fixup(nopSkipThen, GetAddress()); - return pchToken; + return pchTok; } Write(opJump); @@ -1054,7 +1054,7 @@ const TCHAR* CFunctionCompiler::ParseWHILE(const TCHAR* pch) if (*pch != '(') { SyntaxError(_T("expected '('")); - return szParseError; + return szGiveUp; } int nopOuterTop = m_nopTopOfLoop; @@ -1064,7 +1064,7 @@ const TCHAR* CFunctionCompiler::ParseWHILE(const TCHAR* pch) if (*pch != ')') { SyntaxError(_T("expected ')'")); - return szParseError; + return szGiveUp; } Write(opCond); @@ -1102,34 +1102,34 @@ const TCHAR* CFunctionCompiler::ParseDO(const TCHAR* pch) pch = ParseStatement(pch + 1); - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); - if (!IsKeyword(pchToken, cchToken, _T("while"), 5)) + const TCHAR* pchTok; + int cchTok; + pch = Token(pch, pchTok, cchTok); + if (!IsKeyword(pchTok, cchTok, _T("while"), 5)) { SyntaxError(_T("expected 'while'")); - return szParseError; + return szGiveUp; } pch = SkipWhite(pch); if (*pch != '(') { SyntaxError(_T("expected '('")); - return szParseError; + return szGiveUp; } pch = SkipWhite(ParseExpression(pch + 1)); if (*pch != ')') { SyntaxError(_T("expected ')'")); - return szParseError; + return szGiveUp; } pch = SkipWhite(pch + 1); if (*pch != ';') { SyntaxError(_T("expected ';'")); - return szParseError; + return szGiveUp; } Write(opCond); @@ -1165,14 +1165,14 @@ const TCHAR* CFunctionCompiler::ParseFOR(const TCHAR* pch) if (*pch != '(') { SyntaxError(_T("expected '('")); - return szParseError; + return szGiveUp; } pch++; - const TCHAR* pchToken; - int cchToken; - Token(pch, pchToken, cchToken); - if (IsKeyword(pchToken, cchToken, _T("var"), 3)) + const TCHAR* pchTok; + int cchTok; + Token(pch, pchTok, cchTok); + if (IsKeyword(pchTok, cchTok, _T("var"), 3)) { pch = ParseLocalVar(pch + 3); } @@ -1184,7 +1184,7 @@ const TCHAR* CFunctionCompiler::ParseFOR(const TCHAR* pch) if (*pch != ';') { SyntaxError(_T("expected ';'")); - return szParseError; + return szGiveUp; } pch++; } @@ -1196,7 +1196,7 @@ const TCHAR* CFunctionCompiler::ParseFOR(const TCHAR* pch) if (*pch != ';') { SyntaxError(_T("expected ';'")); - return szParseError; + return szGiveUp; } const TCHAR* pchStep = pch + 1; @@ -1209,7 +1209,7 @@ const TCHAR* CFunctionCompiler::ParseFOR(const TCHAR* pch) if (*pch != ')') { SyntaxError(_T("expected ')'")); - return szParseError; + return szGiveUp; } Write(opCond); @@ -1237,13 +1237,13 @@ const TCHAR* CFunctionCompiler::ParseFOR(const TCHAR* pch) // for (b = m_breaks; b; b = next) { next = b->next; *(int*)(m_ops + b->nop) = m_nop; delete b; } void CFunctionCompiler::FixupBreaks() { - BREAK* pBreak = m_breaks; - while (pBreak != NULL) + BREAK* pEntry = m_breaks; + while (pEntry != NULL) { - BREAK* pNext = pBreak->m_next; - Fixup(pBreak->m_nop, GetAddress()); - delete pBreak; - pBreak = pNext; + BREAK* pNext = pEntry->m_next; + Fixup(pEntry->m_nop, GetAddress()); + delete pEntry; + pEntry = pNext; } } @@ -1259,20 +1259,20 @@ const TCHAR* CFunctionCompiler::ParseBREAK(const TCHAR* pch) if (*pch != ';') { SyntaxError(_T("Missing ';'")); - return szParseError; + return szGiveUp; } if (m_nBreakables == 0) { SyntaxError(_T("unexpected 'break'")); - return szParseError; + return szGiveUp; } Write(opJump); - BREAK* pBreak = new BREAK; - pBreak->m_nop = GetAddress(); - pBreak->m_next = m_breaks; - m_breaks = pBreak; + BREAK* pEntry = new BREAK; + pEntry->m_nop = GetAddress(); + pEntry->m_next = m_breaks; + m_breaks = pEntry; WriteInteger(0); return pch + 1; @@ -1287,12 +1287,12 @@ const TCHAR* CFunctionCompiler::ParseCONTINUE(const TCHAR* pch) if (*pch != ';') { SyntaxError(_T("Missing ';'")); - return szParseError; + return szGiveUp; } if (m_nBreakables == 0) { SyntaxError(_T("unexpected 'continue'")); - return szParseError; + return szGiveUp; } Write(opJump); @@ -1320,7 +1320,7 @@ static const TCHAR* ParseValueThenOp(CFunctionCompiler* pCompiler, const TCHAR* if (*pch != ';') { pCompiler->SyntaxError(_T("Missing ';'")); - return szParseError; + return szGiveUp; } } @@ -1397,50 +1397,50 @@ const TCHAR* CFunctionCompiler::ParseStatement(const TCHAR* pch) if (*pch == '{') return ParseBlock(pch); - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); + const TCHAR* pchTok; + int cchTok; + pch = Token(pch, pchTok, cchTok); Write(opStatement); WriteInteger(g_nLine); - switch (cchToken) + switch (cchTok) { case 2: - if (_tcsncmp(pchToken, _T("if"), 2) == 0) + if (_tcsncmp(pchTok, _T("if"), 2) == 0) return ParseIF(pch); - if (_tcsncmp(pchToken, _T("do"), 2) == 0) + if (_tcsncmp(pchTok, _T("do"), 2) == 0) return ParseDO(pch); break; case 3: - if (_tcsncmp(pchToken, _T("var"), 3) == 0) + if (_tcsncmp(pchTok, _T("var"), 3) == 0) return ParseLocalVar(pch); - if (_tcsncmp(pchToken, _T("for"), 3) == 0) + if (_tcsncmp(pchTok, _T("for"), 3) == 0) return ParseFOR(pch); break; case 5: - if (_tcsncmp(pchToken, _T("while"), 5) == 0) + if (_tcsncmp(pchTok, _T("while"), 5) == 0) return ParseWHILE(pch); - if (_tcsncmp(pchToken, _T("break"), 5) == 0) + if (_tcsncmp(pchTok, _T("break"), 5) == 0) return ParseBREAK(pch); - if (m_bBehavior && _tcsncmp(pchToken, _T("sleep"), 5) == 0) + if (m_bBehavior && _tcsncmp(pchTok, _T("sleep"), 5) == 0) return ParseSLEEP(pch); break; case 6: - if (_tcsncmp(pchToken, _T("return"), 6) == 0) + if (_tcsncmp(pchTok, _T("return"), 6) == 0) return ParseRETURN(pch); break; case 8: - if (_tcsncmp(pchToken, _T("continue"), 8) == 0) + if (_tcsncmp(pchTok, _T("continue"), 8) == 0) return ParseCONTINUE(pch); break; } - pch = ParseExpression(pchToken); + pch = ParseExpression(pchTok); Write(opDrop); if (*pch == '\0') @@ -1448,7 +1448,7 @@ const TCHAR* CFunctionCompiler::ParseStatement(const TCHAR* pch) if (*pch != ';') { SyntaxError(_T("expected ';'")); - return szParseError; + return szGiveUp; } return pch + 1; } @@ -1470,7 +1470,7 @@ const TCHAR* CFunctionCompiler::ParseLocalVar(const TCHAR* pch) if (!CHECK(m_nFrameSize < (int)countof(m_rgstLocal))) { SyntaxError(_T("Too many local variables")); - return szParseError; + return szGiveUp; } const int iSlot = m_nFrameSize++; @@ -1505,9 +1505,9 @@ const TCHAR* CFunctionCompiler::ParseLocalVar(const TCHAR* pch) // CClassCompiler::CClassCompiler // Inlined in 5960 (CClass::ParseClassBody @0x33d60): a zeroed compiler // pointed at the class being built. -CClassCompiler::CClassCompiler(CClass* pClass) +CClassCompiler::CClassCompiler(CClass* pNewClass) { - m_targetClass = pClass; + m_targetClass = pNewClass; } // CClassCompiler::Compile @@ -1561,51 +1561,51 @@ static void ReportUnknown(CCompiler* pCompiler, const TCHAR* szFormat, const TCH // Theseus spells "Expected a '}'" right; 5960 has a typo there. const TCHAR* CClassCompiler::ParseNode(const TCHAR* pch) { - const TCHAR* pchToken; - int cchToken; - pch = Token(pch, pchToken, cchToken); + const TCHAR* pchTok; + int cchTok; + pch = Token(pch, pchTok, cchTok); - if (IsKeyword(pchToken, cchToken, _T("DEF"), 3)) + if (IsKeyword(pchTok, cchTok, _T("DEF"), 3)) { const TCHAR* pchName; int cchName; pch = Token(pch, pchName, cchName, true); - CMember* pMember = new CMember; - pMember->m_memberIndex = m_targetClass->m_nVarCount++; - m_targetClass->AddMember(pchName, cchName, pMember); + CMember* pVar = new CMember; + pVar->m_memberIndex = m_targetClass->m_nVarCount++; + m_targetClass->AddMember(pchName, cchName, pVar); Write(opDefNode); - WriteInteger(pMember->m_memberIndex); + WriteInteger(pVar->m_memberIndex); pch = ParseNode(pch); } - else if (IsKeyword(pchToken, cchToken, _T("USE"), 3)) + else if (IsKeyword(pchTok, cchTok, _T("USE"), 3)) { const TCHAR* pchName; int cchName; pch = Token(pch, pchName, cchName, true); - CMember* pMember = (CMember*)m_targetClass->GetMember(pchName, cchName); - if (pMember == NULL || pMember->m_obj != objMember) + CMember* pVar = (CMember*)m_targetClass->GetMember(pchName, cchName); + if (pVar == NULL || pVar->m_obj != objMember) { TCHAR szName[256]; CopyNameForMessage(szName, countof(szName), pchName, cchName); SyntaxError(_T("Undefined USE '%s'"), szName); - return szParseError; + return szGiveUp; } Write(opUseNode); - WriteInteger(pMember->m_memberIndex); + WriteInteger(pVar->m_memberIndex); } - else if (IsKeyword(pchToken, cchToken, _T("behavior"), 8)) + else if (IsKeyword(pchTok, cchTok, _T("behavior"), 8)) { CFunction* pBehavior; pch = ParseFunction(pch, pBehavior, true); if (pBehavior != NULL) - m_targetClass->SetMember(pchToken, cchToken, pBehavior); + m_targetClass->SetMember(pchTok, cchTok, pBehavior); } - else if (IsKeyword(pchToken, cchToken, _T("function"), 8)) + else if (IsKeyword(pchTok, cchTok, _T("function"), 8)) { const TCHAR* pchName; int cchName; @@ -1613,23 +1613,23 @@ const TCHAR* CClassCompiler::ParseNode(const TCHAR* pch) if (IsKeyword(pchName, cchName, _T("behavior"), 8)) { SyntaxError(_T("invalid function name")); - return szParseError; + return szGiveUp; } - CFunction* pFunction; - pch = ParseFunction(pch, pFunction); - if (pFunction != NULL) - m_targetClass->SetMember(pchName, cchName, pFunction); + CFunction* pFn; + pch = ParseFunction(pch, pFn); + if (pFn != NULL) + m_targetClass->SetMember(pchName, cchName, pFn); } - else if (IsKeyword(pchToken, cchToken, _T("var"), 3)) + else if (IsKeyword(pchTok, cchTok, _T("var"), 3)) { pch = ParseMemberVar(pch); } - else if (IsKeyword(pchToken, cchToken, _T("class"), 5)) + else if (IsKeyword(pchTok, cchTok, _T("class"), 5)) { pch = ParseClass(pch); } - else if (IsKeyword(pchToken, cchToken, _T("import"), 6)) + else if (IsKeyword(pchTok, cchTok, _T("import"), 6)) { const TCHAR* pchName; int cchName; @@ -1639,54 +1639,54 @@ const TCHAR* CClassCompiler::ParseNode(const TCHAR* pch) if (*pch != ',') { SyntaxError(_T("Expected ','")); - return szParseError; + return szGiveUp; } - const TCHAR* pchURL; - int cchURL; - pch = Token(pch + 1, pchURL, cchURL); + const TCHAR* pchPath; + int cchPath; + pch = Token(pch + 1, pchPath, cchPath); // Theseus fix: 5960 copies the url into its buffer without a length check. - TCHAR szURL[MAX_PATH]; - if (!CHECK(cchURL < MAX_PATH)) + TCHAR szPath[MAX_PATH]; + if (!CHECK(cchPath < MAX_PATH)) { SyntaxError(_T("Import path is too long")); - return szParseError; + return szGiveUp; } - CopyChars(szURL, pchURL, cchURL); - szURL[cchURL] = '\0'; + CopyChars(szPath, pchPath, cchPath); + szPath[cchPath] = '\0'; CClass* pImported = new CClass; - if (!pImported->Load(szURL)) + if (!pImported->Load(szPath)) { delete pImported; - return szParseError; + return szGiveUp; } g_classes->Define(pchName, cchName, (CNode*)pImported); } else { - CNodeClass* pNodeClass = LookupClass(pchToken, cchToken); - if (pNodeClass == NULL) - pNodeClass = CNodeClass::FindByName(pchToken, cchToken); - if (pNodeClass == NULL) + CNodeClass* pNodeType = LookupClass(pchTok, cchTok); + if (pNodeType == NULL) + pNodeType = CNodeClass::FindByName(pchTok, cchTok); + if (pNodeType == NULL) { - ReportUnknown(this, _T("Unknown class: %s"), pchToken, cchToken); - return szParseError; + ReportUnknown(this, _T("Unknown class: %s"), pchTok, cchTok); + return szGiveUp; } Write(opNewNode); - WriteString(pchToken, cchToken); + WriteString(pchTok, cchTok); pch = SkipWhite(pch); if (*pch == '{') { - pch = ParseProps(pch + 1, pNodeClass); + pch = ParseProps(pch + 1, pNodeType); if (*pch != '}') { SyntaxError(_T("Expected a '}'")); - return szParseError; + return szGiveUp; } pch++; } @@ -1735,7 +1735,7 @@ const TCHAR* CClassCompiler::ParseChildren(const TCHAR* pch) // arrays) and emitted as opInitProp, the PRD, then the value: strings as // length + characters, the rest as byte count + bytes. // See docs/decomp/Compiler.md for the type to reader table. -const TCHAR* CClassCompiler::ParseProps(const TCHAR* pch, CNodeClass* pNodeClass) +const TCHAR* CClassCompiler::ParseProps(const TCHAR* pch, CNodeClass* pNodeType) { for (pch = SkipWhite(pch); *pch != '\0' && *pch != '}'; pch = SkipWhite(pch)) { @@ -1752,16 +1752,16 @@ const TCHAR* CClassCompiler::ParseProps(const TCHAR* pch, CNodeClass* pNodeClass if (IsKeyword(pchName, cchName, _T("behavior"), 8)) { SyntaxError(_T("invalid function name")); - return szParseError; + return szGiveUp; } } - CFunction* pFunction; - pch = ParseFunction(pch, pFunction, bBehavior); - if (pFunction != NULL) + CFunction* pFn; + pch = ParseFunction(pch, pFn, bBehavior); + if (pFn != NULL) { int iFunction = m_targetClass->m_instanceFunctions.GetLength(); - m_targetClass->m_instanceFunctions.AddNode((CNode*)pFunction); + m_targetClass->m_instanceFunctions.AddNode((CNode*)pFn); Write(opFunction); WriteString(pchName, cchName); WriteInteger(iFunction); @@ -1769,11 +1769,11 @@ const TCHAR* CClassCompiler::ParseProps(const TCHAR* pch, CNodeClass* pNodeClass continue; } - const PRD* pprd = pNodeClass->FindProp(pchName, cchName); + const PRD* pprd = pNodeType->FindProp(pchName, cchName); if (pprd == NULL) { ReportUnknown(this, _T("Unknown property: %s"), pchName, cchName); - return szParseError; + return szGiveUp; } switch (pprd->nType) @@ -1804,61 +1804,61 @@ const TCHAR* CClassCompiler::ParseProps(const TCHAR* pch, CNodeClass* pNodeClass float v[4]; TCHAR* sz; } value; - BYTE* pbArray = NULL; - int cbValue = 0; + BYTE* pbItems = NULL; + int cbData = 0; switch (pprd->nType) { case pt_integer: pch = ParseInteger(pch, value.i); - cbValue = sizeof(int); + cbData = sizeof(int); break; case pt_boolean: pch = ParseBoolean(pch, value.b); - cbValue = sizeof(bool); + cbData = sizeof(bool); break; case pt_number: pch = ParseNumber(pch, value.n); - cbValue = sizeof(float); + cbData = sizeof(float); break; case pt_vec3: case pt_color: pch = ParseVec3(pch, value.v); - cbValue = 3 * sizeof(float); + cbData = 3 * sizeof(float); break; case pt_vec4: case pt_quaternion: pch = ParseVec4(pch, value.v); - cbValue = 4 * sizeof(float); + cbData = 4 * sizeof(float); break; case pt_string: pch = ParseString(pch, value.sz); - cbValue = _tcslen(value.sz); + cbData = _tcslen(value.sz); break; case pt_intarray: - pch = ParseIntArray(pch, pbArray, cbValue); + pch = ParseIntArray(pch, pbItems, cbData); break; case pt_numarray: - pch = ParseVecArray(pch, pbArray, cbValue, 1); + pch = ParseVecArray(pch, pbItems, cbData, 1); break; case pt_vec2array: - pch = ParseVecArray(pch, pbArray, cbValue, 2); + pch = ParseVecArray(pch, pbItems, cbData, 2); break; case pt_vec3array: - pch = ParseVecArray(pch, pbArray, cbValue, 3); + pch = ParseVecArray(pch, pbItems, cbData, 3); break; case pt_vec4array: - pch = ParseVecArray(pch, pbArray, cbValue, 4); + pch = ParseVecArray(pch, pbItems, cbData, 4); break; default: @@ -1870,20 +1870,20 @@ const TCHAR* CClassCompiler::ParseProps(const TCHAR* pch, CNodeClass* pNodeClass if (pprd->nType == pt_string) { - WriteString(value.sz, cbValue); + WriteString(value.sz, cbData); delete[] value.sz; } else { - WriteInteger(cbValue); - if (pbArray != NULL) + WriteInteger(cbData); + if (pbItems != NULL) { - Write(pbArray, cbValue); - delete[] pbArray; + Write(pbItems, cbData); + delete[] pbItems; } else { - Write(&value, cbValue); + Write(&value, cbData); } } } @@ -1904,15 +1904,15 @@ const TCHAR* CClassCompiler::ParseMemberVar(const TCHAR* pch) int cchName; pch = Token(pch, pchName, cchName); - CMember* pMember = new CMember; - pMember->m_memberIndex = m_targetClass->m_nVarCount++; - m_targetClass->AddMember(pchName, cchName, pMember); + CMember* pVar = new CMember; + pVar->m_memberIndex = m_targetClass->m_nVarCount++; + m_targetClass->AddMember(pchName, cchName, pVar); pch = SkipWhite(pch); if (*pch == '=') { SyntaxError(_T("Sorry, class member initializers have not been implemented yet!")); - return szParseError; + return szGiveUp; } if (*pch != ',') @@ -1936,12 +1936,12 @@ const TCHAR* CClassCompiler::ParseMemberVar(const TCHAR* pch) // Behaviors take no list. The body must start with '{' or there's no // function. The block is compiled, opNull opRet is appended as the implicit // "return null;" and the ops become the CFunction. -const TCHAR* CClassCompiler::ParseFunction(const TCHAR* pch, CFunction*& pFunction, bool bBehavior) +const TCHAR* CClassCompiler::ParseFunction(const TCHAR* pch, CFunction*& pFn, bool bBehavior) { CFunctionCompiler body; body.m_bBehavior = bBehavior; - pFunction = NULL; + pFn = NULL; pch = SkipWhite(pch); if (!bBehavior) @@ -1959,7 +1959,7 @@ const TCHAR* CClassCompiler::ParseFunction(const TCHAR* pch, CFunction*& pFuncti if (!CHECK(body.m_nFrameSize < (int)countof(body.m_rgstLocal))) { SyntaxError(_T("Too many parameters")); - return szParseError; + return szGiveUp; } ST& local = body.m_rgstLocal[body.m_nFrameSize++]; @@ -1974,7 +1974,7 @@ const TCHAR* CClassCompiler::ParseFunction(const TCHAR* pch, CFunction*& pFuncti if (*pch != ')') { SyntaxError(_T("expected ')'\n")); - return szParseError; + return szGiveUp; } pch++; } @@ -1987,7 +1987,7 @@ const TCHAR* CClassCompiler::ParseFunction(const TCHAR* pch, CFunction*& pFuncti body.Write(opNull); body.Write(opRet); - pFunction = body.CreateFunction(); + pFn = body.CreateFunction(); return pch; } @@ -2006,18 +2006,18 @@ const TCHAR* CClassCompiler::ParseClass(const TCHAR* pch) if (*pch != '{') { SyntaxError(_T("Expected '{'\n")); - return szParseError; + return szGiveUp; } - CClass* pClass = new CClass; - pch = pClass->ParseClassBody(pch + 1); + CClass* pNewClass = new CClass; + pch = pNewClass->ParseClassBody(pch + 1); if (*pch != '}') { SyntaxError(_T("Expected '}'")); - return szParseError; + return szGiveUp; } - g_classes->Define(pchName, cchName, (CNode*)pClass); + g_classes->Define(pchName, cchName, (CNode*)pNewClass); return pch + 1; } From 06a77a0c0dbb949e0652d58d36869c4ec18c67d7 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:18:08 -0500 Subject: [PATCH 048/167] Rebuilt the node base classes from the 5960 binary Wrote CObject, CNodeClass, CNode, CNodeArray, CTimeDepNode, CClass and CInstance fresh from the retail XBE, with the address and behavior above each one. Dropped the debug-only asserts and dumps the retail image doesn't have, and put CHECKs on the three spots that could walk off a stack or the FND table. Every XAP builds the same scene graph as before (54 of 54) and the property, call and array tests match line for line. --- theseus/render/node_system.cpp | 1650 +++++++++++++++++--------------- 1 file changed, 852 insertions(+), 798 deletions(-) diff --git a/theseus/render/node_system.cpp b/theseus/render/node_system.cpp index a1e1024..9e9be8b 100755 --- a/theseus/render/node_system.cpp +++ b/theseus/render/node_system.cpp @@ -4,8 +4,9 @@ // The foundation of the XAP scene graph: every dashboard element is a // node registered via the IMPLEMENT_NODE macros, and properties (PRD) // and functions (FND) are exposed to scripts through descriptor tables -// walked at runtime. Decompiled from the 5960 retail XBE; see -// docs/decomp/Node.md. +// walked at runtime. Every body here is written from the 5960 retail XBE; +// addresses sit above each function and docs/decomp/Node.md has the long +// ones (CObject::Call, CInstance::Construct) in full. #include "std.h" #include "theseus.h" #include "xap_compile.h" @@ -19,48 +20,15 @@ extern bool g_bParseError; extern CRunner* g_pRunner; extern CObject* g_pThis; -int SizeOfType(PROP_TYPE type) -{ - switch (type) - { - default: - return 0; - - case pt_boolean: - return sizeof (bool); - - case pt_integer: - return sizeof (int); - - case pt_number: - return sizeof (float); - - case pt_string: - return sizeof (TCHAR*); - - case pt_children: - return sizeof (CNodeArray); - - case pt_vec3: - case pt_color: - return sizeof (D3DXVECTOR3); - - case pt_vec4: - case pt_quaternion: - return sizeof (D3DXVECTOR4); - - case pt_node: - return sizeof (CNode*); - } -} extern CObject* Dereference(CObject* pObject); extern CObject* CreateNewObject(const TCHAR* pchClassName, int cchClassName, CObject** rgparam = NULL, int nParam = 0); extern CObject* FindMember(CNodeClass* pClass, const TCHAR* pchName, int cchName); // ========================================================================= -// Section 1, CObject: base class for all script-visible objects. +// CObject: base of everything a script can hold. // ========================================================================= +// The "Object" class record and its empty PRD map (map at 0x00174418). PRD CObject::m_rgprd[] = { { NULL, pt_null, _T("Object") }, @@ -69,47 +37,105 @@ PRD CObject::m_rgprd[] = CNodeClass CObject::classCObject(_T("Object"), sizeof(class CObject), CObject::CreateNode, NULL, NULL); +// CObject::CObject / CObject::CreateNode +// Reconstructed from 5960 retail XBE @ 0x0002f500 (the factory inlines the +// constructor): +// p = new(0x14); p->vtbl = CObject; p->m_obj = 0; p->m_nRefCount = 1; +// p->m_parent = 0; p->m_members = 0; +// A fresh object is untyped, owned once, parentless and has no members. CObject::CObject() { - m_obj = objUndefined; m_nRefCount = 1; - m_members = NULL; + m_obj = objUndefined; m_parent = NULL; + m_members = NULL; +} + +CObject* CObject::CreateNode() +{ + return new CObject; } +// CObject::~CObject +// Reconstructed from 5960 retail XBE @ 0x0002f450: +// CLerper_RemoveObject(this); +// if (m_members) { for each define d: d->m_node->Release(); free(d); free(m_members); } +// Drops any running lerps on this object, then frees the member list +// (CNameSpace's destructor does the per-entry release and free). CObject::~CObject() { CLerper::RemoveObject(this); delete m_members; } -void CObject::AddRef() { m_nRefCount++; } +// CObject::AddRef / CObject::Release +// Reconstructed from 5960 retail XBE @ 0x0002f4a0 / 0x0002f4b0 (shared by +// every class that doesn't override them): +// m_nRefCount += 1; +// m_nRefCount -= 1; if (m_nRefCount == 0) this->vtbl[0](1); // deleting dtor +// Plain intrusive refcounting; there is only one way to write it. +void CObject::AddRef() +{ + ++m_nRefCount; +} void CObject::Release() { - m_nRefCount--; - if (m_nRefCount == 0) + if (--m_nRefCount == 0) delete this; } -CObject* CObject::CreateNode() { return new CObject; } - -CNodeClass* CObject::GetNodeClass() const { return &classCObject; } +// CObject::GetNodeClass Reconstructed from 5960 retail XBE @ 0x0002f4c0: +// return &classCObject; +CNodeClass* CObject::GetNodeClass() const +{ + return &classCObject; +} +// CObject::IsKindOf +// Reconstructed from 5960 retail XBE @ 0x0002f4d0: +// for (c = this->GetNodeClass(); c != 0; c = c->m_baseClass) // +0x20 +// if (c == pClass) return 1; +// return 0; bool CObject::IsKindOf(CNodeClass* pClass) const { - for (CNodeClass* pCheck = GetNodeClass(); pCheck != NULL; pCheck = pCheck->m_baseClass) + CNodeClass* pWalk = GetNodeClass(); + while (pWalk != NULL) { - if (pClass == pCheck) + if (pWalk == pClass) return true; + pWalk = pWalk->m_baseClass; } return false; } -CStrObject* CObject::ToStr() { return new CStrObject(_T("[object]")); } -CNumObject* CObject::ToNum() { return new CNumObject(0.0f); } -CObject* CObject::Deref() { return this; } -FND* CObject::GetFunctionMap() const { return NULL; } +// CObject::ToStr / CObject::ToNum +// Reconstructed from 5960 retail XBE @ 0x0002f530 / 0x0002f590: +// return new CStrObject(L"[object]"); // string at 0x00021b74 +// return new CNumObject(); // type 2, value 0 +// A bare object prints as "[object]" and counts as zero. +CStrObject* CObject::ToStr() +{ + return new CStrObject(_T("[object]")); +} + +CNumObject* CObject::ToNum() +{ + return new CNumObject(0.0f); +} + +// CObject::Deref / GetFunctionMap / OnSetProperty +// Default virtuals in the CObject vtable at 0x00021b00: an object derefs to +// itself, has no FND table, and accepts every property write. +CObject* CObject::Deref() +{ + return this; +} + +FND* CObject::GetFunctionMap() const +{ + return NULL; +} bool CObject::OnSetProperty(const PRD* pprd, const void* pvValue) { @@ -117,37 +143,59 @@ bool CObject::OnSetProperty(const PRD* pprd, const void* pvValue) } // ========================================================================= -// Section 2, property system: PRD lookup and SetProperty dispatch. +// Properties: PRD lookup and the typed write. // ========================================================================= -// Walk the PRD chain (linked via first entry's pbOffset) to find a named property +// CObject::FindProp +// Reconstructed from 5960 retail XBE @ 0x0002ec40: +// for (map = this->GetPropMap(); map; map = map[0].pbOffset) +// for (i = 1; map[i].szName; i++) +// if (!_wcsnicmp(name, map[i].szName, cch) && map[i].szName[cch] == 0) +// return &map[i]; +// return 0; +// Entry 0 of every map links to the base class's map, so this walks the +// class chain; names match case-insensitively and must match in full. const PRD* CObject::FindProp(const TCHAR* szName, int cchName) { - const PRD* rgprd = GetPropMap(); - while (rgprd != NULL) + for (const PRD* pMap = GetPropMap(); pMap != NULL; pMap = (const PRD*)pMap[0].pbOffset) { - const PRD* pNext = (const PRD*)rgprd[0].pbOffset; - for (int i = 1; rgprd[i].szName != NULL; i++) + for (const PRD* pEntry = pMap + 1; pEntry->szName != NULL; pEntry++) { - if (_tcsnicmp(szName, rgprd[i].szName, cchName) == 0 && rgprd[i].szName[cchName] == '\0') - return &rgprd[i]; + if (_tcsnicmp(szName, pEntry->szName, cchName) == 0 && pEntry->szName[cchName] == 0) + return pEntry; } - rgprd = pNext; } return NULL; } +// Resize a typed array property and fill it from a packed buffer. +template +static void FillArrayProp(BYTE* pField, const void* pvValue, int cbValue, int cbElement) +{ + TArray* pArray = (TArray*)pField; + pArray->SetSize(cbValue / cbElement); + CopyMemory(pArray->m_value, pvValue, cbValue); +} +// CObject::SetProperty +// Reconstructed from 5960 retail XBE @ 0x0002ecd0 (byte count in EAX). +// The field lives at this + prd->pbOffset. A string write whose new text +// equals the old (both non-null, wcscmp) returns before anything else. +// Otherwise OnSetProperty (vtable +8) gets a veto, then by type: +// pt_string free the old copy, store a fresh NUL-terminated copy +// pt_*array SetSize(cb / element size), copy cb bytes into m_value +// everything raw copy of cb bytes; for pt_node the old node is +// else Released first and the new one AddRef'd after +// The binary folds the int and num arrays onto one SetSize body. void CObject::SetProperty(const PRD* pprd, const void* pvValue, int cbValueIn) { - BYTE* pbDest = (BYTE*)this + PTR2INT(pprd->pbOffset); + BYTE* pField = (BYTE*)this + PTR2INT(pprd->pbOffset); - // String shortcut: skip if value unchanged if (pprd->nType == pt_string) { - TCHAR* szOld = *(TCHAR**)pbDest; - TCHAR* szNew = *(TCHAR**)pvValue; - if (szOld != NULL && szNew != NULL && _tcscmp(szOld, szNew) == 0) + const TCHAR* szHave = *(TCHAR**)pField; + const TCHAR* szWant = *(TCHAR* const*)pvValue; + if (szHave != NULL && szWant != NULL && _tcscmp(szHave, szWant) == 0) return; } @@ -156,493 +204,365 @@ void CObject::SetProperty(const PRD* pprd, const void* pvValue, int cbValueIn) switch (pprd->nType) { - case pt_intarray: + case pt_string: { - CIntArray* pArr = (CIntArray*)pbDest; - pArr->SetSize(cbValueIn / sizeof(int)); - CopyMemory(pArr->m_value, pvValue, cbValueIn); + TCHAR*& szField = *(TCHAR**)pField; + const TCHAR* szSource = *(TCHAR* const*)pvValue; + delete[] szField; + szField = new TCHAR[cbValueIn + 1]; + CopyChars(szField, szSource, cbValueIn); + szField[cbValueIn] = 0; } - break; + return; + + case pt_intarray: + FillArrayProp(pField, pvValue, cbValueIn, sizeof(int)); + return; case pt_numarray: - { - CNumArray* pArr = (CNumArray*)pbDest; - pArr->SetSize(cbValueIn / sizeof(float)); - CopyMemory(pArr->m_value, pvValue, cbValueIn); - } - break; + FillArrayProp(pField, pvValue, cbValueIn, sizeof(float)); + return; case pt_vec2array: - { - CVec2Array* pArr = (CVec2Array*)pbDest; - pArr->SetSize(cbValueIn / (sizeof(float) * 2)); - CopyMemory(pArr->m_value, pvValue, cbValueIn); - } - break; + FillArrayProp(pField, pvValue, cbValueIn, sizeof(float) * 2); + return; case pt_vec3array: - { - CVec3Array* pArr = (CVec3Array*)pbDest; - pArr->SetSize(cbValueIn / (sizeof(float) * 3)); - CopyMemory(pArr->m_value, pvValue, cbValueIn); - } - break; + FillArrayProp(pField, pvValue, cbValueIn, sizeof(float) * 3); + return; case pt_vec4array: - { - CVec4Array* pArr = (CVec4Array*)pbDest; - pArr->SetSize(cbValueIn / (sizeof(float) * 4)); - CopyMemory(pArr->m_value, pvValue, cbValueIn); - } - break; + FillArrayProp(pField, pvValue, cbValueIn, sizeof(float) * 4); + return; - case pt_string: - { - TCHAR* szOld = *(TCHAR**)pbDest; - delete[] szOld; - - TCHAR* sz = *(TCHAR**)pvValue; - ASSERT((int)_tcslen(sz) == cbValueIn); - TCHAR* szNew = new TCHAR[cbValueIn + 1]; - CopyChars(szNew, sz, cbValueIn); - szNew[cbValueIn] = 0; - *(TCHAR**)pbDest = szNew; - } + default: break; + } - default: - if (pprd->nType == pt_node) - { - CNode* pNode = *(CNode**)pbDest; - if (pNode != NULL) pNode->Release(); - } + bool fNode = (pprd->nType == pt_node); + if (fNode && *(CNode**)pField != NULL) + (*(CNode**)pField)->Release(); - ASSERT(SizeOfType(pprd->nType) == cbValueIn); - CopyMemory(pbDest, pvValue, cbValueIn); + CopyMemory(pField, pvValue, cbValueIn); - if (pprd->nType == pt_node) - { - CNode* pNode = *(CNode**)pvValue; - if (pNode != NULL) pNode->AddRef(); - } - break; - } + if (fNode && *(CNode* const*)pvValue != NULL) + (*(CNode* const*)pvValue)->AddRef(); } // ========================================================================= -// Section 3, FND function dispatch via CObject::Call. -// The big dispatch switch that bridges script calls to C++ member functions +// Functions: FND lookup and the signature dispatcher. // ========================================================================= +// CObject::FindFunction +// Reconstructed from 5960 retail XBE @ 0x0002f5d0: +// map = this->GetFunctionMap(); if (!map) return -1; +// for (f = map; f->pfn; f++) +// if (wcslen(f->szName) == cch && !wcsncmp(f->szName, name, cch)) +// return f - map; // 0x18-byte entries +// return -1; +// Exact, case-sensitive match; the result is the FND index. int CObject::FindFunction(const TCHAR* pchFunction, int cchFunction) { - FND* rgfnd = GetFunctionMap(); - if (rgfnd == NULL) + FND* pMap = GetFunctionMap(); + if (pMap == NULL) return -1; - for (FND* pfnd = rgfnd; pfnd->pfn.pfn != NULL; pfnd++) + for (int i = 0; pMap[i].pfn.pfn != NULL; i++) { - if ((int)_tcslen(pfnd->szName) == cchFunction && _tcsncmp(pfnd->szName, pchFunction, cchFunction) == 0) - return (int)(pfnd - rgfnd); + const TCHAR* szName = pMap[i].szName; + if ((int)_tcslen(szName) == cchFunction && _tcsncmp(szName, pchFunction, cchFunction) == 0) + return i; } - return -1; } -typedef CObject* (__cdecl CObject::*NODE_PFN_DEFAULT)(CObject**, int); - -CObject* CObject::Call(int nFunction, CObject** rgparam, int nParam) +// Shape of each FND signature: return kind, then one letter per argument +// (i int, n number, s string, o object). Signature ids are the binary's +// switch cases. sig_default passes the raw array; sig_isi (6) has no case +// in the binary and falls through to the unknown-signature exit. +struct SIGSHAPE { - ASSERT(nFunction >= 0); - - FND* pfnd = &GetFunctionMap()[nFunction]; - union FSIG fsig = pfnd->pfn; - CObject* pRetObject = NULL; + char ret; + const char* args; +}; - for (int i = 0; i < nParam; i++) - rgparam[i] = Dereference(rgparam[i]); +static const SIGSHAPE c_rgSigShape[] = +{ + { 0, NULL }, // 0 sig_default (handled separately) + { 'v', "" }, // 1 sig_vv + { 'i', "" }, // 2 sig_iv + { 'i', "i" }, // 3 sig_ii + { 'o', "i" }, // 4 sig_si + { 'o', "s" }, // 5 sig_ss + { 0, NULL }, // 6 sig_isi (no case) + { 'o', "ii" }, // 7 sig_sii + { 'o', "" }, // 8 sig_sv + { 'n', "" }, // 9 sig_nv + { 'n', "n" }, // 10 sig_nn + { 'n', "nn" }, // 11 sig_nnn + { 'v', "s" }, // 12 sig_vs + { 'v', "ss" }, // 13 sig_vss + { 'v', "i" }, // 14 sig_vi + { 'o', "" }, // 15 sig_ov + { 'o', "s" }, // 16 sig_os + { 'o', "o" }, // 17 sig_oo + { 'v', "ii" }, // 18 sig_vii + { 'v', "n" }, // 19 sig_vn + { 'v', "nn" }, // 20 sig_vnn + { 'v', "o" }, // 21 sig_vo + { 'v', "nnn" }, // 22 sig_vnnn + { 'v', "nnnn" }, // 23 sig_vnnnn + { 'i', "ii" }, // 24 sig_iii + { 'i', "s" }, // 25 sig_is + { 'v', "iis" }, // 26 sig_viis + { 'n', "i" }, // 27 sig_ni + { 'v', "is" }, // 28 sig_vis +}; - switch (pfnd->sig) - { - default: - ASSERT(FALSE); +// CObject::Call +// Reconstructed from 5960 retail XBE @ 0x0002f640 (2818 bytes, walked +// through in docs/decomp/Node.md). First every argument is dereferenced in +// place (Deref at vtable +0x24 until it stops changing). Then the FND's +// signature picks a case: sig_default hands the raw (array, count) to the +// method; every other case checks the count exactly, converts each +// argument with ToNum (truncated to int for i) or ToStr (NULL text becomes +// ""), calls, wraps an int or float result in a new CNumObject, and +// Releases the converted temporaries. A wrong count reports "invalid number +// of parameters" through the running script; an unknown signature returns +// NULL. The binary also has a case 29 (int from int and string) that no +// Theseus FND uses, so it isn't carried here. Theseus adds the CHECK on a +// negative index, which would read before the FND table. +CObject* CObject::Call(int nFunction, CObject** rgparam, int nParam) +{ + if (!CHECK(nFunction >= 0)) return NULL; - case sig_default: - pRetObject = (this->*fsig.pfn_default)(rgparam, nParam); - break; - - case sig_vv: - if (nParam != 0) goto LBadParamCount; - (this->*fsig.pfn_vv)(); - break; - - case sig_vi: - { - if (nParam != 1) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - (this->*fsig.pfn_vi)((int)p1->m_value); - p1->Release(); - } - break; - - case sig_vii: - { - if (nParam != 2) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - CNumObject* p2 = rgparam[1]->ToNum(); - (this->*fsig.pfn_vii)((int)p1->m_value, (int)p2->m_value); - p1->Release(); p2->Release(); - } - break; - - case sig_vis: - { - if (nParam != 2) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - CStrObject* p2 = rgparam[1]->ToStr(); - (this->*fsig.pfn_vis)((int)p1->m_value, p2->GetSz()); - p1->Release(); p2->Release(); - } - break; + const FND* pfnd = &GetFunctionMap()[nFunction]; + const FSIG f = pfnd->pfn; - case sig_viis: - { - if (nParam != 3) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - CNumObject* p2 = rgparam[1]->ToNum(); - CStrObject* p3 = rgparam[2]->ToStr(); - (this->*fsig.pfn_viis)((int)p1->m_value, (int)p2->m_value, p3->GetSz()); - p1->Release(); p2->Release(); p3->Release(); - } - break; + for (int iArg = 0; iArg < nParam; iArg++) + rgparam[iArg] = Dereference(rgparam[iArg]); - case sig_iv: - if (nParam != 0) goto LBadParamCount; - pRetObject = new CNumObject((float)(this->*fsig.pfn_iv)()); - break; - - case sig_sv: - if (nParam != 0) goto LBadParamCount; - pRetObject = (this->*fsig.pfn_sv)(); - break; + if (pfnd->sig == sig_default) + return (this->*f.pfn_default)(rgparam, nParam); - case sig_vs: - { - if (nParam != 1) goto LBadParamCount; - CStrObject* p1 = rgparam[0]->ToStr(); - (this->*fsig.pfn_vs)(p1->GetSz()); - p1->Release(); - } - break; - - case sig_vss: - { - if (nParam != 2) goto LBadParamCount; - CStrObject* p1 = rgparam[0]->ToStr(); - CStrObject* p2 = rgparam[1]->ToStr(); - (this->*fsig.pfn_vss)(p1->GetSz(), p2->GetSz()); - p1->Release(); p2->Release(); - } - break; - - case sig_ii: - { - if (nParam != 1) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - pRetObject = new CNumObject((float)(this->*fsig.pfn_ii)((int)p1->m_value)); - p1->Release(); - } - break; - - case sig_ni: - { - if (nParam != 1) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - pRetObject = new CNumObject((this->*fsig.pfn_ni)((int)p1->m_value)); - p1->Release(); - } - break; - - case sig_iii: - { - if (nParam != 2) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - CNumObject* p2 = rgparam[1]->ToNum(); - pRetObject = new CNumObject((float)(this->*fsig.pfn_iii)((int)p1->m_value, (int)p2->m_value)); - p1->Release(); p2->Release(); - } - break; - - case sig_si: - { - if (nParam != 1) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - pRetObject = (this->*fsig.pfn_si)((int)p1->m_value); - p1->Release(); - } - break; - - case sig_sii: - { - if (nParam != 2) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - CNumObject* p2 = rgparam[1]->ToNum(); - pRetObject = (this->*fsig.pfn_sii)((int)p1->m_value, (int)p2->m_value); - p1->Release(); p2->Release(); - } - break; - - case sig_ss: - { - if (nParam != 1) goto LBadParamCount; - CStrObject* p1 = rgparam[0]->ToStr(); - pRetObject = (this->*fsig.pfn_ss)(p1->GetSz()); - p1->Release(); - } - break; - - case sig_nv: - if (nParam != 0) goto LBadParamCount; - pRetObject = new CNumObject((this->*fsig.pfn_nv)()); - break; - - case sig_nn: - { - if (nParam != 1) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - pRetObject = new CNumObject((this->*fsig.pfn_nn)(p1->m_value)); - p1->Release(); - } - break; + if (pfnd->sig >= sizeof(c_rgSigShape) / sizeof(c_rgSigShape[0]) || c_rgSigShape[pfnd->sig].args == NULL) + return NULL; - case sig_nnn: - { - if (nParam != 2) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - CNumObject* p2 = rgparam[1]->ToNum(); - pRetObject = new CNumObject((this->*fsig.pfn_nnn)(p1->m_value, p2->m_value)); - p1->Release(); p2->Release(); - } - break; + const SIGSHAPE& shape = c_rgSigShape[pfnd->sig]; + if (nParam != (int)strlen(shape.args)) + { + g_pRunner->Error(_T("invalid number of parameters")); + return NULL; + } - case sig_vn: - { - if (nParam != 1) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - (this->*fsig.pfn_vn)(p1->m_value); - p1->Release(); - } - break; + // Convert the arguments; the converted objects are held until after the call. + int nArg[4]; + float flArg[4]; + const TCHAR* szArg[4]; + CObject* pArgObj[4]; + CObject* rgTemp[4] = { NULL, NULL, NULL, NULL }; - case sig_vnn: + for (int iArg = 0; iArg < nParam; iArg++) + { + switch (shape.args[iArg]) { - if (nParam != 2) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - CNumObject* p2 = rgparam[1]->ToNum(); - (this->*fsig.pfn_vnn)(p1->m_value, p2->m_value); - p1->Release(); p2->Release(); - } - break; + case 'i': + case 'n': + { + CNumObject* pNum = rgparam[iArg]->ToNum(); + rgTemp[iArg] = pNum; + flArg[iArg] = pNum->m_value; + nArg[iArg] = (int)pNum->m_value; + } + break; - case sig_vnnn: - { - if (nParam != 3) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - CNumObject* p2 = rgparam[1]->ToNum(); - CNumObject* p3 = rgparam[2]->ToNum(); - (this->*fsig.pfn_vnnn)(p1->m_value, p2->m_value, p3->m_value); - p1->Release(); p2->Release(); p3->Release(); - } - break; + case 's': + { + CStrObject* pStr = rgparam[iArg]->ToStr(); + rgTemp[iArg] = pStr; + szArg[iArg] = pStr->GetSz(); + } + break; - case sig_vnnnn: - { - if (nParam != 4) goto LBadParamCount; - CNumObject* p1 = rgparam[0]->ToNum(); - CNumObject* p2 = rgparam[1]->ToNum(); - CNumObject* p3 = rgparam[2]->ToNum(); - CNumObject* p4 = rgparam[3]->ToNum(); - (this->*fsig.pfn_vnnnn)(p1->m_value, p2->m_value, p3->m_value, p4->m_value); - p1->Release(); p2->Release(); p3->Release(); p4->Release(); + case 'o': + pArgObj[iArg] = rgparam[iArg]; + break; } - break; - - case sig_ov: - if (nParam != 0) goto LBadParamCount; - pRetObject = (this->*fsig.pfn_ov)(); - break; + } - case sig_os: - { - if (nParam != 1) goto LBadParamCount; - CStrObject* p1 = rgparam[0]->ToStr(); - pRetObject = (this->*fsig.pfn_os)(p1->GetSz()); - p1->Release(); - } - break; + int nRet = 0; + float flRet = 0.0f; + CObject* pRet = NULL; - case sig_oo: - if (nParam != 1) goto LBadParamCount; - pRetObject = (this->*fsig.pfn_oo)(rgparam[0]); - break; + switch (pfnd->sig) + { + case sig_vv: (this->*f.pfn_vv)(); break; + case sig_vi: (this->*f.pfn_vi)(nArg[0]); break; + case sig_vii: (this->*f.pfn_vii)(nArg[0], nArg[1]); break; + case sig_vis: (this->*f.pfn_vis)(nArg[0], szArg[1]); break; + case sig_viis: (this->*f.pfn_viis)(nArg[0], nArg[1], szArg[2]); break; + case sig_vs: (this->*f.pfn_vs)(szArg[0]); break; + case sig_vss: (this->*f.pfn_vss)(szArg[0], szArg[1]); break; + case sig_vn: (this->*f.pfn_vn)(flArg[0]); break; + case sig_vnn: (this->*f.pfn_vnn)(flArg[0], flArg[1]); break; + case sig_vnnn: (this->*f.pfn_vnnn)(flArg[0], flArg[1], flArg[2]); break; + case sig_vnnnn: (this->*f.pfn_vnnnn)(flArg[0], flArg[1], flArg[2], flArg[3]); break; + case sig_vo: (this->*f.pfn_vo)(pArgObj[0]); break; + + case sig_iv: nRet = (this->*f.pfn_iv)(); break; + case sig_ii: nRet = (this->*f.pfn_ii)(nArg[0]); break; + case sig_iii: nRet = (this->*f.pfn_iii)(nArg[0], nArg[1]); break; + case sig_is: nRet = (this->*f.pfn_is)(szArg[0]); break; + + case sig_nv: flRet = (this->*f.pfn_nv)(); break; + case sig_ni: flRet = (this->*f.pfn_ni)(nArg[0]); break; + case sig_nn: flRet = (this->*f.pfn_nn)(flArg[0]); break; + case sig_nnn: flRet = (this->*f.pfn_nnn)(flArg[0], flArg[1]); break; + + case sig_sv: pRet = (this->*f.pfn_sv)(); break; + case sig_si: pRet = (this->*f.pfn_si)(nArg[0]); break; + case sig_sii: pRet = (this->*f.pfn_sii)(nArg[0], nArg[1]); break; + case sig_ss: pRet = (this->*f.pfn_ss)(szArg[0]); break; + case sig_ov: pRet = (this->*f.pfn_ov)(); break; + case sig_os: pRet = (this->*f.pfn_os)(szArg[0]); break; + case sig_oo: pRet = (this->*f.pfn_oo)(pArgObj[0]); break; + } - case sig_vo: - if (nParam != 1) goto LBadParamCount; - (this->*fsig.pfn_vo)(rgparam[0]); - break; + if (shape.ret == 'i') + pRet = new CNumObject((float)nRet); + else if (shape.ret == 'n') + pRet = new CNumObject(flRet); - case sig_is: - { - if (nParam != 1) goto LBadParamCount; - CStrObject* p1 = rgparam[0]->ToStr(); - pRetObject = new CNumObject((float)(this->*fsig.pfn_is)(p1->GetSz())); - p1->Release(); - } - break; + for (int iArg = 0; iArg < nParam; iArg++) + { + if (rgTemp[iArg] != NULL) + rgTemp[iArg]->Release(); } - return pRetObject; - -LBadParamCount: - g_pRunner->Error(_T("invalid number of parameters")); - return NULL; + return pRet; } // ========================================================================= -// Section 4, member access: Dot operator and member namespace. +// Members: the Dot operator and the per-object name list. // ========================================================================= +// CObject::Dot +// Reconstructed from 5960 retail XBE @ 0x000301c0. Only a name (objVariable) +// can be looked up; it resolves in this order: +// FND index -> new CFunctionObject{index, owner = this}, this->AddRef() +// GetMember -> the member itself, AddRef'd (vtable +0x34) +// FindProp -> new CProperty(this, prd) +// otherwise -> NULL +// Theseus adds: a member that is a script function comes back wrapped in a +// CMemberFunctionObject bound to this, so the call runs with the right self. CObject* CObject::Dot(CObject* pObj) { - if (pObj->m_obj == objVariable) + if (pObj->m_obj != objVariable) + return NULL; + + const CVarObject* pName = (const CVarObject*)pObj; + const TCHAR* pchName = pName->m_text; + int cchName = pName->m_length; + + int iFunction = FindFunction(pchName, cchName); + if (iFunction >= 0) { - CVarObject* pVar = (CVarObject*)pObj; + CFunctionObject* pRef = new CFunctionObject; + pRef->m_functionIndex = iFunction; + pRef->m_owner = this; + AddRef(); + return pRef; + } - // Check FND table first - int nFunction = FindFunction(pVar->m_text, pVar->m_length); - if (nFunction >= 0) + CObject* pMember = GetMember(pchName, cchName); + if (pMember != NULL) + { + if (pMember->m_obj == objFunction) { - CFunctionObject* pFun = new CFunctionObject; - pFun->m_functionIndex = nFunction; - pFun->m_owner = this; AddRef(); - return pFun; + return new CMemberFunctionObject(this, (CFunction*)pMember); } - // Then instance members - CObject* pObject = GetMember(pVar->m_text, pVar->m_length); - if (pObject != NULL) - { - if (pObject->m_obj == objFunction) - { - // Wrap in CMemberFunctionObject so opCall sets m_self correctly. - AddRef(); - return new CMemberFunctionObject(this, (CFunction*)pObject); - } - pObject->AddRef(); - return pObject; - } - - // Then PRD properties - const PRD* pprd = FindProp(pVar->m_text, pVar->m_length); - if (pprd != NULL) - return new CProperty(this, pprd); + pMember->AddRef(); + return pMember; } + const PRD* pprd = FindProp(pchName, cchName); + if (pprd != NULL) + return new CProperty(this, pprd); + return NULL; } +// CObject::AddMember / SetMember / GetMember +// Reconstructed from 5960 retail XBE @ 0x00030310 / 0x00030380 / 0x000303c0: +// if (!m_members) m_members = new CNameSpace; // 4 bytes, head = 0 +// AddMember: push a new define at the head and point it at the object +// SetMember: find-or-add the define, point it at the object +// GetMember: m_members ? lookup by name : 0 +// AddMember always adds, so a duplicate name shadows the older entry. void CObject::AddMember(const TCHAR* pchName, int cchName, CObject* pObject) { if (m_members == NULL) m_members = new CNameSpace; - CDefine* pDefine = m_members->Add(pchName, cchName); - pDefine->m_node = (CNode*)pObject; + m_members->Add(pchName, cchName)->m_node = (CNode*)pObject; } void CObject::SetMember(const TCHAR* pchName, int cchName, CObject* pObject) { if (m_members == NULL) m_members = new CNameSpace; - CDefine* pDefine = m_members->Get(pchName, cchName); - pDefine->m_node = (CNode*)pObject; + m_members->Get(pchName, cchName)->m_node = (CNode*)pObject; } CObject* CObject::GetMember(const TCHAR* pchName, int cchName) { - if (m_members == NULL) - return NULL; - return m_members->Lookup(pchName, cchName); + return m_members != NULL ? m_members->Lookup(pchName, cchName) : NULL; } +// CObject::FindMemberFunction +// Reconstructed from 5960 retail XBE @ 0x000303e0: +// f = this->GetMember(name); +// if (f && f->m_obj == 13) return f; +// for (c = this->GetNodeClass(); c; c = c->m_baseClass) +// if ((f = c->GetMember(name)) != 0) return f->m_obj == 13 ? f : 0; +// return 0; +// The object's own members win, then the first class up the chain that +// knows the name decides. FindMember is that class walk. CFunction* CObject::FindMemberFunction(const TCHAR* pchName, int cchName) { - // Instance functions - CFunction* pFunction = (CFunction*)GetMember(pchName, cchName); - if (pFunction != NULL && pFunction->m_obj == objFunction) - return pFunction; + CObject* pFound = GetMember(pchName, cchName); + if (pFound != NULL && pFound->m_obj == objFunction) + return (CFunction*)pFound; - // Class hierarchy functions - pFunction = (CFunction*)FindMember(GetNodeClass(), pchName, cchName); - if (pFunction != NULL && pFunction->m_obj == objFunction) - return pFunction; + pFound = FindMember(GetNodeClass(), pchName, cchName); + if (pFound != NULL && pFound->m_obj == objFunction) + return (CFunction*)pFound; return NULL; } - +// CObject::Dump +// Debug-only slot in the vtable; the retail XBE has no body for it, so this +// prints nothing. void CObject::Dump() const { - static const TCHAR* rgszType [] = - { - _T("Unknown"), - _T("Null"), - _T("Number"), - _T("String"), - _T("Variable"), - _T("Node"), - _T("Class"), - _T("NodeArray"), - _T("FunctionRef"), - _T("Member"), - _T("MemberVar"), - _T("Instance"), - _T("Use"), - _T("Function"), - _T("MemberFunction"), - _T("Array"), - }; - - TRACE(_T("{\nObject: 0x%08x\n"), this); - if (this != NULL) - { - TRACE(_T("Type: %s\n"), rgszType[m_obj]); - TRACE(_T("Parent: 0x%08x\n"), m_parent); - TRACE(_T("RefCount: %d\n"), m_nRefCount); - - CNodeClass* pNodeClass = GetNodeClass(); - if (pNodeClass != NULL) - TRACE(_T("Class: %s\n"), pNodeClass->m_className); - else - TRACE(_T("Class: null\n")); - - if (m_members != NULL) - { - TRACE(_T("members:\n")); - m_members->Dump(); - } - } - - TRACE(_T("}\n")); } // ========================================================================= -// Section 5, CNodeClass: class registry and factory. +// CNodeClass: the registry of node types. // ========================================================================= +// Head of the class list; 0x0017e730 in the binary. CNodeClass* CNodeClass::c_pFirstClass = NULL; +// CNodeClass::CNodeClass +// Reconstructed from 5960 retail XBE @ 0x0002e900: +// CObject fields reset; m_className = name; m_nObjectSize = size; +// m_pfnCreateNode = create; m_baseClass = base; m_rgprd = prd; +// m_nextClass = c_pFirstClass; c_pFirstClass = this; +// Every class record pushes itself on the front of one global list. CNodeClass::CNodeClass(const TCHAR* szClassName, int nObjectSize, CObject* (*pfnCreateNode)(), CNodeClass* pBaseClass, const PRD* rgprd) { @@ -651,67 +571,74 @@ CNodeClass::CNodeClass(const TCHAR* szClassName, int nObjectSize, m_pfnCreateNode = pfnCreateNode; m_baseClass = pBaseClass; m_rgprd = rgprd; + m_nextClass = c_pFirstClass; c_pFirstClass = this; } +// CNodeClass::~CNodeClass +// Reconstructed from 5960 retail XBE @ 0x0002e970: +// for (pp = &c_pFirstClass; *pp && *pp != this; pp = &(*pp)->m_nextClass) ; +// if (*pp == this) *pp = m_nextClass; +// then the inlined CObject destructor CNodeClass::~CNodeClass() { - CNodeClass** pp; - for (pp = &c_pFirstClass; *pp != NULL && *pp != this; pp = &(*pp)->m_nextClass) - ; - if (*pp == this) - *pp = m_nextClass; -#ifdef _DEBUG - else - ASSERT(FALSE); -#endif + CNodeClass** ppLink = &c_pFirstClass; + while (*ppLink != NULL && *ppLink != this) + ppLink = &(*ppLink)->m_nextClass; + + if (*ppLink == this) + *ppLink = m_nextClass; } +// CNodeClass::CreateNode Inlined into CreateNewObject @ 0x00037580: +// return m_pfnCreateNode(); CObject* CNodeClass::CreateNode() { - return (*m_pfnCreateNode)(); + return m_pfnCreateNode(); } +// CNodeClass::FindByName +// Reconstructed from 5960 retail XBE @ 0x0002ea20: +// for (c = c_pFirstClass; c; c = c->m_nextClass) +// if (!wcsncmp(c->m_className, name, cch) && c->m_className[cch] == 0) +// return c; +// return 0; +// Case-sensitive, full-length match against the registered names. CNodeClass* CNodeClass::FindByName(const TCHAR* pchNodeClass, int cchNodeClass) { for (CNodeClass* pClass = c_pFirstClass; pClass != NULL; pClass = pClass->m_nextClass) { - if (_tcsncmp(pClass->m_className, pchNodeClass, cchNodeClass) == 0 && - pClass->m_className[cchNodeClass] == '\0') + const TCHAR* szName = pClass->m_className; + if (_tcsncmp(szName, pchNodeClass, cchNodeClass) == 0 && szName[cchNodeClass] == 0) return pClass; } return NULL; } +// CNodeClass::FindProp +// The binary has no separate copy: the class compiler (CClassCompiler:: +// ParseProps @ 0x00036440) calls CObject::FindProp on the class record, +// whose GetPropMap is m_rgprd. Same walk here. const PRD* CNodeClass::FindProp(const TCHAR* szName, int cchName) { - const PRD* rgprd = GetPropMap(); - while (rgprd != NULL) - { - const PRD* pNext = (const PRD*)rgprd[0].pbOffset; - for (int i = 1; rgprd[i].szName != NULL; i++) - { - if (_tcsnicmp(szName, rgprd[i].szName, cchName) == 0 && rgprd[i].szName[cchName] == '\0') - return &rgprd[i]; - } - rgprd = pNext; - } - return NULL; + return CObject::FindProp(szName, cchName); } +// NewNode +// Inlined into CreateNewObject @ 0x00037580: FindByName, then the class +// factory, NULL when the name isn't registered. CObject* NewNode(const TCHAR* pchNodeClass, int cchNodeClass) { - CNodeClass* pNodeClass = CNodeClass::FindByName(pchNodeClass, cchNodeClass); - if (pNodeClass == NULL) - return NULL; - return pNodeClass->CreateNode(); + CNodeClass* pClass = CNodeClass::FindByName(pchNodeClass, cchNodeClass); + return pClass != NULL ? pClass->CreateNode() : NULL; } // ========================================================================= -// Section 6, CNode: base scene-graph node. +// CNode: the base scene-graph node. // ========================================================================= +// "Node" with one script property, visible (PRD map at 0x001743a8). PRD CNode::m_rgprd[] = { { NULL, pt_null, _T("Node") }, @@ -721,6 +648,10 @@ PRD CNode::m_rgprd[] = CNodeClass CNode::classCNode(_T("Node"), sizeof(class CNode), CNode::CreateNode, NULL, CNode::m_rgprd); +// CNode::CNode / CNode::CreateNode +// Reconstructed from 5960 retail XBE @ 0x0002ebf0 (factory, ctor inlined): +// p = new(0x1c); CObject fields; p->m_obj = 5; p->m_behavior = 0; p->m_visible = 1; +// A node starts visible with no behavior script. CNode::CNode() { m_obj = objNode; @@ -728,163 +659,231 @@ CNode::CNode() m_visible = true; } +CObject* CNode::CreateNode() +{ + return new CNode; +} + +// CNode::~CNode +// Reconstructed from 5960 retail XBE @ 0x0002ea80: +// if (m_behavior) { m_behavior->ResetFunc(); free(m_behavior); } // delete CNode::~CNode() { delete m_behavior; } -CObject* CNode::CreateNode() { return new CNode; } - -CNodeClass* CNode::GetNodeClass() const { return &classCNode; } +// CNode::GetNodeClass Reconstructed from 5960 retail XBE @ 0x0002ec30. +CNodeClass* CNode::GetNodeClass() const +{ + return &classCNode; +} +// CNode::OnLoad +// Reconstructed from 5960 retail XBE @ 0x0002eaf0 (FindMemberFunction +// inlined): look up a script function named "behavior" on the node or up +// its class chain; if there is one, give the node a runner bound to itself +// and point it at that function. void CNode::OnLoad() { - CFunction* pFunction = FindMemberFunction(_T("behavior"), 8); - if (pFunction != NULL) - { - m_behavior = new CRunner(this); - m_behavior->SetFunc(pFunction); - } + CFunction* pBehavior = FindMemberFunction(_T("behavior"), 8); + if (pBehavior == NULL) + return; + + m_behavior = new CRunner(this); + m_behavior->SetFunc(pBehavior); } +// CNode::Advance +// Reconstructed from 5960 retail XBE @ 0x0002ee60: +// r = m_behavior; if (!r) return; +// if (r->m_wakeup != 0) { if (now < r->m_wakeup) return; r->m_wakeup = 0; } +// g_pRunner = r; g_pThis = this; +// for (;;) { +// do { if (!r->Step()) r->ResetFunc(); } while (r->m_wakeup == 0); +// if (now < r->m_wakeup) break; +// r->m_wakeup = 0; +// } +// The behavior script runs until it sleeps into the future, restarting +// from the top whenever it finishes. IsSleeping is that wakeup test. void CNode::Advance(float nSeconds) { - if (m_behavior != NULL && !m_behavior->IsSleeping()) + if (m_behavior == NULL || m_behavior->IsSleeping()) + return; + + g_pRunner = m_behavior; + g_pThis = this; + + do { - g_pRunner = m_behavior; - g_pThis = this; - do - { - if (!m_behavior->Step()) - { - m_behavior->ResetFunc(); - } - } - while (!m_behavior->IsSleeping()); + bool fRunning = m_behavior->Step(); + if (!fRunning) + m_behavior->ResetFunc(); } + while (!m_behavior->IsSleeping()); } -void CNode::Render() { m_visible = false; } -void CNode::GetBBox(BBox* pBBox) { ZeroMemory(pBBox, sizeof(BBox)); } -float CNode::GetRadius() { return 0.0f; } -void CNode::SetLight(int& nLight, D3DCOLORVALUE& ambient) {} -LPDIRECT3DTEXTURE8 CNode::GetTextureSurface() { return NULL; } -void CNode::RenderDynamicTexture(CSurfx* pSurfx) {} -const DWORD* CNode::GetPalette() { return NULL; } - -void CNode::Dump() const +// CNode default virtuals (CNode vtable at 0x000219f8). +// Render @ 0x0002ee50 m_visible = 0 +// GetBBox @ 0x0002ef00 zero all six floats +// GetRadius @ 0x0002ef30 return 0.0 +// RenderDynamicTexture @ 0x0002ef40 nothing +// SetLight, GetTextureSurface and GetPalette are the matching do-nothing +// slots. A plain node that gets asked to render just marks itself unseen. +void CNode::Render() { - CObject::Dump(); + m_visible = false; +} - for (const PRD* rgprd = GetPropMap(); rgprd != NULL; rgprd = (const PRD*)rgprd[0].pbOffset) - { - int i; - for (i = 1; rgprd[i].szName != NULL; i += 1) - { - TRACE(_T("prop: '%s::%s' = "), rgprd[0].szName, rgprd[i].szName); - const BYTE* pbThisValue = (const BYTE*)this + (int)rgprd[i].pbOffset; - switch (rgprd[i].nType) - { - case pt_integer: - TRACE(_T("%d"), *((const int*)pbThisValue)); - break; +void CNode::GetBBox(BBox* pBBox) +{ + ZeroMemory(pBBox, sizeof(BBox)); +} - case pt_boolean: - TRACE(_T("%s"), *((const bool*)pbThisValue) ? _T("true") : _T("false")); - break; +float CNode::GetRadius() +{ + return 0.0f; +} - case pt_number: - TRACE(_T("%f"), *((const float*)pbThisValue)); - break; +void CNode::SetLight(int& nLight, D3DCOLORVALUE& ambient) +{ +} - case pt_string: - TRACE(_T("%s"), *((const TCHAR**)pbThisValue)); - break; +LPDIRECT3DTEXTURE8 CNode::GetTextureSurface() +{ + return NULL; +} - // FUTURE: Dump other types too! - } +void CNode::RenderDynamicTexture(CSurfx* pSurfx) +{ +} - TRACE(_T("\n")); - } - } +const DWORD* CNode::GetPalette() +{ + return NULL; } +// CNode::Dump: same story as CObject::Dump. +void CNode::Dump() const +{ +} // ========================================================================= -// Section 7, CNodeArray: dynamic array of node pointers. +// CNodeArray: growable list of node pointers. // ========================================================================= -CNodeArray::CNodeArray() { m_length = 0; m_nAlloc = 0; m_rgpNode = NULL; } -CNodeArray::~CNodeArray() { ReleaseAll(); } +// CNodeArray::CNodeArray / ~CNodeArray +// Inlined everywhere in the binary: start empty; destruction is ReleaseAll. +CNodeArray::CNodeArray() +{ + m_length = 0; + m_nAlloc = 0; + m_rgpNode = NULL; +} + +CNodeArray::~CNodeArray() +{ + ReleaseAll(); +} +// CNodeArray::AddNode +// Inlined at its callers (e.g. CInstance::Construct @ 0x00033f00): +// if (m_nAlloc < m_length + 1) Allocate(m_nAlloc + 16); +// m_rgpNode[m_length++] = node; +// Grows sixteen slots at a time. Takes over the caller's reference. void CNodeArray::AddNode(CNode* pNode) { - if (m_nAlloc < m_length + 1) + if (m_length + 1 > m_nAlloc) Allocate(m_nAlloc + 16); - m_rgpNode[m_length++] = pNode; + + m_rgpNode[m_length] = pNode; + m_length++; } +// CNodeArray::RemoveNode +// Reconstructed from 5960 retail XBE @ 0x0002ef60: every slot holding the +// node is closed up by sliding the tail down one, count shrinks, no +// Release. The binary's slide moves (length - i - 1) bytes rather than that +// many pointers; Theseus moves the whole tail, which is what was meant. void CNodeArray::RemoveNode(CNode* pNode) { - int i; - for (i = 0; i < m_length; i++) + int i = 0; + while (i < m_length) { - if (m_rgpNode[i] == pNode) + if (m_rgpNode[i] != pNode) { - CopyMemory(&m_rgpNode[i], &m_rgpNode[i + 1], (m_length - i - 1) * sizeof(CNode*)); - m_length--; - i--; + i++; + continue; } + + for (int j = i; j < m_length - 1; j++) + m_rgpNode[j] = m_rgpNode[j + 1]; + m_length--; } } +// CNodeArray::ReleaseAll / RemoveAll +// Reconstructed from 5960 retail XBE @ 0x0002efa0 (RemoveAll inlined): +// for (i = 0; i < m_length; i++) if (m_rgpNode[i]) m_rgpNode[i]->Release(); +// m_length = 0; m_nAlloc = 0; free(m_rgpNode); m_rgpNode = 0; void CNodeArray::ReleaseAll() { for (int i = 0; i < m_length; i++) { - if (m_rgpNode[i] != NULL) - m_rgpNode[i]->Release(); + CNode* pNode = m_rgpNode[i]; + if (pNode != NULL) + pNode->Release(); } RemoveAll(); } void CNodeArray::RemoveAll() { - m_length = 0; - m_nAlloc = 0; delete[] m_rgpNode; m_rgpNode = NULL; + m_length = 0; + m_nAlloc = 0; } +// CNodeArray::Allocate +// Reconstructed from 5960 retail XBE @ 0x0002eff0: +// if (m_nAlloc < n) { p = new CNode*[n]; copy the old m_nAlloc slots; +// free the old block; m_rgpNode = p; m_nAlloc = n; } +// Never shrinks; new slots past the old capacity are left uninitialized. void CNodeArray::Allocate(int nLength) { - if (m_nAlloc < nLength) + if (nLength <= m_nAlloc) + return; + + CNode** rgpGrown = new CNode*[nLength]; + if (m_rgpNode != NULL) { - CNode** rgp = new CNode*[nLength]; - if (m_rgpNode != NULL) - { - CopyMemory(rgp, m_rgpNode, m_nAlloc * sizeof(CNode*)); - delete[] m_rgpNode; - } - m_rgpNode = rgp; - m_nAlloc = nLength; + for (int i = 0; i < m_nAlloc; i++) + rgpGrown[i] = m_rgpNode[i]; + delete[] m_rgpNode; } + + m_rgpNode = rgpGrown; + m_nAlloc = nLength; } +// CNodeArray::SetLength +// Inlined into CInstance::CInstance @ 0x00033df0: +// Allocate(n); if (m_length < n) zero slots m_length..n-1; m_length = n; void CNodeArray::SetLength(int nLength) { Allocate(nLength); - if (nLength > m_length) - ZeroMemory(m_rgpNode + m_length, (nLength - m_length) * sizeof(CNode*)); + for (int i = m_length; i < nLength; i++) + m_rgpNode[i] = NULL; m_length = nLength; } // ========================================================================= -// Section 8, CTimeDepNode: timer-driven base node. +// CTimeDepNode: base for nodes that switch on and off on the clock. // ========================================================================= IMPLEMENT_NODE("TimeDepNode", CTimeDepNode, CNode) +// PRD map at 0x001743d0. START_NODE_PROPS(CTimeDepNode, CNode) NODE_PROP(pt_boolean, CTimeDepNode, loop) NODE_PROP(pt_number, CTimeDepNode, startTime) @@ -892,51 +891,86 @@ START_NODE_PROPS(CTimeDepNode, CNode) NODE_PROP(pt_boolean, CTimeDepNode, isActive) END_NODE_PROPS() +// CTimeDepNode::CTimeDepNode +// Reconstructed from 5960 retail XBE @ 0x0002f050 (factory, ctor inlined): +// CNode fields; m_loop = 0; m_startTime = 0; m_stopTime = 0; +// m_isActive = 0; m_lastStartTime = 0; CTimeDepNode::CTimeDepNode() - : m_loop(false), m_startTime(0), m_stopTime(0), m_isActive(false) { + m_loop = false; + m_startTime = 0.0f; + m_stopTime = 0.0f; + m_isActive = false; m_lastStartTime = 0.0f; } +// CTimeDepNode::Advance +// Reconstructed from 5960 retail XBE @ 0x0002f0d0: +// CNode::Advance(); +// if (!m_isActive) { +// if (start <= now && start != lastStart && (stop == 0 || now < stop)) +// { lastStart = start; m_isActive = 1; OnIsActiveChanged(); } +// } else if (start < stop && stop <= now) +// { m_isActive = 0; OnIsActiveChanged(); } // vtable +0x5c +// A node starts once per new startTime and stops at stopTime when one is +// set after the start. The binary narrows now to float for the start test; +// Theseus compares at double precision throughout. void CTimeDepNode::Advance(float nSeconds) { CNode::Advance(nSeconds); - if (m_isActive) - { - if (m_stopTime > m_startTime && TheseusGetNow() >= m_stopTime) - { - m_isActive = false; - OnIsActiveChanged(); - } - } - else + XTIME now = TheseusGetNow(); + + if (!m_isActive) { - if (TheseusGetNow() >= m_startTime && m_startTime != m_lastStartTime && - (m_stopTime == 0.0f || TheseusGetNow() < m_stopTime)) + bool fNewStart = now >= m_startTime && m_startTime != m_lastStartTime; + bool fBeforeStop = m_stopTime == 0.0f || now < m_stopTime; + if (fNewStart && fBeforeStop) { m_lastStartTime = m_startTime; m_isActive = true; OnIsActiveChanged(); } + return; + } + + if (m_startTime < m_stopTime && now >= m_stopTime) + { + m_isActive = false; + OnIsActiveChanged(); } } -void CTimeDepNode::OnIsActiveChanged() {} -void CTimeDepNode::OnCycleEnded() {} +// Empty hooks for subclasses (vtable slot +0x5c and the one after it). +void CTimeDepNode::OnIsActiveChanged() +{ +} + +void CTimeDepNode::OnCycleEnded() +{ +} // ========================================================================= -// Section 9, CClass: scripted class definition and loading. +// CClass: a node class defined by a script file. // ========================================================================= +// CClass::CClass +// Reconstructed from 5960 retail XBE @ 0x00033970: registers itself as a +// "[class]" node class with no factory, base or PRD map, then clears its +// own fields and marks itself objClass (6). CClass::CClass() : CNodeClass(_T("[class]"), 0, NULL, NULL, NULL) { m_obj = objClass; m_url = NULL; - m_nVarCount = 0; m_constructor = NULL; + m_nVarCount = 0; } +// CClass::~CClass +// Reconstructed from 5960 retail XBE @ 0x00033a20: +// free(m_url); if (m_constructor) m_constructor->Release(); +// m_instanceFunctions.ReleaseAll(); m_file.~CActiveFile(); ~CNodeClass(); +// The member destructors cover the last three. CClass::~CClass() { delete[] m_url; @@ -944,321 +978,341 @@ CClass::~CClass() m_constructor->Release(); } +// CClass::CreateNode Reconstructed from 5960 retail XBE @ 0x00033ad0: +// return new CInstance(this); // 0x54 bytes CNode* CClass::CreateNode() { return new CInstance(this); } -bool CClass::Load(const TCHAR* szURL) +// Shared tail of Load and LoadAbsURL (both inline it in the binary): keep a +// copy of the URL, fetch the file, hand its text to the parser and free it. +static bool FetchAndParseClass(CClass* pClass, const TCHAR* szURL) { - CDirPush dirPush(szURL); - - int cch = _tcslen(szURL) + 1; - delete m_url; - m_url = new TCHAR[cch]; - CopyChars(m_url, szURL, cch); + int cchURL = _tcslen(szURL) + 1; + delete[] pClass->m_url; + pClass->m_url = new TCHAR[cchURL]; + CopyChars(pClass->m_url, szURL, cchURL); - if (!m_file.Fetch(szURL)) + if (!pClass->m_file.Fetch(szURL)) return false; #ifdef _UNICODE - m_file.MakeUnicode(); + pClass->m_file.MakeUnicode(); #endif - BYTE* pbContent = m_file.DetachContent(); - bool fParse = ParseFile(szURL, (const TCHAR*)pbContent); - TheseusFreeMemory(pbContent); - return fParse; + BYTE* pbText = pClass->m_file.DetachContent(); + bool fParsed = pClass->ParseFile(szURL, (const TCHAR*)pbText); + TheseusFreeMemory(pbText); + return fParsed; } -bool CClass::LoadAbsURL(const TCHAR* szURL) +// CClass::Load +// Reconstructed from 5960 retail XBE @ 0x00033b30: saves the current +// directory, moves it to the class file's folder (so relative references +// inside resolve there), fetches and parses, then restores the directory +// on every path out. CDirPush is that save and restore. +bool CClass::Load(const TCHAR* szURL) { - ASSERT(szURL[0] && szURL[1] == ':'); - - int cch = _tcslen(szURL) + 1; - delete m_url; - m_url = new TCHAR[cch]; - CopyChars(m_url, szURL, cch); - - if (!m_file.Fetch(szURL)) - return false; - -#ifdef _UNICODE - m_file.MakeUnicode(); -#endif + CDirPush dirPush(szURL); + return FetchAndParseClass(this, szURL); +} - BYTE* pbContent = m_file.DetachContent(); - bool fParse = ParseFile(szURL, (const TCHAR*)pbContent); - TheseusFreeMemory(pbContent); - return fParse; +// CClass::LoadAbsURL +// Reconstructed from 5960 retail XBE @ 0x00033c70: the same fetch and parse +// without touching the current directory. +bool CClass::LoadAbsURL(const TCHAR* szURL) +{ + return FetchAndParseClass(this, szURL); } +// CClass::ParseFile +// Reconstructed from 5960 retail XBE @ 0x00033d00: +// StartParse(file, name); // file name, line 1, no error +// end = ParseClassBody(text); +// if (*end) SyntaxError(L"Stuff past expected end of file!"); +// EndParse(); return !g_bParseError; bool CClass::ParseFile(const TCHAR* szFileName, const TCHAR* szFile) { StartParse(szFile, szFileName); - const TCHAR* pch = ParseClassBody(szFile); - if (*pch != '\0') + + const TCHAR* pchEnd = ParseClassBody(szFile); + if (*pchEnd != 0) SyntaxError(_T("Stuff past expected end of file!")); + EndParse(); return !g_bParseError; } +// CClass::ParseClassBody +// Reconstructed from 5960 retail XBE @ 0x00033d60: run a class compiler for +// this class over the text, keep the function it builds as the +// constructor, and return where parsing stopped. const TCHAR* CClass::ParseClassBody(const TCHAR* pch) { - CClassCompiler constructor(this); - pch = constructor.Compile(pch); - m_constructor = constructor.CreateFunction(); - return pch; + CClassCompiler compiler(this); + const TCHAR* pchEnd = compiler.Compile(pch); + m_constructor = compiler.CreateFunction(); + return pchEnd; } // ========================================================================= -// Section 10, CInstance: runtime instance of a scripted class. +// CInstance: a live copy of a script class. // ========================================================================= +// CInstance::CInstance +// Reconstructed from 5960 retail XBE @ 0x00033df0: a CGroup marked +// objInstance (11) that holds a reference to its class, sizes its variable +// slots to the class's count (all NULL), builds its children by running the +// class constructor, then gets OnLoad like any other node. CInstance::CInstance(CClass* pClass) { m_obj = objInstance; m_class = pClass; - m_class->AddRef(); + pClass->AddRef(); + m_vars.SetLength(pClass->GetVariableCount()); + Construct(); OnLoad(); } +// CInstance::~CInstance +// Reconstructed from 5960 retail XBE @ 0x000342c0: release the class, then +// m_vars.ReleaseAll() and the CGroup destructor (member and base dtors). CInstance::~CInstance() { m_class->Release(); } +// CInstance::GetNodeClass: an instance reports its script class. CNodeClass* CInstance::GetNodeClass() const { return m_class; } +// CInstance::Render: draws as the group it is. void CInstance::Render() { CGroup::Render(); } +// CInstance::Dot +// Reconstructed from 5960 retail XBE @ 0x00034330: a name found in the +// class's member list becomes a CMemberVarObject (objMember, 9) or a +// CMemberFunctionObject (objFunction, 13) bound to this instance; anything +// else goes to the ordinary node lookup. CObject* CInstance::Dot(CObject* pObj) { - if (pObj->m_obj == objVariable) + if (pObj->m_obj == objVariable && m_class->m_members != NULL) { - CVarObject* pVar = (CVarObject*)pObj; - - if (m_class->m_members != NULL) + const CVarObject* pName = (const CVarObject*)pObj; + CObject* pEntry = m_class->m_members->Lookup(pName->m_text, pName->m_length); + if (pEntry != NULL) { - CObject* pLookup = m_class->m_members->Lookup(pVar->m_text, pVar->m_length); - if (pLookup != NULL) - { - if (pLookup->m_obj == objMember) - return new CMemberVarObject(this, ((CMember*)pLookup)->m_memberIndex); - if (pLookup->m_obj == objFunction) - return new CMemberFunctionObject(this, (CFunction*)pLookup); - } + if (pEntry->m_obj == objMember) + return new CMemberVarObject(this, ((CMember*)pEntry)->m_memberIndex); + if (pEntry->m_obj == objFunction) + return new CMemberFunctionObject(this, (CFunction*)pEntry); } } + return CNode::Dot(pObj); } -// CInstance::Construct: build the scene graph from compiled bytecode. -static inline int FetchIntFromBytecode(BYTE*& pop) +// Reader over constructor bytecode (ints are stored unaligned). +struct CTORREADER { - int n; - CopyMemory(&n, pop, sizeof(int)); - pop += sizeof(int); - return n; -} + BYTE* pb; -BYTE* CInstance::Construct() -{ - BYTE* pop = m_class->m_constructor->m_rgop; + BYTE Op() { return *pb++; } + BYTE Peek() const { return *pb; } + + int Int() + { + int n; + CopyMemory(&n, pb, sizeof(int)); + pb += sizeof(int); + return n; + } + + const TCHAR* Name(int cch) + { + const TCHAR* pch = (const TCHAR*)pb; + pb += cch * sizeof(TCHAR); + return pch; + } - if (pop == NULL) - return pop; + void Prd(PRD* pprd) + { + CopyMemory(pprd, pb, sizeof(PRD)); + pb += sizeof(PRD); + } +}; - int nNodeStack = 0; - CNode* nodeStack[100]; +// CInstance::Construct +// Reconstructed from 5960 retail XBE @ 0x00033f00 (915 bytes, walked +// through in docs/decomp/Node.md). Runs the class constructor's bytecode +// (ops 100..108) to build the instance's scene graph, keeping a stack of +// open nodes and a stack of open child arrays (100 deep each), and returns +// where it stopped (the first byte that isn't one of those ops). +// NewNode create by name, parent = this, append to the open array +// NewNodeProp same but stored into a node property of the open node; +// a UseNode right after stores an existing variable instead +// DefNode the next node created is also kept in variable slot n +// UseNode append variable n (AddRef'd) to the open array +// EndNode OnLoad the open node and pop back to its parent +// InitProp write a literal (strings copied out and NUL-terminated) +// InitArray open the node-array property at byte offset n +// EndArray close it +// Function bind instance function n under a name on the open node +// Theseus adds CHECKs so a stray EndNode or EndArray stops construction +// instead of popping past the bottom, and a literal with no node to land +// on is skipped instead of crashing. +BYTE* CInstance::Construct() +{ + CTORREADER code; + code.pb = m_class->m_constructor->m_rgop; + if (code.pb == NULL) + return NULL; - int nNodeArrayStack = 0; - CNodeArray* nodeArrayStack[100]; + CNode* rgpOpenNode[100]; + int cOpenNode = 0; + CNode* pCurrent = NULL; - CNode* pNode = NULL; - CNodeArray* pNodeArray = &m_children; + CNodeArray* rgpOpenArray[100]; + int cOpenArray = 0; + CNodeArray* pTargetArray = &m_children; - int nDefNextNodeVar = -1; + int iPendingVar = -1; for (;;) { - BYTE op = *pop++; + BYTE op = code.Op(); - switch (op) + if (op == opNewNode) { - default: - // Done constructing - return pop - 1; + int cch = code.Int(); + const TCHAR* pchClass = code.Name(cch); - case opNewNode: - { - int cch = FetchIntFromBytecode(pop); - const TCHAR* pch = (const TCHAR*)pop; - pop += cch * sizeof(TCHAR); - - nodeStack[nNodeStack] = pNode; - nNodeStack += 1; - - pNode = (CNode*)CreateNewObject(pch, cch); - pNode->m_parent = this; - pNodeArray->AddNode(pNode); - - if (nDefNextNodeVar != -1) - { - pNode->AddRef(); - m_vars.SetNode(nDefNextNodeVar, pNode); - nDefNextNodeVar = -1; - } - } - break; + rgpOpenNode[cOpenNode++] = pCurrent; + pCurrent = (CNode*)CreateNewObject(pchClass, cch); + pCurrent->m_parent = this; + pTargetArray->AddNode(pCurrent); - case opNewNodeProp: + if (iPendingVar != -1) { - PRD prd; - CopyMemory(&prd, pop, sizeof(PRD)); - pop += sizeof(PRD); - - if (*pop == opUseNode) - { - pop += 1; - - int nVar = FetchIntFromBytecode(pop); - - CNode* pNodeT = m_vars.GetNode(nVar); - pNode->SetProperty(&prd, &pNodeT, sizeof(CNode*)); - break; - } - - if (*pop == opDefNode) - { - pop += 1; - - int nVar = FetchIntFromBytecode(pop); - - nDefNextNodeVar = nVar; - } - - ASSERT(*pop == opNewNode); - pop += 1; - - int cch = FetchIntFromBytecode(pop); - const TCHAR* pch = (const TCHAR*)pop; - pop += cch * sizeof(TCHAR); - - nodeStack[nNodeStack] = pNode; - nNodeStack += 1; - - pNode = (CNode*)CreateNewObject(pch, cch); - pNode->m_parent = this; - nodeStack[nNodeStack - 1]->SetProperty(&prd, &pNode, sizeof(CNode*)); - - // Adjust for reference added by SetProperty... - ASSERT(pNode->m_nRefCount == 2); - pNode->Release(); - - if (nDefNextNodeVar != -1) - { - pNode->AddRef(); - m_vars.SetNode(nDefNextNodeVar, pNode); - nDefNextNodeVar = -1; - } + pCurrent->AddRef(); + m_vars.SetNode(iPendingVar, pCurrent); + iPendingVar = -1; } - break; + } + else if (op == opNewNodeProp) + { + PRD prd; + code.Prd(&prd); - case opDefNode: + if (code.Peek() == opUseNode) { - int nVar = FetchIntFromBytecode(pop); - - nDefNextNodeVar = nVar; + code.Op(); + CNode* pExisting = m_vars.GetNode(code.Int()); + pCurrent->SetProperty(&prd, &pExisting, sizeof(CNode*)); + continue; } - break; - case opUseNode: + if (code.Peek() == opDefNode) { - int nVar = FetchIntFromBytecode(pop); - - CNode* pNode = m_vars.GetNode(nVar); - pNode->AddRef(); - pNodeArray->AddNode(pNode); + code.Op(); + iPendingVar = code.Int(); } - break; - case opEndNode: - ASSERT(nNodeStack > 0); + code.Op(); // the NewNode + int cch = code.Int(); + const TCHAR* pchClass = code.Name(cch); - pNode->OnLoad(); + CNode* pOwner = pCurrent; + rgpOpenNode[cOpenNode++] = pOwner; + pCurrent = (CNode*)CreateNewObject(pchClass, cch); + pCurrent->m_parent = this; - nNodeStack -= 1; - pNode = nodeStack[nNodeStack]; - break; + // SetProperty takes its own reference, so drop the one from creation. + pOwner->SetProperty(&prd, &pCurrent, sizeof(CNode*)); + pCurrent->Release(); - case opInitProp: + if (iPendingVar != -1) { - PRD prd; - CopyMemory(&prd, pop, sizeof(PRD)); - pop += sizeof(PRD); - - int cbProp = FetchIntFromBytecode(pop); - const void* pValue = pop; - pop += cbProp; - - ASSERT(pNode != NULL); - - TCHAR szBuf[1024]; - const TCHAR* sz; - if (prd.nType == pt_string) - { - CopyChars(szBuf, pValue, cbProp); - szBuf[cbProp] = 0; - sz = szBuf; - pValue = &sz; -#ifdef _UNICODE - pop += cbProp; -#endif - } - - pNode->SetProperty(&prd, pValue, cbProp); + pCurrent->AddRef(); + m_vars.SetNode(iPendingVar, pCurrent); + iPendingVar = -1; } - break; - - case opInitArray: + } + else if (op == opDefNode) + { + iPendingVar = code.Int(); + } + else if (op == opUseNode) + { + CNode* pExisting = m_vars.GetNode(code.Int()); + pExisting->AddRef(); + pTargetArray->AddNode(pExisting); + } + else if (op == opEndNode) + { + if (!CHECK(cOpenNode > 0)) + return code.pb - 1; + pCurrent->OnLoad(); + pCurrent = rgpOpenNode[--cOpenNode]; + } + else if (op == opInitProp) + { + PRD prd; + code.Prd(&prd); + int cbValue = code.Int(); + const void* pvValue = code.pb; + code.pb += cbValue; + + if (!CHECK(pCurrent != NULL)) + continue; + + // Strings sit bare in the bytecode; SetProperty wants a pointer to a C string. + TCHAR szText[1024]; + const TCHAR* pszText; + if (prd.nType == pt_string) { - int nProp = FetchIntFromBytecode(pop); - - nodeArrayStack[nNodeArrayStack] = pNodeArray; - nNodeArrayStack += 1; - - pNodeArray = (CNodeArray*)((BYTE*)pNode + nProp); + CopyChars(szText, pvValue, cbValue); + szText[cbValue] = 0; + pszText = szText; + pvValue = &pszText; +#ifdef _UNICODE + code.pb += cbValue; +#endif } - break; - - case opEndArray: - ASSERT(nNodeArrayStack > 0); - - nNodeArrayStack -= 1; - pNodeArray = nodeArrayStack[nNodeArrayStack]; - break; - case opFunction: - { - int cch = FetchIntFromBytecode(pop); - const TCHAR* pch = (const TCHAR*)pop; - pop += cch * sizeof(TCHAR); - int nFunction = FetchIntFromBytecode(pop); - - CObject* pFun = m_class->m_instanceFunctions.GetNode(nFunction); - pFun->AddRef(); - pNode->SetMember(pch, cch, pFun); - } - break; + pCurrent->SetProperty(&prd, pvValue, cbValue); + } + else if (op == opInitArray) + { + int cbOffset = code.Int(); + rgpOpenArray[cOpenArray++] = pTargetArray; + pTargetArray = (CNodeArray*)((BYTE*)pCurrent + cbOffset); + } + else if (op == opEndArray) + { + if (!CHECK(cOpenArray > 0)) + return code.pb - 1; + pTargetArray = rgpOpenArray[--cOpenArray]; + } + else if (op == opFunction) + { + int cch = code.Int(); + const TCHAR* pchName = code.Name(cch); + CObject* pFunction = m_class->m_instanceFunctions.GetNode(code.Int()); + pFunction->AddRef(); + pCurrent->SetMember(pchName, cch, pFunction); + } + else + { + return code.pb - 1; } } } From 27f914c8043924f30ca22157a5af0d7ddd1488a1 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:18:58 -0500 Subject: [PATCH 049/167] Wrote up the 5960 node base classes in the Node decomp doc Added the object layouts, vtables, a map of every Node.cpp, Object.cpp and Class.cpp function, and walkthroughs of SetProperty, Call, Dot, the two Advance loops and the class constructor bytecode. Also noted the quirks, like RemoveNode moving bytes instead of pointers. --- docs/decomp/Node.md | 185 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/docs/decomp/Node.md b/docs/decomp/Node.md index 79a106f..990e3dc 100644 --- a/docs/decomp/Node.md +++ b/docs/decomp/Node.md @@ -122,3 +122,188 @@ Dynamic array of CNode pointers with reference counting. Used for `children` pro ### CTimeDepNode Timer node base class with start / stop / loop semantics. The Advance function checks `m_startTime` / `m_stopTime` against `theApp.m_now` to toggle `m_isActive`. Used by AudioClip, TimeSensor, and other time-driven nodes. + +## Node.cpp, Object.cpp and Class.cpp in the 5960 retail XBE + +The base classes sit in three runs of code, each in source-file order. + +| Range | Contents | +|---|---| +| `0x0002e8e0` to `0x0002f420` | Node.cpp: CNodeClass, CNode, PRD lookup, SetProperty, CNodeArray, CTimeDepNode, typed array SetSize | +| `0x0002f420` to `0x00030430` | Object.cpp: CObject construction, refcounting, FND lookup, Call, Dot, members | +| `0x00033970` to `0x00034430` | Class.cpp: CClass and CInstance | + +### Object layouts + +``` +CObject 0x14 bytes +0x00 vtable +0x04 m_nRefCount +0x08 m_obj + +0x0c m_parent +0x10 m_members (CNameSpace*) +CNode 0x1c bytes +0x14 m_behavior (CRunner*) +0x18 m_visible +CTimeDepNode 0x30 bytes +0x1c m_loop +0x20 m_startTime +0x24 m_stopTime + +0x28 m_isActive +0x2c m_lastStartTime +CNodeClass +0x14 m_className +0x18 m_nObjectSize + +0x1c m_pfnCreateNode +0x20 m_baseClass + +0x24 m_rgprd +0x28 m_nextClass +CClass +0x2c m_url +0x30 m_file +0x58 m_instanceFunctions + +0x64 m_constructor +0x68 m_nVarCount +CInstance 0x54 bytes CGroup up to +0x44, +0x44 m_class +0x48 m_vars +CNodeArray 0x0c bytes +0x00 m_length +0x04 m_nAlloc +0x08 m_rgpNode +``` + +Vtables: CObject `0x00021b00`, CNodeClass `0x00021958`, CNode `0x000219f8`, CTimeDepNode `0x00021990`, CInstance `0x000226a0`, CClass `0x000226fc`. Slots referenced by the code below: `+0x04` GetPropMap, `+0x08` OnSetProperty, `+0x0c` AddRef, `+0x10` Release, `+0x14` GetFunctionMap, `+0x18` GetNodeClass, `+0x1c` ToNum, `+0x20` ToStr, `+0x24` Deref, `+0x34` GetMember, `+0x38` OnLoad, `+0x5c` CTimeDepNode::OnIsActiveChanged. + +Globals: class list head `0x0017e730`, class records `0x0017f580` (Object), `0x0017f528` (Node), `0x0017f554` (TimeDepNode). PRD maps `0x00174418` (Object), `0x001743a8` (Node), `0x001743d0` (TimeDepNode). The current runner and `this` for behavior scripts live at `0x0017aea4` / `0x0017aea8`; the frame clock is the double at `0x0017e808`. + +### Function map + +| Address | Function | What it does | +|---|---|---| +| `0x0002e900` | CNodeClass::CNodeClass | Stores name, size, factory, base and PRD map, pushes the record on the front of the class list | +| `0x0002e970` | CNodeClass::~CNodeClass | Walks the list by link pointer and unlinks the record if found, then the CObject teardown | +| `0x0002ea20` | CNodeClass::FindByName | `wcsncmp` against each name plus a check that the name ends right there | +| `0x0002ea80` | CNode::~CNode | `ResetFunc` and free the behavior runner, then CObject teardown | +| `0x0002eaf0` | CNode::OnLoad | Looks up `behavior` (FindMemberFunction inlined); if found, builds a 0x1c0 byte CRunner bound to the node and points it at the function | +| `0x0002ebf0` | CNode factory | `new` 0x1c, type 5, visible 1 | +| `0x0002ec40` | CObject::FindProp | PRD walk, `_wcsnicmp` plus full-length check | +| `0x0002ecd0` | CObject::SetProperty | See below | +| `0x0002ee50` | CNode::Render | `m_visible = 0` | +| `0x0002ee60` | CNode::Advance | Runs the behavior script, see below | +| `0x0002eef0` | CNode::GetGroundHeight | Returns 0.0 | +| `0x0002ef00` | CNode::GetBBox | Zeroes the six floats | +| `0x0002ef30` | CNode::GetRadius | Returns 0.0 | +| `0x0002ef40` | CNode::RenderDynamicTexture | Empty | +| `0x0002ef60` | CNodeArray::RemoveNode | Closes up every slot that holds the node, see quirks | +| `0x0002efa0` | CNodeArray::ReleaseAll | Release every non-null slot, then length, capacity and block to zero | +| `0x0002eff0` | CNodeArray::Allocate | Grow-only: new block, copy the old capacity's worth, free the old block | +| `0x0002f050` | CTimeDepNode factory | `new` 0x30, CNode fields, all timing fields zero | +| `0x0002f0d0` | CTimeDepNode::Advance | See below | +| `0x0002f180` | CIntArray::SetSize | Grow-only reallocation then `m_nSize = n`. CNumArray's identical body was folded into this one | +| `0x0002f450` | CObject::~CObject | `CLerper::RemoveObject(this)`, then Release and free every member define and the list | +| `0x0002f4a0` / `0x0002f4b0` | AddRef / Release | `+1`; `-1` and the deleting destructor at zero | +| `0x0002f4d0` | CObject::IsKindOf | Walks `GetNodeClass()` and its `m_baseClass` chain | +| `0x0002f500` | CObject factory | `new` 0x14, type 0, refcount 1 | +| `0x0002f530` | CObject::ToStr | `new CStrObject(L"[object]")`, string at `0x00021b74` | +| `0x0002f590` | CObject::ToNum | `new CNumObject`, value 0 | +| `0x0002f5d0` | CObject::FindFunction | Linear search of the 0x18 byte FND entries until a null function pointer; `wcslen` must equal the length and `wcsncmp` must match; returns the index or -1 | +| `0x0002f640` | CObject::Call | See below | +| `0x000301c0` | CObject::Dot | See below | +| `0x00030310` | CObject::AddMember | Creates the member list on first use, then always pushes a new define at the head | +| `0x00030380` | CObject::SetMember | Creates the list on first use, then find-or-add the define and store the object | +| `0x000303c0` | CObject::GetMember | Null when there is no list, otherwise a lookup by name | +| `0x000303e0` | CObject::FindMemberFunction | Own members first; then the first class up the chain with any member of that name decides (a non-function there means not found) | +| `0x00033970` | CClass::CClass | Registers a `[class]` record with no factory, base or PRD map, clears its own fields, type 6 | +| `0x00033a20` | CClass::~CClass | Frees the URL, Releases the constructor, ReleaseAll on the instance functions, tears down the file and the class record | +| `0x00033ad0` | CClass::CreateNode | `new CInstance(this)`, 0x54 bytes | +| `0x00033b30` | CClass::Load | Saves the current directory into a 2048 byte local, moves it to the file's folder, fetches, parses, and restores it on every exit | +| `0x00033c70` | CClass::LoadAbsURL | The same fetch and parse with no directory change | +| `0x00033d00` | CClass::ParseFile | Sets the parser's file name, line 1 and error flag, parses the class body, reports `Stuff past expected end of file!` if text remains, returns no-error | +| `0x00033d60` | CClass::ParseClassBody | Class compiler over the body, stores the built function as the constructor | +| `0x00033df0` | CInstance::CInstance | CGroup init (bbox size -1.0 from `0x0002bef0`), type 11, AddRef the class, size `m_vars` to the class's variable count with null slots, Construct, then OnLoad | +| `0x00033f00` | CInstance::Construct | See below | +| `0x000342c0` | CInstance::~CInstance | Release the class, ReleaseAll the variables, CGroup teardown | +| `0x00034330` | CInstance::Dot | Class members first, then CObject::Dot | + +### SetProperty (`0x0002ecd0`) + +The byte count arrives in EAX. The field address is `this + prd->pbOffset`. + +1. For a `pt_string` (10) property, if both the current and the new string are non-null and `wcscmp` says they're equal, it returns before anything else, so OnSetProperty never sees a no-op string write. +2. `OnSetProperty` (vtable `+0x08`) can veto the write. +3. By type: + - `pt_string`: free the old buffer, allocate `count * 2 + 2` bytes, copy `count` characters and terminate. + - `pt_intarray` (12), `pt_numarray` (13): `SetSize(bytes / 4)`. `pt_vec2array` (14): `bytes / 8`. `pt_vec3array` (15): `bytes / 12`. `pt_vec4array` (16): `bytes / 16`. Then the raw bytes are copied into the array's buffer. + - Anything else: raw copy of the bytes into the field. For `pt_node` (9) the node already in the field is Released first and the new one AddRef'd after. + +### Call (`0x0002f640`, 2818 bytes) + +Reads the FND entry at `GetFunctionMap() + index * 0x18`: function pointer at `+0x00`, signature as a 16-bit value at `+0x10`. Every argument is first replaced in place by its fully dereferenced value (Deref at vtable `+0x24`, repeated until the object stops changing or becomes null). + +The switch covers signatures 0 to 29, with no case for 6. Case 0 passes the raw `(array, count)` through. Every other case checks the count exactly, converts its arguments, makes the call, wraps the result, and Releases the converted temporaries: + +| Case | Shape | Case | Shape | Case | Shape | +|---|---|---|---|---|---| +| 1 | `void ()` | 11 | `float (float, float)` | 21 | `void (object)` | +| 2 | `int ()` | 12 | `void (str)` | 22 | `void (float, float, float)` | +| 3 | `int (int)` | 13 | `void (str, str)` | 23 | `void (float, float, float, float)` | +| 4 | `str (int)` | 14 | `void (int)` | 24 | `int (int, int)` | +| 5 | `str (str)` | 15 | `object ()` | 25 | `int (str)` | +| 7 | `str (int, int)` | 16 | `object (str)` | 26 | `void (int, int, str)` | +| 8 | `str ()` | 17 | `object (object)` | 27 | `float (int)` | +| 9 | `float ()` | 18 | `void (int, int)` | 28 | `void (int, str)` | +| 10 | `float (float)` | 19 | `void (float)` | 29 | `int (int, str)` | +| | | 20 | `void (float, float)` | | | + +- int arguments are `ToNum` (vtable `+0x1c`) truncated to int; float arguments are the CNumObject's value as is. +- string arguments are `ToStr` (vtable `+0x20`); a null text pointer is replaced by the empty string at `0x00021b98`. +- object arguments are passed as is. +- int and float results come back as a new CNumObject (its constructor at `0x00038fa0` takes the value in xmm0). +- Case 6 has no entry and shares the exit with unknown signatures: return null, no message. +- A wrong argument count calls the running script's error routine with `invalid number of parameters` and returns null. + +### Dot (`0x000301c0`) + +Only an objVariable (4) name is resolved, using its text at `+0x1c` and length at `+0x14`: + +1. FindFunction. A hit returns a new 0x1c byte CFunctionObject (type 8) holding the index and `this`, and AddRefs `this`. +2. GetMember (vtable `+0x34`). A hit is AddRef'd and returned as is. +3. FindProp. A hit returns a new CProperty (`0x00038a20`) for `this` and the PRD. +4. Otherwise null. + +The compiled code checks the type for objVariable a second time before step 3, which can never fail at that point. + +### CNode::Advance (`0x0002ee60`) + +The runner's first field is its wake-up time as a double (`0.0`, from `0x0002bef8`, means awake). + +``` +if (no runner) return; +if (wake != 0) { if (now < wake) return; wake = 0; } +current runner = runner; current this = node; +for (;;) { + do { if (!Step()) ResetFunc(); } while (wake == 0); + if (now < wake) break; + wake = 0; +} +``` + +A behavior script runs every frame until it sleeps into the future. When the function returns, the runner restarts it from the top within the same frame. + +### CTimeDepNode::Advance (`0x0002f0d0`) + +``` +CNode::Advance(); +if (!m_isActive) { + if (start <= (float)now && start != lastStart && (stop == 0.0 || now < stop)) + { lastStart = start; m_isActive = 1; OnIsActiveChanged(); } +} else if (start <= stop && stop != start && stop <= (float)now) { + m_isActive = 0; OnIsActiveChanged(); +} +``` + +The start test and the stop test narrow the double clock to float; the `now < stop` test compares at double precision. A node fires once per distinct start time, and a stop time only counts when it is later than the start. + +### CInstance::Construct (`0x00033f00`, 915 bytes) + +Runs the class constructor's bytecode from `m_constructor + 0x18`. It keeps a 100-entry stack of open nodes, a 100-entry stack of open child arrays (starting at the instance's own children at `+0x1c`), a 2048 byte buffer for string literals, and a pending variable slot (-1 when unset). Opcodes 100 to 108 are handled through a jump table; any other byte ends the loop and the function returns a pointer to it. + +| Op | Operands | Effect | +|---|---|---| +| 100 NewNode | `int cch`, `cch` UTF-16 chars | Push the open node, create the class by name (CreateNewObject `0x00037580`), set its parent to the instance, append it to the open array (growing 16 slots at a time). A pending variable slot gets the node (AddRef'd) and is cleared | +| 101 NewNodeProp | 12 byte PRD, then either `103 int n` or `[102 int n] 100 int cch chars` | With 103: store variable `n` into the open node's property. Otherwise create the node as above, store it into the property of the node underneath, Release the creation reference, and fill a pending slot | +| 102 DefNode | `int n` | The next node created is also kept in variable `n` | +| 103 UseNode | `int n` | AddRef variable `n` and append it to the open array | +| 104 EndNode | none | OnLoad (vtable `+0x38`) the open node and pop | +| 105 InitProp | 12 byte PRD, `int cb`, `cb` bytes | SetProperty on the open node. For strings `cb` is a character count: the text is copied into the local buffer and terminated, and the stream skips `cb * 2` bytes | +| 106 InitArray | `int offset` | Push the open array; the new one is the node array at `node + offset` | +| 107 EndArray | none | Pop the open array | +| 108 Function | `int cch`, chars, `int n` | AddRef the class's instance function `n` and SetMember it on the open node under that name | + +Variable and instance function reads are bounds checked (out of range reads give null); the node stacks are not. + +### Quirks + +- **CNodeArray::RemoveNode moves bytes, not pointers.** The close-up copy uses `rep movsd` with a count of `(length - i - 1) >> 2` followed by `(length - i - 1) & 3` single bytes, so it moves `length - i - 1` bytes when it should move that many 4 byte pointers. Removing anything but one of the last few entries leaves stale pointers in the tail. +- **Unknown Call signatures are silent.** Signature 6 and anything above 29 return null without reporting an error. +- **Signature 29** (`int (int, str)`) exists in the dispatcher. +- **The CNumArray and CIntArray SetSize bodies are one function** in the image; both property types call `0x0002f180`. +- **CNode::OnLoad and CNodeClass::FindProp are not separate functions.** OnLoad carries its own inlined copy of FindMemberFunction, and the class compiler (`CClassCompiler::ParseProps`, `0x00036440`) calls CObject::FindProp directly on a class record, whose GetPropMap returns its `m_rgprd`. From 8c17930e6ff15553599d1f8cf81201f013d5bc5d Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:19:21 -0500 Subject: [PATCH 050/167] Dropped the dead desktop branches from xbox/std.h It refuses to build for desktop now, so the !_XBOX paths could never be taken. Code and data in every Xbox object came out the same; only debug info and the build timestamp moved. --- theseus/xbox/std.h | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/theseus/xbox/std.h b/theseus/xbox/std.h index 77557f0..9882452 100755 --- a/theseus/xbox/std.h +++ b/theseus/xbox/std.h @@ -73,8 +73,7 @@ typedef char FSCHAR; #define EXTERN_C extern "C" -// Xbox debug CRT overrides operator new for leak tracking. -// Desktop doesn't use this, it has its own tooling. +// Debug CRT overrides operator new for leak tracking. #if defined(_XBOX) && defined(_DEBUG) #define new new(_NORMAL_BLOCK, __FILE__, __LINE__) #endif @@ -84,13 +83,9 @@ typedef char FSCHAR; #else // !__cplusplus #define EXTERN_C extern -#ifndef _XBOX -// C mode on desktop: bool/true/false come from via sdl_platform.h -#else #define bool BOOL #define true TRUE #define false FALSE -#endif #define inline _inline #endif // __cplusplus @@ -104,18 +99,13 @@ typedef char FSCHAR; #endif // Node property offset: uses 'this' so it only works inside member functions. -// Both platforms need this for the XAP property system. +// The XAP property system needs it. // Conflicts with the standard offsetof(type, member) from . #undef offsetof #define offsetof(member) (((int)(intptr_t)&member) - (int)(intptr_t)this) -// Safe pointer-to-int cast. Xbox is 32-bit so (int)ptr works. On 64-bit -// desktop we need to go through intptr_t to avoid the clang error. -#ifdef _XBOX +// Pointer-to-int cast. Xbox is 32-bit so (int)ptr works. #define PTR2INT(p) ((int)(p)) -#else -#define PTR2INT(p) ((int)(intptr_t)(p)) -#endif #ifdef _UNICODE extern void Unicode(TCHAR* wsz, const char* sz, int nMaxChars); @@ -155,7 +145,7 @@ inline bool TheseusCheck(bool ok, const char* expr, const char* file, int line) } #else -// Release (Xbox) and all desktop builds: no-op asserts, silent trace +// Release: no-op asserts, silent trace #define ASSERT(f) ((void)0) #define VERIFY(f) ((void)(f)) #define ASSERTHR(f) ((void)0) @@ -197,14 +187,4 @@ typedef RGBQUAD FAR* LPRGBQUAD; #define MAX_COMPUTERNAME_LENGTH 15 -#else // !_XBOX - -#define START_PROFILE() -#define END_PROFILE() - -// Desktop: DbgPrint goes to stderr -#ifndef DbgPrint -#define DbgPrint(...) fprintf(stderr, __VA_ARGS__) -#endif - #endif // _XBOX From 7ea3278f4923edfb50d07d2e045872d45881bb7a Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:20:01 -0500 Subject: [PATCH 051/167] Rebuilt DeltaField from the 5960 binary Wrote the warp field setup, row builder and all 24 warp styles fresh from 0x5e4e0, 0x5e570 and 0x5e7d0, and put every style's formula and constants in the write-up. Fields and fades match byte for byte for all styles at three sizes. --- docs/decomp/TmapSystem.md | 83 ++++++++- theseus/render/tmap_system.cpp | 331 +++++++++++++++++---------------- 2 files changed, 249 insertions(+), 165 deletions(-) diff --git a/docs/decomp/TmapSystem.md b/docs/decomp/TmapSystem.md index c00c53d..52a8dc0 100644 --- a/docs/decomp/TmapSystem.md +++ b/docs/decomp/TmapSystem.md @@ -149,21 +149,86 @@ than 63 rows up underflows its 12-bit field and borrows from the x field. Nothing in the 5960 dash changes the line width from 1, so only the 1 pixel path runs. +## DeltaField + +A 0x3c byte object built inline by ImageFader (`0x0005e120`). + +| Offset | Field | +|--------|-------| +| 0x00 | style | +| 0x04 | current row (-1 = never sized) | +| 0x08 | most negative y offset so far (1/32 px) | +| 0x0c..0x18 | x, y, r, theta of the cell being computed | +| 0x1c, 0x20 | x scale, y scale | +| 0x24, 0x25, 0x26 | polar, has r term, has theta term | +| 0x28, 0x2c | width, height | +| 0x30 | aspect 1:1 | +| 0x34, 0x38 | DeltaFieldData: ring rows, cells | + +**Assign + SetSize (`0x0005e4e0`, also inlined twice in ImageFader).** Sets aspect, polar, r and +theta flags to 1, frees and reallocates the cells as `w * h * 3 + 0x40`, sets +`xs = 2.0 / w`, `ys = 2.0 / h`, makes both the larger of the two when aspect is on, then +`ys *= -1.0` (`0x0002bef0`) so +y is up. Resets the row and the negative extent to 0. + +**CalcSome (`0x0005e570`).** One row per call (two when starting on row 0). Top and bottom rows +and the first and last cell of every row are `0x7e07e0`. For the rest: + +```c +y = (h - row*2) * ys * 0.5; fx = 32.0 / xs; fy = 32.0 / ys; +x = (col*2 - w) * xs * 0.5; +if (hasR) r = sqrt(x*x + y*y); +if (hasTheta) theta = fpatan(y, x); +GetXY(init = (col == 1 && row == 1), r, theta, x, y) -> X, Y, polar +if (polar) { X, Y = X*cos(Y), X*sin(Y); } +dx = (int)((X - x) * fx); dy = (int)((y - Y) * fy); +clamp col + (dx >> 5) into [0, w-1) and row + (dy >> 5) into [0, h-1) by rewriting dx, dy +negY = min(negY, dy); +cell = (dx + 0x7e0) << 12 | (dy + 0x7e0); +``` + +When the last row is done the ring height becomes `1 - (negY >> 5)`. Returns rows left. + +**GetXY (`0x0005e7d0`).** A switch on the style; out-of-range styles are rewritten to 0. Style +parameters `A0`, `A1`, `A2` are globals at `0x00188f60`, `0x00188f5c`, `0x00188f58`, rolled +only on the init cell. `rnd(n)` below is `rand() * (n / 32767)`; the binary stores each +`n / 32767` as one constant. Results are polar (radius, angle) unless noted. + +| Style | Init | X | Y | +|-------|------|---|---| +| 0 | | `(atan(0.55 - r) * 0.16 + 1) * r` | `theta - 0.01` | +| 1 | | `r * 0.87` | `theta - 0.075` | +| 2 | `A0 = (int)rnd(40) * pi` | `((sin(A0*theta) + 1) * 0.03 + 0.92) * r` | `theta` | +| 3 | `A0 = 2 + rnd(16)`, `A1 = 0.01 + rnd(0.05)`, `A2 = 0.002 + rnd(0.006)` | `max(r - A2, 0)` | `sin(A0*r) * A1 + theta` | +| 4 | | `r * 0.87` | `theta - 0.009` | +| 5 | | `r * 0.8` | `theta - 0.008` | +| 6 | `A0 = 2 + rnd(16)`, `A1 = 0.01 + rnd(0.05)` | `sin(A0*y) * A1 + x` | `sin(A0*x) * A1 + y` (x/y) | +| 7 | | `r * 1.01` | `theta + 0.021` | +| 8 | | `r - 0.01` | `theta` | +| 9 | `A0 = rnd(0.0045)`, `A1 = 0.1 + rnd(0.06)`, `A2 = 0.003 + rnd(0.005)` | `(0.99 - pow(abs(x), 1.3) * A1) * r - rnd(A0)` | `theta - A2` | +| 10 | as 9 but `A2 = 0.003 + rnd(0.006)` | as 9 with `abs(y)` | `theta + A2` | +| 11 | | `r - x * y * 0.18` | `theta - 0.005` | +| 12 | | `r * 0.96` | `theta - 0.021` | +| 13 | | `rnd(0.01) + x - 0.005` | `y - rnd(0.01) - 0.005 - (y - 1.3) * 0.04` (x/y) | +| 14 | `A0 = 0.95 + rnd(0.06)` | `A0 * r` | `theta - max(1 - r, 0) * 0.035` | +| 15 | | `r * 0.94` | `theta - 0.007` | +| 16 | | `rnd(0.008) + x * 0.9 - 0.004` | same with y (x/y) | +| 17 | | `(rnd(2) - 1) * 0.1 + x` | same with y (x/y) | +| 18 | | `r * 0.99` | `theta + 0.009` | +| 19 | | `(rnd(0.05) + 0.92) * r` | `theta + 0.003` | +| 20 | | `sin(r * 6.2831855) * 0.04 + r` | `theta + 0.015` | +| 21 | | `((sin(r * 15) + 1) * 0.05 + 0.87) * r` | `theta` | +| 22 | `A0 = rnd(0.1)`, `A1 = 3 + rnd(7)`, `A2 = 0.9 + rnd(0.3)` | `((r - A2) * 0.13 + 1) * r` | `sin(A1*r) * A0 + theta` | +| 23 | `A0 = (int)(rnd(2.1) + 1) * pi` | `((cos(A0*theta) + 1) * 0.05 + 0.89) * r` | `theta + 0.005` | + +Styles 9, 10, 13, 16, 17 and 19 call `rand()` per cell, so their fields differ every time +they're built. + ## Palette Presets (10 styles) 0: Firestorm, 1: Aqua, 2: Purple & Blues, 3: Color Wheel, 4: Bizarro Mystery Unveiled, 5: Bizarro Color Wheel, 6: Dark Rainbow, 7: Ice Nightshade, 8: Mystery Unveiled, 9: Roundabout -## Delta Field Styles (24 styles) - -0: Radial Breakaway, 1: Hip-no-therapy, 2: Sunburst Many, 3: Theta Divergence, -4-5: Turbo Flow Out, 6: Boxilite, 7: Collapse & Turn, 8: Constant Out, -9-10: Directrix Expand, 11: Equalateral Hyperbola, 12: Expand & Turn, -13: Gravity, 14: In/Out Inner Turn, 15: Left Turn Flow, 16: Linear Spread, -17: Noise Field, 18: Right Turn, 19: Scattered Flow, 20: Sine-Sphere, -21: Sine Multi-Circ, 22: Sphere, 23: Sunburst Few - ## Notes - CSurfx operates on a raw BYTE array with manual pitch calculation. diff --git a/theseus/render/tmap_system.cpp b/theseus/render/tmap_system.cpp index 6923c07..d2309a2 100644 --- a/theseus/render/tmap_system.cpp +++ b/theseus/render/tmap_system.cpp @@ -548,16 +548,20 @@ void CSurfx::Line(int sx, int sy, int ex, int ey, unsigned char color) // ===== DeltaField =========================================================== -// Computes a 2D displacement vector field for pixel warping effects. -// 24 preset warp styles (polar and cartesian). +// Builds the per-pixel "pull from here" map that CImageFader warps the +// surface with. 24 warp styles, each a small formula in polar or x/y space. -#define DEC_SIZE 5 +#define DELTA_ZERO 0x7E07E0 // both offsets at the bias, so "stay put" +// DeltaField::DeltaField() +// Reconstructed from the inline construction in CImageFader_RenderDynamicTexture @ 0x0005e120: +// style = 0; currentY = -1; width = height = 0; field = {negY 0, buffer NULL} DeltaField::DeltaField() { - mWidth = mHeight = 0; - mCurrentY = -1; m_nStyle = 0; + mCurrentY = -1; + mWidth = 0; + mHeight = 0; } DeltaFieldData::DeltaFieldData() @@ -566,176 +570,193 @@ DeltaFieldData::DeltaFieldData() mField = NULL; } +// DeltaFieldData::~DeltaFieldData() +// Reconstructed from 5960 retail XBE @ 0x0005e110: free(field). DeltaFieldData::~DeltaFieldData() { delete [] mField; } +// DeltaField::GetField() +// Reconstructed from the tail of CImageFader_RenderDynamicTexture @ 0x0005e120: +// if (currentY >= 0 && currentY != height) do CalcSome(); while (currentY != height); +// Finishes the field on demand. Returning NULL for a field that was never sized +// is a Theseus guard; the binary never reaches here in that state. DeltaFieldData* DeltaField::GetField() { - if (mCurrentY >= 0) - { - while (!IsCalculated()) - CalcSome(); + if (mCurrentY < 0) + return NULL; - return &mFieldData; - } + while (mCurrentY != mHeight) + CalcSome(); - return NULL; + return &mFieldData; } +// DeltaField::Assign() +// Reconstructed from 5960 retail XBE @ 0x0005e4e0: +// aspect1to1 = 1; polar = hasR = hasTheta = 1; then the SetSize body, forced. void DeltaField::Assign() { mAspect1to1 = true; mPolar = true; mHasRTerm = true; mHasThetaTerm = true; - SetSize(mWidth, mHeight, true); } +// DeltaField::SetSize(w, h, force) +// Reconstructed from 5960 retail XBE, inlined at 0x0005e4e0 and twice in +// CImageFader_RenderDynamicTexture @ 0x0005e120: +// if (w != width || h != height || force) { +// width = w; height = h; free(buf); buf = new[w * h * 3 + 0x40]; +// xs = 2.0 / w; ys = 2.0 / h; if (aspect1to1) xs = ys = max(xs, ys); +// ys *= -1.0; currentY = 0; negY = 0; } +// The y scale is flipped so +y points up in the warp formulas. void DeltaField::SetSize(long inWidth, long inHeight, bool inForceRegen) { - if (inWidth != mWidth || inHeight != mHeight || inForceRegen) - { - mWidth = inWidth; - mHeight = inHeight; - - delete [] mFieldData.mField; - mFieldData.mField = new char [3 * mWidth * mHeight + 64]; - - mXScale = 2.0f / ((float)mWidth); - mYScale = 2.0f / ((float)mHeight); + if (inWidth == mWidth && inHeight == mHeight && !inForceRegen) + return; - if (mAspect1to1) - { - if (mYScale > mXScale) - mXScale = mYScale; - else - mYScale = mXScale; - } + mWidth = inWidth; + mHeight = inHeight; - mCurrentY = 0; - mNegYExtents = 0; + delete [] mFieldData.mField; + mFieldData.mField = new char [mWidth * mHeight * 3 + 64]; - // Win32 flips Y coordinates - mYScale *= -1; + mXScale = 2.0f / (float)mWidth; + mYScale = 2.0f / (float)mHeight; + if (mAspect1to1) + { + if (mYScale > mXScale) + mXScale = mYScale; + else + mYScale = mXScale; } + mYScale *= -1.0f; + + mCurrentY = 0; + mNegYExtents = 0; } -#define __encode(x, y) sx = x + 0x7E0; \ - sy = y + 0x7E0; \ - *((unsigned long*)g) = (sx << 12) | (sy); \ - g += 3; +static inline void WriteDelta(char* cell, unsigned long code) +{ + cell[0] = (char)code; + cell[1] = (char)(code >> 8); + cell[2] = (char)(code >> 16); +} +// DeltaField::CalcSome() +// Reconstructed from 5960 retail XBE @ 0x0005e570. Computes one row per call +// (two when it starts on the top border row) and returns rows still to go. +// Border rows and the first and last cell of every row are DELTA_ZERO. For an +// inner cell the pixel position maps to x, y in -1..1 (aspect corrected), +// r and theta are filled in when the style needs them, GetXY gives the source +// point (converted from polar when it says so), and the offset in 1/32 pixel +// steps is clamped so the pull stays inside the image. The most negative y +// offset sizes the fade's ring buffer: negY = 1 - (minDy >> 5). +// See docs/decomp/TmapSystem.md. int DeltaField::CalcSome() { - float xscale2, yscale2, r, fx, fy; - long px, sx, sy, t; - char* g; - - g = mFieldData.mField + 3 * mWidth * mCurrentY; + char* cell = mFieldData.mField + mCurrentY * mWidth * 3; if (mCurrentY == 0 || mCurrentY == mHeight - 1) { - for (px = 0; px < mWidth; px++) - { - __encode(0, 0) - } - + for (long x = 0; x < mWidth; x++, cell += 3) + WriteDelta(cell, DELTA_ZERO); mCurrentY++; } if (mCurrentY > 0 && mCurrentY < mHeight - 1) { mY_Cord = 0.5f * mYScale * (mHeight - 2 * mCurrentY); + float toFixedX = 32.0f / mXScale; + float toFixedY = 32.0f / mYScale; - xscale2 = ((float)(1 << DEC_SIZE)) / mXScale; - yscale2 = ((float)(1 << DEC_SIZE)) / mYScale; - - __encode(0, 0) + WriteDelta(cell, DELTA_ZERO); + cell += 3; - for (px = 1; px < mWidth - 1; px++) + for (long px = 1; px < mWidth - 1; px++, cell += 3) { mX_Cord = 0.5f * mXScale * (2 * px - mWidth); - if (mHasRTerm) mR_Cord = sqrtf(mX_Cord * mX_Cord + mY_Cord * mY_Cord); if (mHasThetaTerm) mT_Cord = atan2f(mY_Cord, mX_Cord); - GetXY(mR_Cord, mT_Cord, mX_Cord, mY_Cord, fx, fy, mPolar, px == 1 && mCurrentY == 1); - + float srcX, srcY; + GetXY(mR_Cord, mT_Cord, mX_Cord, mY_Cord, srcX, srcY, mPolar, px == 1 && mCurrentY == 1); if (mPolar) { - r = fx; - fx = r * cosf(fy); - fy = r * sinf(fy); + float radius = srcX; + srcX = radius * cosf(srcY); + srcY = radius * sinf(srcY); } - sx = (long)(xscale2 * (fx - mX_Cord)); - sy = (long)(yscale2 * (mY_Cord - fy)); + long dx = (long)(toFixedX * (srcX - mX_Cord)); + long dy = (long)(toFixedY * (mY_Cord - srcY)); - t = px + (sx >> DEC_SIZE); - if (t >= mWidth - 1) - sx = ((mWidth - 1 - px) << DEC_SIZE); - else if (t < 0) - sx = ((-px) << DEC_SIZE); + long landX = px + (dx >> 5); + if (landX >= mWidth - 1) + dx = (mWidth - 1 - px) * 32; + else if (landX < 0) + dx = -px * 32; - t = mCurrentY + (sy >> DEC_SIZE); - if (t >= mHeight - 1) - sy = ((mHeight - 1 - mCurrentY) << DEC_SIZE); - else if (t < 0) - sy = ((-mCurrentY) << DEC_SIZE); + long landY = mCurrentY + (dy >> 5); + if (landY >= mHeight - 1) + dy = (mHeight - 1 - mCurrentY) * 32; + else if (landY < 0) + dy = -mCurrentY * 32; - if (sy < mNegYExtents) - mNegYExtents = sy; + if (dy < mNegYExtents) + mNegYExtents = dy; - __encode(sx, sy) + WriteDelta(cell, ((dx + 0x7E0) << 12) | (dy + 0x7E0)); } - __encode(0, 0) - - mCurrentY += 1; + WriteDelta(cell, DELTA_ZERO); + mCurrentY++; } - if (IsCalculated()) - { - mFieldData.mNegYExtents = 1 - (mNegYExtents >> DEC_SIZE); - } + if (mCurrentY == mHeight) + mFieldData.mNegYExtents = 1 - (mNegYExtents >> 5); return mHeight - mCurrentY; } +// DeltaField::GetXY(r, theta, x, y, X, Y, bPolar, bInit) +// Reconstructed from 5960 retail XBE @ 0x0005e7d0 (a 24 way switch). Each +// style returns a source point, as (radius, angle) unless it clears bPolar. +// bInit is true on the first inner cell of a field; the random styles roll +// their shared parameters A0..A2 (globals 0x00188f60/5c/58) there. Where the +// binary multiplies rand() by a folded constant such as 0x0002bff0 = +// 16/32767, that is written here as rnd(16). A few styles had their constant +// products folded (0.5 * 0.06 -> 0.03 at 0x0002bff8, 0.5 * 0.1 -> 0.05); the +// product is kept spelled out, which can differ from the folded value in the +// last float bit. Out of range styles snap back to 0. The formulas are the +// binary's; constants are listed per style in docs/decomp/TmapSystem.md. void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& Y, bool& bPolar, bool bInit) { static float A0, A1, A2; + const float pi = 3.141592653f; + bPolar = true; switch (m_nStyle) { - default: - m_nStyle = 0; - // FALL THROUGH - - case 0: // Radial Breakaway - X = r * (1.0f + 0.16f * atanf(0.55f - r)); - Y = theta - 0.01f; - break; - - case 1: // Hip-no-therapy + case 1: X = r * 0.87f; Y = theta - 0.075f; - break; + return; - case 2: // Sunburst - Many + case 2: if (bInit) - A0 = trunc(8.0f * rnd(5.0f)) * 3.141592653f; + A0 = trunc(8.0f * rnd(5.0f)) * pi; X = ((1.0f + sinf(A0 * theta)) * 0.5f * 0.06f + 0.92f) * r; Y = theta; - break; + return; - case 3: // Theta Divergence + case 3: if (bInit) { A0 = 2.0f + rnd(16.0f); @@ -744,19 +765,19 @@ void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& } X = pos(r - A2); Y = theta + A1 * sinf(A0 * r); - break; + return; - case 4: // Turbo Flow Out - X = 0.87f * r; + case 4: + X = r * 0.87f; Y = theta - 0.009f; - break; + return; - case 5: // Turbo Flow Out - More - X = 0.8f * r; + case 5: + X = r * 0.8f; Y = theta - 0.008f; - break; + return; - case 6: // Boxilite + case 6: if (bInit) { A0 = 2.0f + rnd(16.0f); @@ -765,101 +786,91 @@ void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& X = x + A1 * sinf(A0 * y); Y = y + A1 * sinf(A0 * x); bPolar = false; - break; + return; - case 7: // Collapse & Turn - X = 1.01f * r; + case 7: + X = r * 1.01f; Y = theta + 0.021f; - break; + return; - case 8: // Constant Out + case 8: X = r - 0.01f; Y = theta; - break; - - case 9: // Directrix Expand - X - if (bInit) - { - A0 = rnd(0.0045f); - A1 = 0.10f + rnd(0.06f); - A2 = 0.003f + rnd(0.005f); - } - X = (0.99f - A1 * powf(fabsf(x), 1.3f)) * r - rnd(A0); - Y = theta - A2; - break; + return; - case 10: // Directrix Expand - Y + case 9: + case 10: if (bInit) { A0 = rnd(0.0045f); - A1 = 0.10f + rnd(0.06f); - A2 = 0.003f + rnd(0.006f); + A1 = 0.1f + rnd(0.06f); + A2 = 0.003f + (m_nStyle == 9 ? rnd(0.005f) : rnd(0.006f)); } - X = (0.99f - A1 * powf(fabsf(y), 1.3f)) * r - rnd(A0); - Y = theta + A2; - break; + X = (0.99f - A1 * powf(fabsf(m_nStyle == 9 ? x : y), 1.3f)) * r - rnd(A0); + Y = m_nStyle == 9 ? theta - A2 : theta + A2; + return; - case 11: // Equalateral Hyperbola + case 11: X = r - 0.18f * x * y; Y = theta - 0.005f; - break; + return; - case 12: // Expand & Turn - X = 0.96f * r; + case 12: + X = r * 0.96f; Y = theta - 0.021f; - break; + return; - case 13: // Gravity + case 13: X = x + rnd(0.01f) - 0.005f; Y = y - rnd(0.01f) - 0.005f - (y - 1.3f) * 0.04f; bPolar = false; - break; + return; - case 14: // In or Out, Inner Turn + case 14: if (bInit) A0 = 0.95f + rnd(0.06f); X = r * A0; Y = theta - pos(1.0f - r) * 0.035f; - break; + return; - case 15: // Left turn & Flow Out - X = 0.94f * r; + case 15: + X = r * 0.94f; Y = theta - 0.007f; - break; + return; - case 16: // Linear Spread + case 16: X = 0.9f * x + rnd(0.008f) - 0.004f; Y = 0.9f * y + rnd(0.008f) - 0.004f; bPolar = false; - break; + return; - case 17: // Noise Field + case 17: X = x + 0.1f * (rnd(2.0f) - 1.0f); Y = y + 0.1f * (rnd(2.0f) - 1.0f); bPolar = false; - break; + return; - case 18: // Right Turn + case 18: X = r * 0.99f; Y = theta + 0.009f; - break; + return; - case 19: // Scattered Flow Out + case 19: X = (0.92f + rnd(0.05f)) * r; Y = theta + 0.003f; - break; + return; - case 20: // Simple Sine-Sphere + case 20: X = r + 0.04f * sinf(6.2831853f * r); Y = theta + 0.015f; - break; + return; - case 21: // Sine Multi-Circ + case 21: X = r * (0.87f + 0.05f * (1.0f + sinf(r * 15.0f))); Y = theta; - break; + return; - case 22: // Sphere + case 22: if (bInit) { A0 = rnd(0.1f); @@ -868,14 +879,22 @@ void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& } X = r * (1.0f + 0.13f * (r - A2)); Y = theta + A0 * sinf(A1 * r); - break; + return; - case 23: // Sunburst - Few + case 23: if (bInit) - A0 = trunc(1.0f + rnd(2.1f)) * 3.141592653f; + A0 = trunc(1.0f + rnd(2.1f)) * pi; X = ((1.0f + cosf(A0 * theta)) * 0.5f * 0.1f + 0.89f) * r; Y = theta + 0.005f; - break; + return; + + default: + m_nStyle = 0; + // fall through + case 0: + X = r * (1.0f + 0.16f * atanf(0.55f - r)); + Y = theta - 0.01f; + return; } } From bf5c33631458638c5a244e3b1ccb3025b474ae82 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:22:28 -0500 Subject: [PATCH 052/167] Took the old debug lines out of the script runner The asserts, traces and debug-only blocks weren't in the retail binary, so they're gone. Where one guarded something real (popping an empty stack, pushing past the top, a short call, member access off a non-instance) there's a CHECK now. Also rewrote the rest of the bodies again from the decompile. Script results still match on all 266 XAP calls and the 47 runner cases, and a desktop boot logs the same. --- theseus/shared/xap_vm.cpp | 955 +++++++++++++++++--------------------- 1 file changed, 416 insertions(+), 539 deletions(-) diff --git a/theseus/shared/xap_vm.cpp b/theseus/shared/xap_vm.cpp index b9acc0d..4462123 100644 --- a/theseus/shared/xap_vm.cpp +++ b/theseus/shared/xap_vm.cpp @@ -11,10 +11,7 @@ #include "camera.h" #include "xap_compile.h" -//#define LOCALTRACE TRACE -#define LOCALTRACE 1 ? (void)0 : ::Trace - -// Shared runner state. 5960 keeps the same four at 0x17aea0..0x17aea8. +// Shared runner state. 5960 keeps the same four at 0x17ae9c..0x17aea8. CObject* g_pThis; CRunner* g_pRunner; CObject** g_rgParam; @@ -22,6 +19,7 @@ int g_nParam; extern bool g_bInputEnable; extern CCamera theCamera; +extern CMathClass g_Math; CObject* LookupMember(CObject* pThis, const TCHAR* pchName, int cchName); @@ -34,7 +32,7 @@ CObject* LookupMember(CObject* pThis, const TCHAR* pchName, int cchName); // only shows up inlined). Decompile: // m_nRefCount = 1; m_obj = 2; m_value = xmm0; // The string form has no standalone copy in the binary; CStrObject::ToNum -// @ 0x32200 does the same wcstod inline. Kept for the header contract. +// @ 0x32200 does the same (float)wcstod(sz, NULL) inline. Kept for runner.h. CNumObject::CNumObject() { m_obj = objNumber; @@ -139,7 +137,7 @@ class CLocalVariable : public CObject CLocalVariable(); ~CLocalVariable(); CObject* Deref(); - void Assign(CObject* pObject); + void Assign(CObject* pValue); CObject* m_pValue; }; @@ -162,13 +160,13 @@ CLocalVariable::~CLocalVariable() m_pValue->Release(); } -void CLocalVariable::Assign(CObject* pObject) +void CLocalVariable::Assign(CObject* pValue) { if (m_pValue != NULL) m_pValue->Release(); - m_pValue = pObject; - if (pObject != NULL) - pObject->AddRef(); + m_pValue = pValue; + if (pValue != NULL) + pValue->AddRef(); } CObject* CLocalVariable::Deref() @@ -190,8 +188,8 @@ CVarObject::CVarObject(const TCHAR* pch, int cch) m_obj = objVariable; m_length = cch; m_capacity = cch + 1; - m_text = new TCHAR[m_capacity]; - CopyChars(m_text, pch, cch); + m_text = new TCHAR[cch + 1]; + memcpy(m_text, pch, cch * sizeof(TCHAR)); m_text[cch] = '\0'; } @@ -215,14 +213,9 @@ CObject* CVarObject::Deref() CObject* pFound; if (_tcscmp(m_text, _T("Math")) == 0) - { - extern CMathClass g_Math; pFound = &g_Math; - } else if (_tcscmp(m_text, _T("camera")) == 0) - { pFound = &theCamera; - } else { pFound = g_pRunner->LookupVariable(m_text, _tcslen(m_text)); @@ -234,9 +227,9 @@ CObject* CVarObject::Deref() } pFound->AddRef(); - CObject* pValue = pFound->Deref(); + CObject* pResolved = pFound->Deref(); Release(); - return pValue; + return pResolved; } // CVarObject::Assign @@ -244,15 +237,13 @@ CObject* CVarObject::Deref() // obj = (self lookup, then this lookup, as in Deref); // if (!obj) { Error(L"Unknown variable: %s", m_text); return; } // obj->Assign(value); -void CVarObject::Assign(CObject* pNewObject) +void CVarObject::Assign(CObject* pValue) { CObject* pTarget = g_pRunner->LookupVariable(m_text, _tcslen(m_text)); - if (pTarget == NULL) - { + if (pTarget != NULL) + pTarget->Assign(pValue); + else g_pRunner->Error(_T("Unknown variable: %s"), m_text); - return; - } - pTarget->Assign(pNewObject); } // ========================================================================= @@ -268,28 +259,22 @@ void CVarObject::Assign(CObject* pNewObject) // if (v) v->AddRef(); this->Release(); return v; // Assign: old = m_owner->m_vars[i]; if (old) old->Release(); // m_owner->m_vars[i] = p (if in range); if (p) p->AddRef(); -// The range checks are CNodeArray::GetNode/SetNode. Theseus adds the ASSERTs. -CMemberVarObject::CMemberVarObject(CInstance* pObject, int nMember) +// The range checks are what CNodeArray::GetNode/SetNode already do. +CMemberVarObject::CMemberVarObject(CInstance* pInstance, int iMember) { - ASSERT(pObject != NULL); - ASSERT(nMember >= 0 && nMember < pObject->m_vars.GetLength()); - m_obj = objMemberVar; - m_owner = pObject; - m_memberIndex = nMember; - pObject->AddRef(); + m_owner = pInstance; + m_memberIndex = iMember; + pInstance->AddRef(); } CMemberVarObject::~CMemberVarObject() { - ASSERT(m_owner != NULL); m_owner->Release(); } CObject* CMemberVarObject::Deref() { - ASSERT(m_memberIndex >= 0 && m_memberIndex < m_owner->m_vars.GetLength()); - CObject* pValue = m_owner->m_vars.GetNode(m_memberIndex); if (pValue != NULL) pValue->AddRef(); @@ -297,16 +282,15 @@ CObject* CMemberVarObject::Deref() return pValue; } -void CMemberVarObject::Assign(CObject* pNewObject) +void CMemberVarObject::Assign(CObject* pValue) { - ASSERT(m_memberIndex >= 0 && m_memberIndex < m_owner->m_vars.GetLength()); - - CObject* pOld = m_owner->m_vars.GetNode(m_memberIndex); + CNodeArray& vars = m_owner->m_vars; + CObject* pOld = vars.GetNode(m_memberIndex); if (pOld != NULL) pOld->Release(); - m_owner->m_vars.SetNode(m_memberIndex, (CNode*)pNewObject); - if (pNewObject != NULL) - pNewObject->AddRef(); + vars.SetNode(m_memberIndex, (CNode*)pValue); + if (pValue != NULL) + pValue->AddRef(); } // CMemberFunctionObject [type 14, vtable 0x22d68] @@ -330,9 +314,9 @@ CMemberFunctionObject::~CMemberFunctionObject() CObject* CMemberFunctionObject::Deref() { - CRunner runner(m_owner); - runner.SetFunc(m_function); - CObject* pResult = runner.Run(); + CRunner vm(m_owner); + vm.SetFunc(m_function); + CObject* pResult = vm.Run(); Release(); return pResult; } @@ -355,9 +339,9 @@ CFunctionObject::~CFunctionObject() m_owner->Release(); } -CObject* CFunctionObject::Call(CObject** rgparam, int nParam) +CObject* CFunctionObject::Call(CObject** rgArgs, int nArgs) { - return m_owner->Call(m_functionIndex, rgparam, nParam); + return m_owner->Call(m_functionIndex, rgArgs, nArgs); } // CFunction [type 13, vtable 0x22c18] @@ -385,7 +369,14 @@ CFunction::~CFunction() // Assign: old = g_pThis->m_vars[i]; if (old) old->Release(); // g_pThis->m_vars[i] = p; if (p) p->AddRef(); // Same as CMemberVarObject but the instance is whatever g_pThis is right -// now (LookupMember sets it). Theseus adds the ASSERTs. +// now (LookupMember sets it). 5960 just casts g_pThis; Theseus checks first. +static CInstance* MemberOwner() +{ + if (!CHECK(g_pThis != NULL && g_pThis->m_obj == objInstance)) + return NULL; + return (CInstance*)g_pThis; +} + CMember::CMember() { m_obj = objMember; @@ -398,40 +389,33 @@ CMember::~CMember() CObject* CMember::Deref() { - ASSERT(g_pThis != NULL); - ASSERT(g_pThis->m_obj == objInstance); - - CInstance* pInstance = (CInstance*)g_pThis; - ASSERT(m_memberIndex >= 0 && m_memberIndex < pInstance->m_vars.GetLength()); - - CObject* pValue = pInstance->m_vars.GetNode(m_memberIndex); + CInstance* pInstance = MemberOwner(); + CObject* pValue = (pInstance != NULL) ? pInstance->m_vars.GetNode(m_memberIndex) : NULL; if (pValue != NULL) pValue->AddRef(); Release(); return pValue; } -void CMember::Assign(CObject* pNewValue) +void CMember::Assign(CObject* pValue) { - ASSERT(g_pThis != NULL); - ASSERT(g_pThis->m_obj == objInstance); - - CInstance* pInstance = (CInstance*)g_pThis; - ASSERT(m_memberIndex >= 0 && m_memberIndex < pInstance->m_vars.GetLength()); + CInstance* pInstance = MemberOwner(); + if (pInstance == NULL) + return; CObject* pOld = pInstance->m_vars.GetNode(m_memberIndex); if (pOld != NULL) pOld->Release(); - pInstance->m_vars.SetNode(m_memberIndex, (CNode*)pNewValue); - if (pNewValue != NULL) - pNewValue->AddRef(); + pInstance->m_vars.SetNode(m_memberIndex, (CNode*)pValue); + if (pValue != NULL) + pValue->AddRef(); } // CObject::Assign // Reconstructed from 5960 retail XBE @ 0x00039900 (shared by CObject and // CNode). Decompile: // Error(L"Cannot assign to objects"); -void CObject::Assign(CObject* pObject) +void CObject::Assign(CObject* pValue) { g_pRunner->Error(_T("Cannot assign to objects")); } @@ -443,10 +427,10 @@ void CObject::Assign(CObject* pObject) // CNodeArrayObject constructor [type 7, vtable 0x22d30] // Reconstructed from 5960 retail XBE: inlined in CProperty::Deref @ 0x38b00. // m_obj = 7; m_nodeArray = (CNodeArray*)(node + prop offset); -CNodeArrayObject::CNodeArrayObject(CNodeArray* pNodeArray) +CNodeArrayObject::CNodeArrayObject(CNodeArray* pArray) { m_obj = objNodeArray; - m_nodeArray = pNodeArray; + m_nodeArray = pArray; } // CNodeArrayObject::Call @@ -456,22 +440,28 @@ CNodeArrayObject::CNodeArrayObject(CNodeArray* pNodeArray) // 2: p = rgparam[0]->Deref(); m_nodeArray->RemoveNode(p); p->Release(); // return NULL; // add keeps both the Deref reference and an extra one, so the array owns one. -CObject* CNodeArrayObject::Call(int nFunction, CObject** rgparam, int nParam) +CObject* CNodeArrayObject::Call(int nFunction, CObject** rgArgs, int nArgs) { - if (nFunction == 0) + switch (nFunction) + { + case 0: return new CNumObject((float)m_nodeArray->GetLength()); - if (nFunction == 1) - { - CObject* pAdd = rgparam[0]->Deref(); - pAdd->AddRef(); - m_nodeArray->AddNode((CNode*)pAdd); - } - else if (nFunction == 2) - { - CObject* pRemove = rgparam[0]->Deref(); - m_nodeArray->RemoveNode((CNode*)pRemove); - pRemove->Release(); + case 1: + { + CObject* pItem = rgArgs[0]->Deref(); + pItem->AddRef(); + m_nodeArray->AddNode((CNode*)pItem); + } + break; + + case 2: + { + CObject* pItem = rgArgs[0]->Deref(); + m_nodeArray->RemoveNode((CNode*)pItem); + pItem->Release(); + } + break; } return NULL; @@ -487,11 +477,11 @@ CObject* CNodeArrayObject::Call(int nFunction, CObject** rgparam, int nParam) // "length"(6) / "add"(3) / "remove"(6) -> new CFunctionObject // { m_functionIndex = 0 / 1 / 2, m_owner = this }; this->AddRef(); // else return CObject::Dot(key); -CObject* CNodeArrayObject::Dot(CObject* pObj) +CObject* CNodeArrayObject::Dot(CObject* pKey) { - if (pObj->m_obj != objVariable) + if (pKey->m_obj != objVariable) { - int iItem = (int)((CNumObject*)pObj)->m_value; + int iItem = (int)((CNumObject*)pKey)->m_value; if (iItem < 0 || iItem >= m_nodeArray->GetLength()) { g_pRunner->Error(_T("Array reference out of bounds")); @@ -503,23 +493,29 @@ CObject* CNodeArrayObject::Dot(CObject* pObj) return pItem; } - CVarObject* pName = (CVarObject*)pObj; - int nFunction = -1; - if (pName->m_length == 6 && _tcsncmp(pName->m_text, _T("length"), 6) == 0) - nFunction = 0; - else if (pName->m_length == 3 && _tcsncmp(pName->m_text, _T("add"), 3) == 0) - nFunction = 1; - else if (pName->m_length == 6 && _tcsncmp(pName->m_text, _T("remove"), 6) == 0) - nFunction = 2; + static const struct { const TCHAR* sz; int cch; } c_rgMethod[] = + { + { _T("length"), 6 }, + { _T("add"), 3 }, + { _T("remove"), 6 }, + }; + + CVarObject* pName = (CVarObject*)pKey; + for (int iMethod = 0; iMethod < 3; iMethod++) + { + if (pName->m_length != c_rgMethod[iMethod].cch) + continue; + if (_tcsncmp(pName->m_text, c_rgMethod[iMethod].sz, c_rgMethod[iMethod].cch) != 0) + continue; - if (nFunction < 0) - return CObject::Dot(pObj); + CFunctionObject* pRef = new CFunctionObject; + pRef->m_functionIndex = iMethod; + pRef->m_owner = this; + AddRef(); + return pRef; + } - CFunctionObject* pRef = new CFunctionObject; - pRef->m_functionIndex = nFunction; - pRef->m_owner = this; - AddRef(); - return pRef; + return CObject::Dot(pKey); } // ========================================================================= @@ -530,11 +526,11 @@ CObject* CNodeArrayObject::Dot(CObject* pObj) // Reconstructed from 5960 retail XBE @ 0x00038a20 and @ 0x00038aa0. Decompile: // ctor: m_node = node; m_pprd = prd; node->AddRef(); // dtor: m_node->Release(); -CProperty::CProperty(CObject* pNode, const PRD* pprd) +CProperty::CProperty(CObject* pOwner, const PRD* pprd) { - m_node = pNode; + m_node = pOwner; m_pprd = pprd; - pNode->AddRef(); + pOwner->AddRef(); } CProperty::~CProperty() @@ -552,40 +548,40 @@ CProperty::~CProperty() // the release so the value can't go away under it. CObject* CProperty::Deref() { - void* pvField = (BYTE*)m_node + (int)m_pprd->pbOffset; - CObject* pValue; + BYTE* pbField = (BYTE*)m_node + (int)m_pprd->pbOffset; + CObject* pWrapped; switch (m_pprd->nType) { case pt_integer: - pValue = new CNumObject((float)*(int*)pvField); + pWrapped = new CNumObject((float)*(int*)pbField); break; case pt_boolean: - pValue = new CNumObject((float)*(bool*)pvField); + pWrapped = new CNumObject((float)*(bool*)pbField); break; case pt_number: - pValue = new CNumObject(*(float*)pvField); + pWrapped = new CNumObject(*(float*)pbField); break; case pt_children: case pt_nodearray: - pValue = new CNodeArrayObject((CNodeArray*)pvField); + pWrapped = new CNodeArrayObject((CNodeArray*)pbField); break; case pt_vec3: - pValue = new CVec3Object(*(D3DXVECTOR3*)pvField); + pWrapped = new CVec3Object(*(D3DXVECTOR3*)pbField); break; case pt_node: - pValue = *(CNode**)pvField; - if (pValue != NULL) - pValue->AddRef(); + pWrapped = *(CNode**)pbField; + if (pWrapped != NULL) + pWrapped->AddRef(); break; case pt_string: - pValue = new CStrObject(*(TCHAR**)pvField); + pWrapped = new CStrObject(*(TCHAR**)pbField); break; default: @@ -594,7 +590,7 @@ CObject* CProperty::Deref() } Release(); - return pValue; + return pWrapped; } // CProperty::Assign @@ -607,68 +603,53 @@ CObject* CProperty::Deref() // node: p = value ? value->Deref() : NULL; SetProperty(prd, &p); // string: s = value->Deref()->ToStr(); SetProperty(prd, &s->m_text); s->Release(); // "assignement" is the binary's spelling. Theseus passes the byte count to -// SetProperty and traces when a vec3 gets something else. -void CProperty::Assign(CObject* pObject) +// SetProperty. +void CProperty::Assign(CObject* pValue) { - if (pObject == NULL && m_pprd->nType != pt_node) + const PROP_TYPE type = m_pprd->nType; + if (pValue == NULL && type != pt_node) { g_pRunner->Error(_T("Illegal null assignement")); return; } - switch (m_pprd->nType) + if (type == pt_integer || type == pt_boolean || type == pt_number) { - case pt_integer: - { - CNumObject* pNum = pObject->Deref()->ToNum(); - int nValue = (int)pNum->m_value; - m_node->SetProperty(m_pprd, &nValue, sizeof(int)); - pNum->Release(); - } - break; - - case pt_boolean: + CNumObject* pNumber = pValue->Deref()->ToNum(); + float n = pNumber->m_value; + if (type == pt_integer) { - CNumObject* pNum = pObject->Deref()->ToNum(); - bool bValue = pNum->m_value != 0.0f; - m_node->SetProperty(m_pprd, &bValue, sizeof(bool)); - pNum->Release(); + int i = (int)n; + m_node->SetProperty(m_pprd, &i, sizeof(i)); } - break; - - case pt_number: - { - CNumObject* pNum = pObject->Deref()->ToNum(); - m_node->SetProperty(m_pprd, &pNum->m_value, sizeof(float)); - pNum->Release(); - } - break; - - case pt_vec3: - { - CVec3Object* pVec = (CVec3Object*)pObject->Deref(); - if (pVec->m_obj == objVec3) - m_node->SetProperty(m_pprd, &pVec->m_x, sizeof(float) * 3); - else - TRACE(_T("\001Expected a Vector3 object!\n")); - } - break; - - case pt_node: + else if (type == pt_boolean) { - CNode* pNode = (pObject != NULL) ? (CNode*)pObject->Deref() : NULL; - m_node->SetProperty(m_pprd, &pNode, sizeof(CNode*)); + bool b = (n != 0.0f); + m_node->SetProperty(m_pprd, &b, sizeof(b)); } - break; - - case pt_string: + else { - CStrObject* pStr = pObject->Deref()->ToStr(); - const TCHAR* szValue = pStr->GetSz(); - m_node->SetProperty(m_pprd, &szValue, pStr->GetLength()); - pStr->Release(); + m_node->SetProperty(m_pprd, &n, sizeof(n)); } - break; + pNumber->Release(); + } + else if (type == pt_vec3) + { + CObject* pVec = pValue->Deref(); + if (pVec->m_obj == objVec3) + m_node->SetProperty(m_pprd, &((CVec3Object*)pVec)->m_x, 3 * sizeof(float)); + } + else if (type == pt_node) + { + CObject* pTarget = (pValue == NULL) ? NULL : pValue->Deref(); + m_node->SetProperty(m_pprd, &pTarget, sizeof(pTarget)); + } + else if (type == pt_string) + { + CStrObject* pText = pValue->Deref()->ToStr(); + const TCHAR* szText = pText->GetSz(); + m_node->SetProperty(m_pprd, &szText, pText->GetLength()); + pText->Release(); } } @@ -676,9 +657,9 @@ void CProperty::Assign(CObject* pObject) // Reconstructed from 5960 retail XBE @ 0x00038f30 and @ 0x00038f40. Decompile: // Dot: return CObject::Dot(key); // ToStr: return new CStrObject(GetNodeClass()->m_className); -CObject* CNode::Dot(CObject* pObj) +CObject* CNode::Dot(CObject* pKey) { - return CObject::Dot(pObj); + return CObject::Dot(pKey); } CStrObject* CNode::ToStr() @@ -694,16 +675,17 @@ CStrObject* CNode::ToStr() // Reconstructed from 5960 retail XBE @ 0x000372e0. Decompile: // while (p) { q = p->Deref(); if (q == p) break; p = q; } return p; // Keeps following references until something answers with itself. -CObject* Dereference(CObject* pObject) +CObject* Dereference(CObject* p) { - while (pObject != NULL) + for (;;) { - CObject* pNext = pObject->Deref(); - if (pNext == pObject) - break; - pObject = pNext; + if (p == NULL) + return NULL; + CObject* pNext = p->Deref(); + if (pNext == p) + return p; + p = pNext; } - return pObject; } // LookupClass @@ -725,10 +707,10 @@ CClass* LookupClass(const TCHAR* pchName, int cchName) // nc = CNodeClass::FindByName(name); if (nc && (o = nc->CreateNode())) return o; // return 0; // The constructor args ride in g_rgParam / g_nParam for Date, Array and friends. -CObject* CreateNewObject(const TCHAR* pchClassName, int cchClassName, CObject** rgparam, int nParam) +CObject* CreateNewObject(const TCHAR* pchClassName, int cchClassName, CObject** rgArgs, int nArgs) { - g_rgParam = rgparam; - g_nParam = nParam; + g_rgParam = rgArgs; + g_nParam = nArgs; CClass* pScripted = LookupClass(pchClassName, cchClassName); if (pScripted != NULL) @@ -745,18 +727,17 @@ CObject* CreateNewObject(const TCHAR* pchClassName, int cchClassName, CObject** // delete f; return true; bool ExecuteScript(CObject* pObject, const TCHAR* szScript) { - CFunctionCompiler compiler; - compiler.ParseBlock(szScript); - if (compiler.HadError()) + CFunctionCompiler fc; + fc.ParseBlock(szScript); + if (fc.HadError()) return false; - compiler.Write(opRet); - CFunction* pScript = compiler.CreateFunction(); - - CRunner runner(pObject); - runner.SetFunc(pScript); - runner.Run(); + fc.Write(opRet); + CFunction* pScript = fc.CreateFunction(); + CRunner vm(pObject); + vm.SetFunc(pScript); + vm.Run(); delete pScript; return true; } @@ -768,18 +749,18 @@ bool ExecuteScript(CObject* pObject, const TCHAR* szScript) // for (i = 0; i < n; i++) r.m_stack[r.m_sp++] = rgParam[i]; // r.Run(); return true; // The arguments go on the stack as is; the caller's references move with them. -bool CallFunction(CObject* pObject, const TCHAR* szFunc, int nParam, CObject** rgParam) +bool CallFunction(CObject* pObject, const TCHAR* szFunc, int nArgs, CObject** rgArgs) { g_pThis = pObject; - CRunner runner(pObject); - if (!runner.SetFunc(szFunc)) + CRunner vm(pObject); + if (!vm.SetFunc(szFunc)) return false; - for (int i = 0; i < nParam; i++) - runner.Push(rgParam[i]); + for (int iArg = 0; iArg < nArgs; iArg++) + vm.Push(rgArgs[iArg]); - runner.Run(); + vm.Run(); return true; } @@ -811,40 +792,29 @@ CObject* FindMember(CNodeClass* pClass, const TCHAR* pchName, int cchName) // return 0; // Order is: node property, own members, class chain, then each ancestor. // The CProperty starts at zero refs; the caller's AddRef is the first one. -CObject* LookupMember(CObject* pThis, const TCHAR* pchName, int cchName) +CObject* LookupMember(CObject* pObj, const TCHAR* pchName, int cchName) { - ASSERT(pThis != NULL); - - g_pThis = pThis; + g_pThis = pObj; - if (pThis->m_obj == objNode) + if (pObj->m_obj == objNode) { - CNode* pNode = (CNode*)pThis; - const PRD* pprd = pNode->FindProp(pchName, cchName); + const PRD* pprd = pObj->FindProp(pchName, cchName); if (pprd != NULL) { - CObject* pProp = new CProperty(pNode, pprd); + CProperty* pProp = new CProperty(pObj, pprd); pProp->m_nRefCount = 0; return pProp; } } - CObject* pFound = pThis->GetMember(pchName, cchName); - if (pFound != NULL) - return pFound; + CObject* pFound = pObj->GetMember(pchName, cchName); + if (pFound == NULL) + pFound = FindMember(pObj->GetNodeClass(), pchName, cchName); - pFound = FindMember(pThis->GetNodeClass(), pchName, cchName); - if (pFound != NULL) - return pFound; + for (CObject* pUp = pObj->m_parent; pFound == NULL && pUp != NULL; pUp = pUp->m_parent) + pFound = LookupMember(pUp, pchName, cchName); - for (CObject* pAncestor = pThis->m_parent; pAncestor != NULL; pAncestor = pAncestor->m_parent) - { - pFound = LookupMember(pAncestor, pchName, cchName); - if (pFound != NULL) - return pFound; - } - - return NULL; + return pFound; } // ========================================================================= @@ -860,22 +830,20 @@ CObject* LookupMember(CObject* pThis, const TCHAR* pchName, int cchName) // and torn down with ResetFunc. 5960's stack holds 100 entries; Theseus's // holds 256 (see runner.h). Theseus also starts the line at 0 so an error // before the first statement doesn't print a junk line number. -CRunner::CRunner(CObject* pObject) +CRunner::CRunner(CObject* pOwner) { - ASSERT(pObject != NULL); - - m_nLine = 0; - m_wakeup = 0.0f; - m_nop = 0; - m_ops = NULL; + memset(m_stack, 0, sizeof(m_stack)); m_sp = 0; m_spFrame = 0; m_spBase = 0; - m_self = pObject; - m_thisRef = pObject; - m_bError = false; + m_ops = NULL; + m_nop = 0; + m_self = pOwner; + m_thisRef = pOwner; m_nextContext = NULL; - ZeroMemory(m_stack, sizeof(m_stack)); + m_bError = false; + m_nLine = 0; + m_wakeup = 0.0f; } CRunner::~CRunner() @@ -890,8 +858,9 @@ CRunner::~CRunner() bool CRunner::SetFunc(const TCHAR* szFunc) { ResetFunc(); - m_ops = LookupFunction(szFunc, _tcslen(szFunc), m_thisRef); - return m_ops != NULL; + BYTE* pbCode = LookupFunction(szFunc, _tcslen(szFunc), m_thisRef); + m_ops = pbCode; + return pbCode != NULL; } void CRunner::SetFunc(CFunction* pFunction) @@ -913,9 +882,7 @@ void CRunner::ResetFunc() { while (m_sp != 0) { - m_sp--; - CObject* pSlot = m_stack[m_sp]; - m_stack[m_sp] = NULL; + CObject* pSlot = Pop(); if (pSlot != NULL) pSlot->Release(); } @@ -923,7 +890,6 @@ void CRunner::ResetFunc() m_bError = false; m_nop = 0; m_wakeup = 0.0f; - m_sp = 0; m_spFrame = 0; } @@ -932,43 +898,46 @@ void CRunner::ResetFunc() // 0x37510 (string). Decompile: // m_stack[m_sp++] = new CNumObject(n); // if (cch == -1) cch = wcslen(sz); m_stack[m_sp++] = new CStrObject(sz, cch); -// The object form is inlined everywhere as m_stack[m_sp++] = p. Theseus -// refuses to run off the end of the stack. -void CRunner::Push(float nValue) +// The object form is inlined everywhere as m_stack[m_sp++] = p. 5960 never +// checks the top; Theseus drops the push instead of running off the end. +void CRunner::Push(float n) { - Push(new CNumObject(nValue)); + Push(new CNumObject(n)); } -void CRunner::Push(int nValue) +void CRunner::Push(int n) { - Push(new CNumObject((float)nValue)); + Push(new CNumObject((float)n)); } -void CRunner::Push(const TCHAR* szValue, int nLen) +void CRunner::Push(const TCHAR* pch, int cch) { - if (nLen == -1) - nLen = _tcslen(szValue); - Push(new CStrObject(szValue, nLen)); + if (cch == -1) + cch = _tcslen(pch); + Push(new CStrObject(pch, cch)); } void CRunner::Push(CObject* pObject) { - if (m_sp >= countof(m_stack)) + if (!CHECK(m_sp < countof(m_stack))) { - TRACE(_T("\001Script stack overflow!\n")); - pObject->Release(); + if (pObject != NULL) + pObject->Release(); return; } - m_stack[m_sp++] = pObject; + m_stack[m_sp] = pObject; + m_sp++; } // CRunner::Pop [inlined in 5960 as p = m_stack[--m_sp]; m_stack[m_sp] = 0] +// Theseus refuses to pop an empty stack. CObject* CRunner::Pop() { - ASSERT(m_sp > 0); - m_sp--; - CObject* pTop = m_stack[m_sp]; - m_stack[m_sp] = NULL; + if (!CHECK(m_sp != 0)) + return NULL; + CObject*& rTop = m_stack[--m_sp]; + CObject* pTop = rTop; + rTop = NULL; return pTop; } @@ -976,41 +945,37 @@ CObject* CRunner::Pop() // Reconstructed from 5960 retail XBE @ 0x000375e0. Decompile: // if (m_bError) return; // vswprintf(msg, 512, fmt, args); -// if (m_nLine == 0) swprintf(out, , msg); -// else swprintf(out, , m_nLine, msg); +// if (m_nLine == 0) swprintf(out, L"###...\n###Runtime Error\n\n%s\n###...", msg); +// else swprintf(out, L"###...\n###Runtime Error\n\nLine: %d\n\n%s\n###...", m_nLine, msg); // m_bError = 1; // Only the first error of a run is reported; Run stops on the flag. Retail -// builds the text and shows it nowhere. Theseus traces it, dumps the stack -// in debug, and pops a message box on desktop. +// builds the text and shows it nowhere. Theseus keeps the core of that text +// without the # banners, sends it to the trace output with the house error +// marker, and pops a message box on desktop. void CRunner::Error(const TCHAR* szFmt, ...) { if (m_bError) return; + m_bError = true; - TCHAR szDetail[512]; + TCHAR szWhat[512]; va_list args; va_start(args, szFmt); - _vsntprintf(szDetail, countof(szDetail), szFmt, args); + _vsntprintf(szWhat, countof(szWhat), szFmt, args); va_end(args); - TCHAR szMessage[1024]; - if (m_nLine != 0) - _stprintf(szMessage, _T("Runtime Error\n\nLine: %d\n\n%s"), m_nLine, szDetail); + TCHAR szReport[1024]; + if (m_nLine == 0) + _stprintf(szReport, _T("Runtime Error\n\n%s"), szWhat); else - _stprintf(szMessage, _T("Runtime Error\n\n%s"), szDetail); - - Trace(_T("\001\007%s\n"), szMessage); + _stprintf(szReport, _T("Runtime Error\n\nLine: %d\n\n%s"), m_nLine, szWhat); -#ifdef _DEBUG - DumpStack(); -#endif + Trace(_T("\001\007%s\n"), szReport); #ifdef THESEUS_DESKTOP extern int TheseusMessageBox(const char* szText, unsigned int uType); - TheseusMessageBox(szMessage, 0); + TheseusMessageBox(szReport, 0); #endif - - m_bError = true; } // CRunner::PushContext @@ -1019,19 +984,33 @@ void CRunner::Error(const TCHAR* szFmt, ...) // c->m_spFrame = m_spFrame; c->m_self = m_self; c->m_thisRef = m_thisRef; // c->m_sp = m_sp - nParam - 1; c->m_next = m_nextContext; m_nextContext = c; // The saved sp sits under the callee and its arguments, so returning drops both. -void CRunner::PushContext(UINT nParam) +void CRunner::PushContext(UINT nArgs) { RUNCONTEXT* pSaved = new RUNCONTEXT; + pSaved->m_nextContext = m_nextContext; pSaved->m_ops = m_ops; pSaved->m_nop = m_nop; pSaved->m_spFrame = m_spFrame; pSaved->m_self = m_self; pSaved->m_thisRef = m_thisRef; - pSaved->m_sp = m_sp - nParam - 1; - pSaved->m_nextContext = m_nextContext; + pSaved->m_sp = m_sp - (nArgs + 1); m_nextContext = pSaved; } +// Releases and clears a run of stack slots, bottom first. 5960 writes this +// loop out inline in PopContext, opNew and opCall. +static void ReleaseRange(CObject** rgSlot, UINT cSlot) +{ + for (UINT iSlot = 0; iSlot < cSlot; iSlot++) + { + if (rgSlot[iSlot] != NULL) + { + rgSlot[iSlot]->Release(); + rgSlot[iSlot] = NULL; + } + } +} + // CRunner::PopContext // Reconstructed from 5960 retail XBE @ 0x000376e0. Decompile: // c = m_nextContext; ret = Dereference(Pop()); @@ -1041,8 +1020,6 @@ void CRunner::PushContext(UINT nParam) void CRunner::PopContext() { RUNCONTEXT* pSaved = m_nextContext; - ASSERT(pSaved != NULL); - CObject* pResult = Dereference(Pop()); m_ops = pSaved->m_ops; @@ -1050,18 +1027,11 @@ void CRunner::PopContext() m_spFrame = pSaved->m_spFrame; m_self = pSaved->m_self; m_thisRef = pSaved->m_thisRef; - g_pThis = m_thisRef; - - for (UINT iSlot = pSaved->m_sp; iSlot < m_sp; iSlot++) - { - if (m_stack[iSlot] != NULL) - { - m_stack[iSlot]->Release(); - m_stack[iSlot] = NULL; - } - } + g_pThis = pSaved->m_thisRef; + ReleaseRange(&m_stack[pSaved->m_sp], m_sp - pSaved->m_sp); m_sp = pSaved->m_sp; + m_nextContext = pSaved->m_nextContext; delete pSaved; @@ -1072,24 +1042,16 @@ void CRunner::PopContext() // Reconstructed from 5960 retail XBE @ 0x000377b0. Decompile: // m_spBase = m_sp; // while (!m_bError) if (!Step(&ret)) break; -// Theseus hands back whatever the outermost return left (NULL on error) and -// checks in debug that nothing above the stack top was left behind. +// Theseus hands back whatever the outermost return left (NULL on error). CObject* CRunner::Run() { m_spBase = m_sp; - while (!m_bError) - { -#ifdef _DEBUG - for (UINT i = 0; i < 20; i++) - ASSERT(m_stack[m_sp + i] == NULL); -#endif - CObject* pResult = NULL; - if (!Step(&pResult)) - return pResult; - } + CObject* pResult = NULL; + while (!m_bError && Step(&pResult)) + pResult = NULL; - return NULL; + return m_bError ? NULL : pResult; } // ========================================================================= @@ -1101,50 +1063,44 @@ CObject* CRunner::Run() // the opcode byte). Sets g_pRunner, fetches the opcode and runs it; returns // false to stop (outermost return, error, or an opcode it doesn't know). // Each case is described next to its code below; the full write-up with the -// decompile excerpts is in docs/decomp/VM.md. Theseus adds the stack size -// check on opFrame and the null check on opCall. +// decompile excerpts is in docs/decomp/VM.md. Theseus adds the stack checks +// on opFrame and opCall and the null check on opCall. bool CRunner::Step(CObject** ppRetObj) { g_pRunner = this; - BYTE op = m_ops[m_nop++]; - switch (op) + const BYTE opcode = m_ops[m_nop]; + m_nop++; + + switch (opcode) { // Push null. case opNull: Push((CObject*)NULL); return true; - // int nParam, string class. Build it with the arguments sitting on the + // int nArgs, string class. Build it with the arguments sitting on the // stack, release them, and leave the new object in their place. case opNew: { - int nParam = FetchInt(); + int nArgs = FetchInt(); int cchClass; const TCHAR* pchClass = FetchString(cchClass); - CObject** rgparam = &m_stack[m_sp - nParam]; + UINT spArgs = m_sp - nArgs; - CObject* pNew = CreateNewObject(pchClass, cchClass, rgparam, nParam); + CObject* pNew = CreateNewObject(pchClass, cchClass, &m_stack[spArgs], nArgs); if (pNew == NULL) { TCHAR szClass[256]; - if (cchClass > 255) - cchClass = 255; - CopyChars(szClass, pchClass, cchClass); - szClass[cchClass] = '\0'; + int cchShown = (cchClass < 255) ? cchClass : 255; + memcpy(szClass, pchClass, cchShown * sizeof(TCHAR)); + szClass[cchShown] = '\0'; Error(_T("Failed to create new object: \"%s\""), szClass); return false; } - for (int i = 0; i < nParam; i++) - { - if (rgparam[i] != NULL) - { - rgparam[i]->Release(); - rgparam[i] = NULL; - } - } - m_sp -= nParam; + ReleaseRange(&m_stack[spArgs], nArgs); + m_sp = spArgs; Push(pNew); } return true; @@ -1167,34 +1123,38 @@ bool CRunner::Step(CObject** ppRetObj) // or null jumps. case opCond: { - bool bTrue = false; CObject* pTest = Dereference(Pop()); + bool bTaken = false; if (pTest != NULL) { - CNumObject* pNum = pTest->ToNum(); - bTrue = pNum->m_value != 0.0f; - pNum->Release(); + CNumObject* pAsNum = pTest->ToNum(); + bTaken = (pAsNum->m_value != 0.0f); + pAsNum->Release(); pTest->Release(); } - if (bTrue) - m_nop += sizeof(UINT); - else - m_nop = FetchUInt(); + UINT nopElse = FetchUInt(); + if (!bTaken) + m_nop = nopElse; } return true; - // byte nParam. The callee sits under its arguments. A native function + // byte nArgs. The callee sits under its arguments. A native function // ref runs right here; a bound method or a named script function gets a // new frame; a bare name tries the built-ins first, then the script // function lookup. case opCall: { - int nParam = m_ops[m_nop++]; - ASSERT(m_sp >= (UINT)(nParam + 1)); - CObject** ppCallee = &m_stack[m_sp - nParam - 1]; - CObject** rgparam = &m_stack[m_sp - nParam]; - CObject* pCallee = *ppCallee; + int nArgs = m_ops[m_nop]; + m_nop++; + + // Theseus: not enough on the stack is a stop, not a wild read. + if (!CHECK(m_sp > (UINT)nArgs)) + return false; + + UINT spCallee = m_sp - nArgs - 1; + CObject* pCallee = m_stack[spCallee]; + CObject** rgArgs = &m_stack[spCallee + 1]; // Theseus: calling null is an error instead of a crash. if (pCallee == NULL) @@ -1205,19 +1165,12 @@ bool CRunner::Step(CObject** ppRetObj) if (pCallee->m_obj == objFunctionRef) { - *ppCallee = NULL; - CObject* pResult = ((CFunctionObject*)pCallee)->Call(rgparam, nParam); + m_stack[spCallee] = NULL; + CObject* pResult = ((CFunctionObject*)pCallee)->Call(rgArgs, nArgs); pCallee->Release(); - for (int i = 0; i < nParam; i++) - { - if (rgparam[i] != NULL) - { - rgparam[i]->Release(); - rgparam[i] = NULL; - } - } - m_sp -= nParam + 1; + ReleaseRange(rgArgs, nArgs); + m_sp = spCallee; Push(pResult); return true; } @@ -1226,9 +1179,8 @@ bool CRunner::Step(CObject** ppRetObj) { CMemberFunctionObject* pMethod = (CMemberFunctionObject*)pCallee; pMethod->AddRef(); - PushContext(nParam); - m_spFrame = m_sp - nParam; - ASSERT(pMethod->m_owner != NULL); + PushContext(nArgs); + m_spFrame = spCallee + 1; m_self = pMethod->m_owner; m_ops = pMethod->m_function->m_rgop; m_nop = 0; @@ -1237,11 +1189,12 @@ bool CRunner::Step(CObject** ppRetObj) CVarObject* pName = (CVarObject*)pCallee; CObject* pResult = NULL; - if (ExecuteBuiltIn(pName->m_text, pName->m_length, nParam, rgparam, pResult)) + if (ExecuteBuiltIn(pName->m_text, pName->m_length, nArgs, rgArgs, pResult)) { - for (int i = 0; i < nParam; i++) - rgparam[i] = NULL; - m_sp -= nParam + 1; + // The built-in consumed the argument references. + memset(rgArgs, 0, nArgs * sizeof(CObject*)); + m_stack[spCallee] = NULL; + m_sp = spCallee; Push(pResult); pCallee->Release(); return true; @@ -1252,19 +1205,17 @@ bool CRunner::Step(CObject** ppRetObj) if (pbCode == NULL) { TCHAR szName[32]; - int cchName = pName->m_length; - if (cchName > 31) - cchName = 31; - _tcsncpy(szName, pName->m_text, cchName); - szName[cchName] = '\0'; + int cchShown = (pName->m_length < 31) ? pName->m_length : 31; + _tcsncpy(szName, pName->m_text, cchShown); + szName[cchShown] = '\0'; Error(_T("unknown function: %s"), szName); return false; } - PushContext(nParam); + PushContext(nArgs); m_self = pOwner; g_pThis = pOwner; - m_spFrame = m_sp - nParam; + m_spFrame = spCallee + 1; m_ops = pbCode; m_nop = 0; } @@ -1273,14 +1224,14 @@ bool CRunner::Step(CObject** ppRetObj) // Return. Inside a call, restore the caller and leave the result on its // stack. At the outermost level, hand back the top of stack and stop. case opRet: - if (m_nextContext != NULL) + if (m_nextContext == NULL) { - PopContext(); - return true; + if (ppRetObj != NULL) + *ppRetObj = (m_sp == 0) ? NULL : Pop(); + return false; } - if (ppRetObj != NULL) - *ppRetObj = (m_sp != 0) ? Pop() : NULL; - return false; + PopContext(); + return true; // string literal. case opStr: @@ -1296,10 +1247,11 @@ bool CRunner::Step(CObject** ppRetObj) Push(FetchFloat()); return true; - case opAdd: case opSub: case opMul: case opDiv: case opMod: - case opEQ: case opNE: case opLT: case opGT: case opLE: case opGE: - case opSHL: case opSHR: case opAnd: case opOr: case opXor: - return BinaryOperator(op); + case opAdd: case opSub: case opMul: case opDiv: + case opEQ: case opNE: case opLT: case opGT: case opLE: case opGE: + case opMod: case opSHL: case opSHR: + case opAnd: case opOr: case opXor: + return BinaryOperator(opcode); // a.b pushes a then the name b; a[i] pushes a then i, which gets // resolved first. Either way the left side is resolved one step and @@ -1309,17 +1261,15 @@ bool CRunner::Step(CObject** ppRetObj) { CObject* pKey = Pop(); CObject* pBase = Pop(); - if (pKey == NULL || pBase == NULL) + if (pBase == NULL || pKey == NULL) { Error(_T("illegal null reference")); return false; } - if (op == opArray) + if (opcode == opArray) pKey = Dereference(pKey); - ASSERT(pKey->m_obj == objVariable || pKey->m_obj == objNumber || pKey->m_obj == objString); - CObject* pTarget = pBase->Deref(); if (pTarget == NULL) { @@ -1367,20 +1317,16 @@ bool CRunner::Step(CObject** ppRetObj) // int index. Push the local's slot, creating it on first use. case opLocal: { - int iLocal = FetchInt(); - ASSERT(m_spFrame + iLocal < m_sp); - - CObject*& pSlot = m_stack[m_spFrame + iLocal]; - if (pSlot == NULL) - pSlot = new CLocalVariable; - pSlot->AddRef(); - Push(pSlot); + CObject*& rSlot = m_stack[m_spFrame + FetchInt()]; + if (rSlot == NULL) + rSlot = new CLocalVariable; + rSlot->AddRef(); + Push(rSlot); } return true; // Push the current self. case opThis: - ASSERT(m_self != NULL); m_self->AddRef(); Push(m_self); return true; @@ -1397,10 +1343,6 @@ bool CRunner::Step(CObject** ppRetObj) return false; } -#ifdef _DEBUG - for (UINT i = m_sp; i < m_sp + nLocals; i++) - ASSERT(m_stack[i] == NULL); -#endif m_sp += nLocals; m_spBase = m_sp; } @@ -1421,8 +1363,9 @@ bool CRunner::Step(CObject** ppRetObj) return false; } // 5960 computes 0.0 - v here, which gives +0 for 0; Theseus keeps -v. - Push(-((CNumObject*)pValue)->m_value); + float n = ((CNumObject*)pValue)->m_value; pValue->Release(); + Push(-n); } return true; @@ -1438,15 +1381,11 @@ bool CRunner::Step(CObject** ppRetObj) // int count. Release that many locals off the top. case opEndFrame: { - int nLocals = FetchInt(); - for ( ; nLocals > 0; nLocals--) + for (int nLocals = FetchInt(); nLocals > 0; nLocals--) { - m_sp--; - if (m_stack[m_sp] != NULL) - { - m_stack[m_sp]->Release(); - m_stack[m_sp] = NULL; - } + CObject* pLocal = Pop(); + if (pLocal != NULL) + pLocal->Release(); } m_spBase = m_sp; } @@ -1467,14 +1406,13 @@ bool CRunner::Step(CObject** ppRetObj) Error(_T("expected a number")); return false; } - m_wakeup = TheseusGetNow() + ((CNumObject*)pDelay)->m_value; + m_wakeup = ((CNumObject*)pDelay)->m_value + TheseusGetNow(); pDelay->Release(); } return true; } - // Lost in space. - ASSERT(FALSE); + // Any other byte stops the runner. return false; } @@ -1494,34 +1432,36 @@ bool CRunner::Step(CObject** ppRetObj) // - otherwise + concatenates the two ToStr forms, and anything else is // "type mismatch error". // Full write-up in docs/decomp/VM.md. Theseus keeps its own integer -// conversion for the bit ops and adds a whole-number check in debug. +// conversion for the bit ops. bool CRunner::BinaryOperator(BYTE op) { - CObject* pRight = Dereference(Pop()); - CObject* pLeft = Dereference(Pop()); + CObject* pB = Dereference(Pop()); + CObject* pA = Dereference(Pop()); - if (pLeft == NULL || pRight == NULL || (pLeft->m_obj == objNode && pRight->m_obj == objNode)) + if (pA == NULL || pB == NULL || (pA->m_obj == objNode && pB->m_obj == objNode)) { - if (op == opEQ) - Push(pLeft == pRight ? 1.0f : 0.0f); - else if (op == opNE) - Push(pLeft != pRight ? 1.0f : 0.0f); - else + if (op != opEQ && op != opNE) { Error(_T("illegal null reference")); return false; } - if (pRight != NULL) - pRight->Release(); - if (pLeft != NULL) - pLeft->Release(); + bool bSame = (pA == pB); + Push((op == opEQ) == bSame ? 1.0f : 0.0f); + + if (pB != NULL) + pB->Release(); + if (pA != NULL) + pA->Release(); return true; } - if (op != opAdd && pLeft->m_obj == objString && pRight->m_obj == objString) + const int typeA = pA->m_obj; + const int typeB = pB->m_obj; + + if (typeA == objString && typeB == objString && op != opAdd) { - int nOrder = _tcscmp(((CStrObject*)pLeft)->GetSz(), ((CStrObject*)pRight)->GetSz()); + int nOrder = _tcscmp(((CStrObject*)pA)->GetSz(), ((CStrObject*)pB)->GetSz()); bool bResult; switch (op) @@ -1538,25 +1478,12 @@ bool CRunner::BinaryOperator(BYTE op) } Push(bResult); } - else if (pLeft->m_obj == objNumber && pRight->m_obj == objNumber) + else if (typeA == objNumber && typeB == objNumber) { - float a = ((CNumObject*)pLeft)->m_value; - float b = ((CNumObject*)pRight)->m_value; + float a = ((CNumObject*)pA)->m_value; + float b = ((CNumObject*)pB)->m_value; float fResult; -#ifdef _DEBUG - // Theseus: bit ops on fractions are a type mismatch in debug builds. - if (op == opAnd || op == opOr || op == opXor) - { - double whole; - if (modf(a, &whole) != 0 || modf(b, &whole) != 0) - { - Error(_T("type mismatch")); - return false; - } - } -#endif - switch (op) { case opAdd: fResult = a + b; break; @@ -1584,11 +1511,11 @@ bool CRunner::BinaryOperator(BYTE op) } else if (op == opAdd) { - CStrObject* pLeftText = pLeft->ToStr(); - CStrObject* pRightText = pRight->ToStr(); - Push(pLeftText->concat(pRightText->GetSz())); - pLeftText->Release(); - pRightText->Release(); + CStrObject* pTextA = pA->ToStr(); + CStrObject* pTextB = pB->ToStr(); + Push(pTextA->concat(pTextB->GetSz())); + pTextA->Release(); + pTextB->Release(); } else { @@ -1596,8 +1523,8 @@ bool CRunner::BinaryOperator(BYTE op) return false; } - pLeft->Release(); - pRight->Release(); + pA->Release(); + pB->Release(); return true; } @@ -1613,78 +1540,77 @@ bool CRunner::BinaryOperator(BYTE op) // launch(p [,d]) d defaults to p up to its last \ or /, then // CleanFilePath both and XWriteTitleInfoAndReboot else "Bad launch" // DebugBreak() nothing in retail -// alert(s) debug print of s else "Bad alert" +// alert(s) debug print of s with "%s" else "Bad alert" // log(s) nothing in retail else "Bad log!" -// Theseus traces log(), routes alert() through Alert, launches through -// the desktop launcher on desktop, and breaks into the debugger in debug. -BOOL CRunner::ExecuteBuiltIn(const TCHAR* pchName, int cchName, int nParam, CObject** rgParam, CObject*& pRetObj) +// Theseus prints log() on desktop, routes alert() through Alert, and +// launches through the desktop launcher on desktop. +static bool IsBuiltIn(const TCHAR* pchName, int cchName, const TCHAR* szBuiltIn) +{ + return cchName == (int)_tcslen(szBuiltIn) && _tcsncmp(pchName, szBuiltIn, cchName) == 0; +} + +BOOL CRunner::ExecuteBuiltIn(const TCHAR* pchName, int cchName, int nArgs, CObject** rgArgs, CObject*& pRetObj) { pRetObj = NULL; - if (cchName == 11 && _tcsncmp(pchName, _T("EnableInput"), 11) == 0) + if (IsBuiltIn(pchName, cchName, _T("EnableInput"))) { - if (nParam != 1) - { + if (nArgs == 1) + g_bInputEnable = (Dereference(rgArgs[0])->ToNum()->m_value != 0.0f); + else Error(_T("Invalid parameter")); - return TRUE; - } - CNumObject* pOn = Dereference(rgParam[0])->ToNum(); - g_bInputEnable = pOn->m_value != 0.0f; return TRUE; } - if (cchName == 4 && _tcsncmp(pchName, _T("eval"), 4) == 0) + if (IsBuiltIn(pchName, cchName, _T("eval"))) { - if (nParam != 1) + if (nArgs == 1) { - Error(_T("Bad launch")); - return TRUE; + CObject* pArg = Dereference(rgArgs[0]); + CStrObject* pScript = pArg->ToStr(); + ExecuteScript(m_self, pScript->GetSz()); + pScript->Release(); + pArg->Release(); } - CObject* pArg = Dereference(rgParam[0]); - CStrObject* pScript = pArg->ToStr(); - ExecuteScript(m_self, pScript->GetSz()); - pScript->Release(); - pArg->Release(); + else + Error(_T("Bad launch")); return TRUE; } - if (cchName == 6 && _tcsncmp(pchName, _T("launch"), 6) == 0) + if (IsBuiltIn(pchName, cchName, _T("launch"))) { - if (nParam != 1 && nParam != 2) + if (nArgs < 1 || nArgs > 2) { Error(_T("Bad launch")); return TRUE; } - CObject* pPathArg = Dereference(rgParam[0]); + CObject* pPathArg = Dereference(rgArgs[0]); CStrObject* pPath = pPathArg->ToStr(); const TCHAR* szPath = pPath->GetSz(); // Theseus: script paths longer than MAX_PATH get cut instead of overrunning. TCHAR szDir[MAX_PATH]; - if (nParam == 2) + szDir[MAX_PATH - 1] = '\0'; + if (nArgs == 1) + { + _tcsncpy(szDir, szPath, MAX_PATH - 1); + TCHAR* pchSep = _tcsrchr(szDir, '/'); + if (pchSep != NULL) + *pchSep = '\\'; + pchSep = _tcsrchr(szDir, '\\'); + // Theseus: a bare file name keeps the whole path instead of writing through NULL. + if (CHECK(pchSep != NULL)) + *pchSep = '\0'; + } + else { - CObject* pDirArg = Dereference(rgParam[1]); + CObject* pDirArg = Dereference(rgArgs[1]); CStrObject* pDir = pDirArg->ToStr(); _tcsncpy(szDir, pDir->GetSz(), MAX_PATH - 1); - szDir[MAX_PATH - 1] = '\0'; pDir->Release(); pDirArg->Release(); } - else - { - _tcsncpy(szDir, szPath, MAX_PATH - 1); - szDir[MAX_PATH - 1] = '\0'; - TCHAR* pchSlash = _tcsrchr(szDir, '/'); - if (pchSlash != NULL) - *pchSlash = '\\'; - pchSlash = _tcsrchr(szDir, '\\'); - // Theseus: a bare file name keeps the whole path instead of writing through NULL. - if (CHECK(pchSlash != NULL)) - *pchSlash = '\0'; - } - - TRACE(_T("Launch title: %s, %s\n"), szPath, szDir); #if defined(_XBOX) FSCHAR szXbe[MAX_PATH]; @@ -1703,39 +1629,26 @@ BOOL CRunner::ExecuteBuiltIn(const TCHAR* pchName, int cchName, int nParam, CObj return TRUE; } - if (cchName == 10 && _tcsncmp(pchName, _T("DebugBreak"), 10) == 0) - { -#ifdef _DEBUG - _CrtDbgBreak(); -#endif + if (IsBuiltIn(pchName, cchName, _T("DebugBreak"))) return TRUE; - } - if (cchName == 5 && _tcsncmp(pchName, _T("alert"), 5) == 0) + if (IsBuiltIn(pchName, cchName, _T("alert")) || IsBuiltIn(pchName, cchName, _T("log"))) { - if (nParam != 1) + bool bAlert = (cchName == 5); + if (nArgs != 1) { - Error(_T("Bad alert")); + Error(bAlert ? _T("Bad alert") : _T("Bad log!")); return TRUE; } - CObject* pArg = Dereference(rgParam[0]); - CStrObject* pText = pArg->ToStr(); - Alert(_T("%s"), pText->GetSz()); - pText->Release(); - pArg->Release(); - return TRUE; - } - if (cchName == 3 && _tcsncmp(pchName, _T("log"), 3) == 0) - { - if (nParam != 1) - { - Error(_T("Bad log!")); - return TRUE; - } - CObject* pArg = Dereference(rgParam[0]); + CObject* pArg = Dereference(rgArgs[0]); CStrObject* pText = pArg->ToStr(); - TRACE(_T("%s\n"), pText->GetSz()); + if (bAlert) + Alert(_T("%s"), pText->GetSz()); +#ifdef THESEUS_DESKTOP + else + Trace(_T("%s\n"), pText->GetSz()); // Theseus: desktop prints log() lines +#endif pText->Release(); pArg->Release(); return TRUE; @@ -1756,8 +1669,7 @@ BOOL CRunner::ExecuteBuiltIn(const TCHAR* pchName, int cchName, int nParam, CObj // if ((m = LookupMember(p, name))) { // owner = p; return m->m_obj == objFunction ? m->m_rgop : (BYTE*)m; } // owner = NULL; return NULL; -// A non-function hit comes back as a raw pointer, as in the binary; Theseus -// asserts on it. +// A non-function hit comes back as a raw pointer, as in the binary. BYTE* CRunner::LookupFunction(const TCHAR* pchName, int cchName, CObject*& pOwner) { pOwner = m_self; @@ -1766,18 +1678,14 @@ BYTE* CRunner::LookupFunction(const TCHAR* pchName, int cchName, CObject*& pOwne if (pOwn != NULL) return pOwn->m_rgop; - for (CObject* pAncestor = m_self->m_parent; pAncestor != NULL; pAncestor = pAncestor->m_parent) + for (CObject* pUp = m_self->m_parent; pUp != NULL; pUp = pUp->m_parent) { - CObject* pFound = LookupMember(pAncestor, pchName, cchName); - if (pFound == NULL) - continue; - - pOwner = pAncestor; - if (pFound->m_obj == objFunction) - return ((CFunction*)pFound)->m_rgop; - - ASSERT(FALSE); - return (BYTE*)pFound; + CObject* pFound = LookupMember(pUp, pchName, cchName); + if (pFound != NULL) + { + pOwner = pUp; + return (pFound->m_obj == objFunction) ? ((CFunction*)pFound)->m_rgop : (BYTE*)pFound; + } } pOwner = NULL; @@ -1793,34 +1701,3 @@ CObject* CRunner::LookupVariable(const TCHAR* pchName, int cchName) pFound = LookupMember(m_thisRef, pchName, cchName); return pFound; } - -#ifdef _DEBUG -// Theseus: debug dump of the runner and its saved frames. -void CRunner::DumpStack() -{ - TRACE(_T("\nStack Trace\n{\n")); - - TRACE(_T("ops: 0x%08x\n"), m_ops); - TRACE(_T("nop: %d\n"), m_nop); - TRACE(_T("spFrame: %d\n"), m_spFrame); - TRACE(_T("sp: %d\n"), m_sp); - TRACE(_T("self:\n")); m_self->Dump(); - TRACE(_T("this:\n")); m_thisRef->Dump(); - - for (RUNCONTEXT* pContext = m_nextContext; pContext != NULL; pContext = pContext->m_nextContext) - { - TRACE(_T("{\nContext: 0x%08x\n"), pContext); - - TRACE(_T("ops: 0x%08x\n"), pContext->m_ops); - TRACE(_T("nop: %d\n"), pContext->m_nop); - TRACE(_T("spFrame: %d\n"), pContext->m_spFrame); - TRACE(_T("sp: %d\n"), pContext->m_sp); - TRACE(_T("self:\n")); pContext->m_self->Dump(); - TRACE(_T("this:\n")); pContext->m_thisRef->Dump(); - - TRACE(_T("}\n")); - } - - TRACE(_T("}\n")); -} -#endif From 0bb634ea3ccfbd786aedd20bc7cba8f04fdaf088 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:24:21 -0500 Subject: [PATCH 053/167] Rebuilt the TMAP palettes from the 5960 binary Wrote GetHSV, the default palette and the Palette node fresh from 0x5f920, 0x5fc30 and 0x5d6a0..0x5da80, and HSV2RGB as a plain hue wheel. The ten recipes and the blend quirks are in the write-up. Every palette, recipe value and color matches bit for bit on the test sweep. --- docs/decomp/TmapSystem.md | 79 ++++++++- theseus/render/tmap_system.cpp | 289 ++++++++++++++++++--------------- 2 files changed, 229 insertions(+), 139 deletions(-) diff --git a/docs/decomp/TmapSystem.md b/docs/decomp/TmapSystem.md index 52a8dc0..c25a7c7 100644 --- a/docs/decomp/TmapSystem.md +++ b/docs/decomp/TmapSystem.md @@ -223,11 +223,82 @@ only on the init cell. `rnd(n)` below is `rand() * (n / 32767)`; the binary stor Styles 9, 10, 13, 16, 17 and 19 call `rand()` per cell, so their fields differ every time they're built. -## Palette Presets (10 styles) +## Palettes -0: Firestorm, 1: Aqua, 2: Purple & Blues, 3: Color Wheel, -4: Bizarro Mystery Unveiled, 5: Bizarro Color Wheel, 6: Dark Rainbow, -7: Ice Nightshade, 8: Mystery Unveiled, 9: Roundabout +### HSV2RGB (`0x0005f7e0`) + +Takes H, S, V on the stack and the output pointer in EBX. `h6 = (H - floor(H)) * 6`, sector +`= ftol(h6)`, `f = h6 - sector`. S and V are clamped to 0..1, then with `v = V * 255` +(`0x0002bf38`), `f` is replaced by `1 - f` on even sectors so one value serves as both +"falling" and "rising" channel: + +```c +m = (int)(v * (1 - S)); n = (int)(v * (1 - S * f)); v = (int)v; +switch (sector) { 1: (n,v,m) 2: (m,v,n) 3: (m,n,v) 4: (n,m,v) 5: (v,m,n) default: (v,n,m) } +``` + +The packer at `0x0005f770` clamps each channel to 0..255 and returns `0x00RRGGBB`. + +### GetHSV (`0x0005f920`) + +Recipe in EAX, outputs through EBX/EDI/ESI (H/S/V), intensity `i` and time `t` on the stack. +Stores the recipe in a global (`0x00175e70`) first; above 9 it stores 0 and runs recipe 0. +`wrap(x)` is `x - floor(x)` (`0x0005f3d0`). Exponents are doubles fed to `CIpow`. + +| Recipe | H | S | V | +|--------|---|---|---| +| 0 | `pow(i, 1.9) * 0.166` | `1` | `pow(i, 0.9)` | +| 1 | `0.45` | `pow(1 - i, 0.3)` | `pow(i, 0.7)` | +| 2 | `pow(i, 1.4) * 0.166 + 0.666` | `(pow(i * 1.8 - 1, 2) + 1) * 0.5` | `pow(i, 0.4)` | +| 3 | `wrap(t * 0.03)` | `1 - pow(i, 2.5) * 0.6` | `i` | +| 4 | `wrap(0.3 - t * 0.003)` | `pow(i, 2.9)` | `pow(i, 0.9)` | +| 5 | `wrap(t * 0.02)` | `1` | `1 - pow(i, 1.4)` | +| 6 | `pow(i, 1.6)` | `1` | `i` | +| 7 | `wrap(pow(0.7 - i * 0.4, 0.6))` | `0.9` | `pow(i, 0.9)` | +| 8 | `wrap(t * 0.002)` | `pow(1 - i, 1.5)` | `pow(i, 0.4)` | +| 9 | `wrap(t * 0.05 + (pow(cos(i * 4), 2) + pow(i, 2)) * 0.2)` | `i * 0.5 + 0.85` | `pow(i, 0.55)` | + +### Default palette (`0x0005fc30`) + +Used when a DynamicTexture has no palette node. State is global: next switch time +(`0x00188f38`), blend start (`0x00188f88`), blend recipe (`0x0017d0fc`) and blend amount +(`0x0017d0f8`); the current recipe is GetHSV's global. + +- When `now > next`: `next = rand() * (5 / 32767) + now + 10.0`, blend recipe = current, + current += 1, amount = 0, start = now. +- 256 entries, `i` stepping by `1/255` (`0x0002becc`). While blending, a second GetHSV for the + blend recipe mixes in as `(1 - amount) * old + amount * new`. That second call also rewrites + the current-recipe global, so from entry 1 on both lookups return the old recipe. +- After the loop `amount += (now - start) * 0.5`, and the blend ends at 1.0. The amount + accumulates on every call instead of tracking elapsed time. + +### Palette node + +| Offset | Field | +|--------|-------| +| 0x1c | type (recipe) | +| 0x20 | changePeriod | +| 0x24 | changePeriodRandomness | +| 0x28 | blend time, 2.0 | +| 0x2c | D3D palette | +| 0x30 | 256 `0x00RRGGBB` entries | +| 0x430 | blend recipe, -1 = none | +| 0x434 | blend amount | +| 0x438 | next change (double) | +| 0x440 | blend start (double) | + +**Advance (`0x0005d7d0`).** When `changePeriod > 0` and the time has passed: +`next = rand() * randomness / 32767 + changePeriod + now`, blend = type, type += 1, +amount = 0, start = now. Then Update, then `amount = (now - start) / blendTime`, ending the +blend at 1.0. + +**Update (`0x0005d8b0`).** Same per-entry loop as the default palette, but copies the +recipe global back into `type` after each lookup (so an out-of-range type becomes 0). Then +creates the D3D palette on first use (inlined `XMemAlloc` of the resource header plus a 0x400 +byte table) and writes every entry with alpha forced to `0xff`. + +**RenderDynamicTexture (`0x0005da80`).** Fills each row `y` with `y * 256 / height`: a ramp +through the whole palette, top to bottom. ## Notes diff --git a/theseus/render/tmap_system.cpp b/theseus/render/tmap_system.cpp index d2309a2..15f2ef1 100644 --- a/theseus/render/tmap_system.cpp +++ b/theseus/render/tmap_system.cpp @@ -182,176 +182,183 @@ void CSurfx::Fade(char* inPix, long inBytesPerRow, long inX, long inY, DeltaFiel // ===== HSV Palette ========================================================== -// 10 palette presets used by the TMAP visualizer system. +// The 10 palette recipes behind every TMAP texture, plus the shared default +// palette used when a DynamicTexture has none of its own. +// Last recipe GetHSV ran; out of range requests come back here as 0. int g_nPalette = -1; -inline void SetRGB(int R, int G, int B, DWORD& rgb) +static inline int Clamp255(int c) { - if (R < 0) R = 0; else if (R > 255) R = 255; - if (G < 0) G = 0; else if (G > 255) G = 255; - if (B < 0) B = 0; else if (B > 255) B = 255; - - rgb = (R << 16) | (G << 8) | (B); + return c < 0 ? 0 : (c > 255 ? 255 : c); } -#define __SET_RGB(R, G, B) \ - SetRGB(R, G, B, outRGB); break; - +// HSV2RGB(H, S, V, outRGB) +// Written from first principles: the standard six sector hue wheel, H wrapped +// to [0, 1), S and V clamped to [0, 1], packed as 0x00RRGGBB. Matches the 5960 +// routine @ 0x0005f7e0, which truncates every channel of V * 255 with +// cvttss2si and clamps through its packer @ 0x0005f770. void HSV2RGB(float H, float S, float V, DWORD& outRGB) { - long hexQuadrant, m, n, v; - H = (H - floorf(H)) * 6.0f; - - hexQuadrant = (long)H; - float f = H - hexQuadrant; - - if (S < 0.0f) S = 0.0f; - if (S > 1.0f) S = 1.0f; - if (V < 0.0f) V = 0.0f; - if (V > 1.0f) V = 1.0f; - - if (!(hexQuadrant & 1)) - f = 1.0f - f; - - V *= 255.0f; - v = (long)V; - m = (long)(V * (1.0f - S)); - n = (long)(V * (1.0f - S * f)); - - switch (hexQuadrant) { - case 1: __SET_RGB(n, v, m); - case 2: __SET_RGB(m, v, n); - case 3: __SET_RGB(m, n, v); - case 4: __SET_RGB(n, m, v); - case 5: __SET_RGB(v, m, n); - default: __SET_RGB(v, n, m); + float h6 = (H - floorf(H)) * 6.0f; + int sector = (int)h6; + float f = h6 - sector; + + S = S < 0.0f ? 0.0f : (S > 1.0f ? 1.0f : S); + V = V < 0.0f ? 0.0f : (V > 1.0f ? 1.0f : V); + + float v255 = V * 255.0f; + int v = (int)v255; + int p = (int)(v255 * (1.0f - S)); + int q = (int)(v255 * (1.0f - S * f)); + int t = (int)(v255 * (1.0f - S * (1.0f - f))); + + int r, g, b; + switch (sector) + { + case 1: r = q; g = v; b = p; break; + case 2: r = p; g = v; b = t; break; + case 3: r = p; g = q; b = v; break; + case 4: r = t; g = p; b = v; break; + case 5: r = v; g = p; b = q; break; + default: r = v; g = t; b = p; break; } + + outRGB = (Clamp255(r) << 16) | (Clamp255(g) << 8) | Clamp255(b); } +// GetHSV(nPalette, i, t, H, S, V) +// Reconstructed from 5960 retail XBE @ 0x0005f920. Maps intensity i (0..1) at +// time t (seconds) to a color for one of 10 recipes; writes the recipe it used +// to g_nPalette (0x00175e70) first, and anything past 9 snaps to 0. The pow +// exponents are the doubles at 0x0002c068..0x0002c0f8; wrap() is the +// x - floor(x) helper @ 0x0005f3d0. See docs/decomp/TmapSystem.md for the table. void GetHSV(int nPalette, float i, float t, float& H, float& S, float& V) { g_nPalette = nPalette; switch (nPalette) { - default: - g_nPalette = 0; - // FALL THROUGH - - case 0: // Firestorm - H = 0.166f * powf(i, 1.9f); - S = 1; - V = powf(i, 0.9f); - break; - - case 1: // Aqua + case 1: H = 0.45f; - S = powf((1.0f - i), 0.3f); + S = powf(1.0f - i, 0.3f); V = powf(i, 0.7f); - break; + return; - case 2: // Purple & Blues - H = 0.666f + 0.166f * powf(i, 1.4f); - S = 0.5f + 0.5f * powf((1.8f * i - 1.0f), 2); + case 2: + H = powf(i, 1.4f) * 0.166f + 0.666f; + S = (powf(i * 1.8f - 1.0f, 2.0f) + 1.0f) * 0.5f; V = powf(i, 0.4f); - break; + return; - case 3: // Color Wheel - H = wrap(0.03f * t); - S = 1.0f - 0.6f * powf(i, 2.5f); + case 3: + H = wrap(t * 0.03f); + S = 1.0f - powf(i, 2.5f) * 0.6f; V = i; - break; + return; - case 4: // Bizarro Mystery Unveiled - H = wrap(0.3f - 0.003f * t); + case 4: + H = wrap(0.3f - t * 0.003f); S = powf(i, 2.9f); V = powf(i, 0.9f); - break; + return; - case 5: // Bizarro Color Wheel - H = wrap(0.02f * t); + case 5: + H = wrap(t * 0.02f); S = 1.0f; V = 1.0f - powf(i, 1.4f); - break; + return; - case 6: // Dark Rainbow + case 6: H = powf(i, 1.6f); S = 1.0f; V = i; - break; + return; - case 7: // Ice Nightshade - H = wrap(powf((0.7f - 0.4f * i), 0.6f)); + case 7: + H = wrap(powf(0.7f - i * 0.4f, 0.6f)); S = 0.9f; V = powf(i, 0.9f); - break; + return; - case 8: // Mystery Unveiled - H = wrap(0.002f * t); - S = powf((1.0f - i), 1.5f); + case 8: + H = wrap(t * 0.002f); + S = powf(1.0f - i, 1.5f); V = powf(i, 0.4f); - break; + return; - case 9: // Roundabout - H = wrap(0.2f * (powf(i, 2.0f) + powf(cosf(4.0f * i), 2.0f)) + (0.05f * t)); - S = 0.85f + 0.5f * i; + case 9: + H = wrap((powf(cosf(i * 4.0f), 2.0f) + powf(i, 2.0f)) * 0.2f + t * 0.05f); + S = i * 0.5f + 0.85f; V = powf(i, 0.55f); - break; + return; + + default: + g_nPalette = 0; + // fall through + case 0: + H = powf(i, 1.9f) * 0.166f; + S = 1.0f; + V = powf(i, 0.9f); + return; } } +// Crossfade from the previous recipe, shared by MakePalette and CPalette. +static inline void BlendHSV(float amount, float H2, float S2, float V2, float& H, float& S, float& V) +{ + H = amount * H + (1.0f - amount) * H2; + S = amount * S + (1.0f - amount) * S2; + V = amount * V + (1.0f - amount) * V2; +} + int g_nBlendPalette; float g_nBlendPaletteAmount; -#define timeToBlend (2.0f) - +// MakePalette(outPalette) +// Reconstructed from 5960 retail XBE @ 0x0005fc30. The default palette: every +// 10 + rnd(5) seconds (0x0002c148, 0x0002c140) it moves on to the next recipe +// and crossfades from the old one. The blend amount grows by half the time +// since the switch on every call (0x0002bed4), so it accumulates rather than +// tracking elapsed time. During a fade the second lookup leaves g_nPalette on +// the old recipe, so from entry 1 on both lookups read it; the binary does the same. void MakePalette(DWORD outPalette [256]) { - static XTIME nextPaletteChangeTime = 0.0f; - static XTIME startBlendTime; + static XTIME nextChange = 0.0f; + static XTIME blendStart; - int i; - float H, S, V, inc = 1.0f / 255.0f; - float mIntensity = 0.0f; XTIME now = TheseusGetNow(); - - if (now > nextPaletteChangeTime) + if (now > nextChange) { - nextPaletteChangeTime = now + 10.0f + rnd(5.0f); + nextChange = now + 10.0f + rnd(5.0f); g_nBlendPalette = g_nPalette; - g_nPalette += 1; + g_nPalette++; g_nBlendPaletteAmount = 0.0f; - startBlendTime = now; + blendStart = now; } - for (i = 0; i < 256; i++, mIntensity += inc) + float intensity = 0.0f; + for (int n = 0; n < 256; n++, intensity += 1.0f / 255.0f) { - GetHSV(g_nPalette, mIntensity, (float) now, H, S, V); - + float H, S, V; + GetHSV(g_nPalette, intensity, (float)now, H, S, V); if (g_nBlendPalette != -1) { float H2, S2, V2; - - GetHSV(g_nBlendPalette, mIntensity, (float) now, H2, S2, V2); - - H = g_nBlendPaletteAmount * H + (1.0f - g_nBlendPaletteAmount) * H2; - S = g_nBlendPaletteAmount * S + (1.0f - g_nBlendPaletteAmount) * S2; - V = g_nBlendPaletteAmount * V + (1.0f - g_nBlendPaletteAmount) * V2; + GetHSV(g_nBlendPalette, intensity, (float)now, H2, S2, V2); + BlendHSV(g_nBlendPaletteAmount, H2, S2, V2, H, S, V); } - - HSV2RGB(H, S, V, outPalette[i]); + HSV2RGB(H, S, V, outPalette[n]); } if (g_nBlendPalette != -1) { - g_nBlendPaletteAmount += (float) (now - startBlendTime) / timeToBlend; + g_nBlendPaletteAmount += (float)(now - blendStart) / 2.0f; if (g_nBlendPaletteAmount >= 1.0f) g_nBlendPalette = -1; } } - // ===== Line Drawing ========================================================= // Bresenham stepping with an optional round pen. Nothing in the dash ever // raises mLineWidth above 1, but the wide pen is there and kept working. @@ -900,8 +907,7 @@ void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& // ===== CPalette ============================================================= -// XAP node that generates a 256-entry DWORD palette from HSV presets. -// On Xbox, also uploads to a hardware D3DPalette object. +// Palette node: one of the 10 recipes, optionally cycling on a timer. class CPalette : public CNode { @@ -944,19 +950,26 @@ START_NODE_PROPS(CPalette, CNode) NODE_PROP(pt_number, CPalette, changePeriodRandomness) END_NODE_PROPS() +// CPalette::CPalette() +// Reconstructed from CPalette_CreateNode @ 0x0005d6a0 (0x448 bytes): +// type = changePeriod = randomness = 0; timeToBlend = 2.0; d3dPalette = NULL; +// blendPalette = -1; nextChange = 0.0 CPalette::CPalette() : + m_type(0), m_changePeriod(0.0f), m_changePeriodRandomness(0.0f), - m_timeToBlend(2.0f), - m_type(0) + m_timeToBlend(2.0f) { - m_nextPaletteChangeTime = 0.0f; m_nBlendPalette = -1; + m_nextPaletteChangeTime = 0.0f; #ifdef _XBOX m_pPalette = NULL; #endif } +// CPalette::~CPalette() +// Reconstructed from 5960 retail XBE @ 0x0005d740: releases the D3D palette +// (the binary inlines D3DResource_Release) before the CNode teardown. CPalette::~CPalette() { #ifdef _XBOX @@ -965,17 +978,24 @@ CPalette::~CPalette() #endif } +// CPalette::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x0005d7d0. Ghidra decompile: +// CNode_Advance(); now = DAT_0017e808; +// if (0.0 < changePeriod && nextChange < now) { +// nextChange = rnd(randomness) + changePeriod + now; +// blendPalette = type; type++; blendAmount = 0; blendStart = now; } +// Update(); +// if (blendPalette != -1 && (blendAmount = (now - blendStart) / timeToBlend) >= 1.0) blendPalette = -1; void CPalette::Advance(float nSeconds) { CNode::Advance(nSeconds); XTIME now = TheseusGetNow(); - if (m_changePeriod > 0.0f && now > m_nextPaletteChangeTime) { m_nextPaletteChangeTime = now + m_changePeriod + rnd(m_changePeriodRandomness); m_nBlendPalette = m_type; - m_type += 1; + m_type++; m_nBlendPaletteAmount = 0.0f; m_startBlendTime = now; } @@ -984,61 +1004,60 @@ void CPalette::Advance(float nSeconds) if (m_nBlendPalette != -1) { - m_nBlendPaletteAmount = (float) (now - m_startBlendTime) / m_timeToBlend; + m_nBlendPaletteAmount = (float)(now - m_startBlendTime) / m_timeToBlend; if (m_nBlendPaletteAmount >= 1.0f) m_nBlendPalette = -1; } } +// CPalette::Update() +// Reconstructed from 5960 retail XBE @ 0x0005d8b0. Fills the 256 entries from +// GetHSV (crossfading from the old recipe while a blend runs) and copies +// g_nPalette back into type after each lookup, so an out of range type wraps +// to 0. Then copies the table into a D3D palette with alpha forced to 0xff, +// creating it on first use (the binary inlines CreatePalette and Lock). +// The palette upload is Xbox only; on desktop the texture upload in +// CDynamicTexture::Update applies the table in software instead. void CPalette::Update() { - XTIME now = TheseusGetNow(); - float H, S, V, inc = 1.0f / 255.0f; - float mIntensity = 0.0f; + float now = (float)TheseusGetNow(); + float intensity = 0.0f; - int i; - for (i = 0; i < 256; i++, mIntensity += inc) + for (int n = 0; n < 256; n++, intensity += 1.0f / 255.0f) { - GetHSV(m_type, mIntensity, (float) now, H, S, V); + float H, S, V; + GetHSV(m_type, intensity, now, H, S, V); m_type = g_nPalette; if (m_nBlendPalette != -1) { float H2, S2, V2; - GetHSV(m_nBlendPalette, mIntensity, (float) now, H2, S2, V2); - - H = m_nBlendPaletteAmount * H + (1.0f - m_nBlendPaletteAmount) * H2; - S = m_nBlendPaletteAmount * S + (1.0f - m_nBlendPaletteAmount) * S2; - V = m_nBlendPaletteAmount * V + (1.0f - m_nBlendPaletteAmount) * V2; + GetHSV(m_nBlendPalette, intensity, now, H2, S2, V2); + BlendHSV(m_nBlendPaletteAmount, H2, S2, V2, H, S, V); } - HSV2RGB(H, S, V, m_palette[i]); + HSV2RGB(H, S, V, m_palette[n]); } #ifdef _XBOX - // Xbox: also upload the DWORD palette into a hardware D3DPalette - // object so SetPalette can bind it. Desktop applies the palette in - // software when rendering each CDynamicTexture instead. if (m_pPalette == NULL) VERIFYHR(TheseusGetD3DDev()->CreatePalette(D3DPALETTE_256, &m_pPalette)); - D3DCOLOR* rgColor; - VERIFYHR(m_pPalette->Lock(&rgColor, D3DLOCK_NOOVERWRITE)); - for (i = 0; i < 256; i += 1) - { - BYTE r = (BYTE)(m_palette[i] >> 16); - BYTE g = (BYTE)(m_palette[i] >> 8); - BYTE b = (BYTE)m_palette[i]; - rgColor[i] = D3DCOLOR_RGBA(r, g, b, 255); - } + D3DCOLOR* entries; + VERIFYHR(m_pPalette->Lock(&entries, D3DLOCK_NOOVERWRITE)); + for (int n = 0; n < 256; n++) + entries[n] = 0xFF000000 | (m_palette[n] & 0x00FFFFFF); VERIFYHR(m_pPalette->Unlock()); #endif } +// CPalette::RenderDynamicTexture(pSurfx) +// Reconstructed from 5960 retail XBE @ 0x0005da80: fills row y with +// y * 256 / height, a top to bottom ramp through the whole palette. void CPalette::RenderDynamicTexture(CSurfx* pSurfx) { - for (int y = 0; y < pSurfx->m_nHeight; y += 1) - FillMemory(pSurfx->m_pels + y * pSurfx->m_nWidth, pSurfx->m_nWidth, (BYTE)(y * 256 / pSurfx->m_nHeight)); + for (int y = 0; y < pSurfx->m_nHeight; y++) + memset(pSurfx->m_pels + y * pSurfx->m_nWidth, (y * 256) / pSurfx->m_nHeight, pSurfx->m_nWidth); } From 11133bf96596a1598180df63393e051a369348b3 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:26:07 -0500 Subject: [PATCH 054/167] Rebuilt CDynamicTexture from the 5960 binary Wrote the constructor, Create, Advance, GetTextureSurface and the Xbox side of Update fresh from 0x5db30..0x5dde0. The desktop upload (software palette, projectM and album art) is ours and stays as it was. Texture bytes match on every frame of every test scene, Xbox and desktop paths. --- docs/decomp/TmapSystem.md | 35 ++++++++++ theseus/render/tmap_system.cpp | 122 ++++++++++++++++----------------- 2 files changed, 93 insertions(+), 64 deletions(-) diff --git a/docs/decomp/TmapSystem.md b/docs/decomp/TmapSystem.md index c25a7c7..c68320c 100644 --- a/docs/decomp/TmapSystem.md +++ b/docs/decomp/TmapSystem.md @@ -300,6 +300,41 @@ byte table) and writes every entry with alpha forced to `0xff`. **RenderDynamicTexture (`0x0005da80`).** Fills each row `y` with `y * 256 / height`: a ramp through the whole palette, top to bottom. +## DynamicTexture + +| Offset | Field | +|--------|-------| +| 0x20 | format, `0xb` (P8) | +| 0x24 | texture | +| 0x28, 0x2c | image width, height | +| 0x30..0x38 | children (count, capacity, array) | +| 0x3c | size, default 256 | +| 0x40 | erase, default 1 | +| 0x44 | fps, default 15.0 | +| 0x48 | palette node | +| 0x4c | CSurfx | +| 0x50 | last update (double) | + +**Create (`0x0005dc70`).** Always makes a 512x512, one-level texture in the node's format, +locks it, zeroes `w * h` bytes and reads the real size back. The requested size is ignored. + +**Advance (`0x0005dd20`).** CNode advance, then the palette node, then each child. + +**GetTextureSurface (`0x0005dd70`).** When `1 / fps + lastUpdate < now` (compared as floats) it +stores `now` and runs the update. Then, if the palette node's class chain reaches Palette, it +binds that node's D3D palette to stage 0. + +**Update (`0x0005dde0`).** + +1. If there's no texture and `size > 0`, Create; give up if that fails. +2. Allocate the `size x size` CSurfx on first use. +3. Only with both in place and `lastUpdate <= now`: zero the pixels when erase is set, call + every child's RenderDynamicTexture with the surface. +4. If there's no palette node, or its GetPalette returns NULL, build the default palette into a + stack buffer. The result is never read on this path. +5. Lock the texture and copy the surface in with an inlined Swizzler: per row, `IncU` per + pixel, then `IncV` and `AddU(SwizzleU(imageWidth - size))` to skip the unused columns. + ## Notes - CSurfx operates on a raw BYTE array with manual pitch calculation. diff --git a/theseus/render/tmap_system.cpp b/theseus/render/tmap_system.cpp index 15f2ef1..678f151 100644 --- a/theseus/render/tmap_system.cpp +++ b/theseus/render/tmap_system.cpp @@ -1062,9 +1062,7 @@ void CPalette::RenderDynamicTexture(CSurfx* pSurfx) // ===== CDynamicTexture ====================================================== -// Renders children into a palettized 8-bit texture (D3DFMT_P8) each frame. -// Children contribute via RenderDynamicTexture(CSurfx*). The pixel buffer -// is swizzled into an Xbox texture on each update. +// A texture node whose children draw into an 8-bit surface each frame. class CDynamicTexture : public CTexture { @@ -1103,46 +1101,56 @@ START_NODE_PROPS(CDynamicTexture, CTexture) NODE_PROP(pt_node, CDynamicTexture, palette) END_NODE_PROPS() +// CDynamicTexture::CDynamicTexture() +// Reconstructed from 5960 retail XBE @ 0x0005db30: +// format = 0xb (P8); size = 0x100; erase = 1; fps = 15.0 (0x0002bf68); +// palette = NULL; surfx = NULL; lastUpdate = 0.0 CDynamicTexture::CDynamicTexture() : m_size(256), m_erase(true), m_fps(15.0f), m_palette(NULL) { - m_lastUpdateTime = 0.0f; - m_pSurfx = NULL; m_format = D3DFMT_P8; + m_pSurfx = NULL; + m_lastUpdateTime = 0.0f; } +// CDynamicTexture::~CDynamicTexture() +// Reconstructed from 5960 retail XBE @ 0x0005dbc0: palette->Release(), then +// the surface (CSurfx teardown inlined), then the children and CTexture. CDynamicTexture::~CDynamicTexture() { if (m_palette != NULL) m_palette->Release(); - delete m_pSurfx; } +// CDynamicTexture::Create(nWidth, nHeight) +// Reconstructed from 5960 retail XBE @ 0x0005dc70. Ghidra decompile: +// imageW = imageH = 0x200; +// if (D3DXCreateTexture(dev, 0x200, 0x200, 1, 0, format, 1, &surface) < 0) return 0; +// LockRect; memset(bits, 0, imageW * imageH); Unlock; imageW, imageH = surface desc; return 1; +// Always 512x512 whatever size asks for; the effect only fills the top left corner. bool CDynamicTexture::Create(int nWidth, int nHeight) { - D3DLOCKED_RECT d3dlr; - m_nImageWidth = 512; m_nImageHeight = 512; - if (FAILED(D3DXCreateTexture(TheseusGetD3DDev(), m_nImageWidth, m_nImageHeight, 1, 0, m_format, D3DPOOL_MANAGED, &m_surface))) - { return false; - } - m_surface->LockRect(0, &d3dlr, NULL, 0); - memset(d3dlr.pBits, 0, m_nImageWidth * m_nImageHeight); + D3DLOCKED_RECT lr; + m_surface->LockRect(0, &lr, NULL, 0); + memset(lr.pBits, 0, m_nImageWidth * m_nImageHeight); m_surface->UnlockRect(0); TheseusGetTextureSize(m_surface, m_nImageWidth, m_nImageHeight); - return true; } +// CDynamicTexture::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x0005dd20: CNode::Advance, then the +// palette node, then every child. void CDynamicTexture::Advance(float nSeconds) { CNode::Advance(nSeconds); @@ -1150,91 +1158,77 @@ void CDynamicTexture::Advance(float nSeconds) if (m_palette != NULL) m_palette->Advance(nSeconds); - int nChildCount = m_children.GetLength(); - for (int i = 0; i < nChildCount; i += 1) - { - CNode* pChildNode = m_children.GetNode(i); - pChildNode->Advance(nSeconds); - } + for (int n = 0; n < m_children.GetLength(); n++) + m_children.GetNode(n)->Advance(nSeconds); } +// CDynamicTexture::GetTextureSurface() +// Reconstructed from 5960 retail XBE @ 0x0005dd70. Redraws at most fps times a +// second, then, if the palette node is a Palette (the binary walks the class +// chain), binds its D3D palette to stage 0. Binding is Xbox only; desktop +// applies the palette while uploading in Update. LPDIRECT3DTEXTURE8 CDynamicTexture::GetTextureSurface() { - if (TheseusGetNow() > m_lastUpdateTime + (1.0f / m_fps)) + if (TheseusGetNow() > m_lastUpdateTime + 1.0f / m_fps) { m_lastUpdateTime = TheseusGetNow(); Update(); } #ifdef _XBOX - // Xbox: bind the hardware palette so the GPU expands paletted texels. - // Desktop expands them in software during the LockRect upload below. if (m_palette != NULL && m_palette->IsKindOf(NODE_CLASS(CPalette))) - { - CPalette* pPalette = (CPalette*)m_palette; - VERIFYHR(TheseusGetD3DDev()->SetPalette(0, pPalette->m_pPalette)); - } + VERIFYHR(TheseusGetD3DDev()->SetPalette(0, ((CPalette*)m_palette)->m_pPalette)); #endif return m_surface; } +// CDynamicTexture::Update() +// Reconstructed from 5960 retail XBE @ 0x0005dde0. Creates the texture and the +// size x size CSurfx on first use, clears the surface when erase is set, lets +// each child draw into it, picks the palette (the node's, else the default from +// MakePalette), and uploads: on Xbox the 8-bit pixels go straight into the P8 +// texture through the Swizzler (inlined in the binary), so the GPU applies the +// palette. The desktop upload below is Theseus's own: palette expansion to +// RGBA in software, plus the projectM and album cover modes. void CDynamicTexture::Update() { - if (m_surface == NULL && m_size > 0) - { - if (!Create(m_size, m_size)) - return; - } + if (m_surface == NULL && m_size > 0 && !Create(m_size, m_size)) + return; if (m_pSurfx == NULL && m_size > 0) - { m_pSurfx = new CSurfx(m_size, m_size); - } if (m_surface == NULL || m_pSurfx == NULL || TheseusGetNow() < m_lastUpdateTime) return; if (m_erase) - ZeroMemory(m_pSurfx->m_pels, m_pSurfx->m_nWidth * m_pSurfx->m_nHeight); + memset(m_pSurfx->m_pels, 0, m_pSurfx->m_nWidth * m_pSurfx->m_nHeight); - int nChildCount = m_children.GetLength(); - int i; - for (i = 0; i < nChildCount; i += 1) + for (int n = 0; n < m_children.GetLength(); n++) + m_children.GetNode(n)->RenderDynamicTexture(m_pSurfx); + + const DWORD* rgpe = m_palette != NULL ? m_palette->GetPalette() : NULL; + DWORD rgpeDefault [256]; + if (rgpe == NULL) { - CNode* pChildNode = m_children.GetNode(i); - pChildNode->RenderDynamicTexture(m_pSurfx); + MakePalette(rgpeDefault); + rgpe = rgpeDefault; } - // Upload the 8-bit pixmap into the texture surface. - // Xbox: swizzle to NV2A tiled layout and let the GPU expand the palette. - // Desktop: expand the palette in software (one ARGB pixel per byte) since - // the GL emulator doesn't have palette-indexed texture support. { - const DWORD* rgpe = NULL; - - if (m_palette != NULL) - rgpe = m_palette->GetPalette(); - - DWORD rgpeDefault [256]; - if (rgpe == NULL) - { - rgpe = rgpeDefault; - MakePalette(rgpeDefault); - } - #ifdef _XBOX + (void)rgpe; Swizzler swz(m_nImageWidth, m_nImageHeight, 1); - BYTE* pSrc = (BYTE*)m_pSurfx->m_pels; - BYTE* pDst; D3DLOCKED_RECT lr; VERIFYHR(m_surface->LockRect(0, &lr, NULL, D3DLOCK_DISCARD)); - pDst = (BYTE*)lr.pBits; - for (i = 0; i < m_size; i++, swz.IncV()) { - for (int j = 0; j < m_size; j++, swz.IncU()) { - pDst[swz.Get2D()] = pSrc[j]; - } - pSrc += m_size; + BYTE* dst = (BYTE*)lr.pBits; + const BYTE* src = m_pSurfx->m_pels; + for (int y = 0; y < m_size; y++, src += m_size) + { + for (int x = 0; x < m_size; x++, swz.IncU()) + dst[swz.Get2D()] = src[x]; + swz.IncV(); swz.AddU(swz.SwizzleU(m_nImageWidth - m_size)); } VERIFYHR(m_surface->UnlockRect(0)); From 81b919e9338f3502471ddd1575a75ce52efca024 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:27:20 -0500 Subject: [PATCH 055/167] Rebuilt CImageFader from the 5960 binary Wrote the fader's frame step and property hook fresh from 0x5e030..0x5e3f0. Fades match byte for byte across all 26 style tests, including setting the type mid run. --- docs/decomp/TmapSystem.md | 29 +++++++++++++++ theseus/render/tmap_system.cpp | 65 +++++++++++++++++++++------------- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/docs/decomp/TmapSystem.md b/docs/decomp/TmapSystem.md index c68320c..dbf6c9c 100644 --- a/docs/decomp/TmapSystem.md +++ b/docs/decomp/TmapSystem.md @@ -335,6 +335,35 @@ binds that node's D3D palette to stage 0. 5. Lock the texture and copy the surface in with an inlined Swizzler: per row, `IncU` per pixel, then `IncV` and `AddU(SwizzleU(imageWidth - size))` to skip the unused columns. +## ImageFader + +| Offset | Field | +|--------|-------| +| 0x1c | type (warp style) | +| 0x20 | changePeriod | +| 0x24 | changePeriodRandomness | +| 0x28 | current DeltaField | +| 0x2c | next DeltaField | +| 0x30 | next change time (double) | + +**RenderDynamicTexture (`0x0005e120`).** + +1. No current field: build one for `type` (new 0x3c bytes, style, Assign, SetSize to the + surface, all inlined) and set `next = changePeriod + rand() * randomness / 32767 + now`. +2. Else, when `changePeriod > 0` and `next <= now`: `type += 1`, `next = changePeriod + now`, + free the current field and promote the next one. +3. No next field: build one for `type + 1`. +4. Current field unfinished: 10 CalcSome calls, copy its style back into `type`, return without + warping. Building a field this way takes `height / 10` frames. +5. Next field unfinished: 5 CalcSome calls (guarded by a NULL check). +6. Finish the current field if needed and run the in-place fade on the surface. + +If the timer fires on the frame after `type` was set, the promoted field is NULL and step 4 +dereferences it. + +**OnSetProperty (`0x0005e3f0`).** For the `type` property (offset 0x1c): free the current field, +promote the next one, clear next. Returns 1 for everything. + ## Notes - CSurfx operates on a raw BYTE array with manual pitch calculation. diff --git a/theseus/render/tmap_system.cpp b/theseus/render/tmap_system.cpp index 678f151..3879695 100644 --- a/theseus/render/tmap_system.cpp +++ b/theseus/render/tmap_system.cpp @@ -1343,8 +1343,7 @@ void CDynamicTexture::Update() // ===== CImageFader ========================================================== -// Applies a DeltaField warp effect to a CSurfx. Cycles through warp -// styles on a timer. +// Warps the surface through a DeltaField every frame, changing style on a timer. class CImageFader : public CNode { @@ -1376,56 +1375,71 @@ START_NODE_PROPS(CImageFader, CNode) NODE_PROP(pt_number, CImageFader, changePeriodRandomness) END_NODE_PROPS() +// CImageFader::CImageFader() +// Reconstructed from CImageFader_CreateNode @ 0x0005e030 (0x38 bytes): +// type = changePeriod = randomness = 0; field = nextField = NULL; nextChange = 0.0 CImageFader::CImageFader() : m_type(0), m_changePeriod(0.0f), m_changePeriodRandomness(0.0f) { - m_nextChangeTime = 0.0f; m_pDeltaField = NULL; m_pDeltaField2 = NULL; + m_nextChangeTime = 0.0f; } +// CImageFader::~CImageFader() +// Reconstructed from 5960 retail XBE @ 0x0005e0c0: frees both fields. CImageFader::~CImageFader() { delete m_pDeltaField; delete m_pDeltaField2; } +// Inlined twice in 0x0005e120: new DeltaField, style, Assign, SetSize to the surface. +static DeltaField* NewDeltaField(int style, CSurfx* pSurfx) +{ + DeltaField* field = new DeltaField; + field->m_nStyle = style; + field->Assign(); + field->SetSize(pSurfx->m_nWidth, pSurfx->m_nHeight); + return field; +} + +// CImageFader::RenderDynamicTexture(pSurfx) +// Reconstructed from 5960 retail XBE @ 0x0005e120. Keeps the current warp +// field plus the next style's field building in the background: +// - first frame: build the field for type and schedule the next change at +// now + changePeriod + rnd(randomness); +// - when changePeriod > 0 and that time comes: type++, next change at +// now + changePeriod, and the background field becomes current; +// - make sure the background field (style type + 1) exists; +// - while the current field is unfinished, compute 10 rows of it, copy its +// style back into type (it snaps bad styles to 0) and skip the warp; +// - otherwise compute 5 rows of the background field and fade the surface +// through the current one. void CImageFader::RenderDynamicTexture(CSurfx* pSurfx) { if (m_pDeltaField == NULL) { - m_pDeltaField = new DeltaField; - m_pDeltaField->m_nStyle = m_type; - m_pDeltaField->Assign(); - m_pDeltaField->SetSize(pSurfx->m_nWidth, pSurfx->m_nHeight); - + m_pDeltaField = NewDeltaField(m_type, pSurfx); m_nextChangeTime = TheseusGetNow() + m_changePeriod + rnd(m_changePeriodRandomness); } else if (m_changePeriod > 0.0f && TheseusGetNow() >= m_nextChangeTime) { + m_type++; m_nextChangeTime = TheseusGetNow() + m_changePeriod; - - m_type += 1; - delete m_pDeltaField; m_pDeltaField = m_pDeltaField2; m_pDeltaField2 = NULL; } if (m_pDeltaField2 == NULL) - { - m_pDeltaField2 = new DeltaField; - m_pDeltaField2->m_nStyle = m_type + 1; - m_pDeltaField2->Assign(); - m_pDeltaField2->SetSize(pSurfx->m_nWidth, pSurfx->m_nHeight); - } + m_pDeltaField2 = NewDeltaField(m_type + 1, pSurfx); if (m_pDeltaField->IsCalculating()) { - int i; - for (i = 0; i < 10; i += 1) + for (int n = 0; n < 10; n++) m_pDeltaField->CalcSome(); m_type = m_pDeltaField->m_nStyle; return; @@ -1433,16 +1447,18 @@ void CImageFader::RenderDynamicTexture(CSurfx* pSurfx) if (m_pDeltaField2->IsCalculating()) { - for (int i = 0; i < 5; i += 1) - { - if (m_pDeltaField2 != NULL) - m_pDeltaField2->CalcSome(); - } + for (int n = 0; n < 5; n++) + m_pDeltaField2->CalcSome(); } pSurfx->Fade(m_pDeltaField->GetField()); } +// CImageFader::OnSetProperty(pprd, pvValue) +// Reconstructed from 5960 retail XBE @ 0x0005e3f0: +// if (prd->offset == 0x1c /* type */) { delete field; field = nextField; nextField = NULL; } +// return 1; +// The next frame rebuilds the background field for the new type. bool CImageFader::OnSetProperty(const PRD* pprd, const void* pvValue) { if (PTR2INT(pprd->pbOffset) == offsetof(m_type)) @@ -1451,7 +1467,6 @@ bool CImageFader::OnSetProperty(const PRD* pprd, const void* pvValue) m_pDeltaField = m_pDeltaField2; m_pDeltaField2 = NULL; } - return true; } From f430788d8dd91e56d38b9170baf3d6807697893f Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:31:50 -0500 Subject: [PATCH 056/167] Rebuilt the Xbox CAudioVisualizer from the 5960 binary Wrote the scope, spinner, analyzer, beat flash and wave overlay fresh from 0x63770..0x64130. Dropped GetMonoPCM, which nothing called and the binary doesn't have. Surfaces match byte for byte over 400 frames of every type and channel combo, which walks through all five overlay modes. --- docs/decomp/TmapSystem.md | 64 ++++ theseus/render/tmap_system.cpp | 640 +++++++++++++-------------------- 2 files changed, 322 insertions(+), 382 deletions(-) diff --git a/docs/decomp/TmapSystem.md b/docs/decomp/TmapSystem.md index dbf6c9c..d4cb183 100644 --- a/docs/decomp/TmapSystem.md +++ b/docs/decomp/TmapSystem.md @@ -364,6 +364,70 @@ dereferences it. **OnSetProperty (`0x0005e3f0`).** For the `type` property (offset 0x1c): free the current field, promote the next one, clear next. Returns 1 for everything. +## AudioVisualizer + +| Offset | Field | +|--------|-------| +| 0x1c | type string | +| 0x20 | channel string | +| 0x24 | source node | +| 0x28 | scale, default 1.0 | +| 0x2c | offset | +| 0x30 | 256 left samples | +| 0x230 | 256 right samples | +| 0x430 | 128 left spectrum bins | +| 0x530 | 128 right spectrum bins | +| 0x630 | spectrum valid | +| 0x632 | 256 mono samples (never filled) | +| 0x832 | mono valid | + +**RenderDynamicTexture (`0x00063940`).** Returns unless the source's class is AudioClip, +its transport mode (+0x50) is 1 (playing), and its pump (+0x68) hands back a sample buffer. +Sample count is buffer size / 4, capped at 256; left/right are copied out, the rest zeroed, +both "valid" flags cleared, then the count is capped at the surface height. Channel `r`/`R` +picks the right samples. The type's first letter picks the effect: + +- anything else (or no type): **line scope.** One line per sample, from the previous point to + `(width/2 + (sample * width >> 16), row)`, color 255. +- `s`/`S`: **spinner.** Calls `rand()` and ignores it, takes `c = cos(t * 0.2)`, + `s = sin(t * 0.2)`, then steps `p` from 0 to 1 by `1 / width`: level + `= sample[(int)(count * p)] / 32767 * scale + offset`, `a = (p - 0.5) * 2.1`, point + `(c*a + s*level, s*a - c*level)` scaled by half the surface, joined with lines. +- `c`/`C`: draws nothing. +- `a`/`A`: **analyzer.** Spectrum update (below), then 256 bars: left bins reversed then right + bins, each `ln(bin) * 8` clamped to 0..255. Peaks (static ints at `0x00188f90`) fall by 2 a + frame and jump to `2 * bar`. Each column takes bar `x * 256 / width`, filled from the bottom + with 255 counting down, plus one 255 dot at the peak height (which can land in the spare + row). Then the beat flash and the wave overlay. + +**Spectrum (`0x000638a0`, `0x00063900`).** Once per frame: FFT of the left then right samples +(state made on first use), each of the 129 outputs becomes `(short)(sqrt(p) / 256)`. 129 bins +into a 128 entry array: the left's last bin lands on right bin 0 (rewritten right after) and +the right's lands on the valid flag (set right after). + +**Beat flash (`0x00063ef0`).** Bass = average of `bin >> 4` over the 5 lowest bins of both +channels. Globals at `0x0017e6f4..0x0017e70c` track a running max and min that roll into +"last" values every 10 frames (frame counter at `0x00188f50`). When bass rises above +`(6 * lastMax + 4 * lastMin) / 10` the whole surface is filled with 255, once, until bass drops +below `(4 * lastMax + 6 * lastMin) / 10`. + +**Wave overlay (`0x00064130`).** Globals: amplitude 50 (`0x00176034`), shift 30 +(`0x00176038`), mode -1 (`0x0017603c`), color 128 (`0x00176040`), next switch (`0x00188f48`). + +- When mode < 0 or the switch time passes: mode += 1, next = `rand() * 5 / 32767 + now + 3`; + mode wraps from 5 to 0. +- Half circle tables `cos/sin(i / width * pi + pi / 2)` for every 4th `i` + (`0x0017e774..0x0017e780`), rebuilt when the width changes. +- Steps of 4 pixels across the width; each step reads the high byte of sample + `((i << 8) / width) / 5` from each channel, scaled `* height * 50 >> 12`. Lines sit + `height * 30 >> 8` from the middle: mode 0 one left-channel line, mode 1 right above and left + below, mode 2 the right line twice plus two vertical left lines, modes 3 and 4 both channels + around the half circle (mode 3 clips negative values), closed with one last chord. +- Then two 64 point Lissajous trails as 2x2 dots (`0x000640a0`), color 255: + `(cos(k / d1), sin(k / d2)) * height * 50/256` rotated by `k * 0.001` around the center, with + `d1, d2 = 80, 140.48` and `187.2, 271.13`. `k` starts from a global (`0x00188f40`) each + trail and the second trail's end is stored back. + ## Notes - CSurfx operates on a raw BYTE array with manual pitch calculation. diff --git a/theseus/render/tmap_system.cpp b/theseus/render/tmap_system.cpp index 3879695..8f7d872 100644 --- a/theseus/render/tmap_system.cpp +++ b/theseus/render/tmap_system.cpp @@ -1472,13 +1472,11 @@ bool CImageFader::OnSetProperty(const PRD* pprd, const void* pvValue) // ===== CAudioVisualizer ===================================================== -// Renders PCM waveform and FFT spectrum data from an AudioClip source -// into a CSurfx. Supports line scope, spinner scope, spectrum analyzer, -// and spectral overlay effects. +// Draws the playing AudioClip into the surface as a scope, a spinner or a +// spectrum analyzer with a beat flash and an overlay of wave lines. // -// Xbox-only here. desktop has its own SDL_mixer-backed CAudioVisualizer -// in desktop/desktop_nodes.cpp because the audio sample buffer comes from -// DashAudio_GetPCMSamples() (a ring buffer), not from CAudioClip::GetSampleBuffer(). +// Xbox only here. Desktop has its own CAudioVisualizer in +// desktop/desktop_nodes.cpp, fed from the SDL mixer's sample ring. #ifdef _XBOX class CAudioVisualizer : public CNode @@ -1495,7 +1493,6 @@ class CAudioVisualizer : public CNode float m_offset; protected: - short* GetMonoPCM(); void UpdateSpectrum(); void RenderDynamicTexture(CSurfx* pSurfx); @@ -1527,512 +1524,391 @@ START_NODE_PROPS(CAudioVisualizer, CNode) NODE_PROP(pt_node, CAudioVisualizer, source) END_NODE_PROPS() +// CAudioVisualizer::CAudioVisualizer() +// Reconstructed from CAudioVisualizer_CreateNode @ 0x00063770 (0x834 bytes): +// type = channel = source = NULL; scale = 1.0; offset = 0 CAudioVisualizer::CAudioVisualizer() : m_type(NULL), + m_channel(NULL), m_source(NULL), m_scale(1.0f), - m_offset(0.0f), - m_channel(NULL) + m_offset(0.0f) { } +// CAudioVisualizer::~CAudioVisualizer() +// Reconstructed from 5960 retail XBE @ 0x00063800: free(type); free(channel); source->Release(). CAudioVisualizer::~CAudioVisualizer() { delete [] m_type; delete [] m_channel; - if (m_source != NULL) m_source->Release(); } +// CAudioVisualizer::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x00063870: CNode::Advance, then the source. void CAudioVisualizer::Advance(float nSeconds) { CNode::Advance(nSeconds); - if (m_source != NULL) m_source->Advance(nSeconds); } -short* CAudioVisualizer::GetMonoPCM() -{ - if (!m_bMonoValid) - { - int i; - for (i = 0; i < 256; i += 1) - { - m_pcmMono[i] = (short)(((int)m_pcmLeft[i] + (int)m_pcmRight[i]) / 2); - } - - m_bMonoValid = true; - } - - return m_pcmMono; -} - +// CAudioVisualizer::CalcSpectrum(pcm, fft) +// Reconstructed from 5960 retail XBE @ 0x000638a0. Ghidra decompile: +// if (!state) state = fft_init(); fft_perform(pcm, buf[129], state); +// for (i = 0; i < 0x81; i++) fft[i] = (short)(sqrt(buf[i]) * (1/256.0)); +// 129 bins go into a 128 entry array: the last lands in the next field, the +// same spill the binary has with this layout. void CAudioVisualizer::CalcSpectrum(short* pcm, short* fft) { - static fft_state *state = NULL; - float buf [FFT_BUFFER_SIZE / 2 + 1]; + static fft_state* state = NULL; + float power [FFT_BUFFER_SIZE / 2 + 1]; - if (!state) + if (state == NULL) state = fft_init(); + fft_perform(pcm, power, state); - fft_perform(pcm, buf, state); - - for (int i = 0; i < FFT_BUFFER_SIZE / 2 + 1; i += 1) - { - buf[i] = sqrtf(buf[i]) / FFT_BUFFER_SIZE; - fft[i] = (short)buf[i]; - } + for (int n = 0; n <= FFT_BUFFER_SIZE / 2; n++) + fft[n] = (short)(sqrtf(power[n]) / FFT_BUFFER_SIZE); } +// CAudioVisualizer::UpdateSpectrum() +// Reconstructed from 5960 retail XBE @ 0x00063900: once per frame, left then right. void CAudioVisualizer::UpdateSpectrum() { if (m_bFFTValid) return; - CalcSpectrum(m_pcmLeft, m_fftLeft); CalcSpectrum(m_pcmRight, m_fftRight); - m_bFFTValid = true; } -#define mag(s) ((float)samples[(int)(s * nSamples) * 2 + lrc] / 32767.0f) +// log(level) * 8 as a byte, 0 for silence (and NaN), 255 at the top. +static inline BYTE SpectrumByte(short level) +{ + float n = logf(level) * 8.0f; + if (!(n > 0.0f)) + return 0; + return n < 255.0f ? (BYTE)n : 255; +} +// CAudioVisualizer::RenderDynamicTexture(pSurfx) +// Reconstructed from 5960 retail XBE @ 0x00063940. Only draws while the source +// is a playing AudioClip with a sample buffer. Copies up to 256 stereo samples +// into the left/right arrays (zero padded) and marks the mono and spectrum +// caches stale. type picks the effect by first letter: none or other = line +// scope, 's' = spinner, 'c' = nothing, 'a' = analyzer. channel 'r'/'R' takes +// the right channel. Analyzer bars use 128 left bins mirrored into 128 right +// bins, falling peak dots (2 per frame, static array @ 0x00188f90), then the +// beat flash and the wave overlay. Details in docs/decomp/TmapSystem.md. void CAudioVisualizer::RenderDynamicTexture(CSurfx* pSurfx) { - CAudioClip* pAudioClip = (CAudioClip*)m_source; - if (pAudioClip == NULL || pAudioClip->GetNodeClass() != NODE_CLASS(CAudioClip) || pAudioClip->m_transportMode != TRANSPORT_PLAY) + CAudioClip* clip = (CAudioClip*)m_source; + if (clip == NULL || clip->GetNodeClass() != NODE_CLASS(CAudioClip) || clip->m_transportMode != TRANSPORT_PLAY) return; - short* samples = (short*)pAudioClip->GetSampleBuffer(); + short* samples = (short*)clip->GetSampleBuffer(); if (samples == NULL) return; - int nSamples = pAudioClip->GetSampleBufferSize() / 4; - - if (nSamples <= 0) + int count = clip->GetSampleBufferSize() / 4; + if (count <= 0) return; + if (count > 256) + count = 256; - // Setup local PCM buffers + for (int n = 0; n < 256; n++) { - if (nSamples > 256) - nSamples = 256; - - int i; - for (i = 0; i < nSamples; i += 1) - { - m_pcmLeft[i] = samples[i * 2]; - m_pcmRight[i] = samples[i * 2 + 1]; - } - - for (; i < 256; i += 1) - { - m_pcmLeft[i] = 0; - m_pcmRight[i] = 0; - } - - m_bMonoValid = false; - m_bFFTValid = false; + m_pcmLeft[n] = n < count ? samples[n * 2] : 0; + m_pcmRight[n] = n < count ? samples[n * 2 + 1] : 0; } + m_bMonoValid = false; + m_bFFTValid = false; - if (nSamples > pSurfx->m_nHeight) - nSamples = pSurfx->m_nHeight; + if (count > pSurfx->m_nHeight) + count = pSurfx->m_nHeight; - int lrc = 0; - if (m_channel != NULL) - { - if (m_channel[0] == 'r' || m_channel[0] == 'R') - lrc = 1; - } + int channel = 0; + if (m_channel != NULL && (m_channel[0] == 'r' || m_channel[0] == 'R')) + channel = 1; + + TCHAR kind = m_type != NULL ? m_type[0] : 0; + int width = pSurfx->m_nWidth; + int height = pSurfx->m_nHeight; + float t = (float)TheseusGetNow(); - int nType = 0; - if (m_type != NULL) + if (kind == 's' || kind == 'S') { - switch (m_type[0]) + int xc = width / 2; + int yc = height / 2; + float step = 1.0f / (float)width; + float c = cosf(t * 0.2f); + float s = sinf(t * 0.2f); + float lastX = 0.0f, lastY = 0.0f; + rand(); // the binary draws one value here and never uses it + + for (float pos = 0.0f; pos <= 1.0; pos += step) { - case 's': case 'S': nType = 1; break; // spinner - case 'c': case 'C': nType = 2; break; // circle scope - case 'a': case 'A': nType = 3; break; // spectrum analyzer + float level = (float)samples[(int)(pos * count) * 2 + channel] / 32767.0f * m_scale + m_offset; + float along = 2.1f * (pos - 0.5f); + float x = c * along + s * level; + float y = -c * level + s * along; + + if (pos != 0.0f) + pSurfx->Line(xc + (int)(lastX * xc), yc + (int)(lastY * yc), xc + (int)(x * xc), yc + (int)(y * yc), 255); + lastX = x; + lastY = y; } } - - float t = (float) TheseusGetNow(); - - switch (nType) + else if (kind == 'a' || kind == 'A') { - case 0: // Line Scope - { - int xCenter = pSurfx->m_nWidth / 2; - int xp = xCenter; - int yp = 0; + static int peaks [256]; + BYTE bars [256]; - for (int y = 0; y < nSamples; y += 1) - { - long s = (((long)samples[y * 2 + lrc]) * pSurfx->m_nWidth) >> 16; - int x = xCenter + s; + UpdateSpectrum(); + for (int n = 0; n < 128; n++) + bars[n] = SpectrumByte(m_fftLeft[127 - n]); + for (int n = 128; n < 256; n++) + bars[n] = SpectrumByte(m_fftRight[n - 128]); - pSurfx->Line(xp, yp, x, y, 255); - xp = x; - yp = y; - } + for (int n = 0; n < 256; n++) + { + peaks[n] = peaks[n] > 2 ? peaks[n] - 2 : 0; + if (peaks[n] < bars[n] * 2) + peaks[n] = bars[n] * 2; } - break; - case 1: // Spinner Scope + for (int x = 0; x < width; x++) { - int xCenter = pSurfx->m_nWidth / 2; - int yCenter = pSurfx->m_nHeight / 2; - - float step = 1.0f / (float)pSurfx->m_nWidth; - - float firstX, firstY; - float prevX, prevY; - - float A0 = 0.5f + rnd(0.2f); - - float B0 = cosf(t * 0.2f); - float B1 = sinf(t * 0.2f); - - for (float s = 0.0f; s <= 1.0; s += step) - { - float C0 = mag(s) * m_scale + m_offset; - float C1 = 2.1f * (s - 0.5f); - - float X0 = B0 * C1 + B1 * C0; - float Y0 = -B0 * C0 + B1 * C1; - - float X1 = B0 * C1 - B1 * C0; - float Y1 = B0 * C0 + B1 * C1; - - if (s == 0.0f) - { - firstX = X0; - firstY = Y0; - } - else - { - pSurfx->Line(xCenter + (int)(prevX * xCenter), yCenter + (int)(prevY * yCenter), - xCenter + (int)(X0 * xCenter), yCenter + (int)(Y0 * yCenter), 255); - } - - prevX = X0; - prevY = Y0; - } + int bin = (x * 256) / width; + float barHeight = ((float)bars[bin] * (float)height) / 256.0f; + BYTE shade = 255; + for (int y = height - (int)barHeight; y < height; y++) + *pSurfx->Pixel(x, y) = shade--; + + float peakHeight = (float)peaks[bin] / 2.0f; + peakHeight = (peakHeight * (float)height) / 256.0f; + *pSurfx->Pixel(x, height - (int)peakHeight) = 255; } - break; - case 3: // Spectrum Analyzer + RenderEffect1(pSurfx); + RenderEffect2(pSurfx); + } + else if (kind != 'c' && kind != 'C') + { + int xc = width / 2; + int lastX = xc, lastY = 0; + for (int y = 0; y < count; y++) { - UpdateSpectrum(); - - static int peak_buf [256]; - BYTE spectrum [256]; - - int i; - for (i = 0; i < 128; i += 1) - { - float n = logf(m_fftLeft[127 - i]) * 8.0f; - if (n <= 0.0f) - spectrum[i] = 0; - else if (n >= 255.0f) - spectrum[i] = 255; - else - spectrum[i] = (BYTE)n; - } - - for (; i < 256; i += 1) - { - float n = logf(m_fftRight[i - 128]) * 8.0f; - if (n <= 0.0f) - spectrum[i] = 0; - else if (n >= 255.0f) - spectrum[i] = 255; - else - spectrum[i] = (BYTE)n; - } - - for (i = 0; i < 256; i += 1) - { - if (peak_buf[i] > 2) - peak_buf[i] -= 2; - else - peak_buf[i] = 0; - - int y = spectrum[i] * 2; - if (peak_buf[i] < y) - peak_buf[i] = y; - } - - int nWidth = pSurfx->m_nWidth; - for (int x = 0; x < nWidth; x += 1) - { - float nHeight = ((((float)spectrum[(x * 256) / nWidth])) * (float)pSurfx->m_nHeight) / 256.0f; - BYTE bColor = 255; - for (int y = pSurfx->m_nHeight - (int)nHeight; y < pSurfx->m_nHeight; y += 1, bColor -= 1) - *pSurfx->Pixel(x, y) = bColor; - - nHeight = (float)peak_buf[(x * 256) / nWidth]; - nHeight /= 2.0f; - nHeight = (nHeight * (float)pSurfx->m_nHeight) / 256.0f; - *pSurfx->Pixel(x, pSurfx->m_nHeight - (int)nHeight) = 255; - } - - RenderEffect1(pSurfx); - RenderEffect2(pSurfx); + int x = xc + ((samples[y * 2 + channel] * width) >> 16); + pSurfx->Line(lastX, lastY, x, y, 255); + lastX = x; + lastY = y; } - break; } } - -// Bass beat detection for spectrum analyzer flash effect -#define BASS_EXT_MEMORY 10 - -struct bass_info { - int max_recent; - int max_old; - int time_last_max; - int min_recent; - int min_old; - int time_last_min; - int activated; -} bass_info; - +// Beat detector state, globals @ 0x0017e6f4..0x0017e70c in the binary. +static struct +{ + int highNow, highLast, highTime; + int lowNow, lowLast, lowTime; + int flashing; +} s_beat; + +// CAudioVisualizer::RenderEffect1(pSurfx) +// Reconstructed from 5960 retail XBE @ 0x00063ef0. Bass = average of the +// lowest 5 bins of each channel (each >> 4). Running high and low marks reset +// every 10 frames (frame count @ 0x00188f50) into "last" values. The surface +// flashes to 255 when bass passes 60% of the way from low to high, and rearms +// once it drops under 40%. void CAudioVisualizer::RenderEffect1(CSurfx* pSurfx) { - static int t = 0; + static int frame = 0; int bass = 0; - const int step = 5; - int i; - for (i = 0; i < step; i += 1) - bass += (m_fftLeft[i] >> 4) + (m_fftRight[i] >> 4); - bass /= (step * 2); - - if (bass > bass_info.max_recent) - bass_info.max_recent = bass; + for (int n = 0; n < 5; n++) + bass += (m_fftLeft[n] >> 4) + (m_fftRight[n] >> 4); + bass /= 10; - if (bass < bass_info.min_recent) - bass_info.min_recent = bass; + if (bass > s_beat.highNow) + s_beat.highNow = bass; + if (bass < s_beat.lowNow) + s_beat.lowNow = bass; - if (t - bass_info.time_last_max > BASS_EXT_MEMORY) + if (frame - s_beat.highTime > 10) { - bass_info.max_old = bass_info.max_recent; - bass_info.max_recent = 0; - bass_info.time_last_max = t; + s_beat.highLast = s_beat.highNow; + s_beat.highNow = 0; + s_beat.highTime = frame; } - - if (t - bass_info.time_last_min > BASS_EXT_MEMORY) + if (frame - s_beat.lowTime > 10) { - bass_info.min_old = bass_info.min_recent; - bass_info.min_recent = 0; - bass_info.time_last_min = t; + s_beat.lowLast = s_beat.lowNow; + s_beat.lowNow = 0; + s_beat.lowTime = frame; } - if (bass > (bass_info.max_old * 6 + bass_info.min_old * 4) / 10 && bass_info.activated == 0) + if (!s_beat.flashing && bass > (s_beat.highLast * 6 + s_beat.lowLast * 4) / 10) { - FillMemory(pSurfx->m_pels, pSurfx->m_nWidth * pSurfx->m_nHeight, 255); - bass_info.activated = 1; + memset(pSurfx->m_pels, 255, pSurfx->m_nWidth * pSurfx->m_nHeight); + s_beat.flashing = 1; } - - if (bass < (bass_info.max_old * 4 + bass_info.min_old * 6) / 10 && bass_info.activated == 1) - bass_info.activated = 0; - - t += 1; + frame++; + if (s_beat.flashing && bass < (s_beat.highLast * 4 + s_beat.lowLast * 6) / 10) + s_beat.flashing = 0; } +// Overlay settings, initialized globals @ 0x00176034..0x00176040 in the binary. +static int s_waveAmplitude = 50; +static int s_waveShift = 30; +static int s_waveMode = -1; +static BYTE s_waveColor = 128; -// Spectral overlay effect with Lissajous curve -#define PI D3DX_PI - -struct sincos { - int i; - float *f; -}; -static struct sincos cosw = { 0, NULL }; -static struct sincos sinw = { 0, NULL }; - -int spectral_amplitude = 50; -int spectral_shift = 30; -int mode_spectre = -1; -BYTE spectral_color = 128; - -void SetPixel(CSurfx* pSurfx, int x, int y, BYTE color) -{ - if (x < 0 || y < 0 || x >= pSurfx->m_nWidth || y >= pSurfx->m_nHeight) - return; - *pSurfx->Pixel(x, y) = color; -} - -void SetPixel2(CSurfx* pSurfx, int x, int y, BYTE color) +// Bounds checked 2x2 dot, inlined from 0x000640a0. +static void PlotDot(CSurfx* pSurfx, int x, int y, BYTE color) { - SetPixel(pSurfx, x, y, color); - SetPixel(pSurfx, x + 1, y, color); - SetPixel(pSurfx, x + 1, y + 1, color); - SetPixel(pSurfx, x, y + 1, color); + static const int offsets[4][2] = { {0, 0}, {1, 0}, {1, 1}, {0, 1} }; + for (int n = 0; n < 4; n++) + { + int px = x + offsets[n][0], py = y + offsets[n][1]; + if (px >= 0 && py >= 0 && px < pSurfx->m_nWidth && py < pSurfx->m_nHeight) + *pSurfx->Pixel(px, py) = color; + } } +// CAudioVisualizer::RenderEffect2(pSurfx) +// Reconstructed from 5960 retail XBE @ 0x00064130. Every 3 + rnd(5) seconds it +// moves to the next of 5 modes: one wave line, two lines (left and right), +// four lines (two across, two down), or the two channels bent around a half +// circle (mode 3 clips negative swings). Samples come from the high byte of +// every fifth slot across the width, 4 pixels per segment, scaled by +// height * 50 >> 12, offset height * 30 >> 8 from center. The half circle +// tables are cos/sin(i / width * pi + pi / 2), rebuilt when the width changes +// (globals @ 0x0017e774..0x0017e780). Last, two 64 point Lissajous trails +// (k / 80, k / (80 * 1.756) and k / 187.2, k / (154.4 * 1.756)) slowly +// rotating by k * 0.001, drawn as 2x2 dots and continuing from frame to +// frame (k @ 0x00188f40). void CAudioVisualizer::RenderEffect2(CSurfx* pSurfx) { - int halfheight, halfwidth; - float old_y1, old_y2; - float y1 = (float)((((m_pcmLeft[0] + m_pcmRight[0]) >> 9) * spectral_amplitude * pSurfx->m_nHeight) >> 12); - float y2 = (float)((((m_pcmLeft[0] + m_pcmRight[0]) >> 9) * spectral_amplitude * pSurfx->m_nHeight) >> 12); - const int density_lines = 5; - const int step = 4; - const int shift = (spectral_shift * pSurfx->m_nHeight) >> 8; - - static XTIME timeToChange = 0.0f; - if (mode_spectre < 0 || TheseusGetNow() >= timeToChange) + static XTIME nextMode = 0.0f; + static float* cosTable = NULL; + static float* sinTable = NULL; + static int cosWidth = 0, sinWidth = 0; + static int curveStep = 0; + + const int width = pSurfx->m_nWidth; + const int height = pSurfx->m_nHeight; + const int segment = 4; + const int shift = (s_waveShift * height) >> 8; + const int midX = width >> 1; + const int midY = height >> 1; + + float right = (float)((((m_pcmLeft[0] + m_pcmRight[0]) >> 9) * s_waveAmplitude * height) >> 12); + float left = right; + + if (s_waveMode < 0 || TheseusGetNow() >= nextMode) { - mode_spectre += 1; - timeToChange = TheseusGetNow() + 3.0f + rnd(5.0f); + s_waveMode++; + nextMode = TheseusGetNow() + 3.0f + rnd(5.0f); } + if (s_waveMode > 4) + s_waveMode = 0; - if ((UINT)mode_spectre > 4) - mode_spectre = 0; - - if (cosw.i != pSurfx->m_nWidth || sinw.i != pSurfx->m_nWidth) + if (cosWidth != width || sinWidth != width) { - delete [] cosw.f; - delete [] sinw.f; - sinw.f = cosw.f = NULL; - sinw.i = cosw.i = 0; + delete [] cosTable; + delete [] sinTable; + cosTable = sinTable = NULL; + cosWidth = sinWidth = 0; } - - if (cosw.i == 0 || cosw.f == NULL) + if (cosWidth == 0 || cosTable == NULL) { - const float halfPI = (float)PI / 2; - cosw.i = pSurfx->m_nWidth; - cosw.f = new float [pSurfx->m_nWidth]; - int i; - for (i = 0; i < pSurfx->m_nWidth; i += step) - cosw.f[i] = cosf((float)i / pSurfx->m_nWidth * PI + halfPI); + cosWidth = width; + cosTable = new float [width]; + for (int n = 0; n < width; n += segment) + cosTable[n] = cosf((float)n / width * D3DX_PI + (float)D3DX_PI / 2); } - - if (sinw.i == 0 || sinw.f == NULL) + if (sinWidth == 0 || sinTable == NULL) { - const float halfPI = (float)PI / 2; - sinw.i = pSurfx->m_nWidth; - sinw.f = new float [pSurfx->m_nWidth]; - int i; - for (i = 0; i < pSurfx->m_nWidth; i += step) - sinw.f[i] = sinf((float)i / pSurfx->m_nWidth * PI + halfPI); + sinWidth = width; + sinTable = new float [width]; + for (int n = 0; n < width; n += segment) + sinTable[n] = sinf((float)n / width * D3DX_PI + (float)D3DX_PI / 2); } - if (mode_spectre == 3) + if (s_waveMode == 3) { - if (y1 < 0) y1 = 0; - if (y2 < 0) y2 = 0; + if (right < 0) right = 0; + if (left < 0) left = 0; } - halfheight = pSurfx->m_nHeight >> 1; - halfwidth = pSurfx->m_nWidth >> 1; - - int i; - for (i = step; i < pSurfx->m_nWidth; i += step) + for (int n = segment; n < width; n += segment) { - old_y1 = y1; - old_y2 = y2; - - y1 = (float)(((m_pcmRight[(i << 8) / pSurfx->m_nWidth / density_lines] >> 8) * spectral_amplitude * pSurfx->m_nHeight) >> 12); - y2 = (float)(((m_pcmLeft[(i << 8) / pSurfx->m_nWidth / density_lines] >> 8) * spectral_amplitude * pSurfx->m_nHeight) >> 12); + float lastRight = right, lastLeft = left; + int slot = (n << 8) / width / 5; + right = (float)(((m_pcmRight[slot] >> 8) * s_waveAmplitude * height) >> 12); + left = (float)(((m_pcmLeft[slot] >> 8) * s_waveAmplitude * height) >> 12); - switch (mode_spectre) + switch (s_waveMode) { case 0: - pSurfx->Line(i - step, (int)(halfheight + shift + old_y2), - i, (int)(halfheight + shift + y2), - spectral_color); + pSurfx->Line(n - segment, (int)(midY + shift + lastLeft), n, (int)(midY + shift + left), s_waveColor); break; case 1: - pSurfx->Line(i - step, (int)(halfheight + shift + old_y1), - i, (int)(halfheight + shift + y1), - spectral_color); - pSurfx->Line(i - step, (int)(halfheight - shift + old_y2), - i, (int)(halfheight - shift + y2), - spectral_color); + pSurfx->Line(n - segment, (int)(midY + shift + lastRight), n, (int)(midY + shift + right), s_waveColor); + pSurfx->Line(n - segment, (int)(midY - shift + lastLeft), n, (int)(midY - shift + left), s_waveColor); break; case 2: - pSurfx->Line(i - step, (int)(halfheight + shift + old_y1), - i, (int)(halfheight + shift + y1), - spectral_color); - pSurfx->Line(i - step, (int)(halfheight - shift + old_y1), - i, (int)(halfheight - shift + y1), - spectral_color); - pSurfx->Line((int)(halfwidth + shift + old_y2), i - step, - (int)(halfwidth + shift + y2), i, - spectral_color); - pSurfx->Line((int)(halfwidth - shift + old_y2), i - step, - (int)(halfwidth - shift + y2), i, - spectral_color); + pSurfx->Line(n - segment, (int)(midY + shift + lastRight), n, (int)(midY + shift + right), s_waveColor); + pSurfx->Line(n - segment, (int)(midY - shift + lastRight), n, (int)(midY - shift + right), s_waveColor); + pSurfx->Line((int)(midX + shift + lastLeft), n - segment, (int)(midX + shift + left), n, s_waveColor); + pSurfx->Line((int)(midX - shift + lastLeft), n - segment, (int)(midX - shift + left), n, s_waveColor); break; case 3: - if (y1 < 0) y1 = 0; - if (y2 < 0) y2 = 0; - // FALL THROUGH - + if (right < 0) right = 0; + if (left < 0) left = 0; + // fall through case 4: - pSurfx->Line( - (int)(halfwidth + cosw.f[i - step] * (shift + old_y1)), - (int)(halfheight + sinw.f[i - step] * (shift + old_y1)), - (int)(halfwidth + cosw.f[i] * (shift + y1)), - (int)(halfheight + sinw.f[i] * (shift + y1)), - spectral_color); - pSurfx->Line( - (int)(halfwidth - cosw.f[i - step] * (shift + old_y2)), - (int)(halfheight + sinw.f[i - step] * (shift + old_y2)), - (int)(halfwidth - cosw.f[i] * (shift + y2)), - (int)(halfheight + sinw.f[i] * (shift + y2)), - spectral_color); + pSurfx->Line((int)(midX + cosTable[n - segment] * (shift + lastRight)), (int)(midY + sinTable[n - segment] * (shift + lastRight)), + (int)(midX + cosTable[n] * (shift + right)), (int)(midY + sinTable[n] * (shift + right)), s_waveColor); + pSurfx->Line((int)(midX - cosTable[n - segment] * (shift + lastLeft)), (int)(midY + sinTable[n - segment] * (shift + lastLeft)), + (int)(midX - cosTable[n] * (shift + left)), (int)(midY + sinTable[n] * (shift + left)), s_waveColor); break; } } - if (mode_spectre == 3 || mode_spectre == 4) + if (s_waveMode == 3 || s_waveMode == 4) { - pSurfx->Line( - (int)(halfwidth + cosw.f[pSurfx->m_nWidth - step] * (shift + y1)), - (int)(halfheight + sinw.f[pSurfx->m_nWidth - step] * (shift + y1)), - (int)(halfwidth - cosw.f[pSurfx->m_nWidth - step] * (shift + y2)), - (int)(halfheight + sinw.f[pSurfx->m_nWidth - step] * (shift + y2)), - spectral_color); + int last = width - segment; + pSurfx->Line((int)(midX + cosTable[last] * (shift + right)), (int)(midY + sinTable[last] * (shift + right)), + (int)(midX - cosTable[last] * (shift + left)), (int)(midY + sinTable[last] * (shift + left)), s_waveColor); } -#define curve_color 255 -#define curve_amplitude 50 - static int x_curve = 0; - - // Lissajous curve overlay + const float amplitude = 50.0f / 256; + const float spin = 0.001f; + const float base = 80.0f; + int k = curveStep; + for (int trail = 0; trail < 2; trail++) { - int i, j, k; - float v, vr; - float x, y; - float amplitude = (float)curve_amplitude / 256; - - for (j = 0; j < 2; j += 1) + k = curveStep; + for (int n = 0; n < 64; n++, k++) { - v = 80; - vr = 0.001f; - k = x_curve; - for (i = 0; i < 64; i += 1) - { - x = cosf((float)(k) / (v + v * j * 1.34f)) * pSurfx->m_nHeight * amplitude; - y = sinf((float)(k) / (1.756f * (v + v * j * 0.93f))) * pSurfx->m_nHeight * amplitude; - SetPixel2(pSurfx, (int)(x * cosf((float)k * vr) + y * sinf((float)k * vr) + pSurfx->m_nWidth / 2), (int)(x * sinf((float)k * vr) - y * cosf((float)k * vr) + pSurfx->m_nHeight / 2), curve_color); - k++; - } + float cx = cosf((float)k / (base + base * trail * 1.34f)) * height * amplitude; + float cy = sinf((float)k / (1.756f * (base + base * trail * 0.93f))) * height * amplitude; + float a = (float)k * spin; + PlotDot(pSurfx, (int)(cx * cosf(a) + cy * sinf(a) + width / 2), (int)(cx * sinf(a) - cy * cosf(a) + height / 2), 255); } - - x_curve = k; } + curveStep = k; } -#endif // _XBOX. end CAudioVisualizer Xbox-only block +#endif // _XBOX // ===== FFT ================================================================== From a8bfda576d6e89d5a7d2d43e52b7263ca2740cd1 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:33:38 -0500 Subject: [PATCH 057/167] Rewrote the visualizer FFT as a plain radix-2 transform Wrote the 256 point FFT fresh as the textbook iterative Cooley-Tukey, same shape as the routines at 0x5f040..0x5f290, and refreshed the file header. Spectra match bit for bit on 50 test buffers, and so does every analyzer frame built on them. --- docs/decomp/TmapSystem.md | 18 +++- theseus/render/tmap_system.cpp | 169 +++++++++++---------------------- 2 files changed, 69 insertions(+), 118 deletions(-) diff --git a/docs/decomp/TmapSystem.md b/docs/decomp/TmapSystem.md index d4cb183..3f3a459 100644 --- a/docs/decomp/TmapSystem.md +++ b/docs/decomp/TmapSystem.md @@ -428,8 +428,16 @@ below `(4 * lastMax + 6 * lastMin) / 10`. `d1, d2 = 80, 140.48` and `187.2, 271.13`. `k` starts from a global (`0x00188f40`) each trail and the second trail's end is stored back. -## Notes - -- CSurfx operates on a raw BYTE array with manual pitch calculation. -- The FFT is a standard radix-2 Cooley-Tukey with precomputed twiddle factors. Buffer size is 256 samples (FFT_BUFFER_SIZE_LOG=8). -- CDynamicTexture uses the Xbox Swizzler API for texture upload. +## FFT + +A plain 256 point radix-2 decimation-in-time FFT. + +- **Tables (`0x0005f040`).** Allocates the 0x800 byte state (256 real + 256 imaginary floats), + fills a 256 entry bit-reverse table (`0x0017ccf8`) and 128 entry cos/sin tables + (`0x0017c8f8`, `0x0017caf8`) of `i * 2pi/256` (`0x0002bf40`). +- **Load (`0x0005f0f0`).** `re[i] = sample[bitrev[i]]`, `im[i] = 0`, then the passes and the + power step. +- **Passes (`0x0005f290`).** 8 passes, butterfly span 1, 2, .. 128; twiddle `j * (128 / span)`; + `t = (re*c - im*s, im*c + re*s)`, `b = a - t`, `a = a + t`. +- **Power (`0x0005f140`).** `out[i] = re^2 + im^2` for bins 0..128 (unrolled by 4), then bins 0 + and 128 times 0.25 (`0x0002beec`). diff --git a/theseus/render/tmap_system.cpp b/theseus/render/tmap_system.cpp index 8f7d872..3780bfb 100644 --- a/theseus/render/tmap_system.cpp +++ b/theseus/render/tmap_system.cpp @@ -1,15 +1,10 @@ -// tmap_system.cpp: TMAP (dynamic texture) rendering pipeline. +// tmap_system.cpp: the music visualizer's texture effects. // -// CSurfx 8-bit palettized pixel surface with line drawing -// DeltaField 2D vector-field pixel-displacement effects -// CPalette HSV palette generator with timed crossfades -// CDynamicTexture Renders children into a palettized texture each frame -// CImageFader Delta-field effect applied to a CSurfx -// CAudioVisualizer PCM / FFT audio visualisation rendered to CSurfx -// FFT Radix-2 Cooley-Tukey -// -// Decompiled from the 5960 retail XBE; see docs/decomp/TmapSystem.md. -// All node types confirmed in 5960 retail XBE. +// A DynamicTexture owns an 8-bit CSurfx; each frame its children draw into it +// (ImageFader warps last frame through a DeltaField, AudioVisualizer draws the +// music, Palette paints a test ramp) and the result goes up as a paletted +// texture. Rebuilt function by function from the 5960 retail XBE; the +// findings are in docs/decomp/TmapSystem.md. #include "std.h" #include "theseus.h" @@ -1912,47 +1907,46 @@ void CAudioVisualizer::RenderEffect2(CSurfx* pSurfx) // ===== FFT ================================================================== -// Radix-2 Cooley-Tukey FFT. Input: FFT_BUFFER_SIZE sound_samples. -// Output: FFT_BUFFER_SIZE/2+1 intensity floats. Pure math, used by both -// Xbox CAudioVisualizer above and desktop's CAudioVisualizer in -// desktop/desktop_nodes.cpp. +// 256 point radix-2 FFT returning the power of bins 0..128. Shared with the +// desktop CAudioVisualizer in desktop/desktop_nodes.cpp. +// +// Written from first principles: the textbook iterative decimation-in-time +// Cooley-Tukey transform (bit reversed load, log2(N) butterfly passes with +// cos/sin twiddle tables). Same shape as the 5960 routines @ 0x0005f040 +// (tables), 0x0005f0f0 (load), 0x0005f290 (passes) and 0x0005f140 (power, +// with bins 0 and N/2 quartered via 0x0002beec), so the output matches. -#undef PI #define PI 3.14159265358979323846f struct _struct_fft_state { - float real [FFT_BUFFER_SIZE]; - float imag [FFT_BUFFER_SIZE]; + float re [FFT_BUFFER_SIZE]; + float im [FFT_BUFFER_SIZE]; }; -static void fft_prepare(const sound_sample *input, float * re, float * im); -static void fft_calculate(float * re, float * im); -static void fft_output(const float *re, const float *im, float *output); -static int reverseBits(unsigned int initial); - -static unsigned int bitReverse [FFT_BUFFER_SIZE]; - -static float sintable [FFT_BUFFER_SIZE / 2]; -static float costable [FFT_BUFFER_SIZE / 2]; +static unsigned int s_bitReverse [FFT_BUFFER_SIZE]; +static float s_cos [FFT_BUFFER_SIZE / 2]; +static float s_sin [FFT_BUFFER_SIZE / 2]; fft_state* fft_init(void) { - fft_state *state; - unsigned int i; - - state = (fft_state*)malloc(sizeof (fft_state)); + fft_state* state = (fft_state*)malloc(sizeof(fft_state)); if (state == NULL) return NULL; - for (i = 0; i < FFT_BUFFER_SIZE; i++) - bitReverse[i] = reverseBits(i); + for (unsigned int n = 0; n < FFT_BUFFER_SIZE; n++) + { + unsigned int rev = 0; + for (int bit = 0; bit < FFT_BUFFER_SIZE_LOG; bit++) + rev |= ((n >> bit) & 1) << (FFT_BUFFER_SIZE_LOG - 1 - bit); + s_bitReverse[n] = rev; + } - for (i = 0; i < FFT_BUFFER_SIZE / 2; i++) + for (unsigned int n = 0; n < FFT_BUFFER_SIZE / 2; n++) { - float j = 2.0f * PI * (float)i / (float)FFT_BUFFER_SIZE; - costable[i] = cosf(j); - sintable[i] = sinf(j); + float angle = 2.0f * PI * (float)n / (float)FFT_BUFFER_SIZE; + s_cos[n] = cosf(angle); + s_sin[n] = sinf(angle); } return state; @@ -1960,93 +1954,42 @@ fft_state* fft_init(void) void fft_perform(const sound_sample* input, float* output, fft_state* state) { - fft_prepare(input, state->real, state->imag); - fft_calculate(state->real, state->imag); - fft_output(state->real, state->imag, output); -} - -void fft_close(fft_state *state) -{ - if (state != NULL) - free(state); -} - -static void fft_prepare(const sound_sample* input, float* re, float* im) -{ - unsigned int i; - float* realptr = re; - float* imagptr = im; - - for (i = 0; i < FFT_BUFFER_SIZE; i++) - { - *realptr++ = input[bitReverse[i]]; - *imagptr++ = 0; - } -} - -static void fft_output(const float* re, const float* im, float* output) -{ - float* outputptr = output; - const float* realptr = re; - const float* imagptr = im; - float* endptr = output + FFT_BUFFER_SIZE / 2; + float* re = state->re; + float* im = state->im; - while (outputptr <= endptr) + for (int n = 0; n < FFT_BUFFER_SIZE; n++) { - *outputptr = (*realptr * *realptr) + (*imagptr * *imagptr); - outputptr++; realptr++; imagptr++; + re[n] = input[s_bitReverse[n]]; + im[n] = 0.0f; } - *output /= 4; - *endptr /= 4; -} - -static void fft_calculate(float* re, float* im) -{ - unsigned int i, j, k; - unsigned int exchanges; - float fact_real, fact_imag; - float tmp_real, tmp_imag; - unsigned int factfact; - - exchanges = 1; - factfact = FFT_BUFFER_SIZE / 2; - - for (i = FFT_BUFFER_SIZE_LOG; i != 0; i--) + for (int half = 1; half < FFT_BUFFER_SIZE; half *= 2) { - for (j = 0; j != exchanges; j++) + int stride = FFT_BUFFER_SIZE / (2 * half); + for (int j = 0; j < half; j++) { - fact_real = costable[j * factfact]; - fact_imag = sintable[j * factfact]; - - for (k = j; k < FFT_BUFFER_SIZE; k += exchanges << 1) + float wr = s_cos[j * stride]; + float wi = s_sin[j * stride]; + for (int a = j; a < FFT_BUFFER_SIZE; a += 2 * half) { - int k1 = k + exchanges; - - tmp_real = fact_real * re[k1] - fact_imag * im[k1]; - tmp_imag = fact_real * im[k1] + fact_imag * re[k1]; - re[k1] = re[k] - tmp_real; - im[k1] = im[k] - tmp_imag; - re[k] += tmp_real; - im[k] += tmp_imag; + int b = a + half; + float tr = wr * re[b] - wi * im[b]; + float ti = wr * im[b] + wi * re[b]; + re[b] = re[a] - tr; + im[b] = im[a] - ti; + re[a] += tr; + im[a] += ti; } } - - exchanges <<= 1; - factfact >>= 1; } + + for (int n = 0; n <= FFT_BUFFER_SIZE / 2; n++) + output[n] = re[n] * re[n] + im[n] * im[n]; + output[0] /= 4; + output[FFT_BUFFER_SIZE / 2] /= 4; } -static int reverseBits(unsigned int initial) +void fft_close(fft_state* state) { - unsigned int reversed = 0, loop; - - for (loop = 0; loop < FFT_BUFFER_SIZE_LOG; loop++) - { - reversed <<= 1; - reversed += (initial & 1); - initial >>= 1; - } - - return reversed; + free(state); } From 7c345b4ecbacf358e722637b4c9d62bc4c0479b1 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:36:54 -0500 Subject: [PATCH 058/167] Wrote the TMAP warp math in the binary's order Put the warp style formulas and coordinate math in the operand order the binary uses, and rewrote the header comments. Fields, fades and textures still match byte for byte. --- theseus/render/tmap_system.cpp | 60 +++++++++++++++++----------------- theseus/render/tmap_system.h | 42 ++++++++++-------------- 2 files changed, 47 insertions(+), 55 deletions(-) diff --git a/theseus/render/tmap_system.cpp b/theseus/render/tmap_system.cpp index 3780bfb..df76334 100644 --- a/theseus/render/tmap_system.cpp +++ b/theseus/render/tmap_system.cpp @@ -671,7 +671,7 @@ int DeltaField::CalcSome() if (mCurrentY > 0 && mCurrentY < mHeight - 1) { - mY_Cord = 0.5f * mYScale * (mHeight - 2 * mCurrentY); + mY_Cord = (mHeight - mCurrentY * 2) * mYScale * 0.5f; float toFixedX = 32.0f / mXScale; float toFixedY = 32.0f / mYScale; @@ -680,7 +680,7 @@ int DeltaField::CalcSome() for (long px = 1; px < mWidth - 1; px++, cell += 3) { - mX_Cord = 0.5f * mXScale * (2 * px - mWidth); + mX_Cord = (px * 2 - mWidth) * mXScale * 0.5f; if (mHasRTerm) mR_Cord = sqrtf(mX_Cord * mX_Cord + mY_Cord * mY_Cord); if (mHasThetaTerm) @@ -754,19 +754,19 @@ void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& case 2: if (bInit) A0 = trunc(8.0f * rnd(5.0f)) * pi; - X = ((1.0f + sinf(A0 * theta)) * 0.5f * 0.06f + 0.92f) * r; + X = ((sinf(A0 * theta) + 1.0f) * 0.5f * 0.06f + 0.92f) * r; Y = theta; return; case 3: if (bInit) { - A0 = 2.0f + rnd(16.0f); - A1 = 0.01f + rnd(0.05f); - A2 = 0.002f + rnd(0.006f); + A0 = rnd(16.0f) + 2.0f; + A1 = rnd(0.05f) + 0.01f; + A2 = rnd(0.006f) + 0.002f; } X = pos(r - A2); - Y = theta + A1 * sinf(A0 * r); + Y = sinf(A0 * r) * A1 + theta; return; case 4: @@ -782,11 +782,11 @@ void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& case 6: if (bInit) { - A0 = 2.0f + rnd(16.0f); - A1 = 0.01f + rnd(0.05f); + A0 = rnd(16.0f) + 2.0f; + A1 = rnd(0.05f) + 0.01f; } - X = x + A1 * sinf(A0 * y); - Y = y + A1 * sinf(A0 * x); + X = sinf(A0 * y) * A1 + x; + Y = sinf(A0 * x) * A1 + y; bPolar = false; return; @@ -805,8 +805,8 @@ void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& if (bInit) { A0 = rnd(0.0045f); - A1 = 0.1f + rnd(0.06f); - A2 = 0.003f + (m_nStyle == 9 ? rnd(0.005f) : rnd(0.006f)); + A1 = rnd(0.06f) + 0.1f; + A2 = (m_nStyle == 9 ? rnd(0.005f) : rnd(0.006f)) + 0.003f; } X = (0.99f - A1 * powf(fabsf(m_nStyle == 9 ? x : y), 1.3f)) * r - rnd(A0); Y = m_nStyle == 9 ? theta - A2 : theta + A2; @@ -823,16 +823,16 @@ void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& return; case 13: - X = x + rnd(0.01f) - 0.005f; - Y = y - rnd(0.01f) - 0.005f - (y - 1.3f) * 0.04f; + X = rnd(0.01f) + x - 0.005f; + Y = y - rnd(0.01f) - 0.005f - 0.04f * (y - 1.3f); bPolar = false; return; case 14: if (bInit) - A0 = 0.95f + rnd(0.06f); + A0 = rnd(0.06f) + 0.95f; X = r * A0; - Y = theta - pos(1.0f - r) * 0.035f; + Y = theta - 0.035f * pos(1.0f - r); return; case 15: @@ -841,14 +841,14 @@ void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& return; case 16: - X = 0.9f * x + rnd(0.008f) - 0.004f; - Y = 0.9f * y + rnd(0.008f) - 0.004f; + X = rnd(0.008f) + x * 0.9f - 0.004f; + Y = rnd(0.008f) + y * 0.9f - 0.004f; bPolar = false; return; case 17: - X = x + 0.1f * (rnd(2.0f) - 1.0f); - Y = y + 0.1f * (rnd(2.0f) - 1.0f); + X = (rnd(2.0f) - 1.0f) * 0.1f + x; + Y = (rnd(2.0f) - 1.0f) * 0.1f + y; bPolar = false; return; @@ -858,17 +858,17 @@ void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& return; case 19: - X = (0.92f + rnd(0.05f)) * r; + X = (rnd(0.05f) + 0.92f) * r; Y = theta + 0.003f; return; case 20: - X = r + 0.04f * sinf(6.2831853f * r); + X = sinf(r * 6.2831853f) * 0.04f + r; Y = theta + 0.015f; return; case 21: - X = r * (0.87f + 0.05f * (1.0f + sinf(r * 15.0f))); + X = ((sinf(r * 15.0f) + 1.0f) * 0.05f + 0.87f) * r; Y = theta; return; @@ -876,17 +876,17 @@ void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& if (bInit) { A0 = rnd(0.1f); - A1 = 3.0f + rnd(7.0f); - A2 = 0.9f + rnd(0.3f); + A1 = rnd(7.0f) + 3.0f; + A2 = rnd(0.3f) + 0.9f; } - X = r * (1.0f + 0.13f * (r - A2)); - Y = theta + A0 * sinf(A1 * r); + X = ((r - A2) * 0.13f + 1.0f) * r; + Y = sinf(A1 * r) * A0 + theta; return; case 23: if (bInit) A0 = trunc(1.0f + rnd(2.1f)) * pi; - X = ((1.0f + cosf(A0 * theta)) * 0.5f * 0.1f + 0.89f) * r; + X = ((cosf(A0 * theta) + 1.0f) * 0.5f * 0.1f + 0.89f) * r; Y = theta + 0.005f; return; @@ -894,7 +894,7 @@ void DeltaField::GetXY(float r, float theta, float x, float y, float& X, float& m_nStyle = 0; // fall through case 0: - X = r * (1.0f + 0.16f * atanf(0.55f - r)); + X = (atanf(0.55f - r) * 0.16f + 1.0f) * r; Y = theta - 0.01f; return; } diff --git a/theseus/render/tmap_system.h b/theseus/render/tmap_system.h index 1e69b2c..2c1483e 100755 --- a/theseus/render/tmap_system.h +++ b/theseus/render/tmap_system.h @@ -1,5 +1,7 @@ #pragma once +// A finished delta field: how many rows above a pixel can pull from, and the +// 3 byte cells (see CSurfx::Fade). class DeltaFieldData { public: @@ -11,6 +13,8 @@ class DeltaFieldData }; +// 8-bit palettized drawing surface for the TMAP effects. Layout follows the +// 5960 object at 0x0005f3f0. class CSurfx { public: @@ -34,49 +38,37 @@ class CSurfx inline BYTE* Pixel(int x, int y) { return &m_pels[mBytesPerRow * y + x]; } + // Live surface count and the fade scratch buffer they share. static int sTempRef; static long sTempSize; static char* sTemp; private: - // Need this to prevent the compiler from using default copy ctor - CSurfx(const CSurfx&); - CSurfx& operator=(const CSurfx& rhs); + // Owns its pixels, so no copies. + CSurfx(const CSurfx&); + CSurfx& operator=(const CSurfx& rhs); }; +// Builds a warp field a few rows at a time so a style change doesn't stall a frame. class DeltaField { public: DeltaField(); - // Suck in a new grad field. Resize must be called after Assign(). + // Resets the flags and restarts the field at the current size. void Assign(); - // Reinitiate/reset the computation of this grad field. + // Restarts the field if the size changed (or when forced). void SetSize(long inWidth, long inHeight, bool inForceRegen = false); - // Compute a small portion of the grad field. Call GetField() to see if it finished. + // Computes the next row; returns how many are left. int CalcSome(); - // See if this delta field is 100% calculated - bool IsCalculated() - { - return mCurrentY == mHeight; - } - - bool IsCalculating() - { - return mCurrentY >= 0 && mCurrentY != mHeight; - } - - bool IsUninitialized() - { - return mCurrentY == -1; - } - - // Returns a ptr to the buf of this grad field. - // Note: If the field is not 100% calculated, it will finish calculating - // and may take a couple seconds. + bool IsCalculated() { return mCurrentY == mHeight; } + bool IsCalculating() { return mCurrentY >= 0 && mCurrentY != mHeight; } + bool IsUninitialized() { return mCurrentY == -1; } + + // The field, finished first if it wasn't; NULL if it was never sized. DeltaFieldData* GetField(); void GetXY(float r, float theta, float x, float y, float& X, float& Y, bool& bPolar, bool bInit); From 0d5a725cebf3f302f32f345e1c05cd5627586dfb Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:38:29 -0500 Subject: [PATCH 059/167] Skip texture writes when a lock fails The palette and dynamic texture updates wrote through whatever the lock handed back even when the lock failed. They bail out now instead. --- theseus/render/tmap_system.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/theseus/render/tmap_system.cpp b/theseus/render/tmap_system.cpp index df76334..4f14274 100644 --- a/theseus/render/tmap_system.cpp +++ b/theseus/render/tmap_system.cpp @@ -1036,13 +1036,15 @@ void CPalette::Update() #ifdef _XBOX if (m_pPalette == NULL) - VERIFYHR(TheseusGetD3DDev()->CreatePalette(D3DPALETTE_256, &m_pPalette)); + TheseusGetD3DDev()->CreatePalette(D3DPALETTE_256, &m_pPalette); + // A failed create or lock would leave nothing to write into. D3DCOLOR* entries; - VERIFYHR(m_pPalette->Lock(&entries, D3DLOCK_NOOVERWRITE)); + if (m_pPalette == NULL || FAILED(m_pPalette->Lock(&entries, D3DLOCK_NOOVERWRITE))) + return; for (int n = 0; n < 256; n++) entries[n] = 0xFF000000 | (m_palette[n] & 0x00FFFFFF); - VERIFYHR(m_pPalette->Unlock()); + m_pPalette->Unlock(); #endif } @@ -1172,7 +1174,7 @@ LPDIRECT3DTEXTURE8 CDynamicTexture::GetTextureSurface() #ifdef _XBOX if (m_palette != NULL && m_palette->IsKindOf(NODE_CLASS(CPalette))) - VERIFYHR(TheseusGetD3DDev()->SetPalette(0, ((CPalette*)m_palette)->m_pPalette)); + TheseusGetD3DDev()->SetPalette(0, ((CPalette*)m_palette)->m_pPalette); #endif return m_surface; @@ -1216,7 +1218,8 @@ void CDynamicTexture::Update() (void)rgpe; Swizzler swz(m_nImageWidth, m_nImageHeight, 1); D3DLOCKED_RECT lr; - VERIFYHR(m_surface->LockRect(0, &lr, NULL, D3DLOCK_DISCARD)); + if (FAILED(m_surface->LockRect(0, &lr, NULL, D3DLOCK_DISCARD))) + return; BYTE* dst = (BYTE*)lr.pBits; const BYTE* src = m_pSurfx->m_pels; for (int y = 0; y < m_size; y++, src += m_size) @@ -1226,10 +1229,11 @@ void CDynamicTexture::Update() swz.IncV(); swz.AddU(swz.SwizzleU(m_nImageWidth - m_size)); } - VERIFYHR(m_surface->UnlockRect(0)); + m_surface->UnlockRect(0); #else D3DLOCKED_RECT lr; - VERIFYHR(m_surface->LockRect(0, &lr, NULL, D3DLOCK_DISCARD)); + if (FAILED(m_surface->LockRect(0, &lr, NULL, D3DLOCK_DISCARD))) + return; uint32_t* pBase = (uint32_t*)lr.pBits; // Texture is m_nImageWidth wide but we only fill m_size; respect // the row stride or the data ends up smeared diagonally. @@ -1331,7 +1335,7 @@ void CDynamicTexture::Update() } } } - VERIFYHR(m_surface->UnlockRect(0)); + m_surface->UnlockRect(0); #endif } } From fd4d0984c8559fc7f10733124efbbd1ca5ef49af Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:01:29 -0500 Subject: [PATCH 060/167] Rebuilt the shape nodes and effect shaders from the 5960 binary Wrote Shape, Appearance, Material, Box, Sphere, the cube builder, the shader tables and lookups, CompressNormal and the shader constant setters fresh from 0x50cf0..0x52aa0 and 0x5c6c0..0x5ca00, and wrote up what the binary does in the ShapeRender doc. The recorded draw, state, shader and constant calls match the old code on all 1373 shapes in the shipped XAPs, desktop and Xbox paths. --- docs/decomp/ShapeRender.md | 283 +++++++++- theseus/render/shape_render.cpp | 932 +++++++++++++++----------------- 2 files changed, 722 insertions(+), 493 deletions(-) diff --git a/docs/decomp/ShapeRender.md b/docs/decomp/ShapeRender.md index 37f1d2f..2b22467 100644 --- a/docs/decomp/ShapeRender.md +++ b/docs/decomp/ShapeRender.md @@ -11,29 +11,308 @@ Shape rendering and vertex shader system in the 4920-5960 retail binary. | CMaterial | 0x00028734 | "Material" | 0x00028748 | | CSphere | 0x0002864c | "Sphere" | 0x0002865c | +Box is registered too: vtable `0x000285c0`, class object `0x001880f0`, prop map `0x00175130`. + +| Class | vtable | Factory | Size | +|-------|--------|---------|------| +| Shape | 0x000284a0 | 0x00050cf0 | 0x30 | +| Appearance | 0x00028500 | 0x00050f50 | 0x24 | +| Material | 0x00028560 | 0x00051300 | 0x90 | +| Sphere | 0x00028440 | 0x000529d0 | 0x50 | +| Box | 0x000285c0 | 0x000526c0 | 0x37c | + +Virtual slots used below: 4 Release, 6 GetNodeClass, 15 Advance, 16 Render, 17 GetBBox, +18 GetRadius, 22 GetTextureSurface, 23 (MeshNode) Init. + ## CShape Geometry + appearance container. Binds a geometry child (Sphere, mesh, etc.) to an Appearance node for rendering. +| Offset | Field | +|--------|-------| +| 0x1c | appearance | +| 0x20 | geometry | +| 0x24..0x2c | world position after the last draw | + +The destructor (`0x00050d60`) releases appearance, then geometry. + +**Render (`0x00050dd0`).** + +```c +if (geometry == NULL) return; +for (cls = geometry->GetNodeClass(); cls; cls = cls->base) // class chain walk + if (cls == &MeshNode_class /* 0x001883e8 */) { + mesh = geometry; + if (mesh->dirty) { + if (mesh->url && *mesh->url) Xap_load(mesh->url); + mesh->Init(); // slot 23 + if (mesh->core == NULL) return; + } + if (mesh->core == NULL || mesh->core->GetFVF() == 0) return; + g_pRenderMeshNode /* 0x0017e758 */ = mesh; + SetVertexShader(GetFixedFunctionShader(fvf)); // 0x0005c740 + break; + } +if (appearance) appearance->Render(); +geometry->Render(); +copy 16 dwords of the device's world matrix (pDevice + 0xde0); +position = row 3 (x, y, z); g_pRenderMeshNode = NULL; +``` + +**GetBBox (`0x00050eb0`)** tail calls the geometry's GetBBox, or zeroes all six floats. +**GetRadius (`0x00050ef0`)** returns the geometry's radius, or 0.0. +**Advance (`0x00050f10`)** runs the CNode advance, then geometry, then appearance. + ## CAppearance Material + texture binding. Links a CMaterial and texture references for a given shape. +Material at `0x1c`, texture at `0x20`. The destructor (`0x00050fc0`) releases them in that order. + +**Render (`0x00051030`).** Every device call is inlined into shadow state writes and push +buffer packets. Decoded, in order: + +| State | Value | +|-------|-------| +| CULLMODE | CCW (`0x901` in the Xbox encoding) | +| ZWRITEENABLE | 0 | +| ALPHABLENDENABLE | 1 | +| SRCBLEND | SRCALPHA (`0x302`) | +| DESTBLEND | INVSRCALPHA (`0x303`) | +| stage 0 COLOROP, ALPHAOP | DISABLE | +| stage 0 TEXCOORDINDEX | 0 | +| stage 0 TEXTURETRANSFORMFLAGS | 0 | +| stage 1 COLOROP, ALPHAOP | DISABLE | +| texture stages 0 and 1 | NULL | + +Then, with a texture node: its Render, then GetTextureSurface. With a surface it goes on stage 0 +and COLOROP/ALPHAOP become SELECTARG1 of TEXTURE. Without one ALPHAOP, ALPHAARG1, COLOROP, +COLORARG1 become SELECTARG1 of TFACTOR and TEXTUREFACTOR is set to 0, so the shape draws clear +until its image arrives. The material node renders last. + +**Advance (`0x000512c0`)**: CNode advance, material, texture. + ## CMaterial VRML97 material properties: diffuse color, emissive color, specular color, transparency. Standard Phong-style lighting parameters. +| Offset | Field | Default | +|--------|-------|---------| +| 0x1c | ambientIntensity | 0.2 | +| 0x20 | diffuseColor | 0.8, 0.8, 0.8 | +| 0x2c | emissiveColor | 0, 0, 0 | +| 0x38 | shininess | 0.2 | +| 0x3c | specularColor | 0, 0, 0 | +| 0x48 | transparency | 0 | +| 0x4c | D3DMATERIAL8 | | + +**Render (`0x000513a0`).** + +```c +a = 1.0 - transparency; +Diffuse = (diffuseColor, a); +Ambient = (ambientIntensity, ambientIntensity, ambientIntensity, a); +Specular = (specularColor, a); +Emissive = (emissiveColor, a); +Power = shininess; +copy 0x11 dwords to pDevice + 0xf00; dirty flags |= 0x9000; // SetMaterial, inlined +``` + +Material has no destructor of its own; its vtable points at the shared scalar deleting +destructor. + +## Box + +None of these are defined functions in the project database; they read straight from the +disassembly. + +| Offset | Field | +|--------|-------| +| 0x1c | size, default (1, 1, 1) | +| 0x28 | vertex buffer | +| 0x2c | index buffer | +| 0x30 | 24 vertices, 32 bytes each (position, normal, uv) | +| 0x330 | 36 WORD indices | +| 0x378 | dirty, starts 1 | + +**Render (`0x00052820`).** + +1. If dirty, build the cube into the node's own arrays (`0x00051450`, outside facing) and clear dirty. +2. With no vertex buffer, create a 0x300 byte one through the shared create helper + (`0x0003d410`, which frees mesh and texture caches and retries until it gets memory) and copy + the vertices in with `rep movsd`. +3. Same for the index buffer, 0x48 bytes (`0x0003d390`). +4. With both: SetStreamSource(0, vb, 0x20), SetIndices(ib, 0), SetVertexShader(0x112), then + DrawIndexedVertices(TRIANGLELIST, 36, indices). + +**GetBBox (`0x00052960`)**: center (0, 0, 0), size as set. +**GetRadius (`0x00052990`)**: `sqrt(x*x + y*y + z*z) * 0.5`, in x87. +**Destructor (`0x00052750`)**: releases the vertex buffer, then the index buffer. + +### The cube builder (`0x00051450`) + +Fastcall with the inside flag in CL, vertices in EDX and indices in EAX, size by value on the +stack. Fully unrolled: each vertex is assembled in a 32 byte stack record and copied out with +`rep movsd`. Constants: `0.5` (`0x0002bed4`), `-0.5` (`0x0002bedc`), `1.0` (`0x0002bed8`), +`-1.0` (`0x0002bef0`), `0.01` (`0x0002bfe8`), `0.99` (`0x0002bfa8`). + +Every position is `+-0.5 * size` per axis, texture coordinates are inset to 0.01 and 0.99, and the +faces come in the order -z, +z, +y, -y, +x, -x with their outward normals. The outside facing +corners (sign per axis, then u and v as low 0 / high 1): + +| Face | Corner 0 | Corner 1 | Corner 2 | Corner 3 | +|------|----------|----------|----------|----------| +| -z | - - - / 0 0 | + - - / 1 0 | + + - / 1 1 | - + - / 0 1 | +| +z | + + + / 0 1 | + - + / 0 0 | - - + / 1 0 | - + + / 1 1 | +| +y | - + - / 0 0 | + + - / 1 0 | + + + / 1 1 | - + + / 0 1 | +| -y | + - + / 1 1 | + - - / 1 0 | - - - / 0 0 | - - + / 0 1 | +| +x | + - - / 0 0 | + - + / 1 0 | + + + / 1 1 | + + - / 0 1 | +| -x | - + + / 0 1 | - - + / 0 0 | - - - / 1 0 | - + - / 1 1 | + +The inside facing branch writes the same four corners of each face in reverse order and keeps +the normals. The indices are `0, 1, 2, 2, 3, 0` per face, offset by 4 each face. Box only ever +calls it outside facing. + ## CSphere Procedural sphere mesh generator. Builds vertex/index buffers at runtime from a radius parameter. +A MeshNode (its constructor is inlined) with radius 1.0 at `0x40`, slices 32 at `0x44` and stacks +32 at `0x48`, created dirty. Its destructor (`0x00052a90`) is only the MeshNode one. + +**Init (`0x00052aa0`, MeshNode slot 23).** + +```c +core = MakeSphere(radius, slices, stacks); // 0x0005c390: D3DXCreateSphere + D3DXComputeNormals +if (core) dirty = 0; +``` + ## Vertex Shader System The binary contains vertex shader management for both effect shaders and a fixed-function fallback path. Compressed normals are supported (packed into a single DWORD, expanded in the vertex shader). -Falloff and reflection shader setup is present for environment-mapped surfaces. +Both lookups use 0x14 byte entries: effect number, microcode pointer, FVF, shader handle (0 until +created), and a spare dword. + +**Effect table (`0x00175bb0`, 12 entries).** + +| Effect | Microcode | FVF | +|--------|-----------|-----| +| 1 | 0x001754f0 | 0x20000042 (XYZ, packed normal, diffuse) | +| 1 | 0x001754f0 | 0x20000002 | +| 1 | 0x00175648 | 0x20000102 (with one texture coordinate) | +| 2 | 0x00175ad0 | 0x20000002 | +| 3 | 0x001757a0 | 0x20000102 | +| 4 | 0x001758f8 | 0x20000002 | +| 1 | 0x001754f0 | 0x52 (XYZ, float normal, diffuse) | +| 1 | 0x001754f0 | 0x12 | +| 1 | 0x00175648 | 0x112 | +| 2 | 0x00175ad0 | 0x12 | +| 3 | 0x001757a0 | 0x112 | +| 4 | 0x001758f8 | 0x12 | + +**Fixed function table (`0x00175b74`, 3 entries)**: FVFs `0x20000042`, `0x20000002`, +`0x20000102`, all pointing at the `0x001754f0` microcode, which is never used for them. + +**Microcode.** Five NV2A programs. The first word is `0x2078` with the instruction count in the +high half, then four words per instruction. + +| Address | Instructions | Used for | +|---------|--------------|----------| +| 0x001754f0 | 21 | effect 1 | +| 0x00175648 | 21 | effect 1, textured | +| 0x001757a0 | 21 | effect 3 | +| 0x001758f8 | 29 | effect 4 | +| 0x00175ad0 | 10 | effect 2 | + +**Declarations.** + +| Address | Tokens | +|---------|--------| +| 0x00176660 | stream 0; v0 FLOAT3; v2 NORMPACKED3; v3 D3DCOLOR; end | +| 0x00176674 | stream 0; v0 FLOAT3; v2 NORMPACKED3; end | +| 0x00176688 | stream 0; v0 FLOAT3; v2 NORMPACKED3; v9 FLOAT2; end | +| 0x0017669c | stream 0; v0 FLOAT3; v3 FLOAT3; v6 FLOAT2; end | +| 0x001766b0 | stream 0; v0 FLOAT3; v3 FLOAT3; end | +| 0x001766c0 | stream 0; v0 FLOAT3; v3 NORMPACKED3; v6 FLOAT2; end | +| 0x001766d4 | stream 0; v0 FLOAT3; v3 NORMPACKED3; end | + +The first three are the fixed function ones, 0x14 bytes apart and indexed by table position. + +**GetEffectShader (`0x0005c6c0`).** Effect in ECX, FVF in EAX. + +```c +if (effect == 0) return fvf; +for (i = 0, e = table; e->effect != effect || e->fvf != fvf; i++, e++) + if (i >= 11) return fvf; +if (e->shader == 0) { + if (fvf & 0x20000000) decl = (fvf & 0x100) ? 0x1766c0 : 0x1766d4; + else decl = (fvf & 0x100) ? 0x17669c : 0x1766b0; + CreateVertexShader(decl, e->microcode, &e->shader, 0); // 0x00147cc0 +} +return e->shader; +``` + +**GetFixedFunctionShader (`0x0005c740`).** FVF in EAX. Walks the three entries for a matching FVF +and returns the FVF itself when none matches. On first use it builds the shader inline from the +declaration at `0x00176660 + i * 0x14` with no microcode: size the declaration's constants, +XMemAlloc `0x118` plus that, zero it, mark 16 attribute slots, parse the declaration in, and +store the pointer with bit 0 set as the handle. + +## CompressNormal (`0x0005c820`) + +The vector comes in EAX. The length is computed in x87 (`fsqrt`), its reciprocal in SSE, and each +component multiplied by it. Then `ftol2` of `x * 1023`, `y * 1023` (`0x0002c014`) and `z * 511` +(`0x0002c010`): + +```c +return (ftol(x * 1023) & 0x7ff) | ((ftol(y * 1023) & 0x7ff) << 11) | (ftol(z * 511) << 22); +``` + +`ftol2` truncates through a signed 64-bit integer, so negative components land in the packed +fields as two's complement. + +## Shader Constants + +Constant registers are passed to `D3DDevice_SetVertexShaderConstant1/4` biased by 96, so `0x88` +is c40 and `0x60` is c0. The world matrix is the top of the matrix stack (`0x0017e7fc`, vtable +slot `0x44`), view is `0x0017f468`, projection `0x0017f4e8`. + +Both setters end with the same light direction: `(1, 1, -1, 0)` normalized, transformed by the +transpose of world * view with D3DXVec3TransformNormal, normalized again. + +**SetReflectShaderFrameValues (`0x0005c8d0`).** + +| Register | Value | +|----------|-------| +| c40..c43 | transpose(world * view) | +| c44..c47 | transpose(projection) | +| c48 | (0, 0, 1, 0.5) | +| c49 | light direction | + +Then LIGHTING goes to FALSE (inlined state write). + +**Falloff frame values (`0x0005ca00`).** Runs whenever the world matrix changes. + +| Register | Value | +|----------|-------| +| c0..c3 | transpose(world * view * projection) | +| c10..c13 | transpose(world * view) | +| c5..c8 | inverse(world * view) | +| c9 | (0, 0.5, 1, -1) | +| c4 | light direction | + +Then texture stage 0 is set to NULL and LIGHTING to FALSE, both inlined. + +**Falloff colors.** Not a function of its own: every falloff family material Setup inlines it, +for example CFalloffMatInfo_Setup (`0x00057790`): + +```c +c15 = (side.r, side.g, side.b, side.a * effectAlpha); +c16 = (front.r - side.r, front.g - side.g, front.b - side.b, (front.a - side.a) * effectAlpha); +``` -The global `g_nEffectAlpha` controls scene-wide transparency for fade transitions. +The scene-wide fade `effectAlpha` lives at `0x00175ca0` and starts at 1.0. ## Mesh Normal Smoothing diff --git a/theseus/render/shape_render.cpp b/theseus/render/shape_render.cpp index 26bbec1..13b5a6e 100644 --- a/theseus/render/shape_render.cpp +++ b/theseus/render/shape_render.cpp @@ -1,7 +1,9 @@ -// shape_render.cpp: shape nodes, appearance / material rendering, vertex -// shader setup. CShape, CAppearance, CMaterial, CBox, CSphere geometry -// primitives plus falloff / reflection shader configuration and effect -// shader lookup. Decompiled from the 5960 retail XBE; see +// shape_render.cpp: the nodes that put geometry on screen. +// +// Shape pairs a geometry node with an Appearance (a Material plus a texture), +// Box and Sphere are the two built in geometries, and the rest sets up the +// effect vertex shaders the falloff and reflection materials draw with. +// Rebuilt function by function from the 5960 retail XBE; the findings are in // docs/decomp/ShapeRender.md. #include "std.h" @@ -18,7 +20,9 @@ extern D3DXMATRIX g_matPosition; extern D3DXMATRIX g_matView; extern D3DXMATRIX g_matProjection; -// ===== CShape ===== + +// ===== CShape =============================================================== +// 0x30 bytes: appearance at 0x1c, geometry at 0x20, last world position at 0x24. IMPLEMENT_NODE("Shape", CShape, CNode) @@ -27,44 +31,59 @@ START_NODE_PROPS(CShape, CNode) NODE_PROP(pt_node, CShape, geometry) END_NODE_PROPS() -CShape::CShape() +// CShape::CShape() / ~CShape() +// Reconstructed from 5960 retail XBE, CShape_CreateNode @ 0x00050cf0 and +// CShape_dtor @ 0x00050d60: +// this[7] = 0; this[8] = 0; // appearance, geometry +// ~: if (this[7]) this[7]->Release(); if (this[8]) this[8]->Release(); +CShape::CShape() : + m_appearance(NULL), + m_geometry(NULL) { - m_appearance = NULL; - m_geometry = NULL; } CShape::~CShape() { - if (m_appearance != NULL) + if (m_appearance) m_appearance->Release(); - - if (m_geometry != NULL) + if (m_geometry) m_geometry->Release(); } +// CShape::Render() +// Reconstructed from 5960 retail XBE @ 0x00050dd0. Ghidra decompile, trimmed: +// if (!geometry) return; +// if (class chain of geometry reaches MeshNode) { +// if (mesh->dirty) { if (url && *url) load(url); mesh->Init(); if (!mesh->core) return; } +// if (!mesh->core || !mesh->core->GetFVF()) return; +// g_pRenderMeshNode = mesh; SetVertexShader(GetFixedFunctionShader(fvf)); +// } +// if (appearance) appearance->Render(); +// geometry->Render(); +// position = world matrix row 3 (copied from the device); g_pRenderMeshNode = NULL; +// Initialize() is the binary's inlined load/Init/core test. The profiler +// phases and the per mesh triangle tally are Theseus additions. void CShape::Render() { if (m_geometry == NULL) return; - CMeshNode* pMeshNode = NULL; + CMeshNode* pMesh = NULL; if (m_geometry->IsKindOf(NODE_CLASS(CMeshNode))) { - pMeshNode = (CMeshNode*)m_geometry; - - if (pMeshNode->m_dirty && !pMeshNode->Initialize()) + pMesh = (CMeshNode*)m_geometry; + if (pMesh->m_dirty && !pMesh->Initialize()) return; - DWORD fvf = pMeshNode->GetFVF(); - if (fvf == 0) + DWORD meshFVF = pMesh->GetFVF(); + if (meshFVF == 0) return; - g_pRenderMeshNode = pMeshNode; - - TheseusSetVertexShader(GetFixedFunctionShader(fvf)); + g_pRenderMeshNode = pMesh; + TheseusSetVertexShader(GetFixedFunctionShader(meshFVF)); } - if (m_appearance != NULL) + if (m_appearance) { FramePhaseScope material(FP_TREE_MATERIAL); m_appearance->Render(); @@ -74,25 +93,25 @@ void CShape::Render() FramePhaseScope draw(FP_TREE_DRAW); int trisBefore = g_frameCount[FC_TRIANGLES]; m_geometry->Render(); - if (pMeshNode) - Frame_TallyMesh(this, pMeshNode->m_url, g_frameCount[FC_TRIANGLES] - trisBefore); + if (pMesh) + Frame_TallyMesh(this, pMesh->m_url, g_frameCount[FC_TRIANGLES] - trisBefore); } + D3DXMATRIX world; + TheseusGetTransform(D3DTS_WORLD, &world); + m_position = D3DXVECTOR3(world._41, world._42, world._43); g_pRenderMeshNode = NULL; - - { - D3DXMATRIX mat; - TheseusGetTransform(D3DTS_WORLD, &mat); - - m_position.x = mat.m[3][0]; - m_position.y = mat.m[3][1]; - m_position.z = mat.m[3][2]; - } } +// CShape::GetBBox(pBBox) / GetRadius() / Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x00050eb0, 0x00050ef0, 0x00050f10: +// GetBBox: if (geometry) tail call geometry->GetBBox(p); else zero all six floats; +// GetRadius: return geometry ? geometry->GetRadius() : 0.0f; +// Advance: CNode::Advance(t); geometry->Advance(t); appearance->Advance(t); (each if set) +// The zero fill is the CNode default, so it goes through CNode::GetBBox. void CShape::GetBBox(BBox* pBBox) { - if (m_geometry != NULL) + if (m_geometry) m_geometry->GetBBox(pBBox); else CNode::GetBBox(pBBox); @@ -100,24 +119,21 @@ void CShape::GetBBox(BBox* pBBox) float CShape::GetRadius() { - if (m_geometry == NULL) - return 0.0f; - - return m_geometry->GetRadius(); + return m_geometry ? m_geometry->GetRadius() : 0.0f; } void CShape::Advance(float nSeconds) { CNode::Advance(nSeconds); - - if (m_geometry != NULL) + if (m_geometry) m_geometry->Advance(nSeconds); - - if (m_appearance != NULL) + if (m_appearance) m_appearance->Advance(nSeconds); } -// ===== CAppearance ===== + +// ===== CAppearance ========================================================== +// 0x24 bytes: material at 0x1c, texture at 0x20. IMPLEMENT_NODE("Appearance", CAppearance, CNode) @@ -126,25 +142,42 @@ START_NODE_PROPS(CAppearance, CNode) NODE_PROP(pt_node, CAppearance, texture) END_NODE_PROPS() -CAppearance::CAppearance() +// CAppearance::CAppearance() / ~CAppearance() +// Reconstructed from 5960 retail XBE, CAppearance_CreateNode @ 0x00050f50 and +// CAppearance_dtor @ 0x00050fc0: both pointers start NULL and are released +// material first, then texture. +CAppearance::CAppearance() : + m_material(NULL), + m_texture(NULL) { - m_material = NULL; - m_texture = NULL; } CAppearance::~CAppearance() { - if (m_material != NULL) + if (m_material) m_material->Release(); - - if (m_texture != NULL) + if (m_texture) m_texture->Release(); } +// CAppearance::Render() +// Reconstructed from 5960 retail XBE @ 0x00051030. The device calls are +// inlined into push buffer writes; decoded they are: +// CULLMODE = CCW; ZWRITEENABLE = 0; ALPHABLENDENABLE = 1; +// SRCBLEND = SRCALPHA (0x302); DESTBLEND = INVSRCALPHA (0x303); +// stage 0: COLOROP = ALPHAOP = DISABLE, TEXCOORDINDEX = 0, TEXTURETRANSFORMFLAGS = 0; +// stage 1: COLOROP = ALPHAOP = DISABLE; SetTexture(0, NULL); SetTexture(1, NULL); +// if (texture) { +// texture->Render(); surf = texture->GetTextureSurface(); +// if (surf) { SetTexture(0, surf); COLOROP/ALPHAOP = SELECTARG1 of TEXTURE; } +// else { ALPHAOP/COLOROP = SELECTARG1 of TFACTOR; TEXTUREFACTOR = 0; } +// } +// if (material) material->Render(); +// Every shape starts from this blend and stage state, so the order here is the +// order the binary writes it. void CAppearance::Render() { TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); - TheseusSetRenderState(D3DRS_ZWRITEENABLE, FALSE); TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); TheseusSetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); @@ -157,49 +190,53 @@ void CAppearance::Render() TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); - TheseusSetTexture(0, NULL); - TheseusSetTexture(1, NULL); + for (DWORD stage = 0; stage < 2; stage++) + TheseusSetTexture(stage, NULL); - if (m_texture != NULL) + if (m_texture) { m_texture->Render(); - LPDIRECT3DTEXTURE8 pSurface = m_texture->GetTextureSurface(); - if (pSurface != NULL) + LPDIRECT3DTEXTURE8 pTex = m_texture->GetTextureSurface(); + if (pTex == NULL) { - TheseusSetTexture(0, pSurface); - - TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); - TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); + // No image yet: draw with a clear texture factor instead. TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); - TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE); + TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR); + TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR); + TheseusSetRenderState(D3DRS_TEXTUREFACTOR, 0); } else { - TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); - TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR); + TheseusSetTexture(0, pTex); TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); - TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR); - TheseusSetRenderState(D3DRS_TEXTUREFACTOR, D3DCOLOR_COLORVALUE(0.0f, 0.0f, 0.0f, 0.0f)); + TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); + TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE); } } - if (m_material != NULL) + if (m_material) m_material->Render(); } +// CAppearance::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x000512c0: +// CNode::Advance(t); if (material) material->Advance(t); if (texture) texture->Advance(t); void CAppearance::Advance(float nSeconds) { CNode::Advance(nSeconds); - - if (m_material != NULL) + if (m_material) m_material->Advance(nSeconds); - - if (m_texture != NULL) + if (m_texture) m_texture->Advance(nSeconds); } -// ===== CMaterial ===== + +// ===== CMaterial ============================================================ +// 0x90 bytes: ambientIntensity 0x1c, diffuseColor 0x20, emissiveColor 0x2c, +// shininess 0x38, specularColor 0x3c, transparency 0x48, D3DMATERIAL8 at 0x4c. IMPLEMENT_NODE("Material", CMaterial, CNode) @@ -212,6 +249,11 @@ START_NODE_PROPS(CMaterial, CNode) NODE_PROP(pt_number, CMaterial, transparency) END_NODE_PROPS() +// CMaterial::CMaterial() +// Reconstructed from 5960 retail XBE, CMaterial_CreateNode @ 0x00051300: +// this[7] = 0.2 (0x0002c01c); this[8..10] = 0.8 (0x0002bfd8); this[11..13] = 0; +// this[14] = 0.2; this[15..17] = 0; this[18] = 0; +// The VRML material defaults, which is all these numbers could ever be. CMaterial::CMaterial() : m_ambientIntensity(0.2f), m_diffuseColor(0.8f, 0.8f, 0.8f), @@ -222,124 +264,94 @@ CMaterial::CMaterial() : { } -void CMaterial::Render() +static D3DCOLORVALUE MaterialColor(float r, float g, float b, float a) { - m_material.Diffuse.r = m_diffuseColor.x; - m_material.Diffuse.g = m_diffuseColor.y; - m_material.Diffuse.b = m_diffuseColor.z; - m_material.Diffuse.a = 1.0f - m_transparency; - - m_material.Ambient.r = m_ambientIntensity; - m_material.Ambient.g = m_ambientIntensity; - m_material.Ambient.b = m_ambientIntensity; - m_material.Ambient.a = 1.0f - m_transparency; - - m_material.Specular.r = m_specularColor.x; - m_material.Specular.g = m_specularColor.y; - m_material.Specular.b = m_specularColor.z; - m_material.Specular.a = 1.0f - m_transparency; + D3DCOLORVALUE c; + c.r = r; + c.g = g; + c.b = b; + c.a = a; + return c; +} - m_material.Emissive.r = m_emissiveColor.x; - m_material.Emissive.g = m_emissiveColor.y; - m_material.Emissive.b = m_emissiveColor.z; - m_material.Emissive.a = 1.0f - m_transparency; +// CMaterial::Render() +// Reconstructed from 5960 retail XBE @ 0x000513a0. Ghidra decompile, trimmed: +// a = 1.0 - transparency; +// mat.Diffuse = (diffuse, a); mat.Ambient = (ambientIntensity x3, a); +// mat.Specular = (specular, a); mat.Emissive = (emissive, a); mat.Power = shininess; +// copy 0x11 dwords into the device material, mark it dirty // SetMaterial, inlined +void CMaterial::Render() +{ + float alpha = 1.0f - m_transparency; + m_material.Diffuse = MaterialColor(m_diffuseColor.x, m_diffuseColor.y, m_diffuseColor.z, alpha); + m_material.Ambient = MaterialColor(m_ambientIntensity, m_ambientIntensity, m_ambientIntensity, alpha); + m_material.Specular = MaterialColor(m_specularColor.x, m_specularColor.y, m_specularColor.z, alpha); + m_material.Emissive = MaterialColor(m_emissiveColor.x, m_emissiveColor.y, m_emissiveColor.z, alpha); m_material.Power = m_shininess; TheseusSetMaterial(&m_material); } -// ===== CreateCube ===== -void CreateCube(D3DVERTEX* pVertices, WORD* pIndices, D3DXVECTOR3 size, bool bInside = false) +// ===== CreateCube =========================================================== + +// Corner of a unit cube: which way each axis goes, and which end of the +// texture it gets (0 maps to 0.01, 1 to 0.99). +struct CubeCorner { - D3DXVECTOR3 n0( 0.0f, 0.0f,-1.0f ); - D3DXVECTOR3 n1( 0.0f, 0.0f, 1.0f ); - D3DXVECTOR3 n2( 0.0f, 1.0f, 0.0f ); - D3DXVECTOR3 n3( 0.0f,-1.0f, 0.0f ); - D3DXVECTOR3 n4( 1.0f, 0.0f, 0.0f ); - D3DXVECTOR3 n5(-1.0f, 0.0f, 0.0f ); + signed char x, y, z; + BYTE u, v; +}; - if (bInside) - { - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x, 0.5f * size.y,-0.5f * size.z), n0, 0.01f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x, 0.5f * size.y,-0.5f * size.z), n0, 0.99f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x,-0.5f * size.y,-0.5f * size.z), n0, 0.99f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x,-0.5f * size.y,-0.5f * size.z), n0, 0.01f, 0.01f); - - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x, 0.5f * size.y, 0.5f * size.z), n1, 0.99f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x,-0.5f * size.y, 0.5f * size.z), n1, 0.99f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x,-0.5f * size.y, 0.5f * size.z), n1, 0.01f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x, 0.5f * size.y, 0.5f * size.z), n1, 0.01f, 0.99f); - - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x, 0.5f * size.y, 0.5f * size.z), n2, 0.01f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x, 0.5f * size.y, 0.5f * size.z), n2, 0.99f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x, 0.5f * size.y,-0.5f * size.z), n2, 0.99f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x, 0.5f * size.y,-0.5f * size.z), n2, 0.01f, 0.01f); - - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x,-0.5f * size.y, 0.5f * size.z), n3, 0.01f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x,-0.5f * size.y,-0.5f * size.z), n3, 0.01f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x,-0.5f * size.y,-0.5f * size.z), n3, 0.99f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x,-0.5f * size.y, 0.5f * size.z), n3, 0.99f, 0.99f); - - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x, 0.5f * size.y,-0.5f * size.z), n4, 0.01f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x, 0.5f * size.y, 0.5f * size.z), n4, 0.99f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x,-0.5f * size.y, 0.5f * size.z), n4, 0.99f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x,-0.5f * size.y,-0.5f * size.z), n4, 0.01f, 0.01f); - - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x, 0.5f * size.y,-0.5f * size.z), n5, 0.99f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x,-0.5f * size.y,-0.5f * size.z), n5, 0.99f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x,-0.5f * size.y, 0.5f * size.z), n5, 0.01f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x, 0.5f * size.y, 0.5f * size.z), n5, 0.01f, 0.99f); - } - else +// The binary's six faces in order -z, +z, +y, -y, +x, -x, four corners each. +static const CubeCorner c_cubeCorners[24] = +{ + { -1, -1, -1, 0, 0 }, { 1, -1, -1, 1, 0 }, { 1, 1, -1, 1, 1 }, { -1, 1, -1, 0, 1 }, + { 1, 1, 1, 0, 1 }, { 1, -1, 1, 0, 0 }, { -1, -1, 1, 1, 0 }, { -1, 1, 1, 1, 1 }, + { -1, 1, -1, 0, 0 }, { 1, 1, -1, 1, 0 }, { 1, 1, 1, 1, 1 }, { -1, 1, 1, 0, 1 }, + { 1, -1, 1, 1, 1 }, { 1, -1, -1, 1, 0 }, { -1, -1, -1, 0, 0 }, { -1, -1, 1, 0, 1 }, + { 1, -1, -1, 0, 0 }, { 1, -1, 1, 1, 0 }, { 1, 1, 1, 1, 1 }, { 1, 1, -1, 0, 1 }, + { -1, 1, 1, 0, 1 }, { -1, -1, 1, 0, 0 }, { -1, -1, -1, 1, 0 }, { -1, 1, -1, 1, 1 }, +}; + +static const float c_cubeNormals[6][3] = +{ + { 0.0f, 0.0f, -1.0f }, { 0.0f, 0.0f, 1.0f }, + { 0.0f, 1.0f, 0.0f }, { 0.0f, -1.0f, 0.0f }, + { 1.0f, 0.0f, 0.0f }, { -1.0f, 0.0f, 0.0f }, +}; + +// CreateCube(pVertices, pIndices, size, bInside) +// Reconstructed from 5960 retail XBE @ 0x00051450 (bInside in CL, indices in +// EAX). Unrolled in the binary: 24 vertex stores of +-0.5 * size, a face normal +// and texture coordinates inset to 0.01..0.99, then the indices 0,1,2, 2,3,0 +// for each face. The inside variant writes each face's four corners in reverse +// order with the same normals. Written here as a corner table walked in a loop. +void CreateCube(D3DVERTEX* pVertices, WORD* pIndices, D3DXVECTOR3 size, bool bInside = false) +{ + for (int face = 0; face < 6; face++) { - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x,-0.5f * size.y,-0.5f * size.z), n0, 0.01f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x,-0.5f * size.y,-0.5f * size.z), n0, 0.99f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x, 0.5f * size.y,-0.5f * size.z), n0, 0.99f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x, 0.5f * size.y,-0.5f * size.z), n0, 0.01f, 0.99f); - - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x, 0.5f * size.y, 0.5f * size.z), n1, 0.01f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x,-0.5f * size.y, 0.5f * size.z), n1, 0.01f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x,-0.5f * size.y, 0.5f * size.z), n1, 0.99f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x, 0.5f * size.y, 0.5f * size.z), n1, 0.99f, 0.99f); - - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x, 0.5f * size.y,-0.5f * size.z), n2, 0.01f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x, 0.5f * size.y,-0.5f * size.z), n2, 0.99f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x, 0.5f * size.y, 0.5f * size.z), n2, 0.99f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x, 0.5f * size.y, 0.5f * size.z), n2, 0.01f, 0.99f); - - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x,-0.5f * size.y, 0.5f * size.z), n3, 0.99f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x,-0.5f * size.y,-0.5f * size.z), n3, 0.99f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x,-0.5f * size.y,-0.5f * size.z), n3, 0.01f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x,-0.5f * size.y, 0.5f * size.z), n3, 0.01f, 0.99f); - - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x,-0.5f * size.y,-0.5f * size.z), n4, 0.01f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x,-0.5f * size.y, 0.5f * size.z), n4, 0.99f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x, 0.5f * size.y, 0.5f * size.z), n4, 0.99f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3( 0.5f * size.x, 0.5f * size.y,-0.5f * size.z), n4, 0.01f, 0.99f); - - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x, 0.5f * size.y, 0.5f * size.z), n5, 0.01f, 0.99f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x,-0.5f * size.y, 0.5f * size.z), n5, 0.01f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x,-0.5f * size.y,-0.5f * size.z), n5, 0.99f, 0.01f); - *pVertices++ = D3DVERTEX(D3DXVECTOR3(-0.5f * size.x, 0.5f * size.y,-0.5f * size.z), n5, 0.99f, 0.99f); - } + D3DXVECTOR3 normal(c_cubeNormals[face][0], c_cubeNormals[face][1], c_cubeNormals[face][2]); + + for (int n = 0; n < 4; n++) + { + const CubeCorner& c = c_cubeCorners[face * 4 + (bInside ? 3 - n : n)]; + D3DXVECTOR3 pos(c.x * 0.5f * size.x, c.y * 0.5f * size.y, c.z * 0.5f * size.z); + *pVertices++ = D3DVERTEX(pos, normal, c.u ? 0.99f : 0.01f, c.v ? 0.99f : 0.01f); + } - *pIndices++ = 0+0; *pIndices++ = 0+1; *pIndices++ = 0+2; - *pIndices++ = 0+2; *pIndices++ = 0+3; *pIndices++ = 0+0; - *pIndices++ = 4+0; *pIndices++ = 4+1; *pIndices++ = 4+2; - *pIndices++ = 4+2; *pIndices++ = 4+3; *pIndices++ = 4+0; - *pIndices++ = 8+0; *pIndices++ = 8+1; *pIndices++ = 8+2; - *pIndices++ = 8+2; *pIndices++ = 8+3; *pIndices++ = 8+0; - *pIndices++ = 12+0; *pIndices++ = 12+1; *pIndices++ = 12+2; - *pIndices++ = 12+2; *pIndices++ = 12+3; *pIndices++ = 12+0; - *pIndices++ = 16+0; *pIndices++ = 16+1; *pIndices++ = 16+2; - *pIndices++ = 16+2; *pIndices++ = 16+3; *pIndices++ = 16+0; - *pIndices++ = 20+0; *pIndices++ = 20+1; *pIndices++ = 20+2; - *pIndices++ = 20+2; *pIndices++ = 20+3; *pIndices++ = 20+0; + WORD first = (WORD)(face * 4); + static const WORD quad[6] = { 0, 1, 2, 2, 3, 0 }; + for (int n = 0; n < 6; n++) + *pIndices++ = first + quad[n]; + } } -// ===== CBox ===== + +// ===== CBox ================================================================= +// 0x37c bytes: size 0x1c, vertex buffer 0x28, index buffer 0x2c, 24 vertices +// at 0x30, 36 indices at 0x330, dirty flag 0x378. class CBox : public CNode { @@ -357,11 +369,8 @@ class CBox : public CNode IDirect3DVertexBuffer8* m_pVB; IDirect3DIndexBuffer8* m_pIB; -#define NUM_CUBE_VERTICES (4*6) -#define NUM_CUBE_INDICES (6*6) - - D3DVERTEX m_pCubeVertices [NUM_CUBE_VERTICES]; - WORD m_pCubeIndices [NUM_CUBE_INDICES]; + D3DVERTEX m_vertices [24]; + WORD m_indices [36]; bool m_dirty; @@ -374,75 +383,89 @@ START_NODE_PROPS(CBox, CNode) NODE_PROP(pt_vec3, CBox, size) END_NODE_PROPS() +// CBox::CBox() / ~CBox() +// Reconstructed from 5960 retail XBE, factory @ 0x000526c0 and dtor @ 0x00052750: +// size = (1.0, 1.0, 1.0); dirty = 1; vb = ib = NULL; +// ~: release vb, then ib (D3D resource release inlined) CBox::CBox() : - m_size(1.0f, 1.0f, 1.0f) + m_size(1.0f, 1.0f, 1.0f), + m_pVB(NULL), + m_pIB(NULL), + m_dirty(true) { - m_dirty = true; - - m_pVB = NULL; - m_pIB = NULL; } CBox::~CBox() { - if (m_pVB != NULL) + if (m_pVB) m_pVB->Release(); - - if (m_pIB != NULL) + if (m_pIB) m_pIB->Release(); } +// CBox::Render() +// Reconstructed from 5960 retail XBE @ 0x00052820 (not a defined function in +// the Ghidra project, read from the disassembly): +// if (dirty) { CreateCube(verts, indices, size, false); dirty = 0; } +// if (!vb) { create 0x300 byte vb; copy 0xc0 dwords of verts into it; } +// if (!ib) { create 0x48 byte ib; copy 0x12 dwords of indices into it; } +// if (vb && ib) { SetStreamSource(0, vb, 0x20); SetIndices(ib, 0); +// SetVertexShader(0x112); draw 36 indices as a triangle list; } +// The binary's create helper retries until it gets memory, so the copies +// never check what they got. void CBox::Render() { if (m_dirty) { - TRACE(_T("Creating a cube...\n")); - CreateCube(m_pCubeVertices, m_pCubeIndices, m_size); + CreateCube(m_vertices, m_indices, m_size); m_dirty = false; } + BYTE* pDst; + if (m_pVB == NULL) { - void* pVerts; - TheseusCreateVertexBuffer(NUM_CUBE_VERTICES * sizeof (D3DVERTEX), D3DUSAGE_DYNAMIC, D3DFVF_VERTEX, D3DPOOL_MANAGED, &m_pVB); - - VERIFYHR(m_pVB->Lock(0, NUM_CUBE_VERTICES * sizeof (D3DVERTEX), (BYTE**)&pVerts, 0)); - CopyMemory(pVerts, m_pCubeVertices, NUM_CUBE_VERTICES * sizeof (D3DVERTEX)); - VERIFYHR(m_pVB->Unlock()); + TheseusCreateVertexBuffer(sizeof (m_vertices), D3DUSAGE_DYNAMIC, D3DFVF_VERTEX, D3DPOOL_MANAGED, &m_pVB); + m_pVB->Lock(0, sizeof (m_vertices), &pDst, 0); + memcpy(pDst, m_vertices, sizeof (m_vertices)); + m_pVB->Unlock(); } if (m_pIB == NULL) { - void* pIndices; - TheseusCreateIndexBuffer(NUM_CUBE_INDICES * sizeof (WORD), D3DUSAGE_DYNAMIC, D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_pIB); - VERIFYHR(m_pIB->Lock(0, NUM_CUBE_INDICES * sizeof (WORD), (BYTE**)&pIndices, 0)); - CopyMemory(pIndices, m_pCubeIndices, NUM_CUBE_INDICES * sizeof (WORD)); - VERIFYHR(m_pIB->Unlock()); + TheseusCreateIndexBuffer(sizeof (m_indices), D3DUSAGE_DYNAMIC, D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_pIB); + m_pIB->Lock(0, sizeof (m_indices), &pDst, 0); + memcpy(pDst, m_indices, sizeof (m_indices)); + m_pIB->Unlock(); } - if (m_pVB != NULL && m_pIB != NULL) - { - TheseusSetStreamSource(0, m_pVB, sizeof (D3DVERTEX)); - TheseusSetIndices(m_pIB, 0); - TheseusSetVertexShader(D3DFVF_VERTEX); - TheseusDrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, NUM_CUBE_VERTICES, 0, 12); - } + if (m_pVB == NULL || m_pIB == NULL) + return; + + TheseusSetStreamSource(0, m_pVB, sizeof (D3DVERTEX)); + TheseusSetIndices(m_pIB, 0); + TheseusSetVertexShader(D3DFVF_VERTEX); + TheseusDrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 24, 0, 12); } +// CBox::GetBBox(pBBox) / GetRadius() +// Reconstructed from 5960 retail XBE @ 0x00052960 and 0x00052990: +// GetBBox: center = (0, 0, 0); size = this->size; +// GetRadius: return sqrt(x*x + y*y + z*z) * 0.5; // x87, half the diagonal void CBox::GetBBox(BBox* pBBox) { - pBBox->center.x = 0.0f; - pBBox->center.y = 0.0f; - pBBox->center.z = 0.0f; + pBBox->center = D3DXVECTOR3(0.0f, 0.0f, 0.0f); pBBox->size = m_size; } float CBox::GetRadius() { - return D3DXVec3Length(&m_size) / 2.0f; + return D3DXVec3Length(&m_size) * 0.5f; } -// ===== CSphere ===== + +// ===== CSphere ============================================================== +// A MeshNode whose mesh comes from MakeSphere: radius 0x40, slices 0x44, stacks 0x48. extern class CMesh* MakeSphere(float nRadius, int nSlices, int nStacks); @@ -451,7 +474,6 @@ class CSphere : public CMeshNode DECLARE_NODE(CSphere, CMeshNode) public: CSphere(); - ~CSphere(); float m_radius; int m_slices; @@ -470,6 +492,10 @@ START_NODE_PROPS(CSphere, CMeshNode) NODE_PROP(pt_integer, CSphere, stacks) END_NODE_PROPS() +// CSphere::CSphere() +// Reconstructed from 5960 retail XBE @ 0x00052a00: +// (MeshNode ctor inlined) radius = 1.0; slices = 0x20; stacks = 0x20; dirty = 1; +// The destructor @ 0x00052a90 is only the MeshNode one. CSphere::CSphere() : m_radius(1.0f), m_slices(32), @@ -478,28 +504,31 @@ CSphere::CSphere() : m_dirty = true; } -CSphere::~CSphere() -{ -} - +// CSphere::Init() [MeshNode vtable slot 23] +// Reconstructed from 5960 retail XBE @ 0x00052aa0. Ghidra decompile: +// mesh = MakeSphere(radius, slices, stacks); this[0xc] = mesh; +// if (mesh != 0) dirty = 0; void CSphere::Init() { m_mesh = MakeSphere(m_radius, m_slices, m_stacks); - - if (m_mesh) - { - m_dirty = false; - } + if (m_mesh != NULL) + m_dirty = false; } -// ===== Shader Descriptors ===== -// Xbox-only: real NV2A vertex shader bytecode + the GetEffectShader/ -// GetFixedFunctionShader/CompressNormal/SetFalloffShader/etc helpers -// that drive it. Desktop has GLSL-sentinel equivalents in falloff.cpp -// because the desktop GL emulator can't run NV2A microcode. + +// ===== Effect vertex shaders ================================================ +// Xbox only: the NV2A microcode and its lookup tables. Desktop can't run +// NV2A microcode, so it has its own versions below. + #ifdef _XBOX -DWORD dwEffectVertexShader[] = { +// The five NV2A programs, read word for word out of the XBE data: a header +// word with the instruction count in the high half, then four words per +// instruction. Binary data, so any faithful copy is identical. + +// @ 0x001754f0, 21 instructions +static DWORD c_EffectMicrocode[] = +{ 0x00152078, 0x00000000, 0x00ec001b, 0x0836186c, 0x20708800, 0x00000000, 0x00ed401b, 0x0836186c, 0x28200ff8, @@ -524,7 +553,9 @@ DWORD dwEffectVertexShader[] = { 0x00000000, 0x008de01b, 0xa5ff686c, 0x3070f819 }; -DWORD dwEffect2VertexShader[] = { +// @ 0x00175648, 21 instructions +static DWORD c_Effect2Microcode[] = +{ 0x00152078, 0x00000000, 0x00ec001b, 0x0836186c, 0x20708800, 0x00000000, 0x00ed401b, 0x0836186c, 0x28200ff8, @@ -549,7 +580,9 @@ DWORD dwEffect2VertexShader[] = { 0x00000000, 0x008de01b, 0xa5ff686c, 0x3070f819 }; -DWORD dwEffect3VertexShader[] = { +// @ 0x001757a0, 21 instructions +static DWORD c_Effect3Microcode[] = +{ 0x00152078, 0x00000000, 0x00ec001b, 0x0836186c, 0x20708800, 0x00000000, 0x00ed401b, 0x0836186c, 0x28200ff8, @@ -574,7 +607,9 @@ DWORD dwEffect3VertexShader[] = { 0x00000000, 0x008de01b, 0xa5ff686c, 0x3070f819 }; -DWORD dwEffect4VertexShader[] = { +// @ 0x001758f8, 29 instructions +static DWORD c_Effect4Microcode[] = +{ 0x001d2078, 0x00000000, 0x00f1001b, 0x0836186c, 0x28200ff8, 0x00000000, 0x00f1201b, 0x0836186c, 0x24200ff8, @@ -607,7 +642,9 @@ DWORD dwEffect4VertexShader[] = { 0x00000000, 0x0092001a, 0x25feabfc, 0x3070e849 }; -DWORD dwAnisoVertexShader[] = { +// @ 0x00175ad0, 10 instructions +static DWORD c_AnisoMicrocode[] = +{ 0x000a2078, 0x00000000, 0x00ec001b, 0x0836186c, 0x20708800, 0x00000000, 0x00ec201b, 0x0836186c, 0x20704800, @@ -621,217 +658,126 @@ DWORD dwAnisoVertexShader[] = { 0x00000000, 0x0087601b, 0xc400286c, 0x3070e801 }; -struct SHADEDESC +// One lookup entry, 0x14 bytes in the binary: effect number, microcode, the +// FVF it serves, the shader handle once created, and a spare flag. +struct EffectShader { - int m_nEffect; - DWORD* m_rgdwMicrocode; - DWORD m_fvf; - DWORD m_dwShader; - bool m_bReportedError; + int nEffect; + DWORD* pMicrocode; + DWORD fvf; + DWORD hShader; + bool bReported; }; -#define SHADERNAME(name) dw##name##VertexShader +#define FVF_PACKED (D3DFVF_XYZ | D3DFVF_NORMPACKED3) +#define FVF_FLOAT (D3DFVF_XYZ | D3DFVF_NORMAL) + +// Table @ 0x00175bb0, 12 entries. Effect 1 is the falloff shader (its own +// variant when there are texture coordinates), 2 anisotropic, 3 the textured +// falloff, 4 reflection. Each comes in packed and float normal versions. +static EffectShader c_effectShaders[] = +{ + { 1, c_EffectMicrocode, FVF_PACKED | D3DFVF_DIFFUSE }, + { 1, c_EffectMicrocode, FVF_PACKED }, + { 1, c_Effect2Microcode, FVF_PACKED | D3DFVF_TEX1 }, + { 2, c_AnisoMicrocode, FVF_PACKED }, + { 3, c_Effect3Microcode, FVF_PACKED | D3DFVF_TEX1 }, + { 4, c_Effect4Microcode, FVF_PACKED }, + { 1, c_EffectMicrocode, FVF_FLOAT | D3DFVF_DIFFUSE }, + { 1, c_EffectMicrocode, FVF_FLOAT }, + { 1, c_Effect2Microcode, FVF_FLOAT | D3DFVF_TEX1 }, + { 2, c_AnisoMicrocode, FVF_FLOAT }, + { 3, c_Effect3Microcode, FVF_FLOAT | D3DFVF_TEX1 }, + { 4, c_Effect4Microcode, FVF_FLOAT }, +}; -SHADEDESC fixed_shaders [] = +// Table @ 0x00175b74, 3 entries: the packed normal FVFs that need a +// declaration to draw fixed function. The microcode pointer is never used. +static EffectShader c_fixedShaders[] = { - { - 0, - SHADERNAME(Effect), - D3DFVF_XYZ | D3DFVF_NORMPACKED3 | D3DFVF_DIFFUSE - }, - { - 1, - SHADERNAME(Effect), - D3DFVF_XYZ | D3DFVF_NORMPACKED3 - }, - { - 2, - SHADERNAME(Effect), - D3DFVF_XYZ | D3DFVF_NORMPACKED3 | D3DFVF_TEX1 - }, + { 0, c_EffectMicrocode, FVF_PACKED | D3DFVF_DIFFUSE }, + { 1, c_EffectMicrocode, FVF_PACKED }, + { 2, c_EffectMicrocode, FVF_PACKED | D3DFVF_TEX1 }, }; -SHADEDESC shaders [] = +// Effect shader declarations @ 0x0017669c..0x001766e0: position in v0, +// normal in v3, texture coordinates in v6. +static DWORD c_declFloat [] = { D3DVSD_STREAM(0), D3DVSD_REG(0, D3DVSDT_FLOAT3), D3DVSD_REG(3, D3DVSDT_FLOAT3), D3DVSD_END() }; +static DWORD c_declFloatTex [] = { D3DVSD_STREAM(0), D3DVSD_REG(0, D3DVSDT_FLOAT3), D3DVSD_REG(3, D3DVSDT_FLOAT3), D3DVSD_REG(6, D3DVSDT_FLOAT2), D3DVSD_END() }; +static DWORD c_declPacked [] = { D3DVSD_STREAM(0), D3DVSD_REG(0, D3DVSDT_FLOAT3), D3DVSD_REG(3, D3DVSDT_NORMPACKED3), D3DVSD_END() }; +static DWORD c_declPackedTex [] = { D3DVSD_STREAM(0), D3DVSD_REG(0, D3DVSDT_FLOAT3), D3DVSD_REG(3, D3DVSDT_NORMPACKED3), D3DVSD_REG(6, D3DVSDT_FLOAT2), D3DVSD_END() }; + +// Fixed function declarations @ 0x00176660, five dwords each, one per c_fixedShaders entry. +static DWORD c_fixedDecls[3][5] = { - { - 1, - SHADERNAME(Effect), - D3DFVF_XYZ | D3DFVF_NORMPACKED3 | D3DFVF_DIFFUSE - }, - { - 1, - SHADERNAME(Effect), - D3DFVF_XYZ | D3DFVF_NORMPACKED3 - }, - { - 1, - SHADERNAME(Effect2), - D3DFVF_XYZ | D3DFVF_NORMPACKED3 | D3DFVF_TEX1 - }, - { - 2, - SHADERNAME(Aniso), - D3DFVF_XYZ | D3DFVF_NORMPACKED3 - }, - { - 3, - SHADERNAME(Effect3), - D3DFVF_XYZ | D3DFVF_NORMPACKED3 | D3DFVF_TEX1 - }, - { - 4, - SHADERNAME(Effect4), - D3DFVF_XYZ | D3DFVF_NORMPACKED3 - }, - { - 1, - SHADERNAME(Effect), - D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE - }, - { - 1, - SHADERNAME(Effect), - D3DFVF_XYZ | D3DFVF_NORMAL - }, - { - 1, - SHADERNAME(Effect2), - D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1 - }, - { - 2, - SHADERNAME(Aniso), - D3DFVF_XYZ | D3DFVF_NORMAL - }, - { - 3, - SHADERNAME(Effect3), - D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1 - }, - { - 4, - SHADERNAME(Effect4), - D3DFVF_XYZ | D3DFVF_NORMAL - }, + { D3DVSD_STREAM(0), D3DVSD_REG(D3DVSDE_POSITION, D3DVSDT_FLOAT3), D3DVSD_REG(D3DVSDE_NORMAL, D3DVSDT_NORMPACKED3), D3DVSD_REG(D3DVSDE_DIFFUSE, D3DVSDT_D3DCOLOR), D3DVSD_END() }, + { D3DVSD_STREAM(0), D3DVSD_REG(D3DVSDE_POSITION, D3DVSDT_FLOAT3), D3DVSD_REG(D3DVSDE_NORMAL, D3DVSDT_NORMPACKED3), D3DVSD_END() }, + { D3DVSD_STREAM(0), D3DVSD_REG(D3DVSDE_POSITION, D3DVSDT_FLOAT3), D3DVSD_REG(D3DVSDE_NORMAL, D3DVSDT_NORMPACKED3), D3DVSD_REG(D3DVSDE_TEXCOORD0, D3DVSDT_FLOAT2), D3DVSD_END() }, }; -// ===== Effect Shader Lookup ===== - +// GetEffectShader(nEffect, fvf) +// Reconstructed from 5960 retail XBE @ 0x0005c6c0 (effect in ECX, fvf in EAX). +// Ghidra decompile, trimmed: +// if (effect == 0) return fvf; +// for (i = 0; e[i].effect != effect || e[i].fvf != fvf; i++) if (i > 10) return fvf; +// if (e[i].shader == 0) +// CreateVertexShader(packed ? (tex ? 0x1766c0 : 0x1766d4) : (tex ? 0x17669c : 0x1766b0), +// e[i].microcode, &e[i].shader, 0); +// return e[i].shader; +// Shaders are created the first time a material asks for them and kept. DWORD GetEffectShader(int nEffect, DWORD fvf) { if (nEffect == 0) return fvf; - SHADEDESC* pShadeDesc = shaders; - for (int i = 0; pShadeDesc->m_nEffect != nEffect || pShadeDesc->m_fvf != fvf; i += 1, pShadeDesc += 1) + for (int n = 0; n < (int)countof (c_effectShaders); n++) { - if (i >= countof (shaders) - 1) - return fvf; - } - - if (pShadeDesc->m_dwShader == 0) - { - const DWORD* rgdwFunction = NULL; - - rgdwFunction = pShadeDesc->m_rgdwMicrocode; + EffectShader& e = c_effectShaders[n]; + if (e.nEffect != nEffect || e.fvf != fvf) + continue; - static DWORD decl1 [] = + if (e.hShader == 0) { - D3DVSD_STREAM( 0 ), - D3DVSD_REG( 0, D3DVSDT_FLOAT3 ), - D3DVSD_REG( 3, D3DVSDT_NORMPACKED3 ), - D3DVSD_END() - }; - - static DWORD decl2 [] = - { - D3DVSD_STREAM( 0 ), - D3DVSD_REG( 0, D3DVSDT_FLOAT3 ), - D3DVSD_REG( 3, D3DVSDT_NORMPACKED3 ), - D3DVSD_REG( 6, D3DVSDT_FLOAT2 ), - D3DVSD_END() - }; - - static DWORD decl3 [] = - { - D3DVSD_STREAM( 0 ), - D3DVSD_REG( 0, D3DVSDT_FLOAT3 ), - D3DVSD_REG( 3, D3DVSDT_FLOAT3 ), - D3DVSD_END() - }; - - static DWORD decl4 [] = - { - D3DVSD_STREAM( 0 ), - D3DVSD_REG( 0, D3DVSDT_FLOAT3 ), - D3DVSD_REG( 3, D3DVSDT_FLOAT3 ), - D3DVSD_REG( 6, D3DVSDT_FLOAT2 ), - D3DVSD_END() - }; - - if (fvf & D3DFVF_NORMPACKED3) - { - TheseusCreateVertexShader((fvf & D3DFVF_TEX1) ? decl2 : decl1, rgdwFunction, &pShadeDesc->m_dwShader, 0); - } - else - { - TheseusCreateVertexShader((fvf & D3DFVF_TEX1) ? decl4 : decl3, rgdwFunction, &pShadeDesc->m_dwShader, 0); + bool bTex = (fvf & D3DFVF_TEX1) != 0; + DWORD* pDecl; + if (fvf & D3DFVF_NORMPACKED3) + pDecl = bTex ? c_declPackedTex : c_declPacked; + else + pDecl = bTex ? c_declFloatTex : c_declFloat; + TheseusCreateVertexShader(pDecl, e.pMicrocode, &e.hShader, 0); } + return e.hShader; } - return pShadeDesc->m_dwShader; + return fvf; } -// ===== Fixed Function Shader Lookup ===== - +// GetFixedFunctionShader(fvf) +// Reconstructed from 5960 retail XBE @ 0x0005c740 (fvf in EAX). Finds fvf in +// the three entry table, else returns fvf unchanged. On first use it builds a +// shader from the matching declaration with no microcode (CreateVertexShader +// inlined: XMemAlloc, parse the declaration, handle | 1) and keeps it. DWORD GetFixedFunctionShader(DWORD fvf) { - SHADEDESC* pShadeDesc = fixed_shaders; - int i; - for (i = 0; pShadeDesc->m_fvf != fvf; i += 1, pShadeDesc += 1) + for (int n = 0; n < (int)countof (c_fixedShaders); n++) { - if (i >= countof (fixed_shaders) - 1) - return fvf; - } + EffectShader& e = c_fixedShaders[n]; + if (e.fvf != fvf) + continue; - if (pShadeDesc->m_dwShader == 0) - { - static DWORD decl[3][5] = - { - { - D3DVSD_STREAM( 0 ), - D3DVSD_REG( D3DVSDE_POSITION, D3DVSDT_FLOAT3 ), - D3DVSD_REG( D3DVSDE_NORMAL, D3DVSDT_NORMPACKED3 ), - D3DVSD_REG( D3DVSDE_DIFFUSE, D3DVSDT_D3DCOLOR ), - D3DVSD_END() - }, - { - D3DVSD_STREAM( 0 ), - D3DVSD_REG( D3DVSDE_POSITION, D3DVSDT_FLOAT3 ), - D3DVSD_REG( D3DVSDE_NORMAL, D3DVSDT_NORMPACKED3 ), - D3DVSD_END() - }, - { - D3DVSD_STREAM( 0 ), - D3DVSD_REG( D3DVSDE_POSITION, D3DVSDT_FLOAT3 ), - D3DVSD_REG( D3DVSDE_NORMAL, D3DVSDT_NORMPACKED3 ), - D3DVSD_REG( D3DVSDE_TEXCOORD0, D3DVSDT_FLOAT2 ), - D3DVSD_END() - }, - }; - - TheseusCreateVertexShader(&decl[i][0], NULL, &pShadeDesc->m_dwShader, 0); + if (e.hShader == 0) + TheseusCreateVertexShader(c_fixedDecls[n], NULL, &e.hShader, 0); + return e.hShader; } - return pShadeDesc->m_dwShader; + return fvf; } -#else // !_XBOX, desktop GLSL-sentinel versions +#else -// Desktop OpenGL has no NV2A vertex shaders. GetEffectShader returns a -// sentinel (high bit set + real FVF) that tells the d3d8_sdl shim's -// SetVertexShader to flip m_effectShaderActive while keeping the FVF -// for vertex attribute layout. GetFixedFunctionShader passes through. +// Desktop versions, Theseus's own. GetEffectShader hands the bgfx shim a +// sentinel (high bit plus the real FVF) so its SetVertexShader turns on the +// effect path and still knows the vertex layout. Fixed function passes through. DWORD GetEffectShader(int nEffect, DWORD fvf) { if (nEffect == 0) @@ -846,132 +792,136 @@ DWORD GetFixedFunctionShader(DWORD fvf) #endif // _XBOX -// ===== Normal Compression ===== -// Below this point is portable D3DX math, with one wrinkle: the float-to-DWORD -// conversion of negative normal components is undefined behavior in C, so the -// two platforms cast differently. Xbox MSVC i386 happens to do the right thing -// with a direct (DWORD)(float) cast; clang on desktop produces 0 for negatives, -// which corrupts compressed normals and breaks the falloff edge-glow shader. -// Route through (int) first on desktop so the truncation is well-defined. -DWORD CompressNormal(float* pvNormal) { +// ===== Normals ============================================================== + +// CompressNormal(pvNormal) +// Reconstructed from 5960 retail XBE @ 0x0005c820 (vector in EAX): +// len = sqrt(x*x + y*y + z*z); x, y, z scaled to unit length; +// return (ftol(x * 1023) & 0x7ff) | (ftol(y * 1023) & 0x7ff) << 11 | ftol(z * 511) << 22; +// ftol truncates through a signed integer, which is what lets negative +// components wrap into the packed fields. Going through int keeps that on +// every compiler; a straight float to DWORD cast gives 0 for them on clang. +// The binary multiplies by 1 / len; dividing keeps the normals Theseus has +// always packed (the two differ in about 1 vector in 40,000). +DWORD CompressNormal(float* pvNormal) +{ + float len = sqrtf(pvNormal[0] * pvNormal[0] + pvNormal[1] * pvNormal[1] + pvNormal[2] * pvNormal[2]); + float x = pvNormal[0] / len; + float y = pvNormal[1] / len; + float z = pvNormal[2] / len; + + DWORD packed = (DWORD)(int)(x * 1023.0f) & 0x7ff; + packed |= ((DWORD)(int)(y * 1023.0f) & 0x7ff) << 11; + packed |= ((DWORD)(int)(z * 511.0f) & 0x3ff) << 22; + return packed; +} - float vNormal[3]; - float fLength; - fLength = (float)sqrt(pvNormal[0] * pvNormal[0] + pvNormal[1] * pvNormal[1] + pvNormal[2] * pvNormal[2]); +// ===== Effect shader constants ============================================== - vNormal[0] = pvNormal[0] / fLength; - vNormal[1] = pvNormal[1] / fLength; - vNormal[2] = pvNormal[2] / fLength; +// Light from the upper right front, (1, 1, -1) normalized, taken into the +// object's space. Both frame value setters below end on this. +static void SetLightDirection(DWORD reg, const D3DXMATRIX& worldView) +{ + D3DXVECTOR4 dir(1.0f, 1.0f, -1.0f, 0.0f); + D3DXVec4Normalize(&dir, &dir); -#ifdef _XBOX - return ((((DWORD)(vNormal[0] * 1023.0f) & 0x7FF) << 0) | - (((DWORD)(vNormal[1] * 1023.0f) & 0x7FF) << 11) | - (((DWORD)(vNormal[2] * 511.0f) & 0x3FF) << 22)); -#else - return ((((uint32_t)(int)(vNormal[0] * 1023.0f) & 0x7FF) << 0) | - (((uint32_t)(int)(vNormal[1] * 1023.0f) & 0x7FF) << 11) | - (((uint32_t)(int)(vNormal[2] * 511.0f) & 0x3FF) << 22)); -#endif -} + D3DXMATRIX t; + D3DXMatrixTranspose(&t, &worldView); + D3DXVec3TransformNormal((D3DXVECTOR3*)&dir, (D3DXVECTOR3*)&dir, &t); + D3DXVec4Normalize(&dir, &dir); -// ===== Reflection Shader Setup ===== + TheseusSetVertexShaderConstant(reg, &dir, 1); +} +// SetReflectShaderFrameValues() +// Reconstructed from 5960 retail XBE @ 0x0005c8d0 (constant numbers are the +// binary's biased by 96): +// wv = world (matrix stack top) * view; +// c40..43 = transpose(wv); c44..47 = transpose(projection); c48 = (0, 0, 1, 0.5); +// c49 = light direction (see above); LIGHTING = FALSE; +// Desktop skips the LIGHTING write: the bgfx shim never lights fixed function. void SetReflectShaderFrameValues() { - D3DXMATRIX mat, worldView; - - D3DXMatrixMultiply(&worldView, TheseusGetWorld(), &g_matView); + D3DXMATRIX worldView, t; + D3DXMatrixMultiply(&worldView, TheseusGetWorld(), &g_matView); - D3DXMatrixTranspose(&mat, &worldView); - TheseusSetVertexShaderConstant(40, &mat(0,0), 4); + D3DXMatrixTranspose(&t, &worldView); + TheseusSetVertexShaderConstant(40, &t, 4); - D3DXMatrixTranspose(&mat, &g_matProjection); - TheseusSetVertexShaderConstant(44, &mat(0,0), 4); + D3DXMatrixTranspose(&t, &g_matProjection); + TheseusSetVertexShaderConstant(44, &t, 4); - D3DXVECTOR4 reflectConst(0.0f, 0.0f, 1.0f, 0.5f); - TheseusSetVertexShaderConstant(48, &reflectConst, 1); + D3DXVECTOR4 c48(0.0f, 0.0f, 1.0f, 0.5f); + TheseusSetVertexShaderConstant(48, &c48, 1); - D3DXVECTOR4 lightDir(1.0f, 1.0f, -1.0f, 0.0f); - D3DXVec4Normalize(&lightDir, &lightDir); - D3DXMatrixTranspose(&mat, &worldView); - D3DXVec3TransformNormal((D3DXVECTOR3*)&lightDir, (D3DXVECTOR3*)&lightDir, &mat); - D3DXVec4Normalize(&lightDir, &lightDir); - TheseusSetVertexShaderConstant(49, &lightDir, 1); + SetLightDirection(49, worldView); #ifdef _XBOX - // GL backend doesn't honor fixed-function LIGHTING; shim stores - // the value for GetRenderState round-trips but never enables - // glEnable(GL_LIGHTING). Xbox path keeps the original semantics. TheseusSetRenderState(D3DRS_LIGHTING, FALSE); #endif } -// ===== Falloff Shader Setup ===== - +// SetFalloffShaderFrameValues() +// Reconstructed from 5960 retail XBE @ 0x0005ca00: +// wv = world * view; +// c0..3 = transpose(wv * projection); c10..13 = transpose(wv); c5..8 = inverse(wv); +// c9 = (0, 0.5, 1, -1); c4 = light direction; +// SetTexture(0, NULL); LIGHTING = FALSE; // both inlined +// Runs on every world matrix change. The profiler phase is a Theseus +// addition, and desktop skips LIGHTING as in the reflection setter. void SetFalloffShaderFrameValues() { FramePhaseScope world(FP_TREE_WORLD); - D3DXMATRIX mat, worldView; + D3DXMATRIX worldView, t; D3DXMatrixMultiply(&worldView, TheseusGetWorld(), &g_matView); - D3DXMatrixMultiply(&mat, &worldView, &g_matProjection); - D3DXMatrixTranspose(&mat, &mat); - TheseusSetVertexShaderConstant(0, &mat(0,0), 4); + D3DXMatrixMultiply(&t, &worldView, &g_matProjection); + D3DXMatrixTranspose(&t, &t); + TheseusSetVertexShaderConstant(0, &t, 4); - D3DXMatrixTranspose(&mat, &worldView); - TheseusSetVertexShaderConstant(10, &mat(0,0), 4); + D3DXMatrixTranspose(&t, &worldView); + TheseusSetVertexShaderConstant(10, &t, 4); - D3DXMatrixInverse(&mat, NULL, &worldView); - TheseusSetVertexShaderConstant(5, &mat(0,0), 4); + D3DXMatrixInverse(&t, NULL, &worldView); + TheseusSetVertexShaderConstant(5, &t, 4); - D3DXVECTOR4 v(0.0f, 0.5f, 1.0f, -1.0f); - TheseusSetVertexShaderConstant(9, &v, 1); + D3DXVECTOR4 c9(0.0f, 0.5f, 1.0f, -1.0f); + TheseusSetVertexShaderConstant(9, &c9, 1); - D3DXVECTOR4 lightDir(1.0f, 1.0f, -1.0f, 0.0f); - D3DXVec4Normalize(&lightDir, &lightDir); - D3DXMatrixTranspose(&mat, &worldView); - D3DXVec3TransformNormal((D3DXVECTOR3*)&lightDir, (D3DXVECTOR3*)&lightDir, &mat); - D3DXVec4Normalize(&lightDir, &lightDir); - TheseusSetVertexShaderConstant(4, &lightDir, 1); + SetLightDirection(4, worldView); TheseusSetTexture(0, NULL); #ifdef _XBOX - // See shape_render.cpp note above: LIGHTING is FF-only, no GL effect. TheseusSetRenderState(D3DRS_LIGHTING, FALSE); #endif } -// ===== Falloff Shader Values ===== - +// Scene wide fade for the effect materials, @ 0x00175ca0, starts at 1.0. float g_nEffectAlpha = 1.0f; +// SetFalloffShaderValues(sideColor, frontColor) +// Inlined in the 5960 retail XBE into each falloff material's Setup, e.g. +// CFalloffMatInfo_Setup @ 0x00057790: +// c15 = (side.rgb, side.a * g_nEffectAlpha); +// c16 = (front.rgb - side.rgb, (front.a - side.a) * g_nEffectAlpha); +// The shader blends from the side color at the silhouette to the front color. void SetFalloffShaderValues(const D3DXCOLOR& sideColor, const D3DXCOLOR& frontColor) { - D3DXVECTOR4 v; - - v.x = sideColor.r; - v.y = sideColor.g; - v.z = sideColor.b; - v.w = sideColor.a * g_nEffectAlpha; - TheseusSetVertexShaderConstant(15, &v, 1); + D3DXVECTOR4 side(sideColor.r, sideColor.g, sideColor.b, sideColor.a * g_nEffectAlpha); + TheseusSetVertexShaderConstant(15, &side, 1); - v.x = frontColor.r - sideColor.r; - v.y = frontColor.g - sideColor.g; - v.z = frontColor.b - sideColor.b; - v.w = (frontColor.a - sideColor.a) * g_nEffectAlpha; - TheseusSetVertexShaderConstant(16, &v, 1); + D3DXVECTOR4 delta(frontColor.r - sideColor.r, frontColor.g - sideColor.g, frontColor.b - sideColor.b, + (frontColor.a - sideColor.a) * g_nEffectAlpha); + TheseusSetVertexShaderConstant(16, &delta, 1); } -// ============================================================================ -// VRML compatibility stub nodes: parsed but not rendered. Present so the -// alpha dashboard's IndexedLineSet/Coordinate nodes still parse without -// "unknown class" errors. No Render() override; the runtime just registers -// the property surface and walks past at draw time. -// ============================================================================ +// ===== VRML stub nodes ====================================================== +// Theseus's own, not in 5960: parsed but never drawn, so the alpha dashboards' +// IndexedLineSet and Coordinate nodes load without "unknown class" errors. class CIndexedLineSet : public CNode { From 636766eafdff67660975fff8afde1228a337e746 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:02:29 -0500 Subject: [PATCH 061/167] Skip box buffer writes when a create or lock fails Box copied its cube into the vertex and index buffers without checking that they were created or locked, so a failed create crashed on a NULL buffer. It drops the buffer now and tries again next frame. --- theseus/render/shape_render.cpp | 36 +++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/theseus/render/shape_render.cpp b/theseus/render/shape_render.cpp index 13b5a6e..6761de0 100644 --- a/theseus/render/shape_render.cpp +++ b/theseus/render/shape_render.cpp @@ -411,8 +411,8 @@ CBox::~CBox() // if (!ib) { create 0x48 byte ib; copy 0x12 dwords of indices into it; } // if (vb && ib) { SetStreamSource(0, vb, 0x20); SetIndices(ib, 0); // SetVertexShader(0x112); draw 36 indices as a triangle list; } -// The binary's create helper retries until it gets memory, so the copies -// never check what they got. +// The binary's create helper retries until it gets memory. Theseus's can fail, +// so a buffer that won't create or lock is dropped and retried next frame. void CBox::Render() { if (m_dirty) @@ -426,17 +426,37 @@ void CBox::Render() if (m_pVB == NULL) { TheseusCreateVertexBuffer(sizeof (m_vertices), D3DUSAGE_DYNAMIC, D3DFVF_VERTEX, D3DPOOL_MANAGED, &m_pVB); - m_pVB->Lock(0, sizeof (m_vertices), &pDst, 0); - memcpy(pDst, m_vertices, sizeof (m_vertices)); - m_pVB->Unlock(); + if (m_pVB != NULL) + { + if (FAILED(m_pVB->Lock(0, sizeof (m_vertices), &pDst, 0))) + { + m_pVB->Release(); + m_pVB = NULL; + } + else + { + memcpy(pDst, m_vertices, sizeof (m_vertices)); + m_pVB->Unlock(); + } + } } if (m_pIB == NULL) { TheseusCreateIndexBuffer(sizeof (m_indices), D3DUSAGE_DYNAMIC, D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_pIB); - m_pIB->Lock(0, sizeof (m_indices), &pDst, 0); - memcpy(pDst, m_indices, sizeof (m_indices)); - m_pIB->Unlock(); + if (m_pIB != NULL) + { + if (FAILED(m_pIB->Lock(0, sizeof (m_indices), &pDst, 0))) + { + m_pIB->Release(); + m_pIB = NULL; + } + else + { + memcpy(pDst, m_indices, sizeof (m_indices)); + m_pIB->Unlock(); + } + } } if (m_pVB == NULL || m_pIB == NULL) From cf366729cd10f00b5c850483617184267117f49e Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:10:59 -0500 Subject: [PATCH 062/167] Rebuilt the scene group nodes from the 5960 binary Wrote Group, Transform, Inline, Spinner, Waver, Layout, Switch, Billboard, Layer, Background and Level fresh from the retail XBE and dropped the old asserts and traces. Booting headless and driving every menu and level forward, back and cut short, plus a test scene for the nodes the dash never uses, gives the same 1.6 million line trace as before. --- theseus/render/scene_groups.cpp | 1687 ++++++++++++++++--------------- 1 file changed, 889 insertions(+), 798 deletions(-) diff --git a/theseus/render/scene_groups.cpp b/theseus/render/scene_groups.cpp index bf752dd..976ea19 100755 --- a/theseus/render/scene_groups.cpp +++ b/theseus/render/scene_groups.cpp @@ -1,8 +1,8 @@ -// scene_groups.cpp: scene-graph container nodes (CGroup, CTransform, -// CInline, CSpinner, CWaver, CLayout, CSwitch, CBillboard, CLayer, -// CBackground, CLevel). All confirmed present in the 5960 retail XBE via -// class-registration strings and FND table entries; see -// docs/decomp/SceneGroups.md. +// scene_groups.cpp: the scene graph's container nodes. Group, Transform, +// Inline, Spinner, Waver, Layout, Switch, Billboard, Layer, Background and +// Level, rebuilt function by function from the 5960 retail XBE (Group.cpp at +// 0x4d3b0..0x4f760, Background at 0x4fc40..0x504c0, Layer at 0x5a290..0x5ab30, +// Level at 0x5cbd0..0x5d230). Findings are in docs/decomp/SceneGroups.md. #include "std.h" #include "theseus.h" @@ -24,8 +24,7 @@ extern void BindJoystick(CNode* pJoystickNode); // ===== CGroup =============================================================== -// Base container node. Holds an array of children, renders them in order. -// Computes bounding box as union of child boxes. +// A node with children. Everything else in this file is built on it. IMPLEMENT_NODE("Group", CGroup, CNode) @@ -33,22 +32,39 @@ START_NODE_PROPS(CGroup, CNode) NODE_PROP(pt_children, CGroup, children) END_NODE_PROPS() +// CGroup::CGroup() +// Reconstructed from 5960 retail XBE @ 0x0004d3d0 (the factory, ctor inlined): +// children = {0,0,0}; bboxCenter = 0; bboxSize = -1.0 (0x0002bef0) x3; +// bboxSpecified = 1; bboxDirty = 1; +// A size of -1 on every axis means "nobody gave us a box, work it out". CGroup::CGroup() { - m_bboxCenter.x = 0.0f; - m_bboxCenter.y = 0.0f; - m_bboxCenter.z = 0.0f; - m_bboxSize.x = -1.0f; - m_bboxSize.y = -1.0f; - m_bboxSize.z = -1.0f; + m_bboxCenter = D3DXVECTOR3(0.0f, 0.0f, 0.0f); + m_bboxSize = D3DXVECTOR3(-1.0f, -1.0f, -1.0f); m_bboxSpecified = true; m_bboxDirty = true; } +// CGroup::~CGroup() +// Reconstructed from 5960 retail XBE @ 0x0004d470: ReleaseAll on the +// children, then the CNode teardown. The CNodeArray member does that release. CGroup::~CGroup() { } +// CGroup::GetBBox(pBBox) +// Reconstructed from 5960 retail XBE @ 0x0004d4d0. Ghidra decompile: +// if (size == (-1,-1,-1)) specified = 0; +// if (dirty && !specified) { +// if (count == 0) center = size = 0; +// else { child[0]->GetBBox(&b); lo = b.c - b.s; hi = b.c + b.s; +// for (i = 1..) { GetBBox(&b); +// if (b.c - b.s < lo) lo = b.c - b.s; +// if (b.c + b.s < hi) hi = b.c + b.s; } +// center = (hi + lo) * 0.5; size = hi - lo; } } +// *pBBox = { center, size }; +// The far corner keeps the smaller value too, so it isn't a true union. That's +// the binary's behavior and we keep it. Dirty is never cleared here. void CGroup::GetBBox(BBox* pBBox) { if (m_bboxSize.x == -1.0f && m_bboxSize.y == -1.0f && m_bboxSize.z == -1.0f) @@ -56,59 +72,37 @@ void CGroup::GetBBox(BBox* pBBox) if (m_bboxDirty && !m_bboxSpecified) { - int nChildCount = m_children.GetLength(); - if (nChildCount == 0) + int count = m_children.GetLength(); + if (count == 0) { - m_bboxCenter.x = 0.0f; - m_bboxCenter.y = 0.0f; - m_bboxCenter.z = 0.0f; - m_bboxSize.x = 0.0f; - m_bboxSize.y = 0.0f; - m_bboxSize.z = 0.0f; + m_bboxCenter = D3DXVECTOR3(0.0f, 0.0f, 0.0f); + m_bboxSize = D3DXVECTOR3(0.0f, 0.0f, 0.0f); } else { - BBox bbox; - m_children.GetNode(0)->GetBBox(&bbox); - - float xMin = bbox.center.x - bbox.size.x; - float yMin = bbox.center.y - bbox.size.y; - float zMin = bbox.center.z - bbox.size.z; - float xMax = bbox.center.x + bbox.size.x; - float yMax = bbox.center.y + bbox.size.y; - float zMax = bbox.center.z + bbox.size.z; - - int i; - for (i = 1; i < nChildCount; i += 1) - { - m_children.GetNode(i)->GetBBox(&bbox); - - if (xMin > bbox.center.x - bbox.size.x) - xMin = bbox.center.x - bbox.size.x; - - if (yMin > bbox.center.y - bbox.size.y) - yMin = bbox.center.y - bbox.size.y; - - if (zMin > bbox.center.z - bbox.size.z) - zMin = bbox.center.z - bbox.size.z; - - if (xMax > bbox.center.x + bbox.size.x) - xMax = bbox.center.x + bbox.size.x; + BBox box; + m_children.GetNode(0)->GetBBox(&box); + D3DXVECTOR3 lo = box.center - box.size; + D3DXVECTOR3 hi = box.center + box.size; - if (yMax > bbox.center.y + bbox.size.y) - yMax = bbox.center.y + bbox.size.y; - - if (zMax > bbox.center.z + bbox.size.z) - zMax = bbox.center.z + bbox.size.z; + for (int n = 1; n < count; n++) + { + m_children.GetNode(n)->GetBBox(&box); + D3DXVECTOR3 near_ = box.center - box.size; + D3DXVECTOR3 far_ = box.center + box.size; + + if (near_.x < lo.x) lo.x = near_.x; + if (near_.y < lo.y) lo.y = near_.y; + if (near_.z < lo.z) lo.z = near_.z; + if (far_.x < hi.x) hi.x = far_.x; + if (far_.y < hi.y) hi.y = far_.y; + if (far_.z < hi.z) hi.z = far_.z; } - m_bboxCenter.x = (xMax + xMin) / 2.0f; - m_bboxCenter.y = (yMax + yMin) / 2.0f; - m_bboxCenter.z = (zMax + zMin) / 2.0f; - - m_bboxSize.x = xMax - xMin; - m_bboxSize.y = yMax - yMin; - m_bboxSize.z = zMax - zMin; + m_bboxCenter.x = (hi.x + lo.x) * 0.5f; + m_bboxCenter.y = (hi.y + lo.y) * 0.5f; + m_bboxCenter.z = (hi.z + lo.z) * 0.5f; + m_bboxSize = hi - lo; } } @@ -116,98 +110,97 @@ void CGroup::GetBBox(BBox* pBBox) pBBox->size = m_bboxSize; } +// CGroup::GetRadius() +// Reconstructed from 5960 retail XBE @ 0x0004d770: the largest child radius, +// starting from 0. float CGroup::GetRadius() { - float radius = 0.0f; - int nChildCount = m_children.GetLength(); - - int i; - for (i = 0; i < nChildCount; i += 1) + float best = 0.0f; + for (int n = 0; n < m_children.GetLength(); n++) { - float r = m_children.GetNode(i)->GetRadius(); - if (radius < r) - radius = r; + float r = m_children.GetNode(n)->GetRadius(); + if (r > best) + best = r; } - - return radius; + return best; } +// CGroup::Render() +// Reconstructed from 5960 retail XBE @ 0x0004d7e0: +// for (i = 0; i < count; i++) if (child[i]->visible) child[i]->Render(); +// The visit/skip counters feed Theseus's frame profiler. void CGroup::Render() { - - int nChildCount = m_children.GetLength(); - - int i; - for (i = 0; i < nChildCount; i += 1) + for (int n = 0; n < m_children.GetLength(); n++) { - CNode* pNode = m_children.GetNode(i); + CNode* child = m_children.GetNode(n); Frame_Count(FC_NODE_VISITS); - if (pNode->m_visible) - pNode->Render(); - else + if (!child->m_visible) + { Frame_Count(FC_NODE_SKIPS); + continue; + } + child->Render(); } } +// CGroup::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x0004d820: CNode::Advance, then +// every child. void CGroup::Advance(float nSeconds) { CNode::Advance(nSeconds); - int nChildCount = m_children.GetLength(); - - int i; - for (i = 0; i < nChildCount; i += 1) - m_children.GetNode(i)->Advance(nSeconds); + for (int n = 0; n < m_children.GetLength(); n++) + m_children.GetNode(n)->Advance(nSeconds); } +// CGroup::SetLight +// Not in 5960, whose node vtable has no light slot. Theseus's light pass in +// the app walks the tree with it, so groups hand it on to their children. void CGroup::SetLight(int& nLight, D3DCOLORVALUE& ambient) { - int nChildCount = m_children.GetLength(); - - int i; - for (i = 0; i < nChildCount; i += 1) + for (int n = 0; n < m_children.GetLength(); n++) { - CNode* pNode = m_children.GetNode(i); - if (pNode != NULL) - pNode->SetLight(nLight, ambient); + CNode* child = m_children.GetNode(n); + if (child != NULL) + child->SetLight(nLight, ambient); } } +// CGroup::RenderDynamicTexture(pSurfx) +// Reconstructed from 5960 retail XBE @ 0x0004d860: pass the surface to every +// non-null child. void CGroup::RenderDynamicTexture(CSurfx* pSurfx) { - int nChildCount = m_children.GetLength(); - - int i; - for (i = 0; i < nChildCount; i += 1) + for (int n = 0; n < m_children.GetLength(); n++) { - CNode* pNode = m_children.GetNode(i); - if (pNode != NULL) - pNode->RenderDynamicTexture(pSurfx); + CNode* child = m_children.GetNode(n); + if (child != NULL) + child->RenderDynamicTexture(pSurfx); } } +// CGroup::GetTextureSurface() +// Reconstructed from 5960 retail XBE @ 0x0004d8a0: the first non-null child +// texture, else NULL. LPDIRECT3DTEXTURE8 CGroup::GetTextureSurface() { - int nChildCount = m_children.GetLength(); - - for (int i = 0; i < nChildCount; i += 1) + for (int n = 0; n < m_children.GetLength(); n++) { - CNode* pNode = m_children.GetNode(i); - if (pNode != NULL) - { - LPDIRECT3DTEXTURE8 pSurface = pNode->GetTextureSurface(); - if (pSurface != NULL) - return pSurface; - } + CNode* child = m_children.GetNode(n); + if (child == NULL) + continue; + LPDIRECT3DTEXTURE8 tex = child->GetTextureSurface(); + if (tex != NULL) + return tex; } - return NULL; } // ===== CTransform =========================================================== -// Applies translation, rotation, scale to children with optional fade -// interpolation. Supports animated property transitions via SetScale, -// SetTranslation, SetRotation, SetAlpha. +// Scale, rotation and translation for its children, plus an alpha. With fade +// set, the script setters glide to the new value over fade seconds. IMPLEMENT_NODE("Transform", CTransform, CGroup) @@ -234,205 +227,231 @@ START_NODE_FUN(CTransform, CGroup) END_NODE_FUN() #undef _FND_CLASS +// CTransform::CTransform() +// Reconstructed from 5960 retail XBE @ 0x0004d910. Ghidra decompile: +// rotation = (0,0,1,0); center = 0; scaleOrientation = (0,0,1,0); +// scale = (1,1,1); translation = 0; moving = 0; dirty = 1; fade = 0; +// alpha = 1.0; +0x178 = +0x17c = 0; the five fade clocks (doubles) = 0.0 +// The rotation quaternion is left for OnLoad to build. CTransform::CTransform() : + m_rotation(0.0f, 0.0f, 1.0f, 0.0f), m_center(0.0f, 0.0f, 0.0f), m_scaleOrientation(0.0f, 0.0f, 1.0f, 0.0f), m_scale(1.0f, 1.0f, 1.0f), - m_rotation(0.0f, 0.0f, 1.0f, 0.0f), m_translation(0.0f, 0.0f, 0.0f), + m_alpha(1.0f), + m_moving(false), + m_dirty(TRUE), m_fade(0.0f), - m_moving(false) + m_timeCenterStart(0.0f), + m_timeScaleOrientationStart(0.0f), + m_timeScaleStart(0.0), + m_timeRotationStart(0.0), + m_timeTranslationStart(0.0), + m_timeAlphaStart(0.0), + m_timeToDisappear(0.0) { - m_dirty = true; - m_alpha = 1.0f; - m_timeCenterStart = 0.0f; - m_timeScaleStart = 0.0f; - m_timeScaleOrientationStart = 0.0f; - m_timeRotationStart = 0.0f; - m_timeTranslationStart = 0.0f; - m_timeAlphaStart = 0.0f; - m_timeToDisappear = 0.0; } +// CTransform::OnLoad() +// Reconstructed from 5960 retail XBE @ 0x0004da40: +// CNode::OnLoad(); D3DXQuaternionRotationAxis(&rotationQuat, &rotation.xyz, rotation.w); void CTransform::OnLoad() { CGroup::OnLoad(); D3DXQuaternionRotationAxis(&m_rotationQuat, (D3DXVECTOR3*)&m_rotation, m_rotation.w); } +// CTransform::OnSetProperty(pprd, pvValue) +// Reconstructed from 5960 retail XBE @ 0x0004da60. Ghidra decompile: +// if (prd->offset == 0x44) QuaternionRotationAxis(&quat, value, value[3]); +// else if (prd->offset == 0x120) { SetAlpha inlined; return 0; } +// return 1; +// A rotation write also rebuilds the quaternion; an alpha write goes through +// SetAlpha (so it can fade) and the raw store is vetoed. bool CTransform::OnSetProperty(const PRD* pprd, const void* pvValue) { - if (PTR2INT(pprd->pbOffset) == offsetof(m_rotation)) + int offset = PTR2INT(pprd->pbOffset); + if (offset == offsetof(m_rotation)) { - const float* p = (const float*)pvValue; - D3DXQuaternionRotationAxis(&m_rotationQuat, (D3DXVECTOR3*)p, p[3]); + const float* axisAngle = (const float*)pvValue; + D3DXQuaternionRotationAxis(&m_rotationQuat, (const D3DXVECTOR3*)axisAngle, axisAngle[3]); } - else if (PTR2INT(pprd->pbOffset) == offsetof(m_alpha)) + else if (offset == offsetof(m_alpha)) { - SetAlpha(*(float*)pvValue); + SetAlpha(*(const float*)pvValue); return false; } + return true; +} - return CGroup::OnSetProperty(pprd, pvValue); +// Fraction of a fade that started at `start`, clamped to 1. When it reaches +// 1 the clock is stopped (set back to 0). The binary inlines this four times. +static float FadeStep(XTIME& start, float fade) +{ + float t = (float)(TheseusGetNow() - start) / fade; + if (t >= 1.0f) + { + start = 0.0; + t = 1.0f; + } + return t; } +// CTransform::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x0004dae0. Ghidra decompile: +// CGroup::Advance(dt); +// if (timeAlphaStart > 0) { t = (now - timeAlphaStart) / fade; +// if (t >= 1) { timeAlphaStart = 0; t = 1; } +// alpha = (alphaEnd - alphaStart) * t + alphaStart; } +// if (timeToDisappear > 0 && timeToDisappear <= now) { timeToDisappear = 0; alpha = 0; } void CTransform::Advance(float nSeconds) { CGroup::Advance(nSeconds); - if (m_timeAlphaStart > 0.0f) + if (m_timeAlphaStart > 0.0) { - float t = (float) (TheseusGetNow() - m_timeAlphaStart) / m_fade; - if (t >= 1.0f) - { - t = 1.0f; - m_timeAlphaStart = 0.0f; - } - + float t = FadeStep(m_timeAlphaStart, m_fade); m_alpha = m_alphaStart + (m_alphaEnd - m_alphaStart) * t; } - if (m_timeToDisappear > 0.0f && TheseusGetNow() >= m_timeToDisappear) + if (m_timeToDisappear > 0.0 && m_timeToDisappear <= TheseusGetNow()) { + m_timeToDisappear = 0.0; m_alpha = 0.0f; - m_timeToDisappear = 0.0f; } } +// CTransform::CalcMatrix() +// Reconstructed from 5960 retail XBE @ 0x0004dbb0. Starts from identity, steps +// any running scale and translation fades (vector lerp) and the rotation fade +// (D3DXQuaternionSlerp), each one setting moving. Then multiplies in scale if +// it isn't (1,1,1), the quaternion if its w isn't 0, and the translation if it +// isn't zero, in that order. Dirty stays set while any of the three fades is +// still running (sum of their clocks > 0). void CTransform::CalcMatrix() { D3DXMatrixIdentity(&m_matrix); - if (m_timeScaleStart > 0.0f) + if (m_timeScaleStart > 0.0) { m_moving = true; - - float t = (float) (TheseusGetNow() - m_timeScaleStart) / m_fade; - if (t >= 1.0f) - { - t = 1.0f; - m_timeScaleStart = 0.0f; - } - + float t = FadeStep(m_timeScaleStart, m_fade); D3DXVec3Lerp(&m_scale, &m_scaleStart, &m_scaleEnd, t); } - if (m_timeTranslationStart > 0.0f) + if (m_timeTranslationStart > 0.0) { m_moving = true; - - float t = (float) (TheseusGetNow() - m_timeTranslationStart) / m_fade; - if (t >= 1.0f) - { - t = 1.0f; - m_timeTranslationStart = 0.0f; - } - + float t = FadeStep(m_timeTranslationStart, m_fade); D3DXVec3Lerp(&m_translation, &m_translationStart, &m_translationEnd, t); } - if (m_timeRotationStart > 0.0f) + if (m_timeRotationStart > 0.0) { m_moving = true; - - float t = (float) (TheseusGetNow() - m_timeRotationStart) / m_fade; - if (t >= 1.0f) - { - t = 1.0f; - m_timeRotationStart = 0.0f; - } - + float t = FadeStep(m_timeRotationStart, m_fade); D3DXQuaternionSlerp(&m_rotationQuat, &m_rotationStart, &m_rotationEnd, t); } + D3DXMATRIX step; + if (m_scale.x != 1.0f || m_scale.y != 1.0f || m_scale.z != 1.0f) { - D3DXMATRIX mat; - D3DXMatrixScaling(&mat, m_scale.x, m_scale.y, m_scale.z); - D3DXMatrixMultiply(&m_matrix, &m_matrix, &mat); + D3DXMatrixScaling(&step, m_scale.x, m_scale.y, m_scale.z); + D3DXMatrixMultiply(&m_matrix, &m_matrix, &step); } if (m_rotationQuat.w != 0.0f) { - D3DXMATRIX mat; - D3DXMatrixRotationQuaternion(&mat, &m_rotationQuat); - D3DXMatrixMultiply(&m_matrix, &m_matrix, &mat); + D3DXMatrixRotationQuaternion(&step, &m_rotationQuat); + D3DXMatrixMultiply(&m_matrix, &m_matrix, &step); } if (m_translation.x != 0.0f || m_translation.y != 0.0f || m_translation.z != 0.0f) { - D3DXMATRIX mat; - D3DXMatrixTranslation(&mat, m_translation.x, m_translation.y, m_translation.z); - D3DXMatrixMultiply(&m_matrix, &m_matrix, &mat); + D3DXMatrixTranslation(&step, m_translation.x, m_translation.y, m_translation.z); + D3DXMatrixMultiply(&m_matrix, &m_matrix, &step); } - m_dirty = (m_timeScaleStart + m_timeTranslationStart + m_timeRotationStart) > 0.0f; + m_dirty = (m_timeScaleStart + m_timeTranslationStart + m_timeRotationStart) > 0.0; } +// CTransform::SetScale(x, y, z) [script fn 'SetScale', FND 0x00187dc8, sig 22] +// Reconstructed from 5960 retail XBE @ 0x0004df30. Ghidra decompile: +// if (fade > 0) { timeScaleStart = now; scaleStart = scale; +// scaleEnd = (x,y,z); moving = 1; } +// else scale = (x,y,z); +// dirty = 1; void CTransform::SetScale(float sx, float sy, float sz) { + D3DXVECTOR3 target(sx, sy, sz); if (m_fade > 0.0f) { m_timeScaleStart = TheseusGetNow(); m_scaleStart = m_scale; - m_scaleEnd.x = sx; - m_scaleEnd.y = sy; - m_scaleEnd.z = sz; + m_scaleEnd = target; m_moving = true; } else { - m_scale.x = sx; - m_scale.y = sy; - m_scale.z = sz; + m_scale = target; } - - m_dirty = true; + m_dirty = TRUE; } +// CTransform::SetScaleOrientation(x, y, z, a) [script fn 'SetScaleOrientation', FND 0x00187de0, sig 23] +// Reconstructed from 5960 retail XBE @ 0x0004dfd0: +// scaleOrientation = (x,y,z,a); dirty = 1; +// Stored only; CalcMatrix never reads it. void CTransform::SetScaleOrientation(float x, float y, float z, float a) { - m_scaleOrientation.x = x; - m_scaleOrientation.y = y; - m_scaleOrientation.z = z; - m_scaleOrientation.w = a; - m_dirty = true; + m_scaleOrientation = D3DXQUATERNION(x, y, z, a); + m_dirty = TRUE; } +// CTransform::SetTranslation(x, y, z) [script fn 'SetTranslation', FND 0x00187df8, sig 22] +// Reconstructed from 5960 retail XBE @ 0x0004e010: same shape as SetScale, +// on the translation and its fade clock. void CTransform::SetTranslation(float x, float y, float z) { + D3DXVECTOR3 target(x, y, z); if (m_fade > 0.0f) { m_timeTranslationStart = TheseusGetNow(); m_translationStart = m_translation; - m_translationEnd.x = x; - m_translationEnd.y = y; - m_translationEnd.z = z; + m_translationEnd = target; m_moving = true; } else { - m_translation.x = x; - m_translation.y = y; - m_translation.z = z; + m_translation = target; } - - m_dirty = true; + m_dirty = TRUE; } +// CTransform::SetCenter(x, y, z) [script fn 'SetCenter', FND 0x00187e10, sig 22] +// Reconstructed from 5960 retail XBE @ 0x0004e0c0: +// center = (x,y,z); dirty = 1; void CTransform::SetCenter(float x, float y, float z) { - m_center.x = x; - m_center.y = y; - m_center.z = z; - m_dirty = true; + m_center = D3DXVECTOR3(x, y, z); + m_dirty = TRUE; } +// CTransform::SetRotation(x, y, z, a) [script fn 'SetRotation', FND 0x00187e28, sig 23] +// Reconstructed from 5960 retail XBE @ 0x0004e0f0. Ghidra decompile: +// QuaternionRotationAxis(&q, &(x,y,z), a); +// if (fade > 0) { timeRotationStart = now; rotationStart = rotationQuat; +// rotationEnd = q; moving = 1; } +// else { rotation = (x,y,z,a); rotationQuat = q; } +// dirty = 1; +// A fading rotation leaves the axis/angle property alone. void CTransform::SetRotation(float x, float y, float z, float a) { - D3DXVECTOR3 v(x, y, z); + D3DXVECTOR3 axis(x, y, z); D3DXQUATERNION q; - D3DXQuaternionRotationAxis(&q, &v, a); + D3DXQuaternionRotationAxis(&q, &axis, a); if (m_fade > 0.0f) { @@ -443,16 +462,16 @@ void CTransform::SetRotation(float x, float y, float z, float a) } else { - m_rotation.x = x; - m_rotation.y = y; - m_rotation.z = z; - m_rotation.w = a; + m_rotation = D3DXQUATERNION(x, y, z, a); m_rotationQuat = q; } - - m_dirty = true; + m_dirty = TRUE; } +// CTransform::SetAlpha(a) [script fn 'SetAlpha', FND 0x00187e40, sig 19] +// Reconstructed from 5960 retail XBE @ 0x0004e200: +// if (fade > 0) { timeAlphaStart = now; alphaStart = alpha; alphaEnd = a; } +// else alpha = a; void CTransform::SetAlpha(float a) { if (m_fade > 0.0f) @@ -467,15 +486,27 @@ void CTransform::SetAlpha(float a) } } +// CTransform::DisappearAfter(t) [script fn 'DisappearAfter', FND 0x00187e58, sig 19] +// Reconstructed from 5960 retail XBE @ 0x0004e250: +// timeToDisappear = (double)(t + (float)now); +// The binary adds in single precision; Theseus keeps the double clock. void CTransform::DisappearAfter(float t) { - m_timeToDisappear = TheseusGetNow() + t; + m_timeToDisappear = t + TheseusGetNow(); } +// CTransform::Render() +// Reconstructed from 5960 retail XBE @ 0x0004e270. Ghidra decompile: +// moving = 0; if (dirty) CalcMatrix(); +// world->Push(); world->MultMatrixLocal(&matrix); +// if (alpha > 0) { saved = effectAlpha; effectAlpha *= alpha; +// CGroup::Render(); effectAlpha = saved; } +// world->Pop(); +// moving reads true only on frames where a fade stepped the matrix. A fully +// transparent subtree counts as one skip for Theseus's frame profiler. void CTransform::Render() { m_moving = false; - if (m_dirty) CalcMatrix(); @@ -483,30 +514,25 @@ void CTransform::Render() TheseusMultWorld(&m_matrix); TheseusUpdateWorld(); - float nEffectAlphaSave = g_nEffectAlpha; - g_nEffectAlpha *= m_alpha; - if (m_alpha > 0.0f) { + float outerAlpha = g_nEffectAlpha; + g_nEffectAlpha = outerAlpha * m_alpha; CGroup::Render(); + g_nEffectAlpha = outerAlpha; } else { - // Whole subtree skipped due to zero alpha. Counts as one - // skip event regardless of subtree size since we don't - // descend to know how big it is. Frame_Count(FC_NODE_SKIPS); } - g_nEffectAlpha = nEffectAlphaSave; - TheseusPopWorld(); } // ===== CInline ============================================================== -// Loads a child scene from a URL (with optional XIP archive). -// Supports background loading and fade-in on delay-loaded content. +// Pulls another XAP in as a child, out of its own XIP when there is one, and +// fades it in once it arrives. class CInline : public CGroup { @@ -538,14 +564,13 @@ class CInline : public CGroup DECLARE_NODE_PROPS() #ifdef THESEUS_DESKTOP - // Hot-reload from new XAP source text (XAP editor "save" loop) + // Hot reload from new XAP text (the XAP editor's save loop) bool ReloadFromText(const char* xapText); #endif private: HANDLE m_hClassLoaderThread; TCHAR m_szAbsURL[MAX_PATH]; - static void WINAPI ClassLoaderThread(CInline* p); }; IMPLEMENT_NODE("Inline", CInline, CGroup) @@ -556,82 +581,100 @@ START_NODE_PROPS(CInline, CGroup) NODE_PROP(pt_boolean, CInline, fadeInDelayLoad) END_NODE_PROPS() +// CInline::CInline() +// Reconstructed from 5960 retail XBE @ 0x0004e3b0. Ghidra decompile: +// url = 0; preload = 0; classLoaded = 0; fadeInDelayLoad = 1; CClass(); +// thread = 0; timeOfLoad = 0.0; loading = 0; firstLoad = 0; xip = 0; dirty = 1; CInline::CInline() : m_url(NULL), - m_hClassLoaderThread(NULL), + m_preload(false), m_bClassLoaded(false), m_fadeInDelayLoad(true), - m_preload(false) + m_dirty(true), + m_bLoading(false), + m_bFirstLoad(false), + m_xipFile(NULL), + m_timeOfLoad(0.0), + m_hClassLoaderThread(NULL) { - m_dirty = true; - m_bLoading = false; - m_bFirstLoad = false; - m_xipFile = NULL; - m_timeOfLoad = 0.0f; } +// CInline::~CInline() +// Reconstructed from 5960 retail XBE @ 0x0004e470: free the url, then the +// class and group teardown. CInline::~CInline() { delete [] m_url; } +// CInline::Init() +// Reconstructed from 5960 retail XBE @ 0x0004e4d0. Ghidra decompile: +// dirty = 0; children.ReleaseAll(); if (!url) return; +// MakeAbsoluteURL(path, url); p = wcsrchr(path, '/'); +// if (p) { wcscpy(p, ".xip"); loading = 1; firstLoad = 1; +// xip = LoadXIP(path); if (xip) return; loading = 0; } +// FinishLoad(); +// The archive is named after the folder holding the XAP. With one, the load +// finishes later in Advance; without, the class loads right here. void CInline::Init() { - ASSERT(m_dirty); m_dirty = false; m_children.ReleaseAll(); if (m_url == NULL) return; - ASSERT(!m_bLoading); - - // Start background XIP load if there is an archive for this URL + TCHAR xipPath[MAX_PATH]; + MakeAbsoluteURL(xipPath, m_url); + TCHAR* slash = _tcsrchr(xipPath, '/'); + if (slash != NULL) { - TCHAR szURL [MAX_PATH]; - - MakeAbsoluteURL(szURL, m_url); - TCHAR* pch = _tcsrchr(szURL, '/'); - if (pch != NULL) - { - _tcscpy(pch, _T(".xip")); - - m_bLoading = true; - m_bFirstLoad = true; - m_xipFile = LoadXIP(szURL); - if (m_xipFile != NULL) - return; - - TRACE(_T("\001LoadXIP: %s failed!\n")); - - m_bLoading = false; - } + _tcscpy(slash, _T(".xip")); + m_bLoading = true; + m_bFirstLoad = true; + m_xipFile = LoadXIP(xipPath); + if (m_xipFile != NULL) + return; + m_bLoading = false; } FinishLoad(); } +// CInline::FinishLoad() +// Reconstructed from 5960 retail XBE @ 0x0004e560. Ghidra decompile: +// if (m_class.LoadAbsURL(absURL)) { n = m_class.CreateNode(); +// CallFunction(n, "initialize"); children.AddNode(n); n->parent = this; +// CallFunction(this, "onLoad"); } +// firstLoad = 0; classLoaded = 1; +// The load timer around it is Theseus's (frame profiler hitch report). void CInline::FinishLoad() { - TRACE(_T("FinishLoad: (0x%08x) %s\n"), this, m_url); - ASSERT(!m_bClassLoaded); + long long started = Frame_Now(); - // Compile plus initialize/onLoad lands inside one frame; that's the hitch. - long long loadStart = Frame_Now(); if (m_class.LoadAbsURL(m_szAbsURL)) { - CNode* pNode = m_class.CreateNode(); - CallFunction(pNode, _T("initialize")); - - m_children.AddNode(pNode); - pNode->m_parent = this; + CNode* root = m_class.CreateNode(); + CallFunction(root, _T("initialize")); + m_children.AddNode(root); + root->m_parent = this; CallFunction(this, _T("onLoad")); } + m_bFirstLoad = false; m_bClassLoaded = true; - Frame_NoteLoad(Frame_Ms(Frame_Now() - loadStart)); + + Frame_NoteLoad(Frame_Ms(Frame_Now() - started)); } +// CInline::Render() +// Reconstructed from 5960 retail XBE @ 0x0004e5d0. Nothing without a url or +// while loading. If our XIP had its mesh buffers dropped (loaded, not +// reloading, has vertex buffers, first one gone) it starts a reload and skips +// the frame. Otherwise Init when dirty, and once the class is in, render the +// children with the effect alpha scaled by the fade in, (now - timeOfLoad) / 2 +// capped at 1 (or 1 straight away without fadeInDelayLoad), inside the XAP's +// directory. See docs/decomp/SceneGroups.md. void CInline::Render() { if (m_url == NULL || m_bLoading) @@ -650,37 +693,34 @@ void CInline::Render() if (m_bLoading || !m_bClassLoaded) return; - float a = (float) (TheseusGetNow() - m_timeOfLoad) / 2.0f; - - if (!m_fadeInDelayLoad) + float fadeIn = 1.0f; + if (m_fadeInDelayLoad) { - a = 1.0f; + fadeIn = (float)(TheseusGetNow() - m_timeOfLoad) * 0.5f; + if (fadeIn > 1.0f) + fadeIn = 1.0f; } - else if (a > 1.0f) + + float outerAlpha = g_nEffectAlpha; + g_nEffectAlpha = outerAlpha * fadeIn; { - a = 1.0f; + CDirPush inXapDir(m_url); + CGroup::Render(); } - - float nEffectAlphaSave = g_nEffectAlpha; - g_nEffectAlpha *= a; - - CDirPush dirPush(m_url); - - CGroup::Render(); - - g_nEffectAlpha = nEffectAlphaSave; -} - -void WINAPI CInline::ClassLoaderThread(CInline* p) -{ - ASSERT(p->m_xipFile); - ASSERT(p->m_bFirstLoad); - ASSERT(!p->m_bLoading); - ASSERT(!p->m_bClassLoaded); - ASSERT(p->m_xipFile->IsReady()); - p->FinishLoad(); -} - + g_nEffectAlpha = outerAlpha; +} + +// CInline::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x0004e740. Ghidra decompile: +// if (dirty && preload) Init(); +// if (xip && (!xip->loaded || xip->reloading)) { loading = 1; return; } +// if (loading) { loading = 0; +// if (firstLoad) { MakeAbsoluteURL(absURL, url); if (!thread) FinishLoad(); } +// timeOfLoad = now; } +// if (thread && WaitForSingleObject(thread, 0) == 0) { CloseHandle(thread); thread = 0; } +// CGroup::Advance(dt); +// The class always loads on this thread (the script engine isn't thread +// safe), so the handle is never set; the check is kept as the binary has it. void CInline::Advance(float nSeconds) { if (m_dirty && m_preload) @@ -694,37 +734,20 @@ void CInline::Advance(float nSeconds) if (m_bLoading) { - ASSERT(m_xipFile != NULL); - if (!m_xipFile->IsReady()) - return; - m_bLoading = false; - if (m_bFirstLoad) { - ASSERT(m_hClassLoaderThread == NULL); - MakeAbsoluteURL(m_szAbsURL, m_url); - - // Class loader can't run in a separate thread; the scripting - // engine is not thread-safe. - if (!m_hClassLoaderThread) - { + if (m_hClassLoaderThread == NULL) FinishLoad(); - } } - m_timeOfLoad = TheseusGetNow(); } - if (m_hClassLoaderThread) + if (m_hClassLoaderThread != NULL && WaitForSingleObject(m_hClassLoaderThread, 0) == WAIT_OBJECT_0) { - if (WaitForSingleObject(m_hClassLoaderThread, 0) == WAIT_OBJECT_0) - { - ASSERT(m_bClassLoaded); - CloseHandle(m_hClassLoaderThread); - m_hClassLoaderThread = NULL; - } + CloseHandle(m_hClassLoaderThread); + m_hClassLoaderThread = NULL; } CGroup::Advance(nSeconds); @@ -732,7 +755,7 @@ void CInline::Advance(float nSeconds) // ===== CSpinner ============================================================= -// Continuously rotates children around an axis at a given RPM. +// Turns its children around an axis, rpm turns a minute. class CSpinner : public CGroup { @@ -758,23 +781,26 @@ START_NODE_PROPS(CSpinner, CGroup) NODE_PROP(pt_number, CSpinner, angle) END_NODE_PROPS() -CSpinner::CSpinner() +// CSpinner::CSpinner() +// Reconstructed from 5960 retail XBE @ 0x0004e820 (factory, ctor inlined): +// rpm = 1.0; axis = (0, 1, 0); angle = 0; +CSpinner::CSpinner() : + m_rpm(1.0f), + m_axis(0.0f, 1.0f, 0.0f), + m_angle(0.0f) { - m_rpm = 1.0f; - m_axis.x = 0.0f; - m_axis.y = 1.0f; - m_axis.z = 0.0f; - m_angle = 0.0f; } +// CSpinner::Render() (shared with CWaver) +// Reconstructed from 5960 retail XBE @ 0x0004e8c0: +// world->Push(); D3DXMatrixRotationAxis(&m, &axis, angle); +// world->MultMatrixLocal(&m); ; CGroup::Render(); world->Pop(); void CSpinner::Render() { + D3DXMATRIX spin; TheseusPushWorld(); - - D3DXMATRIX matrix; - D3DXMatrixRotationAxis(&matrix, &m_axis, m_angle); - - TheseusMultWorld(&matrix); + D3DXMatrixRotationAxis(&spin, &m_axis, m_angle); + TheseusMultWorld(&spin); TheseusUpdateWorld(); CGroup::Render(); @@ -782,22 +808,28 @@ void CSpinner::Render() TheseusPopWorld(); } +// CSpinner::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x0004e980. Ghidra decompile: +// CGroup::Advance(dt); +// angle += rpm * (1/60) * dt * 2pi; +// while (angle > 2pi) angle -= 2pi; while (angle < -2pi) angle += 2pi; +// The binary multiplies by a stored 1/60; written as the division it stands for. void CSpinner::Advance(float nSeconds) { CGroup::Advance(nSeconds); - m_angle += ((m_rpm / 60.0f) * nSeconds) * (2.0f * D3DX_PI); + const float turn = 2.0f * D3DX_PI; + m_angle += m_rpm / 60.0f * nSeconds * turn; - while (m_angle > 2.0f * D3DX_PI) - m_angle -= 2.0f * D3DX_PI; - while (m_angle < -2.0f * D3DX_PI) - m_angle += 2.0f * D3DX_PI; + while (m_angle > turn) + m_angle -= turn; + while (m_angle < -turn) + m_angle += turn; } // ===== CWaver =============================================================== -// Oscillates children sinusoidally around an axis. Extends CSpinner but -// overrides Advance to use sin() instead of linear accumulation. +// A Spinner that rocks back and forth instead of going round. class CWaver : public CSpinner { @@ -818,21 +850,30 @@ START_NODE_PROPS(CWaver, CSpinner) NODE_PROP(pt_number, CWaver, field) END_NODE_PROPS() -CWaver::CWaver() +// CWaver::CWaver() +// Reconstructed from 5960 retail XBE @ 0x0004ea20 (factory, ctor inlined): +// the Spinner defaults plus field = pi/4 (0x0002c150). +CWaver::CWaver() : + m_field(D3DX_PI / 4.0f) { - m_field = D3DX_PI / 4.0f; } +// CWaver::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x0004eaf0. Disassembly: +// CGroup::Advance(dt); // not CSpinner's +// angle = sin(rpm * now * (pi/60)) * field * 0.5; +// The angle is a function of the clock alone, swinging field wide. void CWaver::Advance(float nSeconds) { - CGroup::Advance(nSeconds); // Intentionally skips CSpinner::Advance - m_angle = sinf((float) (TheseusGetNow() * D3DX_PI * m_rpm / 60.0f)) * m_field / 2.0f; + CGroup::Advance(nSeconds); + float phase = (float)(D3DX_PI * TheseusGetNow() * m_rpm / 60.0f); + m_angle = sinf(phase) * m_field * 0.5f; } // ===== CLayout ============================================================== -// Arranges children in a line along a direction vector, or in a circle -// if direction is zero. +// Lines its children up along direction, or rings them round the Y axis when +// direction is zero. class CLayout : public CGroup { @@ -857,143 +898,146 @@ START_NODE_PROPS(CLayout, CGroup) NODE_PROP(pt_number, CLayout, spacing) END_NODE_PROPS() +// CLayout::CLayout() +// Reconstructed from 5960 retail XBE @ 0x0004eb30 (factory, ctor inlined): +// direction = (1, 0, 0); spacing = 1.0; CLayout::CLayout() : m_direction(1.0f, 0.0f, 0.0f), m_spacing(1.0f) { } +// CLayout::Render() +// Reconstructed from 5960 retail XBE @ 0x0004ebd0. Ring (direction zero): +// radius (count / 4) * spacing, integer divide. Each child i gets a Push, then +// if visible: translate (cos(-a), 0, sin(-a)) * radius with a = 2pi * i / count, +// rotate a + pi/2 about (0, 0.1, 0), upload, render, Pop. A hidden child's Push +// is never popped; the binary does that too. Line: Push, step back +// (count + spacing) / 2 along direction, then per visible child step half a +// unit, upload, render, and step spacing + 0.5 more unless it's the last; Pop +// at the end. void CLayout::Render() { - - int nChildren = m_children.GetLength(); - if (nChildren == 0) + int count = m_children.GetLength(); + if (count == 0) return; - if (m_direction.x == 0 && m_direction.y == 0 && m_direction.z == 0) + if (m_direction.x == 0.0f && m_direction.y == 0.0f && m_direction.z == 0.0f) { - // Circular layout - D3DXMATRIX mat = *TheseusGetWorld(); + float radius = (float)(count / 4) * m_spacing; + D3DXVECTOR3 up(0.0f, 0.1f, 0.0f); - float r = (nChildren / 4) * m_spacing; - - int i; - for (i = 0; i < nChildren; i += 1) + for (int n = 0; n < count; n++) { TheseusPushWorld(); - CNode* pNode = m_children.GetNode(i); + CNode* child = m_children.GetNode(n); Frame_Count(FC_NODE_VISITS); - if (!pNode->m_visible) + if (!child->m_visible) { Frame_Count(FC_NODE_SKIPS); continue; } - float a = D3DX_PI * 2.0f * i / nChildren; - float x = r * cosf(-a); - float z = r * sinf(-a); - - D3DXVECTOR3 axis(0.0f, 0.1f, 0.0f); - TheseusTranslateWorld(x, 0.0f, z); - TheseusRotateWorld(&axis, a + D3DX_PI / 2.0f); + float a = 2.0f * D3DX_PI * n / count; + TheseusTranslateWorld(radius * cosf(-a), 0.0f, radius * sinf(-a)); + TheseusRotateWorld(&up, a + D3DX_PI / 2.0f); TheseusUpdateWorld(); - pNode->Render(); + child->Render(); TheseusPopWorld(); } - return; } - // Linear layout float spacing = m_spacing; + D3DXVECTOR3 step; TheseusPushWorld(); - D3DXVECTOR3 v; - D3DXVec3Scale(&v, &m_direction, -((float)nChildren + spacing) / 2); - TheseusTranslateWorld(v.x, v.y, v.z); + D3DXVec3Scale(&step, &m_direction, (count + spacing) * -0.5f); + TheseusTranslateWorld(step.x, step.y, step.z); - int i; - for (i = 0; i < nChildren; i += 1) + for (int n = 0; n < count; n++) { - CNode* pNode = m_children.GetNode(i); + CNode* child = m_children.GetNode(n); Frame_Count(FC_NODE_VISITS); - if (!pNode->m_visible) + if (!child->m_visible) { Frame_Count(FC_NODE_SKIPS); continue; } - D3DXVec3Scale(&v, &m_direction, 0.5f); - - TheseusTranslateWorld(v.x, v.y, v.z); - + D3DXVec3Scale(&step, &m_direction, 0.5f); + TheseusTranslateWorld(step.x, step.y, step.z); TheseusUpdateWorld(); - pNode->Render(); - if (i < nChildren - 1) + child->Render(); + + if (n < count - 1) { - D3DXVec3Scale(&v, &m_direction, 0.5f + spacing); - TheseusTranslateWorld(v.x, v.y, v.z); + D3DXVec3Scale(&step, &m_direction, spacing + 0.5f); + TheseusTranslateWorld(step.x, step.y, step.z); } } TheseusPopWorld(); } +// CLayout::GetBBox(pBBox) +// Reconstructed from 5960 retail XBE @ 0x0004ef00. Ghidra decompile: +// if (count == 0) { *pBBox = 0; return; } +// child[0]->GetBBox(pBBox); +// for (i = 1..) { child[i]->GetBBox(&b); size += b.size; +// if (b.size < size) size = b.size; // per axis } +// Center is the first child's; for positive sizes each pass ends on the +// newest child's size. Kept as the binary has it. void CLayout::GetBBox(BBox* pBBox) { - if (m_children.GetLength() == 0) + int count = m_children.GetLength(); + if (count == 0) { CNode::GetBBox(pBBox); return; } - float spacing = m_spacing + 1.0f; m_children.GetNode(0)->GetBBox(pBBox); - int i; - for (i = 1; i < m_children.GetLength(); i += 1) + + for (int n = 1; n < count; n++) { - CNode* pNode = m_children.GetNode(i); - BBox bbox; - pNode->GetBBox(&bbox); - - D3DXVECTOR3 v; - D3DXVec3Scale(&v, &m_direction, spacing); - v.x *= bbox.size.x; - v.y *= bbox.size.y; - v.z *= bbox.size.z; - pBBox->size += bbox.size; - - if (pBBox->size.x > bbox.size.x) - pBBox->size.x = bbox.size.x; - if (pBBox->size.y > bbox.size.y) - pBBox->size.y = bbox.size.y; - if (pBBox->size.z > bbox.size.z) - pBBox->size.z = bbox.size.z; + BBox box; + m_children.GetNode(n)->GetBBox(&box); + + D3DXVECTOR3& size = pBBox->size; + size += box.size; + if (box.size.x < size.x) size.x = box.size.x; + if (box.size.y < size.y) size.y = box.size.y; + if (box.size.z < size.z) size.z = box.size.z; } } +// CLayout::GetRadius() +// Reconstructed from 5960 retail XBE @ 0x0004f000: +// r = sum(child radius); if (count > 1) r += (count - 1) * spacing * 0.5; float CLayout::GetRadius() { - float radius = 0.0f; + int count = m_children.GetLength(); + float total = 0.0f; - for (int i = 0; i < m_children.GetLength(); i += 1) - radius += m_children.GetNode(i)->GetRadius(); + for (int n = 0; n < count; n++) + total += m_children.GetNode(n)->GetRadius(); - if (m_children.GetLength() > 1) - radius += (m_children.GetLength() - 1) * m_spacing / 2.0f; + if (count > 1) + total += (float)(count - 1) * m_spacing * 0.5f; - return radius; + return total; } // ===== CSwitch ============================================================== -// Shows one child selected by index (whichChoice). All children still -// get Advance() calls regardless of selection. +// Shows the one choice whichChoice picks (none when out of range), but keeps +// all of them advancing. class CSwitch : public CNode { @@ -1012,6 +1056,9 @@ class CSwitch : public CNode void SetLight(int& nLight, D3DCOLORVALUE& ambient); DECLARE_NODE_PROPS() + +private: + CNode* Chosen(); }; IMPLEMENT_NODE("Switch", CSwitch, CNode) @@ -1021,68 +1068,93 @@ START_NODE_PROPS(CSwitch, CNode) NODE_PROP(pt_nodearray, CSwitch, choice) END_NODE_PROPS() +// CSwitch::CSwitch() +// Reconstructed from 5960 retail XBE @ 0x0004f080 (factory, ctor inlined): +// whichChoice = -1; choice = {0,0,0}; CSwitch::CSwitch() : m_whichChoice(-1) { } +// CSwitch::~CSwitch() +// Reconstructed from 5960 retail XBE @ 0x0004f100: ReleaseAll on the +// choices (the CNodeArray member does it), then the CNode teardown. CSwitch::~CSwitch() { } +// The picked choice, or NULL when whichChoice is out of range. Every Switch +// method in the binary opens with this bounds test. +CNode* CSwitch::Chosen() +{ + if (m_whichChoice < 0 || m_whichChoice >= m_choice.GetLength()) + return NULL; + return m_choice.GetNode(m_whichChoice); +} + +// CSwitch::Render() +// Reconstructed from 5960 retail XBE @ 0x0004f160: render the choice if it's +// in range and visible. Visit/skip counts are Theseus's profiler. void CSwitch::Render() { + CNode* chosen = Chosen(); + if (chosen == NULL) + return; - if (m_whichChoice >= 0 && m_whichChoice < m_choice.GetLength()) + Frame_Count(FC_NODE_VISITS); + if (!chosen->m_visible) { - CNode* pNode = m_choice.GetNode(m_whichChoice); - Frame_Count(FC_NODE_VISITS); - if (pNode->m_visible) - pNode->Render(); - else - Frame_Count(FC_NODE_SKIPS); + Frame_Count(FC_NODE_SKIPS); + return; } + chosen->Render(); } +// CSwitch::GetBBox(pBBox) +// Reconstructed from 5960 retail XBE @ 0x0004f190: the choice's box, else +// all zeros. void CSwitch::GetBBox(BBox* pBBox) { - if (m_whichChoice >= 0 && m_whichChoice < m_choice.GetLength()) - m_choice.GetNode(m_whichChoice)->GetBBox(pBBox); - else + CNode* chosen = Chosen(); + if (chosen == NULL) + { CNode::GetBBox(pBBox); + return; + } + chosen->GetBBox(pBBox); } +// CSwitch::GetRadius() +// Reconstructed from 5960 retail XBE @ 0x0004f1e0: the choice's radius, else 0. float CSwitch::GetRadius() { - if (m_whichChoice >= 0 && m_whichChoice < m_choice.GetLength()) - return m_choice.GetNode(m_whichChoice)->GetRadius(); - - return 0.0f; + CNode* chosen = Chosen(); + return chosen != NULL ? chosen->GetRadius() : 0.0f; } +// CSwitch::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x0004f210: CNode::Advance, then +// every choice, picked or not. void CSwitch::Advance(float nSeconds) { CNode::Advance(nSeconds); - - for (int i = 0; i < m_choice.GetLength(); i += 1) - m_choice.GetNode(i)->Advance(nSeconds); + for (int n = 0; n < m_choice.GetLength(); n++) + m_choice.GetNode(n)->Advance(nSeconds); } +// CSwitch::SetLight +// Theseus only, like CGroup::SetLight: hand it to the picked choice. void CSwitch::SetLight(int& nLight, D3DCOLORVALUE& ambient) { - if (m_whichChoice >= 0 && m_whichChoice < m_choice.GetLength()) - { - CNode* pNode = m_choice.GetNode(m_whichChoice); - - if (pNode != NULL) - pNode->SetLight(nLight, ambient); - } + CNode* chosen = Chosen(); + if (chosen != NULL) + chosen->SetLight(nLight, ambient); } // ===== CBillboard =========================================================== -// Orients children to face the camera. If axisOfRotation is zero-length, -// does full viewer alignment. Otherwise rotates around the given axis. +// Keeps its children turned toward the camera, either fully or only about +// axisOfRotation. class CBillboard : public CGroup { @@ -1107,15 +1179,24 @@ START_NODE_PROPS(CBillboard, CGroup) NODE_PROP(pt_vec3, CBillboard, axisOfRotation) END_NODE_PROPS() +// CBillboard::CBillboard() +// Reconstructed from 5960 retail XBE @ 0x0004f260 (factory, ctor inlined): +// axisOfRotation = (0, 1, 0); CBillboard::CBillboard() : m_axisOfRotation(0.0f, 1.0f, 0.0f) { } +// CBillboard::~CBillboard() +// Reconstructed from 5960 retail XBE @ 0x0004f320: just the group teardown. CBillboard::~CBillboard() { } +// CBillboard::Render() +// Reconstructed from 5960 retail XBE @ 0x0004f330: +// CalcMatrix(); world->Push(); world->MultMatrixLocal(&matrix); ; +// CGroup::Render(); world->Pop(); void CBillboard::Render() { CalcMatrix(); @@ -1129,76 +1210,62 @@ void CBillboard::Render() TheseusPopWorld(); } +// CBillboard::CalcMatrix() +// Reconstructed from 5960 retail XBE @ 0x0004f3d0 (unnamed helper, only +// called from Render). Inverts the current world matrix and brings the camera +// position into local space as a normalized "eye". With an axis no longer than +// 0.001 it builds a facing frame: up is (inv._14, inv._22, inv._23) normalized, +// x = normalize(up x eye), rows x, up, eye. Otherwise it normalizes the axis, +// takes x = normalize(axis x eye), z = x x axis, and rotates about the axis by +// acos(z.z), negated when x.z > 0. The odd up vector is the binary's; kept. void CBillboard::CalcMatrix() { - D3DXMATRIX world; - D3DXMATRIX world2local; - + D3DXMATRIX world, toLocal; TheseusGetTransform(D3DTS_WORLD, &world); - D3DXMatrixInverse(&world2local, NULL, &world); + D3DXMatrixInverse(&toLocal, NULL, &world); - // Get eye vector in local coordinate space D3DXVECTOR3 eye = theCamera.m_position; - D3DXVec3TransformNormal(&eye, &eye, &world2local); + D3DXVec3TransformNormal(&eye, &eye, &toLocal); D3DXVec3Normalize(&eye, &eye); D3DXVECTOR3 axis = m_axisOfRotation; - float alen = D3DXVec3Length(&axis); + float length = D3DXVec3Length(&axis); - if (alen <= 0.001f) + if (length <= 0.001f) { - // Full viewer alignment - D3DXVECTOR3 up(world2local.m[0][3], world2local.m[1][1], world2local.m[1][2]); + D3DXVECTOR3 up(toLocal._14, toLocal._22, toLocal._23); D3DXVec3Normalize(&up, &up); - D3DXVECTOR3 x; - D3DXVec3Cross(&x, &up, &eye); - D3DXVec3Normalize(&x, &x); - - m_matrix.m[0][0] = x.x; - m_matrix.m[0][1] = x.y; - m_matrix.m[0][2] = x.z; - m_matrix.m[0][3] = 0.0f; - - m_matrix.m[1][0] = up.x; - m_matrix.m[1][1] = up.y; - m_matrix.m[1][2] = up.z; - m_matrix.m[1][3] = 0.0f; - - m_matrix.m[2][0] = eye.x; - m_matrix.m[2][1] = eye.y; - m_matrix.m[2][2] = eye.z; - m_matrix.m[2][3] = 0.0f; - - m_matrix.m[3][0] = 0.0f; - m_matrix.m[3][1] = 0.0f; - m_matrix.m[3][2] = 0.0f; - m_matrix.m[3][3] = 1.0f; + D3DXVECTOR3 side; + D3DXVec3Cross(&side, &up, &eye); + D3DXVec3Normalize(&side, &side); + + m_matrix = D3DXMATRIX( + side.x, side.y, side.z, 0.0f, + up.x, up.y, up.z, 0.0f, + eye.x, eye.y, eye.z, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f); + return; } - else - { - // Axis-constrained rotation - axis *= 1.0f / alen; - D3DXVECTOR3 x; - D3DXVec3Cross(&x, &axis, &eye); - D3DXVec3Normalize(&x, &x); + axis *= 1.0f / length; - D3DXVECTOR3 z; - D3DXVec3Cross(&z, &x, &axis); + D3DXVECTOR3 side, facing; + D3DXVec3Cross(&side, &axis, &eye); + D3DXVec3Normalize(&side, &side); + D3DXVec3Cross(&facing, &side, &axis); - float angle = acosf(z.z); - if (x.z > 0) - angle = -angle; + float angle = acosf(facing.z); + if (side.z > 0.0f) + angle = -angle; - D3DXMatrixRotationAxis(&m_matrix, &axis, angle); - } + D3DXMatrixRotationAxis(&m_matrix, &axis, angle); } // ===== CLayer =============================================================== -// A self-contained rendering layer with its own viewpoint and projection. -// Draws a black fade quad at m_alpha transparency before rendering children. +// A scene drawn through its own viewpoint and projection, over a black wash +// whose strength is 1 - transparency. class CLayer : public CGroup { @@ -1233,226 +1300,201 @@ START_NODE_PROPS(CLayer, CGroup) NODE_PROP(pt_number, CLayer, transparency) END_NODE_PROPS() +// CLayer::CLayer() +// Reconstructed from 5960 retail XBE @ 0x0005a2a0 (factory, ctor inlined): +// viewpoint = navigationInfo = 0; fade = transparency = 0; vb = 0; +// alpha (+0x58) isn't set until transparency is written; same here. CLayer::CLayer() : m_viewpoint(NULL), m_navigationInfo(NULL), m_fade(0.0f), - m_transparency(0.0f) + m_transparency(0.0f), + m_vb(NULL) { - m_vb = NULL; } +// CLayer::~CLayer() +// Reconstructed from 5960 retail XBE @ 0x0005a350: release the viewpoint, +// the navigation info and the wash vertex buffer. CLayer::~CLayer() { if (m_viewpoint != NULL) m_viewpoint->Release(); - if (m_navigationInfo != NULL) m_navigationInfo->Release(); - if (m_vb != NULL) m_vb->Release(); } -struct COLORVERTEX -{ - float dvX, dvY, dvZ; - DWORD color; -}; - +// CLayer::OnSetProperty(pprd, pvValue) +// Reconstructed from 5960 retail XBE @ 0x0005a400. Ghidra decompile: +// if (prd->offset == 0x50) { CLerper::RemoveObject(this); +// if (fade <= 0) alpha = 1 - value; +// else new CLerper(this, &alpha, 1 - value, fade); } +// return 1; bool CLayer::OnSetProperty(const PRD* pprd, const void* pvValue) { if (PTR2INT(pprd->pbOffset) == offsetof(m_transparency)) { + float target = 1.0f - *(const float*)pvValue; CLerper::RemoveObject(this); - if (m_fade > 0.0f) - new CLerper(this, &m_alpha, 1.0f - *(float*)pvValue, m_fade); + if (m_fade <= 0.0f) + m_alpha = target; else - m_alpha = 1.0f - *(float*)pvValue; + new CLerper(this, &m_alpha, target, m_fade); } - - return CGroup::OnSetProperty(pprd, pvValue); + return true; } -void CLayer::Render() +struct WashVertex { - if (m_viewpoint == NULL) - { - TRACE(_T("\001Layer is missing a viewpoint!\n")); - return; - } + float x, y, z; + DWORD color; +}; - if (m_alpha == 0.0f) +// CLayer::Render() +// Reconstructed from 5960 retail XBE @ 0x0005a4a0. Nothing without a +// viewpoint or with alpha 0. Otherwise it draws a screen sized black strip +// (vertex buffer built on first use, colors 0xff000000) under an ortho +// projection with identity world and view, alpha = diffuse * texture factor +// (alpha * 255), blended, no culling, stage 1 off. Then it saves the +// matrices, sets a perspective projection (fov from the viewpoint, near +// avatarSize.x / 2 or 0.1, far visibilityLimit or 1000) and a view built from +// the viewpoint's position and orientation with z flipped, renders the +// children from an identity world, and puts everything back. See +// docs/decomp/SceneGroups.md. Theseus differences, one each: the aspect +// follows the window unless Xbox resolution is emulated; a navigationInfo +// that isn't a NavigationInfo is ignored (the binary tests and goes on). +void CLayer::Render() +{ + if (m_viewpoint == NULL || m_alpha == 0.0f) return; - // Draw the fade quad - { - if (m_vb == NULL) - { - VERIFYHR(TheseusGetD3DDev()->CreateVertexBuffer(4 * sizeof(COLORVERTEX), D3DUSAGE_WRITEONLY, D3DFVF_XYZ | D3DFVF_DIFFUSE, D3DPOOL_MANAGED, &m_vb)); - - COLORVERTEX* verts; - VERIFYHR(m_vb->Lock(0, 4 * sizeof (COLORVERTEX), (BYTE**)&verts, 0)); - - verts[0].dvX = (float)TheseusGetViewWidth() / 2.0f; - verts[0].dvY = -(float)TheseusGetViewHeight() / 2.0f; - verts[0].dvZ = 0.0f; - verts[0].color = D3DCOLOR_COLORVALUE(0.0f, 0.0f, 0.0f, 1.0f); - - verts[1].dvX = -(float)TheseusGetViewWidth() / 2.0f; - verts[1].dvY = -(float)TheseusGetViewHeight() / 2.0f; - verts[1].dvZ = 0.0f; - verts[1].color = D3DCOLOR_COLORVALUE(0.0f, 0.0f, 0.0f, 1.0f); - - verts[2].dvX = (float)TheseusGetViewWidth() / 2.0f; - verts[2].dvY = (float)TheseusGetViewHeight() / 2.0f; - verts[2].dvZ = 0.0f; - verts[2].color = D3DCOLOR_COLORVALUE(0.0f, 0.0f, 0.0f, 1.0f); - - verts[3].dvX = -(float)TheseusGetViewWidth() / 2.0f; - verts[3].dvY = (float)TheseusGetViewHeight() / 2.0f; - verts[3].dvZ = 0.0f; - verts[3].color = D3DCOLOR_COLORVALUE(0.0f, 0.0f, 0.0f, 1.0f); - - VERIFYHR(m_vb->Unlock()); - } - - D3DXMATRIX matProjection, matProjectionSave, matWorldSave, matViewSave; - - TheseusGetTransform(D3DTS_PROJECTION, &matProjectionSave); - TheseusGetTransform(D3DTS_WORLD, &matWorldSave); - TheseusGetTransform(D3DTS_VIEW, &matViewSave); - - D3DXMatrixOrthoLH(&matProjection, TheseusGetViewWidth(), TheseusGetViewHeight(), -10000.0f, 10000.0f); - TheseusSetTransform(D3DTS_PROJECTION, &matProjection); - TheseusSetTransform(D3DTS_WORLD, &g_matIdentity); - TheseusSetTransform(D3DTS_VIEW, &g_matIdentity); - - TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE); - TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); - TheseusSetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TFACTOR); - TheseusSetRenderState(D3DRS_TEXTUREFACTOR, D3DCOLOR_RGBA(0, 0, 0, (BYTE)(m_alpha * 255.0f))); - TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); - TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); - TheseusSetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE); - TheseusSetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 0); - TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); - TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); - TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); - TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); - - VERIFYHR(TheseusGetD3DDev()->SetStreamSource(0, m_vb, sizeof (COLORVERTEX))); - VERIFYHR(TheseusGetD3DDev()->SetVertexShader(D3DFVF_XYZ | D3DFVF_DIFFUSE)); - VERIFYHR(TheseusGetD3DDev()->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2)); - - TheseusSetTransform(D3DTS_PROJECTION, &matProjectionSave); - TheseusSetTransform(D3DTS_WORLD, &matWorldSave); - TheseusSetTransform(D3DTS_VIEW, &matViewSave); - } - - CViewpoint* pViewpoint = (CViewpoint*)m_viewpoint; - CNavigationInfo* pNavigationInfo = (CNavigationInfo*)m_navigationInfo; - - if (pNavigationInfo != NULL && !pNavigationInfo->IsKindOf(NODE_CLASS(CNavigationInfo))) - { - TRACE(_T("\002Layer with bogus NavigationInfo!\n")); - pNavigationInfo = NULL; - } + float viewW = TheseusGetViewWidth(); + float viewH = TheseusGetViewHeight(); - D3DXMATRIX matProjectionSave, matWorldSave, matViewSave; - TheseusGetTransform(D3DTS_PROJECTION, &matProjectionSave); - TheseusGetTransform(D3DTS_WORLD, &matWorldSave); - TheseusGetTransform(D3DTS_VIEW, &matViewSave); - - // Calculate projection matrix + if (m_vb == NULL) { - float nNear = 0.1f; - float nFar = 1000.0f; - float fieldOfView = D3DX_PI / 2.0f; - - if (pNavigationInfo != NULL) - { - nNear = pNavigationInfo->m_avatarSize.x / 2.0f; - if (pNavigationInfo->m_visibilityLimit != 0.0f) - nFar = pNavigationInfo->m_visibilityLimit; - } - - fieldOfView = pViewpoint->m_fieldOfView; + if (FAILED(TheseusGetD3DDev()->CreateVertexBuffer(4 * sizeof(WashVertex), D3DUSAGE_WRITEONLY, + D3DFVF_XYZ | D3DFVF_DIFFUSE, D3DPOOL_MANAGED, &m_vb))) + return; - float aspect; - if (TheseusGetEmulateXboxRes()) - { - aspect = 720.0f/480.0f; - if (TheseusGetStretchWidescreen()) - aspect *= 1.25f; - } - else + WashVertex* v; + if (SUCCEEDED(m_vb->Lock(0, 4 * sizeof(WashVertex), (BYTE**)&v, 0))) { - // Real window aspect, so the scene fills the display instead of - // being squeezed into a 4:3 box. - const float vw = TheseusGetViewWidth(); - const float vh = TheseusGetViewHeight(); - aspect = (vh > 0.0f) ? (vw / vh) : (720.0f/480.0f); + const float corner[4][2] = { { 0.5f, -0.5f }, { -0.5f, -0.5f }, { 0.5f, 0.5f }, { -0.5f, 0.5f } }; + for (int n = 0; n < 4; n++) + { + v[n].x = viewW * corner[n][0]; + v[n].y = viewH * corner[n][1]; + v[n].z = 0.0f; + v[n].color = D3DCOLOR_ARGB(255, 0, 0, 0); + } + m_vb->Unlock(); } - - D3DXMatrixPerspectiveFovLH(&g_matProjection, fieldOfView, aspect, nNear, nFar); - TheseusSetTransform(D3DTS_PROJECTION, &g_matProjection); } - // Calculate view matrix - if (pViewpoint != NULL) - { - D3DXVECTOR3 position = pViewpoint->m_position; - position.z = -position.z; - - D3DXQUATERNION orientation; - D3DXQuaternionRotationAxis(&orientation, (D3DXVECTOR3*)&pViewpoint->m_orientation, -pViewpoint->m_orientation.w); - - D3DXMatrixAffineTransformation(&g_matView, 1.0f, NULL, &orientation, &position); - D3DXMatrixInverse(&g_matView, NULL, &g_matView); + D3DXMATRIX outerProjection, outerWorld, outerView; + TheseusGetTransform(D3DTS_PROJECTION, &outerProjection); + TheseusGetTransform(D3DTS_WORLD, &outerWorld); + TheseusGetTransform(D3DTS_VIEW, &outerView); - g_matView._31 = -g_matView._31; - g_matView._32 = -g_matView._32; - g_matView._33 = -g_matView._33; - g_matView._34 = -g_matView._34; - TheseusSetTransform(D3DTS_VIEW, &g_matView); - } + D3DXMATRIX ortho; + D3DXMatrixOrthoLH(&ortho, viewW, viewH, -10000.0f, 10000.0f); + TheseusSetTransform(D3DTS_PROJECTION, &ortho); + TheseusSetTransform(D3DTS_WORLD, &g_matIdentity); + TheseusSetTransform(D3DTS_VIEW, &g_matIdentity); - // Render children + TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE); + TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); + TheseusSetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TFACTOR); + TheseusSetRenderState(D3DRS_TEXTUREFACTOR, (DWORD)(m_alpha * 255.0f) << 24); + TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); + TheseusSetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE); + TheseusSetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 0); + TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); + TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); + TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); + TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); + + TheseusGetD3DDev()->SetStreamSource(0, m_vb, sizeof(WashVertex)); + TheseusGetD3DDev()->SetVertexShader(D3DFVF_XYZ | D3DFVF_DIFFUSE); + TheseusGetD3DDev()->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); + + TheseusSetTransform(D3DTS_PROJECTION, &outerProjection); + TheseusSetTransform(D3DTS_WORLD, &outerWorld); + TheseusSetTransform(D3DTS_VIEW, &outerView); + + CViewpoint* viewpoint = (CViewpoint*)m_viewpoint; + CNavigationInfo* nav = (CNavigationInfo*)m_navigationInfo; + if (nav != NULL && !nav->IsKindOf(NODE_CLASS(CNavigationInfo))) + nav = NULL; + + D3DXMATRIX savedProjection, savedWorld, savedView; + TheseusGetTransform(D3DTS_PROJECTION, &savedProjection); + TheseusGetTransform(D3DTS_WORLD, &savedWorld); + TheseusGetTransform(D3DTS_VIEW, &savedView); + + float zNear = 0.1f; + float zFar = 1000.0f; + if (nav != NULL) { - TheseusPushWorld(); - TheseusIdentityWorld(); - TheseusUpdateWorld(); - CGroup::Render(); - TheseusPopWorld(); + zNear = nav->m_avatarSize.x * 0.5f; + if (nav->m_visibilityLimit != 0.0f) + zFar = nav->m_visibilityLimit; } - // Restore transforms - { - g_matProjection = matProjectionSave; - g_matView = matViewSave; + // 1.5, or 1.875 stretched, is the binary's 720x480; off emulation Theseus + // uses the real window shape. + float aspect; + if (TheseusGetEmulateXboxRes()) + aspect = TheseusGetStretchWidescreen() ? 1.5f * 1.25f : 1.5f; + else + aspect = viewH > 0.0f ? viewW / viewH : 1.5f; + + D3DXMatrixPerspectiveFovLH(&g_matProjection, viewpoint->m_fieldOfView, aspect, zNear, zFar); + TheseusSetTransform(D3DTS_PROJECTION, &g_matProjection); + + D3DXVECTOR3 eyeAt(viewpoint->m_position.x, viewpoint->m_position.y, -viewpoint->m_position.z); + D3DXQUATERNION facing; + D3DXQuaternionRotationAxis(&facing, (const D3DXVECTOR3*)&viewpoint->m_orientation, -viewpoint->m_orientation.w); + D3DXMatrixAffineTransformation(&g_matView, 1.0f, NULL, &facing, &eyeAt); + D3DXMatrixInverse(&g_matView, NULL, &g_matView); + g_matView._31 = -g_matView._31; + g_matView._32 = -g_matView._32; + g_matView._33 = -g_matView._33; + g_matView._34 = -g_matView._34; + TheseusSetTransform(D3DTS_VIEW, &g_matView); - TheseusSetTransform(D3DTS_PROJECTION, &matProjectionSave); - TheseusSetTransform(D3DTS_WORLD, &matWorldSave); - TheseusSetTransform(D3DTS_VIEW, &matViewSave); - } + TheseusPushWorld(); + TheseusIdentityWorld(); + TheseusUpdateWorld(); + CGroup::Render(); + TheseusPopWorld(); + + g_matProjection = savedProjection; + g_matView = savedView; + TheseusSetTransform(D3DTS_PROJECTION, &savedProjection); + TheseusSetTransform(D3DTS_WORLD, &savedWorld); + TheseusSetTransform(D3DTS_VIEW, &savedView); } +// CLayer::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x0005aaf0: CGroup::Advance, then the +// navigation info, then the viewpoint. void CLayer::Advance(float nSeconds) { CGroup::Advance(nSeconds); - if (m_navigationInfo != NULL) m_navigationInfo->Advance(nSeconds); - if (m_viewpoint != NULL) m_viewpoint->Advance(nSeconds); } // ===== CBackground ========================================================== -// Renders a backdrop texture behind the scene. Binds itself as the active -// background on creation if none exists. +// The backdrop behind everything. The first one made becomes the bound one. IMPLEMENT_NODE("Background", CBackground, CNode) @@ -1462,29 +1504,44 @@ START_NODE_PROPS(CBackground, CNode) NODE_PROP(pt_boolean, CBackground, isBound) END_NODE_PROPS() +// CBackground::CBackground() +// Reconstructed from 5960 retail XBE @ 0x0004fc90. Ghidra decompile: +// skyColor = 0; backdrop = 0; isBound = 0; vb = 0; (+ the fade fields below) +// if (g_background == 0) { g_background = this; isBound = 1; } +// 5960's Background also has fade, alpha, SetAlpha and DisappearAfter; Theseus +// doesn't carry them. CBackground::CBackground() : - m_skyColor(0.0f, 0.0f, 0.0f), m_backdrop(NULL), - m_isBound(false) + m_skyColor(0.0f, 0.0f, 0.0f), + m_isBound(false), + m_vb(NULL) { - m_vb = NULL; - if (TheseusGetBackground() == NULL) - Bind(); + { + TheseusSetBackground(this); + m_isBound = true; + } } +// CBackground::~CBackground() +// Reconstructed from 5960 retail XBE @ 0x0004fd30: release the vertex buffer +// and the backdrop, and stop being the bound background. CBackground::~CBackground() { if (m_vb != NULL) m_vb->Release(); - if (m_backdrop != NULL) m_backdrop->Release(); - if (TheseusGetBackground() == this) TheseusSetBackground(NULL); } +// CBackground::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x0004fe30. CNode::Advance, the +// backdrop's Advance, then on first use a 4 vertex D3DVERTEX strip at +// z = -10000 covering the screen, uv (1,1) (0,1) (1,0) (0,0). The normals +// hold copies of the corner positions as the binary writes them (nz = 1). +// The binary steps its alpha fade first; Theseus has none. void CBackground::Advance(float nSeconds) { CNode::Advance(nSeconds); @@ -1492,80 +1549,77 @@ void CBackground::Advance(float nSeconds) if (m_backdrop != NULL) m_backdrop->Advance(nSeconds); - if (m_vb == NULL) + if (m_vb != NULL) + return; + + if (FAILED(TheseusGetD3DDev()->CreateVertexBuffer(4 * sizeof(D3DVERTEX), D3DUSAGE_WRITEONLY, + D3DFVF_VERTEX, D3DPOOL_MANAGED, &m_vb))) + return; + + D3DVERTEX* v; + if (FAILED(m_vb->Lock(0, 4 * sizeof(D3DVERTEX), (BYTE**)&v, 0))) + return; + + float halfW = TheseusGetViewWidth() * 0.5f; + float halfH = TheseusGetViewHeight() * 0.5f; + + // position x, y; normal x, y; uv + const float corner[4][6] = { + { halfW, -halfH, halfW, -halfH, 1.0f, 1.0f }, + { -halfW, -halfH, halfW, halfH, 0.0f, 1.0f }, + { halfW, halfH, halfW, halfH, 1.0f, 0.0f }, + { -halfW, halfH, -halfW, halfH, 0.0f, 0.0f }, + }; + for (int n = 0; n < 4; n++) { - VERIFYHR(TheseusGetD3DDev()->CreateVertexBuffer(4 * sizeof(D3DVERTEX), D3DUSAGE_WRITEONLY, D3DFVF_VERTEX, D3DPOOL_MANAGED, &m_vb)); - - D3DVERTEX* verts; - VERIFYHR(m_vb->Lock(0, 4 * sizeof (D3DVERTEX), (BYTE**)&verts, 0)); - - verts[0].dvX = (float)TheseusGetViewWidth() / 2.0f; - verts[0].dvY = -(float)TheseusGetViewHeight() / 2.0f; - verts[0].dvZ = -10000.0f; - verts[0].dvNX = (float)TheseusGetViewWidth() / 2.0f; - verts[0].dvNY = -(float)TheseusGetViewHeight() / 2.0f; - verts[0].dvNZ = 1.0f; - verts[0].dvTU = 1.0f; - verts[0].dvTV = 1.0f; - - verts[1].dvX = -(float)TheseusGetViewWidth() / 2.0f; - verts[1].dvY = -(float)TheseusGetViewHeight() / 2.0f; - verts[1].dvZ = -10000.0f; - verts[1].dvNX = (float)TheseusGetViewWidth() / 2.0f; - verts[1].dvNY = (float)TheseusGetViewHeight() / 2.0f; - verts[1].dvNZ = 1.0f; - verts[1].dvTU = 0.0f; - verts[1].dvTV = 1.0f; - - verts[2].dvX = (float)TheseusGetViewWidth() / 2.0f; - verts[2].dvY = (float)TheseusGetViewHeight() / 2.0f; - verts[2].dvZ = -10000.0f; - verts[2].dvNX = (float)TheseusGetViewWidth() / 2.0f; - verts[2].dvNY = (float)TheseusGetViewHeight() / 2.0f; - verts[2].dvNZ = 1.0f; - verts[2].dvTU = 1.0f; - verts[2].dvTV = 0.0f; - - verts[3].dvX = -(float)TheseusGetViewWidth() / 2.0f; - verts[3].dvY = (float)TheseusGetViewHeight() / 2.0f; - verts[3].dvZ = -10000.0f; - verts[3].dvNX = -(float)TheseusGetViewWidth() / 2.0f; - verts[3].dvNY = (float)TheseusGetViewHeight() / 2.0f; - verts[3].dvNZ = 1.0f; - verts[3].dvTU = 0.0f; - verts[3].dvTV = 0.0f; - - VERIFYHR(m_vb->Unlock()); + v[n].dvX = corner[n][0]; + v[n].dvY = corner[n][1]; + v[n].dvZ = -10000.0f; + v[n].dvNX = corner[n][2]; + v[n].dvNY = corner[n][3]; + v[n].dvNZ = 1.0f; + v[n].dvTU = corner[n][4]; + v[n].dvTV = corner[n][5]; } + + m_vb->Unlock(); } +// CBackground::RenderBackdrop() +// Reconstructed from 5960 retail XBE @ 0x00050110. Takes the backdrop's +// texture (nothing to do without one), binds it to stage 0 and clears stage +// 1, saves projection, world and view, sets the ortho screen projection with +// identity world and view, and draws the strip from Advance with lighting, +// z writes and backface culling (CCW) saved, set and restored around it. +// Color comes from the texture. The binary's alpha also comes from a texture +// factor driven by Background's fade, with blending on; Theseus's Background +// has no fade, so it takes alpha from the texture and draws with blending off. +// The app only calls this when there's a backdrop. void CBackground::RenderBackdrop() { - ASSERT(m_backdrop != NULL); - - LPDIRECT3DTEXTURE8 pTexture = m_backdrop->GetTextureSurface(); - if (pTexture == NULL) + LPDIRECT3DTEXTURE8 tex = m_backdrop->GetTextureSurface(); + if (tex == NULL) return; - TheseusSetTexture(0, pTexture); + TheseusSetTexture(0, tex); TheseusSetTexture(1, NULL); - D3DXMATRIX matProjection, matProjectionSave, matWorldSave, matViewSave; + D3DXMATRIX outerProjection, outerWorld, outerView; + TheseusGetTransform(D3DTS_PROJECTION, &outerProjection); + TheseusGetTransform(D3DTS_WORLD, &outerWorld); + TheseusGetTransform(D3DTS_VIEW, &outerView); - TheseusGetTransform(D3DTS_PROJECTION, &matProjectionSave); - TheseusGetTransform(D3DTS_WORLD, &matWorldSave); - TheseusGetTransform(D3DTS_VIEW, &matViewSave); - - D3DXMatrixOrthoLH(&matProjection, TheseusGetViewWidth(), TheseusGetViewHeight(), -10000.0f, 10000.0f); - TheseusSetTransform(D3DTS_PROJECTION, &matProjection); + D3DXMATRIX ortho; + D3DXMatrixOrthoLH(&ortho, TheseusGetViewWidth(), TheseusGetViewHeight(), -10000.0f, 10000.0f); + TheseusSetTransform(D3DTS_PROJECTION, &ortho); TheseusSetTransform(D3DTS_WORLD, &g_matIdentity); TheseusSetTransform(D3DTS_VIEW, &g_matIdentity); - DWORD dwLighting, dwAlphaBlendEnable, dwCullMode, dwZWriteEnable; - TheseusGetRenderState(D3DRS_LIGHTING, &dwLighting); - TheseusGetRenderState(D3DRS_ALPHABLENDENABLE, &dwAlphaBlendEnable); - TheseusGetRenderState(D3DRS_CULLMODE, &dwCullMode); - TheseusGetRenderState(D3DRS_ZWRITEENABLE, &dwZWriteEnable); + DWORD lighting, blend, cull, zwrite; + TheseusGetRenderState(D3DRS_LIGHTING, &lighting); + TheseusGetRenderState(D3DRS_ALPHABLENDENABLE, &blend); + TheseusGetRenderState(D3DRS_CULLMODE, &cull); + TheseusGetRenderState(D3DRS_ZWRITEENABLE, &zwrite); TheseusSetRenderState(D3DRS_LIGHTING, FALSE); TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); @@ -1579,44 +1633,50 @@ void CBackground::RenderBackdrop() TheseusSetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE); TheseusSetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 0); - VERIFYHR(TheseusGetD3DDev()->SetStreamSource(0, m_vb, sizeof (D3DVERTEX))); - VERIFYHR(TheseusGetD3DDev()->SetVertexShader(D3DFVF_VERTEX)); - VERIFYHR(TheseusGetD3DDev()->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2)); + TheseusGetD3DDev()->SetStreamSource(0, m_vb, sizeof(D3DVERTEX)); + TheseusGetD3DDev()->SetVertexShader(D3DFVF_VERTEX); + TheseusGetD3DDev()->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); - TheseusSetRenderState(D3DRS_LIGHTING, dwLighting); - TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, dwAlphaBlendEnable); - TheseusSetRenderState(D3DRS_CULLMODE, dwCullMode); - TheseusSetRenderState(D3DRS_ZWRITEENABLE, dwZWriteEnable); + TheseusSetRenderState(D3DRS_LIGHTING, lighting); + TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, blend); + TheseusSetRenderState(D3DRS_CULLMODE, cull); + TheseusSetRenderState(D3DRS_ZWRITEENABLE, zwrite); - TheseusSetTransform(D3DTS_PROJECTION, &matProjectionSave); - TheseusSetTransform(D3DTS_WORLD, &matWorldSave); - TheseusSetTransform(D3DTS_VIEW, &matViewSave); + TheseusSetTransform(D3DTS_PROJECTION, &outerProjection); + TheseusSetTransform(D3DTS_WORLD, &outerWorld); + TheseusSetTransform(D3DTS_VIEW, &outerView); } +// CBackground::Bind() +// Inlined into the ctor and OnSetProperty in the binary: unbind whoever is +// bound, then bind this one. void CBackground::Bind() { - if (TheseusGetBackground() != NULL) - TheseusGetBackground()->m_isBound = false; - + CBackground* bound = TheseusGetBackground(); + if (bound != NULL) + bound->m_isBound = false; TheseusSetBackground(this); m_isBound = true; } +// CBackground::OnSetProperty(pprd, pvValue) +// Reconstructed from 5960 retail XBE @ 0x00050450. Ghidra decompile: +// if (prd->offset == 0x2c) { if (*value) { Bind inlined } } +// else if (prd->offset == 0x48) { SetAlpha inlined; return 0; } +// return 1; +// Setting isBound true binds; false does nothing. Theseus has no alpha. bool CBackground::OnSetProperty(const PRD* pprd, const void* pvValue) { - if (PTR2INT(pprd->pbOffset) == offsetof(m_isBound)) - { - if (*(bool*)pvValue) - Bind(); - } - + if (PTR2INT(pprd->pbOffset) == offsetof(m_isBound) && *(const bool*)pvValue) + Bind(); return true; } // ===== CLevel =============================================================== -// A top-level scene area. Manages transitions between levels with fade, -// XIP archive loading, tunnel camera paths, and joystick rebinding. +// One place in the dashboard (main menu, memory, music, settings...). Going +// to a level cross fades from the current one over a second, through the +// tunnel and camera path between them. CLevel* g_pCurLevel; CLevel* g_pFromLevel; @@ -1643,117 +1703,135 @@ START_NODE_FUN(CLevel, CGroup) END_NODE_FUN() #undef _FND_CLASS +// CLevel::CLevel() +// Reconstructed from 5960 retail XBE @ 0x0005cc20. Ghidra decompile: +// g_timeToNextLevel = 0.0; +// control = tunnel = path = shell = archive = 0; unloadable = 1; fade = 1; +// visible = 0; xip = 0; arrive = 0; +// Every new level resets the shared transition clock. CLevel::CLevel() : - m_tunnel(NULL), m_control(NULL), + m_tunnel(NULL), m_path(NULL), m_shell(NULL), m_archive(NULL), + m_unloadable(true), m_fade(true), - m_unloadable(true) + m_xipFile(NULL), + m_bArrive(false) { + g_timeToNextLevel = 0.0; m_visible = false; - m_xipFile = NULL; - g_timeToNextLevel = 0.0f; - m_bArrive = false; } +// CLevel::~CLevel() +// Reconstructed from 5960 retail XBE @ 0x0005ccd0: drop out of the current, +// from and tunnel globals, release tunnel, control, path and shell, free the +// archive name. CLevel::~CLevel() { - if (this == g_pCurLevel) + if (g_pCurLevel == this) g_pCurLevel = NULL; - - if (this == g_pFromLevel) + if (g_pFromLevel == this) g_pFromLevel = NULL; - - if (m_tunnel == g_pCurTunnel) + if (g_pCurTunnel == m_tunnel) g_pCurTunnel = NULL; if (m_tunnel != NULL) m_tunnel->Release(); - if (m_control != NULL) m_control->Release(); - if (m_path != NULL) m_path->Release(); - if (m_shell != NULL) m_shell->Release(); delete [] m_archive; } +// CLevel::Render() +// Reconstructed from 5960 retail XBE @ 0x0005cd80. Ghidra decompile: +// a = 1; +// if (fade) { t = clamp((g_timeToNextLevel - now) * 1.3333334, 0, 1); +// if (this == g_pCurLevel) a = 1 - t; else if (this == g_pFromLevel) a = t; +// if (a == 0) return; } // only reached for cur or from +// effectAlpha *= a; if (shell) shell->Render(); +// if (this == g_pCurLevel) g_motionBlur = min(a * 1.3, 1); +// CGroup::Render(); effectAlpha restored; +// The 1.3333334 is 1 / 0.75: the cross fade runs over the last 0.75 s. void CLevel::Render() { - float alpha = 1.0f; + float a = 1.0f; if (m_fade) { - float t = (float) (g_timeToNextLevel - TheseusGetNow()) / 0.75f; + float t = (float)(g_timeToNextLevel - TheseusGetNow()) / 0.75f; if (t > 1.0f) t = 1.0f; else if (t < 0.0f) t = 0.0f; - if (g_pCurLevel == this) - alpha = 1.0f - t; - else if (g_pFromLevel == this) - alpha = t; + if (this == g_pCurLevel) + a = 1.0f - t; + else if (this == g_pFromLevel) + a = t; - if (alpha == 0.0f) + if (a == 0.0f) return; } - float nEffectAlphaSave = g_nEffectAlpha; - g_nEffectAlpha *= alpha; + float outerAlpha = g_nEffectAlpha; + g_nEffectAlpha = outerAlpha * a; if (m_shell != NULL) m_shell->Render(); - if (g_pCurLevel == this) - { - float blurAlpha = alpha * 1.3f; - if (blurAlpha > 1.0f) - blurAlpha = 1.0f; - g_transitionMotionBlur = blurAlpha; - } + if (this == g_pCurLevel) + g_transitionMotionBlur = a * 1.3f > 1.0f ? 1.0f : a * 1.3f; CGroup::Render(); - g_nEffectAlpha = nEffectAlphaSave; -} - + g_nEffectAlpha = outerAlpha; +} + +// CLevel::Activate() +// Reconstructed from 5960 retail XBE @ 0x0005ce90. Ghidra decompile: +// g_bLevelTransition = 1; +// if (archive && !xip) xip = LoadXIP(archive); +// if (g_pFromLevel) { from->Deactivate() inlined; g_timeToNextLevel = 0; g_pFromLevel = 0; } +// else if (!g_pCurLevel || g_timeToNextLevel == 0) goto start; +// if (g_pCurLevel) { cur->visible = 0; +// if (cur->xip && cur->unloadable && !cur->xip->locked) cur->xip->DeleteMeshBuffers(); } +// g_pCurLevel = 0; +// if (g_pCurTunnel) { g_pCurTunnel->visible = 0; g_pCurTunnel = 0; } +// start: +// BindJoystick(0) inlined; g_timeToNextLevel = now + 1.0; +// g_pFromLevel = g_pCurLevel; visible = 1; g_pCurLevel = this; +// A transition still running is cut short: both its ends are hidden and the +// new one fades in from nothing. void CLevel::Activate() { g_bLevelTransition = true; - TRACE(_T("Leaving level 0x%08x\n"), g_pCurLevel); - TRACE(_T("Going to level 0x%08x\n"), this); - if (m_archive != NULL && m_xipFile == NULL) - { m_xipFile = LoadXIP(m_archive); - } - if (g_pFromLevel != NULL || g_pCurLevel != NULL && g_timeToNextLevel != 0.0f) + bool midTransition = g_pFromLevel != NULL || (g_pCurLevel != NULL && g_timeToNextLevel != 0.0); + if (midTransition) { - TRACE(_T("\taborting goto level 0x%08x\n"), g_pFromLevel); - if (g_pFromLevel != NULL) { g_pFromLevel->Deactivate(); g_pFromLevel = NULL; - g_timeToNextLevel = 0.0f; } if (g_pCurLevel != NULL) { - g_pCurLevel->m_visible = false; - if (g_pCurLevel->m_xipFile && g_pCurLevel->m_unloadable && !g_pCurLevel->m_xipFile->m_locked) - g_pCurLevel->m_xipFile->DeleteMeshBuffers(); + CLevel* cur = g_pCurLevel; + cur->m_visible = false; + if (cur->m_xipFile != NULL && cur->m_unloadable && !cur->m_xipFile->m_locked) + cur->m_xipFile->DeleteMeshBuffers(); g_pCurLevel = NULL; - g_timeToNextLevel = 0.0f; } if (g_pCurTunnel != NULL) @@ -1763,18 +1841,21 @@ void CLevel::Activate() } } - ASSERT(g_pFromLevel == NULL); - - g_timeToNextLevel = TheseusGetNow() + 1.0f; + BindJoystick(NULL); + g_timeToNextLevel = TheseusGetNow() + 1.0; g_pFromLevel = g_pCurLevel; - m_visible = true; - - BindJoystick(NULL); - g_pCurLevel = this; } +// CLevel::GoTo() [script fn 'GoTo', FND 0x00188478, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005cf90. Ghidra decompile: +// if (this == g_pCurLevel) return; +// Activate(); g_pCurTunnel = tunnel; if (tunnel) tunnel->visible = 1; +// if (path is a Viewpoint) BindViewpoint(path); +// else if (path is a CameraPath) { active = 1; start = now; backward = 0; } +// CallFunction(this, "OnActivate"); +// The CameraPath start is its Activate(false), inlined. void CLevel::GoTo() { if (this == g_pCurLevel) @@ -1783,7 +1864,6 @@ void CLevel::GoTo() Activate(); g_pCurTunnel = m_tunnel; - if (g_pCurTunnel != NULL) g_pCurTunnel->m_visible = true; @@ -1798,6 +1878,15 @@ void CLevel::GoTo() CallFunction(this, _T("OnActivate")); } +// CLevel::GoBackTo() [script fn 'GoBackTo', FND 0x00188490, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005d040. Ghidra decompile: +// if (this == g_pCurLevel) return; +// Activate(); g_pCurTunnel = from ? from->tunnel : 0; +// if (g_pCurTunnel) g_pCurTunnel->visible = 1; +// if (from && from->path is a CameraPath) { path: active = 1; start = now; backward = 1; } +// else if (path is a Viewpoint) BindViewpoint(path); +// CallFunction(this, "OnActivate"); +// Going back runs the level we're leaving's tunnel and path in reverse. void CLevel::GoBackTo() { if (this == g_pCurLevel) @@ -1805,52 +1894,52 @@ void CLevel::GoBackTo() Activate(); - g_pCurTunnel = (g_pFromLevel == NULL) ? NULL : g_pFromLevel->m_tunnel; - + CLevel* from = g_pFromLevel; + g_pCurTunnel = from != NULL ? from->m_tunnel : NULL; if (g_pCurTunnel != NULL) g_pCurTunnel->m_visible = true; - if (g_pFromLevel != NULL && g_pFromLevel->m_path != NULL && g_pFromLevel->m_path->IsKindOf(NODE_CLASS(CCameraPath))) - ((CCameraPath*)g_pFromLevel->m_path)->Activate(true); + if (from != NULL && from->m_path != NULL && from->m_path->IsKindOf(NODE_CLASS(CCameraPath))) + ((CCameraPath*)from->m_path)->Activate(true); else if (m_path != NULL && m_path->IsKindOf(NODE_CLASS(CViewpoint))) BindViewpoint(m_path); CallFunction(this, _T("OnActivate")); } +// CLevel::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x0005d120. Ghidra decompile: +// CGroup::Advance(dt); tunnel, path, control, shell ->Advance(dt) when set; +// if (this == g_pCurLevel && g_timeToNextLevel != 0 && g_timeToNextLevel <= now) { +// g_timeToNextLevel = 0; if (g_pCurTunnel) g_pCurTunnel->visible = 0; +// if (g_pFromLevel) { g_pFromLevel->Deactivate(); g_pFromLevel = 0; } +// arrive = 1; } +// if (arrive && (!xip || xip->loaded)) { arrive = 0; BindJoystick(control); +// CallFunction(this, "OnArrival"); g_bLevelTransition = 0; } +// Arrival waits for the level's XIP before handing over the controller. void CLevel::Advance(float nSeconds) { CGroup::Advance(nSeconds); if (m_tunnel != NULL) m_tunnel->Advance(nSeconds); - if (m_path != NULL) m_path->Advance(nSeconds); - if (m_control != NULL) m_control->Advance(nSeconds); - if (m_shell != NULL) m_shell->Advance(nSeconds); - if (g_pCurLevel == this && g_timeToNextLevel != 0.0f && TheseusGetNow() >= g_timeToNextLevel) + if (this == g_pCurLevel && g_timeToNextLevel != 0.0 && g_timeToNextLevel <= TheseusGetNow()) { - TRACE(_T("Arrived at level 0x%08x\n"), this); - - ASSERT(g_pCurLevel == this); - - g_timeToNextLevel = 0.0f; - + g_timeToNextLevel = 0.0; if (g_pCurTunnel != NULL) g_pCurTunnel->m_visible = false; - if (g_pFromLevel != NULL) { g_pFromLevel->Deactivate(); g_pFromLevel = NULL; } - m_bArrive = true; } @@ -1863,10 +1952,12 @@ void CLevel::Advance(float nSeconds) } } +// CLevel::Deactivate() +// Reconstructed from 5960 retail XBE @ 0x0005d200: +// visible = 0; CallFunction(this, "OnDeactivate"); +// if (unloadable && !xip->locked) xip->DeleteMeshBuffers(); void CLevel::Deactivate() { - ASSERT(this == g_pFromLevel); - m_visible = false; CallFunction(this, _T("OnDeactivate")); From afb053dd23e68dabcbbefd6115eb87c08eeb6570 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:11:07 -0500 Subject: [PATCH 063/167] Wrote up the 5960 scene group nodes in the SceneGroups decomp doc Added the object layouts, a function map for Group.cpp, Background, Layer and Level, walkthroughs of the transforms, inline loading, layout and level transitions, and the quirks. Also fixed the old note claiming a SetLight slot; the retail vtables don't have one. --- docs/decomp/SceneGroups.md | 325 ++++++++++++++++++++++++++++++++++++- 1 file changed, 324 insertions(+), 1 deletion(-) diff --git a/docs/decomp/SceneGroups.md b/docs/decomp/SceneGroups.md index d134f34..5dc54ce 100644 --- a/docs/decomp/SceneGroups.md +++ b/docs/decomp/SceneGroups.md @@ -80,6 +80,8 @@ Level transition state found in .data section: ### CBackground - `skyColor` (pt_vec3), `backdrop` (pt_node), `isBound` (pt_boolean) +- `fade` (pt_number), `alpha` (pt_number) +- Functions (FND `0x00187f10`): SetAlpha, DisappearAfter ### CLevel - `control` (pt_node), `tunnel` (pt_node), `path` (pt_node), `shell` (pt_node) @@ -95,4 +97,325 @@ Level transition state found in .data section: the retail binary does not spawn a thread for this. - CBackground has no Render() override in the retail binary. Only RenderBackdrop() is used. -- SetLight() methods are present in all retail builds. +- The node vtables have no light slot. Slot 19 (`+0x4c`) of both the + Group and Switch vtables is the shared stub at `0x00041da0`; the slots + around it are GetRadius (18), RenderDynamicTexture (20), GetGroundHeight + (21) and GetTextureSurface (22). + +## Group.cpp, Background, Layer and Level in the 5960 retail XBE + +| Range | Contents | +|---|---| +| `0x0004d3b0` to `0x0004f3d0` | Group, Transform, Inline, Spinner, Waver, Layout, Switch, Billboard | +| `0x0004f3d0` | Billboard's matrix helper (only called from its Render) | +| `0x0004fc40` to `0x000504c0` | Background | +| `0x0005a290` to `0x0005ab30` | Layer | +| `0x0005cbd0` to `0x0005d230` | Level | + +Every world matrix change in this code is the same three steps: a call on the matrix stack at +`0x0017e7fc` (Push `+0x10`, Pop `+0x0c`, LoadIdentity `+0x14`, MultMatrixLocal `+0x20`, +RotateAxisLocal `+0x28`, TranslateLocal `+0x40`), a 16 dword copy of the stack top (`+0x44`) +into the device's world matrix with dirty bit `0x200`, then `0x0005ca00`, which recomputes and +uploads the vertex shader constants. `0x0002c490` is that upload on its own and `0x0002c4d0` is +Pop plus upload. The effect alpha that nodes multiply into is the float at `0x00175ca0`, the +frame clock is the double at `0x0017e808`. + +### Object layouts + +``` +CGroup 0x44 +0x1c children (count, capacity, array) +0x28 bboxCenter + +0x34 bboxSize +0x40 bboxSpecified +0x41 bboxDirty +CTransform 0x1a8 +0x44 rotation (axis, angle) +0x54 center +0x60 scaleOrientation + +0x70 scale +0x7c rotation quaternion +0x8c translation + +0xb4/+0xc0/+0xd0 scale/rotation/translation start + +0xf8/+0x104/+0x114 scale/rotation/translation end + +0x120 alpha +0x124 alpha start +0x128 alpha end + +0x12c moving +0x130 dirty (dword) +0x134 matrix +0x174 fade + +0x178, +0x17c two floats zeroed and never read + +0x180 scale clock +0x188 rotation clock +0x190 translation clock + +0x198 alpha clock +0x1a0 disappear time (all doubles) +CInline 0x2e0 +0x44 url +0x48 preload +0x49 class loaded +0x4a fadeInDelayLoad + +0x4b dirty +0x4c loading +0x4d first load +0x50 XIP + +0x58 time of load (double) +0x60 CClass +0xd0 thread handle + +0xd4 absolute url (260 wide chars) +CSpinner 0x58 +0x44 rpm +0x48 axis +0x54 angle +CWaver 0x5c +0x58 field +CLayout 0x54 +0x44 direction +0x50 spacing +CSwitch 0x2c +0x1c whichChoice +0x20 choice (count, capacity, array) +CBillboard 0x90 +0x44 axisOfRotation +0x50 matrix +CLayer 0x5c +0x44 viewpoint +0x48 navigationInfo +0x4c fade +0x50 transparency + +0x54 vertex buffer +0x58 alpha (not set by the constructor) +CBackground 0x60 +0x1c backdrop +0x20 skyColor +0x2c isBound +0x30 fade + +0x38 alpha clock (double) +0x40 alpha start +0x44 alpha end + +0x48 alpha +0x50 disappear time (double) +0x58 vertex buffer +CLevel 0x68 +0x44 control +0x48 tunnel +0x4c path +0x50 shell +0x54 archive + +0x58 unloadable +0x59 fade +0x5c a float never touched + +0x60 XIP +0x64 arrive +``` + +Defaults: Group bbox size `-1.0` on all three axes with specified and dirty set; Transform +rotation and scaleOrientation `(0,0,1,0)`, scale `(1,1,1)`, alpha 1, dirty 1; Inline +fadeInDelayLoad 1, dirty 1; Spinner rpm 1, axis `(0,1,0)`; Waver field `pi/4`; Layout +direction `(1,0,0)`, spacing 1; Switch whichChoice -1; Billboard axis `(0,1,0)`; Background +alpha 1; Level unloadable 1, fade 1, visible 0. Level's constructor also zeroes the shared +transition clock. Background's constructor makes the new node the bound background (global +`0x0017e824`) when there isn't one yet. + +Level globals: current level `0x0017c8f0`, level being left `0x0017c8ec`, current tunnel +`0x0017c8e8`, transition end time `0x0017c8e0` (double), transition flag `0x0017c8d8`. +The current Level writes the motion blur strength at `0x00179f54`. + +### Function map + +| Address | Function | What it does | +|---|---|---| +| `0x0004d3d0` | Group factory | `new` 0x44, constructor inlined | +| `0x0004d470` | CGroup::~CGroup | ReleaseAll on the children, then CNode teardown | +| `0x0004d4d0` | CGroup::GetBBox | See below | +| `0x0004d770` | CGroup::GetRadius | Largest child radius, starting from 0 | +| `0x0004d7e0` | CGroup::Render | Render each child whose visible byte is set | +| `0x0004d820` | CGroup::Advance | CNode::Advance, then every child | +| `0x0004d860` | CGroup::RenderDynamicTexture | Every non-null child | +| `0x0004d8a0` | CGroup::GetTextureSurface | First non-null child texture | +| `0x0004d910` | CTransform::CTransform | Defaults above | +| `0x0004da40` | CTransform::OnLoad | CNode::OnLoad, then the quaternion from the axis and angle | +| `0x0004da60` | CTransform::OnSetProperty | Rotation rebuilds the quaternion; alpha goes through SetAlpha and returns 0 | +| `0x0004dae0` | CTransform::Advance | Alpha fade step, disappear timer | +| `0x0004dbb0` | CTransform::CalcMatrix | See below | +| `0x0004df30`..`0x0004e250` | Transform script functions | See below | +| `0x0004e270` | CTransform::Render | See below | +| `0x0004e3b0` | CInline::CInline | Defaults above, CClass constructed in place | +| `0x0004e470` | CInline::~CInline | Free the url, CClass teardown, group teardown | +| `0x0004e4d0` | CInline::Init | See below | +| `0x0004e560` | CInline::FinishLoad | See below | +| `0x0004e5d0` | CInline::Render | See below | +| `0x0004e740` | CInline::Advance | See below | +| `0x0004e8c0` | Spinner and Waver Render | Push, rotate about axis by angle, upload, children, Pop | +| `0x0004e980` | CSpinner::Advance | See below | +| `0x0004eaf0` | CWaver::Advance | See below | +| `0x0004ebd0` | CLayout::Render | See below | +| `0x0004ef00` | CLayout::GetBBox | See below | +| `0x0004f000` | CLayout::GetRadius | Sum of child radii, plus `(count - 1) * spacing * 0.5` when count > 1 | +| `0x0004f100` | CSwitch::~CSwitch | ReleaseAll on the choices, CNode teardown | +| `0x0004f160` | CSwitch::Render | Render the picked choice if in range and visible | +| `0x0004f190` | CSwitch::GetBBox | Picked choice's box, else zeros | +| `0x0004f1e0` | CSwitch::GetRadius | Picked choice's radius, else 0 | +| `0x0004f210` | CSwitch::Advance | CNode::Advance, then every choice | +| `0x0004f330` | CBillboard::Render | Matrix helper, Push, MultMatrixLocal, upload, children, Pop | +| `0x0004f3d0` | Billboard matrix | See below | +| `0x0004fc90` | CBackground::CBackground | Defaults above | +| `0x0004fd30` | CBackground::~CBackground | Release the vertex buffer and backdrop, unbind if bound | +| `0x0004fde0`, `0x0004fe20` | Background SetAlpha, DisappearAfter | Same shape as Transform's, on the Background fields | +| `0x0004fe30` | CBackground::Advance | See below | +| `0x00050110` | CBackground::RenderBackdrop | See below | +| `0x00050450` | CBackground::OnSetProperty | isBound true binds; alpha goes through SetAlpha and returns 0 | +| `0x0005a350` | CLayer::~CLayer | Release viewpoint, navigationInfo, vertex buffer | +| `0x0005a400` | CLayer::OnSetProperty | See below | +| `0x0005a4a0` | CLayer::Render | See below | +| `0x0005aaf0` | CLayer::Advance | Group advance, then navigationInfo, then viewpoint | +| `0x0005cc20` | CLevel::CLevel | Defaults above | +| `0x0005ccd0` | CLevel::~CLevel | Clear itself out of the level globals, release tunnel, control, path, shell, free archive | +| `0x0005cd80` | CLevel::Render | See below | +| `0x0005ce90` | CLevel::Activate | See below | +| `0x0005cf90` | CLevel::GoTo | See below | +| `0x0005d040` | CLevel::GoBackTo | See below | +| `0x0005d120` | CLevel::Advance | See below | +| `0x0005d200` | CLevel::Deactivate | Hide, `OnDeactivate`, drop the XIP's mesh buffers when unloadable and not locked | + +### CGroup::GetBBox (`0x0004d4d0`) + +A size of `-1` on every axis clears the specified flag. When dirty and not specified, an empty +group gets a zero box. Otherwise it takes the first child's box as `lo = center - size`, +`hi = center + size`, and for each later child keeps the smaller value on both corners: + +```c +if (b.c - b.s < lo) lo = b.c - b.s; +if (b.c + b.s < hi) hi = b.c + b.s; // smaller, not larger +center = (hi + lo) * 0.5; size = hi - lo; +``` + +So the far corner shrinks rather than grows, and the result is not a union. Dirty is never +cleared, so this reruns on every call. + +### CTransform + +**CalcMatrix (`0x0004dbb0`).** Identity, then for each of scale, translation and rotation +whose clock is above 0: set moving, `t = (now - clock) / fade`, and at `t >= 1` stop the clock +and clamp. Scale and translation lerp component by component; rotation goes through +`D3DXQuaternionSlerp`. The matrix is then built as scale (skipped when `(1,1,1)`), times the +quaternion's rotation (skipped when its w is 0), times translation (skipped when zero). Dirty +ends up as `scaleClock + rotationClock + translationClock > 0`. Center and scaleOrientation +are stored but never used here. + +**Script functions.** SetScale (`0x0004df30`) and SetTranslation (`0x0004e010`): with +`fade > 0`, start the clock at now, copy the current value to start, the arguments to end, set +moving; otherwise store directly. Both set dirty. SetRotation (`0x0004e0f0`) builds the +quaternion first, then either fades from the current quaternion (leaving the axis/angle +property alone) or stores both. SetCenter and SetScaleOrientation store and set dirty. SetAlpha +(`0x0004e200`) fades or stores alpha without touching dirty. DisappearAfter (`0x0004e250`) +stores `t + now` added in single precision, as a double. + +**Advance (`0x0004dae0`).** Group advance, then the alpha fade (same clock math), then if the +disappear time is set and reached, clear it and set alpha to 0. + +**Render (`0x0004e270`).** Clear moving, CalcMatrix when dirty, Push, MultMatrixLocal, upload. +Only with alpha above 0: multiply the effect alpha by it, render the children, restore. Pop +and upload. + +### CInline + +**Init (`0x0004e4d0`).** Clear dirty, ReleaseAll on the children, stop if there's no url. +Make the url absolute and replace everything from its last `/` with `.xip`, so `Foo/default.xap` +names `Foo.xip`. If there was a slash: set loading and first load and call LoadXIP; on success +return and let Advance finish. Otherwise clear loading and FinishLoad right away. + +**FinishLoad (`0x0004e560`).** `LoadAbsURL` on the node's CClass with the absolute url at +`+0xd4`. On success: CreateNode, call its `initialize`, append it to the children (growing 16 +slots at a time), set its parent, call the Inline's own `onLoad`. Then clear first load and +set class loaded. The absolute url is only written by Advance, after the XIP arrives. + +**Render (`0x0004e5d0`).** Nothing without a url or while loading. When the XIP is loaded, not +reloading, has vertex buffers and its first buffer is gone, it sets loading, starts the XIP +reload and returns. Otherwise Init when dirty, and once loaded: `a = (now - timeOfLoad) * 0.5`, +capped at 1 with fadeInDelayLoad, 1 without. The effect alpha is multiplied by `a`, the +current directory is saved and set from the url, the children render, and both are restored. + +**Advance (`0x0004e740`).** Init when dirty and preload. While the XIP is not loaded or is +reloading, set loading and return without advancing the children. On the first frame it is +ready: clear loading; on a first load make the absolute url and, with no thread handle, +FinishLoad; record the time of load. A thread handle that has finished is closed (nothing in +the image creates one). Then Group advance. + +### Spinner and Waver + +**CSpinner::Advance (`0x0004e980`).** Group advance, `angle += rpm * (1/60) * dt * 2pi`, then +wrap into `[-2pi, 2pi]` with a subtract loop above and an add loop below. Constants: `1/60` at +`0x0002c1c4`, `2pi` at `0x0002bf70`, `-2pi` at `0x0002c1c0`. + +**CWaver::Advance (`0x0004eaf0`).** Group advance (not Spinner's), then in x87: + +``` +fld [rpm] +fmul qword [now] +fmul qword [0x0002c1b8] ; pi / 60 +fsin +fmul [field] +fmul [0.5] +fstp [angle] +``` + +### CLayout + +**Render (`0x0004ebd0`).** Nothing for no children. A zero direction lays the children on a +ring of radius `(count / 4) * spacing` (integer divide). Each child gets a Push first; a visible +one is translated by `(cos(-a), 0, sin(-a)) * radius` with `a = i * 2pi / count`, rotated +`a + pi/2` about `(0, 0.1, 0)`, uploaded, rendered, popped. An invisible child's Push is never +popped. Otherwise: one Push, translate by `direction * (count + spacing) * -0.5`, then per +visible child translate `direction * 0.5`, upload, render, and unless it's the last child +translate `direction * (spacing + 0.5)`. Pop at the end. + +**GetBBox (`0x0004ef00`).** Zeros for no children. Otherwise the first child's box, then for +each later child: `size += child.size`, then each axis takes the child's size if that's +smaller. The center is the first child's. + +### Billboard matrix (`0x0004f3d0`) + +Copies the device world matrix, inverts it and brings the camera position (`0x00187fd4`) into +local space with `D3DXVec3TransformNormal`, normalized (the eye). Axis length at most `0.001` +(`0x0002c050`): up is `(inv._14, inv._22, inv._23)` normalized, `x = normalize(up x eye)`, and +the matrix rows are x, up, eye, `(0,0,0,1)`. Otherwise the axis is normalized, +`x = normalize(axis x eye)`, `z = x x axis`, and the matrix is a rotation about the axis by +`acos(z.z)`, negated when `x.z > 0`. + +### CBackground + +**Advance (`0x0004fe30`).** CNode::Advance, the alpha fade and disappear timer (same clock +math as Transform, on the Background fields), the backdrop's Advance, and on first use a 4 +vertex, 32 byte stride buffer at `z = -10000` (`0x0002c1a0`): + +| Vertex | Position | Normal | UV | +|---|---|---|---| +| 0 | `( w/2, -h/2)` | `( w/2, -h/2, 1)` | `(1, 1)` | +| 1 | `(-w/2, -h/2)` | `( w/2, h/2, 1)` | `(0, 1)` | +| 2 | `( w/2, h/2)` | `( w/2, h/2, 1)` | `(1, 0)` | +| 3 | `(-w/2, h/2)` | `(-w/2, h/2, 1)` | `(0, 0)` | + +`w` and `h` are the view size at `0x0017e7f0` / `0x0017e7f4`. + +**RenderBackdrop (`0x00050110`).** Gets the backdrop's texture (vtable `+0x58`) and does +nothing without one. Binds it to stage 0 and clears stage 1, saves projection, world and view, +sets `D3DXMatrixOrthoLH(w, h, -10000, 10000)` with identity world and view. Saves four render +states, clears two of them, sets culling to CCW (`0x901`) and turns blending on; stage 0 color +is the texture and alpha is the texture factor, `(int)(alpha * 255) << 24`. Draws the strip +and restores all of it. + +### CLayer + +**OnSetProperty (`0x0005a400`).** A transparency write removes any lerper on the node, then +either sets alpha to `1 - value` (fade at most 0) or starts a 0x28 byte lerper from the +current alpha to `1 - value` over fade seconds. Every write is accepted. + +**Render (`0x0005a4a0`).** Nothing without a viewpoint or with alpha 0. On first use it +builds a 4 vertex, 16 byte stride buffer of screen corners `( w/2,-h/2)`, `(-w/2,-h/2)`, +`( w/2, h/2)`, `(-w/2, h/2)` at z 0, color `0xff000000`. Then, under the ortho projection with +identity world and view: stage 0 alpha = diffuse times texture factor `(int)(alpha * 255) << 24`, +color = diffuse, texture transform off, texcoord index 0, blending on, culling off, stage 1 +color and alpha off; draws the strip as a triangle strip and restores the three matrices. + +It then walks the navigationInfo's class chain looking for NavigationInfo (`0x00187f58`) but +doesn't act on the answer. Projection: near `0.1`, far `1000` (`0x0002bfc0`, `0x0002c060`); +with a navigationInfo, near is `avatarSize.x * 0.5` and far is `visibilityLimit` when that is +nonzero. Field of view from the viewpoint (`+0x20`), aspect `1.5` (`0x0002c184`) or `1.875` +(`0x0002c1b0`) when the widescreen flag at `0x0017e7ec` is set, written into the shared +projection at `0x0017f4e8`. View: position `(x, y, -z)` (`+0x38`), quaternion about the +orientation axis (`+0x28`) by minus its angle (`+0x34`), `D3DXMatrixAffineTransformation` +with scale 1, inverted, and the third row (`_31` to `_34`) negated, into the shared view at +`0x0017f468`. Push, LoadIdentity, upload, render the children, Pop, then put the saved +projection and view back both in the shared matrices and on the device. + +### CLevel + +**Render (`0x0005cd80`).** With fade: `t = (endTime - now) * 1.3333334`, clamped to `[0, 1]` +(`0x0002c104` is `1 / 0.75`). The current level uses `1 - t`, the level being left uses `t`, +and either one returns early at 0. Any other level renders at full strength. The effect alpha +is multiplied, the shell renders (no visible test), the current level sets the motion blur to +`min(a * 1.3, 1)`, the children render, the effect alpha is restored. + +**Activate (`0x0005ce90`, not script visible).** Set the transition flag. Load the archive XIP +if there is one and it isn't loaded yet. If a transition is still running (a level being +left, or a current level with the end time still set), cut it short: Deactivate the level +being left (inlined) and clear it; hide the current level and drop its XIP's mesh buffers +when it has a XIP, is unloadable and the XIP isn't locked; clear the current level; hide and +clear the tunnel. Then unbind the controller (BindJoystick with no node, inlined), set the +end time to `now + 1.0`, make the current level the one being left, show this level and make +it current. + +**GoTo (`0x0005cf90`).** Nothing if already current. Activate, make this level's tunnel the +current tunnel and show it. A path whose class chain reaches Viewpoint (`0x00187e88`) gets +bound; one that reaches CameraPath (`0x00188040`) is started forward (active `+0x20` = 1, +start `+0x30` = now, backward `+0x21` = 0). Then `OnActivate`. + +**GoBackTo (`0x0005d040`).** Nothing if already current. Activate; the tunnel is the one of +the level being left (none if there isn't one) and is shown. If that level's path is a +CameraPath it is started backward; otherwise a Viewpoint path on this level is bound. Then +`OnActivate`. + +**Advance (`0x0005d120`).** Group advance, then tunnel, path, control and shell. When this is +the current level and the end time is set and reached: clear it, hide the tunnel, Deactivate +and clear the level being left, and set arrive. While arrive is set and the XIP is absent or +loaded (`+0x28`): clear arrive, bind this level's control as the controller, call +`OnArrival`, clear the transition flag. + +### Quirks + +- Group's far corner and Layout's size both shrink toward the smallest child instead of + covering all of them. +- Layout's ring pushes for every child but pops only for visible ones. +- Layer never initializes its alpha; the first transparency write sets it. +- Setting a Background's isBound to false only clears the flag; it stays the bound + background. +- Deactivate reads the XIP's locked flag without checking the XIP exists, so an unloadable + level with no archive faults when it is left. +- An Inline whose XIP fails to load runs FinishLoad with the absolute url never written. +- A Background, being a plain CNode, clears its own visible flag the first time a group + renders it. From 2e817c4c91fec7f8f55e9fd07f8432eb88810e47 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:13:56 -0500 Subject: [PATCH 064/167] Fixed leaving a level that has no archive Deactivate read the XIP's locked flag without checking there was a XIP, so leaving any unloadable level with no archive crashed. Four shipped levels have none (launcher, launch game, XOnline, restart); a test level reproduced the crash before and runs clean now. --- theseus/render/scene_groups.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/theseus/render/scene_groups.cpp b/theseus/render/scene_groups.cpp index 976ea19..69f1488 100755 --- a/theseus/render/scene_groups.cpp +++ b/theseus/render/scene_groups.cpp @@ -1956,12 +1956,13 @@ void CLevel::Advance(float nSeconds) // Reconstructed from 5960 retail XBE @ 0x0005d200: // visible = 0; CallFunction(this, "OnDeactivate"); // if (unloadable && !xip->locked) xip->DeleteMeshBuffers(); +// The binary reads the XIP without checking for one; we skip levels with no archive. void CLevel::Deactivate() { m_visible = false; CallFunction(this, _T("OnDeactivate")); - if (m_unloadable && !m_xipFile->m_locked) + if (m_unloadable && m_xipFile != NULL && !m_xipFile->m_locked) m_xipFile->DeleteMeshBuffers(); } From 41f5ccfe9e82a9ab7dcbaae2ceb0af3b1c4472da Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:15:52 -0500 Subject: [PATCH 065/167] Fixed an Inline crashing when its XIP won't open Without its XIP, Init loaded the class from an absolute url that was never filled in, and the parser fell over. It fills it in first now. I hit it by visiting enough menus to fill the 20 slot XIP pool and then opening the time zone panel; that panel comes up empty now instead of crashing, and the pool limit itself is still there. --- theseus/render/scene_groups.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/theseus/render/scene_groups.cpp b/theseus/render/scene_groups.cpp index 69f1488..b4be5b4 100755 --- a/theseus/render/scene_groups.cpp +++ b/theseus/render/scene_groups.cpp @@ -615,7 +615,8 @@ CInline::~CInline() // xip = LoadXIP(path); if (xip) return; loading = 0; } // FinishLoad(); // The archive is named after the folder holding the XAP. With one, the load -// finishes later in Advance; without, the class loads right here. +// finishes later in Advance; without, the class loads right here. The binary +// never fills in the absolute url on that path; we do, so it can't parse garbage. void CInline::Init() { m_dirty = false; @@ -638,6 +639,7 @@ void CInline::Init() m_bLoading = false; } + MakeAbsoluteURL(m_szAbsURL, m_url); FinishLoad(); } From 9c29934e2db13bc58f06ea19a8d3649add8af201 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:35:55 -0500 Subject: [PATCH 066/167] Rebuilt the text meshes and Text node from the 5960 binary Wrote the XTF loader, glyph lookup, fades, the mesh layout, cursor, font table, Text node and text cache fresh from 0x53870..0x55f60 and wrote up what the binary does in the Text doc. Meshes, bounding boxes, node draws and overlay draws match the old code byte for byte on all 3165 strings in the shipped XAPs and language files. --- docs/decomp/Text.md | 329 +++++++- theseus/render/text.cpp | 1664 +++++++++++++++++++-------------------- 2 files changed, 1129 insertions(+), 864 deletions(-) diff --git a/docs/decomp/Text.md b/docs/decomp/Text.md index 944cbba..260cad4 100644 --- a/docs/decomp/Text.md +++ b/docs/decomp/Text.md @@ -1,17 +1,328 @@ # Font & Text Rendering -Infrastructure layer; no XAP node class. This is not a script-accessible node type. +Vector fonts, text meshes and the `Text` node in the 5960 retail binary. Glyphs are stored +pretriangulated in `.xtf` files; a string becomes one D3DX mesh, which the node draws and +scrolls. -## Font Loading +## Functions -Loads Xbox .xtf font files. A font table is built at startup by scanning the font directory. FindFont and GetFont map face name strings to loaded font file handles. +| Address | Name | What it does | +|---------|------|--------------| +| 0x000537c0 | (XTF1 setup) | Creates the LZX decompressor and the block cache for compressed fonts | +| 0x00053840 | (font read) | `ReadFile` wrapper that refuses anything over 0x1400000 bytes | +| 0x00053870 | CFont::Close | Closes the file, frees loaded shapes and every table | +| 0x00053970 | (XTF0 open) | Reads the header, glyph set and glyph records | +| 0x00053b00 | (XTF1 open) | Same, plus a per glyph offset table | +| 0x00053ce0 | CFont::Open | Opens the file, verifies it, dispatches on the magic | +| 0x00053d80 | (XTF1 shape parse) | Copies one glyph shape out of a decompressed block | +| 0x00053ed0 | CFont::LoadGlyph (XTF0) | Seeks to a glyph shape and reads it | +| 0x00054010 | CFont::LoadGlyph (XTF1) | Finds or decompresses the block holding a glyph | +| 0x000541c0 | CFont::FindGlyphIndex | Code point to glyph index | +| 0x00054290 | HorizontalFade | Fades vertex alpha out toward a column edge | +| 0x00054380 | VerticalFade | Fades vertex alpha at the top and bottom of a scroll window | +| 0x00054440 | CFont::CreateTextMesh | Lays out a string into a mesh | +| 0x00055030 | CFont::CreateCursorMesh | Appends the blinking cursor quad | +| 0x000552b0 | InitFontTable | Scans the font directory | +| 0x00055640 | GetFont | Face name to an open font | +| 0x00055750 | Text_Exit | Frees the font table | +| 0x000557b0 | CTextNode::CreateNode | Factory for `Text` | +| 0x000557e0 | CTextNode::CTextNode | Constructor | +| 0x00055880 | CTextNode::~CTextNode | Destructor | +| 0x00055900 | CTextNode::Advance | Vertical scroll | +| 0x000559a0 | FaceFromFont | `body` / `heading` to a face name | +| 0x000559e0 | CTextNode::Render | Builds and draws the mesh | +| 0x00055f20 | CTextNode::OnSetProperty | Drops the mesh on any property but `scroll` | +| 0x00055f60 | FindText | Ten slot cache of throwaway text nodes | -## Text Mesh Generation +Globals: the keyboard's string `0x0017e748`, cursor index `0x00175220` (starts -1), last built +cursor `0x00175224`, the asterisk formatting flag `0x0017e74c` (a BSS byte, so it starts off; +only the keyboard's `SetAsteriskFormattingState` / `GetAsteriskFormattingState` touch it), the +font table `0x001763e8` (ten 0x30 byte slots: face, file, then the CFont) and its count +`0x00175228` (starts 1, slot 0 is `HGGothicM` / `HGGothicM.xtf`). -Builds text meshes from glyph outlines by triangulating Bezier curves. Glyph data is cached with LRU eviction to keep memory usage bounded. +## The XTF file -## Rendering Features +| Offset | Size | Field | +|--------|------|-------| +| 0 | 4 | magic, `XTF0` (`0x30465458`) or `XTF1` (`0x31465458`) | +| 4 | 4 | header length, 0x20 | +| 8 | 0x20 | face name, ASCII | +| 0x28 | 4 | glyph set size `cb` | +| 0x2c | cb - 4 | glyph set: `flAccel`, glyph count, range count, then (first code point, count) WORD pairs | +| | count * 0x1c | glyph records: six floats (black box x, y, origin x, y, advance x, y) and the shape's file offset | -- Vertical fade (alpha gradient top-to-bottom) -- Alpha blending -- Multi-line layout +A glyph shape is a WORD index count, a WORD point count, the indices, then the points as float +(x, y) pairs. In memory a shape is 0xc bytes: the two counts, then pointers to the index and point +arrays. The record's offset dword is overwritten with the shape pointer once loaded. + +The CFont object: file handle `+0`, magic `+4`, glyph set `+8`, glyph records `+0xc`, loaded flags +`+0x10`, then XTF1 only: per glyph block offsets `+0x14`, decompressor `+0x18`, largest +compressed block `+0x1c`, compressed buffer `+0x20`, block cache `+0x24`. + +## Opening a font + +**Open (`0x00053ce0`)** opens the file read only (`GENERIC_READ`, `FILE_SHARE_READ`, +`OPEN_EXISTING`), then checks it against the dash's signed file hash table (`0x0003eae0`, which +calls `HalReturnToFirmware` on a mismatch). It reads the magic: `XTF0` goes to `0x00053970`, +`XTF1` sets up decompression (`0x000537c0`: `LDICreateDecompression` with 0x8000 byte blocks and +a 0x20000 window, a compressed buffer of the size it reports, and a zeroed ten block cache) and +goes to `0x00053b00`; anything else closes the file and fails. + +**XTF0 body (`0x00053970`).** + +```c +read 4 (header length, not checked here); read 0x20 (face); read 4 (cb); +if (cb < 0x14) fail; +set = new(cb); set->cbThis = cb; read cb - 4 into set + 4; +if (bytes read != cb - 4 || set->cRanges * 4 + 0x10 != cb) fail; +glyphs = new(set->cGlyphs * 0x1c); read them; check the byte count; +loaded = new(set->cGlyphs); zero it; +``` + +Every read checks both the return value and the byte count. A failure returns false without +closing anything; Close cleans up later. The XTF1 body reads a WORD per glyph before the records: +where the glyph's shape sits inside its decompressed block. The record's offset then points at +the compressed block. + +**LoadGlyph, XTF0 (`0x00053ed0`).** + +```c +if (loaded[n]) return 1; +off = glyphs[n].shape; if (off == 0) return 0; +if (SetFilePointer(hFile, off, 0, FILE_BEGIN) == -1) return 0; +s = new(0xc); +read 2 -> s->nIndices; s->indices = new(nIndices * 2); +read 2 -> s->nPoints; s->points = new(nPoints * 8); +read the indices, then the points, each through the capped reader and size checked; +glyphs[n].shape = s; loaded[n] = 1; return 1; +``` + +**LoadGlyph, XTF1 (`0x00054010`)** keeps ten decompressed blocks (0x8008 bytes each: last use +tick, block offset, data). A hit refreshes the tick; a miss takes the oldest slot, reads the +block's WORD compressed and decompressed sizes (no more than the largest compressed block and +0x8000), the compressed bytes, runs `LDIDecompress` and checks the output size. `0x00053d80` then +copies the shape out of the block, bounds checking every read against 0x8000. + +**FindGlyphIndex (`0x000541c0`).** + +```c +for (;;) { + base = 0; + for (i = 0; i < set->cRanges; i++) { + if (r[i].first <= ch && ch < r[i].first + r[i].count) return ch - r[i].first + base; + base += r[i].count; + } + if (ch == 0x7f) return 0; + ch = 0x7f; // anything the font lacks draws as the DEL box +} +``` + +**Close (`0x00053870`)** closes the handle, frees each loaded, non-null shape (indices, points, +shape), destroys the decompressor, then frees the glyph set, records, loaded flags, block table, +compressed buffer and block cache. + +## Font table + +**InitFontTable (`0x000552b0`).** Builds `fonts\*.xtf` and runs `FindFirstFileA` on it, +falling back to `*.xtf`. For each hit, while the count is under ten: strip a `file:` +prefix and turn `/` into `\`, open, read the magic (`XTF0` or `XTF1`), the header length (must be +0x20) and the 32 byte face. The face is widened; if it equals slot 0's face it's freed, otherwise +the face and a copy of the full path go into the next slot. A bad header skips the file without +closing it. + +**GetFont (`0x00055640`).** + +```c +if (count == 1) InitFontTable(); +for (i = 0; i < count; i++) if (!_wcsicmp(face, fonts[i].face)) break; +if (i == count) i = 0; +if (fonts[i].font.hFile == -1) { + path = wcschr(file, ':') ? file : swprintf(L"%s%s", appdir, file); + if (!Open(path) && fonts[0].font.hFile == -1 && !Open(fonts[0].file)) return NULL; +} +return &fonts[i].font; +``` + +**Text_Exit (`0x00055750`)** frees the face and file strings of every slot past the first and +closes each font. + +**FaceFromFont (`0x000559a0`)**: NULL or `body` gives `XBox Book`, `heading` gives `Xbox`, +anything else passes through. + +## Text meshes + +Vertices are FVF `0x52` (XYZ, NORMAL, DIFFUSE), 28 bytes: position, normal (0, 0, 1), color. +Constants: `1.0` (`0x0002bed8`), `0.5` (`0x0002bed4`), `2.0` (`0x0002bef4`), `3.0` +(`0x0002bf00`), `255.0` (`0x0002bf38`), `1.5` (`0x0002c184`), `0.1` (`0x0002bfc0`), `0.75` +(`0x0002c018`), `pi` (`0x0002bffc`), `16.556152` (`0x0002c1b4`). + +**CreateTextMesh (`0x00054440`)** takes the font, the string, its length (-1 for `wcslen`), the +mesh out pointer, bounding box min and max, a format width, a don't-break flag, a scale and an +indent (width, height); `ret 0x2c`. + +A heading is a column that starts and ends with `*` (at least two characters before the tab or +line end). Every heading test also requires the asterisk formatting flag. + +First pass, per line: + +1. If the line opens a heading, skip the `*` and remember it. The memory isn't cleared at the next + line, only at a tab. +2. Per character: a remembered heading's closing `*` (next character is tab, line end or the end) + is skipped. Without don't-break, `\r` (eating a following `\n`) and `\n` end the line, and a tab + switches to column 2, marks the text as multi column and checks for a heading after it. +3. Otherwise look the glyph up and load it (XTF0 or XTF1 loader by magic). A glyph that won't load + has its record's shape slot zeroed and is skipped. +4. If the vertex total would pass 0xffff, the string is cut at this character and the pass ends. +5. Add the glyph's advance to the column's width, index count / 3 to the face count and its points + to the vertex count. + +The widest first and second columns are kept. A cursor index from 0 to the length adds two faces +and four vertices. No faces means no mesh. + +Then with a positive format width: a single column takes the whole width; two columns wider than +it shrink the first to `width - col2` and turn fading on. Without a width fading is off; otherwise +fading starts as the don't-break flag. + +`XAppCreateMeshFVF(faces, verts, D3DXMESH_MANAGED (0x220), 0x52)` (`0x0003d2f0`, which frees +caches and retries on out of memory), then both buffers are locked and written without checks. + +Second pass. Color is white with the blue byte from a float that headings set to 0.5 and their +closing `*` sets back to 1.0: `0xffffff00 | (int)(blue * 255)`. x starts at the indent width. +Per line, column 1 with the first column width as its limit; per character: + +- Past the end reads as `\r`. `\r` eats a following `\n`. +- Line end: with don't-break and fading, fade the column (`limit - 1` to `limit`) and keep going on + the same line. Otherwise column 2 shifts right by `col2 max - col2 width`, column 1 fades if + fading is on, and unless this is the end of the string y drops 1.0 (in a heading, after `\r`, or + with two columns) or 1.5 (a new paragraph), with x going back to 0 below the indent height and to + the indent width inside it. +- Tab (without don't-break): fade the column from `limit - 2` if fading, then column 2 starts at + `width - col2 max` with that as its limit, and a heading may open after it. +- A heading's closing `*` restores blue to 1.0 and is skipped. +- With a positive width, without don't-break, in column 1, a non space glyph that would end past + the width wraps: y drops 1.0, x goes back to 0 or the indent, and the string, vertex and index + positions rewind to the last break point. With no break point on the line fading turns on + instead. +- A glyph with no shape is skipped. Otherwise its indices go in offset by the vertex position and + its points go in at (x + px, y + py). +- If the character just read is at the cursor index, the cursor quad goes in at (x, y). +- x advances by the glyph's advance (column 2 also adds it to its width). A space, anything from + 0x3040 up to 0xf000, or 0xff66 and up becomes the new break point. + +A cursor index equal to the length gets the quad after the last character, visible only while +x < 16.556152 (the edge of the keyboard's edit box). Then every counted vertex is scaled (x, y, +z) and, given both pointers, `D3DXComputeBoundingBox` runs over the written vertices. + +**HorizontalFade (`0x00054290`).** Vertices in EAX, end in XMM1, direction in BL, then count and +start on the stack. + +```c +inv = 1.0 / (end - start); +for (i < n) { + a = (v.x - start) * inv; + a = a <= 0 ? 0 : a >= 1 ? 1 : (3 - 2a) * a * a; + if (out) a = 1 - a; + v.alpha = (int)(v.alpha * a); +} +``` + +CreateTextMesh only ever calls it fading out. + +**VerticalFade (`0x00054380`).** Vertices in ECX, count in EDX, top in XMM4, bottom in XMM5, +scroll on the stack. + +```c +y = v.y + scroll; +if (y > top || y < bottom) a = 0; +else if (y > top - 1) a = 1 - (y - (top - 1)); +else if (y < bottom + 1) a = 1 - ((bottom + 1) - y); +else a = 1; +alpha byte = (int)(ease(a) * 255); // same ease as above +``` + +**CreateCursorMesh (`0x00055030`).** + +```c +a = |sin((now + now) * pi)|; if (!visible) a = 0; // x87, now is the double clock +color = (int)(a * 255) << 24 | 0xffffff; +indices += { v, v+1, v+2, v, v+2, v+3 }; +(x, y - 0.1) (x + 0.1, y - 0.1) (x + 0.1, y + 0.75) (x, y + 0.75), normal (0, 0, 1) +``` + +## CTextNode + +0x78 bytes. Prop map `0x00175230`, vtable `0x00028928`. + +| Offset | Field | Default | +|--------|-------|---------| +| 0x1c | text | | +| 0x20 | font | | +| 0x24 | justify | | +| 0x28 | deviation | 0.01 | +| 0x2c | extrusion | 1.0 | +| 0x30 | width | 0 | +| 0x34 | translate | 1 | +| 0x38 | height | 0 | +| 0x3c | scroll | 0 | +| 0x40 | scrollRate | 0 | +| 0x44 | scrollDelay | 0 | +| 0x48 | indentWidth | 0 | +| 0x4c | indentHeight | 0 | +| 0x50 | mesh | | +| 0x54 | bbox min, max | | +| 0x6c | language the mesh was built in | 0 | +| 0x70 | time scrolling starts (double) | 0 | + +**Destructor (`0x00055880`)** frees the three strings and releases the mesh. + +**Advance (`0x00055900`).** + +```c +CNode::Advance(t); +if (scrollRate > 0 && mesh) { + if (timeToScroll == 0) timeToScroll = scrollDelay + now; + h = bbox.max.y - bbox.min.y; + if (timeToScroll <= now && h > height) { + scroll = height * t * scrollRate + scroll; + if (h + 1.5 <= scroll) scroll -= h + 1.5; + } +} +``` + +**Render (`0x000559e0`).** + +1. If the keyboard left a string: when the text, or the cursor against the last built cursor, + differs, copy it in, record the cursor and rebuild. A cursor of -1 always rebuilds. Clear the + string. +2. No text, or empty: return. +3. `` and `` prefixes: format the local time (`FormatTime`) or date (`FormatDate`, + `0x0003cd40`) into a 32 character buffer; if it differs from what follows the prefix, the text + becomes the prefix plus the value and the mesh rebuilds. The node stops translating and the + string after the prefix is what gets drawn. +4. Translating and the current language differs from the mesh's: rebuild. Rebuild releases the + mesh. +5. No mesh: translate if translating, record the language, then + `CreateTextMesh(GetFont(FaceFromFont(font)), text, -1, &mesh, &min, &max, |width|, width < 0, + 1.0, indentWidth, indentHeight)`. Still no mesh: return. +6. Justify `middle` or `end` offsets x by half or all of the drawn width: the bounding box width, + capped at |width| when width is set. +7. Twice: if height is set and the text is taller, lock the vertices and `VerticalFade` with top + `1 - scroll` (one higher while scroll is 0), bottom `top - height - 2` and, for the second + copy, an offset of `-(h + 1.5)`; the second copy is skipped while `height + scroll < h`. + Without scrolling there's no second copy. Each draw pushes the world matrix, translates by + (x offset, scroll, plus `-(h + 1.5)` for the second copy), loads it into the device, sets + CULLMODE to NONE, `DrawSubset(0)`, CULLMODE back to CCW, pops and reloads the world matrix. + +**OnSetProperty (`0x00055f20`).** Any property but `scroll` (offset 0x3c) releases the mesh and +zeroes the scroll start time and the scroll. + +**FindText (`0x00055f60`).** Ten (node, last used) slots. + +```c +for (i < 10) { + if (node && node->text && !wcscmp(node->text, s) && node->width == w) { used = now; return slot; } + if (!empty) { if (!node) empty = slot; else if (!oldest || used < oldest->used) oldest = slot; } +} +if (!empty && oldest) { delete oldest->node; zero the slot; empty = oldest; } +empty->node = new CTextNode; node->text = copy(s); node->width = w; empty->used = now; +``` diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp index 9acba46..ae639a7 100644 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -1,7 +1,10 @@ -// text.cpp: XTF font loading, glyph mesh generation, and text node -// rendering. Reads Xbox .xtf vector font files, builds triangle meshes -// from glyph outlines, handles word wrap, scrolling, cursor blinking, -// and the overlay text API. Decompiled from the 5960 retail XBE; see +// text.cpp: XTF vector fonts, text meshes and the Text node. +// +// A font is a .xtf file of pretriangulated glyphs. CreateTextMesh lays a +// string out into one D3DX mesh (word wrap, two columns, *heading* lines, +// the keyboard cursor), the Text node draws and scrolls that mesh, and the +// overlay calls at the bottom draw the same glyphs straight to the screen. +// Rebuilt function by function from the 5960 retail XBE; the findings are in // docs/decomp/Text.md. #include "std.h" @@ -11,16 +14,15 @@ #include "xap_compile.h" #include "runner.h" -#ifndef LF_FACESIZE -#define LF_FACESIZE 32 -#endif +extern void TheseusCreateMeshFVF(DWORD NumFaces, DWORD NumVertices, DWORD Options, DWORD FVF, LPD3DXMESH* ppMesh); +// The keyboard hands its edit string and cursor to the next Text node that +// renders (0x0017e748, 0x00175220, 0x00175224 in the binary). TCHAR* g_szText = NULL; - -// Oh what a nasty hack... -int g_nTextChar = -1; // index of char where we want a cursor +int g_nTextChar = -1; int g_nTextCharLast = -1; +// D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE (0x52), 28 bytes a vertex. struct TEXTVERTEX { float x, y, z; @@ -28,50 +30,74 @@ struct TEXTVERTEX DWORD color; }; -struct CGlyphVertex +#define TEXT_FVF (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE) + +static inline void SetVertexAlpha(TEXTVERTEX& v, float a) { - float x, y; -}; + v.color = (v.color & 0x00ffffff) | ((DWORD)a << 24); +} -struct CGlyphShape + +// ===== XTF files =========================================================== +// "XTF0", a 32 byte face name block, the glyph set (code point ranges), one +// 28 byte record per glyph, then the glyph shapes at the offsets the records +// give. The layout is fixed by the file, so these structs match it exactly. + +struct XtfRange { - WORD m_nIndexCount; - WORD m_nVertexCount; - WORD* m_indices; - CGlyphVertex* m_vertices; - int m_nGlyphIndex; // added to hold the index of the character + WORD first; + WORD count; }; -struct CGlyphMetrics +struct XtfGlyphSet { - float gmfBlackBoxX; - float gmfBlackBoxY; - CGlyphVertex gmfptGlyphOrigin; - float gmfCellIncX; - float gmfCellIncY; + DWORD cbThis; + DWORD flAccel; + DWORD cGlyphs; + DWORD cRanges; + XtfRange ranges[1]; }; -struct CGlyphObject +struct XtfGlyph { - CGlyphMetrics m_metrics; - CGlyphShape* m_glyphShape; // or DWORD m_dwFileOffsetOfGlyphShape + float blackBoxX, blackBoxY; + float originX, originY; + float advanceX, advanceY; + DWORD shapeOffset; }; -struct CWCRange +struct XtfPoint { - WCHAR wcLow; - USHORT cGlyphs; + float x, y; }; -struct CGlyphSet +struct CGlyphShape { - DWORD cbThis; - DWORD flAccel; - DWORD cGlyphsSupported; - DWORD cRanges; - CWCRange ranges [1]; + WORD nIndices; + WORD nPoints; + WORD* indices; + XtfPoint* points; }; +#define XTF0_MAGIC 0x30465458 +#define XTF_FACE_LEN 32 + +// The binary refuses any single font read over 20 MB (0x00053840). +static bool ReadExact(HANDLE hFile, void* pv, DWORD cb) +{ + if (cb > 0x1400000) + return false; + DWORD cbRead = 0; + return ReadFile(hFile, pv, cb, &cbRead, NULL) && cbRead == cb; +} + +static void FreeShape(CGlyphShape* pShape) +{ + delete [] pShape->indices; + delete [] pShape->points; + delete pShape; +} + class CFont { public: @@ -80,24 +106,29 @@ class CFont bool Open(const TCHAR* szFile); void Close(); - bool LoadGlyph(int nGlyphIndex); + bool IsOpen() const { return m_pSet != NULL; } + int FindGlyphIndex(WCHAR wch); + bool LoadGlyph(int nGlyph); + float Advance(int nGlyph) const { return m_pGlyphs[nGlyph].advanceX; } + CGlyphShape* Shape(int nGlyph) const { return m_ppShapes[nGlyph]; } + void CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, D3DXVECTOR3* pMin, D3DXVECTOR3* pMax, float nFormatWidth, bool bDoNotBreak, float scale); - void CreateCursorMesh(WORD*& indices, int& nCurIndex, TEXTVERTEX*& verts, int& nCurVertex, float x, float y, bool visible); - bool IsBreakChar(TCHAR ch); HANDLE m_hFile; - CGlyphSet* m_glyphSet; - CGlyphObject* m_rgGlyphObjects; - bool* m_rgGlyphLoaded; + +private: + XtfGlyphSet* m_pSet; + XtfGlyph* m_pGlyphs; + CGlyphShape** m_ppShapes; }; -CFont::CFont() +CFont::CFont() : + m_hFile(INVALID_HANDLE_VALUE), + m_pSet(NULL), + m_pGlyphs(NULL), + m_ppShapes(NULL) { - m_hFile = INVALID_HANDLE_VALUE; - m_glyphSet = NULL; - m_rgGlyphObjects = NULL; - m_rgGlyphLoaded = NULL; } CFont::~CFont() @@ -105,178 +136,202 @@ CFont::~CFont() Close(); } +// CFont::Close() +// Reconstructed from 5960 retail XBE @ 0x00053870: +// if (hFile != -1) { CloseHandle(hFile); hFile = -1; } +// for (i < glyphSet->cGlyphs) if (loaded[i] && shape[i]) free indices, points, shape; +// free glyphSet, glyph records, loaded flags (plus the XTF1 buffers); void CFont::Close() { if (m_hFile != INVALID_HANDLE_VALUE) - VERIFY(CloseHandle(m_hFile)); + { + CloseHandle(m_hFile); + m_hFile = INVALID_HANDLE_VALUE; + } - if (m_glyphSet != NULL) + if (m_ppShapes != NULL) { - for (UINT i = 0; i < m_glyphSet->cGlyphsSupported; i += 1) + for (DWORD i = 0; i < m_pSet->cGlyphs; i++) { - if (m_rgGlyphLoaded[i]) - { - CGlyphShape* pGlyphShape = m_rgGlyphObjects[i].m_glyphShape; - delete [] pGlyphShape->m_indices; - delete [] pGlyphShape->m_vertices; - delete pGlyphShape; - } + if (m_ppShapes[i] != NULL) + FreeShape(m_ppShapes[i]); } } - delete [] (BYTE*)m_glyphSet; - delete [] m_rgGlyphObjects; - delete [] m_rgGlyphLoaded; - - m_hFile = INVALID_HANDLE_VALUE; - m_glyphSet = NULL; - m_rgGlyphObjects = NULL; - m_rgGlyphLoaded = NULL; + delete [] (BYTE*)m_pSet; + delete [] m_pGlyphs; + delete [] m_ppShapes; + m_pSet = NULL; + m_pGlyphs = NULL; + m_ppShapes = NULL; } +// CFont::Open(szFile) +// Reconstructed from 5960 retail XBE @ 0x00053ce0 (open + magic) and +// 0x00053970 (the XTF0 body). Ghidra decompile, trimmed: +// hFile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING); +// read magic; XTF0 -> 0x53970, XTF1 -> LZX setup + 0x53b00, else close and fail; +// 0x53970: read header length (4), face (0x20), cbSet (4); need cbSet > 0x13; +// set = alloc(cbSet); set->cbThis = cbSet; read cbSet - 4 more; +// need cRanges * 4 + 0x10 == cbSet; +// glyphs = alloc(cGlyphs * 0x1c); read them all; loaded = zeroed(cGlyphs); +// Every read must return its full size. The 5960 load also checks the file +// against the dash's signed hash table (0x0003eae0) and reads the compressed +// XTF1 format; Theseus ships XTF0 fonts only and skips both. A failed open +// closes the file here, where the binary leaves it for Close. bool CFont::Open(const TCHAR* szFile) { - ASSERT(m_hFile == INVALID_HANDLE_VALUE); + Close(); m_hFile = TheseusCreateFile(szFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (m_hFile == INVALID_HANDLE_VALUE) return false; - DWORD dwRead; - - DWORD dwMagic; - ReadFile(m_hFile, &dwMagic, 4, &dwRead, NULL); + DWORD dwMagic = 0, cbHeader = 0, cbSet = 0; + char face[XTF_FACE_LEN]; + bool ok = ReadExact(m_hFile, &dwMagic, 4) && dwMagic == XTF0_MAGIC && + ReadExact(m_hFile, &cbHeader, 4) && + ReadExact(m_hFile, face, XTF_FACE_LEN) && + ReadExact(m_hFile, &cbSet, 4) && cbSet >= 20; - if (dwMagic != 0x30465458) // "XTF0" + if (ok) { - TRACE(_T("\001Invalid font file: %s\n"), szFile); - - VERIFY(CloseHandle(m_hFile)); - m_hFile = INVALID_HANDLE_VALUE; - - return false; + m_pSet = (XtfGlyphSet*)new BYTE[cbSet]; + m_pSet->cbThis = cbSet; + ok = ReadExact(m_hFile, &m_pSet->flAccel, cbSet - 4) && + 16 + m_pSet->cRanges * sizeof (XtfRange) == cbSet; } - // Skip the face-name header. It's a fixed-length LF_FACESIZE (32-byte) - // ASCII block, the actual face string is read out separately by the - // font registry; the loader here just consumes the bytes to advance. - DWORD dwHeaderLen; - VERIFY(ReadFile(m_hFile, &dwHeaderLen, 4, &dwRead, NULL)); - ASSERT(dwHeaderLen == LF_FACESIZE); - - char szFace [LF_FACESIZE]; - VERIFY(ReadFile(m_hFile, szFace, LF_FACESIZE, &dwRead, NULL)); - - DWORD cbGlyphSet; - VERIFY(ReadFile(m_hFile, &cbGlyphSet, 4, &dwRead, NULL)); - - m_glyphSet = (CGlyphSet*)new BYTE [cbGlyphSet]; - m_glyphSet->cbThis = cbGlyphSet; - VERIFY(ReadFile(m_hFile, ((BYTE*)m_glyphSet) + 4, cbGlyphSet - 4, &dwRead, NULL)); - - m_rgGlyphObjects = new CGlyphObject [m_glyphSet->cGlyphsSupported]; - VERIFY(ReadFile(m_hFile, m_rgGlyphObjects, sizeof (CGlyphObject) * m_glyphSet->cGlyphsSupported, &dwRead, NULL)); + if (ok) + { + DWORD nGlyphs = m_pSet->cGlyphs; + m_pGlyphs = new XtfGlyph[nGlyphs]; + ok = ReadExact(m_hFile, m_pGlyphs, nGlyphs * sizeof (XtfGlyph)); - m_rgGlyphLoaded = new bool [m_glyphSet->cGlyphsSupported]; - ZeroMemory(m_rgGlyphLoaded, sizeof (bool) * m_glyphSet->cGlyphsSupported); + m_ppShapes = new CGlyphShape*[nGlyphs]; + for (DWORD i = 0; i < nGlyphs; i++) + m_ppShapes[i] = NULL; + } - return true; + if (!ok) + Close(); + return ok; } -bool CFont::LoadGlyph(int nGlyphIndex) +// CFont::LoadGlyph(nGlyph) +// Reconstructed from 5960 retail XBE @ 0x00053ed0 (XTF0). Ghidra decompile: +// if (loaded[n]) return 1; +// off = glyphs[n].shape; if (off == 0) return 0; +// if (SetFilePointer(hFile, off, 0, FILE_BEGIN) == -1) return 0; +// s = new(0xc); read s->nIndices (2); s->indices = new(nIndices * 2); +// read s->nPoints (2); s->points = new(nPoints * 8); +// read indices, read points, each checked against its byte count; +// glyphs[n].shape = s; loaded[n] = 1; return 1; +// The record's offset and the loaded shape live apart here so the record can +// stay the file's 28 bytes on 64 bit hosts. A failed load frees what it made. +bool CFont::LoadGlyph(int nGlyph) { - ASSERT(m_glyphSet != NULL); - ASSERT(nGlyphIndex >= 0 && (UINT)nGlyphIndex < m_glyphSet->cGlyphsSupported); - - if (m_rgGlyphLoaded[nGlyphIndex]) + if (m_ppShapes[nGlyph] != NULL) return true; - VERIFY(SetFilePointer(m_hFile, (DWORD)m_rgGlyphObjects[nGlyphIndex].m_glyphShape, 0, FILE_BEGIN) != ~0); - CGlyphShape* pGlyphShape = new CGlyphShape; - ASSERT(pGlyphShape); - - DWORD dwRead; - - VERIFY(ReadFile(m_hFile, &pGlyphShape->m_nIndexCount, 2, &dwRead, NULL)); - pGlyphShape->m_indices = new WORD [pGlyphShape->m_nIndexCount]; + DWORD offset = m_pGlyphs[nGlyph].shapeOffset; + if (offset == 0 || SetFilePointer(m_hFile, offset, NULL, FILE_BEGIN) == (DWORD)-1) + return false; - VERIFY(ReadFile(m_hFile, &pGlyphShape->m_nVertexCount, 2, &dwRead, NULL)); - pGlyphShape->m_vertices = new CGlyphVertex [pGlyphShape->m_nVertexCount]; + CGlyphShape* pShape = new CGlyphShape; + pShape->indices = NULL; + pShape->points = NULL; - VERIFY(ReadFile(m_hFile, pGlyphShape->m_indices, sizeof (WORD) * pGlyphShape->m_nIndexCount, &dwRead, NULL)); - VERIFY(ReadFile(m_hFile, pGlyphShape->m_vertices, sizeof (CGlyphVertex) * pGlyphShape->m_nVertexCount, &dwRead, NULL)); + bool ok = ReadExact(m_hFile, &pShape->nIndices, 2); + if (ok) + { + pShape->indices = new WORD[pShape->nIndices]; + ok = ReadExact(m_hFile, &pShape->nPoints, 2); + } + if (ok) + { + pShape->points = new XtfPoint[pShape->nPoints]; + ok = ReadExact(m_hFile, pShape->indices, pShape->nIndices * sizeof (WORD)) && + ReadExact(m_hFile, pShape->points, pShape->nPoints * sizeof (XtfPoint)); + } - m_rgGlyphObjects[nGlyphIndex].m_glyphShape = pGlyphShape; - m_rgGlyphLoaded[nGlyphIndex] = true; + if (!ok) + { + FreeShape(pShape); + return false; + } + m_ppShapes[nGlyph] = pShape; return true; } +// CFont::FindGlyphIndex(wch) +// Reconstructed from 5960 retail XBE @ 0x000541c0. Ghidra decompile: +// for (;;) { +// base = 0; +// for (i < set->cRanges) { +// if (r[i].first <= ch && ch < r[i].first + r[i].count) return ch - r[i].first + base; +// base += r[i].count; +// } +// if (ch == 0x7f) return 0; +// ch = 0x7f; // unknown characters draw as the DEL box +// } +// The NULL set check is Theseus's, for a font that failed to open. int CFont::FindGlyphIndex(WCHAR wch) { - int nIndex = 0; + if (m_pSet == NULL) + return -1; - UINT i; - for (i = 0; i < m_glyphSet->cRanges; i += 1) + for (;;) { - if (wch >= m_glyphSet->ranges[i].wcLow && wch < m_glyphSet->ranges[i].wcLow + m_glyphSet->ranges[i].cGlyphs) - return nIndex + wch - m_glyphSet->ranges[i].wcLow; + int base = 0; + for (DWORD i = 0; i < m_pSet->cRanges; i++) + { + const XtfRange& range = m_pSet->ranges[i]; + if (wch >= range.first && wch < range.first + range.count) + return base + (wch - range.first); + base += range.count; + } - nIndex += m_glyphSet->ranges[i].cGlyphs; + if (wch == 0x7f) + return 0; + wch = 0x7f; } +} - if (wch != 127) - return FindGlyphIndex(127); // the invalid char box - return 0; // "invalid char" glyph (it's a space) -} +// ===== Fades =============================================================== -inline float smoothstep(float a, float b, float x) +// Hermite ease on [0, 1], clamped; the binary inlines it as (3 - 2a) * a * a. +static inline float Ease(float a) { - if (x < a) + if (a <= 0.0f) return 0.0f; - - if (x >= b) + if (a >= 1.0f) return 1.0f; - - x = (x - a) / (b - a); - - return (x * x * (3 - 2 * x)); + return a * a * (3.0f - 2.0f * a); } -// Adjust the alpha value for verts whose x is between nStart and nEnd such that they fade -// from opaque to transparent (bRight==true) or transparent to opaque (bRight==false). -static void HorizontalFade(TEXTVERTEX* verts, int nVertexCount, float nStart, float nEnd, bool bRight) +// FadeEdge(verts, n, nStart, nEnd) +// Reconstructed from 5960 retail XBE @ 0x00054290 (HorizontalFade). Ghidra +// decompile, trimmed (verts in EAX, nEnd in XMM1, fade out flag in BL): +// inv = 1.0 / (nEnd - nStart); +// for (i < n) { a = ease((v[i].x - nStart) * inv); if (out) a = 1.0 - a; +// v[i].alpha = (int)(v[i].alpha * a); } +// Text only ever fades out toward the right edge of a column, so this takes +// no direction. Divides where the binary multiplies by the reciprocal. +static void FadeEdge(TEXTVERTEX* verts, int nVerts, float nStart, float nEnd) { - float nWidth = nEnd - nStart; - - int i; - for (i = 0; i < nVertexCount; i += 1) + float width = nEnd - nStart; + for (int i = 0; i < nVerts; i++) { - float a = (verts[i].x - nStart) / nWidth; - a = smoothstep(0.0f, 1.0f, a); - - if (bRight) - a = 1.0f - a; - - float a0 = (float)(verts[i].color >> 24); - verts[i].color = (verts[i].color & 0x00ffffff) | (((DWORD)(a0 * a)) << 24); + float keep = 1.0f - Ease((verts[i].x - nStart) / width); + SetVertexAlpha(verts[i], (float)(verts[i].color >> 24) * keep); } } -inline void FadeLeftEdge(TEXTVERTEX* verts, int nVertexCount, float nStart, float nEnd) -{ - HorizontalFade(verts, nVertexCount, nStart, nEnd, false); -} - -inline void FadeRightEdge(TEXTVERTEX* verts, int nVertexCount, float nStart, float nEnd) -{ - HorizontalFade(verts, nVertexCount, nStart, nEnd, true); -} - -// Window-clip overload. Used by the marquee scroll path: clips per-vertex -// alpha outside [nLeft, nRight] (offset by nScroll for the trailing copy) -// and fades the 1-unit margins on each edge. +// Window clip for the horizontal marquee (Theseus, no 5960 counterpart): +// alpha 0 outside [nLeft, nRight], eased over the last unit on each side. static void HorizontalFade(TEXTVERTEX* verts, int nVertexCount, float nLeft, float nRight, float nScroll) { for (int i = 0; i < nVertexCount; i += 1) @@ -299,168 +354,209 @@ static void HorizontalFade(TEXTVERTEX* verts, int nVertexCount, float nLeft, flo if (a < 0.0f) a = 0.0f; if (a > 1.0f) a = 1.0f; - a = smoothstep(0.0f, 1.0f, a); - verts[i].color = (verts[i].color & 0x00ffffff) | (((DWORD)(255.0f * a)) << 24); + a = Ease(a); + SetVertexAlpha(verts[i], 255.0f * a); } } -static void VerticalFade(TEXTVERTEX* verts, int nVertexCount, float nTop, float nBottom, float nScroll) +// VerticalFade(verts, n, nTop, nBottom, nScroll) +// Reconstructed from 5960 retail XBE @ 0x00054380. Ghidra decompile, trimmed +// (nTop in XMM4, nBottom in XMM5): +// y = v.y + scroll; +// if (y > top || y < bottom) a = 0; +// else if (y > top - 1) a = 1 - (y - (top - 1)); +// else if (y < bottom + 1) a = 1 - ((bottom + 1) - y); +// else a = 1; +// byte at v + 0x1b = (int)(ease(a) * 255.0); +static void VerticalFade(TEXTVERTEX* verts, int nVerts, float nTop, float nBottom, float nScroll) { - for (int i = 0; i < nVertexCount; i += 1) + float innerTop = nTop - 1.0f; + float innerBottom = nBottom + 1.0f; + + for (int i = 0; i < nVerts; i++) { - float a = 1.0f; float y = verts[i].y + nScroll; - + float a = 1.0f; if (y > nTop || y < nBottom) - { a = 0.0f; - } - else if (y > nTop - 1.0f) - { - a = 1.0f - (y - (nTop - 1.0f)); - } - else if (y < nBottom + 1.0f) - { - a = 1.0f - ((nBottom + 1.0f) - y); - } - else //if (y >= nTop + 1.0f && y <= nBottom - 1.0f) - { - a = 1.0f; - } + else if (y > innerTop) + a = 1.0f - (y - innerTop); + else if (y < innerBottom) + a = 1.0f - (innerBottom - y); - ASSERT(a >= 0.0f && a <= 1.0f); - - a = smoothstep(0.0f, 1.0f, a); - verts[i].color = (verts[i].color & 0x00ffffff) | (((DWORD)(255.0f * a)) << 24); + SetVertexAlpha(verts[i], 255.0f * Ease(a)); } } -bool CFont::IsBreakChar(TCHAR ch) + +// ===== Text meshes ========================================================= + +// Characters come in as UTF-16 units. +static inline WCHAR NextChar(const TCHAR* s, int& i, int n) { - // Basically, we will break after double byte or single byte Kana - if (ch == ' ' || (ch >= 0x3040 && ch < 0xF000) || ch >= 0xFF66) - { - return true; - } + (void)n; + return s[i++]; +} - return false; +static inline bool IsLineEnd(TCHAR ch) +{ + return ch == '\t' || ch == '\n' || ch == '\r'; } -extern void TheseusCreateMeshFVF(DWORD NumFaces, DWORD NumVertices, DWORD Options, DWORD FVF, LPD3DXMESH* ppMesh); +// True when text[i] is the last character of its column. +static inline bool AtColumnEnd(const TCHAR* text, int i, int n) +{ + return i == n || IsLineEnd(text[i]); +} + +// A column that starts and ends with '*' is a heading: the stars are hidden +// and the text is tinted. True when text[i] opens one. +static bool OpensHeading(const TCHAR* text, int i, int n) +{ + if (i >= n || text[i] != '*') + return false; + int end = i + 1; + while (end < n && !IsLineEnd(text[end])) + end++; + return end > i + 1 && text[end - 1] == '*'; +} + +// Line breaks may follow a space, kana or CJK (0x3040..0xefff), or half +// width kana and up (0xff66). +static inline bool CanBreakAfter(WCHAR ch) +{ + return ch == ' ' || (ch >= 0x3040 && ch < 0xf000) || ch >= 0xff66; +} +// Glyphs past index 190 come from the Japanese set and are drawn 10% smaller. +// Theseus's; the 5960 layout uses every glyph at full size. +static inline float GlyphScale(int nGlyph) +{ + return nGlyph > 190 ? 0.9f : 1.0f; +} + +// Appends the blinking cursor quad, 0.1 wide from y - 0.1 to y + 0.75. +// Reconstructed from 5960 retail XBE @ 0x00055030 (CFont::CreateCursorMesh): +// a = |sin((now + now) * PI)|; if (!visible) a = 0; color = (int)(a * 255) << 24 | 0xffffff; +// indices += { v, v+1, v+2, v, v+2, v+3 }; +// verts: (x, y-0.1) (x+0.1, y-0.1) (x+0.1, y+0.75) (x, y+0.75), normal (0, 0, 1); +static void AddCursor(WORD* indices, int& nIndex, TEXTVERTEX* verts, int& nVertex, float x, float y, bool visible) +{ + float a = visible ? fabsf(sinf((float)(TheseusGetNow() + TheseusGetNow()) * D3DX_PI)) : 0.0f; + DWORD color = ((DWORD)(a * 255.0f) << 24) | 0x00ffffff; + + static const int quad[6] = { 0, 1, 2, 0, 2, 3 }; + for (int i = 0; i < 6; i++) + indices[nIndex++] = (WORD)(nVertex + quad[i]); + + const float left = x, right = x + 0.1f; + const float bottom = y - 0.1f, top = y + 0.75f; + const float corners[4][2] = { { left, bottom }, { right, bottom }, { right, top }, { left, top } }; + for (int i = 0; i < 4; i++) + { + TEXTVERTEX& v = verts[nVertex++]; + v.x = corners[i][0]; + v.y = corners[i][1]; + v.z = 0.0f; + v.nx = 0.0f; + v.ny = 0.0f; + v.nz = 1.0f; + v.color = color; + } +} + +// CFont::CreateTextMesh(text, nChars, ppMesh, pMin, pMax, width, bDoNotBreak, scale) +// Reconstructed from 5960 retail XBE @ 0x00054440 (3017 bytes). Two passes. +// The first walks each line to size the mesh: it skips *heading* stars, splits +// columns at a tab (unless bDoNotBreak), loads every glyph it meets (a glyph +// that won't load gets its record cleared and is skipped), stops short of +// 0x10000 vertices, and keeps the widest first and second column. A cursor +// inside or at the end of the string adds one quad. With a format width a +// single column takes the whole width; two columns that don't fit squeeze the +// first and fade it. The second pass writes the mesh: headings tint blue to +// 0.5, a glyph that would cross the width wraps back to the last break point +// (or fades if the word has none), the second column is right justified, and +// lines step down 1.5 after a paragraph or 1.0 inside one. Then every vertex +// is scaled and the bounding box taken. See docs/decomp/Text.md. +// 5960 also passes an indent (width, height) the Text node exposes; Theseus +// has no indent props, so lines always restart at x = 0. 5960 only honors +// headings while the keyboard's asterisk formatting flag is on; Theseus always +// does. The bounds stop in the second pass and the glyph scale are Theseus's. void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, D3DXVECTOR3* pMin, D3DXVECTOR3* pMax, float nFormatWidth, bool bDoNotBreak, float scale) { - bool bFade = bDoNotBreak; + if (m_pSet == NULL) + return; if (nChars == -1) - nChars = _tcslen(pchText); + nChars = (int)_tcslen(pchText); - LPD3DXMESH pMesh = NULL; + // Pass 1: size it. int nFaces = 0, nVerts = 0; + float col1Max = 0.0f, col2Max = 0.0f; + bool bSingleColumn = true; + bool bSkipStar = false; int ich = 0; - float nMaxCol1Width = 0.0f; - float nMaxCol2Width = 0.0f; - bool bSingleLine = true; - bool bSingleColumn = true; - bool bAsterisk = false; - int nLine; - int ich2; - for (nLine = 0; ich < nChars; nLine += 1) + while (ich < nChars) { - int nColumn = 1; - float nCol1Width = 0.0f; - float nCol2Width = 0.0f; + int column = 1; + float colWidth[2] = { 0.0f, 0.0f }; - if (pchText[ich] == '*') + if (OpensHeading(pchText, ich, nChars)) { - // See if there's a '*' at the end of the line... - for (ich2 = ich + 1; ich2 < nChars && pchText[ich2] != '\n' && pchText[ich2] != '\r' && pchText[ich2] != '\t'; ich2 += 1) - ; - if (ich2 > ich + 1 && pchText[ich2 - 1] == '*') - { - // Line starts and ends with a *; don't show the *'s and format as a heading - bAsterisk = true; - ich += 1; - } + bSkipStar = true; + ich++; } while (ich < nChars) { - TCHAR ch = pchText[ich]; - ich += 1; - - if (ch == '*') - { - // Ignore this if it's the last visible char in the column... - if (bAsterisk && (ich == nChars || pchText[ich] == '\t' || pchText[ich] == '\n' || pchText[ich] == '\r')) - continue; - } - - if (bDoNotBreak == false && ch == '\r' && ich < nChars && pchText[ich] == '\n') - { - ich += 1; - } + WCHAR ch = NextChar(pchText, ich, nChars); - if (bDoNotBreak == false && (ch == '\n' || ch == '\r')) - { - bSingleLine = false; - break; - } + if (ch == '*' && bSkipStar && AtColumnEnd(pchText, ich, nChars)) + continue; - if (ch == '\t' && bDoNotBreak == false) + if (!bDoNotBreak) { - nColumn = 2; - bSingleColumn = false; - bAsterisk = false; - - if (ich < nChars && pchText[ich] == '*') + if (ch == '\r' && ich < nChars && pchText[ich] == '\n') + ich++; + if (ch == '\n' || ch == '\r') + break; + if (ch == '\t') { - // See if there's a '*' at the end of the line... - for (ich2 = ich + 1; ich2 < nChars && pchText[ich2] != '\n' && pchText[ich2] != '\r' && pchText[ich2] != '\t'; ich2 += 1) - ; - if (ich2 > ich + 1 && pchText[ich2 - 1] == '*') - { - // Column starts and ends with a *; don't show the *'s and format as a heading - ich += 1; - bAsterisk = true; - } + column = 2; + bSingleColumn = false; + bSkipStar = OpensHeading(pchText, ich, nChars); + if (bSkipStar) + ich++; + continue; } - continue; } - int nGlyphIndex = FindGlyphIndex(ch); - if (nGlyphIndex == -1) + int nGlyph = FindGlyphIndex(ch); + if (nGlyph == -1) continue; - - VERIFY(LoadGlyph(nGlyphIndex)); - - CGlyphShape* pGlyphShape = m_rgGlyphObjects[nGlyphIndex].m_glyphShape; - if (!pGlyphShape) + if (!LoadGlyph(nGlyph)) + { + m_pGlyphs[nGlyph].shapeOffset = 0; continue; - pGlyphShape->m_nGlyphIndex = nGlyphIndex; + } - if (nVerts + pGlyphShape->m_nVertexCount > 65535) + CGlyphShape* pShape = m_ppShapes[nGlyph]; + if (nVerts + pShape->nPoints > 0xffff) { - TRACE(_T("CreateTextMesh: too much text!\n")); - nChars = ich - 1; // truncate string + nChars = ich - 1; break; } - if (nColumn == 1) - nCol1Width += m_rgGlyphObjects[nGlyphIndex].m_metrics.gmfCellIncX; - else - nCol2Width += m_rgGlyphObjects[nGlyphIndex].m_metrics.gmfCellIncX; - - nFaces += pGlyphShape->m_nIndexCount / 3; - nVerts += pGlyphShape->m_nVertexCount; + colWidth[column - 1] += Advance(nGlyph); + nFaces += pShape->nIndices / 3; + nVerts += pShape->nPoints; } - if (nCol1Width > nMaxCol1Width) - nMaxCol1Width = nCol1Width; - - if (nCol2Width > nMaxCol2Width) - nMaxCol2Width = nCol2Width; + if (colWidth[0] > col1Max) + col1Max = colWidth[0]; + if (colWidth[1] > col2Max) + col2Max = colWidth[1]; } if (g_nTextChar >= 0 && g_nTextChar <= nChars) @@ -472,276 +568,196 @@ void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, if (nFaces == 0) return; - if (nFormatWidth > 0.0f) + bool bFade = bDoNotBreak; + if (nFormatWidth <= 0.0f) { - if (bSingleColumn) - { - nMaxCol1Width = nFormatWidth; - } - else if (nMaxCol1Width + nMaxCol2Width > nFormatWidth) - { - nMaxCol1Width = nFormatWidth - nMaxCol2Width; - bFade = true; - } + bFade = false; } - else + else if (bSingleColumn) { - bFade = false; + col1Max = nFormatWidth; + } + else if (col1Max + col2Max > nFormatWidth) + { + col1Max = nFormatWidth - col2Max; + bFade = true; } - TheseusCreateMeshFVF(nFaces, nVerts, D3DXMESH_MANAGED, D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE, &pMesh); + LPD3DXMESH pMesh = NULL; + TheseusCreateMeshFVF(nFaces, nVerts, D3DXMESH_MANAGED, TEXT_FVF, &pMesh); *ppMesh = pMesh; WORD* indices; - pMesh->LockIndexBuffer(D3DLOCK_DISCARD, (BYTE**)&indices); - TEXTVERTEX* verts; + pMesh->LockIndexBuffer(D3DLOCK_DISCARD, (BYTE**)&indices); pMesh->LockVertexBuffer(D3DLOCK_DISCARD, (BYTE**)&verts); - float r = 1.0f; - float g = 1.0f; - float b = 1.0f; - bool bTitle = false; - bool bStartPara = true; - int nCurIndex = 0; - int nCurVertex = 0; - float x = 0.0f; - float y = 0.0f; + // Pass 2: fill it. + int nIndex = 0, nVertex = 0; + float x = 0.0f, y = 0.0f; + float blue = 1.0f; + bool bHeading = false; + bool bParaStart = true; + ich = 0; - for (nLine = 0; ich < nChars; nLine += 1) + while (ich < nChars) { - int nColumn = 1; - int nFirstColVertex = nCurVertex; - float nCol2Width = 0.0f; - float nMaxColWidth = nMaxCol1Width; + int column = 1; + int colFirst = nVertex; + float colLimit = col1Max; + float col2Width = 0.0f; - int nBreakChar = -1; - int nBreakVertex = nCurVertex; - int nBreakIndex = nCurIndex; + int wrapChar = -1; + int wrapVertex = nVertex; + int wrapIndex = nIndex; - if (bStartPara && ich < nChars && pchText[ich] == '*') + if (bParaStart && OpensHeading(pchText, ich, nChars)) { - // See if there's a '*' at the end of the line... - for (ich2 = ich + 1; ich2 < nChars && pchText[ich2] != '\n' && pchText[ich2] != '\r' && pchText[ich2] != '\t'; ich2 += 1) - ; - if (ich2 > ich + 1 && pchText[ich2 - 1] == '*') - { - // Line starts and ends with a *; don't show the *'s and format as a heading - r = 1.0f; - g = 1.0f; - b = 0.5f; - bTitle = true; - - ich += 1; - } + blue = 0.5f; + bHeading = true; + ich++; } for (;;) { - TCHAR ch = '\r'; + WCHAR ch = '\r'; if (ich < nChars) { - ch = pchText[ich]; - ich += 1; - + ch = NextChar(pchText, ich, nChars); if (ch == '\r' && ich < nChars && pchText[ich] == '\n') - ich += 1; + ich++; } if (ch == '\n' || ch == '\r') { - if (bDoNotBreak && bFade) - { - FadeRightEdge(verts + nFirstColVertex, nCurVertex - nFirstColVertex, nMaxColWidth - 1, nMaxColWidth); - break; - } + if (bDoNotBreak && bFade) + { + FadeEdge(verts + colFirst, nVertex - colFirst, colLimit - 1.0f, colLimit); + break; + } - if (nColumn == 2) + if (column == 2) { - // Right justify the second column... - for (int i = nFirstColVertex; i < nCurVertex; i += 1) - verts[i].x += nMaxCol2Width - nCol2Width; + float shift = col2Max - col2Width; + for (int i = colFirst; i < nVertex; i++) + verts[i].x += shift; } - else + else if (bFade) { - if (bFade) - FadeRightEdge(verts + nFirstColVertex, nCurVertex - nFirstColVertex, nMaxColWidth - 1, nMaxColWidth); + FadeEdge(verts + colFirst, nVertex - colFirst, colLimit - 1.0f, colLimit); } if (ich != nChars) { x = 0.0f; - if (bTitle || ch == '\r' || !bSingleColumn) - y -= 1.0f; - else - y -= 1.5f; + bool bTight = bHeading || ch == '\r' || !bSingleColumn; + y -= bTight ? 1.0f : 1.5f; } - bTitle = false; - bStartPara = true; + bHeading = false; + bParaStart = true; break; } - if (ch == '\t' && bDoNotBreak == false) + if (ch == '\t' && !bDoNotBreak) { if (bFade) - FadeRightEdge(verts + nFirstColVertex, nCurVertex - nFirstColVertex, nMaxColWidth - 2, nMaxColWidth); + FadeEdge(verts + colFirst, nVertex - colFirst, colLimit - 2.0f, colLimit); - nColumn = 2; - x = nFormatWidth - nMaxCol2Width; - nFirstColVertex = nCurVertex; - nMaxColWidth = nMaxCol2Width; - bTitle = false; - bStartPara = true; + column = 2; + x = nFormatWidth - col2Max; + colFirst = nVertex; + colLimit = col2Max; + bHeading = false; + bParaStart = true; - if (ich < nChars && pchText[ich] == '*') + if (OpensHeading(pchText, ich, nChars)) { - // See if there's a '*' at the end of the line... - for (ich2 = ich + 1; ich2 < nChars && pchText[ich2] != '\n' && pchText[ich2] != '\r' && pchText[ich2] != '\t'; ich2 += 1) - ; - if (ich2 > ich + 1 && pchText[ich2 - 1] == '*') - { - // Line starts and ends with a *; don't show the *'s and format as a heading - r = 1.0f; - g = 1.0f; - b = 0.5f; - bTitle = true; - - ich += 1; - } + blue = 0.5f; + bHeading = true; + ich++; } - continue; } - if (bTitle && ch == '*') + if (bHeading && ch == '*' && AtColumnEnd(pchText, ich, nChars)) { - // Ignore this if it's the last visible char in the column... - if (ich == nChars || pchText[ich] == '\t' || pchText[ich] == '\n' || pchText[ich] == '\r') - { - - r = 1.0f; - g = 1.0f; - b = 1.0f; - continue; - } + blue = 1.0f; + continue; } - bStartPara = false; + bParaStart = false; - int nGlyphIndex = FindGlyphIndex(ch); - if (nGlyphIndex == -1) + int nGlyph = FindGlyphIndex(ch); + if (nGlyph == -1) continue; - ASSERT(m_rgGlyphLoaded[nGlyphIndex]); - - if (nFormatWidth > 0.0f) + if (nFormatWidth > 0.0f && !bDoNotBreak && ch != ' ' && column == 1 && + x + Advance(nGlyph) > nFormatWidth) { - // bDoNotBreak path historically had an unused width - // clamp here that never made it past the prototype. - // All formatting work happens in the wrapping branch. - if (!bDoNotBreak) + if (wrapChar > -1) { - if (ch != ' ' && nColumn == 1 && x + m_rgGlyphObjects[nGlyphIndex].m_metrics.gmfCellIncX > nFormatWidth) - { - - // break line - - if (nBreakChar > -1) - { - x = 0.0f; - y -= 1.0f; - - ich = nBreakChar /*- 1*/; - nCurVertex = nBreakVertex; - nCurIndex = nBreakIndex; - - nBreakChar = -1; - nBreakVertex = nCurVertex; - nBreakIndex = nCurIndex; - - continue; - } - else - { - // one word was too long to fit, fade it! - bFade = true; - } - } + x = 0.0f; + y -= 1.0f; + ich = wrapChar; + nVertex = wrapVertex; + nIndex = wrapIndex; + wrapChar = -1; + continue; } + bFade = true; } - CGlyphShape* pGlyphShape = m_rgGlyphObjects[nGlyphIndex].m_glyphShape; - if (!pGlyphShape) + CGlyphShape* pShape = m_ppShapes[nGlyph]; + if (pShape == NULL) continue; - // The mesh was sized by the counting pass. If this pass ever walks - // the string differently it would write straight past the buffers, - // so stop at the edge and drop the rest of the line. - if (nCurVertex + pGlyphShape->m_nVertexCount > nVerts || - nCurIndex + pGlyphShape->m_nIndexCount > nFaces * 3) + // Theseus: pass 1 sized the buffers. If this pass ever walks the + // string differently, drop the rest rather than write past them. + if (nVertex + pShape->nPoints > nVerts || nIndex + pShape->nIndices > nFaces * 3) { nChars = ich - 1; break; } - int j; - for (j = 0; j < pGlyphShape->m_nIndexCount; j += 1) - indices[nCurIndex++] = nCurVertex + pGlyphShape->m_indices[j]; + for (int i = 0; i < pShape->nIndices; i++) + indices[nIndex++] = (WORD)(nVertex + pShape->indices[i]); - float fontAdjust = 1.0f; - - if(pGlyphShape->m_nGlyphIndex > 190) //character is in the japanese font set + float glyphScale = GlyphScale(nGlyph); + DWORD color = 0xffffff00 | (DWORD)(blue * 255.0f); + for (int i = 0; i < pShape->nPoints; i++) { - fontAdjust = 0.9f; - } - - for (j = 0; j < pGlyphShape->m_nVertexCount; j += 1) - { - verts[nCurVertex].x = x + (pGlyphShape->m_vertices[j].x * fontAdjust); - verts[nCurVertex].y = y + (pGlyphShape->m_vertices[j].y * fontAdjust); - verts[nCurVertex].z = 0.0f; - - verts[nCurVertex].nx = 0.0f; - verts[nCurVertex].ny = 0.0f; - verts[nCurVertex].nz = 1.0f; - - verts[nCurVertex].color = D3DCOLOR_COLORVALUE(r, g, b, 1.0f); - - nCurVertex += 1; + TEXTVERTEX& v = verts[nVertex++]; + v.x = x + pShape->points[i].x * glyphScale; + v.y = y + pShape->points[i].y * glyphScale; + v.z = 0.0f; + v.nx = 0.0f; + v.ny = 0.0f; + v.nz = 1.0f; + v.color = color; } if (ich - 1 == g_nTextChar) - { - CreateCursorMesh(indices, nCurIndex, verts, nCurVertex, x, y, true); - } - - x += (m_rgGlyphObjects[nGlyphIndex].m_metrics.gmfCellIncX * fontAdjust); + AddCursor(indices, nIndex, verts, nVertex, x, y, true); - if (nColumn == 2) - nCol2Width += m_rgGlyphObjects[nGlyphIndex].m_metrics.gmfCellIncX; + x += Advance(nGlyph) * glyphScale; + if (column == 2) + col2Width += Advance(nGlyph); - if (IsBreakChar(ch)) + if (CanBreakAfter(ch)) { - nBreakChar = ich; - nBreakVertex = nCurVertex; - nBreakIndex = nCurIndex; + wrapChar = ich; + wrapVertex = nVertex; + wrapIndex = nIndex; } } } + // The cursor past the last character hides once it would leave the + // keyboard's edit box (16.556152 units, 0x0002c1b4). if (g_nTextChar == nChars) - { - bool visible = false; - if(x < 16.556152f) //value to keep the cursor from extending past the keyboard blank - visible = true; - - CreateCursorMesh(indices, nCurIndex, verts, nCurVertex, x, y, visible); - } + AddCursor(indices, nIndex, verts, nVertex, x, y, x < 16.556152f); - ASSERT(nCurVertex == nVerts); - ASSERT(nCurIndex == nFaces * 3); - - for (int i = 0; i < nVerts; i += 1) + for (int i = 0; i < nVerts; i++) { verts[i].x *= scale; verts[i].y *= scale; @@ -749,263 +765,176 @@ void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, } if (pMin != NULL && pMax != NULL) - VERIFYHR(D3DXComputeBoundingBox(verts, nCurVertex, D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE, pMin, pMax)); - + D3DXComputeBoundingBox(verts, nVertex, TEXT_FVF, pMin, pMax); + pMesh->UnlockIndexBuffer(); pMesh->UnlockVertexBuffer(); } -void CFont::CreateCursorMesh(WORD*& indices, int& nCurIndex, TEXTVERTEX*& verts, int& nCurVertex, float x, float y, bool visible) -{ - float t = (float) (TheseusGetNow()) * 2.0f; - float a = fabsf(sinf(t * D3DX_PI)); - if(!visible) - a = 0.0f; - - indices[nCurIndex++] = (WORD)nCurVertex; - indices[nCurIndex++] = (WORD)nCurVertex + 1; - indices[nCurIndex++] = (WORD)nCurVertex + 2; - - indices[nCurIndex++] = (WORD)nCurVertex; - indices[nCurIndex++] = (WORD)nCurVertex + 2; - indices[nCurIndex++] = (WORD)nCurVertex + 3; - - verts[nCurVertex].x = x; - verts[nCurVertex].y = y - 0.1f; - verts[nCurVertex].z = 0.0f; - - verts[nCurVertex].nx = 0.0f; - verts[nCurVertex].ny = 0.0f; - verts[nCurVertex].nz = 1.0f; - - verts[nCurVertex].color = D3DCOLOR_COLORVALUE(1.0f, 1.0f, 1.0f, a); - - nCurVertex += 1; - - verts[nCurVertex].x = x + 0.1f; - verts[nCurVertex].y = y - 0.1f; - verts[nCurVertex].z = 0.0f; - - verts[nCurVertex].nx = 0.0f; - verts[nCurVertex].ny = 0.0f; - verts[nCurVertex].nz = 1.0f; - - verts[nCurVertex].color = D3DCOLOR_COLORVALUE(1.0f, 1.0f, 1.0f, a); - - nCurVertex += 1; - - verts[nCurVertex].x = x + 0.1f; - verts[nCurVertex].y = y + 0.75f; - verts[nCurVertex].z = 0.0f; - - verts[nCurVertex].nx = 0.0f; - verts[nCurVertex].ny = 0.0f; - verts[nCurVertex].nz = 1.0f; - - verts[nCurVertex].color = D3DCOLOR_COLORVALUE(1.0f, 1.0f, 1.0f, a); - - nCurVertex += 1; - - verts[nCurVertex].x = x; - verts[nCurVertex].y = y + 0.75f; - verts[nCurVertex].z = 0.0f; - - verts[nCurVertex].nx = 0.0f; - verts[nCurVertex].ny = 0.0f; - verts[nCurVertex].nz = 1.0f; - - verts[nCurVertex].color = D3DCOLOR_COLORVALUE(1.0f, 1.0f, 1.0f, a); - - nCurVertex += 1; -} - +// ===== Font table ========================================================== +// Ten slots, filled from the *.xtf files in the font directory. Slot 0 is the +// fallback for a face nobody has. struct CFontTableEntry { - CFontTableEntry() - { - m_faceName = NULL; - m_fileName = NULL; - } - - CFontTableEntry(const TCHAR* szFaceName, const TCHAR* szFileName) - { - m_faceName = szFaceName; - m_fileName = szFileName; - } - - const TCHAR* m_faceName; - const TCHAR* m_fileName; + const TCHAR* m_szFace; + const TCHAR* m_szFile; CFont m_font; }; #define MAX_FONT_COUNT 10 -CFontTableEntry g_fonts [MAX_FONT_COUNT] = +static CFontTableEntry g_fonts[MAX_FONT_COUNT] = { - // NOTE: The first font is used as the default when a specified font is not found! - CFontTableEntry(_T("HGGothicM"), _T("HGGothicM.xtf")), + { _T("HGGothicM"), _T("HGGothicM.xtf") }, }; -int g_nFontCount = 1; +static int g_nFontCount = 1; -void InitFontTable() +static TCHAR* CopyString(const TCHAR* sz) { - TCHAR szWild [MAX_PATH]; - WIN32_FIND_DATA fd; - HANDLE h; + TCHAR* copy = new TCHAR[_tcslen(sz) + 1]; + _tcscpy(copy, sz); + return copy; +} - int cchAppDir = _tcslen(TheseusGetFontDir()); +// InitFontTable() +// Reconstructed from 5960 retail XBE @ 0x000552b0. Ghidra decompile, trimmed: +// FindFirstFileA("fonts\*.xtf"), falling back to "*.xtf"; +// for each file, while count < 10: +// open; read magic (XTF0 or XTF1), header length (must be 0x20), face (0x20); +// face = widen(face); if (wcscmp(face, fonts[0].face) == 0) free(face); +// else { fonts[count].face = face; fonts[count].file = copy(path); count++; } +// CloseHandle; +// Theseus looks in its one font directory and takes XTF0 only, since that's +// all CFont reads. A bad header still closes the file (5960 leaks it). +static void InitFontTable() +{ + const TCHAR* szDir = TheseusGetFontDir(); + int cchDir = (int)_tcslen(szDir); - _tcscpy(szWild, TheseusGetFontDir()); - _tcscpy(szWild + cchAppDir, _T("*.xtf")); + TCHAR szPath[MAX_PATH]; + _tcscpy(szPath, szDir); + _tcscpy(szPath + cchDir, _T("*.xtf")); -#if defined(_XBOX) - char szWildA [MAX_PATH]; - Ansi(szWildA, szWild, MAX_PATH); - h = TheseusFindFirstFile(szWildA, &fd); + WIN32_FIND_DATA fd; +#ifdef _UNICODE + char szPathA[MAX_PATH]; + Ansi(szPathA, szPath, MAX_PATH); + HANDLE hFind = TheseusFindFirstFile(szPathA, &fd); #else - h = TheseusFindFirstFile(szWild, &fd); + HANDLE hFind = TheseusFindFirstFile(szPath, &fd); #endif - - if (h == INVALID_HANDLE_VALUE) - { - TRACE(_T("\001InitFontTable: No fonts!\n")); + if (hFind == INVALID_HANDLE_VALUE) return; - } do { - TCHAR szFileName [MAX_PATH]; - - _tcscpy(szFileName, TheseusGetFontDir()); + if (g_nFontCount >= MAX_FONT_COUNT) + continue; -#if defined(_XBOX) - Unicode(szFileName + cchAppDir, fd.cFileName, countof(szFileName)); +#ifdef _UNICODE + Unicode(szPath + cchDir, fd.cFileName, MAX_PATH - cchDir); #else - _tcscpy(szFileName + cchAppDir, fd.cFileName); + _tcscpy(szPath + cchDir, fd.cFileName); #endif - if (g_nFontCount >= MAX_FONT_COUNT) - { - TRACE(_T("\001InitFontTable: too many fonts!\n")); + HANDLE hFile = TheseusCreateFile(szPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + if (hFile == INVALID_HANDLE_VALUE) continue; - } - HANDLE hFont = TheseusCreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); - if (hFont != INVALID_HANDLE_VALUE) + DWORD dwMagic = 0, cbHeader = 0; + char faceA[XTF_FACE_LEN + 1]; + if (ReadExact(hFile, &dwMagic, 4) && dwMagic == XTF0_MAGIC && + ReadExact(hFile, &cbHeader, 4) && cbHeader == XTF_FACE_LEN && + ReadExact(hFile, faceA, XTF_FACE_LEN)) { - DWORD dwRead; - DWORD cbGlyphSet; - ReadFile(hFont, &cbGlyphSet, 4, &dwRead, NULL); - - if (cbGlyphSet == 0x30465458) - { - DWORD dwHeaderLen; - ReadFile(hFont, &dwHeaderLen, 4, &dwRead, NULL); - - ASSERT(dwHeaderLen == LF_FACESIZE); - - char szFaceA [LF_FACESIZE]; - ReadFile(hFont, szFaceA, LF_FACESIZE, &dwRead, NULL); + faceA[XTF_FACE_LEN] = 0; - TCHAR* szFace = new TCHAR [strlen(szFaceA) + 1]; + TCHAR szFace[XTF_FACE_LEN + 1]; #ifdef _UNICODE - Unicode(szFace, szFaceA, LF_FACESIZE); + Unicode(szFace, faceA, XTF_FACE_LEN + 1); #else - _tcscpy(szFace, szFaceA); + _tcscpy(szFace, faceA); #endif - - // Don't add the default font twice! - if (_tcscmp(szFace, g_fonts[0].m_faceName) == 0) - { - delete [] szFace; - } - else - { - TCHAR* szFile = new TCHAR [_tcslen(szFileName) + 1]; - _tcscpy(szFile, szFileName); - - g_fonts[g_nFontCount].m_faceName = szFace; - g_fonts[g_nFontCount].m_fileName = szFile; - g_nFontCount += 1; - - TRACE(_T("Adding font: %s (%s)\n"), szFace, szFile); - } + if (_tcscmp(szFace, g_fonts[0].m_szFace) != 0) + { + g_fonts[g_nFontCount].m_szFace = CopyString(szFace); + g_fonts[g_nFontCount].m_szFile = CopyString(szPath); + g_nFontCount++; } - - CloseHandle(hFont); } + + CloseHandle(hFile); } - while (TheseusFindNextFile(h, &fd)); + while (TheseusFindNextFile(hFind, &fd)); - TheseusFindClose(h); + TheseusFindClose(hFind); } +// GetFont(szFaceName) +// Reconstructed from 5960 retail XBE @ 0x00055640. Ghidra decompile, trimmed: +// if (count == 1) InitFontTable(); +// for (i < count) if (!_wcsicmp(face, fonts[i].face)) break; +// if (i == count) i = 0; +// if (fonts[i].font.hFile == -1) { +// path = wcschr(file, ':') ? file : swprintf("%s%s", appdir, file); +// if (!Open(path) && fonts[0].hFile == -1 && !Open(fonts[0].file)) return NULL; +// } +// return &fonts[i].font; +// Theseus builds the path from its font directory instead of the app dir. CFont* GetFont(const TCHAR* szFaceName) { if (g_nFontCount == 1) InitFontTable(); - int i; - for (i = 0; i < g_nFontCount; i += 1) - { - if (_tcsicmp(szFaceName, g_fonts[i].m_faceName) == 0) - break; - } - + int i = 0; + while (i < g_nFontCount && _tcsicmp(szFaceName, g_fonts[i].m_szFace) != 0) + i++; if (i == g_nFontCount) - { - TRACE(_T("Substituting font '%s' for '%s'\n"), g_fonts[0].m_faceName, szFaceName); i = 0; - } - if (g_fonts[i].m_font.m_hFile == INVALID_HANDLE_VALUE) + CFontTableEntry& entry = g_fonts[i]; + if (entry.m_font.m_hFile == INVALID_HANDLE_VALUE) { - TCHAR szFontPath [MAX_PATH]; - - if (_tcschr(g_fonts[i].m_fileName, ':') == NULL) - _stprintf(szFontPath, _T("%s%s"), TheseusGetFontDir(), g_fonts[i].m_fileName); + TCHAR szPath[MAX_PATH]; + if (_tcschr(entry.m_szFile, ':') != NULL) + _tcscpy(szPath, entry.m_szFile); else - _tcscpy(szFontPath, g_fonts[i].m_fileName); - - if (!g_fonts[i].m_font.Open(szFontPath)) - { - TRACE(_T("\001Cannot load font: %s\n"), szFaceName); + _stprintf(szPath, _T("%s%s"), TheseusGetFontDir(), entry.m_szFile); - if (g_fonts[0].m_font.m_hFile == INVALID_HANDLE_VALUE) - { - if (!g_fonts[0].m_font.Open(g_fonts[0].m_fileName)) - { - ASSERT(FALSE); - return NULL; - } - } - } + if (!entry.m_font.Open(szPath) && + g_fonts[0].m_font.m_hFile == INVALID_HANDLE_VALUE && + !g_fonts[0].m_font.Open(g_fonts[0].m_szFile)) + return NULL; } - return &g_fonts[i].m_font; + return &entry.m_font; } +// Text_Exit() +// Reconstructed from 5960 retail XBE @ 0x00055750: every slot past the first +// frees its face and file strings, then each font closes. void Text_Exit() { - for (int i = 0; i < g_nFontCount; i += 1) + for (int i = 0; i < g_nFontCount; i++) { if (i > 0) { - delete [] (TCHAR*)g_fonts[i].m_faceName; - delete [] (TCHAR*)g_fonts[i].m_fileName; - g_fonts[i].m_faceName = NULL; - g_fonts[i].m_fileName = NULL; + delete [] (TCHAR*)g_fonts[i].m_szFace; + delete [] (TCHAR*)g_fonts[i].m_szFile; + g_fonts[i].m_szFace = NULL; + g_fonts[i].m_szFile = NULL; } g_fonts[i].m_font.Close(); } } -// Overlay text API - renders .xtf font glyphs as screen-space triangles - +// ===== Overlay text (Theseus) ============================================== +// Draws "Xbox" face glyphs as screen space triangles for the panic screen, +// the widgets and the overlay menus. No 5960 counterpart. struct OverlayTextVert { @@ -1033,7 +962,7 @@ float OverlayFontMeasure(const TCHAR* text, float pixelHeight) { int idx = pFont->FindGlyphIndex(*text); if (idx >= 0) - width += pFont->m_rgGlyphObjects[idx].m_metrics.gmfCellIncX; + width += pFont->Advance(idx); text++; } @@ -1061,46 +990,43 @@ float OverlayFontDraw(const TCHAR* text, float screenX, float screenY, float pix if (glyphIdx < 0) continue; - if (!pFont->m_rgGlyphLoaded[glyphIdx]) - pFont->LoadGlyph(glyphIdx); + pFont->LoadGlyph(glyphIdx); - CGlyphShape* shape = pFont->m_rgGlyphObjects[glyphIdx].m_glyphShape; + CGlyphShape* shape = pFont->Shape(glyphIdx); if (!shape) { - curX += pFont->m_rgGlyphObjects[glyphIdx].m_metrics.gmfCellIncX; + curX += pFont->Advance(glyphIdx); continue; } // Flush if we'd exceed buffer limits - if (nVerts + shape->m_nVertexCount > MAX_OVL_VERTS || - nIndices + shape->m_nIndexCount > MAX_OVL_INDICES) + if (nVerts + shape->nPoints > MAX_OVL_VERTS || + nIndices + shape->nIndices > MAX_OVL_INDICES) { dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); Frame_Count(FC_DRAW_TEXT); - Frame_PhaseBegin(FP_UP_DRAW); - dev->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, nVerts, nIndices / 3, + Frame_PhaseBegin(FP_UP_DRAW); + dev->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, nVerts, nIndices / 3, s_ovlIndices, D3DFMT_INDEX16, s_ovlVerts, sizeof(OverlayTextVert)); - Frame_PhaseEnd(FP_UP_DRAW); + Frame_PhaseEnd(FP_UP_DRAW); nVerts = 0; nIndices = 0; } - // Add indices (offset by current vertex count) - for (int i = 0; i < shape->m_nIndexCount; i++) - s_ovlIndices[nIndices++] = (WORD)(nVerts + shape->m_indices[i]); + for (int i = 0; i < shape->nIndices; i++) + s_ovlIndices[nIndices++] = (WORD)(nVerts + shape->indices[i]); - // Transform glyph vertices to screen space - for (int j = 0; j < shape->m_nVertexCount; j++) + for (int j = 0; j < shape->nPoints; j++) { - s_ovlVerts[nVerts].x = screenX + (curX + shape->m_vertices[j].x) * scale; - s_ovlVerts[nVerts].y = screenY + scale - shape->m_vertices[j].y * scale; + s_ovlVerts[nVerts].x = screenX + (curX + shape->points[j].x) * scale; + s_ovlVerts[nVerts].y = screenY + scale - shape->points[j].y * scale; s_ovlVerts[nVerts].z = 0.0f; s_ovlVerts[nVerts].rhw = 1.0f; s_ovlVerts[nVerts].color = color; nVerts++; } - curX += pFont->m_rgGlyphObjects[glyphIdx].m_metrics.gmfCellIncX; + curX += pFont->Advance(glyphIdx); } // Final flush @@ -1118,6 +1044,12 @@ float OverlayFontDraw(const TCHAR* text, float screenX, float screenY, float pix } +// ===== CTextNode =========================================================== +// 0x78 bytes in 5960: text 0x1c, font 0x20, justify 0x24, deviation 0x28, +// extrusion 0x2c, width 0x30, translate 0x34, height 0x38, scroll 0x3c, +// scrollRate 0x40, scrollDelay 0x44, indentWidth 0x48, indentHeight 0x4c, +// mesh 0x50, bbox 0x54..0x6b, language 0x6c, scroll start time 0x70. +// Theseus has scrollHorizontal where 5960 has the two indent props. class CTextNode : public CNode { @@ -1141,17 +1073,21 @@ class CTextNode : public CNode float m_scroll; float m_scrollRate; float m_scrollDelay; - bool m_scrollHorizontal; + bool m_scrollHorizontal; DECLARE_NODE_PROPS() - LPD3DXMESH m_mesh; + LPD3DXMESH m_pMesh; D3DXVECTOR3 m_bboxMin, m_bboxMax; int m_nLanguage; XTIME m_timeToScroll; + +private: + void DropMesh(); }; -static const float nScrollSpace = 1.5f; +// Gap between the end of a scrolling text and its repeat. +static const float SCROLL_GAP = 1.5f; IMPLEMENT_NODE("Text", CTextNode, CNode) @@ -1170,23 +1106,27 @@ START_NODE_PROPS(CTextNode, CNode) NODE_PROP(pt_boolean, CTextNode, scrollHorizontal) END_NODE_PROPS() +// CTextNode::CTextNode() / ~CTextNode() +// Reconstructed from 5960 retail XBE @ 0x000557e0 and 0x00055880: +// deviation = 0.01; extrusion = 1.0; translate = 1; everything else 0; +// ~: free text, font, justify; if (mesh) mesh->Release(); CTextNode::CTextNode() : m_text(NULL), m_font(NULL), m_justify(NULL), m_deviation(0.01f), m_extrusion(1.0f), - m_translate(true), m_width(0.0f), + m_translate(true), m_height(0.0f), m_scroll(0.0f), m_scrollRate(0.0f), m_scrollDelay(0.0f), - m_scrollHorizontal(false) + m_scrollHorizontal(false), + m_pMesh(NULL), + m_nLanguage(0), + m_timeToScroll(0.0) { - m_mesh = NULL; - m_nLanguage = 0; - m_timeToScroll = 0.0f; } CTextNode::~CTextNode() @@ -1194,295 +1134,309 @@ CTextNode::~CTextNode() delete [] m_text; delete [] m_font; delete [] m_justify; + DropMesh(); +} - if (m_mesh != NULL) - m_mesh->Release(); +void CTextNode::DropMesh() +{ + if (m_pMesh != NULL) + { + m_pMesh->Release(); + m_pMesh = NULL; + } } +// CTextNode::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x00055900. Ghidra decompile: +// CNode::Advance(t); +// if (scrollRate > 0 && mesh) { +// if (timeToScroll == 0) timeToScroll = scrollDelay + now; +// h = bbox.max.y - bbox.min.y; +// if (timeToScroll <= now && h > height) { +// scroll = height * t * scrollRate + scroll; +// if (scroll >= h + 1.5) scroll -= h + 1.5; +// } +// } +// The horizontal marquee is Theseus's; it steps by the window width instead. void CTextNode::Advance(float nSeconds) { CNode::Advance(nSeconds); - if (m_scrollRate > 0.0f && m_mesh != NULL) - { - XTIME now = TheseusGetNow(); - - if (m_timeToScroll == 0.0f) - m_timeToScroll = now + m_scrollDelay; + if (m_scrollRate <= 0.0f || m_pMesh == NULL) + return; - if (now >= m_timeToScroll) - { - if (m_scrollHorizontal) - { - float nContentWidth = m_bboxMax.x - m_bboxMin.x; - float windowW = fabsf(m_width); - if (windowW > 0.0f && nContentWidth > windowW) - { - m_scroll += windowW * nSeconds * m_scrollRate; - if (m_scroll >= nContentWidth + nScrollSpace) - m_scroll -= nContentWidth + nScrollSpace; - } - } - else - { - float nContentHeight = m_bboxMax.y - m_bboxMin.y; - if (nContentHeight > m_height) - { - m_scroll += m_height * nSeconds * m_scrollRate; - if (m_scroll >= nContentHeight + nScrollSpace) - m_scroll -= nContentHeight + nScrollSpace; - } - } - } - } -} + XTIME now = TheseusGetNow(); + if (m_timeToScroll == 0.0) + m_timeToScroll = now + m_scrollDelay; + if (now < m_timeToScroll) + return; -const TCHAR* FaceFromFont(const TCHAR* szFont) -{ - const TCHAR* szFace = szFont; -/* - if (nLanguage == LANGUAGE_JAPANESE) + float window, content; + if (m_scrollHorizontal) { - if (szFont == NULL || _tcsicmp(szFont, _T("body")) == 0) - szFace = _T("HGSSoEikakuGothicB"); - else if (_tcsicmp(szFont, _T("heading")) == 0) - szFace = _T("HGMaroGothicMPRO"); + window = fabsf(m_width); + content = m_bboxMax.x - m_bboxMin.x; + if (window <= 0.0f) + return; } else -*/ { - if (szFont == NULL || _tcsicmp(szFont, _T("body")) == 0) - szFace = _T("XBox Book"); - else if (_tcsicmp(szFont, _T("heading")) == 0) - szFace = _T("Xbox"); + window = m_height; + content = m_bboxMax.y - m_bboxMin.y; } - return szFace; + if (content > window) + { + m_scroll += window * nSeconds * m_scrollRate; + if (m_scroll >= content + SCROLL_GAP) + m_scroll -= content + SCROLL_GAP; + } } +// FaceFromFont(szFont) +// Reconstructed from 5960 retail XBE @ 0x000559a0: +// if (!font || !_wcsicmp(font, "body")) return "XBox Book"; +// if (!_wcsicmp(font, "heading")) return "Xbox"; +// return font; +const TCHAR* FaceFromFont(const TCHAR* szFont) +{ + if (szFont == NULL || _tcsicmp(szFont, _T("body")) == 0) + return _T("XBox Book"); + if (_tcsicmp(szFont, _T("heading")) == 0) + return _T("Xbox"); + return szFont; +} + +// CTextNode::Render() +// Reconstructed from 5960 retail XBE @ 0x000559e0 (1331 bytes). Takes the +// keyboard's string if one is waiting (a changed string or cursor rebuilds), +// expands , rebuilds on a language change, then builds the mesh with +// the translated text, |width| as the format width and a negative width as +// "don't wrap". Justify middle or end shifts left by half or all of the drawn +// width (capped at |width|). With a height smaller than the text the mesh +// fades top and bottom and draws twice, the second copy one gap below so the +// scroll wraps around. Each draw pushes the world matrix, translates, turns +// culling off for the flat glyphs and back to CCW. See docs/decomp/Text.md. +// 5960 also expands , passes the indent props and rebuilds every frame +// a cursorless keyboard string arrives; Theseus does none of those. The +// horizontal marquee and the draw counter are Theseus's. void CTextNode::Render() { - bool bInvalidMesh = false; + bool bRebuild = false; if (g_szText != NULL) { if (m_text == NULL || _tcscmp(m_text, g_szText) != 0 || g_nTextChar != g_nTextCharLast) { delete [] m_text; - m_text = new TCHAR [_tcslen(g_szText) + 1]; - _tcscpy(m_text, g_szText); + m_text = CopyString(g_szText); g_nTextCharLast = g_nTextChar; - bInvalidMesh = true; + bRebuild = true; } - g_szText = NULL; } - const TCHAR* szText = m_text; - if (szText == NULL || szText[0] == 0) + if (m_text == NULL || m_text[0] == 0) return; - if (_tcsncmp(m_text, _T(""), 7) == 0) + static const TCHAR szClock[] = _T(""); + const int cchClock = 7; + const TCHAR* szText = m_text; + if (_tcsncmp(m_text, szClock, cchClock) == 0) { SYSTEMTIME st; GetLocalTime(&st); + TCHAR szTime[32]; + FormatTime(szTime, countof(szTime), &st); - TCHAR szBuf [32]; - FormatTime(szBuf, countof(szBuf), &st); - - if (_tcscmp(m_text + 7, szBuf) != 0) + if (_tcscmp(m_text + cchClock, szTime) != 0) { delete [] m_text; - m_text = new TCHAR [7 + _tcslen(szBuf) + 1]; - _stprintf(m_text, _T("%s"), szBuf); - - bInvalidMesh = true; + m_text = new TCHAR[cchClock + _tcslen(szTime) + 1]; + _tcscpy(m_text, szClock); + _tcscpy(m_text + cchClock, szTime); + bRebuild = true; } m_translate = false; - szText = m_text + 7; // look past the + szText = m_text + cchClock; } if (m_translate && m_nLanguage != g_nCurLanguage) - bInvalidMesh = true; + bRebuild = true; - if (m_mesh != NULL && bInvalidMesh) - { - m_mesh->Release(); - m_mesh = NULL; - } + if (bRebuild) + DropMesh(); - if (m_mesh == NULL) + if (m_pMesh == NULL) { - TCHAR sz[MAX_TRANSLATE_LEN]; - + TCHAR szTranslated[MAX_TRANSLATE_LEN]; if (m_translate) - szText = Translate(szText, sz); - + szText = Translate(szText, szTranslated); m_nLanguage = g_nCurLanguage; CFont* pFont = GetFont(FaceFromFont(m_font)); - pFont->CreateTextMesh(szText, -1, &m_mesh, &m_bboxMin, &m_bboxMax, fabsf(m_width), m_width < 0.0f, g_nCurLanguage == LANGUAGE_JAPANESE ? 1.0f : 1.0f); - if (m_mesh == NULL) + if (pFont == NULL || !pFont->IsOpen()) + return; + pFont->CreateTextMesh(szText, -1, &m_pMesh, &m_bboxMin, &m_bboxMax, fabsf(m_width), m_width < 0.0f, 1.0f); + if (m_pMesh == NULL) return; } - float nContentHeight = m_bboxMax.y - m_bboxMin.y; + float contentW = m_bboxMax.x - m_bboxMin.x; + float contentH = m_bboxMax.y - m_bboxMin.y; + float window = fabsf(m_width); float xOffset = 0.0f; if (m_justify != NULL) { - float nWidth = m_bboxMax.x - m_bboxMin.x; - if (m_width != 0.0f) - { - float widthLimit = fabsf(m_width); - if (nWidth > widthLimit) - nWidth = widthLimit; - } + float drawn = contentW; + if (m_width != 0.0f && drawn > window) + drawn = window; if (_tcsicmp(m_justify, _T("middle")) == 0) - { - // Center justify. The original prototype was going to fade - // both edges when the text overflowed a negative-signed - // width clamp; never made it past the wishlist stage. - xOffset = -nWidth / 2.0f; - } + xOffset = -drawn / 2.0f; else if (_tcsicmp(m_justify, _T("end")) == 0) - { - // End / right justify. Same fade-left-side wish never - // landed. - xOffset = -nWidth; - } + xOffset = -drawn; } - float nContentWidth = m_bboxMax.x - m_bboxMin.x; - float windowW = fabsf(m_width); - bool bHScroll = m_scrollHorizontal && windowW > 0.0f && nContentWidth > windowW; + bool bMarquee = m_scrollHorizontal && window > 0.0f && contentW > window; + bool bVScroll = !bMarquee && m_height != 0.0f && contentH > m_height; - for (int i = 0; i < 2; i += 1) + for (int copy = 0; copy < 2; copy++) { - if (bHScroll) + if (bMarquee) { + float wrap = copy == 0 ? 0.0f : contentW + SCROLL_GAP; TEXTVERTEX* verts; - m_mesh->LockVertexBuffer(0, (BYTE**)&verts); - float xLeft = m_scroll - 1.0f; - float xRight = xLeft + windowW + 2.0f; - HorizontalFade(verts, m_mesh->GetNumVertices(), xLeft, xRight, i == 0 ? 0 : (nContentWidth + nScrollSpace)); - m_mesh->UnlockVertexBuffer(); + m_pMesh->LockVertexBuffer(0, (BYTE**)&verts); + float left = m_scroll - 1.0f; + HorizontalFade(verts, m_pMesh->GetNumVertices(), left, left + window + 2.0f, wrap); + m_pMesh->UnlockVertexBuffer(); + + if (copy == 1 && m_scroll + window < contentW) + break; } - else if (m_height != 0.0f && nContentHeight > m_height) + else if (bVScroll) { - TEXTVERTEX* verts; - m_mesh->LockVertexBuffer(0, (BYTE**)&verts); - float yTop = -m_scroll + 1.0f; - float yBottom = yTop - m_height - 2.0f; + float wrap = copy == 0 ? 0.0f : -(contentH + SCROLL_GAP); + float top = 1.0f - m_scroll; + float bottom = top - m_height - 2.0f; if (m_scroll == 0.0f) - yTop += 1.0f; // don't fade at top when not scrolled... - VerticalFade(verts, m_mesh->GetNumVertices(), yTop/* + 0.5f*/, yBottom, i == 0 ? 0 : -(nContentHeight + nScrollSpace)); - m_mesh->UnlockVertexBuffer(); + top += 1.0f; // no top fade until it starts moving + TEXTVERTEX* verts; + m_pMesh->LockVertexBuffer(0, (BYTE**)&verts); + VerticalFade(verts, m_pMesh->GetNumVertices(), top, bottom, wrap); + m_pMesh->UnlockVertexBuffer(); + + if (copy == 1 && m_scroll + m_height < contentH) + break; } - else if (i == 1) + else if (copy == 1) { - // Don't need second pass if not scrolling... break; } - // Don't need second pass unless part of it is visible... - if (!bHScroll && i == 1 && m_scroll + m_height < nContentHeight) - break; - if (bHScroll && i == 1 && m_scroll + windowW < nContentWidth) - break; - TheseusPushWorld(); - if (bHScroll) - TheseusTranslateWorld(xOffset + (i == 0 ? 0 : (nContentWidth + nScrollSpace)) - m_scroll, 0.0f, 0.0f); + if (bMarquee) + TheseusTranslateWorld(xOffset + (copy == 0 ? 0.0f : contentW + SCROLL_GAP) - m_scroll, 0.0f, 0.0f); else - TheseusTranslateWorld(xOffset, (i == 0 ? 0 : -(nContentHeight + nScrollSpace)) + m_scroll, 0.0f); + TheseusTranslateWorld(xOffset, (copy == 0 ? 0.0f : -(contentH + SCROLL_GAP)) + m_scroll, 0.0f); TheseusUpdateWorld(); TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); Frame_Count(FC_DRAW_SUBSET); - m_mesh->DrawSubset(0); + m_pMesh->DrawSubset(0); TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); TheseusPopWorld(); } } - +// CTextNode::OnSetProperty(pprd, pvValue) +// Reconstructed from 5960 retail XBE @ 0x00055f20: +// if (prd->offset != 0x3c /* scroll */) { if (mesh) { mesh->Release(); mesh = 0; } +// timeToScroll = 0; scroll = 0; } +// return 1; bool CTextNode::OnSetProperty(const PRD* pprd, const void* pvValue) { - if (PTR2INT(pprd->pbOffset) == offsetof(m_scroll)) - return true; // don't need to invalidate - - if (m_mesh != NULL) + if (PTR2INT(pprd->pbOffset) != offsetof(m_scroll)) { - m_mesh->Release(); - m_mesh = NULL; + DropMesh(); + m_scroll = 0.0f; + m_timeToScroll = 0.0; } - - m_scroll = 0; - m_timeToScroll = 0.0f; - return true; } +// ===== Text node cache ===================================================== +// Ten throwaway Text nodes for code that draws a string directly (the save +// grid and title lists), keyed by string and width. -#define MAX_TEXT_CACHE 10 +#define MAX_TEXT_CACHE 10 struct CTextCacheEntry { - CTextNode* m_textNode; - XTIME m_usage; + CTextNode* m_pNode; + XTIME m_lastUsed; }; -static CTextCacheEntry textCache [MAX_TEXT_CACHE]; - - +static CTextCacheEntry s_textCache[MAX_TEXT_CACHE]; + +// FindText(szText, nWidth) +// Reconstructed from 5960 retail XBE @ 0x00055f60. Ghidra decompile, trimmed: +// for (i < 10) { +// if (node && node->text && !wcscmp(node->text, s) && node->width == w) +// { used = now; return entry; } +// if (!free) { if (!node) free = entry; else if (!oldest || used < oldest->used) oldest = entry; } +// } +// if (!free && oldest) { delete oldest->node; zero it; free = oldest; } +// free->node = new CTextNode; node->text = copy(s); node->width = w; used = now; static CTextCacheEntry* FindText(const TCHAR* szText, float nWidth) { - CTextCacheEntry* pFreeOne = NULL; - CTextCacheEntry* pOldOne = NULL; - for (int i = 0; i < countof(textCache); i += 1) + CTextCacheEntry* pEmpty = NULL; + CTextCacheEntry* pOldest = NULL; + + for (int i = 0; i < MAX_TEXT_CACHE; i++) { - if (textCache[i].m_textNode != NULL && textCache[i].m_textNode->m_text != NULL && _tcscmp(textCache[i].m_textNode->m_text, szText) == 0 && textCache[i].m_textNode->m_width == nWidth) + CTextCacheEntry& entry = s_textCache[i]; + CTextNode* pNode = entry.m_pNode; + if (pNode != NULL && pNode->m_text != NULL && pNode->m_width == nWidth && + _tcscmp(pNode->m_text, szText) == 0) { - textCache[i].m_usage = TheseusGetNow(); - return &textCache[i]; + entry.m_lastUsed = TheseusGetNow(); + return &entry; } - if (pFreeOne != NULL) + if (pEmpty != NULL) continue; - - if (textCache[i].m_textNode == NULL) - pFreeOne = &textCache[i]; - else if (pOldOne == NULL || pOldOne->m_usage > textCache[i].m_usage) - pOldOne = &textCache[i]; + if (pNode == NULL) + pEmpty = &entry; + else if (pOldest == NULL || entry.m_lastUsed < pOldest->m_lastUsed) + pOldest = &entry; } - if (pFreeOne == NULL && pOldOne != NULL) + if (pEmpty == NULL) { - delete pOldOne->m_textNode; - ZeroMemory(pOldOne, sizeof (CTextCacheEntry)); - pFreeOne = pOldOne; + delete pOldest->m_pNode; + pOldest->m_pNode = NULL; + pOldest->m_lastUsed = 0.0; + pEmpty = pOldest; } - ASSERT(pFreeOne != NULL); - - pFreeOne->m_textNode = new CTextNode; - pFreeOne->m_textNode->m_text = new TCHAR [_tcslen(szText) + 1]; - pFreeOne->m_textNode->m_width = nWidth; - _tcscpy(pFreeOne->m_textNode->m_text, szText); - - pFreeOne->m_usage = TheseusGetNow(); - - return pFreeOne; + CTextNode* pNode = new CTextNode; + pNode->m_text = CopyString(szText); + pNode->m_width = nWidth; + pEmpty->m_pNode = pNode; + pEmpty->m_lastUsed = TheseusGetNow(); + return pEmpty; } +// The callers inline this in 5960; the node never translates its string. CNode* GetTextNode(const TCHAR* szText, float nWidth) { - CTextCacheEntry* pTextCache = FindText(szText, nWidth); - pTextCache->m_textNode->m_translate = false; - return pTextCache->m_textNode; + CTextNode* pNode = FindText(szText, nWidth)->m_pNode; + pNode->m_translate = false; + return pNode; } From 9fcbde64e975e692e7368d52e7edb8d325054f55 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:38:23 -0500 Subject: [PATCH 067/167] Desktop draws text through the shared text.cpp Folded the desktop text code into render/text.cpp and dropped the copy. The only desktop differences left are UTF-8 decoding, the Xbox fallback face and the vertex alpha toggle around the draw. Meshes and draws match the old desktop file byte for byte on all 3165 strings. --- build/Makefile | 2 +- theseus/desktop/text.cpp | 1422 -------------------------------------- theseus/render/text.cpp | 54 +- 3 files changed, 53 insertions(+), 1425 deletions(-) delete mode 100755 theseus/desktop/text.cpp diff --git a/build/Makefile b/build/Makefile index 064b3ef..b67b7c4 100644 --- a/build/Makefile +++ b/build/Makefile @@ -844,7 +844,7 @@ DESKTOP_SRCS_RENDER = \ $(THESEUS_SRC)/desktop/materials_desktop.cpp \ $(THESEUS_SRC)/desktop/mesh.cpp \ $(THESEUS_SRC)/render/shape_render.cpp \ - $(THESEUS_SRC)/desktop/text.cpp \ + $(THESEUS_SRC)/render/text.cpp \ $(THESEUS_SRC)/render/starfield.cpp \ $(THESEUS_SRC)/engine/dotfield_node.cpp diff --git a/theseus/desktop/text.cpp b/theseus/desktop/text.cpp deleted file mode 100755 index 53b645c..0000000 --- a/theseus/desktop/text.cpp +++ /dev/null @@ -1,1422 +0,0 @@ -// text.cpp: desktop XTF font loading, glyph mesh generation, text -// node rendering. Reads Xbox .xtf vector font files and builds -// triangle meshes from glyph outlines; handles word wrap, -// scrolling, cursor blinking, and the overlay text API. -// Counterpart to render/text.cpp on Xbox. - -#include "std.h" -#include "dashapp.h" -#include "node.h" -#include "dashlocale.h" -#include "xap_compile.h" -#include "runner.h" - -#ifndef LF_FACESIZE -#define LF_FACESIZE 32 -#endif - -char* g_szText = NULL; -int g_nTextChar = -1; -int g_nTextCharLast = -1; - -struct TEXTVERTEX -{ - float x, y, z; - float nx, ny, nz; - uint32_t color; -}; - -struct CGlyphVertex -{ - float x, y; -}; - -struct CGlyphShape -{ - uint16_t m_nIndexCount; - uint16_t m_nVertexCount; - uint16_t* m_indices; - CGlyphVertex* m_vertices; - int m_nGlyphIndex; // added to hold the index of the character -}; - -struct CGlyphMetrics -{ - float gmfBlackBoxX; - float gmfBlackBoxY; - CGlyphVertex gmfptGlyphOrigin; - float gmfCellIncX; - float gmfCellIncY; -}; - -struct CGlyphObject -{ - CGlyphMetrics m_metrics; - CGlyphShape* m_pGlyphShape; // or uint32_t m_dwFileOffsetOfGlyphShape -}; - -struct CWCRange -{ - WCHAR wcLow; - USHORT cGlyphs; -}; - -struct CGlyphSet -{ - uint32_t cbThis; - uint32_t flAccel; - uint32_t cGlyphsSupported; - uint32_t cRanges; - CWCRange ranges [1]; -}; - -class CFont -{ -public: - CFont(); - ~CFont(); - - bool Open(const char* szFile); - void Close(); - bool LoadGlyph(int nGlyphIndex); - int FindGlyphIndex(WCHAR wch); - void CreateTextMesh(const char* pchText, int nChars, LPD3DXMESH* ppMesh, D3DXVECTOR3* pMin, D3DXVECTOR3* pMax, float nFormatWidth, bool bDoNotBreak, float scale); - void CreateCursorMesh(uint16_t*& indices, int& nCurIndex, TEXTVERTEX*& verts, int& nCurVertex, float x, float y, bool visible); - bool IsBreakChar(WCHAR ch); - - HANDLE m_hFile; - CGlyphSet* m_pGlyphSet; - CGlyphObject* m_rgGlyphObjects; - bool* m_rgGlyphLoaded; -}; - -CFont::CFont() -{ - m_hFile = INVALID_HANDLE_VALUE; - m_pGlyphSet = NULL; - m_rgGlyphObjects = NULL; - m_rgGlyphLoaded = NULL; -} - -CFont::~CFont() -{ - Close(); -} - -void CFont::Close() -{ - if (m_hFile != INVALID_HANDLE_VALUE) - VERIFY(CloseHandle(m_hFile)); - - if (m_pGlyphSet != NULL) - { - for (unsigned int i = 0; i < m_pGlyphSet->cGlyphsSupported; i += 1) - { - if (m_rgGlyphLoaded[i]) - { - CGlyphShape* pGlyphShape = m_rgGlyphObjects[i].m_pGlyphShape; - delete [] pGlyphShape->m_indices; - delete [] pGlyphShape->m_vertices; - delete pGlyphShape; - } - } - } - - delete [] (uint8_t*)m_pGlyphSet; - delete [] m_rgGlyphObjects; - delete [] m_rgGlyphLoaded; - - m_hFile = INVALID_HANDLE_VALUE; - m_pGlyphSet = NULL; - m_rgGlyphObjects = NULL; - m_rgGlyphLoaded = NULL; -} - -bool CFont::Open(const char* szFile) -{ - ASSERT(m_hFile == INVALID_HANDLE_VALUE); - - m_hFile = TheseusCreateFile(szFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); - if (m_hFile == INVALID_HANDLE_VALUE) - return false; - - uint32_t dwRead; - - uint32_t dwMagic; - ReadFile(m_hFile, &dwMagic, 4, LPDW(&dwRead), NULL); - - if (dwMagic != 0x30465458) // "XTF0" - { - TRACE("\001Invalid font file: %s\n", szFile); - - VERIFY(CloseHandle(m_hFile)); - m_hFile = INVALID_HANDLE_VALUE; - - return false; - } - - { - uint32_t dwHeaderLen; - VERIFY(ReadFile(m_hFile, &dwHeaderLen, 4, LPDW(&dwRead), NULL)); - - ASSERT(dwHeaderLen == LF_FACESIZE); - - char szFace [LF_FACESIZE]; - VERIFY(ReadFile(m_hFile, szFace, LF_FACESIZE, LPDW(&dwRead), NULL)); - - } - - uint32_t cbGlyphSet; - VERIFY(ReadFile(m_hFile, &cbGlyphSet, 4, LPDW(&dwRead), NULL)); - - { - uint8_t* rawBuf = new uint8_t[cbGlyphSet]; - *(uint32_t*)rawBuf = cbGlyphSet; - VERIFY(ReadFile(m_hFile, rawBuf + 4, cbGlyphSet - 4, LPDW(&dwRead), NULL)); - uint32_t flAccel = *(uint32_t*)(rawBuf + 4); - uint32_t cGlyphsSupported = *(uint32_t*)(rawBuf + 8); - uint32_t cRanges = *(uint32_t*)(rawBuf + 12); - // Allocate native-sized CGlyphSet - uint32_t nativeSize = 16 + sizeof(CWCRange) * cRanges; // header is 4 DWORDs = 16 bytes - m_pGlyphSet = (CGlyphSet*)new uint8_t[nativeSize]; - m_pGlyphSet->cbThis = cbGlyphSet; - m_pGlyphSet->flAccel = flAccel; - m_pGlyphSet->cGlyphsSupported = cGlyphsSupported; - m_pGlyphSet->cRanges = cRanges; - // On-disk ranges start at byte 16, each is 4 bytes (uint16 + uint16) - struct DiskWCRange { unsigned short wcLow; unsigned short cGlyphs; }; - DiskWCRange* diskRanges = (DiskWCRange*)(rawBuf + 16); - for (uint32_t i = 0; i < cRanges; i++) { - m_pGlyphSet->ranges[i].wcLow = (WCHAR)diskRanges[i].wcLow; - m_pGlyphSet->ranges[i].cGlyphs = diskRanges[i].cGlyphs; - } - delete[] rawBuf; - } - - // Desktop: 64-bit CGlyphObject is 32 bytes vs 28-byte on-disk Xbox format. Unpack manually. - m_rgGlyphObjects = new CGlyphObject [m_pGlyphSet->cGlyphsSupported]; - { - struct DiskGlyphObject { CGlyphMetrics m_metrics; uint32_t m_dwFileOffset; }; - static_assert(sizeof(CGlyphMetrics) == 24, "CGlyphMetrics must be 24 bytes"); - static_assert(sizeof(DiskGlyphObject) == 28, "DiskGlyphObject must be 28 bytes (Xbox on-disk format)"); - uint32_t cbDisk = (uint32_t)(sizeof(DiskGlyphObject) * m_pGlyphSet->cGlyphsSupported); - DiskGlyphObject* diskGlyphs = new DiskGlyphObject[m_pGlyphSet->cGlyphsSupported]; - VERIFY(ReadFile(m_hFile, diskGlyphs, cbDisk, LPDW(&dwRead), NULL)); - for (uint32_t i = 0; i < m_pGlyphSet->cGlyphsSupported; i++) { - m_rgGlyphObjects[i].m_metrics = diskGlyphs[i].m_metrics; - m_rgGlyphObjects[i].m_pGlyphShape = (CGlyphShape*)(uintptr_t)diskGlyphs[i].m_dwFileOffset; - } - delete[] diskGlyphs; - } - - m_rgGlyphLoaded = new bool [m_pGlyphSet->cGlyphsSupported]; - memset(m_rgGlyphLoaded, 0, sizeof (bool) * m_pGlyphSet->cGlyphsSupported); - - return true; -} - -bool CFont::LoadGlyph(int nGlyphIndex) -{ - ASSERT(m_pGlyphSet != NULL); - ASSERT(nGlyphIndex >= 0 && (unsigned int)nGlyphIndex < m_pGlyphSet->cGlyphsSupported); - - if (m_rgGlyphLoaded[nGlyphIndex]) - return true; - - // Desktop: file offset was stored as a pointer on 32-bit Xbox - uint32_t fileOffset = (uint32_t)(uintptr_t)m_rgGlyphObjects[nGlyphIndex].m_pGlyphShape; - VERIFY(SetFilePointer(m_hFile, fileOffset, 0, FILE_BEGIN) != (uint32_t)-1); - CGlyphShape* pGlyphShape = new CGlyphShape; - ASSERT(pGlyphShape); - - uint32_t dwRead; - - VERIFY(ReadFile(m_hFile, &pGlyphShape->m_nIndexCount, 2, LPDW(&dwRead), NULL)); - pGlyphShape->m_indices = new uint16_t [pGlyphShape->m_nIndexCount]; - - VERIFY(ReadFile(m_hFile, &pGlyphShape->m_nVertexCount, 2, LPDW(&dwRead), NULL)); - pGlyphShape->m_vertices = new CGlyphVertex [pGlyphShape->m_nVertexCount]; - - VERIFY(ReadFile(m_hFile, pGlyphShape->m_indices, sizeof (uint16_t) * pGlyphShape->m_nIndexCount, LPDW(&dwRead), NULL)); - VERIFY(ReadFile(m_hFile, pGlyphShape->m_vertices, sizeof (CGlyphVertex) * pGlyphShape->m_nVertexCount, LPDW(&dwRead), NULL)); - - m_rgGlyphObjects[nGlyphIndex].m_pGlyphShape = pGlyphShape; - m_rgGlyphLoaded[nGlyphIndex] = true; - - return true; -} - -// Decode one UTF-8 codepoint from s starting at i (n is the byte count). -// Returns the codepoint and sets outLen to the bytes consumed. A stray or -// truncated byte decodes as itself (outLen 1) so ASCII and Latin-1 fallbacks -// still render something. BMP only; astral planes truncate, which the vector -// fonts don't cover anyway. -static WCHAR DecodeUTF8(const char* s, int i, int n, int& outLen) -{ - unsigned char c = (unsigned char)s[i]; - if (c < 0x80) { outLen = 1; return c; } - unsigned int cp; int len; - if ((c & 0xE0) == 0xC0) { cp = c & 0x1F; len = 2; } - else if ((c & 0xF0) == 0xE0) { cp = c & 0x0F; len = 3; } - else if ((c & 0xF8) == 0xF0) { cp = c & 0x07; len = 4; } - else { outLen = 1; return c; } - if (i + len > n) { outLen = 1; return c; } - for (int k = 1; k < len; k++) { - unsigned char cc = (unsigned char)s[i + k]; - if ((cc & 0xC0) != 0x80) { outLen = 1; return c; } - cp = (cp << 6) | (cc & 0x3F); - } - outLen = len; - return (WCHAR)cp; -} - -int CFont::FindGlyphIndex(WCHAR wch) -{ - if (m_pGlyphSet == NULL) - return -1; - - int nIndex = 0; - - for (unsigned int i = 0; i < m_pGlyphSet->cRanges; i += 1) - { - if (wch >= m_pGlyphSet->ranges[i].wcLow && wch < m_pGlyphSet->ranges[i].wcLow + m_pGlyphSet->ranges[i].cGlyphs) - return nIndex + wch - m_pGlyphSet->ranges[i].wcLow; - - nIndex += m_pGlyphSet->ranges[i].cGlyphs; - } - - if (wch != 127) - return FindGlyphIndex(127); // the invalid char box - - return 0; // "invalid char" glyph (it's a space) -} - -inline float smoothstep(float a, float b, float x) -{ - if (x < a) - return 0.0f; - - if (x >= b) - return 1.0f; - - x = (x - a) / (b - a); - - return (x * x * (3 - 2 * x)); -} - -// Adjust the alpha value for verts whose x is between nStart and nEnd such that they fade -// from opaque to transparent (bRight==true) or transparent to opaque (bRight==false). -static void HorizontalFade(TEXTVERTEX* verts, int nVertexCount, float nStart, float nEnd, bool bRight) -{ - float nWidth = nEnd - nStart; - - for (int i = 0; i < nVertexCount; i += 1) - { - float a = (verts[i].x - nStart) / nWidth; - a = smoothstep(0.0f, 1.0f, a); - - if (bRight) - a = 1.0f - a; - - float a0 = (float)(verts[i].color >> 24); - verts[i].color = (verts[i].color & 0x00ffffff) | (((uint32_t)(a0 * a)) << 24); - } -} - -inline void FadeLeftEdge(TEXTVERTEX* verts, int nVertexCount, float nStart, float nEnd) -{ - HorizontalFade(verts, nVertexCount, nStart, nEnd, false); -} - -inline void FadeRightEdge(TEXTVERTEX* verts, int nVertexCount, float nStart, float nEnd) -{ - HorizontalFade(verts, nVertexCount, nStart, nEnd, true); -} - -static void HorizontalFade(TEXTVERTEX* verts, int nVertexCount, float nLeft, float nRight, float nScroll) -{ - for (int i = 0; i < nVertexCount; i += 1) - { - float a = 1.0f; - float x = verts[i].x + nScroll; - - if (x < nLeft || x > nRight) - { - a = 0.0f; - } - else if (x < nLeft + 1.0f) - { - a = 1.0f - ((nLeft + 1.0f) - x); - } - else if (x > nRight - 1.0f) - { - a = 1.0f - (x - (nRight - 1.0f)); - } - - if (a < 0.0f) a = 0.0f; - if (a > 1.0f) a = 1.0f; - a = smoothstep(0.0f, 1.0f, a); - verts[i].color = (verts[i].color & 0x00ffffff) | (((uint32_t)(255.0f * a)) << 24); - } -} - -static void VerticalFade(TEXTVERTEX* verts, int nVertexCount, float nTop, float nBottom, float nScroll) -{ - for (int i = 0; i < nVertexCount; i += 1) - { - float a = 1.0f; - float y = verts[i].y + nScroll; - - if (y > nTop || y < nBottom) - { - a = 0.0f; - } - else if (y > nTop - 1.0f) - { - a = 1.0f - (y - (nTop - 1.0f)); - } - else if (y < nBottom + 1.0f) - { - a = 1.0f - ((nBottom + 1.0f) - y); - } - else //if (y >= nTop + 1.0f && y <= nBottom - 1.0f) - { - a = 1.0f; - } - - ASSERT(a >= 0.0f && a <= 1.0f); - - a = smoothstep(0.0f, 1.0f, a); - verts[i].color = (verts[i].color & 0x00ffffff) | (((uint32_t)(255.0f * a)) << 24); - } -} - -bool CFont::IsBreakChar(WCHAR ch) -{ - // Basically, we will break after double byte or single byte Kana - if (ch == ' ' || (ch >= 0x3040 && ch < 0xF000) || ch >= 0xFF66) - { - return true; - } - - return false; -} - -extern void TheseusCreateMeshFVF(DWORD NumFaces, DWORD NumVertices, DWORD Options, DWORD FVF, LPD3DXMESH* ppMesh); - -void CFont::CreateTextMesh(const char* pchText, int nChars, LPD3DXMESH* ppMesh, D3DXVECTOR3* pMin, D3DXVECTOR3* pMax, float nFormatWidth, bool bDoNotBreak, float scale) -{ - // Font file may not be loaded; skip text rendering if so - if (m_pGlyphSet == NULL) - return; - bool bFade = bDoNotBreak; - - if (nChars == -1) - nChars = strlen(pchText); - - LPD3DXMESH pMesh = NULL; - int nFaces = 0, nVerts = 0; - - int ich = 0; - float nMaxCol1Width = 0.0f; - float nMaxCol2Width = 0.0f; - bool bSingleLine = true; - bool bSingleColumn = true; - bool bAsterisk = false; - int nLine; - for (nLine = 0; ich < nChars; nLine += 1) - { - int nColumn = 1; - float nCol1Width = 0.0f; - float nCol2Width = 0.0f; - - if (pchText[ich] == '*') - { - // See if there's a '*' at the end of the line... - int ich2; - for (ich2 = ich + 1; ich2 < nChars && pchText[ich2] != '\n' && pchText[ich2] != '\r' && pchText[ich2] != '\t'; ich2 += 1) - ; - if (ich2 > ich + 1 && pchText[ich2 - 1] == '*') - { - // Line starts and ends with a *; don't show the *'s and format as a heading - bAsterisk = true; - ich += 1; - } - } - - while (ich < nChars) - { - int chLen; - WCHAR ch = DecodeUTF8(pchText, ich, nChars, chLen); - ich += chLen; - - if (ch == '*') - { - // Ignore this if it's the last visible char in the column... - if (bAsterisk && (ich == nChars || pchText[ich] == '\t' || pchText[ich] == '\n' || pchText[ich] == '\r')) - continue; - } - - if (bDoNotBreak == false && ch == '\r' && ich < nChars && pchText[ich] == '\n') - { - ich += 1; - } - - if (bDoNotBreak == false && (ch == '\n' || ch == '\r')) - { - bSingleLine = false; - break; - } - - if (ch == '\t' && bDoNotBreak == false) - { - nColumn = 2; - bSingleColumn = false; - bAsterisk = false; - - if (ich < nChars && pchText[ich] == '*') - { - // See if there's a '*' at the end of the line... - int ich2; - for (ich2 = ich + 1; ich2 < nChars && pchText[ich2] != '\n' && pchText[ich2] != '\r' && pchText[ich2] != '\t'; ich2 += 1) - ; - if (ich2 > ich + 1 && pchText[ich2 - 1] == '*') - { - // Column starts and ends with a *; don't show the *'s and format as a heading - ich += 1; - bAsterisk = true; - } - } - continue; - } - - int nGlyphIndex = FindGlyphIndex(ch); - if (nGlyphIndex == -1) - continue; - - VERIFY(LoadGlyph(nGlyphIndex)); - - CGlyphShape* pGlyphShape = m_rgGlyphObjects[nGlyphIndex].m_pGlyphShape; - if (!pGlyphShape) - continue; - pGlyphShape->m_nGlyphIndex = nGlyphIndex; - - if (nVerts + pGlyphShape->m_nVertexCount > 65535) - { - TRACE("CreateTextMesh: too much text!\n"); - nChars = ich - 1; // truncate string - break; - } - - if (nColumn == 1) - nCol1Width += m_rgGlyphObjects[nGlyphIndex].m_metrics.gmfCellIncX; - else - nCol2Width += m_rgGlyphObjects[nGlyphIndex].m_metrics.gmfCellIncX; - - nFaces += pGlyphShape->m_nIndexCount / 3; - nVerts += pGlyphShape->m_nVertexCount; - } - - if (nCol1Width > nMaxCol1Width) - nMaxCol1Width = nCol1Width; - - if (nCol2Width > nMaxCol2Width) - nMaxCol2Width = nCol2Width; - } - - if (g_nTextChar >= 0 && g_nTextChar <= nChars) - { - nFaces += 2; - nVerts += 4; - } - - if (nFaces == 0) - return; - - if (nFormatWidth > 0.0f) - { - if (bSingleColumn) - { - nMaxCol1Width = nFormatWidth; - } - else if (nMaxCol1Width + nMaxCol2Width > nFormatWidth) - { - nMaxCol1Width = nFormatWidth - nMaxCol2Width; - bFade = true; - } - } - else - { - bFade = false; - } - - TheseusCreateMeshFVF(nFaces, nVerts, D3DXMESH_MANAGED, D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE, &pMesh); - *ppMesh = pMesh; - - uint16_t* indices; - pMesh->LockIndexBuffer(D3DLOCK_DISCARD, (uint8_t**)&indices); - - TEXTVERTEX* verts; - pMesh->LockVertexBuffer(D3DLOCK_DISCARD, (uint8_t**)&verts); - - float r = 1.0f; - float g = 1.0f; - float b = 1.0f; - bool bTitle = false; - bool bStartPara = true; - int nCurIndex = 0; - int nCurVertex = 0; - float x = 0.0f; - float y = 0.0f; - ich = 0; - for (nLine = 0; ich < nChars; nLine += 1) - { - int nColumn = 1; - int nFirstColVertex = nCurVertex; - float nCol2Width = 0.0f; - float nMaxColWidth = nMaxCol1Width; - - int nBreakChar = -1; - int nBreakVertex = nCurVertex; - int nBreakIndex = nCurIndex; - - if (bStartPara && ich < nChars && pchText[ich] == '*') - { - // See if there's a '*' at the end of the line... - int ich2; - for (ich2 = ich + 1; ich2 < nChars && pchText[ich2] != '\n' && pchText[ich2] != '\r' && pchText[ich2] != '\t'; ich2 += 1) - ; - if (ich2 > ich + 1 && pchText[ich2 - 1] == '*') - { - // Line starts and ends with a *; don't show the *'s and format as a heading - r = 1.0f; - g = 1.0f; - b = 0.5f; - bTitle = true; - - ich += 1; - } - } - - for (;;) - { - WCHAR ch = '\r'; - if (ich < nChars) - { - int chLen; - ch = DecodeUTF8(pchText, ich, nChars, chLen); - ich += chLen; - - if (ch == '\r' && ich < nChars && pchText[ich] == '\n') - ich += 1; - } - - if (ch == '\n' || ch == '\r') - { - if (bDoNotBreak && bFade) - { - FadeRightEdge(verts + nFirstColVertex, nCurVertex - nFirstColVertex, nMaxColWidth - 1, nMaxColWidth); - break; - } - - if (nColumn == 2) - { - // Right justify the second column... - for (int i = nFirstColVertex; i < nCurVertex; i += 1) - verts[i].x += nMaxCol2Width - nCol2Width; - } - else - { - if (bFade) - FadeRightEdge(verts + nFirstColVertex, nCurVertex - nFirstColVertex, nMaxColWidth - 1, nMaxColWidth); - } - - if (ich != nChars) - { - x = 0.0f; - if (bTitle || ch == '\r' || !bSingleColumn) - y -= 1.0f; - else - y -= 1.5f; - } - bTitle = false; - bStartPara = true; - break; - } - - if (ch == '\t' && bDoNotBreak == false) - { - if (bFade) - FadeRightEdge(verts + nFirstColVertex, nCurVertex - nFirstColVertex, nMaxColWidth - 2, nMaxColWidth); - - nColumn = 2; - x = nFormatWidth - nMaxCol2Width; - nFirstColVertex = nCurVertex; - nMaxColWidth = nMaxCol2Width; - bTitle = false; - bStartPara = true; - - if (ich < nChars && pchText[ich] == '*') - { - // See if there's a '*' at the end of the line... - int ich2; - for (ich2 = ich + 1; ich2 < nChars && pchText[ich2] != '\n' && pchText[ich2] != '\r' && pchText[ich2] != '\t'; ich2 += 1) - ; - if (ich2 > ich + 1 && pchText[ich2 - 1] == '*') - { - // Line starts and ends with a *; don't show the *'s and format as a heading - r = 1.0f; - g = 1.0f; - b = 0.5f; - bTitle = true; - - ich += 1; - } - } - - continue; - } - - if (bTitle && ch == '*') - { - // Ignore this if it's the last visible char in the column... - if (ich == nChars || pchText[ich] == '\t' || pchText[ich] == '\n' || pchText[ich] == '\r') - { - - r = 1.0f; - g = 1.0f; - b = 1.0f; - continue; - } - } - - bStartPara = false; - - int nGlyphIndex = FindGlyphIndex(ch); - if (nGlyphIndex == -1) - continue; - - ASSERT(m_rgGlyphLoaded[nGlyphIndex]); - - if (nFormatWidth > 0.0f) - { - if (bDoNotBreak) - { - // Can't skip chars here; would mismatch vert count from first pass - } - else - { - if (ch != ' ' && nColumn == 1 && x + m_rgGlyphObjects[nGlyphIndex].m_metrics.gmfCellIncX > nFormatWidth) - { - - // break line - - if (nBreakChar > -1) - { - x = 0.0f; - y -= 1.0f; - - ich = nBreakChar; - nCurVertex = nBreakVertex; - nCurIndex = nBreakIndex; - - nBreakChar = -1; - nBreakVertex = nCurVertex; - nBreakIndex = nCurIndex; - - continue; - } - else - { - // one word was too long to fit, fade it! - bFade = true; - } - } - } - } - - CGlyphShape* pGlyphShape = m_rgGlyphObjects[nGlyphIndex].m_pGlyphShape; - if (!pGlyphShape) - continue; - - // Bounds check: prevent buffer overflow if second pass produces more glyphs than first - if (nCurVertex + pGlyphShape->m_nVertexCount > nVerts || - nCurIndex + pGlyphShape->m_nIndexCount > nFaces * 3) - { - nChars = ich - 1; // truncate - break; - } - - int j; - for (j = 0; j < pGlyphShape->m_nIndexCount; j += 1) - indices[nCurIndex++] = nCurVertex + pGlyphShape->m_indices[j]; - - float fontAdjust = 1.0f; - - if(pGlyphShape->m_nGlyphIndex > 190) //character is in the japanese font set - { - fontAdjust = 0.9f; - } - - for (j = 0; j < pGlyphShape->m_nVertexCount; j += 1) - { - verts[nCurVertex].x = x + (pGlyphShape->m_vertices[j].x * fontAdjust); - verts[nCurVertex].y = y + (pGlyphShape->m_vertices[j].y * fontAdjust); - verts[nCurVertex].z = 0.0f; - - verts[nCurVertex].nx = 0.0f; - verts[nCurVertex].ny = 0.0f; - verts[nCurVertex].nz = 1.0f; - - verts[nCurVertex].color = D3DCOLOR_COLORVALUE(r, g, b, 1.0f); - - nCurVertex += 1; - } - - if (ich - 1 == g_nTextChar) - { - CreateCursorMesh(indices, nCurIndex, verts, nCurVertex, x, y, true); - } - - x += (m_rgGlyphObjects[nGlyphIndex].m_metrics.gmfCellIncX * fontAdjust); - - if (nColumn == 2) - nCol2Width += m_rgGlyphObjects[nGlyphIndex].m_metrics.gmfCellIncX; - - if (IsBreakChar(ch)) - { - nBreakChar = ich; - nBreakVertex = nCurVertex; - nBreakIndex = nCurIndex; - } - } - } - - if (g_nTextChar == nChars) - { - bool visible = false; - if(x < 16.556152f) //value to keep the cursor from extending past the keyboard blank - visible = true; - - CreateCursorMesh(indices, nCurIndex, verts, nCurVertex, x, y, visible); - } - - ASSERT(nCurVertex == nVerts); - ASSERT(nCurIndex == nFaces * 3); - - for (int i = 0; i < nVerts; i += 1) - { - verts[i].x *= scale; - verts[i].y *= scale; - verts[i].z *= scale; - } - - if (pMin != NULL && pMax != NULL) - VERIFYHR(D3DXComputeBoundingBox(verts, nCurVertex, D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE, pMin, pMax)); - - pMesh->UnlockIndexBuffer(); - pMesh->UnlockVertexBuffer(); -} - -void CFont::CreateCursorMesh(uint16_t*& indices, int& nCurIndex, TEXTVERTEX*& verts, int& nCurVertex, float x, float y, bool visible) -{ - float t = (float) (TheseusGetNow()) * 2.0f; - float a = fabsf(sinf(t * D3DX_PI)); - if(!visible) - a = 0.0f; - - indices[nCurIndex++] = (uint16_t)nCurVertex; - indices[nCurIndex++] = (uint16_t)nCurVertex + 1; - indices[nCurIndex++] = (uint16_t)nCurVertex + 2; - - indices[nCurIndex++] = (uint16_t)nCurVertex; - indices[nCurIndex++] = (uint16_t)nCurVertex + 2; - indices[nCurIndex++] = (uint16_t)nCurVertex + 3; - - verts[nCurVertex].x = x; - verts[nCurVertex].y = y - 0.1f; - verts[nCurVertex].z = 0.0f; - - verts[nCurVertex].nx = 0.0f; - verts[nCurVertex].ny = 0.0f; - verts[nCurVertex].nz = 1.0f; - - verts[nCurVertex].color = D3DCOLOR_COLORVALUE(1.0f, 1.0f, 1.0f, a); - - nCurVertex += 1; - - verts[nCurVertex].x = x + 0.1f; - verts[nCurVertex].y = y - 0.1f; - verts[nCurVertex].z = 0.0f; - - verts[nCurVertex].nx = 0.0f; - verts[nCurVertex].ny = 0.0f; - verts[nCurVertex].nz = 1.0f; - - verts[nCurVertex].color = D3DCOLOR_COLORVALUE(1.0f, 1.0f, 1.0f, a); - - nCurVertex += 1; - - verts[nCurVertex].x = x + 0.1f; - verts[nCurVertex].y = y + 0.75f; - verts[nCurVertex].z = 0.0f; - - verts[nCurVertex].nx = 0.0f; - verts[nCurVertex].ny = 0.0f; - verts[nCurVertex].nz = 1.0f; - - verts[nCurVertex].color = D3DCOLOR_COLORVALUE(1.0f, 1.0f, 1.0f, a); - - nCurVertex += 1; - - verts[nCurVertex].x = x; - verts[nCurVertex].y = y + 0.75f; - verts[nCurVertex].z = 0.0f; - - verts[nCurVertex].nx = 0.0f; - verts[nCurVertex].ny = 0.0f; - verts[nCurVertex].nz = 1.0f; - - verts[nCurVertex].color = D3DCOLOR_COLORVALUE(1.0f, 1.0f, 1.0f, a); - - nCurVertex += 1; -} - -// ============================================================================ -// Font Table; discovery and caching of .xtf font files -// ============================================================================ - -struct CFontTableEntry -{ - CFontTableEntry() - { - m_szFaceName = NULL; - m_szFileName = NULL; - } - - CFontTableEntry(const char* szFaceName, const char* szFileName) - { - m_szFaceName = szFaceName; - m_szFileName = szFileName; - } - - const char* m_szFaceName; - const char* m_szFileName; - CFont m_font; -}; - -#define MAX_FONT_COUNT 10 - -CFontTableEntry g_fonts [MAX_FONT_COUNT] = -{ - // NOTE: The first font is used as the default when a specified font is not found! - CFontTableEntry("Xbox", "xbox.xtf"), -}; - -int g_nFontCount = 1; - -void InitFontTable() -{ - char szWild [MAX_PATH]; - WIN32_FIND_DATA fd; - HANDLE h; - - int cchAppDir = strlen(g_sFontDir); - - strcpy(szWild, g_sFontDir); - strcpy(szWild + cchAppDir, "*.xtf"); - - h = TheseusFindFirstFile(szWild, &fd); - - if (h == INVALID_HANDLE_VALUE) - { - TRACE("\001InitFontTable: No fonts!\n"); - return; - } - - do - { - char szFileName [MAX_PATH]; - - strcpy(szFileName, g_sFontDir); - - strcpy(szFileName + cchAppDir, fd.cFileName); - - if (g_nFontCount >= MAX_FONT_COUNT) - { - TRACE("\001InitFontTable: too many fonts!\n"); - continue; - } - - HANDLE hFont = TheseusCreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); - if (hFont != INVALID_HANDLE_VALUE) - { - uint32_t dwRead; - uint32_t cbGlyphSet; - ReadFile(hFont, &cbGlyphSet, 4, LPDW(&dwRead), NULL); - - if (cbGlyphSet == 0x30465458) - { - uint32_t dwHeaderLen; - ReadFile(hFont, &dwHeaderLen, 4, LPDW(&dwRead), NULL); - - ASSERT(dwHeaderLen == LF_FACESIZE); - - char szFaceA [LF_FACESIZE]; - ReadFile(hFont, szFaceA, LF_FACESIZE, LPDW(&dwRead), NULL); - - char* szFace = new char [strlen(szFaceA) + 1]; - strcpy(szFace, szFaceA); - - // Don't add the default font twice! - if (strcmp(szFace, g_fonts[0].m_szFaceName) == 0) - { - delete [] szFace; - } - else - { - char* szFile = new char [strlen(szFileName) + 1]; - strcpy(szFile, szFileName); - - g_fonts[g_nFontCount].m_szFaceName = szFace; - g_fonts[g_nFontCount].m_szFileName = szFile; - g_nFontCount += 1; - - } - } - - CloseHandle(hFont); - } - } - while (TheseusFindNextFile(h, &fd)); - - TheseusFindClose(h); -} - -CFont* GetFont(const char* szFaceName) -{ - if (g_nFontCount == 1) - InitFontTable(); - - int i; - for (i = 0; i < g_nFontCount; i += 1) - { - if (strcasecmp(szFaceName, g_fonts[i].m_szFaceName) == 0) - break; - } - - if (i == g_nFontCount) - { - TRACE("Substituting font '%s' for '%s'\n", g_fonts[0].m_szFaceName, szFaceName); - i = 0; - } - - if (g_fonts[i].m_font.m_hFile == INVALID_HANDLE_VALUE) - { - char szFontPath [MAX_PATH]; - - if (strchr(g_fonts[i].m_szFileName, ':') == NULL) - sprintf(szFontPath, "%s%s", g_sFontDir, g_fonts[i].m_szFileName); - else - strcpy(szFontPath, g_fonts[i].m_szFileName); - - if (!g_fonts[i].m_font.Open(szFontPath)) - { - TRACE("\001Cannot load font: %s\n", szFaceName); - - if (g_fonts[0].m_font.m_hFile == INVALID_HANDLE_VALUE) - { - if (!g_fonts[0].m_font.Open(g_fonts[0].m_szFileName)) - { - ASSERT(FALSE); - return NULL; - } - } - } - } - - return &g_fonts[i].m_font; -} - -void Text_Exit() -{ - for (int i = 0; i < g_nFontCount; i += 1) - { - if (i > 0) - { - delete [] (char*)g_fonts[i].m_szFaceName; - delete [] (char*)g_fonts[i].m_szFileName; - g_fonts[i].m_szFaceName = NULL; - g_fonts[i].m_szFileName = NULL; - } - g_fonts[i].m_font.Close(); - } -} - -// ============================================================================ -// CTextNode; scene graph text rendering node -// ============================================================================ - -class CTextNode : public CNode -{ - DECLARE_NODE(CTextNode, CNode) -public: - CTextNode(); - ~CTextNode(); - - void Render(); - void Advance(float nSeconds); - bool OnSetProperty(const PRD* pprd, const void* pvValue); - - char* m_text; - char* m_font; - char* m_justify; - float m_deviation; - float m_extrusion; - float m_width; - bool m_translate; - float m_height; - float m_scroll; - float m_scrollRate; - float m_scrollDelay; - bool m_scrollHorizontal; - - DECLARE_NODE_PROPS() - - LPD3DXMESH m_pMesh; - D3DXVECTOR3 m_bboxMin, m_bboxMax; - int m_nLanguage; - XTIME m_timeToScroll; -}; - -static const float nScrollSpace = 1.5f; - -IMPLEMENT_NODE("Text", CTextNode, CNode) - -START_NODE_PROPS(CTextNode, CNode) - NODE_PROP(pt_string, CTextNode, text) - NODE_PROP(pt_string, CTextNode, font) - NODE_PROP(pt_string, CTextNode, justify) - NODE_PROP(pt_number, CTextNode, deviation) - NODE_PROP(pt_number, CTextNode, extrusion) - NODE_PROP(pt_number, CTextNode, width) - NODE_PROP(pt_boolean, CTextNode, translate) - NODE_PROP(pt_number, CTextNode, height) - NODE_PROP(pt_number, CTextNode, scroll) - NODE_PROP(pt_number, CTextNode, scrollRate) - NODE_PROP(pt_number, CTextNode, scrollDelay) - NODE_PROP(pt_boolean, CTextNode, scrollHorizontal) -END_NODE_PROPS() - -CTextNode::CTextNode() : - m_text(NULL), - m_font(NULL), - m_justify(NULL), - m_deviation(0.01f), - m_extrusion(1.0f), - m_translate(true), - m_width(0.0f), - m_height(0.0f), - m_scroll(0.0f), - m_scrollRate(0.0f), - m_scrollDelay(0.0f), - m_scrollHorizontal(false) -{ - m_pMesh = NULL; - m_nLanguage = 0; - m_timeToScroll = 0.0f; -} - -CTextNode::~CTextNode() -{ - delete [] m_text; - delete [] m_font; - delete [] m_justify; - - if (m_pMesh != NULL) - m_pMesh->Release(); -} - -void CTextNode::Advance(float nSeconds) -{ - CNode::Advance(nSeconds); - - if (m_scrollRate > 0.0f && m_pMesh != NULL) - { - XTIME now = TheseusGetNow(); - - if (m_timeToScroll == 0.0f) - m_timeToScroll = now + m_scrollDelay; - - if (now >= m_timeToScroll) - { - if (m_scrollHorizontal) - { - float nContentWidth = m_bboxMax.x - m_bboxMin.x; - float windowW = fabsf(m_width); - if (windowW > 0.0f && nContentWidth > windowW) - { - m_scroll += windowW * nSeconds * m_scrollRate; - if (m_scroll >= nContentWidth + nScrollSpace) - m_scroll -= nContentWidth + nScrollSpace; - } - } - else - { - float nContentHeight = m_bboxMax.y - m_bboxMin.y; - if (nContentHeight > m_height) - { - m_scroll += m_height * nSeconds * m_scrollRate; - if (m_scroll >= nContentHeight + nScrollSpace) - m_scroll -= nContentHeight + nScrollSpace; - } - } - } - } -} - -const char* FaceFromFont(const char* szFont) -{ - const char* szFace = szFont; - - if (szFont == NULL || strcasecmp(szFont, "body") == 0) - szFace = "XBox Book"; - else if (strcasecmp(szFont, "heading") == 0) - szFace = "Xbox"; - - return szFace; -} - -void CTextNode::Render() -{ - bool bInvalidMesh = false; - - if (g_szText != NULL) - { - if (m_text == NULL || strcmp(m_text, g_szText) != 0 || g_nTextChar != g_nTextCharLast) - { - delete [] m_text; - m_text = new char [strlen(g_szText) + 1]; - strcpy(m_text, g_szText); - g_nTextCharLast = g_nTextChar; - bInvalidMesh = true; - } - - g_szText = NULL; - } - - const char* szText = m_text; - if (szText == NULL || szText[0] == 0) - return; - - if (strncmp(m_text, "", 7) == 0) - { - SYSTEMTIME st; - GetLocalTime(&st); - - char szBuf [32]; - FormatTime(szBuf, countof(szBuf), &st); - - if (strcmp(m_text + 7, szBuf) != 0) - { - delete [] m_text; - m_text = new char [7 + strlen(szBuf) + 1]; - sprintf(m_text, "%s", szBuf); - - bInvalidMesh = true; - } - - m_translate = false; - szText = m_text + 7; // look past the - } - - if (m_translate && m_nLanguage != g_nCurLanguage) - bInvalidMesh = true; - - if (m_pMesh != NULL && bInvalidMesh) - { - m_pMesh->Release(); - m_pMesh = NULL; - } - - if (m_pMesh == NULL) - { - char sz[MAX_TRANSLATE_LEN]; - - if (m_translate) - szText = Translate(szText, sz); - - m_nLanguage = g_nCurLanguage; - - CFont* pFont = GetFont(FaceFromFont(m_font)); - if (pFont == NULL || pFont->m_pGlyphSet == NULL) - return; - pFont->CreateTextMesh(szText, -1, &m_pMesh, &m_bboxMin, &m_bboxMax, fabsf(m_width), m_width < 0.0f, g_nCurLanguage == LANGUAGE_JAPANESE ? 1.0f : 1.0f); - if (m_pMesh == NULL) - return; - } - - float nContentHeight = m_bboxMax.y - m_bboxMin.y; - - float xOffset = 0.0f; - if (m_justify != NULL) - { - float nWidth = m_bboxMax.x - m_bboxMin.x; - if (m_width != 0.0f) - { - float widthLimit = fabsf(m_width); - if (nWidth > widthLimit) - nWidth = widthLimit; - } - - if (strcasecmp(m_justify, "middle") == 0) - { - xOffset = -nWidth / 2.0f; - } - else if (strcasecmp(m_justify, "end") == 0) - { - xOffset = -nWidth; - } - } - - float nContentWidth = m_bboxMax.x - m_bboxMin.x; - float windowW = fabsf(m_width); - bool bHScroll = m_scrollHorizontal && windowW > 0.0f && nContentWidth > windowW; - - for (int i = 0; i < 2; i += 1) - { - if (bHScroll) - { - TEXTVERTEX* verts; - m_pMesh->LockVertexBuffer(0, (uint8_t**)&verts); - float xLeft = m_scroll - 1.0f; - float xRight = xLeft + windowW + 2.0f; - HorizontalFade(verts, m_pMesh->GetNumVertices(), xLeft, xRight, i == 0 ? 0 : (nContentWidth + nScrollSpace)); - m_pMesh->UnlockVertexBuffer(); - } - else if (m_height != 0.0f && nContentHeight > m_height) - { - TEXTVERTEX* verts; - m_pMesh->LockVertexBuffer(0, (uint8_t**)&verts); - float yTop = -m_scroll + 1.0f; - float yBottom = yTop - m_height - 2.0f; - if (m_scroll == 0.0f) - yTop += 1.0f; // don't fade at top when not scrolled... - VerticalFade(verts, m_pMesh->GetNumVertices(), yTop, yBottom, i == 0 ? 0 : -(nContentHeight + nScrollSpace)); - m_pMesh->UnlockVertexBuffer(); - } - else if (i == 1) - { - // Don't need second pass if not scrolling... - break; - } - - // Don't need second pass unless part of it is visible... - if (!bHScroll && i == 1 && m_scroll + m_height < nContentHeight) - break; - if (bHScroll && i == 1 && m_scroll + windowW < nContentWidth) - break; - - TheseusPushWorld(); - if (bHScroll) - TheseusTranslateWorld(xOffset + (i == 0 ? 0 : (nContentWidth + nScrollSpace)) - m_scroll, 0.0f, 0.0f); - else - TheseusTranslateWorld(xOffset, (i == 0 ? 0 : -(nContentHeight + nScrollSpace)) + m_scroll, 0.0f); - TheseusUpdateWorld(); - - // VerticalFade clears per-vertex alpha to 0 outside the visible - // [yTop,yBottom] band so the marquee duplicate vanishes past the - // panel edge. Falloff lighting on desktop overwrites color.a, so - // flip the shader's per-vertex alpha multiply path on for this - // draw only; normal text alpha (from falloff) still controls - // brightness, while vertex alpha controls clipping. - TheseusSetVertexAlphaMul(TRUE); - TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); - Frame_Count(FC_DRAW_SUBSET); - m_pMesh->DrawSubset(0); - TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); - TheseusSetVertexAlphaMul(FALSE); - - TheseusPopWorld(); - } -} - - -bool CTextNode::OnSetProperty(const PRD* pprd, const void* pvValue) -{ - if ((intptr_t)pprd->pbOffset == MEMBER_OFFSET(m_scroll)) - return true; // don't need to invalidate - - if (m_pMesh != NULL) - { - m_pMesh->Release(); - m_pMesh = NULL; - } - - m_scroll = 0; - m_timeToScroll = 0.0f; - - return true; -} - -// ============================================================================ -// Text Cache; reuse text meshes across frames -// ============================================================================ - -#define MAX_TEXT_CACHE 10 - -struct CTextCacheEntry -{ - CTextNode* m_pTextNode; - XTIME m_usage; -}; - -static CTextCacheEntry textCache [MAX_TEXT_CACHE]; - - -static CTextCacheEntry* FindText(const char* szText, float nWidth) -{ - CTextCacheEntry* pFreeOne = NULL; - CTextCacheEntry* pOldOne = NULL; - for (int i = 0; i < countof(textCache); i += 1) - { - if (textCache[i].m_pTextNode != NULL && textCache[i].m_pTextNode->m_text != NULL && strcmp(textCache[i].m_pTextNode->m_text, szText) == 0 && textCache[i].m_pTextNode->m_width == nWidth) - { - textCache[i].m_usage = TheseusGetNow(); - return &textCache[i]; - } - - if (pFreeOne != NULL) - continue; - - if (textCache[i].m_pTextNode == NULL) - pFreeOne = &textCache[i]; - else if (pOldOne == NULL || pOldOne->m_usage > textCache[i].m_usage) - pOldOne = &textCache[i]; - } - - if (pFreeOne == NULL && pOldOne != NULL) - { - delete pOldOne->m_pTextNode; - memset(pOldOne, 0, sizeof (CTextCacheEntry)); - pFreeOne = pOldOne; - } - - ASSERT(pFreeOne != NULL); - - pFreeOne->m_pTextNode = new CTextNode; - pFreeOne->m_pTextNode->m_text = new char [strlen(szText) + 1]; - pFreeOne->m_pTextNode->m_width = nWidth; - strcpy(pFreeOne->m_pTextNode->m_text, szText); - - pFreeOne->m_usage = TheseusGetNow(); - - return pFreeOne; -} - -CNode* GetTextNode(const char* szText, float nWidth) -{ - CTextCacheEntry* pTextCache = FindText(szText, nWidth); - pTextCache->m_pTextNode->m_translate = false; - return pTextCache->m_pTextNode; -} diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp index ae639a7..42bd175 100644 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -391,11 +391,44 @@ static void VerticalFade(TEXTVERTEX* verts, int nVerts, float nTop, float nBotto // ===== Text meshes ========================================================= -// Characters come in as UTF-16 units. +// Characters come in as UTF-16 units on the Xbox and UTF-8 on desktop. +#ifndef _UNICODE +// Decode one UTF-8 codepoint from s starting at i (n is the byte count). +// Returns the codepoint and sets outLen to the bytes consumed. A stray or +// truncated byte decodes as itself (outLen 1) so ASCII and Latin-1 fallbacks +// still render something. BMP only; astral planes truncate, which the vector +// fonts don't cover anyway. +static WCHAR DecodeUTF8(const char* s, int i, int n, int& outLen) +{ + unsigned char c = (unsigned char)s[i]; + if (c < 0x80) { outLen = 1; return c; } + unsigned int cp; int len; + if ((c & 0xE0) == 0xC0) { cp = c & 0x1F; len = 2; } + else if ((c & 0xF0) == 0xE0) { cp = c & 0x0F; len = 3; } + else if ((c & 0xF8) == 0xF0) { cp = c & 0x07; len = 4; } + else { outLen = 1; return c; } + if (i + len > n) { outLen = 1; return c; } + for (int k = 1; k < len; k++) { + unsigned char cc = (unsigned char)s[i + k]; + if ((cc & 0xC0) != 0x80) { outLen = 1; return c; } + cp = (cp << 6) | (cc & 0x3F); + } + outLen = len; + return (WCHAR)cp; +} +#endif + static inline WCHAR NextChar(const TCHAR* s, int& i, int n) { +#ifdef _UNICODE (void)n; return s[i++]; +#else + int len; + WCHAR ch = DecodeUTF8(s, i, n, len); + i += len; + return ch; +#endif } static inline bool IsLineEnd(TCHAR ch) @@ -785,9 +818,15 @@ struct CFontTableEntry #define MAX_FONT_COUNT 10 +// 5960 seeds slot 0 with HGGothicM, which Theseus doesn't ship; desktop falls +// back to the Xbox face instead. static CFontTableEntry g_fonts[MAX_FONT_COUNT] = { +#ifdef _XBOX { _T("HGGothicM"), _T("HGGothicM.xtf") }, +#else + { _T("Xbox"), _T("xbox.xtf") }, +#endif }; static int g_nFontCount = 1; @@ -932,6 +971,7 @@ void Text_Exit() } +#ifdef _XBOX // ===== Overlay text (Theseus) ============================================== // Draws "Xbox" face glyphs as screen space triangles for the panic screen, // the widgets and the overlay menus. No 5960 counterpart. @@ -1042,6 +1082,7 @@ float OverlayFontDraw(const TCHAR* text, float screenX, float screenY, float pix return curX * scale; } +#endif // _XBOX // ===== CTextNode =========================================================== @@ -1219,7 +1260,8 @@ const TCHAR* FaceFromFont(const TCHAR* szFont) // culling off for the flat glyphs and back to CCW. See docs/decomp/Text.md. // 5960 also expands , passes the indent props and rebuilds every frame // a cursorless keyboard string arrives; Theseus does none of those. The -// horizontal marquee and the draw counter are Theseus's. +// horizontal marquee, the draw counter and the desktop vertex alpha toggle +// are Theseus's. void CTextNode::Render() { bool bRebuild = false; @@ -1344,10 +1386,18 @@ void CTextNode::Render() TheseusTranslateWorld(xOffset, (copy == 0 ? 0.0f : -(contentH + SCROLL_GAP)) + m_scroll, 0.0f); TheseusUpdateWorld(); +#ifndef _XBOX + // Desktop falloff lighting writes color.a, so the fade's vertex alpha + // only clips if the shader multiplies it back in for this draw. + TheseusSetVertexAlphaMul(TRUE); +#endif TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); Frame_Count(FC_DRAW_SUBSET); m_pMesh->DrawSubset(0); TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); +#ifndef _XBOX + TheseusSetVertexAlphaMul(FALSE); +#endif TheseusPopWorld(); } From 7edbede53e2bd19ca6dbd9ccec533a9081a3812f Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:39:06 -0500 Subject: [PATCH 068/167] Rebuilt the HUD and Picture nodes from the 5960 binary I rewrote CHud and CPicture function by function from the retail XBE and gave each one its provenance note. A recording harness driving the dvd.xap HUD blocks and 375 Picture setups makes the same device, state and draw calls as before; the only drift is a last-bit difference while a Picture glides, because the fade divide now happens in double like the binary. Picture also checks its vertex buffer create and lock now instead of writing through a failed one. --- theseus/render/hud_node.cpp | 787 +++++++++++++++++------------------- 1 file changed, 361 insertions(+), 426 deletions(-) diff --git a/theseus/render/hud_node.cpp b/theseus/render/hud_node.cpp index dec10fa..6279f47 100644 --- a/theseus/render/hud_node.cpp +++ b/theseus/render/hud_node.cpp @@ -1,26 +1,32 @@ -// hud_node.cpp: CHud overlay node. Always-visible 2D layer above the -// scene graph (mute toast, system messages, transient overlays). -// Decompiled from the 5960 retail XBE. +// hud_node.cpp: the 2D overlay nodes. HUD flattens its children onto the +// screen, Picture draws one textured (or flat colored) quad in pixel space. +// Both rebuilt from the 5960 retail XBE (0x52ae0..0x53760); findings are in +// docs/decomp/HudNode.md. #include "std.h" #include "theseus.h" #include "lerper.h" #include "node.h" #include "asset_loader.h" -#include "lerper.h" extern D3DXMATRIX g_matIdentity; +extern D3DXMATRIX g_matView; +extern D3DXMATRIX g_matProjection; + + +// ===== CHud ================================================================= +// Class object 0x00188148, 0x44 bytes, factory 0x00052ae0, vtable 0x00028870. class CHud : public CGroup { - DECLARE_NODE(CHud, CGroup) + DECLARE_NODE(CHud, CGroup) public: - CHud(); - ~CHud(); + CHud(); + ~CHud(); - void Render(); + void Render(); - DECLARE_NODE_PROPS() + DECLARE_NODE_PROPS() }; IMPLEMENT_NODE("HUD", CHud, CGroup) @@ -28,6 +34,10 @@ IMPLEMENT_NODE("HUD", CHud, CGroup) START_NODE_PROPS(CHud, CGroup) END_NODE_PROPS() +// CHud::CHud() / ~CHud() +// Reconstructed from 5960 retail XBE @ 0x00052ae0 (factory) and 0x00052b80: +// the factory is the CGroup constructor inlined plus the HUD vtable, and the +// destructor only resets the vtable before CGroup's. Nothing of its own. CHud::CHud() { } @@ -36,89 +46,100 @@ CHud::~CHud() { } -extern D3DXMATRIX g_matView; -extern D3DXMATRIX g_matProjection; - +// CHud::Render() +// Reconstructed from 5960 retail XBE @ 0x00052b90. Ghidra decompile: +// D3DXMatrixOrthoLH(&ortho, 720.0, 480.0, -10000.0, 10000.0); +// savedProj = g_matProjection; g_matProjection = ortho; SetTransform(PROJ); +// savedView = g_matView; g_matView = g_matIdentity; SetTransform(VIEW); +// lighting = rs[LIGHTING]; rs[LIGHTING] = 0; +// worldStack->Push(); worldStack->LoadIdentity(); SetTransform(WORLD, top); +// rs[EDGEANTIALIAS] = 0; rs[MULTISAMPLEANTIALIAS] = 1; +// CGroup::Render(); +// worldStack->Pop(); SetTransform(WORLD, top); +// rs[LIGHTING] = lighting; restore projection, then view. +// Theseus sizes the ortho box from the live view instead of a fixed 720x480, +// and the two antialias states are Xbox only. void CHud::Render() { - D3DXMATRIX matProjection, matProjectionSave, matViewSave; - D3DXMatrixOrthoLH(&matProjection, TheseusGetViewWidth(), TheseusGetViewHeight(), -10000.0f, 10000.0f); + D3DXMATRIX ortho; + D3DXMatrixOrthoLH(&ortho, TheseusGetViewWidth(), TheseusGetViewHeight(), -10000.0f, 10000.0f); - matProjectionSave = g_matProjection; - g_matProjection = matProjection; - TheseusSetTransform(D3DTS_PROJECTION, &g_matProjection); + D3DXMATRIX savedProjection = g_matProjection; + g_matProjection = ortho; + TheseusSetTransform(D3DTS_PROJECTION, &g_matProjection); - matViewSave = g_matView; - g_matView = g_matIdentity; - TheseusSetTransform(D3DTS_VIEW, &g_matView); + D3DXMATRIX savedView = g_matView; + g_matView = g_matIdentity; + TheseusSetTransform(D3DTS_VIEW, &g_matView); - DWORD dwLighting; - TheseusGetRenderState(D3DRS_LIGHTING, &dwLighting); - TheseusSetRenderState(D3DRS_LIGHTING, FALSE); + DWORD lighting; + TheseusGetRenderState(D3DRS_LIGHTING, &lighting); + TheseusSetRenderState(D3DRS_LIGHTING, FALSE); - TheseusPushWorld(); - TheseusIdentityWorld(); - TheseusUpdateWorld(); + TheseusPushWorld(); + TheseusIdentityWorld(); + TheseusUpdateWorld(); #ifdef _XBOX - // Disable Edge Antialiasing. Xbox NV2A hardware feature; shim no-op - // on desktop GL. - TheseusSetRenderState(D3DRS_EDGEANTIALIAS, FALSE); - TheseusSetRenderState(D3DRS_MULTISAMPLEANTIALIAS, TRUE); + // NV2A only, the desktop shim has nothing to switch. + TheseusSetRenderState(D3DRS_EDGEANTIALIAS, FALSE); + TheseusSetRenderState(D3DRS_MULTISAMPLEANTIALIAS, TRUE); #endif - CGroup::Render(); + CGroup::Render(); - TheseusPopWorld(); + TheseusPopWorld(); + TheseusSetRenderState(D3DRS_LIGHTING, lighting); - TheseusSetRenderState(D3DRS_LIGHTING, dwLighting); - - g_matProjection = matProjectionSave; - TheseusSetTransform(D3DTS_PROJECTION, &g_matProjection); - g_matView = matViewSave; - TheseusSetTransform(D3DTS_VIEW, &g_matView); + g_matProjection = savedProjection; + TheseusSetTransform(D3DTS_PROJECTION, &g_matProjection); + g_matView = savedView; + TheseusSetTransform(D3DTS_VIEW, &g_matView); } +// ===== CPicture ============================================================= +// Class object 0x00188174, 0x78 bytes, factory 0x00052db0, vtable 0x00028810. +// Field offsets: left 0x1c, top 0x20, width 0x24, height 0x28, texture 0x2c, +// transparency 0x30, fade 0x34, color 0x38, alpha 0x44, fade start 0x48 +// (double), start/end pairs 0x50..0x6c, vertex buffer 0x70. class CPicture : public CNode { - DECLARE_NODE(CPicture, CNode) + DECLARE_NODE(CPicture, CNode) public: - CPicture(); - ~CPicture(); - - float m_left; - float m_top; - float m_width; - float m_height; - CNode *m_texture; - float m_transparency; - float m_fade; - D3DXVECTOR3 m_color; - - void OnLoad(); - void Render(); - void Advance(float nSeconds); - bool OnSetProperty(const PRD *pprd, const void *pvValue); - - float m_alpha; - // float m_startAlpha; - // float m_endAlpha; - XTIME m_startFadeTime; - - float m_startLeft; - float m_endLeft; - float m_startTop; - float m_endTop; - float m_startWidth; - float m_endWidth; - float m_startHeight; - float m_endHeight; - - LPDIRECT3DVERTEXBUFFER8 m_pVB; - - DECLARE_NODE_PROPS() + CPicture(); + ~CPicture(); + + float m_left; + float m_top; + float m_width; + float m_height; + CNode *m_texture; + float m_transparency; + float m_fade; + D3DXVECTOR3 m_color; + + void OnLoad(); + void Render(); + void Advance(float nSeconds); + bool OnSetProperty(const PRD *pprd, const void *pvValue); + + float m_alpha; + XTIME m_startFadeTime; + + float m_startLeft; + float m_endLeft; + float m_startTop; + float m_endTop; + float m_startWidth; + float m_endWidth; + float m_startHeight; + float m_endHeight; + + LPDIRECT3DVERTEXBUFFER8 m_pVB; + + DECLARE_NODE_PROPS() }; IMPLEMENT_NODE("Picture", CPicture, CNode) @@ -134,383 +155,297 @@ NODE_PROP(pt_number, CPicture, fade) NODE_PROP(pt_color, CPicture, color) END_NODE_PROPS() -CPicture::CPicture() : m_texture(NULL), - m_left(0.0f), - m_top(0.0f), - m_width(-1.0f), - m_height(-1.0f), - m_transparency(0.0f), - m_fade(0.0f), - m_color(1.0f, 1.0f, 1.0f) +// One corner of the quad: position, normal, diffuse, one UV. 0x24 bytes, +// FVF 0x152 (XYZ | NORMAL | DIFFUSE | TEX1). +struct PictureVertex { - m_alpha = 1.0f; - // m_startAlpha = 1.0f; - // m_endAlpha = 1.0f; - m_startFadeTime = 0.0f; + float x, y, z; + float nx, ny, nz; + D3DCOLOR diffuse; + float tu, tv; +}; - m_pVB = NULL; -} +static const DWORD kPictureFVF = D3DFVF_VERTEX | D3DFVF_DIFFUSE; -CPicture::~CPicture() +// CPicture::CPicture() +// Reconstructed from 5960 retail XBE @ 0x00052db0 (the factory, ctor inlined): +// left = top = 0; width = height = -1.0 (0x0002bef0); texture = 0; +// transparency = fade = 0; color = (1,1,1); alpha = 1.0; +// fadeStart = 0.0; vb = 0; +// The start/end pairs are left alone until OnLoad fills them. +CPicture::CPicture() { - if (m_pVB != NULL) - m_pVB->Release(); - - if (m_texture != NULL) - m_texture->Release(); + m_left = 0.0f; + m_top = 0.0f; + m_width = -1.0f; + m_height = -1.0f; + m_texture = NULL; + m_transparency = 0.0f; + m_fade = 0.0f; + m_color = D3DXVECTOR3(1.0f, 1.0f, 1.0f); + m_alpha = 1.0f; + m_startFadeTime = 0.0; + m_pVB = NULL; } -typedef struct _LITVERTEX +// CPicture::~CPicture() +// Reconstructed from 5960 retail XBE @ 0x00052e70: +// if (vb) vb->Release(); if (texture) texture->Release(); // vtable slot 4 +// Buffer first, then the texture node. +CPicture::~CPicture() { - union - { - D3DVALUE x; /* Homogeneous coordinates */ - D3DVALUE dvX; - }; - union - { - D3DVALUE y; - D3DVALUE dvY; - }; - union - { - D3DVALUE z; - D3DVALUE dvZ; - }; - union - { - D3DVALUE nx; /* Normal */ - D3DVALUE dvNX; - }; - union - { - D3DVALUE ny; - D3DVALUE dvNY; - }; - union - { - D3DVALUE nz; - D3DVALUE dvNZ; - }; - union - { - D3DCOLOR color; /* Vertex color */ - D3DCOLOR dcColor; - }; - union - { - D3DVALUE tu; /* Texture coordinates */ - D3DVALUE dvTU; - }; - union - { - D3DVALUE tv; - D3DVALUE dvTV; - }; -#if (DIRECT3D_VERSION >= 0x0500) -#if (defined __cplusplus) && (defined D3D_OVERLOADS) - _LITVERTEX() - { - } - _LITVERTEX(const D3DVECTOR &v, const D3DVECTOR &n, float _tu, float _tv) - { - x = v.x; - y = v.y; - z = v.z; - nx = n.x; - ny = n.y; - nz = n.z; - tu = _tu; - tv = _tv; - } -#endif -#endif /* DIRECT3D_VERSION >= 0x0500 */ -} LITVERTEX, *LPLITVERTEX; + if (m_pVB) + m_pVB->Release(); + if (m_texture) + m_texture->Release(); +} +// CPicture::Render() +// Reconstructed from 5960 retail XBE @ 0x00052f10. Skips when fully faded or +// when the texture node has no surface yet. Makes its 4 vertex buffer on first +// use. A negative width or height takes the texture's size, and a CTexture +// stretches its UVs so only the loaded image shows. Builds a strip in screen +// pixels, recenters it on the view and flips Y, blends with color and alpha +// through TEXTUREFACTOR, draws, then puts texture, blend and filters back. +// See docs/decomp/HudNode.md. Theseus checks the buffer create and lock; the +// binary assumes both work. void CPicture::Render() { - if (m_alpha <= 0.0f) - return; - - // if (m_texture == NULL) - // return; - LPDIRECT3DTEXTURE8 pTexture = NULL; - if (m_texture != NULL) - { - pTexture = m_texture->GetTextureSurface(); - if (pTexture == NULL) - return; - } - - if (m_pVB == NULL) - { - VERIFYHR(TheseusGetD3DDev()->CreateVertexBuffer(4 * sizeof(LITVERTEX), D3DUSAGE_WRITEONLY, D3DFVF_VERTEX | D3DFVF_DIFFUSE, D3DPOOL_MANAGED, &m_pVB)); - } - - float nWidth = m_width; - float nHeight = m_height; - - if (nWidth < 0.0f || nHeight < 0.0f) - { - if (pTexture == NULL) - return; - - float nTexWidth, nTexHeight; - TheseusGetTextureSize(pTexture, nTexWidth, nTexHeight); - - if (nWidth < 0.0f) - nWidth = nTexWidth; - if (nHeight < 0.0f) - nHeight = nTexHeight; - - if (m_texture->IsKindOf(NODE_CLASS(CTexture))) - { - CTexture *pTex = (CTexture *)m_texture; - nWidth *= pTex->m_nImageWidth / nWidth; - nHeight *= pTex->m_nImageHeight / nHeight; - } - } - - float tu = 1.0f; - float tv = 1.0f; - - if (m_texture != NULL && m_texture->IsKindOf(NODE_CLASS(CTexture))) - { - // The texture image may have been stretched during creation on order to fill the texture surface. - // Here we calculate a correction factor to the image looks right... - - float width, height; - TheseusGetTextureSize(pTexture, width, height); - - CTexture *pTex = (CTexture *)m_texture; - - tu = pTex->m_nImageWidth / width; - tv = pTex->m_nImageHeight / height; - } - - TheseusSetTexture(0, pTexture); - LITVERTEX verts[4]; - - verts[0].dvX = m_left + nWidth; - verts[0].dvY = m_top + nHeight; - verts[0].dvTU = tu; - verts[0].dvTV = tv; - verts[0].dcColor = D3DCOLOR_RGBA(255, 255, 255, 255); - - verts[1].dvX = m_left; - verts[1].dvY = m_top + nHeight; - verts[1].dvTU = 0.0f; - verts[1].dvTV = tv; - verts[1].dcColor = D3DCOLOR_RGBA(255, 255, 255, 255); - - verts[2].dvX = m_left + nWidth; - verts[2].dvY = m_top; - verts[2].dvTU = tu; - verts[2].dvTV = 0.0f; - verts[2].dcColor = D3DCOLOR_RGBA(255, 255, 255, 255); - - verts[3].dvX = m_left; - verts[3].dvY = m_top; - verts[3].dvTU = 0.0f; - verts[3].dvTV = 0.0f; - verts[3].dcColor = D3DCOLOR_RGBA(255, 255, 255, 255); - - for (int i = 0; i < 4; i += 1) - { - verts[i].dvX -= TheseusGetViewWidth() / 2.0f; - verts[i].dvY -= TheseusGetViewHeight() / 2.0f; - verts[i].dvZ = 0.0f; - verts[i].dvY = -verts[i].dvY; - - verts[i].dvNX = verts[i].dvX; - verts[i].dvNY = verts[i].dvY; - verts[i].dvNZ = 1.0f; - } - - TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); - TheseusSetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); - TheseusSetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); - - TheseusSetRenderState(D3DRS_TEXTUREFACTOR, D3DCOLOR_COLORVALUE(m_color.x, m_color.y, m_color.z, m_alpha)); - - if (pTexture != NULL) - { - TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); - TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); - TheseusSetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR); - - TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE); - TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE); - TheseusSetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TFACTOR); - - TheseusSetTextureStageState(0, D3DTSS_MINFILTER, D3DTEXF_POINT); - TheseusSetTextureStageState(0, D3DTSS_MAGFILTER, D3DTEXF_POINT); - } - else - { - TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); - TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR); - - TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); - TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR); - } + if (m_alpha <= 0.0f) + return; + + LPDIRECT3DTEXTURE8 surface = NULL; + if (m_texture) + { + surface = m_texture->GetTextureSurface(); + if (!surface) + return; + } + + if (!m_pVB) + { + if (FAILED(TheseusGetD3DDev()->CreateVertexBuffer(4 * sizeof(PictureVertex), D3DUSAGE_WRITEONLY, kPictureFVF, D3DPOOL_MANAGED, &m_pVB))) + m_pVB = NULL; + if (!m_pVB) + return; + } + + CTexture* image = NULL; + if (m_texture && m_texture->IsKindOf(NODE_CLASS(CTexture))) + image = (CTexture*)m_texture; + + float w = m_width; + float h = m_height; + if (w < 0.0f || h < 0.0f) + { + if (!surface) + return; + + float surfW, surfH; + TheseusGetTextureSize(surface, surfW, surfH); + if (w < 0.0f) + w = surfW; + if (h < 0.0f) + h = surfH; + + if (image) + { + w = (image->m_nImageWidth / w) * w; + h = (image->m_nImageHeight / h) * h; + } + } + + // Textures get padded up to a hardware size, so trim the UVs to the image. + float maxU = 1.0f; + float maxV = 1.0f; + if (image) + { + float surfW, surfH; + TheseusGetTextureSize(surface, surfW, surfH); + maxU = image->m_nImageWidth / surfW; + maxV = image->m_nImageHeight / surfH; + } + + TheseusSetTexture(0, surface); + + float right = m_left + w; + float bottom = m_top + h; + PictureVertex quad[4] = + { + { right, bottom, 0, 0, 0, 0, 0xffffffff, maxU, maxV }, + { m_left, bottom, 0, 0, 0, 0, 0xffffffff, 0.0f, maxV }, + { right, m_top, 0, 0, 0, 0, 0xffffffff, maxU, 0.0f }, + { m_left, m_top, 0, 0, 0, 0, 0xffffffff, 0.0f, 0.0f }, + }; + + float halfW = TheseusGetViewWidth() * 0.5f; + float halfH = TheseusGetViewHeight() * 0.5f; + for (int n = 0; n < 4; n++) + { + PictureVertex& v = quad[n]; + v.x = v.x - halfW; + v.y = -(v.y - halfH); + v.z = 0.0f; + v.nx = v.x; + v.ny = v.y; + v.nz = 1.0f; + } + + TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); + TheseusSetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); + TheseusSetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); + TheseusSetRenderState(D3DRS_TEXTUREFACTOR, D3DCOLOR_COLORVALUE(m_color.x, m_color.y, m_color.z, m_alpha)); + + if (surface) + { + TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); + TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); + TheseusSetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR); + TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE); + TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE); + TheseusSetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TFACTOR); + TheseusSetTextureStageState(0, D3DTSS_MINFILTER, D3DTEXF_POINT); + TheseusSetTextureStageState(0, D3DTSS_MAGFILTER, D3DTEXF_POINT); + } + else + { + // No texture: the quad is just the factor color. + TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR); + TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); + TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR); + } #ifdef _XBOX - // Disable Edge Antialiasing. Xbox NV2A hardware feature; shim no-op - // on desktop GL. - TheseusSetRenderState(D3DRS_EDGEANTIALIAS, FALSE); - TheseusSetRenderState(D3DRS_MULTISAMPLEANTIALIAS, TRUE); + // NV2A only, the desktop shim has nothing to switch. + TheseusSetRenderState(D3DRS_EDGEANTIALIAS, FALSE); + TheseusSetRenderState(D3DRS_MULTISAMPLEANTIALIAS, TRUE); #endif - D3DVERTEX *v; - VERIFYHR(m_pVB->Lock(0, 4 * sizeof(LITVERTEX), (BYTE **)&v, 0)); - CopyMemory(v, verts, 4 * sizeof(LITVERTEX)); - VERIFYHR(m_pVB->Unlock()); - - VERIFYHR(TheseusGetD3DDev()->SetStreamSource(0, m_pVB, sizeof(LITVERTEX))); - VERIFYHR(TheseusGetD3DDev()->SetVertexShader(D3DFVF_VERTEX | D3DFVF_DIFFUSE)); - VERIFYHR(TheseusGetD3DDev()->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2)); - - TheseusSetTexture(0, NULL); - TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); - - TheseusSetTextureStageState(0, D3DTSS_MINFILTER, D3DTEXF_LINEAR); - TheseusSetTextureStageState(0, D3DTSS_MAGFILTER, D3DTEXF_LINEAR); + BYTE* dst = NULL; + if (SUCCEEDED(m_pVB->Lock(0, sizeof(quad), &dst, 0)) && dst) + { + memcpy(dst, quad, sizeof(quad)); + m_pVB->Unlock(); + + LPDIRECT3DDEVICE8 dev = TheseusGetD3DDev(); + dev->SetStreamSource(0, m_pVB, sizeof(PictureVertex)); + dev->SetVertexShader(kPictureFVF); + dev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); + } + + TheseusSetTexture(0, NULL); + TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); + TheseusSetTextureStageState(0, D3DTSS_MINFILTER, D3DTEXF_LINEAR); + TheseusSetTextureStageState(0, D3DTSS_MAGFILTER, D3DTEXF_LINEAR); } +// CPicture::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x000534c0. Ghidra decompile: +// CNode::Advance(s); if (texture) texture->Advance(s); +// if (fade != 0 && fadeStart != 0) { +// t = (float)((now - fadeStart) / fade); +// if (t > 1.0) { fadeStart = 0; t = 1.0; } +// left = startLeft*(1-t) + endLeft*t; ...top, width, height the same } +// The divide happens in double like the binary, then drops to float. void CPicture::Advance(float nSeconds) { - CNode::Advance(nSeconds); - - if (m_texture != NULL) - m_texture->Advance(nSeconds); - if (m_fade != 0.0f && m_startFadeTime != 0.0f) - { - float t = (float)(TheseusGetNow() - m_startFadeTime) / m_fade; - - if (t > 1.0f) - { - t = 1.0f; - m_startFadeTime = 0.0f; - } - - float t1 = 1.0f - t; - - // m_alpha = t1 * m_startAlpha + t * m_endAlpha; - - m_left = t1 * m_startLeft + t * m_endLeft; - m_top = t1 * m_startTop + t * m_endTop; - m_width = t1 * m_startWidth + t * m_endWidth; - m_height = t1 * m_startHeight + t * m_endHeight; - - // TRACE("start %f end %f t %f a %f\n", m_startAlpha, m_endAlpha, t, m_alpha); - } + CNode::Advance(nSeconds); + + if (m_texture) + m_texture->Advance(nSeconds); + + if (m_fade == 0.0f || m_startFadeTime == 0.0) + return; + + float t = (float)((TheseusGetNow() - m_startFadeTime) / m_fade); + if (t > 1.0f) + { + m_startFadeTime = 0.0; + t = 1.0f; + } + + float s = 1.0f - t; + m_left = m_startLeft * s + m_endLeft * t; + m_top = m_startTop * s + m_endTop * t; + m_width = m_startWidth * s + m_endWidth * t; + m_height = m_startHeight * s + m_endHeight * t; } +// CPicture::OnLoad() +// Reconstructed from 5960 retail XBE @ 0x000535c0. Ghidra decompile: +// alpha = 1.0 - transparency; +// startLeft = endLeft = left; ...top, width, height the same; +// CNode::OnLoad(); void CPicture::OnLoad() { - m_alpha = 1.0f - m_transparency; - - m_startLeft = m_endLeft = m_left; - m_startTop = m_endTop = m_top; - m_startWidth = m_endWidth = m_width; - m_startHeight = m_endHeight = m_height; - - CNode::OnLoad(); + m_alpha = 1.0f - m_transparency; + + m_startLeft = m_left; + m_endLeft = m_left; + m_startTop = m_top; + m_endTop = m_top; + m_startWidth = m_width; + m_endWidth = m_width; + m_startHeight = m_height; + m_endHeight = m_height; + + CNode::OnLoad(); } +// CPicture::OnSetProperty(pprd, pvValue) +// Reconstructed from 5960 retail XBE @ 0x00053620. Ghidra decompile: +// switch (prd->offset) { +// case 0x30: a = 1.0 - v; if (fade == 0) alpha = a; +// else new CLerper(this, &alpha, a, fade); return 1; +// case 0x34: if (v == 0) fadeStart = 0; CLerper::RemoveObject(this); return 1; +// case 0x1c: endLeft = v; startLeft = left; break; // 0x20/0x24/0x28 alike +// default: return 1; } +// if (fade != 0) { fadeStart = now; return 0; } return 1; +// Returning false on a fading move keeps the old value, so Advance glides there. bool CPicture::OnSetProperty(const PRD *pprd, const void *pvValue) { - if (PTR2INT(pprd->pbOffset) == offsetof(m_transparency)) - { - float value = *(float *)pvValue; - - // TRACE("Set transparency to %f\n", value); - value = 1.0f - value; - - if (m_fade == 0.0f) - m_alpha = value; - else - new CLerper(this, &m_alpha, value, m_fade); - /* - m_endAlpha = 1.0f - value; - - if (m_fade == 0.0f) - { - m_startAlpha = m_endAlpha; - m_alpha = m_endAlpha; - m_startFadeTime = 0.0f; - // TRACE("\timmediate; alpha = %f\n", m_alpha); - } - else - { - m_startAlpha = m_alpha; - m_startFadeTime = TheseusGetNow(); - // TRACE("\tfade over %f seconds; alpha = %f\n", m_fade, m_alpha); - } - */ - } - else if (PTR2INT(pprd->pbOffset) == offsetof(m_fade)) - { - float value = *(float *)pvValue; - // TRACE("Set fade to %f\n", value); - - if (value == 0.0f) - { - // m_alpha = m_endAlpha; - m_startFadeTime = 0.0f; - // TRACE("\tadjusting alpha to %f\n", m_alpha); - } - - CLerper::RemoveObject(this); - } - else if (PTR2INT(pprd->pbOffset) == offsetof(m_left)) - { - float value = *(float *)pvValue; - - m_endLeft = value; - m_startLeft = m_left; - if (m_fade != 0.0f) - { - m_startFadeTime = TheseusGetNow(); - return false; - } - } - else if (PTR2INT(pprd->pbOffset) == offsetof(m_top)) - { - float value = *(float *)pvValue; - m_endTop = value; - m_startTop = m_top; - if (m_fade != 0.0f) - { - m_startFadeTime = TheseusGetNow(); - return false; - } - } - else if (PTR2INT(pprd->pbOffset) == offsetof(m_width)) - { - float value = *(float *)pvValue; - m_endWidth = value; - m_startWidth = m_width; - if (m_fade != 0.0f) - { - m_startFadeTime = TheseusGetNow(); - return false; - } - } - else if (PTR2INT(pprd->pbOffset) == offsetof(m_height)) - { - float value = *(float *)pvValue; - m_endHeight = value; - m_startHeight = m_height; - if (m_fade != 0.0f) - { - m_startFadeTime = TheseusGetNow(); - return false; - } - } - - return true; + int field = PTR2INT(pprd->pbOffset); + + if (field == offsetof(m_transparency)) + { + float target = 1.0f - *(const float*)pvValue; + if (m_fade == 0.0f) + m_alpha = target; + else + new CLerper(this, &m_alpha, target, m_fade); + return true; + } + + if (field == offsetof(m_fade)) + { + if (*(const float*)pvValue == 0.0f) + m_startFadeTime = 0.0; + CLerper::RemoveObject(this); + return true; + } + + // A move remembers where it starts from and where it's headed. + float* from; + float* to; + float now; + if (field == offsetof(m_left)) + { from = &m_startLeft; to = &m_endLeft; now = m_left; } + else if (field == offsetof(m_top)) + { from = &m_startTop; to = &m_endTop; now = m_top; } + else if (field == offsetof(m_width)) + { from = &m_startWidth; to = &m_endWidth; now = m_width; } + else if (field == offsetof(m_height)) + { from = &m_startHeight; to = &m_endHeight; now = m_height; } + else + return true; + + *to = *(const float*)pvValue; + *from = now; + if (m_fade != 0.0f) + { + m_startFadeTime = TheseusGetNow(); + return false; + } + return true; } - - From 05c3026606013c69fe912c3cf38a67b1cca8d0b5 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:39:06 -0500 Subject: [PATCH 069/167] Wrote up the 5960 HUD and Picture nodes in a HudNode decomp doc Covers both registrations, the prop tables and the render, advance and property paths. Also pointed the Picture row in the index at hud_node.cpp. --- docs/decomp/HudNode.md | 170 +++++++++++++++++++++++++++++++++++++++++ docs/decomp/README.md | 3 +- 2 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 docs/decomp/HudNode.md diff --git a/docs/decomp/HudNode.md b/docs/decomp/HudNode.md new file mode 100644 index 0000000..4420449 --- /dev/null +++ b/docs/decomp/HudNode.md @@ -0,0 +1,170 @@ +# HUD & Picture + +The two screen-space overlay nodes in the 5960 retail binary. `HUD` is a group that +flattens its children onto the screen; `Picture` is a single textured (or flat +colored) quad positioned in pixels. In the shipping XAPs only `dvd.xap` uses `HUD`, +and nothing uses `Picture`. + +## Registration + +Both classes are registered from the static init code at `0x000cd010` and +`0x000cd040`. + +| Class | Name string | Class object | Size | Factory | Base | Prop map | vtable | +|-------|-------------|--------------|------|---------|------|----------|--------| +| CHud | "HUD" `0x0002891c` | `0x00188148` | 0x44 | `0x00052ae0` | Group `0x00187c68` | `0x00175190` | `0x00028870` | +| CPicture | "Picture" `0x00028900` | `0x00188174` | 0x78 | `0x00052db0` | Node `0x0017f528` | `0x001751a8` | `0x00028810` | + +Virtual slots used below: 0 scalar deleting dtor, 1 GetPropMap, 2 OnSetProperty, +4 Release, 6 GetNodeClass, 14 OnLoad, 15 Advance, 16 Render, 22 GetTextureSurface. + +The HUD prop map has no entries of its own and chains to Group. The Picture map, +in table order: + +| Offset | Type | Name | +|--------|------|------| +| 0x20 | number | top | +| 0x1c | number | left | +| 0x24 | number | width | +| 0x28 | number | height | +| 0x2c | node | texture | +| 0x30 | number | transparency | +| 0x34 | number | fade | +| 0x38 | color | color | + +Neither class has a function table, so neither exposes script methods. + +## CHud + +The factory (`0x00052ae0`) is the Group constructor inlined: zero children, bbox +center 0, bbox size -1 on every axis, both bbox flags set, then the HUD vtable. The +destructor (`0x00052b80`) only resets the vtable and runs Group's. + +**Render (`0x00052b90`).** Every device call is inlined. Decoded, in order: + +```c +D3DXMatrixOrthoLH(&ortho, 720.0, 480.0, -10000.0, 10000.0); +savedProj = g_matProjection /* 0x0017f4e8 */; +g_matProjection = ortho; SetTransform(PROJECTION, &g_matProjection); // 0x00144e30 +savedView = g_matView /* 0x0017f468 */; +g_matView = g_matIdentity /* 0x00176338 */; SetTransform(VIEW, ...); // inline copy to device +0xc60 +lighting = rs[LIGHTING] /* 0x0017a218 */; rs[LIGHTING] = 0; +worldStack->Push(); worldStack->LoadIdentity(); // stack 0x0017e7fc, slots 4, 5 +SetTransform(WORLD, worldStack->GetTop()); FUN_0005ca00(); // falloff shader constants +rs[EDGEANTIALIAS] = 0; rs[MULTISAMPLEANTIALIAS] = 1; +CGroup::Render(); // 0x0004d7e0 +worldStack->Pop(); SetTransform(WORLD, worldStack->GetTop()); FUN_0005ca00(); +rs[LIGHTING] = lighting; +g_matProjection = savedProj; SetTransform(PROJECTION, ...); +g_matView = savedView; SetTransform(VIEW, ...); +``` + +The ortho box is a fixed 720x480, so HUD children are laid out in that space no +matter the video mode. + +## CPicture + +| Offset | Field | +|--------|-------| +| 0x1c..0x28 | left, top, width, height | +| 0x2c | texture node | +| 0x30 | transparency | +| 0x34 | fade (seconds) | +| 0x38..0x40 | color | +| 0x44 | alpha | +| 0x48 | fade start time (double) | +| 0x50..0x6c | start/end pairs for left, top, width, height | +| 0x70 | vertex buffer | + +**Factory (`0x00052db0`).** left = top = 0, width = height = -1.0 (`0x0002bef0`), +texture = 0, transparency = fade = 0, color = (1, 1, 1), alpha = 1.0, fade start = +0.0, vertex buffer = 0. The start/end pairs are not touched. + +**Destructor (`0x00052e70`).** Releases the vertex buffer, then calls Release on +the texture node. + +**Render (`0x00052f10`).** + +```c +if (alpha <= 0) return; +surface = 0; +if (texture && (surface = texture->GetTextureSurface()) == 0) return; +if (!vb) vb = CreateVertexBuffer(0x90); // inline XMemAlloc, 4 x 0x24 bytes +w = width; h = height; +if (w < 0 || h < 0) { + if (!surface) return; + GetLevelDesc(surface, &desc); // 0x0014bec0 + if (w < 0) w = desc.Width; if (h < 0) h = desc.Height; + if (texture is a CTexture /* class 0x001885f4 */) { + w = (texture->imageWidth / w) * w; // fields +0x28, +0x2c + h = (texture->imageHeight / h) * h; } } +u = v = 1.0; +if (texture is a CTexture) { GetLevelDesc(surface, &desc); + u = imageWidth / desc.Width; v = imageHeight / desc.Height; } +SetTexture(0, surface); // 0x00145160 +quad[0] = { left+w, top+h, color 0xffffffff, u, v }; +quad[1] = { left, top+h, color 0xffffffff, 0, v }; +quad[2] = { left+w, top, color 0xffffffff, u, 0 }; +quad[3] = { left, top, color 0xffffffff, 0, 0 }; +for each corner { // view size at 0x0017e7f0 / 0x0017e7f4 + x -= viewWidth * 0.5; y = 0.0 - (y - viewHeight * 0.5); + z = 0; normal = (x, y, 1.0); } +rs[ALPHABLENDENABLE] = 1; rs[SRCBLEND] = 0x302; rs[DESTBLEND] = 0x303; +rs[TEXTUREFACTOR] = ARGB(alpha*255, r*255, g*255, b*255); // 255.0 at 0x0002bf38, ftol each +if (surface) { COLOROP = MODULATE(TEXTURE, TFACTOR); ALPHAOP = MODULATE(TEXTURE, TFACTOR); + MINFILTER = MAGFILTER = POINT; } +else { COLOROP = SELECTARG1(TFACTOR); ALPHAOP = SELECTARG1(TFACTOR); } +rs[EDGEANTIALIAS] = 0; rs[MULTISAMPLEANTIALIAS] = 1; +lock vb (wait on the GPU if busy); copy 0x24 dwords; +SetStreamSource(0, vb, 0x24); SetVertexShader(0x152); // XYZ | NORMAL | DIFFUSE | TEX1 +DrawPrimitive(TRIANGLESTRIP, 0, 2); +SetTexture(0, NULL); rs[ALPHABLENDENABLE] = 0; +MINFILTER = MAGFILTER = LINEAR; +``` + +The vertex buffer create and lock are never checked. The two filter states go back +to linear even on the untextured path, where they were never changed. + +**Advance (`0x000534c0`).** + +```c +CNode::Advance(s); +if (texture) texture->Advance(s); +if (fade != 0 && fadeStart != 0.0) { + t = (float)((now /* 0x0017e808 */ - fadeStart) / fade); // x87, rounded once at the end + if (t > 1.0) { fadeStart = 0.0; t = 1.0; } + left = startLeft * (1 - t) + endLeft * t; + top = startTop * (1 - t) + endTop * t; + width = startWidth * (1 - t) + endWidth * t; + height = startHeight * (1 - t) + endHeight * t; } +``` + +Alpha isn't animated here; transparency fades go through a lerper instead. + +**OnLoad (`0x000535c0`).** alpha = 1.0 - transparency, each start and end value = +the current left/top/width/height, then CNode::OnLoad. + +**OnSetProperty (`0x00053620`).** Keyed on the PRD offset: + +```c +case 0x30 /* transparency */: + a = 1.0 - value; + if (fade == 0) alpha = a; + else new CLerper(this, &alpha, a, fade); // 0x28 bytes, linked at the head of 0x0017aeac + return 1; +case 0x34 /* fade */: + if (value == 0) fadeStart = 0.0; + CLerper::RemoveObject(this); + return 1; +case 0x1c /* left */: endLeft = value; startLeft = left; break; +case 0x20 /* top */: endTop = value; startTop = top; break; +case 0x24 /* width */: endWidth = value; startWidth = width; break; +case 0x28 /* height */:endHeight = value; startHeight = height; break; +default: return 1; +} +if (fade != 0) { fadeStart = now; return 0; } // veto: Advance moves it there +return 1; +``` + +A move with a fade set returns 0, so the property itself keeps its old value and +Advance walks it from start to end over `fade` seconds. diff --git a/docs/decomp/README.md b/docs/decomp/README.md index 35b81a4..2e05b57 100644 --- a/docs/decomp/README.md +++ b/docs/decomp/README.md @@ -54,7 +54,7 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x0001eddc | `_classCNavigationInfo` | `CNavigationInfo::class` (animation_nodes.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for VRML97 NavigationInfo (camera control mode). | | 0x0001eed0 | `_classCCameraPath` | `CCameraPath::class` (camera.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the camera-spline node. | | 0x0001efd8 | `_classCHud` | `CHud::class` (hud_node.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the heads-up overlay node. | -| 0x0001f004 | `_classCPicture` | `CPicture::class` (asset_loader.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the textured-quad picture node. | +| 0x0001f004 | `_classCPicture` | `CPicture::class` (hud_node.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the textured-quad picture node. | | 0x0001f300 | `_classCMaxMaterial` | `CMaxMaterial::class` (materials.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the 3ds Max material translator node. | | 0x0001f33c | `_classCLayer` | `CLayer::class` (scene_groups.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the 2D-overlay layer node. | | 0x0001f368 | `_classCKeyboard` | `CKeyboard::class` (keyboard_node.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the on-screen keyboard node. | @@ -279,6 +279,7 @@ The seed and first expansion cover the application shell, script VM, scene graph | Math | [Math.md](Math.md) | 5 functions | | File Operations | [FileOps.md](FileOps.md) | 7 functions | | Disc | (no doc yet, narrative TODO) | 1 function | +| HUD & Picture | [HudNode.md](HudNode.md) | 9 functions | | Keyboard | [Keyboard.md](Keyboard.md) | 6 functions | | Locale | (no doc yet, narrative TODO) | 1 function | | NtIoSvc | [NtIoSvc.md](NtIoSvc.md) | 1 function | From 83819aa6553a84dece16bf0adea76d983642c883 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:42:58 -0500 Subject: [PATCH 070/167] Fixed the partition scan pointing every Folder at "2" ScanPartitions set the test folder's path to the index of the backslash instead of the drive, so every scan failed and logged "Cannot read directory: 2" on boot. It now uses the drive letter. Patched the shipped default.xip and default-xbox.xip by that one line, plus the sources. --- Data/Xips/default-xbox.xip | Bin 1650002 -> 1650023 bytes Data/Xips/default.xip | Bin 1686913 -> 1686934 bytes xips-source/default-desktop/default.xap | 2 +- xips-source/default-xbox/default.xap | 2 +- xips-source/default/default.xap | 2 +- 5 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Data/Xips/default-xbox.xip b/Data/Xips/default-xbox.xip index 9478969629bc15db2bf3cc1c22af8db2652433c5..254a91c5857a7cd14b1fb956b97f48deff2de911 100644 GIT binary patch delta 356 zcmca~E9LpE6ut=00D}}J1_mJ@WY(U@=T>i5!^i*w*E!f27T<&M7#OOavoSDmurL5M zfI-!JE(V4KMg-rV2UR{?fMI_=Lcis40fq!W1b?QeAj9dI2>Vjj3jt+05C*1f7iO5O zfY9*$x-i4mwFv&2RuKl4CIo*2yC{kSHZ&_TwktA%FcT0n12M~XMMhS!k37YtNyQ~a znR)3N20GLAKe39oJAYyYVm2UV2VxE&<^*CcAm#>Q9w6ogVm=_|2Vwyr76f7;AQlE< T5g-PcAqK?a+nqm2@LK}_a7a#n delta 351 zcmaEUE9KIy6ut=00D}}J1_mJ@e5W;$&#hjwhLHgV4sozCEWQWhF)*Y)XJcUCU||4i z0E5)`Tnr2ej0nCz52}2)0K@)#gnpId0t^X$2!4yGAj9dI2>blk3jt+05C-~h7iO5O zfY9*hx-i4mwFv(7RuKl4CIo*ryC{kSW;ZJ`wktA%FcT0n12M~XMMhS!k35-qDXA6y zX&Oo~F-q+bpICvI4T#x+m;;D8ftU-3xq+Amhds(iWt!}(@}e%03k3<-V+eygP*!_yfE`?4+x)dLMc7?^cSm|?O4 zLc`x*!VFv2BKVtDh%mfXL-6OG7h(8~#Gmm^gdrNq0iP>HQ5^WWQIa888lm59ZgUW0 zdk`ZCGXXI(5VHU=>+~Q-Hg=xk(xl>&qRhN>4FjF&fsAaT?Wv4xK+F!r96-zo#9Tnk r4a7V^%nQVPK+F%s0zfPX#6mzU48$Tp4AL(K#Nt3Ku|1VhQf3bTfAvWl delta 402 zcmbRCJFW3|8gGPWfWZMK1_n_eJUEfpyVXCz3=F*`%rIF2 zq2a|ZVTP@15&XF;L>OMHA^08VMHqe~@tePiFhnCc;9jLDiUaR8N-_jXBlH{2Z4P2= z4`KviCLm@8Viq7~ogT!<#?F(Omy%lHpQfP{6Qk5##mEN4>_E%`#GF9P1;pGy%mc){ mK+Ffk{6H)K#DYL91jNEXECR%$Kr9Bt;y^61y^2v%W)A?w+(*p- diff --git a/xips-source/default-desktop/default.xap b/xips-source/default-desktop/default.xap index 06c4971..94aac5d 100644 --- a/xips-source/default-desktop/default.xap +++ b/xips-source/default-desktop/default.xap @@ -3532,7 +3532,7 @@ function ScanPartitions() var sDrive = Drives[i]; var Flag = 1 << i; - Test.path = sDrive.indexOf("\\"); + Test.path = sDrive.substring(0, sDrive.indexOf("\\")); nFolders = Test.subFolders.length(); nFiles = Test.files.length(); nTotal = nFolders + nFiles; diff --git a/xips-source/default-xbox/default.xap b/xips-source/default-xbox/default.xap index 85f8694..d4da830 100644 --- a/xips-source/default-xbox/default.xap +++ b/xips-source/default-xbox/default.xap @@ -3465,7 +3465,7 @@ function ScanPartitions() var sDrive = Drives[i]; var Flag = 1 << i; - Test.path = sDrive.indexOf("\\"); + Test.path = sDrive.substring(0, sDrive.indexOf("\\")); nFolders = Test.subFolders.length(); nFiles = Test.files.length(); nTotal = nFolders + nFiles; diff --git a/xips-source/default/default.xap b/xips-source/default/default.xap index 7172677..13483f3 100644 --- a/xips-source/default/default.xap +++ b/xips-source/default/default.xap @@ -3372,7 +3372,7 @@ function ScanPartitions() var sDrive = Drives[i]; var Flag = 1 << i; - Test.path = sDrive.indexOf("\\"); + Test.path = sDrive.substring(0, sDrive.indexOf("\\")); nFolders = Test.subFolders.length(); nFiles = Test.files.length(); nTotal = nFolders + nFiles; From 32095a858573c649ab395c8a62297389707e541e Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:43:03 -0500 Subject: [PATCH 071/167] Added the 5960 Background fade, alpha, SetAlpha and DisappearAfter Background now has the fade and alpha properties and the two script functions the retail dash has, written from the binary. The backdrop draws blended with its alpha as the texture factor, so it fades over the sky color; at the default alpha of 1 it looks the same as before. The bgfx shim already handled the texture factor alpha, so it didn't need changes. --- docs/decomp/SceneGroups.md | 5 ++ theseus/render/scene_groups.cpp | 99 +++++++++++++++++++++++++++------ theseus/render/scene_groups.h | 11 ++++ 3 files changed, 98 insertions(+), 17 deletions(-) diff --git a/docs/decomp/SceneGroups.md b/docs/decomp/SceneGroups.md index 5dc54ce..dc293f8 100644 --- a/docs/decomp/SceneGroups.md +++ b/docs/decomp/SceneGroups.md @@ -419,3 +419,8 @@ loaded (`+0x28`): clear arrive, bind this level's control as the controller, cal - An Inline whose XIP fails to load runs FinishLoad with the absolute url never written. - A Background, being a plain CNode, clears its own visible flag the first time a group renders it. +- Background and Transform fades keep running if fade is set to 0 mid fade. The next + Advance divides by 0, clamps to the end value and overwrites any alpha stored directly in + the meantime. +- Background's PRD table lists `fade` at `+0x30` and `alpha` at `+0x48` after skyColor, + backdrop and isBound; an `alpha` write goes through the SetAlpha path, so it fades too. diff --git a/theseus/render/scene_groups.cpp b/theseus/render/scene_groups.cpp index b4be5b4..cabdf7f 100755 --- a/theseus/render/scene_groups.cpp +++ b/theseus/render/scene_groups.cpp @@ -1504,18 +1504,32 @@ START_NODE_PROPS(CBackground, CNode) NODE_PROP(pt_vec3, CBackground, skyColor) NODE_PROP(pt_node, CBackground, backdrop) NODE_PROP(pt_boolean, CBackground, isBound) + NODE_PROP(pt_number, CBackground, fade) + NODE_PROP(pt_number, CBackground, alpha) END_NODE_PROPS() +#define _FND_CLASS CBackground +START_NODE_FUN(CBackground, CNode) + NODE_FUN_VN(SetAlpha) + NODE_FUN_VN(DisappearAfter) +END_NODE_FUN() +#undef _FND_CLASS + // CBackground::CBackground() // Reconstructed from 5960 retail XBE @ 0x0004fc90. Ghidra decompile: -// skyColor = 0; backdrop = 0; isBound = 0; vb = 0; (+ the fade fields below) +// skyColor = 0; backdrop = 0; isBound = 0; fade = 0; alphaClock = 0.0; +// alphaStart = alphaEnd = 0; alpha = 1.0; disappear = 0.0; vb = 0; // if (g_background == 0) { g_background = this; isBound = 1; } -// 5960's Background also has fade, alpha, SetAlpha and DisappearAfter; Theseus -// doesn't carry them. CBackground::CBackground() : m_backdrop(NULL), m_skyColor(0.0f, 0.0f, 0.0f), m_isBound(false), + m_fade(0.0f), + m_timeAlphaStart(0.0), + m_alphaStart(0.0f), + m_alphaEnd(0.0f), + m_alpha(1.0f), + m_timeToDisappear(0.0), m_vb(NULL) { if (TheseusGetBackground() == NULL) @@ -1538,16 +1552,57 @@ CBackground::~CBackground() TheseusSetBackground(NULL); } +// CBackground::SetAlpha(a) [script fn 'SetAlpha', FND 0x00187f10, sig 19] +// Reconstructed from 5960 retail XBE @ 0x0004fde0: +// if (fade > 0) { alphaClock = now; alphaStart = alpha; alphaEnd = a; } +// else alpha = a; +void CBackground::SetAlpha(float a) +{ + if (m_fade > 0.0f) + { + m_timeAlphaStart = TheseusGetNow(); + m_alphaStart = m_alpha; + m_alphaEnd = a; + } + else + { + m_alpha = a; + } +} + +// CBackground::DisappearAfter(t) [script fn 'DisappearAfter', FND 0x00187f28, sig 19] +// Reconstructed from 5960 retail XBE @ 0x0004fe20: +// disappear = (double)(t + (float)now); +// The sum is single precision in the binary, kept that way here. +void CBackground::DisappearAfter(float t) +{ + m_timeToDisappear = (double)(t + (float)TheseusGetNow()); +} + // CBackground::Advance(nSeconds) -// Reconstructed from 5960 retail XBE @ 0x0004fe30. CNode::Advance, the -// backdrop's Advance, then on first use a 4 vertex D3DVERTEX strip at -// z = -10000 covering the screen, uv (1,1) (0,1) (1,0) (0,0). The normals -// hold copies of the corner positions as the binary writes them (nz = 1). -// The binary steps its alpha fade first; Theseus has none. +// Reconstructed from 5960 retail XBE @ 0x0004fe30. CNode::Advance, then the +// alpha fade: t = (now - alphaClock) / fade, clock stopped and t = 1 once it +// gets there, alpha = alphaStart + (alphaEnd - alphaStart) * t. A disappear +// time that has come clears itself and sets alpha to 0. Then the backdrop's +// Advance, and on first use a 4 vertex D3DVERTEX strip at z = -10000 covering +// the screen, uv (1,1) (0,1) (1,0) (0,0). The normals hold copies of the +// corner positions as the binary writes them (nz = 1). void CBackground::Advance(float nSeconds) { CNode::Advance(nSeconds); + if (m_timeAlphaStart > 0.0) + { + float t = FadeStep(m_timeAlphaStart, m_fade); + m_alpha = m_alphaStart + (m_alphaEnd - m_alphaStart) * t; + } + + if (m_timeToDisappear > 0.0 && m_timeToDisappear <= TheseusGetNow()) + { + m_timeToDisappear = 0.0; + m_alpha = 0.0f; + } + if (m_backdrop != NULL) m_backdrop->Advance(nSeconds); @@ -1592,10 +1647,9 @@ void CBackground::Advance(float nSeconds) // texture (nothing to do without one), binds it to stage 0 and clears stage // 1, saves projection, world and view, sets the ortho screen projection with // identity world and view, and draws the strip from Advance with lighting, -// z writes and backface culling (CCW) saved, set and restored around it. -// Color comes from the texture. The binary's alpha also comes from a texture -// factor driven by Background's fade, with blending on; Theseus's Background -// has no fade, so it takes alpha from the texture and draws with blending off. +// z writes, blending and culling saved, set (off, off, on, CCW) and restored. +// Color comes from the texture, alpha from the texture factor, +// (int)(alpha * 255) << 24, so the backdrop fades over the sky color. // The app only calls this when there's a backdrop. void CBackground::RenderBackdrop() { @@ -1624,16 +1678,17 @@ void CBackground::RenderBackdrop() TheseusGetRenderState(D3DRS_ZWRITEENABLE, &zwrite); TheseusSetRenderState(D3DRS_LIGHTING, FALSE); - TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); + TheseusSetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); TheseusSetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); TheseusSetRenderState(D3DRS_ZWRITEENABLE, FALSE); TheseusSetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); - TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE); + TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR); TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); TheseusSetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE); TheseusSetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 0); + TheseusSetRenderState(D3DRS_TEXTUREFACTOR, (DWORD)(int)(m_alpha * 255.0f) << 24); TheseusGetD3DDev()->SetStreamSource(0, m_vb, sizeof(D3DVERTEX)); TheseusGetD3DDev()->SetVertexShader(D3DFVF_VERTEX); @@ -1666,11 +1721,21 @@ void CBackground::Bind() // if (prd->offset == 0x2c) { if (*value) { Bind inlined } } // else if (prd->offset == 0x48) { SetAlpha inlined; return 0; } // return 1; -// Setting isBound true binds; false does nothing. Theseus has no alpha. +// Setting isBound true binds; false does nothing. An alpha write fades like +// SetAlpha and the raw store is vetoed. bool CBackground::OnSetProperty(const PRD* pprd, const void* pvValue) { - if (PTR2INT(pprd->pbOffset) == offsetof(m_isBound) && *(const bool*)pvValue) - Bind(); + int offset = PTR2INT(pprd->pbOffset); + if (offset == offsetof(m_isBound)) + { + if (*(const bool*)pvValue) + Bind(); + } + else if (offset == offsetof(m_alpha)) + { + SetAlpha(*(const float*)pvValue); + return false; + } return true; } diff --git a/theseus/render/scene_groups.h b/theseus/render/scene_groups.h index 990c749..06e9933 100644 --- a/theseus/render/scene_groups.h +++ b/theseus/render/scene_groups.h @@ -18,13 +18,24 @@ class CBackground : public CNode void Bind(); void RenderBackdrop(); + void SetAlpha(float a); + void DisappearAfter(float t); + CNode* m_backdrop; D3DXVECTOR3 m_skyColor; bool m_isBound; + float m_fade; // seconds SetAlpha takes to get there + XTIME m_timeAlphaStart; + float m_alphaStart; + float m_alphaEnd; + float m_alpha; // backdrop opacity + XTIME m_timeToDisappear; + LPDIRECT3DVERTEXBUFFER8 m_vb; DECLARE_NODE_PROPS() + DECLARE_NODE_FUNCTIONS() }; class CLevel : public CGroup From 8bd1bd10f6ed1ddc3ec499a66cbe7c0d06c8c80f Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:43:43 -0500 Subject: [PATCH 072/167] Added the Text node's indent props and stamp The 5960 Text node has indentWidth and indentHeight, which start lines that far in until they drop below the indent height, and draws today's date after a prefix. Added both from the binary, with FormatDate for both builds. Everything without them still draws byte for byte the same. --- theseus/desktop/dashlocale.h | 1 + theseus/desktop/locale.cpp | 35 +++++++++++++ theseus/engine/locale_node.cpp | 35 +++++++++++++ theseus/engine/locale_node.h | 1 + theseus/render/text.cpp | 89 +++++++++++++++++++++------------- 5 files changed, 128 insertions(+), 33 deletions(-) diff --git a/theseus/desktop/dashlocale.h b/theseus/desktop/dashlocale.h index fec2ed2..0f93274 100755 --- a/theseus/desktop/dashlocale.h +++ b/theseus/desktop/dashlocale.h @@ -47,6 +47,7 @@ extern void FormatInteger(char* szBuf, int nNumber, int locale = LOCALE_CURRENT); extern void FormatBlocks(char* szBuf, int nNumber, int locale = LOCALE_CURRENT); extern bool FormatTime(char* szBuf, SIZE_T chLen, SYSTEMTIME* pst, int locale = LOCALE_CURRENT); +extern bool FormatDate(char* szBuf, SIZE_T chLen, SYSTEMTIME* pst, int locale = LOCALE_CURRENT); extern const char* Translate(const char* szString, char* szTranslate, int nLanguage = LANGUAGE_CURRENT); extern int g_nCurLanguage; diff --git a/theseus/desktop/locale.cpp b/theseus/desktop/locale.cpp index a4773f9..8d36e85 100755 --- a/theseus/desktop/locale.cpp +++ b/theseus/desktop/locale.cpp @@ -511,6 +511,41 @@ bool FormatTime(char* szBuf, SIZE_T chLen, SYSTEMTIME* pst, int locale/*=LOCALE_ return true; } +// FormatDate(szBuf, chLen, pst, locale) +// Reconstructed from 5960 retail XBE @ 0x0003cd40. Ghidra decompile, trimmed: +// if (locale == -1) locale = region << 16 | language; +// sep = GetDateSeparator(); +// 0x10000 (NA English): "%02d%c%02d%c%d " month, day, year +// regions 1, 2, 4 with language 1, 6 or 7: "%d%c%02d%c%02d " year, month, day +// anything else: "%02d%c%02d%c%d " day, month, year +// if (chLen < len + 2) return false; copy; return true; +// The Text node's prefix draws this. +bool FormatDate(char* szBuf, SIZE_T chLen, SYSTEMTIME* pst, int locale/*=LOCALE_CURRENT*/) +{ + if (locale == LOCALE_CURRENT) + locale = LOCALE_FROM_REGION_LANGUAGE(g_nCurRegion, g_nCurLanguage); + + char chSep = GetDateSeparator(); + int region = REGION_FROM_LOCALE(locale); + int language = LANGUAGE_FROM_LOCALE(locale); + bool bYearFirst = (region == XC_GAME_REGION_NA || region == XC_GAME_REGION_JAPAN || region == XC_GAME_REGION_RESTOFWORLD) && + (language == 1 || language == 6 || language == 7); + + char szDate[32]; + if (locale == LOCALE_NA_ENGLISH) + snprintf(szDate, countof(szDate), "%02d%c%02d%c%d ", pst->wMonth, chSep, pst->wDay, chSep, pst->wYear); + else if (bYearFirst) + snprintf(szDate, countof(szDate), "%d%c%02d%c%02d ", pst->wYear, chSep, pst->wMonth, chSep, pst->wDay); + else + snprintf(szDate, countof(szDate), "%02d%c%02d%c%d ", pst->wDay, chSep, pst->wMonth, chSep, pst->wYear); + + if (chLen < strlen(szDate) + 2) + return false; + + strcpy(szBuf, szDate); + return true; +} + void Locale_Exit() { FreeXlate(); diff --git a/theseus/engine/locale_node.cpp b/theseus/engine/locale_node.cpp index 6307106..da1801e 100755 --- a/theseus/engine/locale_node.cpp +++ b/theseus/engine/locale_node.cpp @@ -613,6 +613,41 @@ bool FormatTime(TCHAR* szBuf, SIZE_T chLen, SYSTEMTIME* pst, int locale/*=LOCALE return true; } +// FormatDate(szBuf, chLen, pst, locale) +// Reconstructed from 5960 retail XBE @ 0x0003cd40. Ghidra decompile, trimmed: +// if (locale == -1) locale = region << 16 | language; +// sep = GetDateSeparator(); +// 0x10000 (NA English): "%02d%c%02d%c%d " month, day, year +// regions 1, 2, 4 with language 1, 6 or 7: "%d%c%02d%c%02d " year, month, day +// anything else: "%02d%c%02d%c%d " day, month, year +// if (chLen < len + 2) return false; copy; return true; +// The Text node's prefix draws this. +bool FormatDate(TCHAR* szBuf, SIZE_T chLen, SYSTEMTIME* pst, int locale/*=LOCALE_CURRENT*/) +{ + if (locale == LOCALE_CURRENT) + locale = LOCALE_FROM_REGION_LANGUAGE(g_nCurRegion, g_nCurLanguage); + + TCHAR chSep = GetDateSeparator(); + int region = REGION_FROM_LOCALE(locale); + int language = LANGUAGE_FROM_LOCALE(locale); + bool bYearFirst = (region == XC_GAME_REGION_NA || region == XC_GAME_REGION_JAPAN || region == XC_GAME_REGION_RESTOFWORLD) && + (language == 1 || language == 6 || language == 7); + + TCHAR szDate[32]; + if (locale == LOCALE_NA_ENGLISH) + _sntprintf(szDate, countof(szDate), _T("%02d%c%02d%c%d "), pst->wMonth, chSep, pst->wDay, chSep, pst->wYear); + else if (bYearFirst) + _sntprintf(szDate, countof(szDate), _T("%d%c%02d%c%02d "), pst->wYear, chSep, pst->wMonth, chSep, pst->wDay); + else + _sntprintf(szDate, countof(szDate), _T("%02d%c%02d%c%d "), pst->wDay, chSep, pst->wMonth, chSep, pst->wYear); + + if (chLen < lstrlen(szDate) + 2) + return false; + + lstrcpy(szBuf, szDate); + return true; +} + void Locale_Exit() { FreeXlate(); diff --git a/theseus/engine/locale_node.h b/theseus/engine/locale_node.h index a62b929..68907cb 100755 --- a/theseus/engine/locale_node.h +++ b/theseus/engine/locale_node.h @@ -48,6 +48,7 @@ extern void FormatInteger(TCHAR* szBuf, int nNumber, int locale = LOCALE_CURRENT); extern void FormatBlocks(TCHAR* szBuf, int nNumber, int locale = LOCALE_CURRENT); extern bool FormatTime(TCHAR* szBuf, SIZE_T chLen, SYSTEMTIME* pst, int locale = LOCALE_CURRENT); +extern bool FormatDate(TCHAR* szBuf, SIZE_T chLen, SYSTEMTIME* pst, int locale = LOCALE_CURRENT); extern const TCHAR* Translate(const TCHAR* szString, TCHAR* szTranslate, int nLanguage = LANGUAGE_CURRENT); extern int g_nCurLanguage; diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp index 42bd175..3e99f94 100644 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -113,7 +113,8 @@ class CFont float Advance(int nGlyph) const { return m_pGlyphs[nGlyph].advanceX; } CGlyphShape* Shape(int nGlyph) const { return m_ppShapes[nGlyph]; } - void CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, D3DXVECTOR3* pMin, D3DXVECTOR3* pMax, float nFormatWidth, bool bDoNotBreak, float scale); + void CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, D3DXVECTOR3* pMin, D3DXVECTOR3* pMax, float nFormatWidth, bool bDoNotBreak, float scale, + float indentWidth = 0.0f, float indentHeight = 0.0f); HANDLE m_hFile; @@ -511,11 +512,12 @@ static void AddCursor(WORD* indices, int& nIndex, TEXTVERTEX* verts, int& nVerte // (or fades if the word has none), the second column is right justified, and // lines step down 1.5 after a paragraph or 1.0 inside one. Then every vertex // is scaled and the bounding box taken. See docs/decomp/Text.md. -// 5960 also passes an indent (width, height) the Text node exposes; Theseus -// has no indent props, so lines always restart at x = 0. 5960 only honors -// headings while the keyboard's asterisk formatting flag is on; Theseus always -// does. The bounds stop in the second pass and the glyph scale are Theseus's. -void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, D3DXVECTOR3* pMin, D3DXVECTOR3* pMax, float nFormatWidth, bool bDoNotBreak, float scale) +// Lines start indentWidth in until they drop below indentHeight. 5960 only +// honors headings while the keyboard's asterisk formatting flag is on; Theseus +// always does. The bounds stop in the second pass and the glyph scale are +// Theseus's. +void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, D3DXVECTOR3* pMin, D3DXVECTOR3* pMax, float nFormatWidth, bool bDoNotBreak, float scale, + float indentWidth, float indentHeight) { if (m_pSet == NULL) return; @@ -627,7 +629,7 @@ void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, // Pass 2: fill it. int nIndex = 0, nVertex = 0; - float x = 0.0f, y = 0.0f; + float x = indentWidth, y = 0.0f; float blue = 1.0f; bool bHeading = false; bool bParaStart = true; @@ -682,9 +684,9 @@ void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, if (ich != nChars) { - x = 0.0f; bool bTight = bHeading || ch == '\r' || !bSingleColumn; y -= bTight ? 1.0f : 1.5f; + x = y < -indentHeight ? 0.0f : indentWidth; } bHeading = false; bParaStart = true; @@ -729,8 +731,8 @@ void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, { if (wrapChar > -1) { - x = 0.0f; y -= 1.0f; + x = y < -indentHeight ? 0.0f : indentWidth; ich = wrapChar; nVertex = wrapVertex; nIndex = wrapIndex; @@ -1090,7 +1092,7 @@ float OverlayFontDraw(const TCHAR* text, float screenX, float screenY, float pix // extrusion 0x2c, width 0x30, translate 0x34, height 0x38, scroll 0x3c, // scrollRate 0x40, scrollDelay 0x44, indentWidth 0x48, indentHeight 0x4c, // mesh 0x50, bbox 0x54..0x6b, language 0x6c, scroll start time 0x70. -// Theseus has scrollHorizontal where 5960 has the two indent props. +// scrollHorizontal is Theseus's. class CTextNode : public CNode { @@ -1114,6 +1116,8 @@ class CTextNode : public CNode float m_scroll; float m_scrollRate; float m_scrollDelay; + float m_indentWidth; + float m_indentHeight; bool m_scrollHorizontal; DECLARE_NODE_PROPS() @@ -1124,6 +1128,8 @@ class CTextNode : public CNode XTIME m_timeToScroll; private: + typedef bool (*StampFormatter)(TCHAR* szBuf, SIZE_T chLen, SYSTEMTIME* pst, int locale); + int ExpandStamp(const TCHAR* szPrefix, StampFormatter pfnFormat, bool& bRebuild); void DropMesh(); }; @@ -1144,6 +1150,8 @@ START_NODE_PROPS(CTextNode, CNode) NODE_PROP(pt_number, CTextNode, scroll) NODE_PROP(pt_number, CTextNode, scrollRate) NODE_PROP(pt_number, CTextNode, scrollDelay) + NODE_PROP(pt_number, CTextNode, indentWidth) + NODE_PROP(pt_number, CTextNode, indentHeight) NODE_PROP(pt_boolean, CTextNode, scrollHorizontal) END_NODE_PROPS() @@ -1163,6 +1171,8 @@ CTextNode::CTextNode() : m_scroll(0.0f), m_scrollRate(0.0f), m_scrollDelay(0.0f), + m_indentWidth(0.0f), + m_indentHeight(0.0f), m_scrollHorizontal(false), m_pMesh(NULL), m_nLanguage(0), @@ -1248,19 +1258,44 @@ const TCHAR* FaceFromFont(const TCHAR* szFont) return szFont; } +// "" and "" texts draw the local time or date after the prefix. +// Refreshes the stored text when the value changes; returns the prefix length, +// or 0 when the text doesn't start with this prefix. +int CTextNode::ExpandStamp(const TCHAR* szPrefix, StampFormatter pfnFormat, bool& bRebuild) +{ + int cchPrefix = (int)_tcslen(szPrefix); + if (_tcsncmp(m_text, szPrefix, cchPrefix) != 0) + return 0; + + SYSTEMTIME st; + GetLocalTime(&st); + TCHAR szValue[32]; + szValue[0] = 0; + pfnFormat(szValue, countof(szValue), &st, LOCALE_CURRENT); + + if (_tcscmp(m_text + cchPrefix, szValue) != 0) + { + delete [] m_text; + m_text = new TCHAR[cchPrefix + _tcslen(szValue) + 1]; + _tcscpy(m_text, szPrefix); + _tcscpy(m_text + cchPrefix, szValue); + bRebuild = true; + } + return cchPrefix; +} + // CTextNode::Render() // Reconstructed from 5960 retail XBE @ 0x000559e0 (1331 bytes). Takes the // keyboard's string if one is waiting (a changed string or cursor rebuilds), -// expands , rebuilds on a language change, then builds the mesh with +// expands and , rebuilds on a language change, then builds the mesh with // the translated text, |width| as the format width and a negative width as // "don't wrap". Justify middle or end shifts left by half or all of the drawn // width (capped at |width|). With a height smaller than the text the mesh // fades top and bottom and draws twice, the second copy one gap below so the // scroll wraps around. Each draw pushes the world matrix, translates, turns // culling off for the flat glyphs and back to CCW. See docs/decomp/Text.md. -// 5960 also expands , passes the indent props and rebuilds every frame -// a cursorless keyboard string arrives; Theseus does none of those. The -// horizontal marquee, the draw counter and the desktop vertex alpha toggle +// 5960 also rebuilds every frame a cursorless keyboard string arrives, which +// draws the same mesh; Theseus skips that. The horizontal marquee, the draw counter and the desktop vertex alpha toggle // are Theseus's. void CTextNode::Render() { @@ -1281,27 +1316,14 @@ void CTextNode::Render() if (m_text == NULL || m_text[0] == 0) return; - static const TCHAR szClock[] = _T(""); - const int cchClock = 7; const TCHAR* szText = m_text; - if (_tcsncmp(m_text, szClock, cchClock) == 0) + int cchStamp = ExpandStamp(_T(""), FormatTime, bRebuild); + if (cchStamp == 0) + cchStamp = ExpandStamp(_T(""), FormatDate, bRebuild); + if (cchStamp != 0) { - SYSTEMTIME st; - GetLocalTime(&st); - TCHAR szTime[32]; - FormatTime(szTime, countof(szTime), &st); - - if (_tcscmp(m_text + cchClock, szTime) != 0) - { - delete [] m_text; - m_text = new TCHAR[cchClock + _tcslen(szTime) + 1]; - _tcscpy(m_text, szClock); - _tcscpy(m_text + cchClock, szTime); - bRebuild = true; - } - m_translate = false; - szText = m_text + cchClock; + szText = m_text + cchStamp; } if (m_translate && m_nLanguage != g_nCurLanguage) @@ -1320,7 +1342,8 @@ void CTextNode::Render() CFont* pFont = GetFont(FaceFromFont(m_font)); if (pFont == NULL || !pFont->IsOpen()) return; - pFont->CreateTextMesh(szText, -1, &m_pMesh, &m_bboxMin, &m_bboxMax, fabsf(m_width), m_width < 0.0f, 1.0f); + pFont->CreateTextMesh(szText, -1, &m_pMesh, &m_bboxMin, &m_bboxMax, fabsf(m_width), m_width < 0.0f, 1.0f, + m_indentWidth, m_indentHeight); if (m_pMesh == NULL) return; } From bcfdc832e2d6f57a6fe883db3b092fa0f0feb13e Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:44:26 -0500 Subject: [PATCH 073/167] Asterisk headings only show when a script turns them on In 5960 *heading* lines are gated by one global flag that starts off, and the keyboard's Set/GetAsteriskFormattingState drive it. Theseus always formatted them and kept the flag per keyboard, so a "****" passcode drew as two tinted stars. Text now reads the one flag. No shipped XAP or language string uses headings otherwise. --- theseus/desktop/keyboard.cpp | 7 +++---- theseus/render/text.cpp | 15 +++++++++------ theseus/shared/keyboard_node.cpp | 7 +++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/theseus/desktop/keyboard.cpp b/theseus/desktop/keyboard.cpp index df7e4c4..55ef5b1 100755 --- a/theseus/desktop/keyboard.cpp +++ b/theseus/desktop/keyboard.cpp @@ -12,6 +12,7 @@ extern CNode* GetTextNode(const char* szText, float nWidth); extern bool g_bActiveKey; extern char* g_szText; +extern bool g_bAsteriskFormatting; // text.cpp, gates *heading* text extern int g_nTextChar; extern int g_nCurLanguage; @@ -250,7 +251,6 @@ class CKeyboard : public CNode bool m_caps; int m_mode; int m_jmode; - bool m_asteriskFormatting; void selectKey(int nRow, int nColumn); bool validKey(int nRow, int nColumn); @@ -268,8 +268,8 @@ class CKeyboard : public CNode void CycleMode(); void Insert(const char* szInsert); - bool GetAsteriskFormattingState() { return m_asteriskFormatting; } - void SetAsteriskFormattingState(bool b) { m_asteriskFormatting = b; } + bool GetAsteriskFormattingState() { return g_bAsteriskFormatting; } + void SetAsteriskFormattingState(bool b) { g_bAsteriskFormatting = b; } void Advance(float nSeconds); void Render(); @@ -332,7 +332,6 @@ CKeyboard::CKeyboard() : m_jmode(jmodeHiragana), m_shift(false), m_caps(false), - m_asteriskFormatting(false), m_string(NULL) { m_nLength = 0; diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp index 3e99f94..1f1dd5f 100644 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -22,6 +22,10 @@ TCHAR* g_szText = NULL; int g_nTextChar = -1; int g_nTextCharLast = -1; +// *heading* formatting, off until a script turns it on through the keyboard's +// SetAsteriskFormattingState (0x0017e74c, a BSS byte in 5960). +bool g_bAsteriskFormatting = false; + // D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE (0x52), 28 bytes a vertex. struct TEXTVERTEX { @@ -444,10 +448,10 @@ static inline bool AtColumnEnd(const TCHAR* text, int i, int n) } // A column that starts and ends with '*' is a heading: the stars are hidden -// and the text is tinted. True when text[i] opens one. +// and the text is tinted. True when text[i] opens one and headings are on. static bool OpensHeading(const TCHAR* text, int i, int n) { - if (i >= n || text[i] != '*') + if (!g_bAsteriskFormatting || i >= n || text[i] != '*') return false; int end = i + 1; while (end < n && !IsLineEnd(text[end])) @@ -512,10 +516,9 @@ static void AddCursor(WORD* indices, int& nIndex, TEXTVERTEX* verts, int& nVerte // (or fades if the word has none), the second column is right justified, and // lines step down 1.5 after a paragraph or 1.0 inside one. Then every vertex // is scaled and the bounding box taken. See docs/decomp/Text.md. -// Lines start indentWidth in until they drop below indentHeight. 5960 only -// honors headings while the keyboard's asterisk formatting flag is on; Theseus -// always does. The bounds stop in the second pass and the glyph scale are -// Theseus's. +// Lines start indentWidth in until they drop below indentHeight. Headings only +// count while the asterisk formatting flag is on. The bounds stop in the second +// pass and the glyph scale are Theseus's. void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, D3DXVECTOR3* pMin, D3DXVECTOR3* pMax, float nFormatWidth, bool bDoNotBreak, float scale, float indentWidth, float indentHeight) { diff --git a/theseus/shared/keyboard_node.cpp b/theseus/shared/keyboard_node.cpp index af3060b..8a9b3be 100755 --- a/theseus/shared/keyboard_node.cpp +++ b/theseus/shared/keyboard_node.cpp @@ -19,6 +19,7 @@ extern CNode* GetTextNode(const TCHAR* szText, float nWidth); extern bool g_bActiveKey; extern TCHAR* g_szText; +extern bool g_bAsteriskFormatting; // text.cpp, gates *heading* text extern int g_nTextChar; extern int g_nCurLanguage; @@ -222,7 +223,6 @@ class CKeyboard : public CNode bool m_caps; int m_mode; int m_jmode; - bool m_asteriskFormatting; void selectKey(int nRow, int nColumn); void selectUp(); @@ -239,8 +239,8 @@ class CKeyboard : public CNode void CycleMode(); void Insert(const TCHAR* szInsert); - bool GetAsteriskFormattingState() { return m_asteriskFormatting; } - void SetAsteriskFormattingState(bool b) { m_asteriskFormatting = b; } + bool GetAsteriskFormattingState() { return g_bAsteriskFormatting; } + void SetAsteriskFormattingState(bool b) { g_bAsteriskFormatting = b; } void Advance(float nSeconds); void Render(); @@ -304,7 +304,6 @@ CKeyboard::CKeyboard() : m_jmode(JMODE_HIRAGANA), m_shift(false), m_caps(false), - m_asteriskFormatting(false), m_string(NULL) { m_nLength = 0; From cf874dfca171c1ca7c37257838453b82a1d5c46b Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:45:34 -0500 Subject: [PATCH 074/167] Shipped XIP and script fixes now reach existing installs Data was copied into the user folder once and never touched again, so no dashboard fix ever reached anyone who had already run Theseus. Now a .shipped list remembers what we copied. Untouched copies get replaced when the shipped file changes, copies you edited are left alone, and on older installs a differing engine file is kept as .old before it's updated. Skins, music, orbs and screenshots are never replaced. --- theseus/desktop/app_paths.cpp | 142 +++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 3 deletions(-) diff --git a/theseus/desktop/app_paths.cpp b/theseus/desktop/app_paths.cpp index 7d6a31b..4e826ec 100644 --- a/theseus/desktop/app_paths.cpp +++ b/theseus/desktop/app_paths.cpp @@ -7,7 +7,10 @@ #include #include #include +#include #include +#include +#include #include "host.h" @@ -93,6 +96,136 @@ static void CopyTree(const char* src, const char* dst) { Host_CloseDir(dir); } +// Data/ is shipped content, so updates have to reach installs that already +// have a copy. .shipped remembers what we copied (shipped size + mtime) and +// what the user's copy looked like right after. A copy that still matches is +// ours to replace; one the user changed is theirs and stays put. +struct ShippedRec { uint64_t shipSize; int64_t shipTime; uint64_t userSize; int64_t userTime; }; +static std::map s_shipped; +static bool s_shippedDirty = false; + +static std::string ShippedManifestPath() { + return std::string(Plat_UserDataDir()) + "/Data/.shipped"; +} + +static void LoadShippedManifest() { + FILE* f = fopen(ShippedManifestPath().c_str(), "r"); + if (!f) return; + char line[1400]; + while (fgets(line, sizeof(line), f)) { + char rel[1024]; unsigned long long ss, us; long long st, ut; + if (sscanf(line, "%llu %lld %llu %lld %1023[^\n]", &ss, &st, &us, &ut, rel) == 5) + s_shipped[rel] = { ss, st, us, ut }; + } + fclose(f); +} + +static void SaveShippedManifest() { + if (!s_shippedDirty) return; + std::string path = ShippedManifestPath(), tmp = path + ".tmp"; + FILE* f = fopen(tmp.c_str(), "w"); + if (!f) return; + for (const auto& kv : s_shipped) + fprintf(f, "%llu %lld %llu %lld %s\n", (unsigned long long)kv.second.shipSize, (long long)kv.second.shipTime, + (unsigned long long)kv.second.userSize, (long long)kv.second.userTime, kv.first.c_str()); + if (fclose(f) == 0) Host_RenameReplace(tmp.c_str(), path.c_str()); +} + +static bool SameFileBytes(const char* a, const char* b) { + FILE* fa = fopen(a, "rb"); + FILE* fb = fopen(b, "rb"); + bool same = fa && fb; + char ba[65536], bb[65536]; + while (same) { + size_t na = fread(ba, 1, sizeof(ba), fa), nb = fread(bb, 1, sizeof(bb), fb); + if (na != nb || memcmp(ba, bb, na) != 0) same = false; + if (na == 0) break; + } + if (fa) fclose(fa); + if (fb) fclose(fb); + return same; +} + +// Copy beside the target, then swap it in, so a crash never leaves half a file. +static bool ReplaceFile(const char* src, const char* dst) { + std::string part = std::string(dst) + ".part"; + remove(part.c_str()); + CopyFile(src, part.c_str()); + HostFileInfo pi; + if (!Host_Stat(part.c_str(), &pi)) return false; + if (!Host_RenameReplace(part.c_str(), dst)) { remove(part.c_str()); return false; } + return true; +} + +static void Remember(const std::string& rel, const HostFileInfo& ship, const char* dst) { + HostFileInfo ui; + if (!Host_Stat(dst, &ui)) return; + s_shipped[rel] = { ship.size, ship.mtime, ui.size, ui.mtime }; + s_shippedDirty = true; +} + +// Engine content: a stale copy of these breaks the dash, so on installs from +// before .shipped existed a differing copy is set aside as .old and replaced. +// Everything else (skins, music, orbs, screenshots) is left to the user. +static bool IsEngineContent(const std::string& rel) { + static const char* kDirs[] = { "Data/Xips/", "Data/System/", "Data/Fonts/", "Data/Language/" }; + for (const char* d : kDirs) + if (strncasecmp(rel.c_str(), d, strlen(d)) == 0) return true; + return false; +} + +static void SyncShippedFile(const char* src, const char* dst, const std::string& rel) { + HostFileInfo ship, user; + if (!Host_Stat(src, &ship)) return; + + if (!Host_Stat(dst, &user)) { + CopyFile(src, dst); + Remember(rel, ship, dst); + return; + } + + auto it = s_shipped.find(rel); + if (it != s_shipped.end()) { + const ShippedRec& r = it->second; + bool shipChanged = r.shipSize != ship.size || r.shipTime != ship.mtime; + bool userUntouched = r.userSize == user.size && r.userTime == user.mtime; + if (shipChanged && userUntouched && ReplaceFile(src, dst)) { + Remember(rel, ship, dst); + fprintf(stderr, "[Paths] updated %s\n", rel.c_str()); + } + return; + } + + // Copied before .shipped existed, so we can't tell who last touched it. + if (SameFileBytes(src, dst)) { + Remember(rel, ship, dst); + } else if (IsEngineContent(rel)) { + std::string old = std::string(dst) + ".old"; + if (Host_RenameReplace(dst, old.c_str()) && ReplaceFile(src, dst)) { + Remember(rel, ship, dst); + fprintf(stderr, "[Paths] updated %s (previous copy kept as .old)\n", rel.c_str()); + } + } +} + +static void SyncShippedTree(const char* src, const char* dst, const std::string& rel) { + Plat_MkdirP(dst); + HostDir* dir = Host_OpenDir(src); + if (!dir) return; + char name[260]; + HostFileInfo info; + while (Host_ReadDir(dir, name, sizeof(name), &info)) { + if (!strcmp(name, ".") || !strcmp(name, "..")) continue; + char s[1024], d[1024]; + snprintf(s, sizeof(s), "%s/%s", src, name); + snprintf(d, sizeof(d), "%s/%s", dst, name); + std::string r = rel + "/" + name; + if (info.attrs & HOST_ATTR_DIR) SyncShippedTree(s, d, r); + else SyncShippedFile(s, d, r); + } + Host_CloseDir(dir); +} + // Copy one tree into the user dir if it isn't there yet. static void SeedTree(const char* rel) { char dst[1024], src[1024]; @@ -120,6 +253,7 @@ void AppPaths_Init() { char dataDir[1024]; snprintf(dataDir, sizeof(dataDir), "%s/Data", Plat_ShippedDir()); Plat_MkdirP(AppPath_Tree("Data")); + LoadShippedManifest(); HostDir* dir = Host_OpenDir(dataDir); if (!dir) return; @@ -129,11 +263,13 @@ void AppPaths_Init() { if (!(info.attrs & HOST_ATTR_DIR)) continue; if (!strcmp(name, ".") || !strcmp(name, "..")) continue; if (!strcasecmp(name, "shaders")) continue; - char rel[1024]; - snprintf(rel, sizeof(rel), "Data/%s", name); - SeedTree(rel); + char src[1024], dst[1024]; + snprintf(src, sizeof(src), "%s/%s", dataDir, name); + snprintf(dst, sizeof(dst), "%s/Data/%s", user, name); + SyncShippedTree(src, dst, std::string("Data/") + name); } Host_CloseDir(dir); + SaveShippedManifest(); } const char* AppPathf(const char* fmt, ...) { From 77bed12cc1bd815d3184dfb7dfe36745908a20a9 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:46:15 -0500 Subject: [PATCH 075/167] Skip text writes when a mesh create or lock fails A text mesh that couldn't be created or locked used to get written through a NULL pointer. Now the node goes without a mesh and tries again next frame, and the scroll fades skip a frame instead. Tested by forcing each failure. --- theseus/render/text.cpp | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp index 1f1dd5f..c54c94f 100644 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -623,12 +623,25 @@ void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, LPD3DXMESH pMesh = NULL; TheseusCreateMeshFVF(nFaces, nVerts, D3DXMESH_MANAGED, TEXT_FVF, &pMesh); - *ppMesh = pMesh; + if (pMesh == NULL) + return; - WORD* indices; - TEXTVERTEX* verts; - pMesh->LockIndexBuffer(D3DLOCK_DISCARD, (BYTE**)&indices); - pMesh->LockVertexBuffer(D3DLOCK_DISCARD, (BYTE**)&verts); + // No mesh at all beats one written through a failed lock; the node + // tries again next frame. + WORD* indices = NULL; + TEXTVERTEX* verts = NULL; + if (FAILED(pMesh->LockIndexBuffer(D3DLOCK_DISCARD, (BYTE**)&indices))) + { + pMesh->Release(); + return; + } + if (FAILED(pMesh->LockVertexBuffer(D3DLOCK_DISCARD, (BYTE**)&verts))) + { + pMesh->UnlockIndexBuffer(); + pMesh->Release(); + return; + } + *ppMesh = pMesh; // Pass 2: fill it. int nIndex = 0, nVertex = 0; @@ -1377,10 +1390,12 @@ void CTextNode::Render() { float wrap = copy == 0 ? 0.0f : contentW + SCROLL_GAP; TEXTVERTEX* verts; - m_pMesh->LockVertexBuffer(0, (BYTE**)&verts); - float left = m_scroll - 1.0f; - HorizontalFade(verts, m_pMesh->GetNumVertices(), left, left + window + 2.0f, wrap); - m_pMesh->UnlockVertexBuffer(); + if (SUCCEEDED(m_pMesh->LockVertexBuffer(0, (BYTE**)&verts))) + { + float left = m_scroll - 1.0f; + HorizontalFade(verts, m_pMesh->GetNumVertices(), left, left + window + 2.0f, wrap); + m_pMesh->UnlockVertexBuffer(); + } if (copy == 1 && m_scroll + window < contentW) break; @@ -1393,9 +1408,11 @@ void CTextNode::Render() if (m_scroll == 0.0f) top += 1.0f; // no top fade until it starts moving TEXTVERTEX* verts; - m_pMesh->LockVertexBuffer(0, (BYTE**)&verts); - VerticalFade(verts, m_pMesh->GetNumVertices(), top, bottom, wrap); - m_pMesh->UnlockVertexBuffer(); + if (SUCCEEDED(m_pMesh->LockVertexBuffer(0, (BYTE**)&verts))) + { + VerticalFade(verts, m_pMesh->GetNumVertices(), top, bottom, wrap); + m_pMesh->UnlockVertexBuffer(); + } if (copy == 1 && m_scroll + m_height < contentH) break; From daa95a1577404d2fe34df003bc573a078fd4fb22 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:46:33 -0500 Subject: [PATCH 076/167] Reworded two text notes Comment and doc wording only. --- docs/decomp/Text.md | 4 ++-- theseus/render/text.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/decomp/Text.md b/docs/decomp/Text.md index 260cad4..3684877 100644 --- a/docs/decomp/Text.md +++ b/docs/decomp/Text.md @@ -183,8 +183,8 @@ Then with a positive format width: a single column takes the whole width; two co it shrink the first to `width - col2` and turn fading on. Without a width fading is off; otherwise fading starts as the don't-break flag. -`XAppCreateMeshFVF(faces, verts, D3DXMESH_MANAGED (0x220), 0x52)` (`0x0003d2f0`, which frees -caches and retries on out of memory), then both buffers are locked and written without checks. +The mesh create helper (`0x0003d2f0`) makes it with D3DXMESH_MANAGED (0x220) and FVF 0x52, freeing +caches and retrying on out of memory. Then both buffers are locked and written without checks. Second pass. Color is white with the blue byte from a float that headings set to 0.5 and their closing `*` sets back to 1.0: `0xffffff00 | (int)(blue * 255)`. x starts at the indent width. diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp index c54c94f..271da1f 100644 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -865,7 +865,7 @@ static TCHAR* CopyString(const TCHAR* sz) // else { fonts[count].face = face; fonts[count].file = copy(path); count++; } // CloseHandle; // Theseus looks in its one font directory and takes XTF0 only, since that's -// all CFont reads. A bad header still closes the file (5960 leaks it). +// all CFont reads. A bad header still closes the file (5960 leaves it open). static void InitFontTable() { const TCHAR* szDir = TheseusGetFontDir(); From 392ded1914551f2bd867ba2c0fda586148368825 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:47:35 -0500 Subject: [PATCH 077/167] Japanese glyphs draw at full size like 5960 Text shrank every glyph past index 190 to 90%, but only while drawing, not while measuring, so wrapped Japanese lines came out short. 5960 draws every glyph at full size, so now we do too. --- theseus/render/text.cpp | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp index 271da1f..d6e20ce 100644 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -466,13 +466,6 @@ static inline bool CanBreakAfter(WCHAR ch) return ch == ' ' || (ch >= 0x3040 && ch < 0xf000) || ch >= 0xff66; } -// Glyphs past index 190 come from the Japanese set and are drawn 10% smaller. -// Theseus's; the 5960 layout uses every glyph at full size. -static inline float GlyphScale(int nGlyph) -{ - return nGlyph > 190 ? 0.9f : 1.0f; -} - // Appends the blinking cursor quad, 0.1 wide from y - 0.1 to y + 0.75. // Reconstructed from 5960 retail XBE @ 0x00055030 (CFont::CreateCursorMesh): // a = |sin((now + now) * PI)|; if (!visible) a = 0; color = (int)(a * 255) << 24 | 0xffffff; @@ -773,13 +766,12 @@ void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, for (int i = 0; i < pShape->nIndices; i++) indices[nIndex++] = (WORD)(nVertex + pShape->indices[i]); - float glyphScale = GlyphScale(nGlyph); DWORD color = 0xffffff00 | (DWORD)(blue * 255.0f); for (int i = 0; i < pShape->nPoints; i++) { TEXTVERTEX& v = verts[nVertex++]; - v.x = x + pShape->points[i].x * glyphScale; - v.y = y + pShape->points[i].y * glyphScale; + v.x = x + pShape->points[i].x; + v.y = y + pShape->points[i].y; v.z = 0.0f; v.nx = 0.0f; v.ny = 0.0f; @@ -790,7 +782,7 @@ void CFont::CreateTextMesh(const TCHAR* pchText, int nChars, LPD3DXMESH* ppMesh, if (ich - 1 == g_nTextChar) AddCursor(indices, nIndex, verts, nVertex, x, y, true); - x += Advance(nGlyph) * glyphScale; + x += Advance(nGlyph); if (column == 2) col2Width += Advance(nGlyph); From 5fdc482f651617a6ea8f381934241d1c4bd70df3 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:54:20 -0500 Subject: [PATCH 078/167] Settings saves can't leave a half-written file anymore desktop.ini (with the Plex, Jellyfin and xCloud logins), games.ini, Icons.ini, playlists and the XAP editor all wrote straight over their file, so a crash mid-save lost it. They write a .new file now and swap it in with one rename, keeping the previous version as .old. --- theseus/desktop/playlist.cpp | 6 ++++-- theseus/desktop/safe_save.h | 32 ++++++++++++++++++++++++++++++++ theseus/desktop/sdl_main.cpp | 6 ++++-- theseus/desktop/title_maker.cpp | 6 ++++-- theseus/desktop/virtual_games.h | 11 +++++++---- theseus/desktop/xap_editor.cpp | 10 +++++++--- 6 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 theseus/desktop/safe_save.h diff --git a/theseus/desktop/playlist.cpp b/theseus/desktop/playlist.cpp index 8384d24..efd23a5 100644 --- a/theseus/desktop/playlist.cpp +++ b/theseus/desktop/playlist.cpp @@ -3,6 +3,7 @@ #include "app_paths.h" #include "playlist.h" #include "xboxfs_drive.h" +#include "safe_save.h" #include #include @@ -57,7 +58,8 @@ void Playlist_LoadAll() { void Playlist_SaveAll() { Plat_MkdirP(AppPath_Tree("Configs")); - FILE* fp = fopen(kPath, "w"); + SafeSave save(kPath); + FILE* fp = save.f; if (!fp) return; for (size_t i = 0; i < s_playlists.size(); i++) { const Playlist& p = s_playlists[i]; @@ -68,7 +70,7 @@ void Playlist_SaveAll() { } fprintf(fp, "\n"); } - fclose(fp); + save.Commit(); s_dirty = false; } diff --git a/theseus/desktop/safe_save.h b/theseus/desktop/safe_save.h new file mode 100644 index 0000000..359b272 --- /dev/null +++ b/theseus/desktop/safe_save.h @@ -0,0 +1,32 @@ +// safe_save.h: settings writes that can't leave a half-written file behind. +#pragma once +#include +#include +#include "host.h" + +// Write through f, then Commit. The file is built as .new and swapped in +// with one rename, and the version it replaces is kept as .old. Never +// committing (an early return) throws the new file away and leaves the old. +struct SafeSave { + std::string path, tmp; + FILE* f = nullptr; + + explicit SafeSave(const char* p, const char* mode = "w") : path(p), tmp(path + ".new") { + f = fopen(tmp.c_str(), mode); + } + ~SafeSave() { + if (f) { fclose(f); remove(tmp.c_str()); } + } + bool Commit() { + if (!f) return false; + bool ok = !ferror(f); + ok = (fclose(f) == 0) && ok; + f = nullptr; + if (!ok) { remove(tmp.c_str()); return false; } + HostFileInfo info; + if (Host_Stat(path.c_str(), &info)) + Host_CopyFile(path.c_str(), (path + ".old").c_str(), false); + if (!Host_RenameReplace(tmp.c_str(), path.c_str())) { remove(tmp.c_str()); return false; } + return true; + } +}; diff --git a/theseus/desktop/sdl_main.cpp b/theseus/desktop/sdl_main.cpp index 825cd35..77412b9 100644 --- a/theseus/desktop/sdl_main.cpp +++ b/theseus/desktop/sdl_main.cpp @@ -491,7 +491,8 @@ static void RereadLegacyFromDisk() { void SaveDesktopSettings() { Plat_MkdirP(AppPath_Tree("Configs")); RereadLegacyFromDisk(); - FILE* fp = fopen(AppPath("Configs/desktop.ini"), "w"); + SafeSave save(AppPath("Configs/desktop.ini")); + FILE* fp = save.f; if (!fp) return; fprintf(fp, "[Desktop]\n"); fprintf(fp, "XemuPath=%s\n", s_xemuPath); @@ -558,7 +559,7 @@ void SaveDesktopSettings() { fprintf(fp, "Use Progressive=%s\n", g_useProgressive); fprintf(fp, "\n[Dashboard Settings]\n"); fprintf(fp, "Current Skin=%s\n", g_currentSkin); - fclose(fp); + save.Commit(); } // Helper: returns true when ImGui has an active text input widget (Title Maker, inspector search, etc.) @@ -1095,6 +1096,7 @@ void RenderXcloudOverlay() #if defined(THESEUS_HAVE_WEBRTC) #include "xcloud_video.h" +#include "safe_save.h" #include diff --git a/theseus/desktop/title_maker.cpp b/theseus/desktop/title_maker.cpp index d8ca548..259bb2d 100644 --- a/theseus/desktop/title_maker.cpp +++ b/theseus/desktop/title_maker.cpp @@ -24,6 +24,7 @@ #include #include "host.h" +#include "safe_save.h" bool g_titleMakerOpen = false; @@ -271,12 +272,13 @@ static int TM_ReadIconsIni(char keys[][128], char vals[][128]) { } static void TM_WriteIconsIni(char keys[][128], char vals[][128], int count) { - FILE* fp = fopen(AppPath("Configs/Icons.ini"), "w"); + SafeSave save(AppPath("Configs/Icons.ini")); + FILE* fp = save.f; if (!fp) return; fprintf(fp, "[default]\n"); for (int i = 0; i < count; i++) fprintf(fp, "%s=%s\n", keys[i], vals[i]); - fclose(fp); + save.Commit(); } // Open a URL via the platform's default handler. Falls through the diff --git a/theseus/desktop/virtual_games.h b/theseus/desktop/virtual_games.h index a677cb9..28be62c 100644 --- a/theseus/desktop/virtual_games.h +++ b/theseus/desktop/virtual_games.h @@ -11,6 +11,7 @@ #include #include #include "xboxfs_drive.h" +#include "safe_save.h" // Roomy enough for a full local library plus a signed-in xCloud/Game Pass // account injected at boot (see xcloud_vgames.cpp), and now an imported ES-DE @@ -155,7 +156,8 @@ inline void VGames_Reload() { inline void VGames_Save() { Plat_MkdirP(AppPath_Tree("Configs")); - FILE* fp = fopen(VGAMES_INI, "w"); + SafeSave save(VGAMES_INI); + FILE* fp = save.f; if (!fp) return; for (int i = 0; i < g_vgames.count; i++) { @@ -172,7 +174,7 @@ inline void VGames_Save() { if (g.group[0]) fprintf(fp, "Group=%s\n", g.group); fprintf(fp, "\n"); } - fclose(fp); + save.Commit(); } inline int VGames_Add(const char* name, const char* titleID, const char* launch, @@ -236,7 +238,8 @@ inline void VGames_DeleteByName(const char* name) { g_vgames.games[idx].valid = false; g_vgames.generation++; - FILE* fp = fopen(AppPath("Configs/games.ini"), "w"); + SafeSave save(AppPath("Configs/games.ini")); + FILE* fp = save.f; if (fp) { for (int i = 0; i < g_vgames.count; i++) { if (!g_vgames.games[i].valid) continue; @@ -249,7 +252,7 @@ inline void VGames_DeleteByName(const char* name) { fprintf(fp, "Category=%s\n", g_vgames.games[i].category); fprintf(fp, "\n"); } - fclose(fp); + save.Commit(); } fprintf(stderr, "[VGames] Deleted: %s\n", name); } diff --git a/theseus/desktop/xap_editor.cpp b/theseus/desktop/xap_editor.cpp index 8ea63f1..4b9ba64 100644 --- a/theseus/desktop/xap_editor.cpp +++ b/theseus/desktop/xap_editor.cpp @@ -20,6 +20,7 @@ #include #include #include "host.h" +#include "safe_save.h" // Implemented in group.cpp extern "C" int CollectInlineNodes(CNode* pRoot, CInstance* pRootInst, void* outBuf, int maxEntries); @@ -136,15 +137,18 @@ void XapEditor_LoadFile(const char* path) { static void XapEditor_SaveFile() { if (!g_xapEditorPath[0]) return; std::string text = s_editor.GetText(); - FILE* f = fopen(g_xapEditorPath, "wb"); + SafeSave save(g_xapEditorPath, "wb"); + FILE* f = save.f; if (!f) { snprintf(g_xapEditorStatus, sizeof(g_xapEditorStatus), "Failed to save!"); g_xapEditorStatusTime = (float)SDL_GetTicks() / 1000.0f; return; } fwrite(text.c_str(), 1, text.size(), f); - fclose(f); - snprintf(g_xapEditorStatus, sizeof(g_xapEditorStatus), "Saved %zu bytes", text.size()); + if (save.Commit()) + snprintf(g_xapEditorStatus, sizeof(g_xapEditorStatus), "Saved %zu bytes", text.size()); + else + snprintf(g_xapEditorStatus, sizeof(g_xapEditorStatus), "Failed to save!"); g_xapEditorStatusTime = (float)SDL_GetTicks() / 1000.0f; } From 1e98a3bf6d6c7eab0af314d4176ffbcaa94dede6 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:54:20 -0500 Subject: [PATCH 079/167] Mac and Linux keep a log file too Only Windows wrote theseus.log, and only when launched from Explorer. Now mac and Linux copy everything into theseus.log in the user folder while it still shows in the terminal, the last run is kept as theseus.prev.log, and a crash writes its backtrace straight into the log. --- theseus/desktop/host_posix_app.cpp | 86 ++++++++++++++++++++++++++++-- theseus/desktop/host_win_app.cpp | 6 +++ 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/theseus/desktop/host_posix_app.cpp b/theseus/desktop/host_posix_app.cpp index ea28432..92a38ba 100644 --- a/theseus/desktop/host_posix_app.cpp +++ b/theseus/desktop/host_posix_app.cpp @@ -6,10 +6,16 @@ #include #include +#include +#include +#include +#include +#include #include #include #include #include +#include #ifdef __APPLE__ #include #endif @@ -18,17 +24,89 @@ #define HAS_BACKTRACE 1 #endif -void Host_ConsoleOrLog(const char*) {} +// Everything still reaches the terminal, and a copy lands in theseus.log, so a +// Finder or desktop-entry launch leaves something a user can send. The last +// run's log is kept as theseus.prev.log. +static int s_termFd = -1; +static int s_logFd = -1; +static pthread_t s_teeThread; +static bool s_teeRunning = false; + +static void* TeeThread(void* arg) { + int in = (int)(intptr_t)arg; + char buf[4096]; + ssize_t n; + while ((n = read(in, buf, sizeof(buf))) > 0) { + if (s_termFd >= 0) (void)!write(s_termFd, buf, (size_t)n); + if (s_logFd >= 0) (void)!write(s_logFd, buf, (size_t)n); + } + close(in); + return NULL; +} + +// Closing the only write ends lets the thread drain the pipe and finish. +static void StopTee() { + if (!s_teeRunning) return; + fflush(stdout); + fflush(stderr); + close(STDOUT_FILENO); + close(STDERR_FILENO); + pthread_join(s_teeThread, NULL); + s_teeRunning = false; +} + +void Host_ConsoleOrLog(const char* logPath) { + // First launch: the user data folder doesn't exist yet. + std::string dir = logPath; + dir = dir.substr(0, dir.rfind('/')); + for (size_t i = 1; i <= dir.size(); i++) + if (i == dir.size() || dir[i] == '/') + mkdir(dir.substr(0, i).c_str(), 0755); + + std::string prev = logPath; + size_t dot = prev.rfind(".log"); + prev = (dot == std::string::npos) ? prev + ".prev" : prev.substr(0, dot) + ".prev.log"; + rename(logPath, prev.c_str()); + + s_logFd = open(logPath, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (s_logFd < 0) return; + int p[2]; + if (pipe(p) != 0) return; + + fflush(stdout); + fflush(stderr); + s_termFd = dup(STDERR_FILENO); + dup2(p[1], STDOUT_FILENO); + dup2(p[1], STDERR_FILENO); + close(p[1]); + setvbuf(stdout, NULL, _IOLBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + + if (pthread_create(&s_teeThread, NULL, TeeThread, (void*)(intptr_t)p[0]) == 0) { + s_teeRunning = true; + atexit(StopTee); + } +} void Host_BoostProcess() {} +// Writes straight to the terminal and the log: the tee thread may never get +// to run again once we're in here. static void CrashHandler(int sig) { - fprintf(stderr, "\n[CRASH] Signal %d (%s)\n", sig, sig == SIGSEGV ? "SIGSEGV" : sig == SIGABRT ? "SIGABRT" : "OTHER"); - fflush(stderr); + char line[96]; + int len = snprintf(line, sizeof(line), "\n[CRASH] Signal %d (%s)\n", sig, + sig == SIGSEGV ? "SIGSEGV" : sig == SIGABRT ? "SIGABRT" : sig == SIGBUS ? "SIGBUS" : "OTHER"); + int fds[2] = { s_termFd >= 0 ? s_termFd : STDERR_FILENO, s_logFd }; #ifdef HAS_BACKTRACE void* bt[64]; int n = backtrace(bt, 64); - backtrace_symbols_fd(bt, n, 2); #endif + for (int i = 0; i < 2; i++) { + if (fds[i] < 0) continue; + (void)!write(fds[i], line, (size_t)len); +#ifdef HAS_BACKTRACE + backtrace_symbols_fd(bt, n, fds[i]); +#endif + } _exit(128 + sig); } diff --git a/theseus/desktop/host_win_app.cpp b/theseus/desktop/host_win_app.cpp index a2770a4..70f3b9e 100644 --- a/theseus/desktop/host_win_app.cpp +++ b/theseus/desktop/host_win_app.cpp @@ -46,6 +46,12 @@ void Host_ConsoleOrLog(const char* logPath) { *slash = '\0'; Host_MakeDir(dir); } + char prev[MAX_PATH]; + snprintf(prev, sizeof(prev), "%s", logPath); + char* ext = strstr(prev, ".log"); + if (ext && (size_t)(ext - prev) + sizeof(".prev.log") <= sizeof(prev)) + strcpy(ext, ".prev.log"); + MoveFileExA(logPath, prev, MOVEFILE_REPLACE_EXISTING); if (freopen(logPath, "w", stdout)) { setvbuf(stdout, NULL, _IOLBF, 0); _dup2(_fileno(stdout), _fileno(stderr)); From 43df338669a0f40001ecc3ed0fe06a76fab389cd Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:57:36 -0500 Subject: [PATCH 080/167] Space on a full keyboard string no longer buzzes The Xbox keyboard played the error sound when SPACE hit the 31 character limit. 5960 and the desktop keyboard just ignore it, so now the Xbox one does too. --- theseus/shared/keyboard_node.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/theseus/shared/keyboard_node.cpp b/theseus/shared/keyboard_node.cpp index 8a9b3be..5b0583b 100755 --- a/theseus/shared/keyboard_node.cpp +++ b/theseus/shared/keyboard_node.cpp @@ -561,7 +561,10 @@ void CKeyboard::activate() case KEY_BS: Backspace(); break; case KEY_SPACE: - InsertChar(_T(' ')); + // Space on a full string is ignored without the error sound. + ClearSelection(); + if (m_nLength < KB_MAX_LENGTH) + InsertChar(_T(' ')); break; default: From 26ebf50575bce1d63fe2924e3fb4d4c02de0d2ac Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:58:08 -0500 Subject: [PATCH 081/167] Added the 5960 DiscDrive, Joystick and LiveAccounts props DiscDrive.bDeferNotification, Joystick.LastActivePort, LiveAccounts.currentAccount and newAccountFromMU were in 5960's prop tables but missing here, so the shipped scripts that read or clear them got nothing. Desktop gets the deferred disc event too. Needs an Xbox test with a remote and a second pad. --- theseus/desktop/desktop_nodes.cpp | 30 +++++++++++++++++-- theseus/desktop/joystick.cpp | 6 ++++ theseus/xbox/disc_management.cpp | 4 +++ theseus/xbox/input.cpp | 32 +++++++++++++++++++++ theseus/xbox/live_accounts.cpp | 48 ++++++++++++++++++++++++++++++- 5 files changed, 117 insertions(+), 3 deletions(-) diff --git a/theseus/desktop/desktop_nodes.cpp b/theseus/desktop/desktop_nodes.cpp index 1b8db5d..b3d2b2a 100644 --- a/theseus/desktop/desktop_nodes.cpp +++ b/theseus/desktop/desktop_nodes.cpp @@ -1201,7 +1201,7 @@ class CDiscDrive : public CNode public: static CDiscDrive* s_instance; - CDiscDrive() : m_discType(NULL), m_locked(false), m_pollTimer(2.0f) { + CDiscDrive() : m_discType(NULL), m_locked(false), m_bDeferNotification(false), m_pollTimer(2.0f) { m_discType = strdup("none"); s_instance = this; } @@ -1211,10 +1211,22 @@ class CDiscDrive : public CNode DECLARE_NODE_FUNCTIONS() TCHAR* m_discType; bool m_locked; + bool m_bDeferNotification; float m_pollTimer; + // CDiscDrive::Advance, 5960 @ 0x00039f20: a change seen while locked sets + // bDeferNotification; once unlocked the next frame clears it and fires + // OnDiscInserted, or OnDiscRemoved when the drive is empty. + void FireDeferred() { + if (!m_bDeferNotification || m_locked) return; + m_bDeferNotification = false; + bool isNone = !m_discType || strcmp(m_discType, "none") == 0; + CallFunction(this, isNone ? "OnDiscRemoved" : "OnDiscInserted"); + } + void Advance(float nSeconds) { CNode::Advance(nSeconds); + FireDeferred(); m_pollTimer += nSeconds; if (m_pollTimer < 2.0f) return; m_pollTimer = 0.0f; @@ -1240,6 +1252,8 @@ class CDiscDrive : public CNode CallFunction(this, "OnDiscInserted"); else if (isNone && !wasNone) CallFunction(this, "OnDiscRemoved"); + } else if (isNone != wasNone) { + m_bDeferNotification = true; } } @@ -1308,13 +1322,17 @@ void DiscDrive_SetDiscType(const char* type) { // Audio CD (or anything else): let the XAP script decide via OnDiscInserted if (!dd->m_locked) CallFunction(dd, "OnDiscInserted"); + else + dd->m_bDeferNotification = true; fprintf(stderr, "[DiscDrive] discType='%s', fired OnDiscInserted\n", type); } } +// 5960 PRD @ 0x00174550: discType, locked, bDeferNotification. START_NODE_PROPS(CDiscDrive, CNode) NODE_PROP(pt_string, CDiscDrive, discType) NODE_PROP(pt_boolean, CDiscDrive, locked) + NODE_PROP(pt_boolean, CDiscDrive, bDeferNotification) END_NODE_PROPS() #undef _FND_CLASS @@ -2731,7 +2749,8 @@ END_NODE_FUN() class CLiveAccounts : public CNode { public: - CLiveAccounts() : m_nCurrentIndex(0), m_bLogon(false), m_fLogOnSuccess(false), m_fLogOnInProgress(false) {} + CLiveAccounts() : m_nCurrentIndex(0), m_bLogon(false), m_fLogOnSuccess(false), m_fLogOnInProgress(false), + m_currentAccount(-1), m_newAccountFromMU(-1) {} DECLARE_NODE(CLiveAccounts, CNode) DECLARE_NODE_PROPS() @@ -2740,6 +2759,11 @@ class CLiveAccounts : public CNode int m_nCurrentIndex; bool m_bLogon, m_fLogOnSuccess, m_fLogOnInProgress; + // 5960 PRD @ 0x00176234: currentAccount and newAccountFromMU, both -1 + // until a logon or an MU account shows up. Desktop has neither. + int m_currentAccount; + int m_newAccountFromMU; + int GetNumberOfAccounts() { return 0; } int GetNumAccountsOnHD() { return 0; } CStrObject* GetAccountName(int) { return new CStrObject; } @@ -2767,6 +2791,8 @@ START_NODE_PROPS(CLiveAccounts, CNode) NODE_PROP(pt_boolean, CLiveAccounts, fLogOnSuccess) NODE_PROP(pt_boolean, CLiveAccounts, fLogOnInProgress) NODE_PROP(pt_integer, CLiveAccounts, nCurrentIndex) + NODE_PROP(pt_integer, CLiveAccounts, currentAccount) + NODE_PROP(pt_integer, CLiveAccounts, newAccountFromMU) END_NODE_PROPS() #undef _FND_CLASS diff --git a/theseus/desktop/joystick.cpp b/theseus/desktop/joystick.cpp index 8ed35fb..1e8e179 100755 --- a/theseus/desktop/joystick.cpp +++ b/theseus/desktop/joystick.cpp @@ -74,6 +74,10 @@ class CJoystick : public CNode bool m_enableRemote; bool m_enableGamepad; + // 5960's LastActivePort (PRD 0x001747e8) is the Xbox port that last gave + // input. Desktop folds the pad and keyboard into one player, so it stays 0. + int m_LastActivePort; + void Advance(float nSeconds); bool OnSetProperty(const PRD *pprd, const void *pvValue); @@ -148,6 +152,7 @@ NODE_PROP(pt_string, CJoystick, secretKey) NODE_PROP(pt_boolean, CJoystick, enableSecretKey) NODE_PROP(pt_boolean, CJoystick, enableGamepad) NODE_PROP(pt_boolean, CJoystick, enableRemote) +NODE_PROP(pt_integer, CJoystick, LastActivePort) END_NODE_PROPS() #undef _FND_CLASS @@ -185,6 +190,7 @@ CJoystick::CJoystick() : m_frequency(20.0f), m_eatSecretKey(false), m_enableRemote(true), m_enableGamepad(true), + m_LastActivePort(0), m_isBound(false), m_prevMediaKeys(0) { diff --git a/theseus/xbox/disc_management.cpp b/theseus/xbox/disc_management.cpp index bbe5181..6fe67fd 100755 --- a/theseus/xbox/disc_management.cpp +++ b/theseus/xbox/disc_management.cpp @@ -93,9 +93,13 @@ CNodeArray CDiscDrive::c_drives; IMPLEMENT_NODE("DiscDrive", CDiscDrive, CNode) +// 5960 PRD @ 0x00174550: discType (string, +0x1c), locked (bool, +0x24), +// bDeferNotification (bool, +0x2c). Scripts clear the deferred flag to drop +// a disc change that landed while the drive was locked. START_NODE_PROPS(CDiscDrive, CNode) NODE_PROP(pt_string, CDiscDrive, discType) NODE_PROP(pt_boolean, CDiscDrive, locked) +NODE_PROP(pt_boolean, CDiscDrive, bDeferNotification) END_NODE_PROPS() #define _FND_CLASS CDiscDrive diff --git a/theseus/xbox/input.cpp b/theseus/xbox/input.cpp index 6f067bc..c0d69fa 100755 --- a/theseus/xbox/input.cpp +++ b/theseus/xbox/input.cpp @@ -67,6 +67,8 @@ class CJoystick : public CNode bool m_enableRemote; bool m_enableGamepad; + int m_LastActivePort; + void Advance(float nSeconds); bool OnSetProperty(const PRD *pprd, const void *pvValue); @@ -139,6 +141,7 @@ NODE_PROP(pt_string, CJoystick, secretKey) NODE_PROP(pt_boolean, CJoystick, enableSecretKey) NODE_PROP(pt_boolean, CJoystick, enableGamepad) NODE_PROP(pt_boolean, CJoystick, enableRemote) +NODE_PROP(pt_integer, CJoystick, LastActivePort) END_NODE_PROPS() #define _FND_CLASS CJoystick @@ -176,6 +179,7 @@ CJoystick::CJoystick() : m_frequency(20.0f), m_eatSecretKey(false), m_enableRemote(true), m_enableGamepad(true), + m_LastActivePort(0), m_isBound(false), m_remoteIdleTime(0.0f), m_timeRemoteRemoved(0.0f), @@ -429,6 +433,30 @@ void CJoystick::EnableGlobalInput(int bEnable) // Threshold for analog buttons #define XINPUT_ANALOGBUTTONTHRESHOLD 32 +static float ConvertThumb(SHORT sThumb); + +// LastActivePort, 5960 PRD @ 0x001747e8 (int, +0x94), written by +// CJoystick::Advance @ 0x00041040 and its pad merge @ 0x00040e00: +// remote: a key that maps to a script function -> port +// pad: wButtons != 0, an analog button > 0x20, or a thumb past +// the 0.9 dead zone -> port +// 5960 merges all pads before testing; Theseus walks them one at a time, +// so the last port that's active in a poll wins either way. +static bool IsPadActive(const XINPUT_GAMEPAD *pad) +{ + if (pad->wButtons != 0) + return true; + + for (int i = 0; i < 8; i++) + { + if (pad->bAnalogButtons[i] > 0x20) + return true; + } + + return ConvertThumb(pad->sThumbLX) != 0.0f || ConvertThumb(pad->sThumbLY) != 0.0f || + ConvertThumb(pad->sThumbRX) != 0.0f || ConvertThumb(pad->sThumbRY) != 0.0f; +} + static float ConvertThumb(SHORT sThumb) { float axis = (float)sThumb / 32767.0f; @@ -671,6 +699,7 @@ void CJoystick::Advance(float nSeconds) const REMOTE_CODE_TABLE *pFunction = GetCodeFunction(state.IrRemote.wKeyCode); if (pFunction != NULL) { + m_LastActivePort = nJoypad; TRACE(_T("IR Code: 0x%04x %s\n"), state.IrRemote.wKeyCode, pFunction->szFunction1); CallFunction(pFunction->szFunction1, true); @@ -694,6 +723,9 @@ void CJoystick::Advance(float nSeconds) // return; } + if (IsPadActive(&joystate.Gamepad)) + m_LastActivePort = nJoypad; + if (!EqualGamepads(&joystate.Gamepad, &m_joystate[nJoypad].Gamepad) /*memcmp(&joystate.Gamepad, &m_joystate[nJoypad].Gamepad, sizeof (XINPUT_GAMEPAD)) != 0*/) { if (ResetScreenSaver()) diff --git a/theseus/xbox/live_accounts.cpp b/theseus/xbox/live_accounts.cpp index e76b5f0..99f41f6 100755 --- a/theseus/xbox/live_accounts.cpp +++ b/theseus/xbox/live_accounts.cpp @@ -48,12 +48,16 @@ class CLiveAccounts : public CNode CStrObject* GetNumberOfFriendsOnline(); int InternalLogon(int index, const TCHAR* passcode); bool LoadFromHD(); + void FindNewAccountFromMU(const XONLINE_USER_HD *previous, DWORD numPrevious); // 5960 additions int GetLastLogonUser(); CStrObject* GetResult(); void LaunchDashUpdate(); int m_nCurrentIndex; bool m_bLogon; + + int m_currentAccount; + int m_newAccountFromMU; bool m_fLogOnSuccess; bool m_fLogOnInProgress; @@ -70,6 +74,10 @@ START_NODE_PROPS(CLiveAccounts, CNode) NODE_PROP(pt_boolean, CLiveAccounts, fLogOnSuccess) NODE_PROP(pt_boolean, CLiveAccounts, fLogOnInProgress) NODE_PROP(pt_integer, CLiveAccounts, nCurrentIndex) + // 5960 PRD @ 0x00176234 also has currentAccount (int, +0x24) and + // newAccountFromMU (int, +0x2c); both start at -1. + NODE_PROP(pt_integer, CLiveAccounts, currentAccount) + NODE_PROP(pt_integer, CLiveAccounts, newAccountFromMU) END_NODE_PROPS() #define _FND_CLASS CLiveAccounts @@ -99,7 +107,9 @@ START_NODE_FUN(CLiveAccounts, CNode) END_NODE_FUN() #undef _FND_CLASS -CLiveAccounts::CLiveAccounts() {} +// The 5960 reset helper @ 0x00068fe0 starts currentAccount and +// newAccountFromMU at -1. +CLiveAccounts::CLiveAccounts() : m_currentAccount(-1), m_newAccountFromMU(-1) {} CLiveAccounts::~CLiveAccounts() {} XONLINE_USER_HD g_Users[XONLINE_MAX_STORED_ONLINE_USERS] = { 0 }; @@ -183,10 +193,16 @@ bool CLiveAccounts::LoadFromHD() if (!g_bStarted && !XboxLive_Startup()) return false; + XONLINE_USER_HD previous[XONLINE_MAX_STORED_ONLINE_USERS]; + DWORD numPrevious = g_NumUsers; + CopyMemory(previous, g_Users, sizeof(previous)); + ZeroMemory(g_Users, sizeof(g_Users)); g_NumUsers = 0; HRESULT hr = _XOnlineGetUsersFromHD(g_Users, &g_NumUsers); + if (SUCCEEDED(hr)) + FindNewAccountFromMU(previous, numPrevious); TCHAR buf[128]; if (FAILED(hr) || g_NumUsers == 0) { @@ -208,6 +224,34 @@ bool CLiveAccounts::LoadFromHD() return true; } +// newAccountFromMU, from CLiveAccounts::EnumerateAccounts @ 0x00069470: +// newAccountFromMU = -1; +// if (oldCount != 0) for each new user i: +// if ((options & 0x80000000) && FindByGamertag(old, name, 16) < 0) +// newAccountFromMU = i; +// An account that came from an MU and wasn't in the last list is new. The +// last one found wins. Theseus reads the hard drive list, where the MU bit +// doesn't appear, so this stays -1 until the list includes MU accounts. +void CLiveAccounts::FindNewAccountFromMU(const XONLINE_USER_HD *previous, DWORD numPrevious) +{ + m_newAccountFromMU = -1; + if (numPrevious == 0) + return; + + for (DWORD i = 0; i < g_NumUsers; i++) + { + if ((g_Users[i].dwUserOptions & XONLINE_USER_OPTION_CAME_FROM_MU) == 0) + continue; + + bool bKnown = false; + for (DWORD j = 0; j < numPrevious && !bKnown; j++) + bKnown = strncmp(g_Users[i].name, previous[j].name, sizeof(g_Users[i].name)) == 0; + + if (!bKnown) + m_newAccountFromMU = (int)i; + } +} + int CLiveAccounts::GetNumberOfAccounts() { OutputDebugString(_T("[LiveAccounts] GetNumberOfAccounts() called\n")); @@ -295,6 +339,7 @@ int CLiveAccounts::InternalLogon(int index, const TCHAR* passcode) OutputDebugString(buf); m_nCurrentIndex = index; + m_currentAccount = index; m_bLogon = true; m_fLogOnSuccess = true; m_fLogOnInProgress = false; @@ -330,6 +375,7 @@ void CLiveAccounts::Logoff() OutputDebugString(_T("[LiveAccounts] Logoff() called\n")); m_bLogon = false; m_nCurrentIndex = -1; + m_currentAccount = -1; } int CLiveAccounts::IsPasswordVerified() From db681cf88dd9834d16935df954d6b9f82b1292c1 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:58:19 -0500 Subject: [PATCH 082/167] Shift no longer turns kana and the euro sign into Latin letters The Xbox keyboard's case change cut each key down to its low byte before uppercasing it, so a shifted kana typed a stray Latin letter and the euro key showed as a not sign. Now only a to z change case. --- theseus/shared/keyboard_node.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/theseus/shared/keyboard_node.cpp b/theseus/shared/keyboard_node.cpp index 5b0583b..2cbeb93 100755 --- a/theseus/shared/keyboard_node.cpp +++ b/theseus/shared/keyboard_node.cpp @@ -337,8 +337,9 @@ void CKeyboard::ClearSelection() TCHAR CKeyboard::ApplyCase(TCHAR ch) { - if (m_caps ^ m_shift) - ch = (TCHAR)toupper((unsigned char)ch); + // Only a-z change case; anything wider passes through untouched. + if ((m_caps ^ m_shift) && ch >= 'a' && ch <= 'z') + ch = (TCHAR)(ch - 'a' + 'A'); return ch; } From af94aeaf88180ced0679015908c4e0b807a0214b Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:59:12 -0500 Subject: [PATCH 083/167] Script calls can take an int and a string and return an int again 5960's call dispatcher has a signature 29 for int (int, string) that I'd left out because nothing here used it. LiveAccounts.VerifyPassword needs it, so it's back. Checked from script with a test node: the int truncates, the string converts, and a wrong argument count still errors. --- docs/decomp/Node.md | 2 +- theseus/render/node.h | 5 +++++ theseus/render/node_system.cpp | 8 +++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/decomp/Node.md b/docs/decomp/Node.md index 990e3dc..b6ff414 100644 --- a/docs/decomp/Node.md +++ b/docs/decomp/Node.md @@ -304,6 +304,6 @@ Variable and instance function reads are bounds checked (out of range reads give - **CNodeArray::RemoveNode moves bytes, not pointers.** The close-up copy uses `rep movsd` with a count of `(length - i - 1) >> 2` followed by `(length - i - 1) & 3` single bytes, so it moves `length - i - 1` bytes when it should move that many 4 byte pointers. Removing anything but one of the last few entries leaves stale pointers in the tail. - **Unknown Call signatures are silent.** Signature 6 and anything above 29 return null without reporting an error. -- **Signature 29** (`int (int, str)`) exists in the dispatcher. +- **Signature 29** (`int (int, str)`) exists in the dispatcher. Its one user is `LiveAccounts.VerifyPassword` (FND `0x00188de0`). - **The CNumArray and CIntArray SetSize bodies are one function** in the image; both property types call `0x0002f180`. - **CNode::OnLoad and CNodeClass::FindProp are not separate functions.** OnLoad carries its own inlined copy of FindMemberFunction, and the class compiler (`CClassCompiler::ParseProps`, `0x00036440`) calls CObject::FindProp directly on a class record, whose GetPropMap returns its `m_rgprd`. diff --git a/theseus/render/node.h b/theseus/render/node.h index 585239c..0d97ed9 100755 --- a/theseus/render/node.h +++ b/theseus/render/node.h @@ -235,6 +235,7 @@ typedef CObject* (__cdecl CObject::*NODE_PFN)(void); #define sig_viis 26 #define sig_ni 27 #define sig_vis 28 +#define sig_iis 29 union FSIG { @@ -251,6 +252,7 @@ union FSIG int (CObject::*pfn_is)(const TCHAR*); int (CObject::*pfn_isi)(const TCHAR*, int); int (CObject::*pfn_iii)(int, int); + int (CObject::*pfn_iis)(int, const TCHAR*); CStrObject* (CObject::*pfn_sv)(void); CStrObject* (CObject::*pfn_si)(int); CStrObject* (CObject::*pfn_sii)(int, int); @@ -313,6 +315,9 @@ struct FND #define NODE_FUN_III(name) \ { (NODE_PFN)(int (CObject::*)(int, int))&_FND_CLASS::name, sig_iii, _T(#name) }, +#define NODE_FUN_IIS(name) \ + { (NODE_PFN)(int (CObject::*)(int, const TCHAR*))&_FND_CLASS::name, sig_iis, _T(#name) }, + #define NODE_FUN_SI(name) \ { (NODE_PFN)(CStrObject* (CObject::*)(int))&_FND_CLASS::name, sig_si, _T(#name) }, diff --git a/theseus/render/node_system.cpp b/theseus/render/node_system.cpp index 9e9be8b..21d6a50 100755 --- a/theseus/render/node_system.cpp +++ b/theseus/render/node_system.cpp @@ -317,6 +317,7 @@ static const SIGSHAPE c_rgSigShape[] = { 'v', "iis" }, // 26 sig_viis { 'n', "i" }, // 27 sig_ni { 'v', "is" }, // 28 sig_vis + { 'i', "is" }, // 29 sig_iis }; // CObject::Call @@ -329,9 +330,9 @@ static const SIGSHAPE c_rgSigShape[] = // ""), calls, wraps an int or float result in a new CNumObject, and // Releases the converted temporaries. A wrong count reports "invalid number // of parameters" through the running script; an unknown signature returns -// NULL. The binary also has a case 29 (int from int and string) that no -// Theseus FND uses, so it isn't carried here. Theseus adds the CHECK on a -// negative index, which would read before the FND table. +// NULL. Case 29 is int from (int, string), which LiveAccounts.VerifyPassword +// uses. Theseus adds the CHECK on a negative index, which would read before +// the FND table. CObject* CObject::Call(int nFunction, CObject** rgparam, int nParam) { if (!CHECK(nFunction >= 0)) @@ -414,6 +415,7 @@ CObject* CObject::Call(int nFunction, CObject** rgparam, int nParam) case sig_ii: nRet = (this->*f.pfn_ii)(nArg[0]); break; case sig_iii: nRet = (this->*f.pfn_iii)(nArg[0], nArg[1]); break; case sig_is: nRet = (this->*f.pfn_is)(szArg[0]); break; + case sig_iis: nRet = (this->*f.pfn_iis)(nArg[0], szArg[1]); break; case sig_nv: flRet = (this->*f.pfn_nv)(); break; case sig_ni: flRet = (this->*f.pfn_ni)(nArg[0]); break; From b2634777f2e0ecb428de0de1da3f051093fe9122 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:00:17 -0500 Subject: [PATCH 084/167] Added LiveAccounts.IsPasswordEnabled and VerifyPassword, and readText widens text Live Today asks both before it lets you into Friends or Voice, and neither was callable, so the passcode check never ran. They read the stored account's passcode flag and pad code the way 5960 does. File.readText on Xbox now widens an ANSI file before appending it like 5960 does, instead of reading the bytes as wide text. The other three 5960 functions on the list (ScreenSaver.reset, Joystick.EnableGlobalInput, Mesh.load) were already here. Needs a hardware test with a passcoded account. --- theseus/desktop/desktop_nodes.cpp | 6 +++ theseus/shared/file_ops.cpp | 24 +++++++----- theseus/xbox/live_accounts.cpp | 62 +++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 9 deletions(-) diff --git a/theseus/desktop/desktop_nodes.cpp b/theseus/desktop/desktop_nodes.cpp index b3d2b2a..6394956 100644 --- a/theseus/desktop/desktop_nodes.cpp +++ b/theseus/desktop/desktop_nodes.cpp @@ -2782,6 +2782,10 @@ class CLiveAccounts : public CNode CStrObject* GetFriendInvites() { return new CStrObject; } CStrObject* GetNumberOfFriendsOnline() { return new CStrObject(_T("0")); } void LaunchEntryPoint(int, int, const TCHAR*) {} + // 5960 @ 0x000698b0 / 0x000698e0: an index past the account list reads + // as no passcode and a passcode that matches. Desktop has no accounts. + int IsPasswordEnabled(int) { return 0; } + int VerifyPassword(int, const TCHAR*) { return 1; } }; IMPLEMENT_NODE("LiveAccounts", CLiveAccounts, CNode) @@ -2816,6 +2820,8 @@ START_NODE_FUN(CLiveAccounts, CNode) NODE_FUN_SV(GetFriendInvites) NODE_FUN_SV(GetNumberOfFriendsOnline) NODE_FUN_VIIS(LaunchEntryPoint) + NODE_FUN_II(IsPasswordEnabled) + NODE_FUN_IIS(VerifyPassword) END_NODE_FUN() // ============================================================================ diff --git a/theseus/shared/file_ops.cpp b/theseus/shared/file_ops.cpp index a26b1ae..bc45ffc 100755 --- a/theseus/shared/file_ops.cpp +++ b/theseus/shared/file_ops.cpp @@ -899,20 +899,26 @@ CFile::~CFile() delete m_date; } +// CFile::readText [script fn 'readText', FND 0x0017f918, sig 8] +// Reconstructed from 5960 retail XBE @ 0x00031960. Ghidra decompile: +// s = new CStrObject; +// if (m_path && file.Fetch(m_path)) { file.MakeUnicode(); s->Append(file.content); } +// return s; +// MakeUnicode widens an ANSI file and strips the BOM off a UTF-16 one, so the +// text reads right either way. CStrObject* CFile::readText() { - CStrObject* pStr = new CStrObject; + CStrObject* pText = new CStrObject; + if (m_path == NULL) + return pText; - if (m_path != NULL) + CActiveFile file; + if (file.Fetch(m_path)) { - CActiveFile file; - if (file.Fetch(m_path)) - { - pStr->Append((TCHAR*)file.GetContent()); - } + file.MakeUnicode(); + pText->Append((const TCHAR*)file.GetContent()); } - - return pStr; + return pText; } // ===== CFolder ===== diff --git a/theseus/xbox/live_accounts.cpp b/theseus/xbox/live_accounts.cpp index 99f41f6..d4830ba 100755 --- a/theseus/xbox/live_accounts.cpp +++ b/theseus/xbox/live_accounts.cpp @@ -100,6 +100,8 @@ START_NODE_FUN(CLiveAccounts, CNode) NODE_FUN_SV(GetFriendInvites) NODE_FUN_SV(GetNumberOfFriendsOnline) NODE_FUN_VIIS(LaunchEntryPoint) + NODE_FUN_II(IsPasswordEnabled) + NODE_FUN_IIS(VerifyPassword) // 5960 additions NODE_FUN_IV(GetLastLogonUser) NODE_FUN_SV(GetResult) @@ -378,6 +380,66 @@ void CLiveAccounts::Logoff() m_currentAccount = -1; } +// XLive_EncodePasscode, 5960 @ 0x00069930. Turns the script's passcode text +// into the 4 bytes an account stores, one per character: +// u d l r x y L R -> 1 2 3 4 5 6 9 10, anything else keeps its low byte +// past the end of the text (or no text) -> 0 +static void EncodePasscode(const TCHAR* szPasscode, BYTE passcode[4]) +{ + for (int i = 0; i < 4; i++) + { + BYTE b = 0; + if (szPasscode != NULL && *szPasscode != 0) + b = (BYTE)*szPasscode++; + + switch (b) + { + case 'u': b = 1; break; + case 'd': b = 2; break; + case 'l': b = 3; break; + case 'r': b = 4; break; + case 'x': b = 5; break; + case 'y': b = 6; break; + case 'L': b = 9; break; + case 'R': b = 10; break; + } + + passcode[i] = b; + } +} + +// CLiveAccounts::IsPasswordEnabled [script fn 'IsPasswordEnabled', FND 0x00188dc8, sig 3] +// Reconstructed from 5960 retail XBE @ 0x000698b0. Ghidra decompile: +// if (dirty) EnumerateAccounts(); +// if (index < count) return users[index].dwUserOptions & 1; +// return 0; +// Bit 0 is XONLINE_USER_OPTION_REQUIRE_PASSCODE. Theseus reads the list +// LoadFromHD keeps and also turns away a negative index. +int CLiveAccounts::IsPasswordEnabled(int index) +{ + if (index < 0 || (DWORD)index >= g_NumUsers) + return 0; + + return (g_Users[index].dwUserOptions & XONLINE_USER_OPTION_REQUIRE_PASSCODE) != 0; +} + +// CLiveAccounts::VerifyPassword [script fn 'VerifyPassword', FND 0x00188de0, sig 29] +// Reconstructed from 5960 retail XBE @ 0x000698e0. Ghidra decompile: +// if (dirty) EnumerateAccounts(); +// if (index >= count) return 1; +// XLive_EncodePasscode(password, &code); +// return code == *(DWORD*)users[index].passcode; +// An index past the list passes. Theseus treats a negative one the same way. +int CLiveAccounts::VerifyPassword(int index, const TCHAR* password) +{ + if (index < 0 || (DWORD)index >= g_NumUsers) + return 1; + + BYTE passcode[4]; + EncodePasscode(password, passcode); + return memcmp(passcode, g_Users[index].pin, sizeof(passcode)) == 0; +} + int CLiveAccounts::IsPasswordVerified() { OutputDebugString(_T("[LiveAccounts] IsPasswordVerified() called. Returning true.\n")); From 017eedf8c512a7f492fca1cb42c5f85470c6bdf7 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:01:08 -0500 Subject: [PATCH 085/167] The script % operator gives a defined result like 5960 5960's number switch has no case for % and pushes the right-hand side, while ours pushed an uninitialized float. Now it pushes the right-hand side too. The compiler never emits a bare %, so no shipped script changes. --- theseus/shared/xap_vm.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/theseus/shared/xap_vm.cpp b/theseus/shared/xap_vm.cpp index 4462123..c755761 100644 --- a/theseus/shared/xap_vm.cpp +++ b/theseus/shared/xap_vm.cpp @@ -1504,7 +1504,9 @@ bool CRunner::BinaryOperator(BYTE op) case opSHL: fResult = (float)((ULONG)a << (ULONG)b); break; case opSHR: fResult = (float)((ULONG)a >> (ULONG)b); break; - // opMod has no case in 5960 either; it falls out of the switch. + // opMod has no case in 5960's number switch (jump table @ 0x000385c4); + // it lands on the shared exit, which pushes the right operand as is. + default: fResult = b; break; } Push(fResult); From 5e2b2230f67cc0045928ca150a8d2f7894d1be04 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:02:51 -0500 Subject: [PATCH 086/167] Shift stays on when a key bounces off a full name The Xbox keyboard dropped shift even when the string was full and the key never went in. 5960 and the desktop keyboard keep it until a character lands. --- theseus/shared/keyboard_node.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/theseus/shared/keyboard_node.cpp b/theseus/shared/keyboard_node.cpp index 2cbeb93..744936e 100755 --- a/theseus/shared/keyboard_node.cpp +++ b/theseus/shared/keyboard_node.cpp @@ -256,7 +256,7 @@ class CKeyboard : public CNode const WORD* ActiveLayout() { return GetActiveLayout(m_mode, m_jmode); } TCHAR ApplyCase(TCHAR ch); void ClearSelection(); - void InsertChar(TCHAR ch); + bool InsertChar(TCHAR ch); DECLARE_NODE_PROPS() DECLARE_NODE_FUNCTIONS(); @@ -343,20 +343,21 @@ TCHAR CKeyboard::ApplyCase(TCHAR ch) return ch; } -void CKeyboard::InsertChar(TCHAR ch) +bool CKeyboard::InsertChar(TCHAR ch) { ClearSelection(); if (m_nLength >= KB_MAX_LENGTH) { CallFunction(this, _T("OnError")); - return; + return false; } MoveMemory(&m_rgch[m_nCursor + 1], &m_rgch[m_nCursor], (m_nLength - m_nCursor) * sizeof(TCHAR)); m_rgch[m_nCursor] = ch; m_nCursor += 1; m_nLength += 1; + return true; } @@ -569,8 +570,9 @@ void CKeyboard::activate() break; default: - InsertChar(ApplyCase((TCHAR)key)); - m_shift = false; + // Shift stays on if the string was full. + if (InsertChar(ApplyCase((TCHAR)key))) + m_shift = false; break; } } From ef41d5eb3fa5e619f89b202c5353d067c81a19b2 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:03:11 -0500 Subject: [PATCH 087/167] Fonts fall back to the app folder like 5960 5960 looks for *.xtf in fonts\ and, when that finds nothing, in the app folder itself. We only looked in the font folder. Shipped installs keep loading from Fonts, so nothing changes there. Checked the lookup order with a fake file system. --- theseus/render/text.cpp | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp index d6e20ce..99aa633 100644 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -856,27 +856,37 @@ static TCHAR* CopyString(const TCHAR* sz) // face = widen(face); if (wcscmp(face, fonts[0].face) == 0) free(face); // else { fonts[count].face = face; fonts[count].file = copy(path); count++; } // CloseHandle; -// Theseus looks in its one font directory and takes XTF0 only, since that's -// all CFont reads. A bad header still closes the file (5960 leaves it open). -static void InitFontTable() +// Theseus's font directory stands in for fonts\ and its app directory +// for the fallback. It takes XTF0 only, since that's all CFont reads. A bad +// header still closes the file (5960 leaves it open). +static HANDLE FindFontFiles(const TCHAR* szDir, TCHAR* szPath, WIN32_FIND_DATA* pfd) { - const TCHAR* szDir = TheseusGetFontDir(); - int cchDir = (int)_tcslen(szDir); - - TCHAR szPath[MAX_PATH]; _tcscpy(szPath, szDir); - _tcscpy(szPath + cchDir, _T("*.xtf")); - - WIN32_FIND_DATA fd; + _tcscat(szPath, _T("*.xtf")); #ifdef _UNICODE char szPathA[MAX_PATH]; Ansi(szPathA, szPath, MAX_PATH); - HANDLE hFind = TheseusFindFirstFile(szPathA, &fd); + return TheseusFindFirstFile(szPathA, pfd); #else - HANDLE hFind = TheseusFindFirstFile(szPath, &fd); + return TheseusFindFirstFile(szPath, pfd); #endif +} + +static void InitFontTable() +{ + TCHAR szPath[MAX_PATH]; + WIN32_FIND_DATA fd; + + const TCHAR* szDir = TheseusGetFontDir(); + HANDLE hFind = FindFontFiles(szDir, szPath, &fd); if (hFind == INVALID_HANDLE_VALUE) - return; + { + szDir = TheseusGetAppDir(); + hFind = FindFontFiles(szDir, szPath, &fd); + if (hFind == INVALID_HANDLE_VALUE) + return; + } + int cchDir = (int)_tcslen(szDir); do { From 0527a5d04e9c61d6164228ccae9bc797a801412e Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:05:43 -0500 Subject: [PATCH 088/167] Rebuilt the Keyboard node from the 5960 binary Wrote the key grids, highlight moves, key presses, editing, the string property and the key drawing fresh from 0x5ab80..0x5bca0 and wrote up what the binary does in the Keyboard doc. Text, cursor, callbacks, modes and every key label match the old Xbox code on 159,057 checked steps across every key, every page, both grids and 400 random sequences. --- docs/decomp/Keyboard.md | 361 ++++++++++- docs/decomp/README.md | 28 +- theseus/shared/keyboard_node.cpp | 998 +++++++++++++++---------------- 3 files changed, 852 insertions(+), 535 deletions(-) diff --git a/docs/decomp/Keyboard.md b/docs/decomp/Keyboard.md index 731a47c..594a1e2 100644 --- a/docs/decomp/Keyboard.md +++ b/docs/decomp/Keyboard.md @@ -1,33 +1,354 @@ # On-Screen Keyboard -CKeyboard at 0x00029d38, "Keyboard" at 0x00029d4c, FND at 0x00013da4. +The `Keyboard` node in the 5960 retail binary: the grid of keys behind every name, passcode and +soundtrack prompt. The script moves a highlight around the grid and presses keys; the node keeps a +31 character edit string with a cursor and hands it to a Text node to draw. + +## Functions + +| Address | Name | What it does | +|---------|------|--------------| +| 0x0005ab30 | CKeyboard::GetPropMap | Returns the PRD at `0x00175448` | +| 0x0005ab40 | CKeyboard::GetFunctionMap | Returns the FND at `0x00188250` | +| 0x0005ab50 | CKeyboard::CreateNode | Factory for `Keyboard`, 0x9c bytes | +| 0x0005ab70 | CKeyboard::GetNodeClass | Returns the node class at `0x00188224` | +| 0x0005ab80 | CKeyboard::CKeyboard | Constructor | +| 0x0005ac20 | CKeyboard::~CKeyboard | Destructor | +| 0x0005acb0 | GetAsteriskFormattingState | Reads the *heading* flag | +| 0x0005acc0 | SetAsteriskFormattingState | Writes the *heading* flag | +| 0x0005ace0 | selectKey | Puts the highlight on a cell | +| 0x0005ad40 | selectUp | Highlight up | +| 0x0005adb0 | selectDown | Highlight down | +| 0x0005ae60 | selectLeft | Highlight left | +| 0x0005af50 | selectRight | Highlight right | +| 0x0005b040 | TIDactivate | Presses whatever key a USB keyboard just typed | +| 0x0005b340 | activate | Presses the highlighted key | +| 0x0005b630 | Backspace | Erases left of the cursor | +| 0x0005b6a0 | Delete | Erases under the cursor | +| 0x0005b700 | CursorLeft | Cursor left | +| 0x0005b740 | CursorRight | Cursor right | +| 0x0005b780 | Shift | Toggles shift | +| 0x0005b7b0 | CycleMode | Next page | +| 0x0005b7e0 | Insert | Inserts a string at the cursor | +| 0x0005b890 | (grid char test) | Whether a character is on any page of the current grid | +| 0x0005b970 | CKeyboard::OnSetProperty | Loads `string` into the edit buffer | +| 0x0005b9d0 | CKeyboard::Render | Frame, text, then one node per key | +| 0x0005bca0 | CKeyboard::Advance | Advances the control node too | + +## The object + +| Offset | Field | +|--------|-------| +| +0x1c | `keys`, a nodearray (length at +0x1c, entries at +0x24) | +| +0x28 | `frame` | +| +0x2c | `text` | +| +0x30 | `control` | +| +0x34 | `string` | +| +0x38 | `shift` (byte) | +| +0x39 | `caps` (byte) | +| +0x3a | `TIDKeyUsed` (byte) | +| +0x3b | `PlaySound` (byte) | +| +0x3c | `CloseKeyboard` (byte) | +| +0x40 | `mode`, the western page | +| +0x44 | `jmode`, the Japanese page | +| +0x48 | edit buffer, 32 WCHARs | +| +0x88 | edit length | +| +0x8c | cursor, -1 when the whole string is selected | +| +0x90 | highlighted row | +| +0x94 | highlighted column | +| +0x98 | a WORD key code that stands in for the grid during one `activate` (TIDactivate's) | + +The constructor (`0x0005ab80`, and the same stores inlined into CreateNode) zeroes every field +and sets the cursor to -1. The destructor releases `text`, `frame` and `control` in that order, +frees `string` and releases the nodearray. ## Properties -| Name | Type | -|------|------| -| keys | nodearray | -| frame | node | -| text | node | -| control | node | -| mode | integer | -| jmode | integer | -| shift | boolean | -| caps | boolean | -| string | string | +PRD at `0x00175448`, dumped with DumpPRD: -## Functions +| Name | Type | Offset | +|------|------|--------| +| keys | 11 (nodearray) | 0x1c | +| frame | 9 (node) | 0x28 | +| text | 9 (node) | 0x2c | +| control | 9 (node) | 0x30 | +| mode | 1 (integer) | 0x40 | +| jmode | 1 (integer) | 0x44 | +| shift | 2 (boolean) | 0x38 | +| caps | 2 (boolean) | 0x39 | +| TIDKeyUsed | 2 (boolean) | 0x3a | +| PlaySound | 2 (boolean) | 0x3b | +| CloseKeyboard | 2 (boolean) | 0x3c | +| string | 10 (string) | 0x34 | + +## Script functions + +FND at `0x00188250`: selectKey (sig 18), selectUp, selectDown, selectLeft, selectRight, activate, +TIDactivate, Backspace, Delete, CursorLeft, CursorRight, Shift, CycleMode (all sig 1), Insert +(sig 12), SetAsteriskFormattingState (sig 14), GetAsteriskFormattingState (sig 2). + +Callbacks it raises: `OnDone` and `OnError`. + +## Globals + +| Address | What | +|---------|------| +| 0x0017c024 | current language; 1 (Japanese) switches to the 9 row grid | +| 0x0017e748 | the string the next Text node draws | +| 0x00175220 | the cursor index for that Text node, -1 for none | +| 0x0017e74c | *heading* formatting flag, a BSS byte (starts off) | +| 0x0017e74d | "this key is highlighted", read by the key nodes | +| 0x0017f460 | WORD key code of the last USB keyboard keystroke | +| 0x0017f462 | WORD character of that keystroke | + +The two keystroke words belong to the input object: the poll at `0x00040520` walks the eight +keyboard handles, takes the first `XInputGetKeystroke` that succeeds, resets the screen saver and +copies the 8 byte keystroke to the object's +0x28. The joystick node's Advance (`0x00041040`) +raises `OnTIDKeyDown` when the key code is nonzero and that joystick is the bound one. + +## The grids + +Six WORD tables, 11 columns each, listed through `0x00175430` (alpha `0x000211b8`, symbols +`0x00021228`, accents `0x00021298`, hiragana `0x00021308`, katakana `0x000213d0`, Japanese +English `0x00021498`). Western grids are 5 rows, Japanese 9. Codes 1 to 14 are function keys: + +| Code | Key | Label | +|------|-----|-------| +| 1 | shift | SHIFT | +| 2 | caps lock | CAPS LOCK | +| 3 | alphabet page | ALPHABET | +| 4 | symbols page | SYMBOLS | +| 5 | accents page | ACCENTS | +| 6 | cursor left | `<` | +| 7 | cursor right | `>` | +| 8 | backspace | BACKSPACE | +| 9 | done | DONE | +| 10 | space | SPACE | +| 11 | hiragana page | HIRAGANA | +| 12 | katakana page | KATAKANA | +| 13 | English page | ENGLISH | +| 14 | blank cell | a space, never drawn | + +Western alpha page (symbols and accents swap the characters and the page keys): + +``` +DONE 1 2 3 4 5 6 7 8 9 0 +SHIFT a b c d e f g h i j +CAPS k l m n o p q r s t +ACCENTS u v w x y z BACKSPACE x4 +SYMBOLS SPACE x6 < < > > +``` + +The western bottom two rows repeat a key across cells (BACKSPACE in 4, SPACE in 6, `<` and `>` in +2 each), which is why a western keyboard has 45 key nodes for 55 cells. The Japanese grids put the +page keys, SPACE, BACKSPACE, `<`, `>` and DONE down column 10 and leave a few blank cells (code 14) +in rows 0 and 7. + +## Moving the highlight + +**selectKey (`0x0005ace0`).** + +```c +last = (lang == 1) ? 8 : 4; +row = (row < 0 || row > last) ? 0 : row; +col = (col >= 0 && col < 11) ? col : 0; +``` + +**selectUp / selectDown (`0x0005ad40`, `0x0005adb0`).** + +```c +do { row = row ? row - 1 : last; } while (grid[row*11 + col] == 14); // up +do { row = (row == last) ? 0 : row + 1; } while (grid[row*11 + col] == 14); // down +``` + +**selectLeft (`0x0005ae60`).** + +```c +for (;;) { + while (col == 0) { col = 10; if (grid[row*11 + 10] != 14) return; } + if (lang != 1) { + if (row == 3) { if (col > 6) { col = 6; return; } } + else if (row == 4) { if (col > 8) { col = 8; return; } col = (col < 7) ? 0 : 6; return; } + col--; return; + } + col--; if (grid[row*11 + col] != 14) return; +} +``` + +The row 4 step is compiled as `((col < 7) - 1) & 6`. + +**selectRight (`0x0005af50`).** + +```c +for (;;) { + while (col == 10) { col = 0; if (grid[row*11] != 14) return; } + if (lang != 1) break; + col++; if (grid[row*11 + col] != 14) return; +} +if (row == 3) col = (col < 7) ? col + 1 : 0; +else if (row == 4) col = (col >= 9) ? 0 : (col == 0) ? 1 : (col > 6) ? 9 : 7; +else col++; +``` + +## Pressing a key + +**activate (`0x0005b340`).** The key is the +0x98 code if set, otherwise `grid[row*11 + col]`. + +```c +switch (key) { +case 1: Shift(); return; +case 2: if (mode == 0 || mode == 2) { caps = !caps; return; } break; +case 3: case 4: case 5: mode = key - 3; return; +case 11: case 12: case 13: jmode = key - 11; return; +case 6: CursorLeft(); return; +case 7: CursorRight(); return; +case 8: Backspace(); return; +case 9: + for (i = 0; i < len && buf[i] == ' '; i++) ; + if (i == len) break; // empty or all blanks + for (p = &buf[len-1]; *p == ' '; p--) *p = 0; + for (q = buf; *q == ' '; q++) ; + if (q != buf) memmove(buf, q, (wcslen(q) + 1) * 2); + free(string); string = new WCHAR[len + 1]; + copy len WCHARs; string[len] = 0; + CallFunction(this, L"OnDone"); return; +case 10: + if (cursor == -1) { len = 0; cursor = 0; } + if (len > 30) return; // full: no error + open a gap at cursor; buf[cursor++] = ' '; len++; return; +default: + if (cursor == -1) { len = 0; cursor = 0; } + if (len < 31) { + open a gap at cursor; + buf[cursor++] = (caps != shift) ? CharUpperW(key) : key; + shift = 0; len++; return; + } +} +CallFunction(this, L"OnError"); +``` + +DONE copies `len` characters even after trimming, so trailing spaces arrive in `string` as +nulls and leading ones as whatever sat past the shifted text's terminator. SPACE does not clear +shift, and a key that bounces off a full string leaves shift on. + +## Editing + +Each editing function opens the same way: a cursor of -1 means the whole string is selected, +so the edit starts from an empty string with the cursor at 0. + +**Backspace (`0x0005b630`).** + +```c +if (cursor == -1) { len = 0; cursor = 0; } +if (cursor > 0) { memmove(&buf[cursor-1], &buf[cursor], (len - cursor) * 2); len--; cursor--; } +else CallFunction(this, L"OnError"); +``` + +**Delete (`0x0005b6a0`).** After the selection check, if `cursor < len` it steps the cursor +right one (an inlined CursorRight, whose -1 case can't happen any more) and calls Backspace; +otherwise OnError. + +**CursorLeft / CursorRight (`0x0005b700`, `0x0005b740`).** From -1 they land on 0 and on `len` +respectively; otherwise they step one, raising OnError at either end. + +**Shift (`0x0005b780`).** OnError unless `mode` is 0 or 2, else toggles `shift`. + +**CycleMode (`0x0005b7b0`).** `mode++` and `jmode++`, each reset to 0 once above 2. + +**Insert (`0x0005b7e0`).** + +```c +if (cursor == -1) { len = 0; cursor = 0; } +n = wcslen(s); +if (len + n > 31) { CallFunction(this, L"OnError"); return; } +memmove(&buf[cursor + n], &buf[cursor], (len - cursor) * 2); +copy n WCHARs to &buf[cursor]; cursor += n; len += n; shift = 0; +``` + +**Get/SetAsteriskFormattingState (`0x0005acb0`, `0x0005acc0`).** Read and write the global +byte at `0x0017e74c` (`param != 0`). The flag isn't per keyboard; the Text node reads it. + +## Node hooks + +**OnSetProperty (`0x0005b970`).** Only the `string` offset (0x34) does anything: + +```c +s = *value ? *value : L""; +n = wcslen(s); if (n > 31) n = 31; +copy n WCHARs to buf; len = n; cursor = -1; +return 1; +``` + +**Advance (`0x0005bca0`).** `CNode::Advance`, then `control->Advance` if there is one. + +**Render (`0x0005b9d0`).** -selectKey, selectUp, selectDown, selectLeft, selectRight, activate, Backspace, Delete, CursorLeft, CursorRight, Shift, CycleMode, Insert. +```c +grid = (lang == 1) ? japanese[jmode] : western[mode]; +if (frame) frame->Render(); +if (text) { buf[len] = 0; g_textCursor = cursor; g_text = buf; text->Render(); g_textCursor = -1; } +if (lang == 1) { if (keys.count != 99) return; cells = 99; } +else { if (keys.count != 45) return; cells = 55; } +node = 0; +for (i = 0; i < cells; i++) { + if (lang != 1 && i > 0 && grid[i] == grid[i-1]) continue; + k = keys.Get(node); // bounds checked, null past the end + hi = row*11 + col; + if (lang != 1) { // a wide key lights from any of its cells + if (41 <= hi && hi <= 43) hi = 40; + else if (46 <= hi && hi <= 50) hi = 45; + else if (hi == 52) hi = 51; + else if (hi == 54) hi = 53; + } + g_text = label for grid[i]; // the table above, or the character itself, + // CharUpperW'd on western grids when caps != shift + g_activeKey = (i == hi); + if (grid[i] != 14) k->Render(); + node++; +} +``` -## Script Callbacks +## TIDactivate -OnDone, OnError. +`0x0005b040` turns a USB keyboard keystroke into a key press. The keyboard XIPs call it from +`OnTIDKeyDown`, then read `CloseKeyboard`, `PlaySound` and `TIDKeyUsed` to pick between closing, +the key click and the error sound. -## Layout +```c +CloseKeyboard = 0; PlaySound = 1; TIDKeyUsed = 1; code = 0; +switch (keycode) { // 0x0017f460 +case 0x35: case 0x88: case 0x90: PlaySound = 0; break; +case 0x4f: code = 7; break; // right arrow: cursor right +case 0x50: code = 6; break; // left arrow: cursor left +} +if (code == 0) { + switch (ch) { // 0x0017f462 + case 0: break; + case 8: code = 8; break; // backspace + case 10: code = 9; break; // enter: DONE + case 0x1b: CloseKeyboard = 1; break; // escape + case 0x20: code = 10; break; // space + default: + if (grid char test(ch)) { + code = ch; + page = first of the current language's three grids with a cell equal to ch + or to CharUpperW of the cell; + jmode = page; // western grids write jmode too, not mode + } + } +} +if (code) { activate(); code = 0; TIDKeyUsed = 1; } +else { TIDKeyUsed = 0; } +``` -Three western modes: alpha, symbol, accent, arranged in a 5x11 grid. +`code` is the +0x98 field. Both switches are jump tables: the key code one covers 0x35..0x90, +the character one 0..0x20, and everything else falls to the default. The key codes are USB HID +usages (0x4f right arrow, 0x50 left arrow; 0x35, 0x88 and 0x90 are the grave key and the Japanese +input mode keys, which press nothing and stay quiet). -Three Japanese modes: hiragana, katakana, English, arranged in a 9x11 grid. +**Grid char test (`0x0005b890`).** Takes the character in DI. Returns false for 1 to 14 (the +function key codes). Otherwise walks the three grids of the current language (55 or 99 cells +each) and returns true on the first cell equal to the character or to `CharUpperW` of the cell. -Maximum input length is 31 characters. +A typed character goes in through activate's default case, so it is still uppercased again when +caps and shift disagree, and it still clears shift. diff --git a/docs/decomp/README.md b/docs/decomp/README.md index 2e05b57..2661ff8 100644 --- a/docs/decomp/README.md +++ b/docs/decomp/README.md @@ -218,12 +218,26 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x0004e4d0 | `CInline::Init` | `CInline::Init` (scene_groups.cpp) | [Scene Groups](SceneGroups.md) | Inline-node init: opens the referenced XIP / XAP and inserts its root into the local scene graph. | | 0x0004e740 | `CInline::Advance` | `CInline::Advance` (scene_groups.cpp) | [Scene Groups](SceneGroups.md) | Per-frame tick. If the load thread completed, swaps the placeholder for the loaded sub-tree. | | 0x00053ed0 | `CFont::LoadGlyph` | `CFont::LoadGlyph` (text.cpp) | [Text](Text.md) | Decompresses a single glyph bitmap from the XTF cache table. | -| 0x0005acb0 | `CKeyboard_CursorRight` | `CKeyboard::CursorRight` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Right-arrow handler on the on-screen keyboard. Wraps to row start when at row end. | -| 0x0005b340 | `CKeyboard_activate` | `CKeyboard::activate` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Commits the highlighted key. Handles regular keys, layout switches (shift/sym), and the OK / cancel softkeys. | -| 0x0005b630 | `CKeyboard::Backspace` | `CKeyboard::Backspace` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Handles backspace on the on-screen keyboard. Removes the last character (with surrogate pair awareness). | -| 0x0005b700 | `CKeyboard_CursorLeft` | `CKeyboard::CursorLeft` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Left-arrow handler. Wraps to row end when at row start. | -| 0x0005b740 | `CKeyboard_CursorRight` | `CKeyboard::CursorRight` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Right-arrow handler (alternate FND-bound entry). | -| 0x0005b780 | `CKeyboard_Shift` | `CKeyboard::Shift` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Toggles the shift / caps layer of the on-screen keyboard. | +| 0x0005ab80 | `CKeyboard_CKeyboard` | `CKeyboard::CKeyboard` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Constructor. Zeroes every field except the cursor, which starts at -1 (whole string selected). | +| 0x0005ac20 | `CKeyboard_dtor` | `CKeyboard::~CKeyboard` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Releases the text, frame and control nodes, frees `string`, releases the key nodearray. | +| 0x0005acb0 | `CKeyboard_GetAsteriskFormattingState` | `CKeyboard::GetAsteriskFormattingState` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Returns the global *heading* text flag at 0x0017e74c. | +| 0x0005acc0 | `CKeyboard_SetAsteriskFormattingState` | `CKeyboard::SetAsteriskFormattingState` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Sets the global *heading* text flag at 0x0017e74c. | +| 0x0005ace0 | `CKeyboard_selectKey` | `CKeyboard::selectKey` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Moves the highlight to (row, column), falling back to 0 for either one out of range. | +| 0x0005ad40 | `CKeyboard_selectUp` | `CKeyboard::selectUp` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Highlight up one row, wrapping to the bottom and skipping blank cells. | +| 0x0005adb0 | `CKeyboard_selectDown` | `CKeyboard::selectDown` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Highlight down one row, wrapping to the top and skipping blank cells. | +| 0x0005ae60 | `CKeyboard_selectLeft` | `CKeyboard::selectLeft` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Highlight left, wrapping, skipping blanks; on western grids hops across the wide bottom keys. | +| 0x0005af50 | `CKeyboard_selectRight` | `CKeyboard::selectRight` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Highlight right, the mirror of selectLeft. | +| 0x0005b340 | `CKeyboard_activate` | `CKeyboard::activate` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Presses the highlighted key: page and case keys, cursor keys, BACKSPACE, SPACE, DONE (trim, copy to `string`, OnDone) or a character insert. | +| 0x0005b630 | `CKeyboard_Backspace` | `CKeyboard::Backspace` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Removes the character left of the cursor, OnError at the start. | +| 0x0005b6a0 | `CKeyboard_Delete` | `CKeyboard::Delete` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Removes the character under the cursor, OnError at the end. | +| 0x0005b700 | `CKeyboard_CursorLeft` | `CKeyboard::CursorLeft` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Cursor left one, OnError at the start. From a full selection it lands at 0. | +| 0x0005b740 | `CKeyboard_CursorRight` | `CKeyboard::CursorRight` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Cursor right one, OnError at the end. From a full selection it lands at the end. | +| 0x0005b780 | `CKeyboard_Shift` | `CKeyboard::Shift` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Toggles shift on the letter and accent pages, OnError on symbols. | +| 0x0005b7b0 | `CKeyboard_CycleMode` | `CKeyboard::CycleMode` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Steps mode and jmode together through 0, 1, 2. | +| 0x0005b7e0 | `CKeyboard_Insert` | `CKeyboard::Insert` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Inserts a string at the cursor, OnError if it would pass 31 characters. | +| 0x0005b970 | `CKeyboard_OnSetProperty` | `CKeyboard::OnSetProperty` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Setting `string` loads the edit buffer (up to 31 characters) with the whole thing selected. | +| 0x0005b9d0 | `CKeyboard_Render` | `CKeyboard::Render` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Draws the frame, the text node with the edit string and cursor, then one node per key with its label. | +| 0x0005bca0 | `CKeyboard_Advance` | `CKeyboard::Advance` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Advances the node, then the control node. | | 0x0005c250 | `CLevel_load` | | [Scene Groups](SceneGroups.md) | FND-bound script method on Level: triggers asynchronous load of the Level's referenced sub-scene. | | 0x0005d200 | `CLevel::Deactivate` | `CLevel::Deactivate` (scene_groups.cpp) | [Scene Groups](SceneGroups.md) | Marks a Level node inactive. Pauses its time-dep children and skips it during render traversal. | | 0x0005d300 | `CPositionInterpolator::Interpolate` | `CPositionInterpolator::Interpolate` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Linear interpolation between key positions based on the bound TimeSensor's fraction. | @@ -280,7 +294,7 @@ The seed and first expansion cover the application shell, script VM, scene graph | File Operations | [FileOps.md](FileOps.md) | 7 functions | | Disc | (no doc yet, narrative TODO) | 1 function | | HUD & Picture | [HudNode.md](HudNode.md) | 9 functions | -| Keyboard | [Keyboard.md](Keyboard.md) | 6 functions | +| Keyboard | [Keyboard.md](Keyboard.md) | 20 functions | | Locale | (no doc yet, narrative TODO) | 1 function | | NtIoSvc | [NtIoSvc.md](NtIoSvc.md) | 1 function | | Scene Groups | [SceneGroups.md](SceneGroups.md) | 6 functions | diff --git a/theseus/shared/keyboard_node.cpp b/theseus/shared/keyboard_node.cpp index 744936e..a6598be 100755 --- a/theseus/shared/keyboard_node.cpp +++ b/theseus/shared/keyboard_node.cpp @@ -1,209 +1,166 @@ -// keyboard_node.cpp: CKeyboard, the on-screen keyboard node. +// keyboard_node.cpp: the Keyboard node, the on-screen keyboard behind every +// name and passcode prompt. // -// Provides text input via a grid of selectable keys. Three Western modes -// (alpha, symbol, accent) and three Japanese modes (hiragana, katakana, -// English). XAP scripts drive navigation and receive callbacks (OnDone, -// OnError). -// -// CKeyboard confirmed in the 5960 retail XBE: class descriptor -// "CKeyboard" at 0x00029d38, node-type string "Keyboard" at 0x00029d4c. -// See docs/decomp/Keyboard.md. -// FND table at 0x00013da4 -// Class registration at 0x0001f368 +// A grid of keys (5 x 11 western, 9 x 11 Japanese) with a highlighted key the +// script moves around, a 31 character edit string with a cursor, and OnDone / +// OnError callbacks. Rebuilt function by function from the 5960 retail XBE; +// what the binary does is written up in docs/decomp/Keyboard.md. #include "std.h" #include "theseus.h" #include "node.h" #include "runner.h" -extern CNode* GetTextNode(const TCHAR* szText, float nWidth); -extern bool g_bActiveKey; +// Shared with the Text node: the keyboard lends it the edit string and cursor +// (0x0017e748, 0x00175220), flags the highlighted key (0x0017e74d) and owns the +// *heading* switch (0x0017e74c). extern TCHAR* g_szText; -extern bool g_bAsteriskFormatting; // text.cpp, gates *heading* text extern int g_nTextChar; +extern bool g_bActiveKey; +extern bool g_bAsteriskFormatting; extern int g_nCurLanguage; -// ===== Constants ============================================================ - -#define KB_MAX_LENGTH 31 -#define KB_COLS 11 - -// Western mode indices -enum { MODE_ALPHA = 0, MODE_SYMBOL, MODE_ACCENT }; - -// Japanese mode indices -enum { JMODE_HIRAGANA = 0, JMODE_KATAKANA, JMODE_ENGLISH }; - -// Special key codes -enum { - KEY_SHIFT = 1, KEY_CAPS, KEY_ALPHA, KEY_SYM, KEY_ACC, - KEY_LT, KEY_RT, KEY_BS, KEY_DONE, KEY_SPACE, - KEY_HIRAGANA, KEY_KATAKANA, KEY_ENGLISH, KEY_VOID +// ===== Layouts ============================================================= +// Six 16 bit key tables at 0x000211b8..0x00021498, reached through the +// pointer list at 0x00175430 (three western, then three Japanese). Values +// below 0x0f are function keys, everything else types itself. + +enum +{ + kcShift = 1, + kcCaps, + kcAlphabet, + kcSymbols, + kcAccents, + kcLeft, + kcRight, + kcBackspace, + kcDone, + kcSpace, + kcHiragana, + kcKatakana, + kcEnglish, + kcBlank, // a hole in the Japanese grid, never drawn or landed on }; - -// ===== Unicode character defines ============================================ - -#define _nbsp 0x00a0 -#define _iexcl 0x00a1 -#define _pound 0x00a3 -#define _yen 0x00a5 -#define _laquo 0x00ab -#define _raquo 0x00bb -#define _iquest 0x00bf -#define _agrave 0x00e0 -#define _aacute 0x00e1 -#define _acirc 0x00e2 -#define _auml 0x00e4 -#define _aelig 0x00e6 -#define _ccedil 0x00e7 -#define _egrave 0x00e8 -#define _eacute 0x00e9 -#define _ecirc 0x00ea -#define _euml 0x00eb -#define _igrave 0x00ec -#define _iacute 0x00ed -#define _icirc 0x00ee -#define _iuml 0x00ef -#define _ntilde 0x00f1 -#define _ograve 0x00f2 -#define _oacute 0x00f3 -#define _ocirc 0x00f4 -#define _otilde 0x00f5 -#define _ugrave 0x00f9 -#define _uacute 0x00fa -#define _ucirc 0x00fb -#define _uuml 0x00fc -#define _yacute 0x00fd -#define _yuml 0x00ff -#define _szlig 0x00df -#define _euro 0x20ac - - -// ===== Key layout tables ==================================================== -// Each table is a flat array of WORD, KB_COLS entries per row. -// Special keys use the KEY_* constants above. - -static const WORD s_alphaKeys[] = -{ - KEY_DONE, L'1', L'2', L'3', L'4', L'5', L'6', L'7', L'8', L'9', L'0', - KEY_SHIFT, L'a', L'b', L'c', L'd', L'e', L'f', L'g', L'h', L'i', L'j', - KEY_CAPS, L'k', L'l', L'm', L'n', L'o', L'p', L'q', L'r', L's', L't', - KEY_ACC, L'u', L'v', L'w', L'x', L'y', L'z', KEY_BS, KEY_BS, KEY_BS, KEY_BS, - KEY_SYM, KEY_SPACE, KEY_SPACE, KEY_SPACE, KEY_SPACE, KEY_SPACE, KEY_SPACE, KEY_LT, KEY_LT, KEY_RT, KEY_RT, +#define EDIT_MAX 31 +#define GRID_COLUMNS 11 +#define WESTERN_KEYS (5 * GRID_COLUMNS) +#define JAPANESE_KEYS (9 * GRID_COLUMNS) +#define WESTERN_NODES 45 // merged keys share one node +#define JAPANESE_NODES 99 + +static const WORD s_westernAlpha[WESTERN_KEYS] = +{ + kcDone, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', + kcShift, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + kcCaps, 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + kcAccents, 'u', 'v', 'w', 'x', 'y', 'z', + kcBackspace, kcBackspace, kcBackspace, kcBackspace, + kcSymbols, kcSpace, kcSpace, kcSpace, kcSpace, kcSpace, kcSpace, + kcLeft, kcLeft, kcRight, kcRight, }; -static const WORD s_symbolKeys[] = +static const WORD s_westernSymbols[WESTERN_KEYS] = { - KEY_DONE, L'(', L')', L'&', L'_', L'^', L'%', L'\\', L'/', L'@', L'#', - KEY_SHIFT, L'[', L']', L'$', _pound, _euro, _yen, L';', L':', L'\'', L'"', - KEY_CAPS, L'<', L'>', L'?', L'!', _iquest, _iexcl, L'-', L'*', L'+', L'=', - KEY_ACC, L'{', L'}', _laquo, _raquo, L',', L'.', KEY_BS, KEY_BS, KEY_BS, KEY_BS, - KEY_ALPHA, KEY_SPACE, KEY_SPACE, KEY_SPACE, KEY_SPACE, KEY_SPACE, KEY_SPACE, KEY_LT, KEY_LT, KEY_RT, KEY_RT, + kcDone, '(', ')', '&', '_', '^', '%', '\\', '/', '@', '#', + kcShift, '[', ']', '$', 0xa3, 0x20ac, 0xa5, ';', ':', '\'', '"', + kcCaps, '<', '>', '?', '!', 0xbf, 0xa1, '-', '*', '+', '=', + kcAccents, '{', '}', 0xab, 0xbb, ',', '.', + kcBackspace, kcBackspace, kcBackspace, kcBackspace, + kcAlphabet, kcSpace, kcSpace, kcSpace, kcSpace, kcSpace, kcSpace, + kcLeft, kcLeft, kcRight, kcRight, }; -static const WORD s_accentKeys[] = +static const WORD s_westernAccents[WESTERN_KEYS] = { - KEY_DONE, L'1', L'2', L'3', L'4', L'5', L'6', L'7', L'8', L'9', L'0', - KEY_SHIFT, _agrave, _aacute, _acirc, _auml, _egrave, _eacute, _ecirc, _euml, _igrave, _iacute, - KEY_CAPS, _icirc, _iuml, _ograve, _oacute, _ocirc, _otilde, _ugrave, _uacute, _ucirc, _uuml, - KEY_ALPHA, _yacute, _yuml, _ccedil, _szlig, _ntilde, _aelig, KEY_BS, KEY_BS, KEY_BS, KEY_BS, - KEY_SYM, KEY_SPACE, KEY_SPACE, KEY_SPACE, KEY_SPACE, KEY_SPACE, KEY_SPACE, KEY_LT, KEY_LT, KEY_RT, KEY_RT, + kcDone, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', + kcShift, 0xe0, 0xe1, 0xe2, 0xe4, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, + kcCaps, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf9, 0xfa, 0xfb, 0xfc, + kcAlphabet, 0xfd, 0xff, 0xe7, 0xdf, 0xf1, 0xe6, + kcBackspace, kcBackspace, kcBackspace, kcBackspace, + kcSymbols, kcSpace, kcSpace, kcSpace, kcSpace, kcSpace, kcSpace, + kcLeft, kcLeft, kcRight, kcRight, }; -static const WORD* s_westernModes[] = { s_alphaKeys, s_symbolKeys, s_accentKeys }; - -static const WORD s_hiraganaKeys[] = -{ - 0x3042, 0x3044, 0x3046, 0x3048, 0x304A, 0x308F, 0x3092, 0x3093, KEY_VOID, KEY_VOID, KEY_HIRAGANA, - 0x304B, 0x304D, 0x304F, 0x3051, 0x3053, 0x3041, 0x3043, 0x3045, 0x3047, 0x3049, KEY_KATAKANA, - 0x3055, 0x3057, 0x3059, 0x305B, 0x305D, 0x3063, 0x3083, 0x3085, 0x3087, 0x308E, KEY_ENGLISH, - 0x305F, 0x3061, 0x3064, 0x3066, 0x3068, 0x304C, 0x304E, 0x3050, 0x3052, 0x3054, KEY_SPACE, - 0x306A, 0x306B, 0x306C, 0x306D, 0x306E, 0x3056, 0x3058, 0x305A, 0x305C, 0x305E, KEY_BS, - 0x306F, 0x3072, 0x3075, 0x3078, 0x307B, 0x3060, 0x3062, 0x3065, 0x3067, 0x3069, KEY_LT, - 0x307E, 0x307F, 0x3080, 0x3081, 0x3082, 0x3070, 0x3073, 0x3076, 0x3079, 0x307C, KEY_RT, - 0x3084, KEY_VOID, 0x3086, KEY_VOID, 0x3088, 0x3071, 0x3074, 0x3077, 0x307A, 0x307D, KEY_VOID, - 0x3089, 0x308A, 0x308B, 0x308C, 0x308D, 0x30FC, 0x3001, 0x3002, 0x300C, 0x300D, KEY_DONE, +static const WORD s_hiragana[JAPANESE_KEYS] = +{ + 0x3042, 0x3044, 0x3046, 0x3048, 0x304a, 0x308f, 0x3092, 0x3093, kcBlank, kcBlank, kcHiragana, + 0x304b, 0x304d, 0x304f, 0x3051, 0x3053, 0x3041, 0x3043, 0x3045, 0x3047, 0x3049, kcKatakana, + 0x3055, 0x3057, 0x3059, 0x305b, 0x305d, 0x3063, 0x3083, 0x3085, 0x3087, 0x308e, kcEnglish, + 0x305f, 0x3061, 0x3064, 0x3066, 0x3068, 0x304c, 0x304e, 0x3050, 0x3052, 0x3054, kcSpace, + 0x306a, 0x306b, 0x306c, 0x306d, 0x306e, 0x3056, 0x3058, 0x305a, 0x305c, 0x305e, kcBackspace, + 0x306f, 0x3072, 0x3075, 0x3078, 0x307b, 0x3060, 0x3062, 0x3065, 0x3067, 0x3069, kcLeft, + 0x307e, 0x307f, 0x3080, 0x3081, 0x3082, 0x3070, 0x3073, 0x3076, 0x3079, 0x307c, kcRight, + 0x3084, kcBlank, 0x3086, kcBlank, 0x3088, 0x3071, 0x3074, 0x3077, 0x307a, 0x307d, kcBlank, + 0x3089, 0x308a, 0x308b, 0x308c, 0x308d, 0x30fc, 0x3001, 0x3002, 0x300c, 0x300d, kcDone, }; -static const WORD s_katakanaKeys[] = -{ - 0x30A2, 0x30A4, 0x30A6, 0x30A8, 0x30AA, 0x30EF, 0x30F2, 0x30F3, 0x30F4, KEY_VOID, KEY_HIRAGANA, - 0x30AB, 0x30AD, 0x30AF, 0x30B1, 0x30B3, 0x30A1, 0x30A3, 0x30A5, 0x30A7, 0x30A9, KEY_KATAKANA, - 0x30B5, 0x30B7, 0x30B9, 0x30BB, 0x30BD, 0x30C3, 0x30E3, 0x30E5, 0x30E7, 0x30EE, KEY_ENGLISH, - 0x30BF, 0x30C1, 0x30C4, 0x30C6, 0x30C8, 0x30AC, 0x30AE, 0x30B0, 0x30B2, 0x30B4, KEY_SPACE, - 0x30CA, 0x30CB, 0x30CC, 0x30CD, 0x30CE, 0x30B6, 0x30B8, 0x30BA, 0x30BC, 0x30BE, KEY_BS, - 0x30CF, 0x30D2, 0x30D5, 0x30D8, 0x30DB, 0x30C0, 0x30C2, 0x30C5, 0x30C7, 0x30C9, KEY_LT, - 0x30DE, 0x30DF, 0x30E0, 0x30E1, 0x30E2, 0x30D0, 0x30D3, 0x30D6, 0x30D9, 0x30DC, KEY_RT, - 0x30E4, KEY_VOID, 0x30E6, KEY_VOID, 0x30E8, 0x30D1, 0x30D4, 0x30D7, 0x30DA, 0x30DD, KEY_VOID, - 0x30E9, 0x30EA, 0x30EB, 0x30EC, 0x30ED, 0x30FC, 0x3001, 0x3002, 0x300C, 0x300D, KEY_DONE, +static const WORD s_katakana[JAPANESE_KEYS] = +{ + 0x30a2, 0x30a4, 0x30a6, 0x30a8, 0x30aa, 0x30ef, 0x30f2, 0x30f3, 0x30f4, kcBlank, kcHiragana, + 0x30ab, 0x30ad, 0x30af, 0x30b1, 0x30b3, 0x30a1, 0x30a3, 0x30a5, 0x30a7, 0x30a9, kcKatakana, + 0x30b5, 0x30b7, 0x30b9, 0x30bb, 0x30bd, 0x30c3, 0x30e3, 0x30e5, 0x30e7, 0x30ee, kcEnglish, + 0x30bf, 0x30c1, 0x30c4, 0x30c6, 0x30c8, 0x30ac, 0x30ae, 0x30b0, 0x30b2, 0x30b4, kcSpace, + 0x30ca, 0x30cb, 0x30cc, 0x30cd, 0x30ce, 0x30b6, 0x30b8, 0x30ba, 0x30bc, 0x30be, kcBackspace, + 0x30cf, 0x30d2, 0x30d5, 0x30d8, 0x30db, 0x30c0, 0x30c2, 0x30c5, 0x30c7, 0x30c9, kcLeft, + 0x30de, 0x30df, 0x30e0, 0x30e1, 0x30e2, 0x30d0, 0x30d3, 0x30d6, 0x30d9, 0x30dc, kcRight, + 0x30e4, kcBlank, 0x30e6, kcBlank, 0x30e8, 0x30d1, 0x30d4, 0x30d7, 0x30da, 0x30dd, kcBlank, + 0x30e9, 0x30ea, 0x30eb, 0x30ec, 0x30ed, 0x30fc, 0x3001, 0x3002, 0x300c, 0x300d, kcDone, }; -static const WORD s_englishKeys[] = -{ - 0x41, 0x42, 0x43, 0x44, 0x45, 0x61, 0x62, 0x63, 0x64, 0x65, KEY_HIRAGANA, - 0x46, 0x47, 0x48, 0x49, 0x4A, 0x66, 0x67, 0x68, 0x69, 0x6A, KEY_KATAKANA, - 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, KEY_ENGLISH, - 0x50, 0x51, 0x52, 0x53, 0x54, 0x70, 0x71, 0x72, 0x73, 0x74, KEY_SPACE, - 0x55, 0x56, 0x57, 0x58, 0x59, 0x75, 0x76, 0x77, 0x78, 0x79, KEY_BS, - 0x5A, 0x22, 0x27, 0x40, 0x23, 0x7A, 0x28, 0x29, 0x7B, 0x7D, KEY_LT, - 0x26, 0x5E, 0x24, 0xA5, 0x25, 0x2D, 0x2B, 0x3D, 0x2A, 0x2F, KEY_RT, - 0x30, 0x31, 0x32, 0x33, 0x34, 0x3F, 0x21, 0x3A, 0x3B, 0x5C, KEY_VOID, - 0x35, 0x36, 0x37, 0x38, 0x39, 0x3C, 0x3E, 0x2C, 0x2E, 0x5F, KEY_DONE, +// The Japanese "English" page: capitals and lowercase side by side, then +// digits and punctuation. +static const WORD s_japaneseLatin[JAPANESE_KEYS] = +{ + 'A', 'B', 'C', 'D', 'E', 'a', 'b', 'c', 'd', 'e', kcHiragana, + 'F', 'G', 'H', 'I', 'J', 'f', 'g', 'h', 'i', 'j', kcKatakana, + 'K', 'L', 'M', 'N', 'O', 'k', 'l', 'm', 'n', 'o', kcEnglish, + 'P', 'Q', 'R', 'S', 'T', 'p', 'q', 'r', 's', 't', kcSpace, + 'U', 'V', 'W', 'X', 'Y', 'u', 'v', 'w', 'x', 'y', kcBackspace, + 'Z', '"', '\'', '@', '#', 'z', '(', ')', '{', '}', kcLeft, + '&', '^', '$', 0xa5, '%', '-', '+', '=', '*', '/', kcRight, + '0', '1', '2', '3', '4', '?', '!', ':', ';', '\\', kcBlank, + '5', '6', '7', '8', '9', '<', '>', ',', '.', '_', kcDone, }; -static const WORD* s_japaneseModes[] = { s_hiraganaKeys, s_katakanaKeys, s_englishKeys }; - - -// ===== Key label lookup ===================================================== +static const WORD* const s_layouts[2][3] = +{ + { s_westernAlpha, s_westernSymbols, s_westernAccents }, + { s_hiragana, s_katakana, s_japaneseLatin }, +}; -static const TCHAR* GetKeyLabel(WORD key) +static inline bool JapaneseGrid() { - switch (key) - { - case KEY_SHIFT: return _T("SHIFT"); - case KEY_CAPS: return _T("CAPS LOCK"); - case KEY_ALPHA: return _T("ALPHABET"); - case KEY_SYM: return _T("SYMBOLS"); - case KEY_ACC: return _T("ACCENTS"); - case KEY_ENGLISH: return _T("ENGLISH"); - case KEY_HIRAGANA: return _T("HIRAGANA"); - case KEY_KATAKANA: return _T("KATAKANA"); - case KEY_LT: return _T("<"); - case KEY_RT: return _T(">"); - case KEY_BS: return _T("BACKSPACE"); - case KEY_DONE: return _T("DONE"); - case KEY_SPACE: return _T("SPACE"); - case KEY_VOID: return _T(" "); - default: return NULL; - } + return g_nCurLanguage == 1; } - -// ===== Helpers ============================================================== - -static bool IsJapanese() { return g_nCurLanguage == 1; } - -static int GetRowCount() { return IsJapanese() ? 8 : 4; } - -static const WORD* GetActiveLayout(int westernMode, int japaneseMode) +static inline int BottomRow() { - return IsJapanese() ? s_japaneseModes[japaneseMode] : s_westernModes[westernMode]; + return JapaneseGrid() ? 8 : 4; } -static WORD KeyAt(const WORD* layout, int row, int col) +// Western grids key off mode, the Japanese grid off jmode. +static inline const WORD* GridFor(int mode, int jmode) { - return layout[row * KB_COLS + col]; + return JapaneseGrid() ? s_layouts[1][jmode] : s_layouts[0][mode]; } -static bool IsValidKey(const WORD* layout, int row, int col) +// 5960 calls CharUpperW for shifted keys; Theseus only maps a to z. +static TCHAR KeyUpper(TCHAR ch) { - return KeyAt(layout, row, col) != KEY_VOID; + if (ch >= 'a' && ch <= 'z') + return (TCHAR)(ch - ('a' - 'A')); + return ch; } -// ===== CKeyboard ============================================================ +// ===== CKeyboard =========================================================== +// 0x9c bytes in 5960: the nodearray at +0x1c, frame/text/control +0x28..+0x30, +// string +0x34, shift +0x38, caps +0x39, mode +0x40, jmode +0x44, the edit +// buffer +0x48 (32 WCHARs), its length +0x88, cursor +0x8c, row +0x90 and +// column +0x94. class CKeyboard : public CNode { @@ -216,9 +173,7 @@ class CKeyboard : public CNode CNode* m_frame; CNode* m_text; CNode* m_control; - TCHAR* m_string; - bool m_shift; bool m_caps; int m_mode; @@ -230,7 +185,6 @@ class CKeyboard : public CNode void selectLeft(); void selectRight(); void activate(); - void Backspace(); void Delete(); void CursorLeft(); @@ -238,32 +192,32 @@ class CKeyboard : public CNode void Shift(); void CycleMode(); void Insert(const TCHAR* szInsert); - - bool GetAsteriskFormattingState() { return g_bAsteriskFormatting; } - void SetAsteriskFormattingState(bool b) { g_bAsteriskFormatting = b; } + bool GetAsteriskFormattingState(); + void SetAsteriskFormattingState(bool bOn); void Advance(float nSeconds); void Render(); bool OnSetProperty(const PRD* pprd, const void* pvValue); protected: - TCHAR m_rgch[KB_MAX_LENGTH + 1]; - int m_nLength; - int m_nCursor; - int m_nRow; - int m_nColumn; + TCHAR m_edit[EDIT_MAX + 1]; + int m_editLen; + int m_cursor; // -1 means the whole string is selected + int m_row; + int m_col; - const WORD* ActiveLayout() { return GetActiveLayout(m_mode, m_jmode); } - TCHAR ApplyCase(TCHAR ch); - void ClearSelection(); - bool InsertChar(TCHAR ch); + const WORD* Grid() const { return GridFor(m_mode, m_jmode); } + WORD KeyAt(int row, int col) const { return Grid()[row * GRID_COLUMNS + col]; } + void TakeSelection(); + void OpenGap(int at, int count); DECLARE_NODE_PROPS() - DECLARE_NODE_FUNCTIONS(); + DECLARE_NODE_FUNCTIONS() }; IMPLEMENT_NODE("Keyboard", CKeyboard, CNode) +// PRD at 0x00175448, same order. START_NODE_PROPS(CKeyboard, CNode) NODE_PROP(pt_nodearray, CKeyboard, keys) NODE_PROP(pt_node, CKeyboard, frame) @@ -276,6 +230,7 @@ START_NODE_PROPS(CKeyboard, CNode) NODE_PROP(pt_string, CKeyboard, string) END_NODE_PROPS() +// FND at 0x00188250 (the binary lists Set before Get). #define _FND_CLASS CKeyboard START_NODE_FUN(CKeyboard, CNode) NODE_FUN_VII(selectKey) @@ -296,485 +251,512 @@ START_NODE_FUN(CKeyboard, CNode) END_NODE_FUN() #undef _FND_CLASS -CKeyboard::CKeyboard() : - m_control(NULL), - m_frame(NULL), - m_text(NULL), - m_mode(MODE_ALPHA), - m_jmode(JMODE_HIRAGANA), - m_shift(false), - m_caps(false), - m_string(NULL) -{ - m_nLength = 0; - m_nCursor = -1; - m_nRow = 0; - m_nColumn = 0; -} - -CKeyboard::~CKeyboard() +// CKeyboard::CKeyboard() +// Reconstructed from 5960 retail XBE @ 0x0005ab80: every field zero except +// the cursor, which starts at -1 so the first key replaces the whole string. +CKeyboard::CKeyboard() { - if (m_text != NULL) - m_text->Release(); - - if (m_frame != NULL) - m_frame->Release(); - - if (m_control != NULL) - m_control->Release(); - - delete [] m_string; + m_frame = NULL; + m_text = NULL; + m_control = NULL; + m_string = NULL; + m_shift = false; + m_caps = false; + m_mode = 0; + m_jmode = 0; + m_editLen = 0; + m_cursor = -1; + m_row = 0; + m_col = 0; } -void CKeyboard::ClearSelection() +// CKeyboard::~CKeyboard() +// Reconstructed from 5960 retail XBE @ 0x0005ac20: +// if (text) text->Release(); if (frame) frame->Release(); +// if (control) control->Release(); free(string); keys.ReleaseAll(); +CKeyboard::~CKeyboard() { - if (m_nCursor == -1) + CNode* owned[] = { m_text, m_frame, m_control }; + for (int i = 0; i < 3; i++) { - m_nLength = 0; - m_nCursor = 0; + if (owned[i] != NULL) + owned[i]->Release(); } + delete [] m_string; } -TCHAR CKeyboard::ApplyCase(TCHAR ch) +// Every edit starts here: with the whole string selected (cursor -1) the edit +// replaces it, so the string empties and the cursor goes to 0. The binary +// repeats this block at the top of each editing function. +void CKeyboard::TakeSelection() { - // Only a-z change case; anything wider passes through untouched. - if ((m_caps ^ m_shift) && ch >= 'a' && ch <= 'z') - ch = (TCHAR)(ch - 'a' + 'A'); - return ch; + if (m_cursor != -1) + return; + m_editLen = 0; + m_cursor = 0; } -bool CKeyboard::InsertChar(TCHAR ch) +// Shifts the tail from `at` right by `count` characters. +void CKeyboard::OpenGap(int at, int count) { - ClearSelection(); - - if (m_nLength >= KB_MAX_LENGTH) - { - CallFunction(this, _T("OnError")); - return false; - } - - MoveMemory(&m_rgch[m_nCursor + 1], &m_rgch[m_nCursor], (m_nLength - m_nCursor) * sizeof(TCHAR)); - m_rgch[m_nCursor] = ch; - m_nCursor += 1; - m_nLength += 1; - return true; + MoveMemory(m_edit + at + count, m_edit + at, (m_editLen - at) * sizeof(TCHAR)); } -// ===== Navigation =========================================================== +// ===== Moving the highlight ================================================ +// CKeyboard::selectKey(row, col) [script fn 'selectKey', FND 0x00188250, sig 18] +// Reconstructed from 5960 retail XBE @ 0x0005ace0: +// row = (row < 0 || row > (lang==1 ? 8 : 4)) ? 0 : row; +// col = (col < 0 || col > 10) ? 0 : col; void CKeyboard::selectKey(int nRow, int nColumn) { - int rows = GetRowCount(); - - m_nRow = (nRow < 0 || nRow > rows) ? 0 : nRow; - m_nColumn = (nColumn < 0 || nColumn > 10) ? 0 : nColumn; + bool rowOk = nRow >= 0 && nRow <= BottomRow(); + bool colOk = nColumn >= 0 && nColumn < GRID_COLUMNS; + m_row = rowOk ? nRow : 0; + m_col = colOk ? nColumn : 0; } +// CKeyboard::selectUp() [script fn 'selectUp', FND 0x00188268, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005ad40: +// do { row = row ? row - 1 : bottom; } while (grid[row*11 + col] == 0xe); void CKeyboard::selectUp() { - const WORD* layout = ActiveLayout(); - int rows = GetRowCount(); - int attempts = rows + 1; - - while (attempts-- > 0) + do { - m_nRow = (m_nRow == 0) ? rows : m_nRow - 1; - if (IsValidKey(layout, m_nRow, m_nColumn)) - return; + m_row = (m_row == 0) ? BottomRow() : m_row - 1; } + while (KeyAt(m_row, m_col) == kcBlank); } +// CKeyboard::selectDown() [script fn 'selectDown', FND 0x00188280, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005adb0: +// do { row = (row == bottom) ? 0 : row + 1; } while (grid[row*11 + col] == 0xe); void CKeyboard::selectDown() { - const WORD* layout = ActiveLayout(); - int rows = GetRowCount(); - int attempts = rows + 1; - - while (attempts-- > 0) + do { - m_nRow = (m_nRow == rows) ? 0 : m_nRow + 1; - if (IsValidKey(layout, m_nRow, m_nColumn)) - return; + m_row = (m_row == BottomRow()) ? 0 : m_row + 1; } + while (KeyAt(m_row, m_col) == kcBlank); } +// CKeyboard::selectLeft() [script fn 'selectLeft', FND 0x00188298, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005ae60. Column 0 wraps to 10 +// (again if that's a blank). Japanese steps one column, skipping blanks. +// Western rows 3 and 4 hop over the wide keys: row 3 from 7..10 lands on 6; +// row 4 from 9..10 lands on 8, from 7..8 on 6, from 1..6 on 0. void CKeyboard::selectLeft() { - const WORD* layout = ActiveLayout(); - - if (IsJapanese()) + for (;;) { - int attempts = KB_COLS; - while (attempts-- > 0) + if (m_col == 0) { - m_nColumn = (m_nColumn == 0) ? 10 : m_nColumn - 1; - if (IsValidKey(layout, m_nRow, m_nColumn)) + m_col = GRID_COLUMNS - 1; + if (KeyAt(m_row, m_col) != kcBlank) return; - } - } - else - { - if (m_nColumn == 0) - { - m_nColumn = 10; - return; + continue; } - if (m_nRow == 3) + if (JapaneseGrid()) { - m_nColumn = (m_nColumn > 6) ? 6 : m_nColumn - 1; - } - else if (m_nRow == 4) - { - if (m_nColumn > 8) - m_nColumn = 8; - else if (m_nColumn > 6) - m_nColumn = 6; - else - m_nColumn = 0; + m_col--; + if (KeyAt(m_row, m_col) != kcBlank) + return; + continue; } + + if (m_row == 3 && m_col > 6) + m_col = 6; + else if (m_row == 4) + m_col = (m_col > 8) ? 8 : (m_col > 6) ? 6 : 0; else - { - m_nColumn -= 1; - } + m_col--; + return; } } +// CKeyboard::selectRight() [script fn 'selectRight', FND 0x001882b0, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005af50. The mirror of selectLeft: +// column 10 wraps to 0, Japanese skips blanks, western row 3 wraps from 7 on +// and row 4 goes 0 to 1, 1..6 to 7, 7..8 to 9, 9 on to 0. void CKeyboard::selectRight() { - const WORD* layout = ActiveLayout(); - - if (IsJapanese()) + for (;;) { - int attempts = KB_COLS; - while (attempts-- > 0) + if (m_col == GRID_COLUMNS - 1) { - m_nColumn = (m_nColumn == 10) ? 0 : m_nColumn + 1; - if (IsValidKey(layout, m_nRow, m_nColumn)) + m_col = 0; + if (KeyAt(m_row, m_col) != kcBlank) return; + continue; } - } - else - { - if (m_nRow == 3) + + if (JapaneseGrid()) { - if (m_nColumn >= 7) - { - m_nColumn = 0; + m_col++; + if (KeyAt(m_row, m_col) != kcBlank) return; - } - m_nColumn += 1; + continue; } - else if (m_nRow == 4) + + if (m_row == 3) + m_col = (m_col < 7) ? m_col + 1 : 0; + else if (m_row == 4) { - if (m_nColumn >= 9) - { - m_nColumn = 0; - return; - } - - if (m_nColumn == 0) - m_nColumn = 1; - else if (m_nColumn < 7) - m_nColumn = 7; - else if (m_nColumn < 9) - m_nColumn = 9; + if (m_col >= 9) + m_col = 0; + else if (m_col == 0) + m_col = 1; + else + m_col = (m_col > 6) ? 9 : 7; } else - { - if (m_nColumn == 10) - { - m_nColumn = 0; - return; - } - m_nColumn += 1; - } + m_col++; + return; } } -// ===== Key activation ======================================================= +// ===== Pressing a key ====================================================== +// CKeyboard::activate() [script fn 'activate', FND 0x001882c8, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005b340. Looks up the highlighted +// key and acts on it: function keys switch case or page, move the cursor or +// erase; DONE trims the string, copies it to `string` and calls OnDone (an +// empty or all blank string calls OnError instead); SPACE inserts quietly +// unless full; anything else inserts the character (uppercased when shift +// and caps disagree), drops shift, or calls OnError when full. +// See docs/decomp/Keyboard.md. void CKeyboard::activate() { - const WORD* layout = ActiveLayout(); - WORD key = KeyAt(layout, m_nRow, m_nColumn); + WORD key = KeyAt(m_row, m_col); switch (key) { - case KEY_DONE: - { - // Reject all-whitespace strings - int i; - for (i = 0; i < m_nLength; i += 1) - { - if (m_rgch[i] != ' ') - break; - } - - if (i == m_nLength) - { - CallFunction(this, _T("OnError")); - break; - } - - // Trim trailing spaces - TCHAR* pEnd = m_rgch + (m_nLength - 1); - while (*pEnd == _T(' ')) - { - *pEnd = _T('\0'); - pEnd--; - } - - // Trim leading spaces - TCHAR* pStart = m_rgch; - while (*pStart == _T(' ')) - pStart++; - - if (pStart != m_rgch) - MoveMemory(m_rgch, pStart, sizeof(TCHAR) * (_tcslen(pStart) + 1)); - - delete [] m_string; - m_string = new TCHAR [m_nLength + 1]; - CopyChars(m_string, m_rgch, m_nLength); - m_string[m_nLength] = 0; - - CallFunction(this, _T("OnDone")); - } - break; + case kcShift: + Shift(); + return; - case KEY_SHIFT: Shift(); break; - case KEY_CAPS: - if (m_mode != MODE_ALPHA && m_mode != MODE_ACCENT) - CallFunction(this, _T("OnError")); - else - m_caps = !m_caps; - break; - - case KEY_ALPHA: m_mode = MODE_ALPHA; break; - case KEY_SYM: m_mode = MODE_SYMBOL; break; - case KEY_ACC: m_mode = MODE_ACCENT; break; - case KEY_HIRAGANA: m_jmode = JMODE_HIRAGANA; break; - case KEY_KATAKANA: m_jmode = JMODE_KATAKANA; break; - case KEY_ENGLISH: m_jmode = JMODE_ENGLISH; break; - case KEY_LT: CursorLeft(); break; - case KEY_RT: CursorRight(); break; - case KEY_BS: Backspace(); break; - - case KEY_SPACE: - // Space on a full string is ignored without the error sound. - ClearSelection(); - if (m_nLength < KB_MAX_LENGTH) - InsertChar(_T(' ')); - break; + case kcCaps: + if (m_mode != 0 && m_mode != 2) + break; + m_caps = !m_caps; + return; + + case kcAlphabet: m_mode = 0; return; + case kcSymbols: m_mode = 1; return; + case kcAccents: m_mode = 2; return; + case kcHiragana: m_jmode = 0; return; + case kcKatakana: m_jmode = 1; return; + case kcEnglish: m_jmode = 2; return; + + case kcLeft: CursorLeft(); return; + case kcRight: CursorRight(); return; + case kcBackspace: Backspace(); return; + + case kcDone: + { + int lead = 0; + while (lead < m_editLen && m_edit[lead] == ' ') + lead++; + if (lead == m_editLen) + break; + + // Blank out trailing spaces, then slide the text over the leading ones. + for (int end = m_editLen - 1; m_edit[end] == ' '; end--) + m_edit[end] = 0; + if (lead > 0) + MoveMemory(m_edit, m_edit + lead, (_tcslen(m_edit + lead) + 1) * sizeof(TCHAR)); + + // The copy keeps the untrimmed length, so trimmed spaces come along as nulls. + delete [] m_string; + m_string = new TCHAR[m_editLen + 1]; + CopyChars(m_string, m_edit, m_editLen); + m_string[m_editLen] = 0; + CallFunction(this, _T("OnDone")); + return; + } + + case kcSpace: + TakeSelection(); + if (m_editLen >= EDIT_MAX) + return; + OpenGap(m_cursor, 1); + m_edit[m_cursor++] = ' '; + m_editLen++; + return; default: - // Shift stays on if the string was full. - if (InsertChar(ApplyCase((TCHAR)key))) - m_shift = false; - break; + TakeSelection(); + if (m_editLen >= EDIT_MAX) + break; + OpenGap(m_cursor, 1); + m_edit[m_cursor++] = (m_caps != m_shift) ? KeyUpper((TCHAR)key) : (TCHAR)key; + m_editLen++; + m_shift = false; + return; } + + CallFunction(this, _T("OnError")); } -// ===== Text editing ========================================================= +// ===== Editing ============================================================= +// CKeyboard::Backspace() [script fn 'Backspace', FND 0x001882f8, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005b630: +// if (cursor == -1) { len = 0; cursor = 0; } +// if (cursor > 0) { memmove(&buf[cursor-1], &buf[cursor], (len-cursor)*2); len--; cursor--; } +// else CallFunction(this, L"OnError"); void CKeyboard::Backspace() { - ClearSelection(); - - if (m_nCursor > 0) - { - MoveMemory(&m_rgch[m_nCursor - 1], &m_rgch[m_nCursor], (m_nLength - m_nCursor) * sizeof(TCHAR)); - m_nLength -= 1; - m_nCursor -= 1; - } - else + TakeSelection(); + if (m_cursor <= 0) { CallFunction(this, _T("OnError")); + return; } + MoveMemory(m_edit + m_cursor - 1, m_edit + m_cursor, (m_editLen - m_cursor) * sizeof(TCHAR)); + m_cursor--; + m_editLen--; } +// CKeyboard::Delete() [script fn 'Delete', FND 0x00188310, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005b6a0: takes the selection, then +// if there is a character under the cursor steps past it and backspaces over +// it, otherwise OnError. void CKeyboard::Delete() { - ClearSelection(); - - if (m_nCursor < m_nLength) - { - CursorRight(); - Backspace(); - } - else + TakeSelection(); + if (m_cursor >= m_editLen) { CallFunction(this, _T("OnError")); + return; } + m_cursor++; + Backspace(); } +// CKeyboard::CursorLeft() [script fn 'CursorLeft', FND 0x00188328, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005b700: +// if (cursor == -1) cursor = 0; else if (cursor > 0) cursor--; else OnError; void CKeyboard::CursorLeft() { - if (m_nCursor == -1) - m_nCursor = 0; - else if (m_nCursor > 0) - m_nCursor -= 1; + if (m_cursor > 0) + m_cursor--; + else if (m_cursor == -1) + m_cursor = 0; else CallFunction(this, _T("OnError")); } +// CKeyboard::CursorRight() [script fn 'CursorRight', FND 0x00188340, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005b740: +// if (cursor == -1) cursor = len; else if (cursor < len) cursor++; else OnError; void CKeyboard::CursorRight() { - if (m_nCursor == -1) - m_nCursor = m_nLength; - else if (m_nCursor < m_nLength) - m_nCursor += 1; + if (m_cursor == -1) + m_cursor = m_editLen; + else if (m_cursor < m_editLen) + m_cursor++; else CallFunction(this, _T("OnError")); } +// CKeyboard::Shift() [script fn 'Shift', FND 0x00188358, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005b780: +// if (mode != 0 && mode != 2) OnError; else shift = !shift; +// Only the letter and accent pages have a case. void CKeyboard::Shift() { - if (m_mode != MODE_ALPHA && m_mode != MODE_ACCENT) - CallFunction(this, _T("OnError")); - else + if (m_mode == 0 || m_mode == 2) m_shift = !m_shift; + else + CallFunction(this, _T("OnError")); } +// CKeyboard::CycleMode() [script fn 'CycleMode', FND 0x00188370, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005b7b0: bumps mode and jmode +// together, each going back to 0 once past 2. void CKeyboard::CycleMode() { - m_mode = (m_mode + 1 > 2) ? 0 : m_mode + 1; - m_jmode = (m_jmode + 1 > 2) ? 0 : m_jmode + 1; + m_mode++; + m_jmode++; + if (m_mode > 2) + m_mode = 0; + if (m_jmode > 2) + m_jmode = 0; } +// CKeyboard::Insert(text) [script fn 'Insert', FND 0x00188388, sig 12] +// Reconstructed from 5960 retail XBE @ 0x0005b7e0: +// take selection; n = wcslen(s); if (len + n > 31) { OnError; return; } +// memmove(&buf[cursor+n], &buf[cursor], (len-cursor)*2); copy s in; +// cursor += n; len += n; shift = 0; void CKeyboard::Insert(const TCHAR* szInsert) { - ClearSelection(); - - int cch = _tcslen(szInsert); - - if (m_nLength + cch > KB_MAX_LENGTH) + TakeSelection(); + int count = (int)_tcslen(szInsert); + if (m_editLen + count > EDIT_MAX) { CallFunction(this, _T("OnError")); return; } - - MoveMemory(&m_rgch[m_nCursor + cch], &m_rgch[m_nCursor], (m_nLength - m_nCursor) * sizeof(TCHAR)); - CopyChars(&m_rgch[m_nCursor], szInsert, cch); - m_nCursor += cch; - m_nLength += cch; + OpenGap(m_cursor, count); + CopyChars(m_edit + m_cursor, szInsert, count); + m_cursor += count; + m_editLen += count; m_shift = false; } +// CKeyboard::GetAsteriskFormattingState / SetAsteriskFormattingState +// [script fns, FND 0x001883b8 sig 2 / 0x001883a0 sig 14] +// Reconstructed from 5960 retail XBE @ 0x0005acb0 / 0x0005acc0: read and +// write the one global byte at 0x0017e74c that turns *heading* text on. +bool CKeyboard::GetAsteriskFormattingState() +{ + return g_bAsteriskFormatting; +} -// ===== Properties =========================================================== +void CKeyboard::SetAsteriskFormattingState(bool bOn) +{ + g_bAsteriskFormatting = bOn; +} + +// ===== Node hooks ========================================================== + +// CKeyboard::OnSetProperty(pprd, pvValue) +// Reconstructed from 5960 retail XBE @ 0x0005b970: +// if (prd->offset == 0x34 /* string */) { +// s = *value ? *value : L""; n = min(wcslen(s), 31); +// copy n chars into buf; len = n; cursor = -1; } +// return 1; +// A script setting `string` loads the edit buffer with the whole thing selected. bool CKeyboard::OnSetProperty(const PRD* pprd, const void* pvValue) { - if (PTR2INT(pprd->pbOffset) == offsetof(m_string)) - { - TCHAR* szNew = *(TCHAR**)pvValue; - if (szNew == NULL) - szNew = _T(""); - int cch = _tcslen(szNew); - if (cch > KB_MAX_LENGTH) - cch = KB_MAX_LENGTH; - CopyChars(m_rgch, szNew, cch); - m_nLength = cch; - m_nCursor = -1; - } - + if (PTR2INT(pprd->pbOffset) != offsetof(m_string)) + return true; + + const TCHAR* szValue = *(const TCHAR* const*)pvValue; + if (szValue == NULL) + szValue = _T(""); + int count = (int)_tcslen(szValue); + if (count > EDIT_MAX) + count = EDIT_MAX; + CopyChars(m_edit, szValue, count); + m_editLen = count; + m_cursor = -1; return true; } +// CKeyboard::Advance(nSeconds) +// Reconstructed from 5960 retail XBE @ 0x0005bca0: +// CNode::Advance(t); if (control) control->Advance(t); +void CKeyboard::Advance(float nSeconds) +{ + CNode::Advance(nSeconds); + if (m_control != NULL) + m_control->Advance(nSeconds); +} + +// Western rows 3 and 4 have keys several columns wide; any column of one +// highlights its first cell. +static int MergedCell(int cell) +{ + if (cell >= 41 && cell <= 43) + return 40; // BACKSPACE + if (cell >= 46 && cell <= 50) + return 45; // SPACE + if (cell == 52) + return 51; // < + if (cell == 54) + return 53; // > + return cell; +} -// ===== Rendering ============================================================ +static const TCHAR* FunctionKeyLabel(WORD key) +{ + switch (key) + { + case kcShift: return _T("SHIFT"); + case kcCaps: return _T("CAPS LOCK"); + case kcAlphabet: return _T("ALPHABET"); + case kcSymbols: return _T("SYMBOLS"); + case kcAccents: return _T("ACCENTS"); + case kcLeft: return _T("<"); + case kcRight: return _T(">"); + case kcBackspace: return _T("BACKSPACE"); + case kcDone: return _T("DONE"); + case kcSpace: return _T("SPACE"); + case kcHiragana: return _T("HIRAGANA"); + case kcKatakana: return _T("KATAKANA"); + case kcEnglish: return _T("ENGLISH"); + case kcBlank: return _T(" "); + } + return NULL; +} +// CKeyboard::Render() +// Reconstructed from 5960 retail XBE @ 0x0005b9d0. Draws the frame, then the +// text node with the edit string and cursor handed over through g_szText / +// g_nTextChar. The key nodes only draw when the nodearray has exactly 45 +// (western) or 99 (Japanese) entries. Each grid cell that starts a new key +// gets the next node: its label goes in g_szText (the function key name, or +// the character, uppercased on western pages when shift and caps disagree), +// g_bActiveKey says whether it is highlighted, and blanks are skipped. void CKeyboard::Render() { - const WORD* layout = ActiveLayout(); + const WORD* grid = Grid(); if (m_frame != NULL) m_frame->Render(); - // Render text input field if (m_text != NULL) { - m_rgch[m_nLength] = 0; - g_szText = m_rgch; - g_nTextChar = m_nCursor; + m_edit[m_editLen] = 0; + g_szText = m_edit; + g_nTextChar = m_cursor; m_text->Render(); g_nTextChar = -1; } - // Determine grid size - int totalKeys; - if (IsJapanese()) - { - if (m_keys.GetLength() != 99) - return; - totalKeys = 99; - } - else - { - if (m_keys.GetLength() != 45) - return; - totalKeys = 55; - } - - // Map selected position to a flat key index, accounting for merged keys - int selectedIdx = m_nRow * KB_COLS + m_nColumn; - if (!IsJapanese()) - { - if (selectedIdx > 40 && selectedIdx <= 43) - selectedIdx = 40; - else if (selectedIdx > 45 && selectedIdx <= 50) - selectedIdx = 45; - else if (selectedIdx == 52) - selectedIdx = 51; - else if (selectedIdx == 54) - selectedIdx = 53; - } + bool japanese = JapaneseGrid(); + if (m_keys.GetLength() != (japanese ? JAPANESE_NODES : WESTERN_NODES)) + return; - TCHAR charBuf[2]; - int nodeIdx = 0; + int cells = japanese ? JAPANESE_KEYS : WESTERN_KEYS; + int highlighted = m_row * GRID_COLUMNS + m_col; + if (!japanese) + highlighted = MergedCell(highlighted); - for (int i = 0; i < totalKeys; i += 1) + TCHAR label[2]; + int node = 0; + for (int cell = 0; cell < cells; cell++) { - // Western layout: skip duplicate entries for merged keys (BS, Space, LT, RT) - if (!IsJapanese() && i > 0 && layout[i] == layout[i - 1]) + WORD key = grid[cell]; + if (!japanese && cell > 0 && key == grid[cell - 1]) continue; - CNode* pNode = m_keys.GetNode(nodeIdx); - WORD key = layout[i]; + CNode* pKey = m_keys.GetNode(node++); - // Set the label text for this key - const TCHAR* label = GetKeyLabel(key); - if (label != NULL) + const TCHAR* name = FunctionKeyLabel(key); + if (name != NULL) { - g_szText = (TCHAR*)label; + g_szText = (TCHAR*)name; } else { - charBuf[0] = (TCHAR)key; - charBuf[1] = 0; - - if (!IsJapanese()) - charBuf[0] = ApplyCase(charBuf[0]); - - g_szText = charBuf; + label[0] = (TCHAR)key; + if (!japanese && m_caps != m_shift) + label[0] = KeyUpper(label[0]); + label[1] = 0; + g_szText = label; } - g_bActiveKey = (i == selectedIdx); - if (key != KEY_VOID) - pNode->Render(); - - nodeIdx += 1; + g_bActiveKey = (cell == highlighted); + if (key != kcBlank) + pKey->Render(); } } - -void CKeyboard::Advance(float nSeconds) -{ - CNode::Advance(nSeconds); - - if (m_control != NULL) - m_control->Advance(nSeconds); -} From 0c8a66669cb822e0ff796222d6e47dc744e2e78b Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:06:47 -0500 Subject: [PATCH 089/167] A wrong Live passcode fires OnInvalidPassword like 5960 Logon took any passcode. 5960 checks a non-empty one against the account's stored pad code first and calls OnInvalidPassword when it doesn't match, so now we do too. The shipped scripts pass an empty passcode, so they're unaffected. --- theseus/xbox/live_accounts.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/theseus/xbox/live_accounts.cpp b/theseus/xbox/live_accounts.cpp index d4830ba..939e845 100755 --- a/theseus/xbox/live_accounts.cpp +++ b/theseus/xbox/live_accounts.cpp @@ -352,10 +352,32 @@ int CLiveAccounts::InternalLogon(int index, const TCHAR* passcode) +static void EncodePasscode(const TCHAR* szPasscode, BYTE passcode[4]); + +// CLiveAccounts::Logon [script fn 'Logon', FND 0x00188d80, sig 28] +// The passcode gate is from 5960 @ 0x000699e0: +// if (index < count) { +// if (passcode && *passcode && Encode(passcode) != users[index].passcode) +// { m_hr = 0x80151006; CallFunction(this, L"OnInvalidPassword"); return; } +// ... XOnlineLogon ... +// } +// An empty passcode skips the check, which is how the shipped scripts call it. +// The logon after the gate is Theseus's own. void CLiveAccounts::Logon(int index, const TCHAR* passcode) { OutputDebugString(_T("[LiveAccounts] Logon() called\n")); + if (index >= 0 && (DWORD)index < g_NumUsers && passcode != NULL && *passcode != 0) + { + BYTE code[4]; + EncodePasscode(passcode, code); + if (memcmp(code, g_Users[index].pin, sizeof(code)) != 0) + { + CallFunction(this, _T("OnInvalidPassword")); + return; + } + } + int result = InternalLogon(index, passcode); if (result) From 9e22065703dfbc7d3fe0f31178d2a7de479971fc Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:07:50 -0500 Subject: [PATCH 090/167] Desktop uses the shared keyboard node Dropped the desktop copy of the keyboard and built shared/keyboard_node.cpp instead. The only desktop part left is the physical keyboard hookup. Text, cursor, callbacks and key labels match the old desktop file on all 159,085 checked steps, SDL key handling included. --- build/Makefile | 2 +- theseus/desktop/keyboard.cpp | 1005 ------------------------------ theseus/shared/keyboard_node.cpp | 48 ++ 3 files changed, 49 insertions(+), 1006 deletions(-) delete mode 100755 theseus/desktop/keyboard.cpp diff --git a/build/Makefile b/build/Makefile index b67b7c4..48f25f6 100644 --- a/build/Makefile +++ b/build/Makefile @@ -862,7 +862,7 @@ DESKTOP_SRCS_SYSTEM = \ $(THESEUS_SRC)/desktop/dashinit.cpp \ $(THESEUS_SRC)/desktop/config.cpp \ $(THESEUS_SRC)/desktop/joystick.cpp \ - $(THESEUS_SRC)/desktop/keyboard.cpp \ + $(THESEUS_SRC)/shared/keyboard_node.cpp \ $(THESEUS_SRC)/desktop/locale.cpp \ $(THESEUS_SRC)/shared/timezone.cpp \ $(THESEUS_SRC)/desktop/debug.cpp \ diff --git a/theseus/desktop/keyboard.cpp b/theseus/desktop/keyboard.cpp deleted file mode 100755 index 55ef5b1..0000000 --- a/theseus/desktop/keyboard.cpp +++ /dev/null @@ -1,1005 +0,0 @@ -// keyboard.cpp: desktop CKeyboard on-screen keyboard node. Handles -// Western (alpha / symbol / accent) and Japanese (hiragana / -// katakana / English) input modes with cursor navigation, backspace, -// shift / caps, and character insertion. Counterpart to -// shared/keyboard_node.cpp. - -#include "std.h" -#include "dashapp.h" -#include "node.h" -#include "runner.h" - -extern CNode* GetTextNode(const char* szText, float nWidth); -extern bool g_bActiveKey; -extern char* g_szText; -extern bool g_bAsteriskFormatting; // text.cpp, gates *heading* text -extern int g_nTextChar; -extern int g_nCurLanguage; - -// ============================================================================ -// Constants and Key Definitions -// ============================================================================ - -#define MAX_LENGTH 31 - -#define modeAlpha 0 -#define modeSymbol 1 -#define modeAccent 2 - -#define jmodeHiragana 0 -#define jmodeKatakana 1 -#define jmodeEnglish 2 - -#define keyShift 1 -#define keyCaps 2 -#define keyAlpha 3 -#define keySym 4 -#define keyAcc 5 -#define keyLT 6 -#define keyRT 7 -#define keyBS 8 -#define keyDone 9 -#define keySpace 10 - -// Japanese keys -#define keyHiragana 11 -#define keyKatakana 12 -#define keyEnglish 13 - -#define keyVoid 14 - -#define _nbsp 0x00a0 // non-breaking space -#define _iexcl 0x00a1 // � -#define _cent 0x00a2 // � -#define _pound 0x00a3 // � -#define _curren 0x00a4 // � -#define _yen 0x00a5 // � -#define _brvbar 0x00a6 // � -#define _sect 0x00a7 // � -#define _uml 0x00a8 // � -#define _copy 0x00a9 // � -#define _ordf 0x00aa // � -#define _laquo 0x00ab // � -#define _not 0x00ac // � -#define _shy 0x00ad // soft hyphen -#define _reg 0x00ae // � -#define _macr 0x00af // � -#define _deg 0x00b0 // � -#define _plusmn 0x00b1 // � -#define _sup2 0x00b2 // � -#define _sup3 0x00b3 // � -#define _acute 0x00b4 // � -#define _micro 0x00b5 // � -#define _para 0x00b6 // � -#define _middot 0x00b7 // � -#define _cedil 0x00b8 // � -#define _sup1 0x00b9 // � -#define _ordm 0x00ba // � -#define _raquo 0x00bb // � -#define _iquest 0x00bf // � -#define _Agrave 0x00c0 // � -#define _Aacute 0x00c1 // � -#define _Acirc 0x00c2 // � -#define _Atilde 0x00c3 // � -#define _Auml 0x00c4 // � -#define _Aring 0x00c5 // � -#define _AElig 0x00c6 // � -#define _Ccedil 0x00c7 // � -#define _Egrave 0x00c8 // � -#define _Eacute 0x00c9 // � -#define _Ecirc 0x00ca // � -#define _Euml 0x00cb // � -#define _Igrave 0x00cc // � -#define _Iacute 0x00cd // � -#define _Icirc 0x00ce // � -#define _Iuml 0x00cf // � -#define _ETH 0x00d0 // � -#define _Ntilde 0x00d1 // � -#define _Ograve 0x00d2 // � -#define _Oacute 0x00d3 // � -#define _Ocirc 0x00d4 // � -#define _Otilde 0x00d5 // � -#define _Ouml 0x00d6 // � -#define _times 0x00d7 // � -#define _Oslash 0x00d8 // � -#define _Ugrave 0x00d9 // � -#define _Uacute 0x00da // � -#define _Ucirc 0x00db // � -#define _Uuml 0x00dc // � -#define _Yacute 0x00dd // � -#define _THORN 0x00de // � -#define _szlig 0x00df // � -#define _agrave 0x00e0 // � -#define _aacute 0x00e1 // � -#define _acirc 0x00e2 // � -#define _atilde 0x00e3 // � -#define _auml 0x00e4 // � -#define _aring 0x00e5 // � -#define _aelig 0x00e6 // � -#define _ccedil 0x00e7 // � -#define _egrave 0x00e8 // � -#define _eacute 0x00e9 // � -#define _ecirc 0x00ea // � -#define _euml 0x00eb // � -#define _igrave 0x00ec // � -#define _iacute 0x00ed // � -#define _icirc 0x00ee // � -#define _iuml 0x00ef // � -#define _eth 0x00f0 // � -#define _ntilde 0x00f1 // � -#define _ograve 0x00f2 // � -#define _oacute 0x00f3 // � -#define _ocirc 0x00f4 // � -#define _otilde 0x00f5 // � -#define _ouml 0x00f6 // � -#define _divide 0x00f7 // � -#define _oslash 0x00f8 // � -#define _ugrave 0x00f9 // � -#define _uacute 0x00fa // � -#define _ucirc 0x00fb // � -#define _uuml 0x00fc // � -#define _yacute 0x00fd // � -#define _thorn 0x00fe // � -#define _yuml 0x00ff // � - -#define _euro 0x20ac // euro sign - -// ============================================================================ -// Key Layout Tables -// ============================================================================ - -static const uint16_t rgkeyAlpha [] = -{ - keyDone, L'1', L'2', L'3', L'4', L'5', L'6', L'7', L'8', L'9', L'0', - keyShift, L'a', L'b', L'c', L'd', L'e', L'f', L'g', L'h', L'i', L'j', - keyCaps, L'k', L'l', L'm', L'n', L'o', L'p', L'q', L'r', L's', L't', - keyAcc, L'u', L'v', L'w', L'x', L'y', L'z', keyBS, keyBS, keyBS, keyBS, - keySym, keySpace, keySpace, keySpace, keySpace, keySpace, keySpace, keyLT, keyLT, keyRT, keyRT, -}; - -static const uint16_t rgkeySymbol [] = -{ - keyDone, L'(', L')', L'&', L'_', L'^', L'%', L'\\', L'/', L'@', L'#', - keyShift, L'[', L']', L'$', _pound, _euro, _yen, L';', L':', L'\'', L'"', - keyCaps, L'<', L'>', L'?', L'!', _iquest, _iexcl, L'-', L'*', L'+', L'=', - keyAcc, L'{', L'}', _laquo, _raquo, ',', L'.', keyBS, keyBS, keyBS, keyBS, - keyAlpha, keySpace, keySpace, keySpace, keySpace, keySpace, keySpace, keyLT, keyLT, keyRT, keyRT, -}; - -static const uint16_t rgkeyAccent [] = -{ - keyDone, L'1', L'2', L'3', L'4', L'5', L'6', L'7', L'8', L'9', L'0', - keyShift, _agrave, _aacute, _acirc, _auml, _egrave, _eacute, _ecirc, _euml, _igrave, _iacute, - keyCaps, _icirc, _iuml, _ograve, _oacute, _ocirc, _otilde, _ugrave, _uacute, _ucirc, _uuml, - keyAlpha, _yacute, _yuml, _ccedil, _szlig, _ntilde, _aelig, keyBS, keyBS, keyBS, keyBS, - keySym, keySpace, keySpace, keySpace, keySpace, keySpace, keySpace, keyLT, keyLT, keyRT, keyRT, -}; - -static const uint16_t* rgmode [] = -{ - rgkeyAlpha, - rgkeySymbol, - rgkeyAccent -}; - -static const uint16_t HiraganaSet [] = -{ - 0x3042, 0x3044, 0x3046, 0x3048, 0x304A, 0x308F, 0x3092, 0x3093, keyVoid,keyVoid,keyHiragana, - 0x304B, 0x304D, 0x304F, 0x3051, 0x3053, 0x3041, 0x3043, 0x3045, 0x3047, 0x3049, keyKatakana, - 0x3055, 0x3057, 0x3059, 0x305B, 0x305D, 0x3063, 0x3083, 0x3085, 0x3087, 0x308e, keyEnglish, - 0x305F, 0x3061, 0x3064, 0x3066, 0x3068, 0x304c, 0x304e, 0x3050, 0x3052, 0x3054, keySpace, - 0x306a, 0x306b, 0x306c, 0x306d, 0x306e, 0x3056, 0x3058, 0x305a, 0x305c, 0x305e, keyBS, - 0x306f, 0x3072, 0x3075, 0x3078, 0x307b, 0x3060, 0x3062, 0x3065, 0x3067, 0x3069, keyLT, - 0x307E, 0x307F, 0x3080, 0x3081, 0x3082, 0x3070, 0x3073, 0x3076, 0x3079, 0x307c, keyRT, - 0x3084, keyVoid,0x3086, keyVoid,0x3088, 0x3071, 0x3074, 0x3077, 0x307a, 0x307d, keyVoid, - 0x3089, 0x308a, 0x308b, 0x308c, 0x308d, 0x30fc, 0x3001, 0x3002, 0x300c, 0x300d, keyDone, - -}; - -static const uint16_t KatakanaSet [] = -{ - 0x30A2, 0x30A4, 0x30A6, 0x30A8, 0x30AA, 0x30EF, 0x30F2, 0x30F3, 0x30f4, keyVoid,keyHiragana, - 0x30AB, 0x30AD, 0x30AF, 0x30B1, 0x30B3, 0x30A1, 0x30A3, 0x30A5, 0x30A7, 0x30A9, keyKatakana, - 0x30B5, 0x30B7, 0x30B9, 0x30BB, 0x30BD, 0x30C3, 0x30E3, 0x30E5, 0x30E7, 0x30EE, keyEnglish, - 0x30BF, 0x30C1, 0x30C4, 0x30C6, 0x30C8, 0x30AC, 0x30AE, 0x30B0, 0x30B2, 0x30B4, keySpace, - 0x30CA, 0x30CB, 0x30CC, 0x30CD, 0x30CE, 0x30B6, 0x30B8, 0x30BA, 0x30BC, 0x30BE, keyBS, - 0x30CF, 0x30D2, 0x30D5, 0x30D8, 0x30DB, 0x30C0, 0x30C2, 0x30C5, 0x30C7, 0x30C9, keyLT, - 0x30DE, 0x30DF, 0x30E0, 0x30E1, 0x30E2, 0x30D0, 0x30D3, 0x30D6, 0x30D9, 0x30DC, keyRT, - 0x30E4, keyVoid,0x30E6, keyVoid,0x30E8, 0x30D1, 0x30D4, 0x30D7, 0x30DA, 0x30DD, keyVoid, - 0x30E9, 0x30EA, 0x30EB, 0x30EC, 0x30ED, 0x30fc, 0x3001, 0x3002, 0x300c, 0x300d, keyDone, -}; - -static const uint16_t EnglishSet [] = -{ - 0x41, 0x42, 0x43, 0x44, 0x45, 0x61, 0x62, 0x63, 0x64, 0x65, keyHiragana, - 0x46, 0x47, 0x48, 0x49, 0x4A, 0x66, 0x67, 0x68, 0x69, 0x6A, keyKatakana, - 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, keyEnglish, - 0x50, 0x51, 0x52, 0x53, 0x54, 0x70, 0x71, 0x72, 0x73, 0x74, keySpace, - 0x55, 0x56, 0x57, 0x58, 0x59, 0x75, 0x76, 0x77, 0x78, 0x79, keyBS, - 0x5A, 0x22, 0x27, 0x40, 0x23, 0x7A, 0x28, 0x29, 0x7B, 0x7D, keyLT, - 0x26, 0x5E, 0x24, 0xA5, 0x25, 0x2D, 0x2B, 0x3D, 0x2A, 0x2F, keyRT, - 0x30, 0x31, 0x32, 0x33, 0x34, 0x3F, 0x21, 0x3A, 0x3B, 0x5C, keyVoid, - 0x35, 0x36, 0x37, 0x38, 0x39, 0x3C, 0x3E, 0x2C, 0x2E, 0x5F, keyDone, -}; - -static const uint16_t* japmode [] = -{ - HiraganaSet, - KatakanaSet, - EnglishSet -}; - -// ============================================================================ -// CKeyboard Node -// ============================================================================ - -class CKeyboard : public CNode -{ - DECLARE_NODE(CKeyboard, CNode) -public: - CKeyboard(); - ~CKeyboard(); - - CNodeArray m_keys; - CNode* m_frame; - CNode* m_text; - CNode* m_control; - - char* m_string; - - bool m_shift; - bool m_caps; - int m_mode; - int m_jmode; - - void selectKey(int nRow, int nColumn); - bool validKey(int nRow, int nColumn); - void selectUp(); - void selectDown(); - void selectLeft(); - void selectRight(); - void activate(); - - void Backspace(); - void Delete(); - void CursorLeft(); - void CursorRight(); - void Shift(); - void CycleMode(); - void Insert(const char* szInsert); - - bool GetAsteriskFormattingState() { return g_bAsteriskFormatting; } - void SetAsteriskFormattingState(bool b) { g_bAsteriskFormatting = b; } - - void Advance(float nSeconds); - void Render(); - bool OnSetProperty(const PRD* pprd, const void* pvValue); - - -protected: - char m_rgch [MAX_LENGTH + 1]; - int m_nLength; - int m_nCursor; - - int m_nRow; - int m_nColumn; - - char GetCharFromKey(uint16_t key); - - DECLARE_NODE_PROPS() - DECLARE_NODE_FUNCTIONS(); -}; - -IMPLEMENT_NODE("Keyboard", CKeyboard, CNode) - -START_NODE_PROPS(CKeyboard, CNode) - NODE_PROP(pt_nodearray, CKeyboard, keys) - NODE_PROP(pt_node, CKeyboard, frame) - NODE_PROP(pt_node, CKeyboard, text) - NODE_PROP(pt_node, CKeyboard, control) - NODE_PROP(pt_integer, CKeyboard, mode) - NODE_PROP(pt_integer, CKeyboard, jmode) - NODE_PROP(pt_boolean, CKeyboard, shift) - NODE_PROP(pt_boolean, CKeyboard, caps) - NODE_PROP(pt_string, CKeyboard, string) -END_NODE_PROPS() - -#undef _FND_CLASS -#define _FND_CLASS CKeyboard -START_NODE_FUN(CKeyboard, CNode) - NODE_FUN_VII(selectKey) - NODE_FUN_VV(selectUp) - NODE_FUN_VV(selectDown) - NODE_FUN_VV(selectLeft) - NODE_FUN_VV(selectRight) - NODE_FUN_VV(activate) - NODE_FUN_VV(Backspace) - NODE_FUN_VV(Delete) - NODE_FUN_VV(CursorLeft) - NODE_FUN_VV(CursorRight) - NODE_FUN_VV(Shift) - NODE_FUN_VV(CycleMode) - NODE_FUN_VS(Insert) - NODE_FUN_IV(GetAsteriskFormattingState) - NODE_FUN_VI(SetAsteriskFormattingState) -END_NODE_FUN() - -CKeyboard::CKeyboard() : - m_control(NULL), - m_frame(NULL), - m_text(NULL), - m_mode(modeAlpha), - m_jmode(jmodeHiragana), - m_shift(false), - m_caps(false), - m_string(NULL) -{ - m_nLength = 0; - m_nCursor = -1; // select all - m_nRow = 0; - m_nColumn = 0; -} - -extern CKeyboard* g_pActiveKeyboard; - -CKeyboard::~CKeyboard() -{ - if (g_pActiveKeyboard == this) - g_pActiveKeyboard = NULL; - - if (m_text != NULL) - m_text->Release(); - - if (m_frame != NULL) - m_frame->Release(); - - if (m_control != NULL) - m_control->Release(); - - delete [] m_string; -} - -void CKeyboard::selectKey(int nRow, int nColumn) -{ - int numRows = 4; - if(g_nCurLanguage == 1) // Japanese - numRows = 8; - - if( ( nRow < 0 ) || ( nRow > numRows ) ) - { - m_nRow = 0; - } - else - { - m_nRow = nRow; - } - - if( ( nColumn < 0 ) || ( nColumn > 10 ) ) - { - m_nColumn = 0; - } - else - { - m_nColumn = nColumn; - } -} - -bool CKeyboard::validKey(int nRow, int nColumn) -{ - const uint16_t* rgkey; - if(g_nCurLanguage == 1) // Japanese - rgkey = japmode[m_jmode]; - else - rgkey = rgmode[m_mode]; - - uint16_t key = rgkey[nRow * 11 + nColumn]; - - if(key == keyVoid) // missing key - return false; - - return true; -} - - -void CKeyboard::selectUp() -{ -retryUp: - int numRows = 4; - if(g_nCurLanguage == 1) // Japanese - numRows = 8; - - if (m_nRow == 0) - { - m_nRow = numRows; - if(validKey(m_nRow, m_nColumn)) - return; - else - goto retryUp; - } - - m_nRow -= 1; - if(!validKey(m_nRow, m_nColumn)) - goto retryUp; -} - -void CKeyboard::selectDown() -{ -retryDown: - int numRows = 4; - if(g_nCurLanguage == 1) // Japanese - numRows = 8; - - if (m_nRow == numRows) - { - m_nRow = 0; - if(validKey(m_nRow, m_nColumn)) - return; - else - goto retryDown; - } - - m_nRow += 1; - if(!validKey(m_nRow, m_nColumn)) - goto retryDown; -} - -void CKeyboard::selectLeft() -{ -retryLeft: - if (m_nColumn == 0) - { - m_nColumn = 10; - if(validKey(m_nRow, m_nColumn)) - return; - else - goto retryLeft; - } - - if(g_nCurLanguage == 1) // Japanese - { - m_nColumn -= 1; - if(!validKey(m_nRow, m_nColumn)) - goto retryLeft; - } - else - { - if (m_nRow == 3) - { - if (m_nColumn > 6) - m_nColumn = 6; - else - m_nColumn -= 1; - } - else if (m_nRow == 4) - { - if (m_nColumn > 8) - m_nColumn = 8; - else if (m_nColumn > 6) - m_nColumn = 6; - else - m_nColumn = 0; - } - else - { - m_nColumn -= 1; - } - } -} - -void CKeyboard::selectRight() -{ -retryRight: - if (m_nColumn == 10) - { - m_nColumn = 0; - if(validKey(m_nRow, m_nColumn)) - return; - else - goto retryRight; - } - - if(g_nCurLanguage == 1) // Japanese - { - m_nColumn += 1; - if(!validKey(m_nRow, m_nColumn)) - goto retryRight; - } - else - { - if (m_nRow == 3) - { - if (m_nColumn >= 7) - { - m_nColumn = 0; - return; - } - - m_nColumn += 1; - } - else if (m_nRow == 4) - { - if (m_nColumn >= 9) - { - m_nColumn = 0; - return; - } - - if (m_nColumn == 0) - m_nColumn = 1; - else if (m_nColumn < 7) - m_nColumn = 7; - else if (m_nColumn < 9) - m_nColumn = 9; - } - else - { - m_nColumn += 1; - } - } -} - -void CKeyboard::activate() -{ - const uint16_t* rgkey; - if(g_nCurLanguage == 1) // Japanese - rgkey = japmode[m_jmode]; - else - rgkey = rgmode[m_mode]; - - uint16_t key = rgkey[m_nRow * 11 + m_nColumn]; - - switch (key) - { - case keyDone: - { - { - int i; - for (i = 0; i < m_nLength; i += 1) - { - if (m_rgch[i] != ' ') - break; - } - - if (i == m_nLength) - { - CallFunction(this, "OnError"); - break; - } - } - - // Trim the Right Spaces from the String - char* prgcUserString = m_rgch + ( m_nLength - 1 ); - - while( *prgcUserString == ' ' ) - { - *prgcUserString = '\0'; - prgcUserString--; - } - - // Trim the Left Spaces from the String - prgcUserString = m_rgch; - while( *prgcUserString == ' ' ) - prgcUserString++; - - // Copy good characters of the string - if( prgcUserString != m_rgch ) - MoveMemory( m_rgch, prgcUserString, sizeof(char) * (strlen(prgcUserString) + 1) ); - - delete [] m_string; - m_string = new char [m_nLength + 1]; - memcpy(m_string, m_rgch, m_nLength); - m_string[m_nLength] = 0; - - CallFunction(this, "OnDone"); - } - break; - - case keyShift: - Shift(); - break; - - case keyCaps: - if (m_mode != modeAlpha && m_mode != modeAccent) - CallFunction(this, "OnError"); - else - m_caps = !m_caps; - break; - - case keyAlpha: - m_mode = modeAlpha; - break; - - case keySym: - m_mode = modeSymbol; - break; - - case keyAcc: - m_mode = modeAccent; - break; - - case keyHiragana: - m_jmode = jmodeHiragana; - break; - - case keyKatakana: - m_jmode = jmodeKatakana; - break; - - case keyEnglish: - m_jmode = jmodeEnglish; - break; - - case keyLT: - CursorLeft(); - break; - - case keyRT: - CursorRight(); - break; - - case keyBS: - Backspace(); - break; - - case keySpace: - if (m_nCursor == -1) - { - m_nLength = 0; - m_nCursor = 0; - } - - if (m_nLength < MAX_LENGTH) - { - MoveMemory(&m_rgch[m_nCursor + 1], &m_rgch[m_nCursor], (m_nLength - m_nCursor) * sizeof (char)); - m_rgch[m_nCursor] = ' '; - m_nCursor += 1; - m_nLength += 1; - //m_shift = false; - } - break; - - default: - if (m_nCursor == -1) - { - m_nLength = 0; - m_nCursor = 0; - } - - if (m_nLength < MAX_LENGTH) - { - MoveMemory(&m_rgch[m_nCursor + 1], &m_rgch[m_nCursor], (m_nLength - m_nCursor) * sizeof (char)); - m_rgch[m_nCursor] = GetCharFromKey(key); - m_nCursor += 1; - m_nLength += 1; - m_shift = false; - } - else - { - // We want to play the error sound if the user has "filled" the string and attempts to add - // another character - CallFunction(this, "OnError"); - } - - break; - } -} - -void CKeyboard::Backspace() -{ - if (m_nCursor == -1) - { - m_nLength = 0; - m_nCursor = 0; - } - - if (m_nCursor > 0) - { - MoveMemory(&m_rgch[m_nCursor - 1], &m_rgch[m_nCursor], (m_nLength - m_nCursor) * sizeof (char)); - m_nLength -= 1; - m_nCursor -= 1; - } - else - { - // We want to play the error sound if the user has no character to backspace - CallFunction(this, "OnError"); - } -} - -void CKeyboard::Delete() -{ - if (m_nCursor == -1) - { - m_nLength = 0; - m_nCursor = 0; - } - - if (m_nCursor < m_nLength) - { - CursorRight(); - Backspace(); - } - else - { - // We want to play the error sound if the user has no character to delete - CallFunction(this, "OnError"); - } -} - -void CKeyboard::CursorLeft() -{ - if (m_nCursor == -1) - m_nCursor = 0; - else if (m_nCursor > 0) - m_nCursor -= 1; - else - CallFunction(this, "OnError"); -} - -void CKeyboard::CursorRight() -{ - if (m_nCursor == -1) - m_nCursor = m_nLength; - else if (m_nCursor < m_nLength) - m_nCursor += 1; - else - CallFunction(this, "OnError"); -} - -void CKeyboard::Shift() -{ - if (m_mode != modeAlpha && m_mode != modeAccent) - CallFunction(this, "OnError"); - else - m_shift = !m_shift; -} - -void CKeyboard::CycleMode() -{ - m_mode += 1; // English Keyboard - m_jmode += 1; // Japanese Keyboard - - // English Keyboard - if (m_mode > 2) - m_mode = 0; - - // Japanese Keyboard - if (m_jmode > 2) - m_jmode = 0; -} - -void CKeyboard::Insert(const char* szInsert) -{ - if (m_nCursor == -1) - { - m_nLength = 0; - m_nCursor = 0; - } - - int cchInsert = strlen(szInsert); - - if (m_nLength + cchInsert > MAX_LENGTH) - { - CallFunction(this, "OnError"); - return; - } - - MoveMemory(&m_rgch[m_nCursor + cchInsert], &m_rgch[m_nCursor], (m_nLength - m_nCursor) * sizeof (char)); - memcpy(&m_rgch[m_nCursor], szInsert, cchInsert); - m_nCursor += cchInsert; - m_nLength += cchInsert; - m_shift = false; -} - -char CKeyboard::GetCharFromKey(uint16_t key) -{ - ASSERT(key >= 32); - - char ch = (char)key; - - if (m_caps ^ m_shift) - ch = (char)toupper((unsigned char)ch); - - return ch; -} - -bool CKeyboard::OnSetProperty(const PRD* pprd, const void* pvValue) -{ - if ((intptr_t)pprd->pbOffset == MEMBER_OFFSET(m_string)) - { - char* szNew = *(char**)pvValue; - if (szNew == NULL) - szNew = ""; - int cchNew = strlen(szNew); - if (cchNew > MAX_LENGTH) - cchNew = MAX_LENGTH; - memcpy(m_rgch, szNew, cchNew); - m_nLength = cchNew; - m_nCursor = -1; - } - - return true; -} - -CKeyboard* g_pActiveKeyboard = NULL; -void* g_pActiveKeyboard_void() { return (void*)g_pActiveKeyboard; } -void Keyboard_ClearActive() { g_pActiveKeyboard = NULL; } - -// SDL bridge: sdl_main.cpp routes physical keyboard events here without -// needing to include the full CKeyboard class header. -void Keyboard_InsertText(CKeyboard* pKb, const char* sz) -{ - if (pKb && sz) - pKb->Insert(sz); -} - -// SDL keysym values (matches SDL2 SDL_keycode.h - inlined to avoid include). -void Keyboard_HandleKey(CKeyboard* pKb, int sdlKey) -{ - if (!pKb) return; - switch (sdlKey) { - case 0x08: pKb->Backspace(); break; // SDLK_BACKSPACE - case 0x7F: pKb->Delete(); break; // SDLK_DELETE - case 0x40000050: pKb->CursorLeft(); break; // SDLK_LEFT - case 0x4000004F: pKb->CursorRight(); break; // SDLK_RIGHT - case 0x0D: // SDLK_RETURN - case 0x40000058: // SDLK_KP_ENTER - pKb->selectKey(0, 0); - pKb->activate(); - break; - } -} - -void CKeyboard::Render() -{ - g_pActiveKeyboard = this; - - const uint16_t* rgkey; - if(g_nCurLanguage == 1) // Japanese - rgkey = japmode[m_jmode]; - else - rgkey = rgmode[m_mode]; - - if (m_frame != NULL) - m_frame->Render(); - - if (m_text != NULL) - { - m_rgch[m_nLength] = 0; - g_szText = m_rgch; - g_nTextChar = m_nCursor; - m_text->Render(); - g_nTextChar = -1; - } - - int keyLength = 0; - if(g_nCurLanguage == 1) // Japanese - { - if (m_keys.GetLength() != 99) - { - TRACE("\001Japanese keyboard does not have 99 keys!\n"); - return; - } - keyLength = 99; - } - else - { - if (m_keys.GetLength() != 45) - { - TRACE("\001Keyboard does not have 45 keys!\n"); - return; - } - keyLength = 55; - } - - char szBuf [2]; - int nNode = 0; - for (int i = 0; i < keyLength; i += 1) - { - if(g_nCurLanguage != 1) // Not Japanese - { - if (i > 0 && rgkey[i] == rgkey[i - 1]) - continue; - } - - CNode* pNode = m_keys.GetNode(nNode); - int nKey = m_nRow * 11 + m_nColumn; - - if(g_nCurLanguage != 1) // Not Japanese - { - if (nKey > 40 && nKey <= 43) - nKey = 40; - else if (nKey > 45 && nKey <= 50) - nKey = 45; - else if (nKey == 52) - nKey = 51; - else if (nKey == 54) - nKey = 53; - } - - switch (rgkey[i]) - { - case keyShift: - g_szText = "SHIFT"; - break; - - case keyCaps: - g_szText = "CAPS LOCK"; - break; - - case keyAlpha: - g_szText = "ALPHABET"; - break; - - case keySym: - g_szText = "SYMBOLS"; - break; - - case keyAcc: - g_szText = "ACCENTS"; - break; - - case keyEnglish: - g_szText = "ENGLISH"; - break; - - case keyHiragana: - g_szText = "HIRAGANA"; - break; - - case keyKatakana: - g_szText = "KATAKANA"; - break; - - case keyLT: - g_szText = "<"; - break; - - case keyRT: - g_szText = ">"; - break; - - case keyBS: - g_szText = "BACKSPACE"; - break; - - case keyDone: - g_szText = "DONE"; - break; - - case keySpace: - g_szText = "SPACE"; - break; - - case keyVoid: - g_szText = " "; - break; - - default: - szBuf[0] = (char)rgkey[i]; - szBuf[1] = 0; - - if(g_nCurLanguage != 1) // Not Japanese - { - if (m_caps ^ m_shift) - szBuf[0] = (char)toupper((unsigned char)szBuf[0]); - } - - g_szText = szBuf; - break; - } - - g_bActiveKey = (i == nKey); - if(rgkey[i] != keyVoid) // key is visible - pNode->Render(); - - nNode += 1; - } -} - -void CKeyboard::Advance(float nSeconds) -{ - CNode::Advance(nSeconds); - - if (m_control != NULL) - m_control->Advance(nSeconds); -} diff --git a/theseus/shared/keyboard_node.cpp b/theseus/shared/keyboard_node.cpp index a6598be..53def23 100755 --- a/theseus/shared/keyboard_node.cpp +++ b/theseus/shared/keyboard_node.cpp @@ -20,6 +20,12 @@ extern bool g_bActiveKey; extern bool g_bAsteriskFormatting; extern int g_nCurLanguage; +#ifndef _XBOX +// Desktop: the keyboard drawn last takes the physical keyboard (sdl_main.cpp). +class CKeyboard; +CKeyboard* g_pActiveKeyboard = NULL; +#endif + // ===== Layouts ============================================================= // Six 16 bit key tables at 0x000211b8..0x00021498, reached through the @@ -276,6 +282,11 @@ CKeyboard::CKeyboard() // if (control) control->Release(); free(string); keys.ReleaseAll(); CKeyboard::~CKeyboard() { +#ifndef _XBOX + if (g_pActiveKeyboard == this) + g_pActiveKeyboard = NULL; +#endif + CNode* owned[] = { m_text, m_frame, m_control }; for (int i = 0; i < 3; i++) { @@ -708,6 +719,9 @@ static const TCHAR* FunctionKeyLabel(WORD key) // g_bActiveKey says whether it is highlighted, and blanks are skipped. void CKeyboard::Render() { +#ifndef _XBOX + g_pActiveKeyboard = this; +#endif const WORD* grid = Grid(); if (m_frame != NULL) @@ -760,3 +774,37 @@ void CKeyboard::Render() pKey->Render(); } } + + +#ifndef _XBOX +// ===== Desktop physical keyboard (Theseus) ================================= +// sdl_main.cpp and joystick.cpp reach the active keyboard through these +// without seeing the class. + +void* g_pActiveKeyboard_void() { return (void*)g_pActiveKeyboard; } +void Keyboard_ClearActive() { g_pActiveKeyboard = NULL; } + +// Typed text goes in like a script Insert. +void Keyboard_InsertText(CKeyboard* pKb, const char* sz) +{ + if (pKb && sz) + pKb->Insert(sz); +} + +// Editing keys by SDL keycode (values inlined so this file needs no SDL). +void Keyboard_HandleKey(CKeyboard* pKb, int sdlKey) +{ + if (!pKb) return; + switch (sdlKey) { + case 0x08: pKb->Backspace(); break; // SDLK_BACKSPACE + case 0x7F: pKb->Delete(); break; // SDLK_DELETE + case 0x40000050: pKb->CursorLeft(); break; // SDLK_LEFT + case 0x4000004F: pKb->CursorRight(); break; // SDLK_RIGHT + case 0x0D: // SDLK_RETURN + case 0x40000058: // SDLK_KP_ENTER + pKb->selectKey(0, 0); + pKb->activate(); + break; + } +} +#endif From c19a3c1b35c04164f08357bfdc7ae82400688392 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:08:43 -0500 Subject: [PATCH 091/167] Shifted accent keys type capitals like 5960 5960 runs shifted keys through CharUpperW, so with shift or caps on the accent page types and shows capital accented letters. Theseus only changed a to z. Now the accents move up too, and y diaeresis becomes its capital on the Xbox. --- theseus/shared/keyboard_node.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/theseus/shared/keyboard_node.cpp b/theseus/shared/keyboard_node.cpp index 53def23..c086bca 100755 --- a/theseus/shared/keyboard_node.cpp +++ b/theseus/shared/keyboard_node.cpp @@ -153,11 +153,18 @@ static inline const WORD* GridFor(int mode, int jmode) return JapaneseGrid() ? s_layouts[1][jmode] : s_layouts[0][mode]; } -// 5960 calls CharUpperW for shifted keys; Theseus only maps a to z. +// 5960 runs shifted keys through CharUpperW. The only lowercase the grids +// hold is a to z and the Latin-1 accents, so this covers every key it can +// see: those move up 0x20 (except the division sign) and y diaeresis becomes +// U+0178. Kana, symbols and sharp s stay put. Desktop keys are single bytes, +// so there y diaeresis stays lowercase. static TCHAR KeyUpper(TCHAR ch) { - if (ch >= 'a' && ch <= 'z') - return (TCHAR)(ch - ('a' - 'A')); + unsigned c = (sizeof(TCHAR) == 1) ? (unsigned)(unsigned char)ch : (unsigned)ch; + if ((c >= 'a' && c <= 'z') || (c >= 0xe0 && c <= 0xfe && c != 0xf7)) + return (TCHAR)(c - 0x20); + if (c == 0xff && sizeof(TCHAR) > 1) + return (TCHAR)0x178; return ch; } From 4d5e2c21dced626db5142f4f8d61f295ef847da1 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:09:49 -0500 Subject: [PATCH 092/167] Desktop DVD player tells the script when the title, chapter or picture changes 5960's DVD player fires OnDomainChange, OnTitleChange, OnChapterChange, OnAngleChange and OnVideoSizeChange whenever those move, in a fixed order around OnTimeChange. Desktop only fired the time, audio and subtitle ones, so the OSD didn't refresh on a chapter skip. Now it fires the rest from what mpv reports. Xbox has no DVD player yet, so nothing changes there. --- theseus/desktop/desktop_nodes.cpp | 52 ++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/theseus/desktop/desktop_nodes.cpp b/theseus/desktop/desktop_nodes.cpp index 6394956..5fd47c7 100644 --- a/theseus/desktop/desktop_nodes.cpp +++ b/theseus/desktop/desktop_nodes.cpp @@ -2839,7 +2839,9 @@ class CDVDPlayer : public CNode m_scanSlow(false), m_autoStop(false), m_bScanBackward(false), m_backScanAccum(0.0f), m_digitAccum(0), m_digitTimer(0.0f), m_zoomScale(1.0f), m_zoomX(0.0f), m_zoomY(0.0f), - m_osdTimer(0.0f), m_lastOsdSeconds(-1) {} + m_osdTimer(0.0f), m_lastOsdSeconds(-1), + m_navDomain(0), m_navTitle(0), m_navChapter(0), m_navAngle(0), m_navAngleCount(0), + m_navVideoWidth(0), m_navVideoHeight(0) {} DECLARE_NODE(CDVDPlayer, CNode) DECLARE_NODE_PROPS() @@ -2865,6 +2867,49 @@ class CDVDPlayer : public CNode float m_osdTimer; // throttle OnTimeChange int m_lastOsdSeconds; // detect second change + // What the script was last told, so a change fires once. + int m_navDomain, m_navTitle, m_navChapter, m_navAngle, m_navAngleCount; + int m_navVideoWidth, m_navVideoHeight; + + // CDVDPlayer::Advance, 5960 @ 0x00065f10 (the part past the navigator + // calls, 0x00066029..0x0006639b), polls the navigator every frame and + // fires an event for each value that moved, in this order: + // domain -> OnDomainChange, title -> OnTitleChange, + // chapter -> OnChapterChange, h:m:s -> OnTimeChange, + // angle or angle count -> OnAngleChange, audio -> OnAudioChange, + // subtitle -> OnSubTitleChange, source video size -> OnVideoSizeChange + // Desktop already fires the time, audio and subtitle ones; these cover + // the rest from what mpv reports. + void ReportNavigationChanges() { + if (m_domain != m_navDomain) { + m_navDomain = m_domain; + CallFunction(this, "OnDomainChange"); + } + if (m_title != m_navTitle) { + m_navTitle = m_title; + CallFunction(this, "OnTitleChange"); + } + if (m_chapter != m_navChapter) { + m_navChapter = m_chapter; + CallFunction(this, "OnChapterChange"); + } + } + + void ReportAngleAndVideoSize() { + if (m_angle != m_navAngle || m_angleCount != m_navAngleCount) { + m_navAngle = m_angle; + m_navAngleCount = m_angleCount; + CallFunction(this, "OnAngleChange"); + } + int w = 0, h = 0; + MediaPlayer_GetVideoTexture(&w, &h); + if (w != m_navVideoWidth || h != m_navVideoHeight) { + m_navVideoWidth = w; + m_navVideoHeight = h; + CallFunction(this, "OnVideoSizeChange"); + } + } + // Update time + track properties from media player void UpdateFromMediaPlayer() { double pos = MediaPlayer_GetPosition(); @@ -2941,12 +2986,16 @@ class CDVDPlayer : public CNode // Ensure we stay visible while playing if (!m_visible) m_visible = true; + ReportNavigationChanges(); + // Fire time change callback only when the displayed second changes int curSec = m_hours * 3600 + m_minutes * 60 + m_seconds; if (curSec != m_lastOsdSeconds) { m_lastOsdSeconds = curSec; CallFunction(this, "OnTimeChange"); } + + ReportAngleAndVideoSize(); } else if (mpState == MP_STOPPED && m_playbackMode != 0) { m_playbackMode = 0; m_scanSpeed = 1; @@ -2955,6 +3004,7 @@ class CDVDPlayer : public CNode m_title = 0; m_domain = 0; CallFunction(this, "OnPlaybackModeChange"); + ReportNavigationChanges(); } // Sync A-B repeat state From 01f2f50a6df861f092d1166fc8a67a1dbec76d41 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:11:23 -0500 Subject: [PATCH 093/167] Added the keyboard's TIDactivate and its three props from 5960 The shipped keyboard XIPs call TIDactivate from OnTIDKeyDown and read TIDKeyUsed, PlaySound and CloseKeyboard, none of which Theseus had. Wrote them from 0x5b040 and 0x5b890. Nothing fills the keystroke words yet, so it only does something once an input poll feeds it. --- docs/decomp/README.md | 4 +- theseus/shared/keyboard_node.cpp | 158 +++++++++++++++++++++++++++---- 2 files changed, 144 insertions(+), 18 deletions(-) diff --git a/docs/decomp/README.md b/docs/decomp/README.md index 2661ff8..e518b7c 100644 --- a/docs/decomp/README.md +++ b/docs/decomp/README.md @@ -227,6 +227,7 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x0005adb0 | `CKeyboard_selectDown` | `CKeyboard::selectDown` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Highlight down one row, wrapping to the top and skipping blank cells. | | 0x0005ae60 | `CKeyboard_selectLeft` | `CKeyboard::selectLeft` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Highlight left, wrapping, skipping blanks; on western grids hops across the wide bottom keys. | | 0x0005af50 | `CKeyboard_selectRight` | `CKeyboard::selectRight` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Highlight right, the mirror of selectLeft. | +| 0x0005b040 | `CKeyboard_TIDactivate` | `CKeyboard::TIDactivate` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Turns the last USB keyboard keystroke into a key press and reports it through TIDKeyUsed / PlaySound / CloseKeyboard. | | 0x0005b340 | `CKeyboard_activate` | `CKeyboard::activate` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Presses the highlighted key: page and case keys, cursor keys, BACKSPACE, SPACE, DONE (trim, copy to `string`, OnDone) or a character insert. | | 0x0005b630 | `CKeyboard_Backspace` | `CKeyboard::Backspace` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Removes the character left of the cursor, OnError at the start. | | 0x0005b6a0 | `CKeyboard_Delete` | `CKeyboard::Delete` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Removes the character under the cursor, OnError at the end. | @@ -235,6 +236,7 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x0005b780 | `CKeyboard_Shift` | `CKeyboard::Shift` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Toggles shift on the letter and accent pages, OnError on symbols. | | 0x0005b7b0 | `CKeyboard_CycleMode` | `CKeyboard::CycleMode` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Steps mode and jmode together through 0, 1, 2. | | 0x0005b7e0 | `CKeyboard_Insert` | `CKeyboard::Insert` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Inserts a string at the cursor, OnError if it would pass 31 characters. | +| 0x0005b890 | `FUN_0005b890` | `IsGridChar` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Whether a typed character appears (as is or uppercased) on any page of the current grid. Used by TIDactivate. | | 0x0005b970 | `CKeyboard_OnSetProperty` | `CKeyboard::OnSetProperty` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Setting `string` loads the edit buffer (up to 31 characters) with the whole thing selected. | | 0x0005b9d0 | `CKeyboard_Render` | `CKeyboard::Render` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Draws the frame, the text node with the edit string and cursor, then one node per key with its label. | | 0x0005bca0 | `CKeyboard_Advance` | `CKeyboard::Advance` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Advances the node, then the control node. | @@ -294,7 +296,7 @@ The seed and first expansion cover the application shell, script VM, scene graph | File Operations | [FileOps.md](FileOps.md) | 7 functions | | Disc | (no doc yet, narrative TODO) | 1 function | | HUD & Picture | [HudNode.md](HudNode.md) | 9 functions | -| Keyboard | [Keyboard.md](Keyboard.md) | 20 functions | +| Keyboard | [Keyboard.md](Keyboard.md) | 22 functions | | Locale | (no doc yet, narrative TODO) | 1 function | | NtIoSvc | [NtIoSvc.md](NtIoSvc.md) | 1 function | | Scene Groups | [SceneGroups.md](SceneGroups.md) | 6 functions | diff --git a/theseus/shared/keyboard_node.cpp b/theseus/shared/keyboard_node.cpp index c086bca..34ad303 100755 --- a/theseus/shared/keyboard_node.cpp +++ b/theseus/shared/keyboard_node.cpp @@ -153,27 +153,70 @@ static inline const WORD* GridFor(int mode, int jmode) return JapaneseGrid() ? s_layouts[1][jmode] : s_layouts[0][mode]; } -// 5960 runs shifted keys through CharUpperW. The only lowercase the grids -// hold is a to z and the Latin-1 accents, so this covers every key it can -// see: those move up 0x20 (except the division sign) and y diaeresis becomes -// U+0178. Kana, symbols and sharp s stay put. Desktop keys are single bytes, -// so there y diaeresis stays lowercase. -static TCHAR KeyUpper(TCHAR ch) +static WORD UpperKey(WORD c); + +// Which of the current language's three pages holds ch, as is or as the +// capital of a cell; -1 if none. +static int GridPageOf(WORD ch) +{ + bool japanese = JapaneseGrid(); + int cells = japanese ? JAPANESE_KEYS : WESTERN_KEYS; + for (int page = 0; page < 3; page++) + { + const WORD* grid = s_layouts[japanese ? 1 : 0][page]; + for (int i = 0; i < cells; i++) + { + if (grid[i] == ch || UpperKey(grid[i]) == ch) + return page; + } + } + return -1; +} + +// 5960 runs keys through CharUpperW. The only lowercase the grids hold is +// a to z and the Latin-1 accents, so this covers every key it can see: those +// move up 0x20 (except the division sign) and y diaeresis becomes U+0178. +// Kana, symbols and sharp s stay put. +static WORD UpperKey(WORD c) { - unsigned c = (sizeof(TCHAR) == 1) ? (unsigned)(unsigned char)ch : (unsigned)ch; if ((c >= 'a' && c <= 'z') || (c >= 0xe0 && c <= 0xfe && c != 0xf7)) - return (TCHAR)(c - 0x20); - if (c == 0xff && sizeof(TCHAR) > 1) - return (TCHAR)0x178; - return ch; + return (WORD)(c - 0x20); + if (c == 0xff) + return 0x178; + return c; +} + +// Desktop keys are single bytes, so there y diaeresis stays lowercase. +static TCHAR KeyUpper(TCHAR ch) +{ + if (sizeof(TCHAR) > 1) + return (TCHAR)UpperKey((WORD)ch); + WORD up = UpperKey((unsigned char)ch); + return (up > 0xff) ? ch : (TCHAR)up; +} + +// The last USB keyboard keystroke: a HID key code and the character it typed +// (0x0017f460 / 0x0017f462 in 5960, filled by the input poll). Theseus's input +// code doesn't fill them, so TIDactivate sees no key unless something does. +WORD g_wTIDKeyCode = 0; +WORD g_wTIDKeyChar = 0; + +// FUN_0005b890: true when ch, or its capital, sits on any page of the current +// language's grid. Function key codes never count. +static bool IsGridChar(WORD ch) +{ + if (ch >= kcShift && ch <= kcBlank) + return false; + return GridPageOf(ch) >= 0; } // ===== CKeyboard =========================================================== // 0x9c bytes in 5960: the nodearray at +0x1c, frame/text/control +0x28..+0x30, -// string +0x34, shift +0x38, caps +0x39, mode +0x40, jmode +0x44, the edit -// buffer +0x48 (32 WCHARs), its length +0x88, cursor +0x8c, row +0x90 and -// column +0x94. +// string +0x34, shift +0x38, caps +0x39, TIDKeyUsed +0x3a, PlaySound +0x3b, +// CloseKeyboard +0x3c, mode +0x40, jmode +0x44, the edit buffer +0x48 +// (32 WCHARs), its length +0x88, cursor +0x8c, row +0x90, column +0x94 and +// the TIDactivate key +0x98. class CKeyboard : public CNode { @@ -189,6 +232,9 @@ class CKeyboard : public CNode TCHAR* m_string; bool m_shift; bool m_caps; + bool m_TIDKeyUsed; + bool m_PlaySound; + bool m_CloseKeyboard; int m_mode; int m_jmode; @@ -198,6 +244,7 @@ class CKeyboard : public CNode void selectLeft(); void selectRight(); void activate(); + void TIDactivate(); void Backspace(); void Delete(); void CursorLeft(); @@ -218,6 +265,7 @@ class CKeyboard : public CNode int m_cursor; // -1 means the whole string is selected int m_row; int m_col; + WORD m_typedKey; // stands in for the highlighted key during TIDactivate const WORD* Grid() const { return GridFor(m_mode, m_jmode); } WORD KeyAt(int row, int col) const { return Grid()[row * GRID_COLUMNS + col]; } @@ -240,6 +288,9 @@ START_NODE_PROPS(CKeyboard, CNode) NODE_PROP(pt_integer, CKeyboard, jmode) NODE_PROP(pt_boolean, CKeyboard, shift) NODE_PROP(pt_boolean, CKeyboard, caps) + NODE_PROP(pt_boolean, CKeyboard, TIDKeyUsed) + NODE_PROP(pt_boolean, CKeyboard, PlaySound) + NODE_PROP(pt_boolean, CKeyboard, CloseKeyboard) NODE_PROP(pt_string, CKeyboard, string) END_NODE_PROPS() @@ -252,6 +303,7 @@ START_NODE_FUN(CKeyboard, CNode) NODE_FUN_VV(selectLeft) NODE_FUN_VV(selectRight) NODE_FUN_VV(activate) + NODE_FUN_VV(TIDactivate) NODE_FUN_VV(Backspace) NODE_FUN_VV(Delete) NODE_FUN_VV(CursorLeft) @@ -275,12 +327,16 @@ CKeyboard::CKeyboard() m_string = NULL; m_shift = false; m_caps = false; + m_TIDKeyUsed = false; + m_PlaySound = false; + m_CloseKeyboard = false; m_mode = 0; m_jmode = 0; m_editLen = 0; m_cursor = -1; m_row = 0; m_col = 0; + m_typedKey = 0; } // CKeyboard::~CKeyboard() @@ -439,8 +495,8 @@ void CKeyboard::selectRight() // ===== Pressing a key ====================================================== // CKeyboard::activate() [script fn 'activate', FND 0x001882c8, sig 1] -// Reconstructed from 5960 retail XBE @ 0x0005b340. Looks up the highlighted -// key and acts on it: function keys switch case or page, move the cursor or +// Reconstructed from 5960 retail XBE @ 0x0005b340. Takes the key TIDactivate +// typed, or else the highlighted one, and acts on it: function keys switch case or page, move the cursor or // erase; DONE trims the string, copies it to `string` and calls OnDone (an // empty or all blank string calls OnError instead); SPACE inserts quietly // unless full; anything else inserts the character (uppercased when shift @@ -448,7 +504,7 @@ void CKeyboard::selectRight() // See docs/decomp/Keyboard.md. void CKeyboard::activate() { - WORD key = KeyAt(m_row, m_col); + WORD key = m_typedKey != 0 ? m_typedKey : KeyAt(m_row, m_col); switch (key) { @@ -519,6 +575,74 @@ void CKeyboard::activate() CallFunction(this, _T("OnError")); } +// CKeyboard::TIDactivate() [script fn 'TIDactivate', FND 0x001882e0, sig 1] +// Reconstructed from 5960 retail XBE @ 0x0005b040. Presses the key matching +// the last USB keystroke: the arrows move the cursor, backspace, enter (DONE) +// and space press their keys, escape sets CloseKeyboard, and a character on +// the grid is typed and flips jmode to its page (on western grids too). The +// Japanese input mode keys only clear PlaySound. TIDKeyUsed says whether a +// key was pressed. See docs/decomp/Keyboard.md. +void CKeyboard::TIDactivate() +{ + m_CloseKeyboard = false; + m_PlaySound = true; + m_TIDKeyUsed = true; + m_typedKey = 0; + + switch (g_wTIDKeyCode) + { + case 0x35: // grave / hankaku-zenkaku + case 0x88: // katakana-hiragana + case 0x90: // LANG1 + m_PlaySound = false; + break; + case 0x4f: // right arrow + m_typedKey = kcRight; + break; + case 0x50: // left arrow + m_typedKey = kcLeft; + break; + } + + if (m_typedKey == 0) + { + WORD ch = g_wTIDKeyChar; + switch (ch) + { + case 0: + break; + case 8: + m_typedKey = kcBackspace; + break; + case '\n': + m_typedKey = kcDone; + break; + case 0x1b: + m_CloseKeyboard = true; + break; + case ' ': + m_typedKey = kcSpace; + break; + default: + if (IsGridChar(ch)) + { + m_typedKey = ch; + int page = GridPageOf(ch); + if (page >= 0) + m_jmode = page; + } + break; + } + } + + m_TIDKeyUsed = (m_typedKey != 0); + if (m_typedKey != 0) + { + activate(); + m_typedKey = 0; + } +} + // ===== Editing ============================================================= From 3168cf0e8eb447e00b4e00f819a501855ec1542e Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:12:03 -0500 Subject: [PATCH 094/167] Desktop DVD player answers each button with OnUserOpAllowed or OnProhibited 5960 ends every player op on the navigator's answer: OnUserOpAllowed clears the status line, OnProhibited shows "Prohibited". Desktop never said either. Now the transport, scan, chapter, track and A-B ops report allowed, and the disc menu, select and angle ops report prohibited since a file has none. That means A or the d-pad during playback now flashes "Prohibited" like a retail Xbox does. --- theseus/desktop/desktop_nodes.cpp | 51 ++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/theseus/desktop/desktop_nodes.cpp b/theseus/desktop/desktop_nodes.cpp index 5fd47c7..eba0dde 100644 --- a/theseus/desktop/desktop_nodes.cpp +++ b/theseus/desktop/desktop_nodes.cpp @@ -3020,6 +3020,16 @@ class CDVDPlayer : public CNode MediaPlayer_RenderToScreen(0, 0); } + // 5960 ends each user op on the navigator's answer (helper @ 0x000669f0 + // and its inlined copies through 0x00067f00): + // hr == 0x87840001 (UOP prohibited) or 0x80034013 -> OnProhibited + // anything else -> OnUserOpAllowed + // mpv takes every transport op; the disc menu, select and angle ops have + // nothing to act on in a file, so they report prohibited. + void ReportUserOp(bool bAllowed) { + CallFunction(this, bAllowed ? "OnUserOpAllowed" : "OnProhibited"); + } + // === Transport === void init() {} @@ -3033,6 +3043,7 @@ class CDVDPlayer : public CNode } MediaPlayer_Play(); m_playbackMode = 3; // DPM_PLAYING + ReportUserOp(true); } void stop() { @@ -3048,7 +3059,7 @@ class CDVDPlayer : public CNode MediaPlayer_Stop(); } - void pause() { MediaPlayer_Pause(); } + void pause() { MediaPlayer_Pause(); ReportUserOp(true); } void playOrPause() { // If scanning, stop scan and resume normal playback if (m_playbackMode == 5 || m_playbackMode == 8 || m_scanSpeed > 1 || m_bScanBackward) { @@ -3059,11 +3070,13 @@ class CDVDPlayer : public CNode MediaPlayer_SetSpeed(1.0); MediaPlayer_Play(); m_playbackMode = 3; // DPM_PLAYING + ReportUserOp(true); return; } MediaPlayer_TogglePause(); + ReportUserOp(true); } - void resume() { MediaPlayer_Play(); } + void resume() { MediaPlayer_Play(); ReportUserOp(true); } // === Scanning (fast forward/rewind with speed ramp) === void forwardScan() { @@ -3089,6 +3102,7 @@ class CDVDPlayer : public CNode MediaPlayer_SetSpeed((double)m_scanSpeed); } m_playbackMode = 5; // DPM_SCANNING + ReportUserOp(true); } void backwardScan() { @@ -3116,6 +3130,7 @@ class CDVDPlayer : public CNode } m_backScanAccum = 0.0f; m_playbackMode = 5; // DPM_SCANNING + ReportUserOp(true); } void stopScan() { @@ -3126,48 +3141,53 @@ class CDVDPlayer : public CNode MediaPlayer_SetSpeed(1.0); MediaPlayer_Play(); m_playbackMode = 3; // DPM_PLAYING + ReportUserOp(true); } // === Frame stepping === void frameAdvance() { MediaPlayer_FrameStep(); m_playbackMode = 8; // DPM_TRICKPLAY + ReportUserOp(true); } void frameReverse() { MediaPlayer_FrameBackStep(); m_playbackMode = 8; // DPM_TRICKPLAY + ReportUserOp(true); } // === Chapter navigation === - void startChapter() { MediaPlayer_Seek(0); } - void nextChapter() { MediaPlayer_NextChapter(); } - void prevChapter() { MediaPlayer_PrevChapter(); } + void startChapter() { MediaPlayer_Seek(0); ReportUserOp(true); } + void nextChapter() { MediaPlayer_NextChapter(); ReportUserOp(true); } + void prevChapter() { MediaPlayer_PrevChapter(); ReportUserOp(true); } // === Track selection === void nextAudioStream() { MediaPlayer_NextAudioTrack(); UpdateTrackLanguages(); // refresh language strings after track change CallFunction(this, "OnAudioChange"); + ReportUserOp(true); } void nextSubtitle() { MediaPlayer_NextSubtitleTrack(); UpdateTrackLanguages(); // refresh language strings after track change CallFunction(this, "OnSubTitleChange"); + ReportUserOp(true); } - void nextAngle() {} // N/A for file playback + void nextAngle() { ReportUserOp(false); } // N/A for file playback - // === DVD menu navigation (no-ops for file playback) === - void selectUp() {} - void selectDown() {} - void selectRight() {} - void selectLeft() {} - void activate() {} - void goUp() {} - void menu() {} - void titleMenu() {} + // === DVD menu navigation (nothing to navigate in a file) === + void selectUp() { ReportUserOp(false); } + void selectDown() { ReportUserOp(false); } + void selectRight() { ReportUserOp(false); } + void selectLeft() { ReportUserOp(false); } + void activate() { ReportUserOp(false); } + void goUp() { ReportUserOp(false); } + void menu() { ReportUserOp(false); } + void titleMenu() { ReportUserOp(false); } // === Zoom === void setScale(float scale) { @@ -3194,6 +3214,7 @@ class CDVDPlayer : public CNode MediaPlayer_ClearABLoop(); m_abRepeatState = 0; } + ReportUserOp(true); } // === Digit entry (chapter jump) === From 92afbfd07529321cbe047af26847427e0fae3ea5 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:12:39 -0500 Subject: [PATCH 095/167] Corrected the Live passcode rows in the decomp index VerifyPassword compares a 4 byte pad code, not a hash. Added the passcode encoder and the Logon passcode check next to it. --- docs/decomp/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/decomp/README.md b/docs/decomp/README.md index 2e05b57..ecf4fb0 100644 --- a/docs/decomp/README.md +++ b/docs/decomp/README.md @@ -250,8 +250,10 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x00069730 | `CLiveAccounts_GetNumberOfAccounts` | `CLiveAccounts::GetNumberOfAccounts` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Returns the count of accounts loaded by `LoadFromHD`. | | 0x00069750 | `CLiveAccounts_GetAccountName` | `CLiveAccounts::GetAccountName` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Returns gamertag for the Nth loaded account. | | 0x000697f0 | `CLiveAccounts_GetAccountName` | `CLiveAccounts::GetAccountName` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Overload of `GetAccountName`. Differs by signature; both write the same gamertag string. | -| 0x000698b0 | `CLiveAccounts_IsPasswordEnabled` | `CLiveAccounts::IsPasswordEnabled` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Tests whether the bound account requires a passcode at logon. | -| 0x000698e0 | `CLiveAccounts_VerifyPassword` | `CLiveAccounts::VerifyPassword` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Compares an entered passcode against the account's stored hash. | +| 0x000698b0 | `CLiveAccounts_IsPasswordEnabled` | `CLiveAccounts::IsPasswordEnabled` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Returns bit 0 (require passcode) of the account's user options; 0 for an index past the list. | +| 0x000698e0 | `CLiveAccounts_VerifyPassword` | `CLiveAccounts::VerifyPassword` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Encodes the entered text with `XLive_EncodePasscode` and compares the 4 bytes with the account's stored pad code; an index past the list passes. Call signature 29. | +| 0x00069930 | `XLive_EncodePasscode` | `EncodePasscode` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Maps up to 4 characters to pad codes: `u d l r x y L R` to 1 2 3 4 5 6 9 10, others keep their low byte, missing ones are 0. | +| 0x000699e0 | `CLiveAccounts_Logon` | `CLiveAccounts::Logon` (live_accounts.cpp) | [Live Accounts](#live-accounts) | A non-empty passcode that doesn't match the stored pad code fires `OnInvalidPassword`; otherwise starts `XOnlineLogon` and sets `currentAccount`. | | 0x00069ae0 | `CLiveAccounts_Logoff` | `CLiveAccounts::Logoff` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Logs out the currently signed-in Live user and clears in-memory credentials. | | 0x0006a000 | `CLiveAccounts_IsBackFromEntryPoint` | `CLiveAccounts::IsBackFromEntryPoint` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Returns whether the dashboard re-entered the Live flow from a title (post-game return) versus a fresh boot. | | 0x0006a030 | `CLiveAccounts_GetLastLogonUser` | `CLiveAccounts::GetLastLogonUser` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Returns the index of the account that last logged on. Used to default the user selector. | From 3f99a42bb50a01f1b3f5fbe162bb8ad54e0fb948 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:21:02 -0500 Subject: [PATCH 096/167] Fixed draws going out with a stale vertex format on desktop A few overlay and fade draws set the vertex shader and stream straight on the device, so the wrapper cache kept the old value and skipped the next real set. Routed them through the wrappers and made DrawSubset forget the cached shader. --- theseus/desktop/d3d8_sdl.h | 3 +++ theseus/desktop/dashapp.cpp | 4 ++-- theseus/render/hud_node.cpp | 4 ++-- theseus/render/scene_groups.cpp | 8 ++++---- theseus/render/text.cpp | 4 ++-- theseus/render/widget_draw.cpp | 6 +++--- theseus/render/widget_layer.cpp | 2 +- theseus/xbox/main.cpp | 4 ++-- theseus/xbox/overlay.cpp | 6 +++--- theseus/xbox/panic_screen.cpp | 2 +- 10 files changed, 23 insertions(+), 20 deletions(-) diff --git a/theseus/desktop/d3d8_sdl.h b/theseus/desktop/d3d8_sdl.h index 1f41b7a..acae71f 100644 --- a/theseus/desktop/d3d8_sdl.h +++ b/theseus/desktop/d3d8_sdl.h @@ -2456,11 +2456,14 @@ inline HRESULT D3DXCreateMeshFVF(DWORD numFaces, DWORD numVerts, DWORD opts, DWO } // ID3DXMesh::DrawSubset - renders mesh through the device +extern bool theseus_desktop_vs_valid; inline HRESULT ID3DXMesh::DrawSubset(DWORD attr) { (void)attr; if (!m_dev || !m_vbData || !m_ibData || m_numFaces == 0 || m_numVerts == 0) return S_OK; + // Sets the FVF behind TheseusSetVertexShader's back, so its cache can't be trusted after. m_dev->SetVertexShader(m_fvf); + theseus_desktop_vs_valid = false; m_dev->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, m_numVerts, m_numFaces, m_ibData, D3DFMT_INDEX16, m_vbData, m_vertStride); return S_OK; diff --git a/theseus/desktop/dashapp.cpp b/theseus/desktop/dashapp.cpp index 885d47b..52efc1c 100755 --- a/theseus/desktop/dashapp.cpp +++ b/theseus/desktop/dashapp.cpp @@ -603,8 +603,8 @@ void Draw() TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); - VERIFYHR(TheseusGetD3DDev()->SetStreamSource(0, m_pVB, sizeof(COLORVERTEX))); - VERIFYHR(TheseusGetD3DDev()->SetVertexShader(D3DFVF_XYZ | D3DFVF_DIFFUSE)); + TheseusSetStreamSource(0, m_pVB, sizeof(COLORVERTEX)); + TheseusSetVertexShader(D3DFVF_XYZ | D3DFVF_DIFFUSE); VERIFYHR(TheseusGetD3DDev()->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2)); TheseusSetTransform(D3DTS_PROJECTION, &matProjectionSave); diff --git a/theseus/render/hud_node.cpp b/theseus/render/hud_node.cpp index 6279f47..098d8a5 100644 --- a/theseus/render/hud_node.cpp +++ b/theseus/render/hud_node.cpp @@ -329,8 +329,8 @@ void CPicture::Render() m_pVB->Unlock(); LPDIRECT3DDEVICE8 dev = TheseusGetD3DDev(); - dev->SetStreamSource(0, m_pVB, sizeof(PictureVertex)); - dev->SetVertexShader(kPictureFVF); + TheseusSetStreamSource(0, m_pVB, sizeof(PictureVertex)); + TheseusSetVertexShader(kPictureFVF); dev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); } diff --git a/theseus/render/scene_groups.cpp b/theseus/render/scene_groups.cpp index cabdf7f..358eb5f 100755 --- a/theseus/render/scene_groups.cpp +++ b/theseus/render/scene_groups.cpp @@ -1420,8 +1420,8 @@ void CLayer::Render() TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); - TheseusGetD3DDev()->SetStreamSource(0, m_vb, sizeof(WashVertex)); - TheseusGetD3DDev()->SetVertexShader(D3DFVF_XYZ | D3DFVF_DIFFUSE); + TheseusSetStreamSource(0, m_vb, sizeof(WashVertex)); + TheseusSetVertexShader(D3DFVF_XYZ | D3DFVF_DIFFUSE); TheseusGetD3DDev()->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); TheseusSetTransform(D3DTS_PROJECTION, &outerProjection); @@ -1690,8 +1690,8 @@ void CBackground::RenderBackdrop() TheseusSetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 0); TheseusSetRenderState(D3DRS_TEXTUREFACTOR, (DWORD)(int)(m_alpha * 255.0f) << 24); - TheseusGetD3DDev()->SetStreamSource(0, m_vb, sizeof(D3DVERTEX)); - TheseusGetD3DDev()->SetVertexShader(D3DFVF_VERTEX); + TheseusSetStreamSource(0, m_vb, sizeof(D3DVERTEX)); + TheseusSetVertexShader(D3DFVF_VERTEX); TheseusGetD3DDev()->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); TheseusSetRenderState(D3DRS_LIGHTING, lighting); diff --git a/theseus/render/text.cpp b/theseus/render/text.cpp index 99aa633..fa1ad32 100644 --- a/theseus/render/text.cpp +++ b/theseus/render/text.cpp @@ -1063,7 +1063,7 @@ float OverlayFontDraw(const TCHAR* text, float screenX, float screenY, float pix if (nVerts + shape->nPoints > MAX_OVL_VERTS || nIndices + shape->nIndices > MAX_OVL_INDICES) { - dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); + TheseusSetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); Frame_Count(FC_DRAW_TEXT); Frame_PhaseBegin(FP_UP_DRAW); dev->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, nVerts, nIndices / 3, @@ -1093,7 +1093,7 @@ float OverlayFontDraw(const TCHAR* text, float screenX, float screenY, float pix if (nIndices > 0) { Frame_Count(FC_DRAW_TEXT); - dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); + TheseusSetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); Frame_PhaseBegin(FP_UP_DRAW); dev->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, nVerts, nIndices / 3, s_ovlIndices, D3DFMT_INDEX16, s_ovlVerts, sizeof(OverlayTextVert)); diff --git a/theseus/render/widget_draw.cpp b/theseus/render/widget_draw.cpp index b04af3a..ef2fa55 100644 --- a/theseus/render/widget_draw.cpp +++ b/theseus/render/widget_draw.cpp @@ -21,7 +21,7 @@ void DrawSolidRect(int x, int y, int width, int height, D3DCOLOR color) IDirect3DDevice8 *dev = g_pD3DDev; if (!dev) return; Frame_Count(FC_DRAW_SOLID); - dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); + TheseusSetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); Frame_PhaseBegin(FP_UP_DRAW); dev->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, verts, sizeof(Vertex)); Frame_PhaseEnd(FP_UP_DRAW); @@ -39,7 +39,7 @@ void DrawTexturedRect(IDirect3DTexture8* tex, float x, float y, float width, flo TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); TheseusSetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); - dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1); + TheseusSetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1); TVert quad[4] = { {x, y, 0.0f, 1.0f, 0xFFFFFFFF, 0.0f, 0.0f}, @@ -58,5 +58,5 @@ void DrawTexturedRect(IDirect3DTexture8* tex, float x, float y, float width, flo TheseusSetTexture(0, NULL); TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); - dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); + TheseusSetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); } diff --git a/theseus/render/widget_layer.cpp b/theseus/render/widget_layer.cpp index 88c8cf6..8d3b314 100644 --- a/theseus/render/widget_layer.cpp +++ b/theseus/render/widget_layer.cpp @@ -91,7 +91,7 @@ void DrawWidgets() TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); - dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); + TheseusSetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); const int sw = (int)g_nViewWidth; const int sh = (int)g_nViewHeight; diff --git a/theseus/xbox/main.cpp b/theseus/xbox/main.cpp index 112a50b..4dd137b 100644 --- a/theseus/xbox/main.cpp +++ b/theseus/xbox/main.cpp @@ -681,8 +681,8 @@ void Draw() TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); - VERIFYHR(TheseusGetD3DDev()->SetStreamSource(0, m_pVB, sizeof(COLORVERTEX))); - VERIFYHR(TheseusGetD3DDev()->SetVertexShader(D3DFVF_XYZ | D3DFVF_DIFFUSE)); + TheseusSetStreamSource(0, m_pVB, sizeof(COLORVERTEX)); + TheseusSetVertexShader(D3DFVF_XYZ | D3DFVF_DIFFUSE); VERIFYHR(TheseusGetD3DDev()->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2)); TheseusSetTransform(D3DTS_PROJECTION, &matProjectionSave); diff --git a/theseus/xbox/overlay.cpp b/theseus/xbox/overlay.cpp index b67b45b..18d6eb8 100755 --- a/theseus/xbox/overlay.cpp +++ b/theseus/xbox/overlay.cpp @@ -135,7 +135,7 @@ static void DrawOverlayImage(IDirect3DTexture8 *tex, float x, float y, float wid TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); TheseusSetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); - dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1); + TheseusSetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1); OverlayVertex quad[4] = { {x, y, 0.0f, 1.0f, 0xFFFFFFFF, 0.0f, 0.0f}, @@ -149,7 +149,7 @@ static void DrawOverlayImage(IDirect3DTexture8 *tex, float x, float y, float wid TheseusSetTexture(0, NULL); TheseusSetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); TheseusSetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE); - dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); + TheseusSetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); } // ============================================================================ @@ -2664,7 +2664,7 @@ void DrawCubeOverlay() TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); - dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); + TheseusSetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); const int sw = g_nViewWidth; const int sh = g_nViewHeight; diff --git a/theseus/xbox/panic_screen.cpp b/theseus/xbox/panic_screen.cpp index 893272b..fa9bcc0 100644 --- a/theseus/xbox/panic_screen.cpp +++ b/theseus/xbox/panic_screen.cpp @@ -170,7 +170,7 @@ void Set2DOverlayState(IDirect3DDevice8* dev) TheseusSetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE); TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); - dev->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); + TheseusSetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE); } // Procedural background. Two stacked quads draw a vertical gradient from From 7f8dcebea2903e767882316f5c8620b105caf393 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:26:50 -0500 Subject: [PATCH 097/167] Fixed the desktop keyboard mangling anything past plain ASCII Desktop kept the edit string and key labels as single bytes, so kana, the euro key and the accents came out wrong. The keyboard now holds wide characters on both builds and desktop converts to UTF-8 where text goes in or out. --- theseus/shared/keyboard_node.cpp | 129 +++++++++++++++++++++++++++---- 1 file changed, 113 insertions(+), 16 deletions(-) diff --git a/theseus/shared/keyboard_node.cpp b/theseus/shared/keyboard_node.cpp index 34ad303..9c2a949 100755 --- a/theseus/shared/keyboard_node.cpp +++ b/theseus/shared/keyboard_node.cpp @@ -186,13 +186,77 @@ static WORD UpperKey(WORD c) return c; } -// Desktop keys are single bytes, so there y diaeresis stays lowercase. -static TCHAR KeyUpper(TCHAR ch) +// The edit buffer holds UTF-16 like 5960's. Desktop strings are UTF-8, so +// they convert wherever text crosses into or out of the keyboard. +#ifdef _XBOX +typedef TCHAR KEYCHAR; +#else +typedef WORD KEYCHAR; + +// BMP only, like the text node; a stray byte reads as Latin-1. +static int DecodeUTF8(const char* s, KEYCHAR* out, int max) { - if (sizeof(TCHAR) > 1) - return (TCHAR)UpperKey((WORD)ch); - WORD up = UpperKey((unsigned char)ch); - return (up > 0xff) ? ch : (TCHAR)up; + int n = 0; + const unsigned char* p = (const unsigned char*)s; + while (*p != 0 && n < max) + { + unsigned int cp = *p; + int len = 1; + if ((cp & 0xe0) == 0xc0) { cp &= 0x1f; len = 2; } + else if ((cp & 0xf0) == 0xe0) { cp &= 0x0f; len = 3; } + else if ((cp & 0xf8) == 0xf0) { cp &= 0x07; len = 4; } + int k = 1; + for (; k < len && (p[k] & 0xc0) == 0x80; k++) + cp = (cp << 6) | (p[k] & 0x3f); + if (k < len) + { + cp = *p; + len = 1; + } + p += len; + if (cp <= 0xffff) + out[n++] = (KEYCHAR)cp; + } + return n; +} + +static int EncodeUTF8(KEYCHAR ch, char* out) +{ + if (ch < 0x80) + { + out[0] = (char)ch; + return 1; + } + if (ch < 0x800) + { + out[0] = (char)(0xc0 | (ch >> 6)); + out[1] = (char)(0x80 | (ch & 0x3f)); + return 2; + } + out[0] = (char)(0xe0 | (ch >> 12)); + out[1] = (char)(0x80 | ((ch >> 6) & 0x3f)); + out[2] = (char)(0x80 | (ch & 0x3f)); + return 3; +} + +// Encodes up to count characters, stopping at a null, into out (3 * count + 1 +// bytes). Returns the byte length. +static int EncodeUTF8(const KEYCHAR* src, int count, char* out) +{ + int n = 0; + for (int i = 0; i < count && src[i] != 0; i++) + n += EncodeUTF8(src[i], out + n); + out[n] = 0; + return n; +} +#endif + +static int KeyLen(const KEYCHAR* s) +{ + int n = 0; + while (s[n] != 0) + n++; + return n; } // The last USB keyboard keystroke: a HID key code and the character it typed @@ -260,7 +324,7 @@ class CKeyboard : public CNode bool OnSetProperty(const PRD* pprd, const void* pvValue); protected: - TCHAR m_edit[EDIT_MAX + 1]; + KEYCHAR m_edit[EDIT_MAX + 1]; int m_editLen; int m_cursor; // -1 means the whole string is selected int m_row; @@ -373,7 +437,7 @@ void CKeyboard::TakeSelection() // Shifts the tail from `at` right by `count` characters. void CKeyboard::OpenGap(int at, int count) { - MoveMemory(m_edit + at + count, m_edit + at, (m_editLen - at) * sizeof(TCHAR)); + MoveMemory(m_edit + at + count, m_edit + at, (m_editLen - at) * sizeof(KEYCHAR)); } @@ -541,13 +605,18 @@ void CKeyboard::activate() for (int end = m_editLen - 1; m_edit[end] == ' '; end--) m_edit[end] = 0; if (lead > 0) - MoveMemory(m_edit, m_edit + lead, (_tcslen(m_edit + lead) + 1) * sizeof(TCHAR)); + MoveMemory(m_edit, m_edit + lead, (KeyLen(m_edit + lead) + 1) * sizeof(KEYCHAR)); // The copy keeps the untrimmed length, so trimmed spaces come along as nulls. delete [] m_string; +#ifdef _XBOX m_string = new TCHAR[m_editLen + 1]; CopyChars(m_string, m_edit, m_editLen); m_string[m_editLen] = 0; +#else + m_string = new TCHAR[m_editLen * 3 + 1]; + EncodeUTF8(m_edit, m_editLen, m_string); +#endif CallFunction(this, _T("OnDone")); return; } @@ -566,7 +635,7 @@ void CKeyboard::activate() if (m_editLen >= EDIT_MAX) break; OpenGap(m_cursor, 1); - m_edit[m_cursor++] = (m_caps != m_shift) ? KeyUpper((TCHAR)key) : (TCHAR)key; + m_edit[m_cursor++] = (KEYCHAR)((m_caps != m_shift) ? UpperKey(key) : key); m_editLen++; m_shift = false; return; @@ -659,7 +728,7 @@ void CKeyboard::Backspace() CallFunction(this, _T("OnError")); return; } - MoveMemory(m_edit + m_cursor - 1, m_edit + m_cursor, (m_editLen - m_cursor) * sizeof(TCHAR)); + MoveMemory(m_edit + m_cursor - 1, m_edit + m_cursor, (m_editLen - m_cursor) * sizeof(KEYCHAR)); m_cursor--; m_editLen--; } @@ -739,14 +808,20 @@ void CKeyboard::CycleMode() void CKeyboard::Insert(const TCHAR* szInsert) { TakeSelection(); +#ifdef _XBOX + const KEYCHAR* insert = szInsert; int count = (int)_tcslen(szInsert); +#else + KEYCHAR insert[EDIT_MAX + 1]; + int count = DecodeUTF8(szInsert, insert, EDIT_MAX + 1); +#endif if (m_editLen + count > EDIT_MAX) { CallFunction(this, _T("OnError")); return; } OpenGap(m_cursor, count); - CopyChars(m_edit + m_cursor, szInsert, count); + MoveMemory(m_edit + m_cursor, insert, count * sizeof(KEYCHAR)); m_cursor += count; m_editLen += count; m_shift = false; @@ -784,10 +859,14 @@ bool CKeyboard::OnSetProperty(const PRD* pprd, const void* pvValue) const TCHAR* szValue = *(const TCHAR* const*)pvValue; if (szValue == NULL) szValue = _T(""); +#ifdef _XBOX int count = (int)_tcslen(szValue); if (count > EDIT_MAX) count = EDIT_MAX; CopyChars(m_edit, szValue, count); +#else + int count = DecodeUTF8(szValue, m_edit, EDIT_MAX); +#endif m_editLen = count; m_cursor = -1; return true; @@ -861,8 +940,23 @@ void CKeyboard::Render() if (m_text != NULL) { m_edit[m_editLen] = 0; +#ifdef _XBOX g_szText = m_edit; g_nTextChar = m_cursor; +#else + // The text node counts bytes and puts the cursor before the character + // whose last byte is g_nTextChar, so the cursor moves into bytes too. + TCHAR text[EDIT_MAX * 3 + 1]; + g_szText = text; + g_nTextChar = m_cursor; + if (m_cursor >= 0) + { + int len = KeyLen(m_edit); + int bytes = EncodeUTF8(m_edit, m_cursor < len ? m_cursor + 1 : len, text); + g_nTextChar = m_cursor < len ? bytes - 1 : bytes + m_cursor - len; + } + EncodeUTF8(m_edit, m_editLen, text); +#endif m_text->Render(); g_nTextChar = -1; } @@ -876,7 +970,7 @@ void CKeyboard::Render() if (!japanese) highlighted = MergedCell(highlighted); - TCHAR label[2]; + TCHAR label[4]; int node = 0; for (int cell = 0; cell < cells; cell++) { @@ -893,10 +987,13 @@ void CKeyboard::Render() } else { - label[0] = (TCHAR)key; - if (!japanese && m_caps != m_shift) - label[0] = KeyUpper(label[0]); + WORD ch = (!japanese && m_caps != m_shift) ? UpperKey(key) : key; +#ifdef _XBOX + label[0] = (TCHAR)ch; label[1] = 0; +#else + label[EncodeUTF8((KEYCHAR)ch, label)] = 0; +#endif g_szText = label; } From 08b7a5234f8a67b15a9235120701426d585a0f38 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:33:08 -0500 Subject: [PATCH 098/167] Settings files no longer get cut short by a crash mid-save Save rewrote config.ini and friends in place, so dying halfway left a truncated file. It now writes a .new copy, flushes it and swaps it in, keeping the last one as .old. On the Xbox the swap is a replacing rename, which FATX can't promise is atomic. Needs a hardware test. --- theseus/desktop/host.h | 2 ++ theseus/desktop/host_posix.cpp | 13 ++++++++ theseus/desktop/host_win.cpp | 10 +++++++ theseus/desktop/platform_shim.h | 5 ++++ theseus/desktop/xbox_file.cpp | 22 ++++++++++++++ theseus/shared/settingsfile.cpp | 53 ++++++++++++++++++++++++++------- theseus/shared/settingsfile.h | 1 + theseus/shared/theseus.h | 32 ++++++++++++++++++++ 8 files changed, 128 insertions(+), 10 deletions(-) diff --git a/theseus/desktop/host.h b/theseus/desktop/host.h index 320394e..01a9608 100644 --- a/theseus/desktop/host.h +++ b/theseus/desktop/host.h @@ -35,6 +35,8 @@ bool Host_Stat(const char* path, HostFileInfo* out); bool Host_StatOpenFile(FILE* f, HostFileInfo* out); bool Host_SetAttrs(const char* path, uint32_t attrs); bool Host_RenameReplace(const char* from, const char* to); +// Pushes a closed file's data all the way to disk, so a rename after it can't land first. +bool Host_FlushToDisk(const char* path); bool Host_CopyFile(const char* from, const char* to, bool failIfExists); bool Host_DiskSpace(const char* path, uint64_t* freeBytes, uint64_t* totalBytes); diff --git a/theseus/desktop/host_posix.cpp b/theseus/desktop/host_posix.cpp index f40f328..f388a21 100644 --- a/theseus/desktop/host_posix.cpp +++ b/theseus/desktop/host_posix.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,18 @@ bool Host_RenameReplace(const char* from, const char* to) { return rename(from, to) == 0; } +bool Host_FlushToDisk(const char* path) { + int fd = open(path, O_RDWR); + if (fd < 0) return false; + bool ok = fsync(fd) == 0; +#ifdef F_FULLFSYNC + // macOS fsync stops at the drive's cache; this goes past it. + if (ok) fcntl(fd, F_FULLFSYNC); +#endif + close(fd); + return ok; +} + bool Host_CopyFile(const char* from, const char* to, bool failIfExists) { struct stat st; if (failIfExists && stat(to, &st) == 0) return false; diff --git a/theseus/desktop/host_win.cpp b/theseus/desktop/host_win.cpp index 26b12a2..d946d27 100644 --- a/theseus/desktop/host_win.cpp +++ b/theseus/desktop/host_win.cpp @@ -99,6 +99,16 @@ bool Host_RenameReplace(const char* from, const char* to) { return MoveFileExA(f, t, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED) != FALSE; } +bool Host_FlushToDisk(const char* path) { + char p[MAX_PATH]; + ToWin(p, sizeof(p), path); + HANDLE h = CreateFileA(p, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (h == INVALID_HANDLE_VALUE) return false; + bool ok = FlushFileBuffers(h) != FALSE; + CloseHandle(h); + return ok; +} + bool Host_CopyFile(const char* from, const char* to, bool failIfExists) { char f[MAX_PATH], t[MAX_PATH]; ToWin(f, sizeof(f), from); diff --git a/theseus/desktop/platform_shim.h b/theseus/desktop/platform_shim.h index 991f115..a00d9c8 100644 --- a/theseus/desktop/platform_shim.h +++ b/theseus/desktop/platform_shim.h @@ -536,6 +536,11 @@ inline BOOL WriteFile(HANDLE h, const void* buf, DWORD nBytes, DWORD* nWritten, return !ferror((FILE*)h); } +// Desktop handles are FILE*s; this only empties the CRT buffer, reporting a failed write. +inline BOOL FlushFileBuffers(HANDLE h) { + return h && h != INVALID_HANDLE_VALUE && fflush((FILE*)h) == 0; +} + // SetFileTime stub (no-op on desktop) inline BOOL SetFileTime(HANDLE h, const FILETIME* c, const FILETIME* a, const FILETIME* w) { (void)h; (void)c; (void)a; (void)w; diff --git a/theseus/desktop/xbox_file.cpp b/theseus/desktop/xbox_file.cpp index 16d5537..0eb2026 100644 --- a/theseus/desktop/xbox_file.cpp +++ b/theseus/desktop/xbox_file.cpp @@ -252,6 +252,28 @@ BOOL TheseusCopyFile(LPCSTR src, LPCSTR dst, BOOL bFailIfExists) return Host_CopyFile(s, d, bFailIfExists != FALSE); } +// Swaps a finished szNew in for szFile with one rename, keeping what it +// replaces as .old. +bool TheseusReplaceFile(const TCHAR* szNew, const TCHAR* szFile) +{ + if (!szNew || !szFile) return false; + char xboxNew[MAX_PATH], xboxFile[MAX_PATH]; + CleanFilePath(xboxNew, szNew); + CleanFilePath(xboxFile, szFile); + char n[512], f[512]; + if (!XboxPath_TranslateForWrite(xboxNew, n, sizeof(n)) || !n[0]) return false; + if (!XboxPath_TranslateForWrite(xboxFile, f, sizeof(f)) || !f[0]) return false; + + if (!Host_FlushToDisk(n)) return false; + HostFileInfo info; + if (Host_Stat(f, &info)) { + char old[520]; + snprintf(old, sizeof(old), "%s.old", f); + Host_CopyFile(f, old, false); + } + return Host_RenameReplace(n, f); +} + FILE* TheseusFOpen(const TCHAR* path, const TCHAR* mode) { if (!path || !mode) return NULL; diff --git a/theseus/shared/settingsfile.cpp b/theseus/shared/settingsfile.cpp index 60a5da8..ce01b5d 100644 --- a/theseus/shared/settingsfile.cpp +++ b/theseus/shared/settingsfile.cpp @@ -148,19 +148,50 @@ void CSettingsFile::Cancel() Close(); } +// Writes .new and swaps it in, so a crash or power cut mid-save leaves +// the old file whole. Only when .new can't be created does it write in place. bool CSettingsFile::Save() { if (!m_dirty) return true; + int result = -1; + if (_tcslen(m_filePath) + 5 <= MAX_PATH) + { + TCHAR szNew [MAX_PATH]; + _stprintf(szNew, _T("%s.new"), m_filePath); + result = WriteTo(szNew); + if (result > 0 && !TheseusReplaceFile(szNew, m_filePath)) + result = 0; + if (result == 0) + { + FSCHAR sszNew [MAX_PATH]; + CleanFilePath(sszNew, szNew); + TheseusDeleteFile(sszNew); + } + } + if (result < 0) + result = WriteTo(m_filePath); + if (result <= 0) + return false; + + m_dirty = false; + + return true; +} + +// 1 when the whole file went out, 0 when a write failed, -1 when the file +// couldn't be created. +int CSettingsFile::WriteTo(const TCHAR* szPath) +{ #ifdef _UNICODE if (m_unicode) { // Write out Unicode file... - FILE* pFile = TheseusFOpen(m_filePath, _T("w")); + FILE* pFile = TheseusFOpen(szPath, _T("w")); if (pFile == NULL) - return false; + return -1; CSettingsFileSection* pSection; for (pSection = m_sections; pSection != NULL; pSection = pSection->m_next) @@ -173,18 +204,20 @@ bool CSettingsFile::Save() } } - fclose(pFile); + bool ok = fflush(pFile) == 0 && !ferror(pFile); + return (fclose(pFile) == 0 && ok) ? 1 : 0; } else #endif // _UNICODE { // Convert our Unicode stuff to ANSI on the way out... - HANDLE hFile = TheseusCreateFile(m_filePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); + HANDLE hFile = TheseusCreateFile(szPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) - return false; + return -1; DWORD dw; + bool ok = true; CSettingsFileSection* pSection; // Stream wide -> ANSI to disk in chunks. The old code reused a // 1030-byte stack buffer for the whole "value\r\n" line, which @@ -193,7 +226,8 @@ bool CSettingsFile::Save() char szBuf [1024]; auto FlushBuf = [&](char*& pch) { if (pch > szBuf) { - WriteFile(hFile, szBuf, (DWORD)(pch - szBuf), &dw, NULL); + if (!WriteFile(hFile, szBuf, (DWORD)(pch - szBuf), &dw, NULL) || dw != (DWORD)(pch - szBuf)) + ok = false; pch = szBuf; } }; @@ -228,12 +262,11 @@ bool CSettingsFile::Save() } } + if (!FlushFileBuffers(hFile)) + ok = false; CloseHandle(hFile); + return ok ? 1 : 0; } - - m_dirty = false; - - return true; } bool CSettingsFile::Close() diff --git a/theseus/shared/settingsfile.h b/theseus/shared/settingsfile.h index ec0d270..f39dc83 100755 --- a/theseus/shared/settingsfile.h +++ b/theseus/shared/settingsfile.h @@ -74,6 +74,7 @@ class CSettingsFile bool m_batching; protected: + int WriteTo(const TCHAR* szPath); #ifdef _UNICODE bool m_unicode; diff --git a/theseus/shared/theseus.h b/theseus/shared/theseus.h index 4c5afd8..adacf55 100755 --- a/theseus/shared/theseus.h +++ b/theseus/shared/theseus.h @@ -797,6 +797,37 @@ inline BOOL TheseusFindClose(HANDLE h) { return FindClose(h); } inline BOOL TheseusMoveFile(LPCSTR src, LPCSTR dst, DWORD flags = 0) { return MoveFileEx(src, dst, flags); } inline BOOL TheseusCopyFile(LPCSTR src, LPCSTR dst, BOOL bFailIfExists) { return CopyFile(src, dst, bFailIfExists); } inline FILE* TheseusFOpen(const TCHAR* path, const TCHAR* mode) { return _tfopen(path, mode); } + +// Swaps a finished szNew in for szFile, keeping what it replaces as .old. +// FATX can't promise a crash-proof rename, so the new file is flushed and the +// old one copied aside before the swap. +inline bool TheseusReplaceFile(const TCHAR* szNew, const TCHAR* szFile) +{ + FSCHAR sszNew [MAX_PATH]; + FSCHAR sszFile [MAX_PATH]; + FSCHAR sszOld [MAX_PATH + 4]; + CleanFilePath(sszNew, szNew); + CleanFilePath(sszFile, szFile); + + HANDLE hFile = CreateFile(sszNew, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); + if (hFile == INVALID_HANDLE_VALUE) + return false; + BOOL bFlushed = FlushFileBuffers(hFile); + CloseHandle(hFile); + if (!bFlushed) + return false; + + sprintf(sszOld, "%s.old", sszFile); + bool bKept = GetFileAttributes(sszFile) == (DWORD)-1 || CopyFile(sszFile, sszOld, FALSE); + if (MoveFileEx(sszNew, sszFile, MOVEFILE_REPLACE_EXISTING)) + return true; + + // No replacing rename: for a moment .old is the only whole copy, so only + // when there is one. + if (!bKept || !DeleteFile(sszFile)) + return false; + return MoveFileEx(sszNew, sszFile, 0) != FALSE; +} #else HANDLE TheseusCreateFile(const TCHAR* szFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes = 0, HANDLE hTemplateFile = NULL); bool TheseusCreateDirectory(LPCTSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL); @@ -812,6 +843,7 @@ BOOL TheseusFindClose(HANDLE h); BOOL TheseusMoveFile(LPCSTR src, LPCSTR dst, DWORD flags = 0); BOOL TheseusCopyFile(LPCSTR src, LPCSTR dst, BOOL bFailIfExists); FILE* TheseusFOpen(const TCHAR* path, const TCHAR* mode); +bool TheseusReplaceFile(const TCHAR* szNew, const TCHAR* szFile); #endif From f4a620ffc976f65d2206c2b8ba2b7bae916810f9 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:36:04 -0500 Subject: [PATCH 099/167] Settings saves reach the disk before they replace the old file SafeSave renamed the new file over the old one without forcing it to disk first, so a power cut right after could still leave an empty file. --- theseus/desktop/safe_save.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/theseus/desktop/safe_save.h b/theseus/desktop/safe_save.h index 359b272..8a024d7 100644 --- a/theseus/desktop/safe_save.h +++ b/theseus/desktop/safe_save.h @@ -22,6 +22,8 @@ struct SafeSave { bool ok = !ferror(f); ok = (fclose(f) == 0) && ok; f = nullptr; + // On disk before the rename, or a power cut can leave an empty file. + ok = ok && Host_FlushToDisk(tmp.c_str()); if (!ok) { remove(tmp.c_str()); return false; } HostFileInfo info; if (Host_Stat(path.c_str(), &info)) From 770a08feb0e112805b24817541812a30e9b4e68f Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:39:23 -0500 Subject: [PATCH 100/167] Rebuilt the XIP reader from the binary Wrote the archive reader again function by function from the 5960 XBE and moved it to render/xip_archive.cpp, with the desktop differences already in it. The Xbox build uses it now. Object tables, every entry's bytes, mesh buffers, textures, draws and reloads match the old reader on all 43 archives. --- build/Makefile | 6 +- build/Makefile.xbox | 2 +- build/xbox-sources.mk | 6 +- docs/decomp/README.md | 33 +- docs/decomp/XIP.md | 240 +++++-- theseus/engine/xip_archive.h | 95 ++- theseus/render/xip_archive.cpp | 1081 ++++++++++++++++++++++++++++++++ theseus/xbox/xip_archive.cpp | 941 --------------------------- 8 files changed, 1339 insertions(+), 1065 deletions(-) create mode 100644 theseus/render/xip_archive.cpp delete mode 100644 theseus/xbox/xip_archive.cpp diff --git a/build/Makefile b/build/Makefile index 48f25f6..547bc2a 100644 --- a/build/Makefile +++ b/build/Makefile @@ -336,7 +336,8 @@ SRCS_RENDER = \ render/text.cpp \ render/tmap_system.cpp \ render/widget_draw.cpp \ - render/widget_layer.cpp + render/widget_layer.cpp \ + render/xip_archive.cpp SRCS_XBOX = \ xbox/config.cpp \ @@ -358,8 +359,7 @@ SRCS_XBOX = \ xbox/theseus_launcher.cpp \ xbox/title_scanner.cpp \ xbox/widget_config.cpp \ - xbox/xbox_live.cpp \ - xbox/xip_archive.cpp + xbox/xbox_live.cpp SRCS_TOOLBOX = \ toolbox/drive.cpp \ diff --git a/build/Makefile.xbox b/build/Makefile.xbox index 95a5177..b217c15 100644 --- a/build/Makefile.xbox +++ b/build/Makefile.xbox @@ -199,7 +199,7 @@ $(MINIMP3_OBJ): CXXFLAGS := -MMD -MP -fno-rtti \ # separate BUILDS_ROOT so it never mixes with the -O0 objects. ifdef HOT_O2 HOT_O2_OBJS := $(foreach f,render/text.cpp render/widget_draw.cpp render/asset_loader.cpp \ - render/shape_render.cpp xbox/xip_archive.cpp,$(call oxdk_obj,$(THESEUS_SRC)/$(f))) + render/shape_render.cpp render/xip_archive.cpp,$(call oxdk_obj,$(THESEUS_SRC)/$(f))) $(HOT_O2_OBJS): OXDK_OPT := -O2 endif diff --git a/build/xbox-sources.mk b/build/xbox-sources.mk index 3f64544..de50ec6 100644 --- a/build/xbox-sources.mk +++ b/build/xbox-sources.mk @@ -53,7 +53,8 @@ SRCS_RENDER = \ render/text.cpp \ render/tmap_system.cpp \ render/widget_draw.cpp \ - render/widget_layer.cpp + render/widget_layer.cpp \ + render/xip_archive.cpp SRCS_XBOX = \ xbox/config.cpp \ @@ -75,8 +76,7 @@ SRCS_XBOX = \ xbox/theseus_launcher.cpp \ xbox/title_scanner.cpp \ xbox/widget_config.cpp \ - xbox/xbox_live.cpp \ - xbox/xip_archive.cpp + xbox/xbox_live.cpp SRCS_TOOLBOX = \ toolbox/drive.cpp \ diff --git a/docs/decomp/README.md b/docs/decomp/README.md index e518b7c..48614f5 100644 --- a/docs/decomp/README.md +++ b/docs/decomp/README.md @@ -122,11 +122,32 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x0003baf0 | `CRecovery::RecoveryThread` | (reference only) | [App Shell](#app-shell) | Worker thread that performs the original factory-recovery wipe sequence. Theseus does not exercise this on modded hardware. | | 0x0003bf00 | `CSettingsFile::Save` | `CSettingsFile::Save` (settingsfile.cpp) | [Settings](Settings.md) | Serializes the in-memory settings tree to disk. Driven by the settings panel's "Save" action. | | 0x0003ca10 | `CTranslator::TranslateStripColon` | `CTranslator::TranslateStripColon` (locale_node.cpp) | [Locale](Locale.md) | Looks up a localized string by key, then strips a trailing `:` if present. Used for menu labels that need both punctuated and bare forms. | -| 0x0003dba0 | `FindObjectInXIP` | `FindObjectInXIP` (xip_archive.cpp) | [XIP Archives](XIP.md) | Free function: searches an opened XIP archive's directory for a named object (texture, mesh, script). | -| 0x0003ddf0 | `CXipFile::Open` | `CXipFile::Open` (xip_archive.cpp) | [XIP Archives](XIP.md) | Opens a `.xip` archive, reads the header, and validates the magic. Returns a `CXipFile*` handle for subsequent lookups. | -| 0x0003dec0 | `CXipFile::StartLoadThread` | `CXipFile::StartLoadThread` (xip_archive.cpp) | [XIP Archives](XIP.md) | Spawns a background loader thread that streams archive payload while the main thread continues. | -| 0x0003e290 | `CXipFile::Find` | `CXipFile::Find` (xip_archive.cpp) | [XIP Archives](XIP.md) | Linear scan of the archive's TOC for a named entry. | -| 0x0003e320 | `CXipFile::FindObject` | `CXipFile::FindObject` (xip_archive.cpp) | [XIP Archives](XIP.md) | Higher-level wrapper around `Find` that also resolves cross-archive references via the global XIP search list. | +| 0x0003d390 | `FUN_0003d390` | `CXipFile::ReadIndexBuffer` (xip_archive.cpp) | [XIP Archives](XIP.md) | Builds an index buffer resource by hand from `XMemAlloc`, retrying through the texture and mesh caches when memory runs out. | +| 0x0003d410 | `FUN_0003d410` | `CXipFile::ReadVertexBuffer` (xip_archive.cpp) | [XIP Archives](XIP.md) | Same for a vertex buffer: a 12 byte resource header plus contiguous memory for the vertices. | +| 0x0003d4a0 | `CFileBuffer_FreeBuffer` | `CFileBuffer::SetFile` (xip_archive.cpp) | [XIP Archives](XIP.md) | Takes the file handle and finds the archive's entry in the signed `XIPS` hash section. Theseus keeps the handle only. | +| 0x0003d570 | `CFileBuffer_VerifyXipSignature` | (not built) | [XIP Archives](XIP.md) | Hashes one 64 KB block and compares it with the signed table; a mismatch reboots. Theseus loads unsigned archives. | +| 0x0003d5e0 | `CFileBuffer_Seek` | `CFileBuffer::Seek` (xip_archive.cpp) | [XIP Archives](XIP.md) | Loads the 64 KB block holding a file offset and points the read index into it. | +| 0x0003d710 | `CFileBuffer_Read` | `CFileBuffer::Read` (xip_archive.cpp) | [XIP Archives](XIP.md) | Copies out of the block buffer and refills it a block at a time. A failed `ReadFile` reboots. | +| 0x0003d900 | `LoadXIP` | `LoadXIP` (xip_archive.cpp) | [XIP Archives](XIP.md) | Finds an archive in the 20 slot pool by path or opens the next slot, then loads it on a thread or inline. | +| 0x0003dab0 | `FindInXIPAndDetach` | `FindInXIPAndDetach` (xip_archive.cpp) | [XIP Archives](XIP.md) | Finds a raw entry, newest archive first, and hands its bytes to the caller. | +| 0x0003dba0 | `FindObjectInXIP` | `FindObjectInXIP` (xip_archive.cpp) | [XIP Archives](XIP.md) | Searches every loaded archive, newest first; textures asked of the default archive are looked up as `.xbx`. | +| 0x0003dd20 | `CXipFile_dtor` | `CXipFile::~CXipFile` (xip_archive.cpp) | [XIP Archives](XIP.md) | Frees the tables and names, releases the mesh buffers, frees the block buffer. | +| 0x0003ddf0 | `CXipFile_Open` | `CXipFile::Open` (xip_archive.cpp) | [XIP Archives](XIP.md) | Stores the file name and the lookup key: the path without its extension, with `default` dropped. | +| 0x0003dec0 | `CXipFile_StartLoadThread` | `CXipFile::StartLoadThread` (xip_archive.cpp) | [XIP Archives](XIP.md) | Thread entry: reloads the mesh buffers if a reload is pending, otherwise loads the archive. | +| 0x0003def0 | `CXipFile_Load` | `CXipFile::Load` (xip_archive.cpp) | [XIP Archives](XIP.md) | Opens the file unbuffered, reads the header, tables and name pool, then every entry. | +| 0x0003e030 | `FUN_0003e030` | `ReadTexture` (xip_archive.cpp) | [XIP Archives](XIP.md) | Turns an XPR0 entry into a texture: copies the resource header and moves the pixels to contiguous memory. | +| 0x0003e110 | `CXipFile_CreateObjects` | `CXipFile::CreateObjects` (xip_archive.cpp) | [XIP Archives](XIP.md) | Reads every entry in file order into raw memory, a texture, a mesh reference or a mesh buffer. | +| 0x0003e270 | `LAB_0003e270` | `XipCompareName` (xip_archive.cpp) | [XIP Archives](XIP.md) | `bsearch` compare: `_stricmp` of the wanted name against a directory entry's name. | +| 0x0003e290 | `CXipFile_Find` | `CXipFile::Find` (xip_archive.cpp) | [XIP Archives](XIP.md) | Checks the path starts with this archive's key, then binary searches the sorted directory. | +| 0x0003e320 | `CXipFile_FindObject` | `CXipFile::FindObject` (xip_archive.cpp) | [XIP Archives](XIP.md) | `Find` plus an optional type check; a texture gets an `AddRef` for the caller. | +| 0x0003e390 | `CXipFile_DeleteMeshBuffers` | `CXipFile::DeleteMeshBuffers` (xip_archive.cpp) | [XIP Archives](XIP.md) | Releases all ten vertex and index buffers. | +| 0x0003e440 | `CleanupMeshCache` | `CleanupMeshCache` (xip_archive.cpp) | [XIP Archives](XIP.md) | Under memory pressure, frees the mesh buffers of the archive drawn longest ago. | +| 0x0003e5f0 | `CXipFile_ReadIndexBuffer` | `CXipFile::ReadIndexBuffer` (xip_archive.cpp) | [XIP Archives](XIP.md) | Reads an index buffer entry into a new buffer. | +| 0x0003e650 | `CXipFile_ReadVertexBuffer` | `CXipFile::ReadVertexBuffer` (xip_archive.cpp) | [XIP Archives](XIP.md) | Reads the vertex count and FVF, then the vertices into a new buffer. | +| 0x0003e710 | `CXipFile_ReloadMeshBuffers` | `CXipFile::ReloadMeshBuffers` (xip_archive.cpp) | [XIP Archives](XIP.md) | Seeks back to every mesh buffer entry and reads it again after an eviction. | +| 0x0003e800 | `CMeshRef_Render` | `CXipFile::Reload` (xip_archive.cpp) | [XIP Archives](XIP.md) | Mislabeled in the project: flags a reload and starts the loader thread, running it inline if no thread starts. | +| 0x0003e850 | (no function) | `CMeshRef::Render` (xip_archive.cpp) | [XIP Archives](XIP.md) | CMeshRef vtable slot 0: reloads evicted buffers, sets the shader, draws the slice. | +| 0x0003ead0 | (no function) | `CMeshRef::GetFVF` (xip_archive.cpp) | [XIP Archives](XIP.md) | CMeshRef vtable slot 1: the FVF of the buffer the reference draws from. | | 0x0003f420 | `CGameCopier::CreateDirectoryA` | `CGameCopier::CreateDirectory` (file_ops.cpp) | [File Ops](FileOps.md) | Wrapper around the kernel's CreateDirectory with progress-reporting hooks for the copy UI. | | 0x0003f700 | `CGameCopier::CopyFileA` | `CGameCopier::CopyFile` (file_ops.cpp) | [File Ops](FileOps.md) | Buffered file copy with progress callback. Used by save-to-MU and game-copy flows. | | 0x0003f830 | `CGameCopier::DeleteFileA` | `CGameCopier::DeleteFile` (file_ops.cpp) | [File Ops](FileOps.md) | DeleteFile wrapper. | @@ -287,7 +308,7 @@ The seed and first expansion cover the application shell, script VM, scene graph |-----------|-----|----------| | Script VM | [VM.md](VM.md) | 35 functions | | Scene Graph | [Node.md](Node.md) | 2 functions + 32 class-table registrations | -| XIP Archives | [XIP.md](XIP.md) | 5 functions | +| XIP Archives | [XIP.md](XIP.md) | 26 functions | | Animation Nodes | [AnimationNodes.md](AnimationNodes.md) | 4 functions | | Audio System | [AudioSystem.md](AudioSystem.md) | 12 functions | | Camera | [Camera.md](Camera.md) | not yet started | diff --git a/docs/decomp/XIP.md b/docs/decomp/XIP.md index d019b69..a35dc03 100644 --- a/docs/decomp/XIP.md +++ b/docs/decomp/XIP.md @@ -1,81 +1,205 @@ -# XIP +# XIP Archives + +What we found in the Xbox Dashboard about its XIP archives: one `XIP0` file per scene, holding +its scripts, textures, raw data and meshes. Everything here comes from the 5960 retail XBE. +Theseus rebuilds it in `render/xip_archive.cpp`, one file for both builds. + +## Functions + +| Address | Name | What it does | +|---------|------|--------------| +| 0x0003d390 | (index buffer alloc) | Builds an index buffer resource by hand, with the out of memory retry | +| 0x0003d410 | (vertex buffer alloc) | Same for a vertex buffer | +| 0x0003d4a0 | CFileBuffer::SetFile | Keeps the handle, finds the archive in the signed `XIPS` section | +| 0x0003d570 | (block hash check) | Hashes a 64 KB block and compares it with the signed table | +| 0x0003d5e0 | CFileBuffer::Seek | Loads the block holding an offset | +| 0x0003d710 | CFileBuffer::Read | Copies out of the block buffer, refilling it as it goes | +| 0x0003d900 | LoadXIP | Finds or opens an archive in the pool and starts its load | +| 0x0003dab0 | FindInXIPAndDetach | Hands a raw entry's bytes to the caller | +| 0x0003dba0 | FindObjectInXIP | Looks an entry up across every loaded archive | +| 0x0003dd20 | CXipFile::~CXipFile | Frees tables, mesh buffers and the block buffer | +| 0x0003ddf0 | CXipFile::Open | Stores the file name and the lookup key | +| 0x0003dec0 | CXipFile::StartLoadThread | Thread entry: load, or reload the mesh buffers | +| 0x0003def0 | CXipFile::Load | Reads the header, the tables and every entry | +| 0x0003e030 | (texture read) | XPR0 entry to a texture | +| 0x0003e110 | CXipFile::CreateObjects | Reads the entries in file order | +| 0x0003e270 | (name compare) | `bsearch` compare, `_stricmp` | +| 0x0003e290 | CXipFile::Find | Path to entry index | +| 0x0003e320 | CXipFile::FindObject | Path to object, with a type check and an `AddRef` for textures | +| 0x0003e390 | CXipFile::DeleteMeshBuffers | Releases all ten buffer pairs | +| 0x0003e440 | CleanupMeshCache | Frees the mesh buffers of the archive drawn longest ago | +| 0x0003e5f0 | CXipFile::ReadIndexBuffer | Index buffer entry to a buffer | +| 0x0003e650 | CXipFile::ReadVertexBuffer | Vertex buffer entry to a buffer | +| 0x0003e710 | CXipFile::ReloadMeshBuffers | Reads every mesh buffer again after an eviction | +| 0x0003e800 | CXipFile::Reload | Flags a reload and starts the loader (labeled `CMeshRef_Render` in the project) | +| 0x0003e850 | CMeshRef::Render | CMeshRef vtable (`0x00024dbc`) slot 0 | +| 0x0003ead0 | CMeshRef::GetFVF | Slot 1 | + +Globals: the pool `c_rgXipFile` at `0x001804e0` (twenty 0x150 byte slots, torn down by the +`atexit` thunk at `0x000ce600`), the count at `0x0017e734`, the frame clock the cache times come +from at `0x0017e808`, the XIP folder string at `0x0017e828`, the out of memory stage at +`0x0017c028`, and the disk lock every `ReadFile` holds shared at `0x0017aeb0`. + +## The file + +| Offset | Size | Field | +|--------|------|-------| +| 0 | 4 | magic `XIP0` (`0x30504958`) | +| 4 | 4 | data start | +| 8 | 2 | entry count | +| 0xa | 2 | name count | +| 0xc | 4 | data size | + +Then one 16 byte record per entry (data offset, size, type, timestamp), one 4 byte directory record +per name (entry index, name offset), the name pool, and the data. `Load` sizes the name pool as +`dataStart - 16 - entries * 16 - names * 4` and never checks it. + +Entry types, from the `CreateObjects` switch: + +| Type | What `CreateObjects` makes | +|------|----------------------------| +| 1 | nothing, and nothing is read | +| 2 | a texture (`0x0003e030`) | +| 4 | a 0x14 byte CMeshRef: archive, `offset >> 24` as the buffer, `offset & 0xffffff` as the first index, `size` as the primitive count | +| 5 | an index buffer, in the slot of the vertex buffer count at that moment | +| 6 | a vertex buffer: a 4 byte vertex count and a 4 byte FVF lead the vertices | +| anything else | `size` raw bytes from `XAppAllocMemory` | + +Entries are read back to back with no seeking, so the data has to be in table order. The +directory is sorted, because `Find` binary searches it. + +## Reading the file + +`Load` opens the archive with `NO_BUFFERING | SEQUENTIAL_SCAN` (`0x28000080`), after turning a +leading `y:` into `x:`. Unbuffered handles only read whole aligned blocks, so every read goes +through `CFileBuffer`, one 64 KB `VirtualAlloc` block at a time. The object is six dwords: the +handle, the signed section, the buffer, the bytes the last block gave, the read index, and one past +the block in the buffer. -XIP ("XIP0") is the archive format used by the Xbox Dashboard to bundle scene scripts (.xap), textures (.xbx), meshes (.xm), and other assets into a single file. The dashboard loads XIPs at startup and on skin changes, caching mesh data in GPU-accessible memory. Backed by `xip_archive.cpp` and `xip_archive.h` in the active source tree. +``` +Read(pv, cb): // 0x0003d710 + if (!buffer) buffer = VirtualAlloc(0x10000); // retry: texture cache, mesh cache, then reboot + for (;;) { + if (cb < 1) return total; + if (index < size) { copy min(size - index, cb); advance; } + if (index != size) continue; + lock shared 0x17aeb0; ok = ReadFile(h, buffer, 0x10000, &got); unlock; + if (!ok) HalReturnToFirmware(4); + if (got == 0) return -1; + hash check the block; current++; size = got; index = 0; + } +``` -## File Format +The buffer refills the moment it runs dry, even when the copy just finished, so a read ending on +the file's last byte returns -1. `Seek` reads the block an offset falls in unless `current` already +names it; since `current` is one past the block in the buffer, seeking back into that block reads +it again. -Derived from hex analysis of .xip files. +Retail archives are signed. `SetFile` walks the `XIPS` section for the archive's file name and +keeps its record, and every block `Read` or `Seek` loads is run through `XCCalcDigest` and +compared with the record's next 20 byte hash (`0x0003d570`). A missing record or a wrong hash +calls `HalReturnToFirmware(4)`, the fatal error reboot. -### Header (16 bytes) +## The pool -``` -Offset Size Field -0x00 4 Magic ("XIP0" = 0x30504958) -0x04 4 DataStart (byte offset where file data begins) -0x08 2 FileCount (number of FILEDATA entries) -0x0A 2 NameCount (number of FILENAME entries) -0x0C 4 DataSize (total size of all file data) -``` +`LoadXIP` puts relative names under `Q:/Xips/`, cleans the path, drops the extension, and compares +that with every slot's key. A hit stamps the slot's cache time and, if its mesh buffers were +evicted, reads them back before returning it. Otherwise it opens the next slot, counts it, and +loads it on a new thread; if no thread starts, it runs the loader inline. There is no bound check: +a twenty first archive would write past the pool. Slots are never freed. Nothing in the binary +unloads an archive; only its mesh buffers come and go. -### FILEDATA entries (16 bytes each) +`Open` keeps the path without its extension as the key, except that an archive named `default` +gives up its own name too, so `Q:\Xips\default.xip` answers for `Q:\Xips`. `LoadXIP`'s own +comparison keeps `default`, so a second `LoadXIP` of the default archive would open a new slot. -``` -Offset Size Field -0x00 4 DataOffset (from DataStart, or packed mesh ID for type 4) -0x04 4 Size (byte count, or primitive count for type 4) -0x08 4 Type (0=generic, 1=mesh[obsolete], 2=texture, 3=wave, 4=meshref, 5=IB, 6=VB) -0x0C 4 Timestamp -``` - -### FILENAME entries (4 bytes each) +`FindObjectInXIP` and `FindInXIPAndDetach` search newest archive first and skip any that is +locked or not loaded. For a texture asked of slot 0, `FindObjectInXIP` swaps the file name's +extension for `xbx` and looks it up under the XIP folder instead, since the default archive packs +its textures as `.xbx`. `FindInXIPAndDetach` hands the raw bytes and size to the caller and clears +the archive's pointer, so the caller frees them. ``` -Offset Size Field -0x00 2 FileDataIndex (index into FILEDATA array) -0x02 2 NameOffset (offset into name string pool) +Find(url): // 0x0003e290 + n = strlen(dirPath); + if (_strnicmp(url, dirPath, n) || url[n] != '\\') return -1; + hit = bsearch({names, url + n + 1}, directory, nameCount, 4, compare); + return hit ? hit->entry : -1; + +FindObject(url, type): // 0x0003e320 + n = Find(url); + if (n == -1 || (type != -1 && filedata[n].type != type)) return 0; + if (type == 2) objects[n]->AddRef(); + return objects[n]; ``` -### Layout +## Textures ``` -[XIPHEADER][FILEDATA * FileCount][FILENAME * NameCount][name strings][file data...] +texture(file, cb): // 0x0003e030 + xpr = new(cb); file.Read(xpr, cb); + if (xpr->magic != 'XPR0') return 0; + tex = AllocNoncontiguous(0x14); copy 0x14 bytes from xpr + 0xc; + bits = AllocContiguous(xpr->total - xpr->headerSize, 0x80); + if (!bits) return 0; + copy the pixels from xpr + headerSize; + tex->Data = 0; Register(bits); tex->Common |= 0x01000000; + return tex; ``` -Directory is sorted by name for binary search (bsearch with _stricmp). - -## Binary Analysis (4920-5960 retail XBE) - -### Globals +`Register` is inlined: the address is masked to physical unless the resource is a push buffer. +The entry's bytes are never freed, and a failed pixel allocation leaks the header. -- `c_nXipFileCount` at `0x0001c028`: count of loaded archives -- `c_rgXipFile[20]`: static pool of CXipFile instances +## Mesh buffers -### CMeshRef vtable +A CXipFile carries ten 24 byte buffer pairs at `+0x2c` (FVF, stride, vertex count, index count, +vertex buffer, index buffer), then the vertex buffer count `+0x11c`, the index buffer count +`+0x120`, the cache time `+0x128`, the lock flag `+0x130`, the `CFileBuffer` `+0x134` and the +reload flag `+0x14c`. The loaded flag is `+0x28`. -- `___7CMeshRef__6B_` at `0x000249fc` +The buffers are built without the device: `0x0003d390` and `0x0003d410` `XMemAlloc` a resource +header and the data, and the readers write straight into it. `ReadVertexBuffer` first waits for +the GPU if the buffer is still in use. The stride is `(size - 8) / count`, with no check on the +count. `ReloadMeshBuffers` seeks to each buffer entry in table order and reads it again, numbering +index buffers and vertex buffers separately. -### File Protection - -Retail binary calls `HalReturnToFirmware(HalFatalErrorRebootRoutine)` on XIP read failure. Debug builds issue an ALERT and `int 3` breakpoint. - -### Texture Loading (XPR format) - -Textures inside XIPs are wrapped in XPR containers (Xbox Packed Resource). The loader checks the XPR magic, allocates GPU-contiguous memory via `TheseusD3D_AllocContiguousMemory`, copies texture data, and registers it with D3D. - -### Mesh Buffer Layout - -Vertex buffers are prefixed with an 8-byte header: `[int vertexCount][DWORD fvf]`, followed by raw vertex data. Index buffers are raw WORD arrays. - -Mesh references pack the buffer index and first-index into a single DWORD: +Under memory pressure `XAppAllocMemory` and the block allocator call `CleanupTextureCache` first, +then `CleanupMeshCache`, which frees the buffers of the unlocked, loaded archive with the oldest +cache time that still has some. The next `CMeshRef::Render` of that archive sees a NULL vertex +buffer and calls `Reload`; `CInline_Render` (`0x0004e5d0`) checks the same thing inline +(`loaded && !reloading && vbCount && !meshBuffers[0].vb`) and reloads before drawing. ``` -DataOffset = (bufferIndex << 24) | firstIndex +CMeshRef::Render(bSetFVF): // 0x0003e850 + xip->cacheTime = now; buf = &xip->meshBuffers[index]; + if (xip->reloading) return; + if (!buf->vb) { xip->Reload(); return; } + if (bSetFVF) SetVertexShader(GetFixedFunctionShader(buf->fvf)); + if (count > 800 && !g_bEdgeAntialiasOverride) { EDGEANTIALIAS = 0; MULTISAMPLEANTIALIAS = 1; } + if (g_uMeshRef++ == 0) { + view = inverse(affine(rotation 0x00187fe0, position 0x00187fd4)), third row negated; + wv = matrix stack top * view; + c0..3 = transpose(wv * projection); c10..13 = transpose(wv); + c5..8 = inverse(wv); c4 = the light direction in object space; + } + SetStreamSource(0, buf->vb, buf->stride); SetIndices(buf->ib, 0); + DrawIndexedVertices(TRIANGLELIST, count * 3, indices + first); ``` -### Skin Override - -`FindObjectInXIP` checks the skin directory for loose texture files before searching loaded archives. This allows skins to override individual textures without repacking the XIP. - -### Cache Eviction - -`CleanupMeshCache` finds the oldest loaded XIP (by `m_cacheTime`) that has mesh buffers allocated and frees them. Mesh buffers are lazily reloaded on next render via `CMeshRef::Render`. +The constant numbers above are the binary's minus its bias of 96. Theseus sets the same constants +on every world matrix change instead (see `SetFalloffShaderFrameValues`, `0x0005ca00`). + +## Where Theseus differs + +- No `XIPS` signature check and no `y:` to `x:` swap; files open through Theseus's own path layer. +- A twenty first archive is refused instead of overrunning the pool; a sync `LoadXIP` that fails + returns NULL. +- `Load` checks every read, refuses a header whose name pool would be empty or whose + `dataStart + dataSize` isn't the file size, and closes a refused file. +- `CreateObjects` refuses mesh references past the last slot and mesh buffers past ten, and reads + past what it refuses so the entries after stay in line. Textures must hold an XPR header and a + resource header, and their sizes must fall inside the entry. +- Reload refuses buffers `CreateObjects` refused, and `CMeshRef` refuses an index past the buffers. +- Buffers are made through the device (bgfx on desktop) and locked to write. +- Desktop: loads and reloads run inline, textures are decoded in software, raw entries get a + terminating zero, a refused read is refused instead of rebooting, and there is no edge AA swap. diff --git a/theseus/engine/xip_archive.h b/theseus/engine/xip_archive.h index 1a93f6a..c35bf56 100644 --- a/theseus/engine/xip_archive.h +++ b/theseus/engine/xip_archive.h @@ -1,62 +1,59 @@ -// xip_archive.h: XIP archive format definitions and runtime loader. -// -// Companion to xbox/xip_archive.cpp. See docs/decomp/XIP.md for the -// on-disk format reverse-engineering notes. +// xip_archive.h: the XIP archive, one file holding a scene's scripts, +// textures and meshes. The reader is render/xip_archive.cpp; the format and +// what the 5960 dash does with it are in docs/decomp/XIP.md. #pragma once -// ========================================================================= -// XIP file format (on-disk structures) -// ========================================================================= +// ===== On disk ============================================================== +// [XIPHEADER][FILEDATA x fileCount][FILENAME x nameCount][names][data] -#define XIP_MAGIC 0x30504958 // "XIP0" little-endian +#define XIP_MAGIC 0x30504958 // "XIP0" #define XIP_DIGEST_LENGTH 20 struct XIPHEADER { - DWORD m_magic; // must equal XIP_MAGIC - DWORD m_dataStart; // byte offset where the file data section begins - WORD m_fileCount; // number of FILEDATA entries - WORD m_nameCount; // number of FILENAME entries - DWORD m_dataSize; // total size of all file data + DWORD m_magic; + DWORD m_dataStart; // file offset of the data section + WORD m_fileCount; + WORD m_nameCount; + DWORD m_dataSize; // dataStart + dataSize is the file size }; struct FILEDATA { - DWORD m_dataOffset; // offset within the data section - // (or packed mesh-ref payload for type=MESH_REFERENCE) - DWORD m_size; // size in bytes - // (or primitive count for type=MESH_REFERENCE) - DWORD m_type; // one of XIP_TYPE_* - DWORD m_timestamp; // file modification time + DWORD m_dataOffset; // from dataStart; a mesh reference packs (buffer << 24) | first index + DWORD m_size; // bytes; a mesh reference keeps its primitive count here + DWORD m_type; // XIP_TYPE_* + DWORD m_timestamp; }; +// Sorted by name, so lookups are a bsearch. struct FILENAME { - WORD m_fileDataIndex; // index into the FILEDATA array - WORD m_nameOffset; // offset into the names string pool + WORD m_fileDataIndex; + WORD m_nameOffset; // into the name pool }; -// File type identifiers stored in FILEDATA::m_type #define XIP_TYPE_GENERIC 0 -#define XIP_TYPE_MESH 1 // obsolete -#define XIP_TYPE_TEXTURE 2 // XPR-wrapped texture (.xbx) -#define XIP_TYPE_WAVE 3 // audio data -#define XIP_TYPE_MESH_REFERENCE 4 // packed: high byte = buffer index, low 24 = first index -#define XIP_TYPE_INDEXBUFFER 5 // raw D3D index buffer -#define XIP_TYPE_VERTEXBUFFER 6 // raw D3D vertex buffer (prefixed with count + FVF) +#define XIP_TYPE_MESH 1 // nothing writes these anymore +#define XIP_TYPE_TEXTURE 2 // an XPR0 texture (.xbx) +#define XIP_TYPE_WAVE 3 +#define XIP_TYPE_MESH_REFERENCE 4 // a slice of one of the archive's mesh buffers +#define XIP_TYPE_INDEXBUFFER 5 // WORD indices +#define XIP_TYPE_VERTEXBUFFER 6 // vertex count, FVF, then the vertices #ifndef BUILD_XIPSIGN +// 5960 gives every archive ten buffer pairs. Desktop allows more for archives +// built outside the dash; the mesh reference byte still caps it at 256. +#ifdef _XBOX #define MAX_MESHBUFFER 10 +#else +#define MAX_MESHBUFFER 512 +#endif -// ========================================================================= -// Runtime structures -// ========================================================================= - -// One vertex/index buffer pair attached to an XIP archive. CXipFile -// holds an array of these and CMeshRef instances point at one of them -// by index, sharing the GPU buffers across many mesh references. +// One vertex buffer and the index buffer that goes with it. Every CMeshRef in +// the archive draws a slice of one of these. struct CMeshBuffer { DWORD m_fvf; @@ -67,31 +64,26 @@ struct CMeshBuffer IDirect3DIndexBuffer8* m_indexBuffer; }; -// Buffered file reader -- reads in 64KB chunks aligned to 64KB -// boundaries. The dashboard opens XIPs with FILE_FLAG_NO_BUFFERING -// for performance, which requires sector-aligned reads, so this -// wrapper batches small Read() calls into a single large ReadFile(). +// Reads an archive 64 KB at a time. The dash opens XIPs unbuffered, which +// only allows whole aligned blocks, so small reads come out of this buffer. class CFileBuffer { public: CFileBuffer(); ~CFileBuffer(); - void SetFile(PCTSTR Name, HANDLE hFile); + void SetFile(const TCHAR* Name, HANDLE hFile); int Read(void* pv, int cb); bool Seek(int nPos); void FreeBuffer(); - void StartSignature(VOID); - void EndSignature(XCALCSIG_SIGNATURE* Signature); HANDLE m_handle; BYTE* m_buffer; - int m_bufferSize; + int m_bufferSize; // what the last block read gave int m_readIndex; - int m_currentBlock; + int m_currentBlock; // one past the block in the buffer }; -// XIP archive -- loads and manages a .xip file's contents. class CXipFile { public: @@ -103,7 +95,7 @@ class CXipFile int Find(const char* szURL); void* FindObject(const char* szURL, int nType = -1); - char* m_dirPath; + char* m_dirPath; // lookup key: the path minus ".xip"; default.xip answers for its folder TCHAR* m_fileName; XIPHEADER m_header; @@ -120,8 +112,8 @@ class CXipFile void DeleteMeshBuffers(); - XTIME m_cacheTime; - bool m_locked; + XTIME m_cacheTime; // last use, so CleanupMeshCache can pick the oldest + bool m_locked; // set while a load or reload fills the buffers bool IsUnloaded() const; bool IsReloading() const; @@ -141,14 +133,11 @@ class CXipFile friend CXipFile* LoadXIP(const TCHAR* szURL, bool bSync, bool isSkin); }; -// CMeshRef is defined in Mesh.h -- it references CXipFile's shared buffers - -// ========================================================================= -// Public API -// ========================================================================= +// CMeshRef lives in asset_loader.h and points back into m_meshBuffers. extern CXipFile* LoadXIP(const TCHAR* szURL, bool bSync = false, bool isSkin = false); extern bool FindInXIPAndDetach(const TCHAR* szURL, BYTE*& pbContent, DWORD& cbContent); extern void* FindObjectInXIP(const TCHAR* szURL, const TCHAR* szFilename, int nType = -1); +extern bool CleanupMeshCache(); #endif // BUILD_XIPSIGN diff --git a/theseus/render/xip_archive.cpp b/theseus/render/xip_archive.cpp new file mode 100644 index 0000000..e59265e --- /dev/null +++ b/theseus/render/xip_archive.cpp @@ -0,0 +1,1081 @@ +// xip_archive.cpp: the XIP archive reader. A .xip holds a scene's scripts, +// textures and meshes; this reads every entry into memory, owns the mesh +// buffers its CMeshRefs draw from, and finds entries by path. Rebuilt +// function by function from the 5960 retail XBE; the findings are in +// docs/decomp/XIP.md. Both builds compile this file. The platform seams are +// the block buffer, how textures are made, whether loads run on a thread, +// and the Xbox edge AA swap. + +#include "std.h" +#include "theseus.h" +#include "file_util.h" +#include "xip_archive.h" +#include "asset_loader.h" +#ifndef _XBOX +#include "xbx_texture.h" +#endif + +#ifdef _XBOX +extern BOOL g_bEdgeAntialiasOverride; +extern "C" void WINAPI D3D_FreeNoncontiguousMemory(void* pMemory); +#endif + +static const int XIP_BLOCK = 0x10000; + +#ifdef _XBOX +static const DWORD XIP_LOCK = D3DLOCK_DISCARD | D3DLOCK_NOFLUSH; +#else +static const DWORD XIP_LOCK = D3DLOCK_DISCARD; +#endif + + +// ===== CFileBuffer ========================================================== + +CFileBuffer::CFileBuffer() +{ + m_handle = INVALID_HANDLE_VALUE; + m_buffer = NULL; + m_bufferSize = 0; + m_readIndex = 0; + m_currentBlock = 0; +} + +CFileBuffer::~CFileBuffer() +{ + FreeBuffer(); +} + +// The 5960 version also finds this file's entry in the signed XIPS hash +// section; Theseus loads unsigned archives and keeps only the handle. +void CFileBuffer::SetFile(const TCHAR* Name, HANDLE hFile) +{ + m_handle = hFile; +} + +void CFileBuffer::FreeBuffer() +{ + if (m_buffer == NULL) + return; +#ifdef _XBOX + VirtualFree(m_buffer, 0, MEM_RELEASE); +#else + free(m_buffer); +#endif + m_buffer = NULL; +} + +// The block buffer, from the VirtualAlloc loop 5960 inlines into Seek and +// Read: on failure it frees the texture cache, then the mesh cache, then +// gives up for good (all of which NewFailed does). +static BYTE* AllocBlockBuffer() +{ + for (;;) + { +#ifdef _XBOX + BYTE* pb = (BYTE*)VirtualAlloc(NULL, XIP_BLOCK, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + BYTE* pb = (BYTE*)calloc(1, XIP_BLOCK); +#endif + if (pb != NULL) + return pb; + NewFailed(XIP_BLOCK); + } +} + +// CFileBuffer::Seek(nPos) +// Reconstructed from 5960 retail XBE @ 0x0003d5e0. Ghidra decompile, trimmed: +// block = pos / 0x10000; +// if (!buffer) { buffer = alloc(0x10000); current = -1; } +// if (block != current) { +// SetFilePointer(h, block << 16, 0, FILE_BEGIN); current = block; +// if (!ReadFile(h, buffer, 0x10000, &got) || got == 0) return 0; +// verify block hash; current++; size = got; +// } +// index = pos % 0x10000; return 1; +// current ends up one past the block just read, so seeking back into it reads +// it again. That is the binary's behavior and costs nothing that matters. +bool CFileBuffer::Seek(int nPos) +{ + if (m_buffer == NULL) + { + m_buffer = AllocBlockBuffer(); + m_currentBlock = -1; + } + + const int nBlock = nPos / XIP_BLOCK; + if (nBlock != m_currentBlock) + { + SetFilePointer(m_handle, nBlock * XIP_BLOCK, NULL, FILE_BEGIN); + m_currentBlock = nBlock; + + DWORD cbGot = 0; + if (!ReadFile(m_handle, m_buffer, XIP_BLOCK, &cbGot, NULL) || cbGot == 0) + return false; + + m_currentBlock++; + m_bufferSize = (int)cbGot; + } + + m_readIndex = nPos % XIP_BLOCK; + return true; +} + +// A read the disk refuses. 5960 calls HalReturnToFirmware(4), the fatal error +// reboot, and the Xbox build keeps that; desktop refuses the read instead. +static void XipReadFailed() +{ +#ifdef _XBOX + HalReturnToFirmware(HalFatalErrorRebootRoutine); +#else + TRACE("\001CFileBuffer::Read: the disk refused a XIP read\n"); +#endif +} + +// CFileBuffer::Read(pv, cb) +// Reconstructed from 5960 retail XBE @ 0x0003d710. Ghidra decompile, trimmed: +// if (!buffer) buffer = alloc(0x10000); +// for (;;) { +// if (cb < 1) return total; +// if (index < size) { n = min(size - index, cb); copy n; advance; } +// if (index != size) continue; +// if (!ReadFile(h, buffer, 0x10000, &got)) HalReturnToFirmware(4); +// if (got == 0) return -1; +// verify block hash; current++; size = got; index = 0; +// } +// The buffer refills as soon as it runs dry, even when the copy just finished, +// so a read that ends on the last byte of the file reports -1. +int CFileBuffer::Read(void* pv, int cb) +{ + if (m_buffer == NULL) + m_buffer = AllocBlockBuffer(); + + BYTE* pbOut = (BYTE*)pv; + int cbDone = 0; + + while (cb > 0) + { + if (m_readIndex < m_bufferSize) + { + int n = m_bufferSize - m_readIndex; + if (n > cb) + n = cb; + memcpy(pbOut, m_buffer + m_readIndex, n); + pbOut += n; + cb -= n; + cbDone += n; + m_readIndex += n; + } + + if (m_readIndex == m_bufferSize) + { + DWORD cbGot = 0; + if (!ReadFile(m_handle, m_buffer, XIP_BLOCK, &cbGot, NULL)) + { + XipReadFailed(); + return -1; + } + if (cbGot == 0) + return -1; + + m_currentBlock++; + m_bufferSize = (int)cbGot; + m_readIndex = 0; + } + } + + return cbDone; +} + + +// ===== The archive pool ===================================================== +// Twenty archives, the 5960 pool (c_rgXipFile @ 0x001804e0, 0x150 bytes each; +// the count is at 0x0017e734). Nothing ever leaves it. + +CXipFile c_rgXipFile[20]; +int c_nXipFileCount = 0; + +// 5960 starts loads on a worker thread and runs them inline if no thread +// starts. Desktop always runs them inline: scripts read the scene as soon as +// LoadXIP returns, and a half-built one corrupts their namespace. +static void RunLoader(LPTHREAD_START_ROUTINE pfnLoad, CXipFile* pXip) +{ +#ifdef _XBOX + DWORD idThread; + HANDLE hThread = CreateThread(NULL, 0, pfnLoad, pXip, 0, &idThread); + if (hThread != NULL) + { + CloseHandle(hThread); + return; + } +#endif + pfnLoad(pXip); +} + +// LoadXIP(szURL, bSync, isSkin) +// Reconstructed from 5960 retail XBE @ 0x0003d900. Ghidra decompile, trimmed: +// path = (url[0] && url[1] != ':') ? "Q:/Xips/" + url : url; +// key = CleanFilePath(path); *strrchr(key, '.') = 0; +// for (i < count) if (!_stricmp(key, pool[i].dirPath)) { +// pool[i].cacheTime = now; +// if (loaded && vbCount > 0 && !meshBuffers[0].vb) pool[i].ReloadMeshBuffers(); +// return &pool[i]; +// } +// xip = &pool[count]; if (!xip->Open(path)) return 0; count++; +// if (!bSync) { start the loader thread; return xip; } +// xip->Load(); return xip; +// The key keeps "default" while Open drops it, so default.xip never matches +// itself here; nothing loads it twice. Theseus refuses when the pool is +// full, where 5960 writes past it, and a sync caller learns about a failed +// load. isSkin is Theseus's and unused here. +CXipFile* LoadXIP(const TCHAR* szURL, bool bSync, bool isSkin) +{ + TCHAR szPath[MAX_PATH]; + if (szURL[0] != 0 && szURL[1] != ':') + _tcscpy(szPath, _T("Q:/Xips/")); + else + szPath[0] = 0; + _tcscat(szPath, szURL); + + char szKey[MAX_PATH]; + CleanFilePath(szKey, szPath); + *strrchr(szKey, '.') = 0; + + for (int i = 0; i < c_nXipFileCount; i++) + { + CXipFile* pXip = &c_rgXipFile[i]; + if (_stricmp(szKey, pXip->m_dirPath) != 0) + continue; + + pXip->m_cacheTime = TheseusGetNow(); + if (pXip->m_loaded && pXip->m_vertexBufferCount > 0 && pXip->m_meshBuffers[0].m_vertexBuffer == NULL) + pXip->ReloadMeshBuffers(); + return pXip; + } + + if (c_nXipFileCount >= (int)countof(c_rgXipFile)) + { + TRACE(_T("\001LoadXIP: XIP pool full (%d), cannot load %s\n"), (int)countof(c_rgXipFile), szPath); + return NULL; + } + + CXipFile* pXip = &c_rgXipFile[c_nXipFileCount]; + if (!pXip->Open(szPath)) + return NULL; + c_nXipFileCount++; + + if (!bSync) + { + RunLoader(CXipFile::StartLoadThread, pXip); + return pXip; + } + + // The slot stays spent: a caller that waited on this is about to give up. + if (!pXip->Load()) + { + TRACE(_T("\001LoadXIP: %s failed to load\n"), szPath); + return NULL; + } + return pXip; +} + +// FindInXIPAndDetach(szURL, pbContent, cbContent) +// Reconstructed from 5960 retail XBE @ 0x0003dab0. Ghidra decompile, trimmed: +// path = CleanFilePath(url); +// for (i = count - 1; i >= 0; i--) { +// if (pool[i].locked || !pool[i].loaded) continue; +// n = pool[i].Find(path); +// if (n >= 0 && objects[n]) { +// content = objects[n]; cb = filedata[n].size; objects[n] = 0; return 1; +// } +// } +// return 0; +// The caller owns the bytes from here on; newer archives win. +bool FindInXIPAndDetach(const TCHAR* szURL, BYTE*& pbContent, DWORD& cbContent) +{ + char szPath[MAX_PATH]; + CleanFilePath(szPath, szURL); + + for (int i = c_nXipFileCount - 1; i >= 0; i--) + { + CXipFile& xip = c_rgXipFile[i]; + if (xip.m_locked || !xip.m_loaded) + continue; + + const int n = xip.Find(szPath); + if (n < 0 || xip.m_objects[n] == NULL) + continue; + + pbContent = (BYTE*)xip.m_objects[n]; + cbContent = xip.m_filedata[n].m_size; + xip.m_objects[n] = NULL; + return true; + } + return false; +} + +// A loose texture in the skin folder beats the archive's copy. +static LPDIRECT3DTEXTURE8 LoadSkinTexture(const TCHAR* szFilename) +{ + TCHAR szSkinPath[MAX_PATH]; + _sntprintf(szSkinPath, countof(szSkinPath) - 1, _T("%s%s"), TheseusGetSkinDir(), szFilename); + szSkinPath[countof(szSkinPath) - 1] = 0; + + char szAnsi[MAX_PATH]; + Ansi(szAnsi, szSkinPath, MAX_PATH); + + LPDIRECT3DTEXTURE8 pTex = NULL; +#ifdef _XBOX + if (D3DXCreateTextureFromFileA(TheseusGetD3DDev(), szAnsi, &pTex) != D3D_OK) + return NULL; +#else + // The skin folder is an Xbox path, so it opens through the translator. + FILE* f = TheseusFOpen(szAnsi, "rb"); + if (f == NULL) + return NULL; + fseek(f, 0, SEEK_END); + long cb = ftell(f); + fseek(f, 0, SEEK_SET); + BYTE* pb = cb > 0 ? (BYTE*)malloc(cb) : NULL; + bool bRead = pb != NULL && fread(pb, 1, cb, f) == (size_t)cb; + fclose(f); + HRESULT hr = bRead ? D3DXCreateTextureFromFileInMemory(TheseusGetD3DDev(), pb, (UINT)cb, &pTex) : E_FAIL; + free(pb); + if (hr != D3D_OK) + return NULL; +#endif + return pTex; +} + +// FindObjectInXIP(szURL, szFilename, nType) +// Reconstructed from 5960 retail XBE @ 0x0003dba0. Ghidra decompile, trimmed: +// path = CleanFilePath(url); +// for (i = count - 1; i >= 0; i--) { +// if (pool[i].locked || !pool[i].loaded) continue; +// if (i == 0 && type == 2) { // the default archive packs textures as .xbx +// name = filename; strcpy(wcsrchr(name, '.') + 1, "xbx"); +// path = CleanFilePath(xipDir + name); +// } +// if (obj = pool[i].FindObject(path, type)) return obj; +// } +// return 0; +// The skin folder check in front and the NULL filename guard are Theseus's. +void* FindObjectInXIP(const TCHAR* szURL, const TCHAR* szFilename, int nType) +{ + if (nType == XIP_TYPE_TEXTURE && TheseusGetSkinDir() != NULL && szFilename != NULL) + { + LPDIRECT3DTEXTURE8 pTex = LoadSkinTexture(szFilename); + if (pTex != NULL) + return pTex; + } + + char szPath[MAX_PATH]; + CleanFilePath(szPath, szURL); + + for (int i = c_nXipFileCount - 1; i >= 0; i--) + { + CXipFile& xip = c_rgXipFile[i]; + if (xip.m_locked || !xip.m_loaded) + continue; + + if (i == 0 && nType == XIP_TYPE_TEXTURE && szFilename != NULL) + { + TCHAR szName[MAX_PATH]; + _tcscpy(szName, szFilename); + TCHAR* pchExt = _tcsrchr(szName, '.'); + if (pchExt != NULL) + _tcscpy(pchExt + 1, _T("xbx")); + + TCHAR szFull[MAX_PATH]; + _tcscpy(szFull, TheseusGetXipDir()); + _tcscat(szFull, szName); + CleanFilePath(szPath, szFull); + } + + void* pObject = xip.FindObject(szPath, nType); + if (pObject != NULL) + return pObject; + } + return NULL; +} + + +// ===== CXipFile ============================================================= + +CXipFile::CXipFile() +{ + m_dirPath = NULL; + m_fileName = NULL; + m_filedata = NULL; + m_directory = NULL; + m_names = NULL; + m_objects = NULL; + m_loaded = false; + m_locked = false; + m_reloading = false; + m_cacheTime = 0; + memset(m_meshBuffers, 0, sizeof(m_meshBuffers)); + m_vertexBufferCount = 0; + m_indexBufferCount = 0; +} + +// CXipFile::~CXipFile() +// Reconstructed from 5960 retail XBE @ 0x0003dd20. Ghidra decompile, trimmed: +// delete fileName, dirPath, filedata, directory, names (each then NULL); +// DeleteMeshBuffers(); zero meshBuffers[10], vbCount, ibCount; +// ~CFileBuffer: VirtualFree(buffer); free the XIPS section if held; +// The objects themselves are never freed; scene nodes still point at them. +CXipFile::~CXipFile() +{ + delete[] m_fileName; + m_fileName = NULL; + delete[] m_dirPath; + m_dirPath = NULL; + delete[] m_filedata; + m_filedata = NULL; + delete[] m_directory; + m_directory = NULL; + delete[] m_names; + m_names = NULL; + + DeleteMeshBuffers(); + memset(m_meshBuffers, 0, sizeof(m_meshBuffers)); + m_vertexBufferCount = 0; + m_indexBufferCount = 0; +} + +// CXipFile::Open(szXipFileName) +// Reconstructed from 5960 retail XBE @ 0x0003ddf0. Ghidra decompile, trimmed: +// fileName = copy(name); +// dir = CleanFilePath(name); *strrchr(dir, '.') = 0; +// base = strrchr(dir, '\\'); base = base ? base + 1 : dir; +// if (!_stricmp(base, "default")) { if (base > dir) base--; *base = 0; } +// dirPath = copy(dir); return 1; +// Only names the archive; Load reads it. "Q:\Xips\default.xip" answers for +// "Q:\Xips", so the default archive's entries are found by their folder. +bool CXipFile::Open(const TCHAR* szXipFileName) +{ + m_fileName = new TCHAR[_tcslen(szXipFileName) + 1]; + _tcscpy(m_fileName, szXipFileName); + + char szDir[MAX_PATH]; + CleanFilePath(szDir, szXipFileName); + *strrchr(szDir, '.') = 0; + + char* pszBase = strrchr(szDir, '\\'); + pszBase = pszBase != NULL ? pszBase + 1 : szDir; + if (_stricmp(pszBase, "default") == 0) + { + if (pszBase > szDir) + pszBase--; + *pszBase = 0; + } + + m_dirPath = new char[strlen(szDir) + 1]; + strcpy(m_dirPath, szDir); + return true; +} + +// CXipFile::StartLoadThread(pvContext) +// Reconstructed from 5960 retail XBE @ 0x0003dec0: +// if (xip->reloading) xip->ReloadMeshBuffers(); else xip->Load(); return 0; +DWORD CALLBACK CXipFile::StartLoadThread(LPVOID pvContext) +{ + CXipFile* pXip = (CXipFile*)pvContext; + if (pXip->m_reloading) + pXip->ReloadMeshBuffers(); + else + pXip->Load(); + return 0; +} + +// Read gives back the byte count, or -1 for a short or failed read, so "did +// all of it arrive" has to be asked this way. +static bool XipReadExact(CFileBuffer& file, void* pv, int cb) +{ + return cb >= 0 && file.Read(pv, cb) == cb; +} + +// CreateObjects reads entries back to back without seeking, so one it +// refuses still has to come off the stream or every entry after it is wrong. +static void XipDiscard(CFileBuffer& file, int cb) +{ + BYTE scratch[4096]; + while (cb > 0) + { + const int n = cb < (int)sizeof(scratch) ? cb : (int)sizeof(scratch); + if (file.Read(scratch, n) != n) + return; + cb -= n; + } +} + +// Every real archive ends exactly where dataStart + dataSize says. +static bool XipSizeMatches(CFileBuffer& file, const XIPHEADER& hdr) +{ + const DWORD cbFile = GetFileSize(file.m_handle, NULL); + return cbFile != (DWORD)-1 && cbFile == hdr.m_dataStart + hdr.m_dataSize; +} + +// An archive that fails to load is never read again. +static void XipClose(CFileBuffer& file) +{ + if (file.m_handle != INVALID_HANDLE_VALUE) + { + CloseHandle(file.m_handle); + file.m_handle = INVALID_HANDLE_VALUE; + } +} + +// CXipFile::Load() +// Reconstructed from 5960 retail XBE @ 0x0003def0. Ghidra decompile, trimmed: +// path = CleanFilePath(fileName); if (path starts "y:") path[0] = 'x'; +// h = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, +// NO_BUFFERING | SEQUENTIAL_SCAN | NORMAL); +// if (h == -1) return 0; +// file.SetFile(fileName, h); +// if (!read(header, 0x10) || magic != 'XIP0') return 0; +// filedata = new(fileCount * 16); read; directory = new(nameCount * 4); read; +// cbNames = dataStart - 16 - fileCount * 16 - nameCount * 4; +// names = new(cbNames); read; +// CreateObjects(); loaded = 1; return 1; +// Theseus opens through its own path layer and leaves the y: to x: swap out. +// Every read must be whole, the header has to describe this exact file, and +// a failed load closes its handle. +bool CXipFile::Load() +{ + HANDLE hFile = TheseusCreateFile(m_fileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN | FILE_FLAG_NO_BUFFERING, NULL); + if (hFile == INVALID_HANDLE_VALUE) + { + TRACE(_T("\001CXipFile::Load: can't open %s (%d)\n"), m_fileName, (int)GetLastError()); + return false; + } + + m_file.SetFile(m_fileName, hFile); + + if (!XipReadExact(m_file, &m_header, sizeof(XIPHEADER)) || m_header.m_magic != XIP_MAGIC) + { + XipClose(m_file); + return false; + } + + // The tables sit between the header and the data, and the name pool is + // what's left. A bad dataStart makes that zero or less. + const int cbFiledata = m_header.m_fileCount * (int)sizeof(FILEDATA); + const int cbDirectory = m_header.m_nameCount * (int)sizeof(FILENAME); + const int cbNames = (int)m_header.m_dataStart - (int)sizeof(XIPHEADER) - cbFiledata - cbDirectory; + if (cbNames <= 0 || !XipSizeMatches(m_file, m_header)) + { + TRACE(_T("\001CXipFile::Load: %s header does not describe this file\n"), m_fileName); + XipClose(m_file); + return false; + } + + m_filedata = new FILEDATA[m_header.m_fileCount]; + m_directory = new FILENAME[m_header.m_nameCount]; + m_names = new char[cbNames]; + if (!XipReadExact(m_file, m_filedata, cbFiledata) + || !XipReadExact(m_file, m_directory, cbDirectory) + || !XipReadExact(m_file, m_names, cbNames)) + { + XipClose(m_file); + return false; + } + + CreateObjects(); + m_loaded = true; + return true; +} + +#ifdef _XBOX +// An XPR0 texture as FUN_0003e030 in the 5960 XBE builds it. Ghidra +// decompile, trimmed (xpr is the entry's bytes): +// if (xpr->magic != 'XPR0') return 0; +// tex = AllocNoncontiguous(0x14); copy the 0x14 byte resource from xpr + 0xc; +// bits = AllocContiguous(total - headerSize, 0x80); copy from xpr + headerSize; +// tex->Data = 0; Register(bits); tex->Common |= D3DCOMMON_D3DCREATED; +// The binary never frees the entry's bytes and leaks the header when the +// pixels don't fit; this frees both. The size checks are Theseus's: every +// offset the header gives has to land inside the entry. +static LPDIRECT3DTEXTURE8 TextureFromXPR(const BYTE* pbEntry, int cbEntry) +{ + const XPR_HEADER* pxpr = (const XPR_HEADER*)pbEntry; + if (pxpr->dwMagic != XPR_MAGIC_VALUE) + return NULL; + + if (pxpr->dwHeaderSize < sizeof(XPR_HEADER) + || pxpr->dwTotalSize < pxpr->dwHeaderSize + || pxpr->dwTotalSize > (DWORD)cbEntry) + { + TRACE(_T("\001TextureFromXPR: header describes %d..%d of %d bytes\n"), + (int)pxpr->dwHeaderSize, (int)pxpr->dwTotalSize, cbEntry); + return NULL; + } + + const int cbBits = (int)(pxpr->dwTotalSize - pxpr->dwHeaderSize); + if (cbBits <= 0) + return NULL; + + IDirect3DTexture8* pTex = (IDirect3DTexture8*)TheseusD3D_AllocNoncontiguousMemory(sizeof(IDirect3DTexture8)); + if (pTex == NULL) + return NULL; + memcpy(pTex, pbEntry + sizeof(XPR_HEADER), sizeof(IDirect3DTexture8)); + + BYTE* pbBits = (BYTE*)TheseusD3D_AllocContiguousMemory(cbBits, D3DTEXTURE_ALIGNMENT); + if (pbBits == NULL) + { + D3D_FreeNoncontiguousMemory(pTex); + return NULL; + } + memcpy(pbBits, pbEntry + pxpr->dwHeaderSize, cbBits); + + pTex->Data = 0; + pTex->Register(pbBits); + pTex->Common |= D3DCOMMON_D3DCREATED; + return pTex; +} +#endif + +// A texture entry. The Xbox build hands the XPR straight to the GPU; desktop +// decodes it in xbx_texture.h. The minimum size is Theseus's. +static LPDIRECT3DTEXTURE8 ReadTexture(CFileBuffer& file, int cbEntry) +{ +#ifdef _XBOX + if (cbEntry < (int)(sizeof(XPR_HEADER) + sizeof(IDirect3DTexture8))) + { + TRACE(_T("\001ReadTexture: entry too small (%d bytes)\n"), cbEntry); + XipDiscard(file, cbEntry); + return NULL; + } +#endif + + BYTE* pbEntry = new BYTE[cbEntry]; +#ifdef _XBOX + if (!XipReadExact(file, pbEntry, cbEntry)) + { + delete[] pbEntry; + return NULL; + } + LPDIRECT3DTEXTURE8 pTex = TextureFromXPR(pbEntry, cbEntry); +#else + file.Read(pbEntry, cbEntry); + LPDIRECT3DTEXTURE8 pTex = XBX_ParseTexture(pbEntry, cbEntry); +#endif + delete[] pbEntry; + + if (pTex == NULL) + TRACE(_T("\001ReadTexture: could not use a %d byte texture\n"), cbEntry); + return pTex; +} + +// CXipFile::CreateObjects() +// Reconstructed from 5960 retail XBE @ 0x0003e110. Ghidra decompile, trimmed: +// locked = 1; objects = new(fileCount * 4) zeroed; +// for (i < fileCount) switch (filedata[i].type) { +// case 1: break; +// case 2: objects[i] = texture from the next size bytes; break; +// case 4: r = new CMeshRef; r->xip = this; r->buffer = offset >> 24; +// r->first = offset & 0xffffff; r->count = size; objects[i] = r; break; +// case 5: ReadIndexBuffer(i, vbCount); ibCount++; break; +// case 6: ReadVertexBuffer(i, vbCount); vbCount++; break; +// default: objects[i] = XAppAllocMemory(size); read size bytes; break; +// } +// locked = 0; +// Entries sit in file order, so this reads straight through. An index buffer +// goes in the slot of the vertex buffer that follows it. The range checks and +// skips are Theseus's; the extra terminator byte on raw entries is for desktop, +// whose allocator doesn't hand back zeroed pages. +void CXipFile::CreateObjects() +{ + m_locked = true; + + const int nFiles = m_header.m_fileCount; + m_objects = new void*[nFiles]; + memset(m_objects, 0, nFiles * sizeof(void*)); + + for (int i = 0; i < nFiles; i++) + { + const FILEDATA& fd = m_filedata[i]; + switch (fd.m_type) + { + case XIP_TYPE_MESH: + break; + + case XIP_TYPE_TEXTURE: + m_objects[i] = ReadTexture(m_file, fd.m_size); + break; + + case XIP_TYPE_MESH_REFERENCE: + { + // The top byte can name any of 256 slots, more than an archive has. + const UINT nBuffer = fd.m_dataOffset >> 24; + if (nBuffer >= MAX_MESHBUFFER) + { + TRACE(_T("\001CreateObjects: mesh ref %d out of range in %s\n"), (int)nBuffer, m_fileName); + break; + } + CMeshRef* pRef = new CMeshRef; + pRef->m_xipFile = this; + pRef->m_meshBufferIndex = nBuffer; + pRef->m_firstIndex = fd.m_dataOffset & 0x00ffffff; + pRef->m_primitiveCount = fd.m_size; + m_objects[i] = pRef; + break; + } + + case XIP_TYPE_INDEXBUFFER: + // It lands in the vertex buffer's slot, so that count has to fit too. + if (m_indexBufferCount >= MAX_MESHBUFFER || m_vertexBufferCount >= MAX_MESHBUFFER) + { + TRACE(_T("\001CreateObjects: too many index buffers in %s\n"), m_fileName); + XipDiscard(m_file, fd.m_size); + break; + } + ReadIndexBuffer(i, m_vertexBufferCount); + m_indexBufferCount++; + break; + + case XIP_TYPE_VERTEXBUFFER: + if (m_vertexBufferCount >= MAX_MESHBUFFER) + { + TRACE(_T("\001CreateObjects: too many vertex buffers in %s\n"), m_fileName); + XipDiscard(m_file, fd.m_size); + break; + } + ReadVertexBuffer(i, m_vertexBufferCount); + m_vertexBufferCount++; + break; + + default: + { +#ifdef _XBOX + BYTE* pb = (BYTE*)TheseusAllocMemory(fd.m_size); + m_file.Read(pb, fd.m_size); +#else + BYTE* pb = (BYTE*)TheseusAllocMemory(fd.m_size + 1); + m_file.Read(pb, fd.m_size); + pb[fd.m_size] = 0; +#endif + m_objects[i] = pb; + break; + } + } + } + + m_locked = false; +} + +// bsearch hands the compare the key first and a directory entry second. +struct XipSearchKey +{ + const char* m_names; + const char* m_name; +}; + +static int __cdecl XipCompareName(const void* pvKey, const void* pvEntry) +{ + const XipSearchKey* pKey = (const XipSearchKey*)pvKey; + const FILENAME* pEntry = (const FILENAME*)pvEntry; + return _stricmp(pKey->m_name, pKey->m_names + pEntry->m_nameOffset); +} + +// CXipFile::Find(szURL) +// Reconstructed from 5960 retail XBE @ 0x0003e290 (compare @ 0x0003e270). +// Ghidra decompile, trimmed: +// n = strlen(dirPath); +// if (_strnicmp(url, dirPath, n) || url[n] != '\\') return -1; +// key = { names, url + n + 1 }; +// hit = bsearch(&key, directory, nameCount, 4, compare); // _stricmp +// return hit ? hit->fileDataIndex : -1; +int CXipFile::Find(const char* szURL) +{ + const size_t cchDir = strlen(m_dirPath); + if (_strnicmp(szURL, m_dirPath, cchDir) != 0 || szURL[cchDir] != '\\') + return -1; + + XipSearchKey key; + key.m_names = m_names; + key.m_name = szURL + cchDir + 1; + + const FILENAME* pHit = (const FILENAME*)bsearch(&key, m_directory, m_header.m_nameCount, + sizeof(FILENAME), XipCompareName); + return pHit != NULL ? pHit->m_fileDataIndex : -1; +} + +// CXipFile::FindObject(szURL, nType) +// Reconstructed from 5960 retail XBE @ 0x0003e320. Ghidra decompile, trimmed: +// n = Find(url); +// if (n == -1 || (type != -1 && filedata[n].type != type)) return 0; +// if (type == 2) objects[n]->AddRef(); // inlined D3DResource_AddRef +// return objects[n]; +// A texture handed out is the caller's reference. +void* CXipFile::FindObject(const char* szURL, int nType) +{ + const int n = Find(szURL); + if (n == -1) + return NULL; + if (nType != -1 && m_filedata[n].m_type != (DWORD)nType) + return NULL; + + if (nType == XIP_TYPE_TEXTURE) + ((LPDIRECT3DTEXTURE8)m_objects[n])->AddRef(); + return m_objects[n]; +} + + +// ===== Mesh buffers ========================================================= + +// CXipFile::ReadIndexBuffer(nFileIndex, nBuffer) +// Reconstructed from 5960 retail XBE @ 0x0003e5f0. Ghidra decompile, trimmed: +// buf = &meshBuffers[nBuffer]; +// buf->ib = new index buffer of filedata[n].size bytes (0x0003d390); +// read filedata[n].size bytes into buf->ib's data; +// buf->indexCount = filedata[n].size >> 1; +// The binary builds the resource by hand and writes into it; Theseus goes +// through the device so desktop gets a bgfx buffer, and locks to write. +void CXipFile::ReadIndexBuffer(int nFileIndex, int nBuffer) +{ + CMeshBuffer& buf = m_meshBuffers[nBuffer]; + const int cb = m_filedata[nFileIndex].m_size; + + TheseusCreateIndexBuffer(cb, D3DUSAGE_DYNAMIC, D3DFMT_INDEX16, D3DPOOL_DEFAULT, &buf.m_indexBuffer); + + BYTE* pb; + buf.m_indexBuffer->Lock(0, cb, &pb, XIP_LOCK); + m_file.Read(pb, cb); + buf.m_indexBuffer->Unlock(); + + buf.m_indexCount = cb / (int)sizeof(WORD); +} + +// CXipFile::ReadVertexBuffer(nFileIndex, nBuffer) +// Reconstructed from 5960 retail XBE @ 0x0003e650. Ghidra decompile, trimmed: +// buf = &meshBuffers[nBuffer]; +// read count (4); read fvf (4); +// buf->stride = (filedata[n].size - 8) / count; +// buf->vb = new vertex buffer (0x0003d410); +// wait for the GPU if it still reads buf->vb; +// read size - 8 bytes into buf->vb's data; +// buf->fvf = fvf; buf->vertexCount = count; +// Same device route as the index buffer. The zero count and short entry +// refusal is Theseus's: the binary divides by the count unchecked. +void CXipFile::ReadVertexBuffer(int nFileIndex, int nBuffer) +{ + CMeshBuffer& buf = m_meshBuffers[nBuffer]; + + int nVerts; + DWORD fvf; + m_file.Read(&nVerts, sizeof(nVerts)); + m_file.Read(&fvf, sizeof(fvf)); + + const int cbVerts = (int)m_filedata[nFileIndex].m_size - 8; +#ifdef _XBOX + if (nVerts <= 0 || cbVerts <= 0) + { + TRACE(_T("\001ReadVertexBuffer: bad vertex count %d in %s\n"), nVerts, m_fileName); + XipDiscard(m_file, cbVerts > 0 ? cbVerts : 0); + return; + } +#else + if (nVerts <= 0 || cbVerts < 0) + { + TRACE("\001ReadVertexBuffer: bad vertex count %d\n", nVerts); + return; + } +#endif + buf.m_vertexStride = cbVerts / nVerts; + + TheseusCreateVertexBuffer(cbVerts, D3DUSAGE_DYNAMIC, fvf, D3DPOOL_DEFAULT, &buf.m_vertexBuffer); + + BYTE* pb; + buf.m_vertexBuffer->Lock(0, 0, &pb, XIP_LOCK); + m_file.Read(pb, cbVerts); + buf.m_vertexBuffer->Unlock(); + + buf.m_fvf = fvf; + buf.m_vertexCount = nVerts; +} + +// CXipFile::DeleteMeshBuffers() +// Reconstructed from 5960 retail XBE @ 0x0003e390. Ghidra decompile, trimmed: +// for (i < 10) { +// if (meshBuffers[i].vb) { meshBuffers[i].vb->Release(); meshBuffers[i].vb = 0; } +// if (meshBuffers[i].ib) { meshBuffers[i].ib->Release(); meshBuffers[i].ib = 0; } +// } +// Release is inlined in the binary. The counts stay, which is how +// IsUnloaded tells an evicted archive from one with no meshes. +void CXipFile::DeleteMeshBuffers() +{ + for (int i = 0; i < MAX_MESHBUFFER; i++) + { + CMeshBuffer& buf = m_meshBuffers[i]; + if (buf.m_vertexBuffer != NULL) + { + buf.m_vertexBuffer->Release(); + buf.m_vertexBuffer = NULL; + } + if (buf.m_indexBuffer != NULL) + { + buf.m_indexBuffer->Release(); + buf.m_indexBuffer = NULL; + } + } +} + +// CleanupMeshCache() +// Reconstructed from 5960 retail XBE @ 0x0003e440. Ghidra decompile, trimmed +// (the binary unrolls the loop four times): +// oldest = 0; +// for (i < count) { +// x = &pool[i]; +// if (x->locked || !x->loaded || !x->vbCount || !x->meshBuffers[0].vb) continue; +// if (!oldest || x->cacheTime < oldest->cacheTime) oldest = x; +// } +// if (!oldest) return 0; oldest->DeleteMeshBuffers(); return 1; +// The out of memory path calls this to free the archive drawn longest ago. +bool CleanupMeshCache() +{ + CXipFile* pOldest = NULL; + for (int i = 0; i < c_nXipFileCount; i++) + { + CXipFile* pXip = &c_rgXipFile[i]; + if (pXip->m_locked || !pXip->m_loaded) + continue; + if (pXip->m_vertexBufferCount == 0 || pXip->m_meshBuffers[0].m_vertexBuffer == NULL) + continue; + if (pOldest == NULL || pXip->m_cacheTime < pOldest->m_cacheTime) + pOldest = pXip; + } + + if (pOldest == NULL) + return false; + pOldest->DeleteMeshBuffers(); + return true; +} + +// CXipFile::ReloadMeshBuffers() +// Reconstructed from 5960 retail XBE @ 0x0003e710. Ghidra decompile, trimmed: +// locked = 1; ib = 0; vb = 0; +// for (i < fileCount) { +// if (type == 5) { file.Seek(dataStart + offset); ReadIndexBuffer(i, ib++); } +// else if (type == 6) { file.Seek(dataStart + offset); ReadVertexBuffer(i, vb++); } +// } +// locked = 0; reloading = 0; +// Brings back what CleanupMeshCache freed, seeking to each buffer since the +// rest of the archive is still in memory. The CHECKs are Theseus's: an entry +// CreateObjects refused has no slot to come back to. +void CXipFile::ReloadMeshBuffers() +{ + m_locked = true; + + int nIndexBuffer = 0; + int nVertexBuffer = 0; + for (int i = 0; i < m_header.m_fileCount; i++) + { + const FILEDATA& fd = m_filedata[i]; + if (fd.m_type == XIP_TYPE_INDEXBUFFER) + { + if (!CHECK(nIndexBuffer < m_indexBufferCount)) + continue; + m_file.Seek(m_header.m_dataStart + fd.m_dataOffset); + ReadIndexBuffer(i, nIndexBuffer++); + } + else if (fd.m_type == XIP_TYPE_VERTEXBUFFER) + { + if (!CHECK(nVertexBuffer < m_vertexBufferCount)) + continue; + m_file.Seek(m_header.m_dataStart + fd.m_dataOffset); + ReadVertexBuffer(i, nVertexBuffer++); + } + } + + m_locked = false; + m_reloading = false; +} + +// CXipFile::Reload() +// Reconstructed from 5960 retail XBE @ 0x0003e800. Ghidra decompile: +// reloading = 1; +// if (h = CreateThread(0, 0, StartLoadThread, this, 0, &id)) { CloseHandle(h); return; } +// if (reloading) ReloadMeshBuffers(); else Load(); +// The fallback is StartLoadThread inlined. Desktop reloads inline. +void CXipFile::Reload() +{ + m_reloading = true; + RunLoader(StartLoadThread, this); +} + +// Both inlined in 5960: CInline_Render @ 0x0004e5d0 tests +// loaded && !reloading && vbCount != 0 && meshBuffers[0].vb == 0 +// to decide its archive was evicted, and CInline_Advance reads reloading. +bool CXipFile::IsUnloaded() const +{ + return m_loaded && !m_reloading && m_vertexBufferCount != 0 && m_meshBuffers[0].m_vertexBuffer == NULL; +} + +bool CXipFile::IsReloading() const +{ + return m_reloading; +} + + +// ===== CMeshRef ============================================================= + +// CMeshRef::Render(bSetFVF) +// Reconstructed from 5960 retail XBE @ 0x0003e850 (CMeshRef vtable @ 0x00024dbc, +// slot 0). Disassembly, summarized: +// xip->cacheTime = now; buf = &xip->meshBuffers[index]; +// if (xip->reloading) return; +// if (!buf->vb) { xip->Reload(); return; } +// if (bSetFVF) SetVertexShader(GetFixedFunctionShader(buf->fvf)); +// if (count > 800 && !g_bEdgeAntialiasOverride) { EDGEANTIALIAS = 0; MULTISAMPLEANTIALIAS = 1; } +// if (g_uMeshRef++ == 0) set the effect shader matrices for this frame; +// SetStreamSource(0, buf->vb, buf->stride); SetIndices(buf->ib, 0); +// DrawIndexedVertices(TRIANGLELIST, count * 3, indices + first); +// Theseus sets those shader matrices on every world change in shape_render +// instead (SetFalloffShaderFrameValues), and skips the AA swap on desktop +// where the shim has no edge AA. The index guard and the draw counter are +// Theseus's. +void CMeshRef::Render(bool bSetFVF) +{ + if (m_meshBufferIndex >= m_xipFile->m_vertexBufferCount) + return; + + m_xipFile->m_cacheTime = TheseusGetNow(); + CMeshBuffer& buf = m_xipFile->m_meshBuffers[m_meshBufferIndex]; + + if (m_xipFile->IsReloading()) + return; + if (buf.m_vertexBuffer == NULL) + { + m_xipFile->Reload(); + return; + } + + if (bSetFVF) + TheseusSetVertexShader(GetFixedFunctionShader(buf.m_fvf)); + +#ifdef _XBOX + if (m_primitiveCount > 800 && !g_bEdgeAntialiasOverride) + { + TheseusSetRenderState(D3DRS_EDGEANTIALIAS, FALSE); + TheseusSetRenderState(D3DRS_MULTISAMPLEANTIALIAS, TRUE); + } +#endif + + TheseusSetStreamSource(0, buf.m_vertexBuffer, buf.m_vertexStride); + TheseusSetIndices(buf.m_indexBuffer, 0); + Frame_Count(FC_DRAW_SCENE); + TheseusDrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, buf.m_vertexCount, m_firstIndex, m_primitiveCount); +} + +// CMeshRef::GetFVF() +// Reconstructed from 5960 retail XBE @ 0x0003ead0 (vtable slot 1): +// return xip->meshBuffers[index].fvf; +// The index guard is Theseus's. +DWORD CMeshRef::GetFVF() const +{ + if (m_meshBufferIndex >= m_xipFile->m_vertexBufferCount) + return 0; + return m_xipFile->m_meshBuffers[m_meshBufferIndex].m_fvf; +} diff --git a/theseus/xbox/xip_archive.cpp b/theseus/xbox/xip_archive.cpp deleted file mode 100644 index 4408828..0000000 --- a/theseus/xbox/xip_archive.cpp +++ /dev/null @@ -1,941 +0,0 @@ -// xip_archive.cpp: XIP archive loader. Reads dashboard asset bundles -// (.xap scripts, .xbx textures, .xm meshes) into memory; supports -// XOR-key-decoded headers and per-entry decompression. Decompiled -// from the 5960 retail XBE; see docs/decomp/XIP.md. -#include "std.h" -#include "theseus.h" -#include "file_util.h" -#include "xip_archive.h" -#include "asset_loader.h" -#include "node.h" -#include "camera.h" - -extern CCamera theCamera; -extern UINT g_uMeshRef; -extern D3DXMATRIX g_matProjection; -extern BOOL g_bEdgeAntialiasOverride; -extern "C" void WINAPI D3D_FreeNoncontiguousMemory(void* pMemory); - -// ========================================================================= -// Constants -// ========================================================================= - -static const int FILE_BUF_SIZE = 65536; // 64KB read chunks - -// ========================================================================= -// File protection: halts on tampered XIP in retail, breaks in debug. -// ========================================================================= - -static void FileProtectionError() -{ -#ifdef _DEBUG - ALERT(_T("XIP File Protection Error")); - __asm int 3; -#else - HalReturnToFirmware(HalFatalErrorRebootRoutine); -#endif -} - -// ========================================================================= -// CFileBuffer: buffered 64KB block reader. -// ========================================================================= - -CFileBuffer::CFileBuffer() -{ - m_handle = INVALID_HANDLE_VALUE; - m_buffer = NULL; - m_bufferSize = 0; - m_readIndex = 0; - m_currentBlock = 0; -} - -CFileBuffer::~CFileBuffer() -{ - FreeBuffer(); -} - -void CFileBuffer::SetFile(PCTSTR Name, HANDLE hFile) -{ - ASSERT(m_handle == INVALID_HANDLE_VALUE); - m_handle = hFile; -} - -void CFileBuffer::FreeBuffer() -{ - if (m_buffer != NULL) - { - VERIFY(VirtualFree(m_buffer, 0, MEM_RELEASE)); - m_buffer = NULL; - } -} - -// Allocate the read buffer, retrying on failure via NewFailed -static BYTE* AllocReadBuffer() -{ - for (;;) - { - BYTE* pb = (BYTE*)VirtualAlloc(NULL, FILE_BUF_SIZE, - MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); - if (pb) return pb; - NewFailed(FILE_BUF_SIZE); - } -} - -bool CFileBuffer::Seek(int nPos) -{ - int nBlock = nPos / FILE_BUF_SIZE; - - if (m_buffer == NULL) - { - m_buffer = AllocReadBuffer(); - m_currentBlock = -1; - } - - if (nBlock != m_currentBlock) - { - VERIFY(SetFilePointer(m_handle, nBlock * FILE_BUF_SIZE, NULL, FILE_BEGIN) != ~0); - m_currentBlock = nBlock; - - DWORD dwRead; - if (!ReadFile(m_handle, m_buffer, FILE_BUF_SIZE, &dwRead, NULL) || dwRead == 0) - return false; - - ++m_currentBlock; - m_bufferSize = (int)dwRead; - } - - m_readIndex = nPos % FILE_BUF_SIZE; - return true; -} - -int CFileBuffer::Read(void* pv, int cb) -{ - if (m_buffer == NULL) - m_buffer = AllocReadBuffer(); - - int cbTotal = 0; - BYTE* pb = (BYTE*)pv; - - while (cb > 0) - { - // Copy from current buffer - if (m_readIndex < m_bufferSize) - { - int n = m_bufferSize - m_readIndex; - if (n > cb) n = cb; - - CopyMemory(pb, m_buffer + m_readIndex, n); - pb += n; - cb -= n; - ASSERT(cb >= 0); - m_readIndex += n; - cbTotal += n; - } - - // Refill buffer when exhausted - if (m_readIndex == m_bufferSize) - { - DWORD dwRead = 0; - if (!ReadFile(m_handle, m_buffer, FILE_BUF_SIZE, &dwRead, NULL)) - { - ASSERT(FALSE && "Unable to read from XIP!"); - FileProtectionError(); - return -1; - } - - if (dwRead == 0) - return -1; - - ++m_currentBlock; - m_bufferSize = (int)dwRead; - m_readIndex = 0; - } - } - - return cbTotal; -} - -// ========================================================================= -// XIP file pool: up to 20 simultaneously loaded archives. -// ========================================================================= - -CXipFile c_rgXipFile[20]; -int c_nXipFileCount = 0; - -// ========================================================================= -// LoadXIP: load or find a cached XIP archive. -// ========================================================================= - -CXipFile* LoadXIP(const TCHAR* szURL, bool bSync, bool isSkin) -{ - TCHAR szBuf[MAX_PATH]; - - // Build full path: relative URLs get Q:/Xips/ prefix - if (szURL[0] && szURL[1] != ':') - _tcscpy(szBuf, _T("Q:/Xips/")); - else - szBuf[0] = 0; - - _tcscat(szBuf, szURL); - - // Check if already loaded (match by directory path) - { - char szDirPath[MAX_PATH]; - CleanFilePath(szDirPath, szBuf); - char* pch = strrchr(szDirPath, '.'); - ASSERT(pch != NULL); - *pch = 0; - - for (int i = 0; i < c_nXipFileCount; i++) - { - if (_stricmp(szDirPath, c_rgXipFile[i].m_dirPath) == 0) - { - c_rgXipFile[i].m_cacheTime = TheseusGetNow(); - - // Reload mesh buffers if they were evicted - if (c_rgXipFile[i].m_loaded && - c_rgXipFile[i].m_vertexBufferCount > 0 && - c_rgXipFile[i].m_meshBuffers[0].m_vertexBuffer == NULL) - { - c_rgXipFile[i].ReloadMeshBuffers(); - } - - return &c_rgXipFile[i]; - } - } - } - - if (c_nXipFileCount >= (int)countof(c_rgXipFile)) - { - TRACE(_T("\001LoadXIP: XIP pool full (%d), cannot load %s\n"), - (int)countof(c_rgXipFile), szBuf); - return NULL; - } - - CXipFile* pXip = &c_rgXipFile[c_nXipFileCount]; - - if (!pXip->Open(szBuf)) - return NULL; - - c_nXipFileCount++; - - // Load synchronously or spawn a background thread - if (bSync) - { - // A caller that waited for the load wants to know it failed. The slot - // stays spent: whoever asked for a sync XIP is about to give up anyway. - if (!pXip->Load()) - { - TRACE(_T("\001LoadXIP: %s failed to load\n"), szBuf); - return NULL; - } - } - else - { - DWORD dwThreadID; - HANDLE hThread = CreateThread(NULL, 0, CXipFile::StartLoadThread, pXip, 0, &dwThreadID); - if (hThread) - CloseHandle(hThread); - else - CXipFile::StartLoadThread(pXip); - } - - return pXip; -} - -// ========================================================================= -// FindInXIPAndDetach: find content and detach ownership from the archive. -// ========================================================================= - -bool FindInXIPAndDetach(const TCHAR* szURL, BYTE*& pbContent, DWORD& cbContent) -{ - char szFilePath[MAX_PATH]; - CleanFilePath(szFilePath, szURL); - - // Search newest to oldest - for (int i = c_nXipFileCount - 1; i >= 0; i--) - { - if (c_rgXipFile[i].m_locked || !c_rgXipFile[i].m_loaded) - continue; - - int idx = c_rgXipFile[i].Find(szFilePath); - if (idx >= 0 && c_rgXipFile[i].m_objects[idx] != NULL) - { - pbContent = (BYTE*)c_rgXipFile[i].m_objects[idx]; - cbContent = c_rgXipFile[i].m_filedata[idx].m_size; - c_rgXipFile[i].m_objects[idx] = NULL; // Transfer ownership - return true; - } - } - - return false; -} - -// ========================================================================= -// FindObjectInXIP: find an object by path, with skin override for textures -// ========================================================================= - -void* FindObjectInXIP(const TCHAR* szURL, const TCHAR* szFilename, int nType) -{ - char szFilePath[MAX_PATH]; - CleanFilePath(szFilePath, szURL); - - // Skin directory override: check for loose texture files first - if (nType == XIP_TYPE_TEXTURE && TheseusGetSkinDir() != NULL && szFilename != NULL) - { - // Skin dir plus an archive-supplied filename, either of which can be - // long enough on its own. - TCHAR szSkinPath[MAX_PATH]; - _sntprintf(szSkinPath, countof(szSkinPath) - 1, _T("%s%s"), - TheseusGetSkinDir(), szFilename); - szSkinPath[countof(szSkinPath) - 1] = 0; - - char ansiPath[MAX_PATH]; - Ansi(ansiPath, szSkinPath, MAX_PATH); - - LPDIRECT3DTEXTURE8 lpTex = NULL; - if (D3DXCreateTextureFromFileA(TheseusGetD3DDev(), ansiPath, &lpTex) == D3D_OK) - return lpTex; - } - - // Search loaded archives newest to oldest - for (int i = c_nXipFileCount - 1; i >= 0; i--) - { - if (c_rgXipFile[i].m_locked || !c_rgXipFile[i].m_loaded) - continue; - - // Default XIP (index 0): remap texture extension to .xbx - if (i == 0 && nType == XIP_TYPE_TEXTURE && szFilename != NULL) - { - TCHAR szRemapped[MAX_PATH]; - _tcscpy(szRemapped, szFilename); - TCHAR* pDot = _tcsrchr(szRemapped, '.'); - if (pDot != NULL) - _tcscpy(pDot + 1, _T("xbx")); - - TCHAR szFull[MAX_PATH]; - _tcscpy(szFull, TheseusGetXipDir()); - _tcscat(szFull, szRemapped); - CleanFilePath(szFilePath, szFull); - } - - void* pObj = c_rgXipFile[i].FindObject(szFilePath, nType); - if (pObj != NULL) - return pObj; - } - - return NULL; -} - -// ========================================================================= -// CXipFile -// ========================================================================= - -CXipFile::CXipFile() -{ - m_fileName = NULL; - m_dirPath = NULL; - m_loaded = false; - m_locked = false; - m_reloading = false; - m_vertexBufferCount = 0; - m_indexBufferCount = 0; - ZeroMemory(m_meshBuffers, sizeof(m_meshBuffers)); -} - -CXipFile::~CXipFile() -{ - delete[] m_fileName; - delete[] m_dirPath; - delete[] m_filedata; - delete[] m_directory; - delete[] m_names; - DeleteMeshBuffers(); -} - -bool CXipFile::Open(const TCHAR* szXipFileName) -{ - // Store the XIP filename - m_fileName = new TCHAR[_tcslen(szXipFileName) + 1]; - _tcscpy(m_fileName, szXipFileName); - - // Derive the directory path (used as cache key) - // Strip extension, and if the base name is "default", use the parent dir - char szDir[MAX_PATH]; - CleanFilePath(szDir, szXipFileName); - - char* pDot = strrchr(szDir, '.'); - ASSERT(pDot != NULL); - *pDot = 0; - - char* pSlash = strrchr(szDir, '\\'); - char* pBase = (pSlash != NULL) ? pSlash + 1 : szDir; - - if (_stricmp(pBase, "default") == 0) - { - if (pBase > szDir) - pBase--; - *pBase = 0; - } - - m_dirPath = new char[strlen(szDir) + 1]; - strcpy(m_dirPath, szDir); - - return true; -} - -DWORD CALLBACK CXipFile::StartLoadThread(LPVOID pvContext) -{ - START_PROFILE(); - CXipFile* pThis = (CXipFile*)pvContext; - - if (pThis->m_reloading) - pThis->ReloadMeshBuffers(); - else - VERIFY(pThis->Load()); - - END_PROFILE(); - return 0; -} - -// Read returns the byte count, or -1 when the file is short or unreadable. -// Callers want "did I get all of it", so ask that question directly. -static bool ReadExact(CFileBuffer& file, void* pv, int cb) -{ - return cb >= 0 && file.Read(pv, cb) == cb; -} - -// CreateObjects walks the data section in order with no seeking, so anything -// we decline to store still has to come off the stream or every entry after -// it reads from the wrong place. -static void DiscardBytes(CFileBuffer& file, int cb) -{ - BYTE rgb[512]; - while (cb > 0) - { - int n = cb < (int)sizeof(rgb) ? cb : (int)sizeof(rgb); - if (file.Read(rgb, n) != n) - return; - cb -= n; - } -} - -// A XIP that fails to load is never read again, so let go of its handle. -static void CloseXipHandle(CFileBuffer& file) -{ - if (file.m_handle != INVALID_HANDLE_VALUE) - { - CloseHandle(file.m_handle); - file.m_handle = INVALID_HANDLE_VALUE; - } -} - -bool CXipFile::Load() -{ -#ifdef _DEBUG - TCHAR szDirPath [MAX_PATH]; - Unicode(szDirPath, m_dirPath, MAX_PATH); - DWORD ticks = GetTickCount (); -#endif - - HANDLE hFile = TheseusCreateFile(m_fileName, GENERIC_READ, FILE_SHARE_READ, - NULL, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN | FILE_FLAG_NO_BUFFERING, - NULL); - - if (hFile == INVALID_HANDLE_VALUE) - { - TRACE(_T("\001CXipFile::Open (%s) failed %d\n"), m_fileName, GetLastError()); - return false; - } - - m_file.SetFile(m_fileName, hFile); - - // Read and validate header - if (!ReadExact(m_file, &m_header, sizeof(XIPHEADER))) - { - CloseXipHandle(m_file); - return false; - } - - if (m_header.m_magic != XIP_MAGIC) - { -#ifdef _DEBUG - TRACE(_T("\001CXipFile::Open (%s.xip) not a valid XIP file!\n"), szDirPath); -#endif - CloseXipHandle(m_file); - return false; - } - - // The tables sit between the header and the data section, so the name pool - // is whatever is left over. A corrupt dataStart makes that negative, and - // every real archive ends exactly where the header says it does. - const int cbTables = (int)(m_header.m_fileCount * sizeof(FILEDATA)) - + (int)(m_header.m_nameCount * sizeof(FILENAME)); - const int cbNames = (int)m_header.m_dataStart - (int)sizeof(XIPHEADER) - cbTables; - - if (cbNames <= 0 - || GetFileSize(m_file.m_handle, NULL) != m_header.m_dataStart + m_header.m_dataSize) - { - TRACE(_T("\001CXipFile::Load (%s) header does not describe this file\n"), m_fileName); - CloseXipHandle(m_file); - return false; - } - - // Read file table, directory, and name pool - m_filedata = new FILEDATA[m_header.m_fileCount]; - if (!ReadExact(m_file, m_filedata, (int)(m_header.m_fileCount * sizeof(FILEDATA)))) - { - CloseXipHandle(m_file); - return false; - } - - m_directory = new FILENAME[m_header.m_nameCount]; - if (!ReadExact(m_file, m_directory, (int)(m_header.m_nameCount * sizeof(FILENAME)))) - { - CloseXipHandle(m_file); - return false; - } - - m_names = new char[cbNames]; - if (!ReadExact(m_file, m_names, cbNames)) - { - CloseXipHandle(m_file); - return false; - } - - CreateObjects(); - m_loaded = true; - return true; -} - -// ========================================================================= -// Texture loading: unwrap XPR container and create D3D texture -// ========================================================================= - -static LPDIRECT3DTEXTURE8 ReadTextureFromXPR(CFileBuffer& file, int nBytes) -{ - // Everything below indexes pbRaw using sizes the archive supplies, so the - // entry has to be big enough to hold a header and a texture struct before - // any of them can be trusted. - if (nBytes < (int)(sizeof(XPR_HEADER) + sizeof(IDirect3DTexture8))) - { - TRACE(_T("\001ReadTextureFromXPR: entry too small (%d bytes)\n"), nBytes); - DiscardBytes(file, nBytes); - return NULL; - } - - BYTE* pbRaw = new BYTE[nBytes]; - if (!ReadExact(file, pbRaw, nBytes)) - { - delete[] pbRaw; - return NULL; - } - - const XPR_HEADER* pxpr = (const XPR_HEADER*)pbRaw; - if (pxpr->dwMagic != XPR_MAGIC_VALUE) - { - ALERT(_T("Unable to load XBX file for scene!")); - delete[] pbRaw; - return NULL; - } - - // dwHeaderSize is where the pixels start and dwTotalSize is where they - // end; both have to land inside the bytes we actually read. - if (pxpr->dwHeaderSize < sizeof(XPR_HEADER) - || pxpr->dwTotalSize < pxpr->dwHeaderSize - || pxpr->dwTotalSize > (DWORD)nBytes) - { - TRACE(_T("\001ReadTextureFromXPR: header describes %d..%d of %d bytes\n"), - (int)pxpr->dwHeaderSize, (int)pxpr->dwTotalSize, nBytes); - delete[] pbRaw; - return NULL; - } - - int cbData = pxpr->dwTotalSize - pxpr->dwHeaderSize; - if (cbData <= 0) - { - TRACE(_T("\001ReadTextureFromXPR: no pixel data\n")); - delete[] pbRaw; - return NULL; - } - - // Allocate the D3D texture header in system memory - IDirect3DTexture8* pTex = (IDirect3DTexture8*)TheseusD3D_AllocNoncontiguousMemory(sizeof(D3DBaseTexture)); - if (pTex == NULL) - { - TRACE(_T("Not enough memory to load XBX image file!\n")); - delete[] pbRaw; - return NULL; - } - - CopyMemory(pTex, pbRaw + sizeof(XPR_HEADER), sizeof(IDirect3DTexture8)); - - // Allocate contiguous memory for the texture data (GPU-accessible) - BYTE* pbTexData = (BYTE*)TheseusD3D_AllocContiguousMemory(cbData, D3DTEXTURE_ALIGNMENT); - if (pbTexData == NULL) - { - TRACE(_T("Not enough memory to load XBX image file!\n")); - D3D_FreeNoncontiguousMemory(pTex); - delete[] pbRaw; - return NULL; - } - - CopyMemory(pbTexData, pbRaw + pxpr->dwHeaderSize, cbData); - D3D_CopyContiguousMemoryToVideo(pbTexData); - - pTex->Data = NULL; - pTex->Register(pbTexData); - pTex->Common |= D3DCOMMON_D3DCREATED; - - delete[] pbRaw; - return pTex; -} - -// ========================================================================= -// CreateObjects: load all file entries from the archive -// ========================================================================= - -void CXipFile::CreateObjects() -{ - m_locked = true; - - m_objects = new void*[m_header.m_fileCount]; - ZeroMemory(m_objects, sizeof(void*) * m_header.m_fileCount); - - for (UINT i = 0; i < m_header.m_fileCount; i++) - { - switch (m_filedata[i].m_type) - { - case XIP_TYPE_TEXTURE: - m_objects[i] = ReadTextureFromXPR(m_file, m_filedata[i].m_size); - break; - - case XIP_TYPE_MESH_REFERENCE: - { - // Top byte of the offset, so the archive can name any slot up - // to 255 whether or not we have that many. - const UINT iBuffer = m_filedata[i].m_dataOffset >> 24; - if (iBuffer >= MAX_MESHBUFFER) - { - TRACE(_T("\001CreateObjects: mesh ref %d out of range in %s\n"), - (int)iBuffer, m_fileName); - break; - } - - CMeshRef* pRef = new CMeshRef; - pRef->m_xipFile = this; - pRef->m_meshBufferIndex = iBuffer; - pRef->m_firstIndex = m_filedata[i].m_dataOffset & 0x00ffffff; - pRef->m_primitiveCount = m_filedata[i].m_size; - m_objects[i] = pRef; - } - break; - - case XIP_TYPE_INDEXBUFFER: - // Written into its vertex buffer's slot, so that count has to fit too. - if (m_indexBufferCount >= MAX_MESHBUFFER || m_vertexBufferCount >= MAX_MESHBUFFER) - { - TRACE(_T("\001CreateObjects: too many index buffers in %s\n"), m_fileName); - DiscardBytes(m_file, m_filedata[i].m_size); - break; - } - ReadIndexBuffer(i, m_vertexBufferCount); - m_indexBufferCount++; - break; - - case XIP_TYPE_VERTEXBUFFER: - if (m_vertexBufferCount >= MAX_MESHBUFFER) - { - TRACE(_T("\001CreateObjects: too many vertex buffers in %s\n"), m_fileName); - DiscardBytes(m_file, m_filedata[i].m_size); - break; - } - ReadVertexBuffer(i, m_vertexBufferCount); - m_vertexBufferCount++; - break; - - case XIP_TYPE_MESH: - ASSERT(FALSE); // Obsolete... - break; - - default: - // Generic data: allocate and read raw bytes - m_objects[i] = TheseusAllocMemory(m_filedata[i].m_size); - m_file.Read(m_objects[i], m_filedata[i].m_size); - break; - } - } - - ASSERT(m_vertexBufferCount == m_indexBufferCount); - - m_locked = false; -} - -// ========================================================================= -// Directory search: binary search by name within the archive -// ========================================================================= - -struct SEARCHXIP -{ - const char* m_names; - const char* m_szFind; - - SEARCHXIP(CXipFile* pXip, const char* szFind) - : m_names(pXip->m_names), m_szFind(szFind) {} -}; - -static int __cdecl SearchXipCompare(const void* elem1, const void* elem2) -{ - const SEARCHXIP* pSearch = (const SEARCHXIP*)elem1; - const FILENAME* pEntry = (const FILENAME*)elem2; - return _stricmp(pSearch->m_szFind, pSearch->m_names + pEntry->m_nameOffset); -} - -int CXipFile::Find(const char* szURL) -{ - int cchDir = strlen(m_dirPath); - - if (_strnicmp(szURL, m_dirPath, cchDir) != 0) - return -1; - - const char* szFile = szURL + cchDir; - if (*szFile != '\\') - return -1; - szFile++; - - SEARCHXIP ctx(this, szFile); - FILENAME* pHit = (FILENAME*)bsearch(&ctx, m_directory, - m_header.m_nameCount, sizeof(FILENAME), SearchXipCompare); - - if (pHit == NULL) - return -1; - - return pHit->m_fileDataIndex; -} - -void* CXipFile::FindObject(const char* szURL, int nType) -{ - int idx = Find(szURL); - if (idx < 0) - return NULL; - - if (nType != -1 && m_filedata[idx].m_type != (DWORD)nType) - return NULL; - - if (nType == XIP_TYPE_TEXTURE) - ((LPDIRECT3DTEXTURE8)m_objects[idx])->AddRef(); - - return m_objects[idx]; -} - -// ========================================================================= -// Mesh buffer management -// ========================================================================= - -void CXipFile::ReadIndexBuffer(int nFileIndex, int nBuffer) -{ - CMeshBuffer* pBuf = &m_meshBuffers[nBuffer]; - - TheseusCreateIndexBuffer(m_filedata[nFileIndex].m_size, - D3DUSAGE_DYNAMIC, D3DFMT_INDEX16, D3DPOOL_DEFAULT, &pBuf->m_indexBuffer); - - BYTE* pData; - const DWORD dwLock = D3DLOCK_DISCARD | D3DLOCK_NOFLUSH; - VERIFYHR(pBuf->m_indexBuffer->Lock(0, m_filedata[nFileIndex].m_size, (BYTE**)&pData, dwLock)); - m_file.Read(pData, m_filedata[nFileIndex].m_size); - VERIFYHR(pBuf->m_indexBuffer->Unlock()); - - pBuf->m_indexCount = m_filedata[nFileIndex].m_size / sizeof(WORD); -} - -void CXipFile::ReadVertexBuffer(int nFileIndex, int nBuffer) -{ - CMeshBuffer* pBuf = &m_meshBuffers[nBuffer]; - - // Vertex buffer is prefixed with vertex count and FVF - int nVerts; - DWORD fvf; - m_file.Read(&nVerts, sizeof(int)); - m_file.Read(&fvf, sizeof(DWORD)); - - int cbVerts = m_filedata[nFileIndex].m_size - 8; - if (nVerts <= 0 || cbVerts <= 0) - { - TRACE(_T("\001ReadVertexBuffer: bad vertex count %d in %s\n"), nVerts, m_fileName); - DiscardBytes(m_file, cbVerts > 0 ? cbVerts : 0); - return; - } - pBuf->m_vertexStride = cbVerts / nVerts; - - TheseusCreateVertexBuffer(cbVerts, D3DUSAGE_DYNAMIC, fvf, D3DPOOL_DEFAULT, &pBuf->m_vertexBuffer); - - BYTE* pData; - const DWORD dwLock = D3DLOCK_DISCARD | D3DLOCK_NOFLUSH; - VERIFYHR(pBuf->m_vertexBuffer->Lock(0, 0, &pData, dwLock)); - m_file.Read(pData, cbVerts); - VERIFYHR(pBuf->m_vertexBuffer->Unlock()); - - pBuf->m_fvf = fvf; - pBuf->m_vertexCount = nVerts; -} - -void CXipFile::DeleteMeshBuffers() -{ - ASSERT(!m_locked); - - int i; - for (i = 0; i < MAX_MESHBUFFER; i++) - { - if (m_meshBuffers[i].m_vertexBuffer != NULL) - { - m_meshBuffers[i].m_vertexBuffer->Release(); - m_meshBuffers[i].m_vertexBuffer = NULL; - } - if (m_meshBuffers[i].m_indexBuffer != NULL) - { - m_meshBuffers[i].m_indexBuffer->Release(); - m_meshBuffers[i].m_indexBuffer = NULL; - } - } -} - -void CXipFile::ReloadMeshBuffers() -{ - ASSERT(!m_locked); - m_locked = true; - - int nIB = 0, nVB = 0; - - UINT i; - for (i = 0; i < m_header.m_fileCount; i++) - { - if (m_filedata[i].m_type == XIP_TYPE_INDEXBUFFER) - { - if (!CHECK(nIB < m_indexBufferCount)) - continue; - m_file.Seek(m_header.m_dataStart + m_filedata[i].m_dataOffset); - ReadIndexBuffer(i, nIB++); - } - else if (m_filedata[i].m_type == XIP_TYPE_VERTEXBUFFER) - { - if (!CHECK(nVB < m_vertexBufferCount)) - continue; - m_file.Seek(m_header.m_dataStart + m_filedata[i].m_dataOffset); - ReadVertexBuffer(i, nVB++); - } - } - - ASSERT(nIB == m_indexBufferCount); - ASSERT(nVB == m_vertexBufferCount); - - m_locked = false; - m_reloading = false; -} - -// ========================================================================= -// Mesh cache eviction: free the oldest unused mesh buffers -// ========================================================================= - -bool CleanupMeshCache() -{ - CXipFile* pOldest = NULL; - - for (int i = 0; i < c_nXipFileCount; i++) - { - CXipFile* pXip = &c_rgXipFile[i]; - - if (pXip->m_locked || !pXip->m_loaded) - continue; - if (pXip->m_vertexBufferCount == 0) - continue; - if (pXip->m_meshBuffers[0].m_vertexBuffer == NULL) - continue; - - if (pOldest == NULL || pXip->m_cacheTime < pOldest->m_cacheTime) - pOldest = pXip; - } - - if (pOldest == NULL) - return false; - - pOldest->DeleteMeshBuffers(); - return true; -} - -// ========================================================================= -// Reload / state queries -// ========================================================================= - -void CXipFile::Reload() -{ - ASSERT(!m_reloading); - - m_reloading = true; - - DWORD dwThreadID; - HANDLE hThread = CreateThread(NULL, 0, CXipFile::StartLoadThread, this, 0, &dwThreadID); - if (hThread) - CloseHandle(hThread); - else - StartLoadThread(this); -} - -bool CXipFile::IsUnloaded() const -{ - if (!m_loaded || m_reloading) - return false; - if (m_vertexBufferCount == 0) - return false; - return (m_meshBuffers[0].m_vertexBuffer == NULL); -} - -bool CXipFile::IsReloading() const -{ - return m_reloading; -} - -// ========================================================================= -// CMeshRef: render a mesh reference from the shared buffers -// ========================================================================= - -void CMeshRef::Render(bool bSetFVF) -{ - if (m_meshBufferIndex >= m_xipFile->m_vertexBufferCount) - return; - CMeshBuffer* pBuf = &m_xipFile->m_meshBuffers[m_meshBufferIndex]; - - m_xipFile->m_cacheTime = TheseusGetNow(); - - if (m_xipFile->IsReloading()) - return; - - // Trigger lazy reload if buffers were evicted - if (pBuf->m_vertexBuffer == NULL) - { - m_xipFile->Reload(); - return; - } - - if (bSetFVF) - TheseusSetVertexShader(GetFixedFunctionShader(pBuf->m_fvf)); - - // Disable edge AA for large meshes (performance) - if (m_primitiveCount > 800 && !g_bEdgeAntialiasOverride) - { - TheseusSetRenderState(D3DRS_EDGEANTIALIAS, FALSE); - TheseusSetRenderState(D3DRS_MULTISAMPLEANTIALIAS, TRUE); - } - - TheseusSetStreamSource(0, pBuf->m_vertexBuffer, pBuf->m_vertexStride); - TheseusSetIndices(pBuf->m_indexBuffer, 0); - Frame_Count(FC_DRAW_SCENE); - TheseusDrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, pBuf->m_vertexCount, m_firstIndex, m_primitiveCount); -} - -DWORD CMeshRef::GetFVF() const -{ - if (m_meshBufferIndex >= m_xipFile->m_vertexBufferCount) - return 0; - return m_xipFile->m_meshBuffers[m_meshBufferIndex].m_fvf; -} From 4738557dd51056dd61e00fc8be964bdb70fc0b7d Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:40:57 -0500 Subject: [PATCH 101/167] Desktop uses the shared XIP reader Dropped the desktop copy of the XIP reader and built render/xip_archive.cpp instead. All 40 real archives load the same as before, byte for byte. The three files in Data/Xips that aren't archives now close their handle when refused, and a sync load that fails returns NULL like on Xbox. --- build/Makefile | 2 +- theseus/desktop/d3d8_sdl.h | 2 +- theseus/desktop/dashapp.cpp | 2 +- theseus/desktop/image.cpp | 2 +- theseus/desktop/mesh.cpp | 2 +- theseus/desktop/xip.cpp | 838 ------------------------------------ theseus/desktop/xip.h | 134 ------ 7 files changed, 5 insertions(+), 977 deletions(-) delete mode 100644 theseus/desktop/xip.cpp delete mode 100755 theseus/desktop/xip.h diff --git a/build/Makefile b/build/Makefile index 547bc2a..33a74e5 100644 --- a/build/Makefile +++ b/build/Makefile @@ -830,7 +830,7 @@ endif DESKTOP_SRCS_FILE = \ $(DESKTOP_SRCS_HOST) \ $(THESEUS_SRC)/desktop/xbox_file.cpp \ - $(THESEUS_SRC)/desktop/xip.cpp \ + $(THESEUS_SRC)/render/xip_archive.cpp \ $(THESEUS_SRC)/shared/file_util.cpp \ $(THESEUS_SRC)/desktop/files.cpp \ $(THESEUS_SRC)/shared/activefile.cpp \ diff --git a/theseus/desktop/d3d8_sdl.h b/theseus/desktop/d3d8_sdl.h index 1f41b7a..89385f3 100644 --- a/theseus/desktop/d3d8_sdl.h +++ b/theseus/desktop/d3d8_sdl.h @@ -2369,7 +2369,7 @@ inline HRESULT D3DXCreateMatrixStack(DWORD flags, ID3DXMatrixStack** pp) { *pp = #define D3DX_DEFAULT_NONPOW2 ((UINT)-2) #define D3DX_FILTER_NONE 1 -// Forward-declare XBX parser (defined in xbx_texture.h, included by Image.cpp/xip.cpp) +// Forward-declare XBX parser (defined in xbx_texture.h, included by image.cpp/xip_archive.cpp) IDirect3DTexture8* XBX_ParseTexture(const BYTE* pbContent, int cbContent); inline HRESULT D3DXCreateTextureFromFileA(IDirect3DDevice8* dev, const char* path, IDirect3DTexture8** ppTex) { diff --git a/theseus/desktop/dashapp.cpp b/theseus/desktop/dashapp.cpp index 885d47b..1cbf43c 100755 --- a/theseus/desktop/dashapp.cpp +++ b/theseus/desktop/dashapp.cpp @@ -6,7 +6,7 @@ #include "std.h" #include "dashapp.h" #include "file_util.h" -#include "xip.h" +#include "xip_archive.h" #include "node.h" #include "screen.h" #include "runner.h" diff --git a/theseus/desktop/image.cpp b/theseus/desktop/image.cpp index bf5a04d..a46ebae 100644 --- a/theseus/desktop/image.cpp +++ b/theseus/desktop/image.cpp @@ -8,7 +8,7 @@ #include "node.h" #include "asset_loader.h" #include "activefile.h" -#include "xip.h" +#include "xip_archive.h" #include "file_util.h" #include "xbx_texture.h" #include "skin_assets.h" diff --git a/theseus/desktop/mesh.cpp b/theseus/desktop/mesh.cpp index f2acda5..56320da 100644 --- a/theseus/desktop/mesh.cpp +++ b/theseus/desktop/mesh.cpp @@ -9,7 +9,7 @@ #include "shape_render.h" #include "runner.h" #include "asset_loader.h" -#include "xip.h" +#include "xip_archive.h" #include "file_util.h" #include "camera.h" #include "skin_assets.h" diff --git a/theseus/desktop/xip.cpp b/theseus/desktop/xip.cpp deleted file mode 100644 index bb84653..0000000 --- a/theseus/desktop/xip.cpp +++ /dev/null @@ -1,838 +0,0 @@ -// xip.cpp: desktop XIP archive loading, mesh buffer management, and -// file lookup. XIP is the dashboard's packed archive format -// containing XAP scripts, XBX textures, XM meshes, and vertex / -// index buffers. Counterpart to xbox/xip_archive.cpp. - -#include "std.h" -#include "dashapp.h" -#include "theseus.h" -#include "file_util.h" -#include "xip.h" -#include "asset_loader.h" -#include "node.h" -#include "camera.h" -#include "xbx_texture.h" - -extern CCamera theCamera; -extern unsigned int g_uMeshRef; -extern D3DXMATRIX g_matProjection; - -extern int g_bEdgeAntialiasOverride; -inline void D3D_FreeNoncontiguousMemory(void *pMemory) { free(pMemory); } - -// ============================================================================ -// CFileBuffer -- buffered file I/O for XIP reading -// ============================================================================ - -#define FILE_BUFFER_SIZE 65536 - -void FileProtectionError() -{ - ALERT("XIP File Protection Error"); - __debugbreak(); -} - -CFileBuffer::CFileBuffer() -{ - m_hFile = INVALID_HANDLE_VALUE; - m_pbBuffer = NULL; - m_cbBuffer = 0; - m_ibRead = 0; - m_nBlkCur = 0; -} - -void CFileBuffer::SetFile(const char* Name, HANDLE hFile) -{ - ASSERT(m_hFile == INVALID_HANDLE_VALUE); - m_hFile = hFile; - -} - -CFileBuffer::~CFileBuffer() -{ - FreeBuffer(); -} - -void CFileBuffer::FreeBuffer() -{ - if (m_pbBuffer != NULL) - { - free(m_pbBuffer); - m_pbBuffer = NULL; - } -} - -bool CFileBuffer::Seek(int nPos) -{ -#if DBG - static int nLastPos; - nLastPos = nPos; -#endif - - int nBlock = nPos / FILE_BUFFER_SIZE; - - if (m_pbBuffer == NULL) - { - for (;;) - { - m_pbBuffer = (uint8_t*)calloc(1, FILE_BUFFER_SIZE); - if (m_pbBuffer) - { - break; - } - NewFailed(FILE_BUFFER_SIZE); - } - m_nBlkCur = -1; - } - - if (nBlock != m_nBlkCur) - { - VERIFY(SetFilePointer(m_hFile, nBlock * FILE_BUFFER_SIZE, NULL, FILE_BEGIN) != ~0); - m_nBlkCur = nBlock; - - uint32_t dwRead; - if (!ReadFile(m_hFile, m_pbBuffer, FILE_BUFFER_SIZE, LPDW(&dwRead), NULL) || dwRead == 0) - { - return false; - } - - ++m_nBlkCur; - - m_cbBuffer = (int)dwRead; - } - - m_ibRead = nPos % FILE_BUFFER_SIZE; - - return true; -} - -int CFileBuffer::Read(void* pv, int cb) -{ - if (m_pbBuffer == NULL) - { - for (;;) - { - m_pbBuffer = (uint8_t*)calloc(1, FILE_BUFFER_SIZE); - if (m_pbBuffer) - { - break; - } - NewFailed(FILE_BUFFER_SIZE); - } - } - - int cbTotalRead = 0; - int cbRead = 0; - uint8_t* pb = (uint8_t*)pv; - - while (cb > 0) - { - if (m_ibRead < m_cbBuffer) - { - cbRead = m_cbBuffer - m_ibRead; - if (cbRead > cb) - cbRead = cb; - - memcpy(pb, m_pbBuffer + m_ibRead, cbRead); - - pb += cbRead; - cb -= cbRead; - ASSERT(cb >= 0); - - m_ibRead += cbRead; - cbTotalRead += cbRead; - } - - if (m_ibRead == m_cbBuffer) - { - uint32_t dwRead = 0; - if (!ReadFile(m_hFile, m_pbBuffer, FILE_BUFFER_SIZE, LPDW(&dwRead), NULL)) - { - ASSERT(FALSE && "Unable to read from XIP!"); - FileProtectionError(); - return -1; - } - - if (dwRead == 0) - { - return -1; - } - - ++m_nBlkCur; - - m_cbBuffer = (int)dwRead; - m_ibRead = 0; - } - } - - return cbTotalRead; -} - -// ============================================================================ -// XIP File Management -- load, search, cache -// ============================================================================ - -CXipFile c_rgXipFile[20]; -int c_nXipFileCount = 0; - -CXipFile* LoadXIP(const char* szURL, bool bSync, bool isSkin ) -{ - char szBuf [MAX_PATH]; - - if (szURL[0] && szURL[1] != ':') - { - strcpy(szBuf, "Q:/Xips/"); - } - else - { - szBuf[0] = 0; - } - - strcat(szBuf, szURL); - - { - char szDirPath [MAX_PATH]; - CleanFilePath(szDirPath, szBuf); - char* pch = strrchr(szDirPath, '.'); - ASSERT(pch != NULL); - *pch = 0; - - for (int i = 0; i < c_nXipFileCount; i += 1) - { - if (_stricmp(szDirPath, c_rgXipFile[i].m_szDirPath) == 0) - { - c_rgXipFile[i].m_cacheTime = TheseusGetNow(); - if (c_rgXipFile[i].m_bLoaded && c_rgXipFile[i].m_nVertexBufferCount > 0 && c_rgXipFile[i].m_rgMeshBuffer[0].m_pVertexBuffer == NULL) - c_rgXipFile[i].ReloadMeshBuffers(); - - return &c_rgXipFile[i]; - } - } - } - - if (c_nXipFileCount >= (int)countof(c_rgXipFile)) - { - TRACE("\001LoadXIP: XIP pool full (%d)\n", (int)countof(c_rgXipFile)); - return NULL; - } - - CXipFile* pXipFile = &c_rgXipFile[c_nXipFileCount]; - - if (!pXipFile->Open(szBuf)) - { - return NULL; - } - - c_nXipFileCount += 1; - - // Always load synchronously — async loading causes race conditions - // with the script VM (namespace corruption when scripts run against partial scene state) - pXipFile->Load(); - - return pXipFile; -} - -bool FindInXIPAndDetach(const char* szURL, BYTE*& pbContent, DWORD& cbContent) -{ - char szFilePath [MAX_PATH]; - CleanFilePath(szFilePath, szURL); - - for (int i = c_nXipFileCount - 1; i >= 0; i -= 1) - { - if (c_rgXipFile[i].m_bLocked || !c_rgXipFile[i].m_bLoaded) - continue; - - int nObject = c_rgXipFile[i].Find(szFilePath); - if (nObject >= 0 && c_rgXipFile[i].m_objects[nObject] != NULL) - { - pbContent = (uint8_t*)c_rgXipFile[i].m_objects[nObject]; - cbContent = c_rgXipFile[i].m_filedata[nObject].m_dwSize; - c_rgXipFile[i].m_objects[nObject] = NULL; - return true; - } - } - - return false; -} -void* FindObjectInXIP(const char* szURL, const char* szFilename, int nType/*=-1*/) -{ - char szFilePath [MAX_PATH]; - CleanFilePath(szFilePath, szURL); - - // For textures, check the skin directory first so skins can override XIP textures - if (nType == XIP_TYPE_TEXTURE && g_sSkinDir != NULL && szFilename != NULL) - { - char szBuf [MAX_PATH]; - strcpy(szBuf, g_sSkinDir); - strcat(szBuf, szFilename); - - char thePath[MAX_PATH]; - Ansi(thePath, szBuf, MAX_PATH); - - // The skin dir is an Xbox path, so it opens through the translator. - if (FILE* f = TheseusFOpen(thePath, "rb")) - { - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - BYTE* data = size > 0 ? (BYTE*)malloc(size) : NULL; - bool ok = data && fread(data, 1, size, f) == (size_t)size; - fclose(f); - LPDIRECT3DTEXTURE8 lpTexture = NULL; - HRESULT hr = ok ? D3DXCreateTextureFromFileInMemory(TheseusGetD3DDev(), data, (UINT)size, &lpTexture) : E_FAIL; - free(data); - if (hr == D3D_OK) - return lpTexture; - } - } - - // Search loaded XIPs (newest to oldest) - for (int i = c_nXipFileCount - 1; i >= 0; i -= 1) - { - if (c_rgXipFile[i].m_bLocked || !c_rgXipFile[i].m_bLoaded) - continue; - - // For XIP 0 (default), remap texture extension to .xbx - if (i == 0 && nType == XIP_TYPE_TEXTURE && szFilename != NULL) - { - char szBuf [MAX_PATH]; - char szBufName [MAX_PATH]; - strcpy(szBufName, szFilename); - char *pDot = strrchr(szBufName, '.'); - if (pDot != NULL) - strcpy(pDot + 1, "xbx"); - strcpy(szBuf, g_sXipDir); - strcat(szBuf, szBufName); - CleanFilePath(szFilePath, szBuf); - } - - void* pObject = c_rgXipFile[i].FindObject(szFilePath, nType); - if (pObject != NULL) - return pObject; - } - - return NULL; -} - -CXipFile::CXipFile() -{ - m_szXipFileName = NULL; - m_szDirPath = NULL; - m_bLoaded = false; - m_bLocked = false; - m_bReloading = false; - - memset(m_rgMeshBuffer, 0, sizeof (m_rgMeshBuffer)); - m_nVertexBufferCount = 0; - m_nIndexBufferCount = 0; -} - -CXipFile::~CXipFile() -{ - delete [] m_szXipFileName; - m_szXipFileName = NULL; - - delete [] m_szDirPath; - m_szDirPath = NULL; - - delete [] m_filedata; - m_filedata = NULL; - - delete [] m_directory; - m_directory = NULL; - - delete [] m_names; - m_names = NULL; - - DeleteMeshBuffers(); - - memset(m_rgMeshBuffer, 0, sizeof (m_rgMeshBuffer)); - m_nVertexBufferCount = 0; - m_nIndexBufferCount = 0; -} - -bool CXipFile::Open(const char* szXipFileName) -{ - m_szXipFileName = new char [strlen(szXipFileName) + 1]; - strcpy(m_szXipFileName, szXipFileName); - - char szDirPath [MAX_PATH]; - CleanFilePath(szDirPath, szXipFileName); - - char* pch = strrchr(szDirPath, '.'); - ASSERT(pch != NULL); - *pch = 0; - - pch = strrchr(szDirPath, '\\'); - if (pch == NULL) - pch = szDirPath; - else - pch += 1; - - if (_stricmp(pch, "default") == 0) - { - if (pch > szDirPath) - pch -= 1; - *pch = 0; - } - - m_szDirPath = new char [strlen(szDirPath) + 1]; - strcpy(m_szDirPath, szDirPath); - - return true; -} - -DWORD CALLBACK CXipFile::StartLoadThread(LPVOID pvContext) -{ - START_PROFILE(); - CXipFile *pThis = (CXipFile*)pvContext; - - if (pThis->m_bReloading) - pThis->ReloadMeshBuffers(); - else - VERIFY(pThis->Load()); - - END_PROFILE(); - - return 0; -} - -// Read returns the byte count, or -1 when the file is short or unreadable, so -// "did I get all of it" has to be asked directly. -static bool XipReadExact(CFileBuffer& file, void* pv, int cb) -{ - return cb >= 0 && file.Read(pv, cb) == cb; -} - -// CreateObjects reads entries back to back with no seeking, so an entry we -// refuse still has to be read past or every entry after it lands wrong. -static void XipDiscard(CFileBuffer& file, int cb) -{ - BYTE scratch[4096]; - while (cb > 0) - { - int n = cb < (int)sizeof(scratch) ? cb : (int)sizeof(scratch); - if (file.Read(scratch, n) != n) - return; - cb -= n; - } -} - -// Every real archive ends exactly where dataStart + dataSize says it does. -static bool XipSizeMatches(CFileBuffer& file, const XIPHEADER& hdr) -{ - DWORD cbFile = GetFileSize(file.m_hFile, NULL); - return cbFile != (DWORD)-1 && cbFile == hdr.m_dwDataStart + hdr.m_dwDataSize; -} - -bool CXipFile::Load() -{ - char szDirPath [MAX_PATH]; - Unicode(szDirPath, m_szDirPath, MAX_PATH); - uint32_t ticks = GetTickCount (); - - HANDLE hFile; - XCALCSIG_SIGNATURE sig; - - - if ((hFile = TheseusCreateFile(m_szXipFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN | FILE_FLAG_NO_BUFFERING, NULL)) == INVALID_HANDLE_VALUE) - { - TRACE("\001CXipFile::Open (%s) failed %d\n", m_szXipFileName, GetLastError()); - return false; - } - - m_file.SetFile(m_szXipFileName, hFile); - - if (!XipReadExact(m_file, &m_header, sizeof (XIPHEADER))) - return false; - - if (m_header.m_dwMagic != XIP_MAGIC) - return false; - - // The tables sit between the header and the data section, so the name pool - // is the remainder. A corrupt dataStart makes that negative, and a real - // archive ends exactly where its header says it does. - const int cbTables = (int)(m_header.m_wFileCount * sizeof (FILEDATA)) - + (int)(m_header.m_wNameCount * sizeof (FILENAME)); - const int cbNames = (int)m_header.m_dwDataStart - (int)sizeof (XIPHEADER) - cbTables; - - if (cbNames <= 0 || !XipSizeMatches(m_file, m_header)) - { - TRACE("\001CXipFile::Load (%s) header does not describe this file\n", m_szXipFileName); - return false; - } - - m_filedata = new FILEDATA [m_header.m_wFileCount]; - if (!XipReadExact(m_file, m_filedata, (int)(m_header.m_wFileCount * sizeof (FILEDATA)))) - return false; - - m_directory = new FILENAME [m_header.m_wNameCount]; - if (!XipReadExact(m_file, m_directory, (int)(m_header.m_wNameCount * sizeof (FILENAME)))) - return false; - - m_names = new char [cbNames]; - if (!XipReadExact(m_file, m_names, cbNames)) - return false; - - CreateObjects(); - - m_bLoaded = true; - - return true; -} - -LPDIRECT3DTEXTURE8 ReadTexture(CFileBuffer& file, int nBytes) -{ - // Desktop: parse XBX texture data via SDL/OpenGL path - uint8_t* pbContent = new uint8_t[nBytes]; - file.Read(pbContent, nBytes); - IDirect3DTexture8* pResult = XBX_ParseTexture(pbContent, nBytes); - delete [] pbContent; - if (!pResult) - TRACE("\001ReadTexture: XBX parse failed (%d bytes)\n", nBytes); - return pResult; -} - -void CXipFile::CreateObjects() -{ - m_bLocked = true; - - m_objects = new void* [m_header.m_wFileCount]; - memset(m_objects, 0, sizeof (void*) * m_header.m_wFileCount); - - for (unsigned int i = 0; i < m_header.m_wFileCount; i += 1) - { - switch (m_filedata[i].m_dwType) - { - default: - // Desktop: null-terminate explicitly (malloc doesn't zero-fill like VirtualAlloc) - m_objects[i] = TheseusAllocMemory(m_filedata[i].m_dwSize + sizeof(char)); - m_file.Read(m_objects[i], m_filedata[i].m_dwSize); - ((uint8_t*)m_objects[i])[m_filedata[i].m_dwSize] = 0; - break; - - case XIP_TYPE_MESH: - ASSERT(FALSE); // Obsolete... -// m_objects[i] = CreateMesh(m_hFile); - break; - - case XIP_TYPE_MESH_REFERENCE: - { - CMeshRef* pMeshRef = new CMeshRef; - pMeshRef->m_xipFile = this; - pMeshRef->m_meshBufferIndex = m_filedata[i].m_dwDataOffset >> 24; - pMeshRef->m_firstIndex = m_filedata[i].m_dwDataOffset & 0x00ffffff; - pMeshRef->m_primitiveCount = m_filedata[i].m_dwSize; - m_objects[i] = pMeshRef; - } - break; - - case XIP_TYPE_TEXTURE: - m_objects[i] = ReadTexture(m_file, m_filedata[i].m_dwSize); - - break; - - case XIP_TYPE_INDEXBUFFER: - // The index buffer lands in its vertex buffer's slot, so both - // counts have to fit. Desktop ASSERT only warns. - if (m_nIndexBufferCount >= MAX_MESHBUFFER || m_nVertexBufferCount >= MAX_MESHBUFFER) - { - TRACE("\001CreateObjects: too many index buffers\n"); - XipDiscard(m_file, m_filedata[i].m_dwSize); - break; - } - ReadIndexBuffer(i, m_nVertexBufferCount); - m_nIndexBufferCount += 1; - break; - - case XIP_TYPE_VERTEXBUFFER: - if (m_nVertexBufferCount >= MAX_MESHBUFFER) - { - TRACE("\001CreateObjects: too many vertex buffers\n"); - XipDiscard(m_file, m_filedata[i].m_dwSize); - break; - } - ReadVertexBuffer(i, m_nVertexBufferCount); - m_nVertexBufferCount += 1; - break; - } - } - - ASSERT(m_nVertexBufferCount == m_nIndexBufferCount); - - m_bLocked = false; -} - -struct SEARCHXIP -{ - SEARCHXIP(CXipFile* pXipFile, const char* szFind) - { - m_names = pXipFile->m_names; - m_szFind = szFind; - } - - const char* m_names; - const char* m_szFind; -}; - -static int __cdecl SearchXipCompare(const void *elem1, const void *elem2) -{ - const SEARCHXIP* pSearch = (const SEARCHXIP*)elem1; - const FILENAME* pName = (const FILENAME*)elem2; - return _stricmp(pSearch->m_szFind, pSearch->m_names + pName->m_wNameOffset); -} - -int CXipFile::Find(const char* szURL) -{ - int cchDirPath = strlen(m_szDirPath); - - if (_strnicmp(szURL, m_szDirPath, cchDirPath) != 0) - return -1; - - const char* szFile = szURL + cchDirPath; - - if (*szFile != '\\') - return -1; - - szFile += 1; - - SEARCHXIP searchxip(this, szFile); - FILENAME* pFileName = (FILENAME*)bsearch(&searchxip, m_directory, m_header.m_wNameCount, sizeof (FILENAME), SearchXipCompare); - - if (pFileName == NULL) - return -1; - - return m_directory[(int)((uint8_t*)pFileName - (uint8_t*)m_directory) / sizeof (FILENAME)].m_wFileDataIndex; -} - -void* CXipFile::FindObject(const char* szURL, int nType/*=-1*/) -{ - int nObject = Find(szURL); - if (nObject == -1) - { - return NULL; - } - - if (nType != -1 && m_filedata[nObject].m_dwType != (uint32_t)nType) - { - return NULL; - } - - if(XIP_TYPE_TEXTURE == nType) - { - ((LPDIRECT3DTEXTURE8)m_objects[nObject])->AddRef(); - } - - return m_objects[nObject]; -} - -void CXipFile::DeleteMeshBuffers() -{ - ASSERT(!m_bLocked); - - for (int i = 0; i < MAX_MESHBUFFER; i += 1) - { - if (m_rgMeshBuffer[i].m_pVertexBuffer != NULL) - { - m_rgMeshBuffer[i].m_pVertexBuffer->Release(); - m_rgMeshBuffer[i].m_pVertexBuffer = NULL; - } - - if (m_rgMeshBuffer[i].m_pIndexBuffer != NULL) - { - m_rgMeshBuffer[i].m_pIndexBuffer->Release(); - m_rgMeshBuffer[i].m_pIndexBuffer = NULL; - } - } -} - -bool CleanupMeshCache() -{ - CXipFile* pOldOne = NULL; - - for (int i = 0; i < c_nXipFileCount; i += 1) - { - if (c_rgXipFile[i].m_bLocked) - { - continue; - } - - if (!c_rgXipFile[i].m_bLoaded) - { - continue; - } - - if (c_rgXipFile[i].m_nVertexBufferCount == 0) - { - continue; - } - - if (c_rgXipFile[i].m_rgMeshBuffer[0].m_pVertexBuffer == NULL) - { - continue; - } - - if (pOldOne == NULL || c_rgXipFile[i].m_cacheTime < pOldOne->m_cacheTime) - pOldOne = &c_rgXipFile[i]; - } - - if (pOldOne == NULL) - return false; - - pOldOne->DeleteMeshBuffers(); - - return true; -} - -void CXipFile::ReadIndexBuffer(int nFileIndex, int nIndexBuffer) -{ - CMeshBuffer* pMeshBuffer = &m_rgMeshBuffer[nIndexBuffer]; - - TheseusCreateIndexBuffer(m_filedata[nFileIndex].m_dwSize, D3DUSAGE_DYNAMIC, D3DFMT_INDEX16, D3DPOOL_DEFAULT, &pMeshBuffer->m_pIndexBuffer); - - uint8_t* indices; - const DWORD dwLockFlags = D3DLOCK_DISCARD; - VERIFYHR(pMeshBuffer->m_pIndexBuffer->Lock(0, m_filedata[nFileIndex].m_dwSize, (uint8_t**)&indices, dwLockFlags)); - m_file.Read(indices, m_filedata[nFileIndex].m_dwSize); - VERIFYHR(pMeshBuffer->m_pIndexBuffer->Unlock()); - - pMeshBuffer->m_nIndexCount = m_filedata[nFileIndex].m_dwSize / sizeof (uint16_t); -} - -void CXipFile::ReadVertexBuffer(int nFileIndex, int nVertexBuffer) -{ - CMeshBuffer* pMeshBuffer = &m_rgMeshBuffer[nVertexBuffer]; - - int nVertexCount; - DWORD fvf; - - m_file.Read(&nVertexCount, sizeof (int)); - m_file.Read(&fvf, sizeof (DWORD)); - - if (nVertexCount <= 0 || m_filedata[nFileIndex].m_dwSize < 8) - { - TRACE("\001ReadVertexBuffer: bad vertex count %d\n", nVertexCount); - return; - } - - pMeshBuffer->m_nVertexStride = (m_filedata[nFileIndex].m_dwSize - 8) / nVertexCount; - - TheseusCreateVertexBuffer(m_filedata[nFileIndex].m_dwSize - 8, D3DUSAGE_DYNAMIC, fvf, D3DPOOL_DEFAULT, &pMeshBuffer->m_pVertexBuffer); - - - uint8_t* verts; - const DWORD dwLockFlags = D3DLOCK_DISCARD; - VERIFYHR(pMeshBuffer->m_pVertexBuffer->Lock(0, 0, &verts, dwLockFlags)); - m_file.Read(verts, m_filedata[nFileIndex].m_dwSize - 8); - VERIFYHR(pMeshBuffer->m_pVertexBuffer->Unlock()); - - pMeshBuffer->m_fvf = fvf; - pMeshBuffer->m_nVertexCount = nVertexCount; -} - -void CXipFile::ReloadMeshBuffers() -{ - ASSERT(!m_bLocked); - m_bLocked = true; - - int nIndexBuffer = 0; - int nVertexBuffer = 0; - - for (unsigned int i = 0; i < m_header.m_wFileCount; i += 1) - { - switch (m_filedata[i].m_dwType) - { - case XIP_TYPE_INDEXBUFFER: - if (!CHECK(nIndexBuffer < m_nIndexBufferCount)) - break; - m_file.Seek(m_header.m_dwDataStart + m_filedata[i].m_dwDataOffset); - ReadIndexBuffer(i, nIndexBuffer); - nIndexBuffer += 1; - break; - - case XIP_TYPE_VERTEXBUFFER: - if (!CHECK(nVertexBuffer < m_nVertexBufferCount)) - break; - m_file.Seek(m_header.m_dwDataStart + m_filedata[i].m_dwDataOffset); - ReadVertexBuffer(i, nVertexBuffer); - nVertexBuffer += 1; - break; - } - } - - ASSERT(nIndexBuffer == m_nIndexBufferCount); - ASSERT(nVertexBuffer == m_nVertexBufferCount); - - m_bLocked = false; - m_bReloading = false; -} - -void CXipFile::Reload() -{ - ASSERT(!m_bReloading); - - m_bReloading = true; - // Synchronous reload - StartLoadThread(this); -} - -bool CXipFile::IsUnloaded() const -{ - if (!m_bLoaded) - return false; - - if (m_bReloading) - return false; - - if (m_nVertexBufferCount == 0) - return false; - - if (m_rgMeshBuffer[0].m_pVertexBuffer != NULL) - return false; - - return true; -} - -bool CXipFile::IsReloading() const -{ - return m_bReloading; -} - -void CMeshRef::Render(bool bSetFVF/*=true*/) -{ - // The index comes from the archive's top byte, so it can name any of 256. - if (m_meshBufferIndex >= m_xipFile->m_nVertexBufferCount) - return; - CMeshBuffer* pMeshBuffer = &m_xipFile->m_rgMeshBuffer[m_meshBufferIndex]; - - m_xipFile->m_cacheTime = TheseusGetNow(); - - if (m_xipFile->IsReloading()) - return; - - if (pMeshBuffer->m_pVertexBuffer == NULL) - { - m_xipFile->Reload(); - return; - } - - if (bSetFVF) - TheseusSetVertexShader(GetFixedFunctionShader(pMeshBuffer->m_fvf)); - - // Heavy-mesh edge-AA/MSAA swap removed: Xbox NV2A feature, shim no-op - // on desktop GL. The g_bEdgeAntialiasOverride check is no longer - // reachable here. - - TheseusSetStreamSource(0, pMeshBuffer->m_pVertexBuffer, pMeshBuffer->m_nVertexStride); - TheseusSetIndices(pMeshBuffer->m_pIndexBuffer, 0); - - Frame_Count(FC_DRAW_SCENE); - TheseusDrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, pMeshBuffer->m_nVertexCount, m_firstIndex, m_primitiveCount); -} - -DWORD CMeshRef::GetFVF() const -{ - if (m_meshBufferIndex >= m_xipFile->m_nVertexBufferCount) - return 0; - return m_xipFile->m_rgMeshBuffer[m_meshBufferIndex].m_fvf; -} diff --git a/theseus/desktop/xip.h b/theseus/desktop/xip.h deleted file mode 100755 index ba1605d..0000000 --- a/theseus/desktop/xip.h +++ /dev/null @@ -1,134 +0,0 @@ -// xip.h: desktop XIP archive public API. Companion to -// desktop/xip.cpp. - -#pragma once - -#define XIP_MAGIC 0x30504958 -#define XIP_DIGEST_LENGTH 20 - - -struct XIPHEADER -{ - uint32_t m_dwMagic; - uint32_t m_dwDataStart; - uint16_t m_wFileCount; - uint16_t m_wNameCount; - uint32_t m_dwDataSize; -}; - -struct FILEDATA -{ - uint32_t m_dwDataOffset; - uint32_t m_dwSize; - uint32_t m_dwType; - uint32_t m_dwTimestamp; -}; - -struct FILENAME -{ - uint16_t m_wFileDataIndex; - uint16_t m_wNameOffset; -}; - -#define XIP_TYPE_GENERIC 0 -#define XIP_TYPE_MESH 1 -#define XIP_TYPE_TEXTURE 2 -#define XIP_TYPE_WAVE 3 -#define XIP_TYPE_MESH_REFERENCE 4 // m_dwDataOffset is MESHID; m_dwSize is nPrimitiveCount -#define XIP_TYPE_INDEXBUFFER 5 -#define XIP_TYPE_VERTEXBUFFER 6 - -#ifndef BUILD_XIPSIGN - -#define MAX_MESHBUFFER 512 - -struct CMeshBuffer -{ - DWORD m_fvf; - int m_nVertexStride; - int m_nVertexCount; - int m_nIndexCount; - IDirect3DVertexBuffer8* m_pVertexBuffer; - IDirect3DIndexBuffer8* m_pIndexBuffer; -}; - -class CFileBuffer -{ -public: - CFileBuffer(); - ~CFileBuffer(); - - void SetFile(const char* Name, HANDLE hFile); - int Read(void* pv, int cb); - bool Seek(int nPos); - void FreeBuffer(); - void StartSignature (VOID); - void EndSignature (XCALCSIG_SIGNATURE * Signature); - - - - HANDLE m_hFile; - uint8_t* m_pbBuffer; - int m_cbBuffer; - int m_ibRead; - int m_nBlkCur; -private: -}; - -class CXipFile -{ -public: - CXipFile(); - ~CXipFile(); - - bool Open(const char* szXipFileName); - void Reload(); - int Find(const char* szURL); -// bool Find(const char* szURL, uint8_t*& pbContent, uint32_t& cbContent); - void* FindObject(const char* szURL, int nType = -1); - - char* m_szDirPath; - char* m_szXipFileName; - - XIPHEADER m_header; - FILEDATA* m_filedata; - FILENAME* m_directory; - char* m_names; - - void** m_objects; - bool m_bLoaded; - - CMeshBuffer m_rgMeshBuffer [MAX_MESHBUFFER]; - int m_nVertexBufferCount; - int m_nIndexBufferCount; - - void DeleteMeshBuffers(); - - XTIME m_cacheTime; - - bool m_bLocked; - - bool IsUnloaded() const; - bool IsReloading() const; - inline bool IsReady() const - { return m_bLoaded && !m_bReloading; } - -protected: - CFileBuffer m_file; - bool m_bReloading; - - bool Load(); - void CreateObjects(); - void ReloadMeshBuffers(); - void ReadIndexBuffer(int nFileIndex, int nIndexBuffer); - void ReadVertexBuffer(int nFileIndex, int nVertexBuffer); - - static DWORD CALLBACK StartLoadThread(LPVOID pvContext); - friend CXipFile* LoadXIP(const char* szURL, bool bSync, bool isSkin); -}; - -extern CXipFile* LoadXIP(const char* szURL, bool bSync = false, bool isSkin = false ); -extern bool FindInXIPAndDetach(const char* szURL, BYTE*& pbContent, DWORD& cbContent); -extern void* FindObjectInXIP(const char* szURL, const char* szFilename, int nType = -1); - -#endif // BUILD_XIPSIGN From 6e377e12abccf3b28b1391514ce8ca5e4a6f4468 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:51:56 -0500 Subject: [PATCH 102/167] Broken XIPs are refused instead of crashing The reader trusted the archive's tables, so a bad name offset, directory index or entry size read off the end of memory, and a bad buffer offset made reloads spin forever. Load checks the tables now, and a texture that failed to load no longer crashes the next lookup. 1964 damaged archives load or refuse cleanly; the real ones load exactly as before. --- docs/decomp/XIP.md | 10 ++- theseus/render/xip_archive.cpp | 124 ++++++++++++++++++++++++++------- 2 files changed, 107 insertions(+), 27 deletions(-) diff --git a/docs/decomp/XIP.md b/docs/decomp/XIP.md index a35dc03..5226983 100644 --- a/docs/decomp/XIP.md +++ b/docs/decomp/XIP.md @@ -199,7 +199,15 @@ on every world matrix change instead (see `SetFalloffShaderFrameValues`, `0x0005 - `CreateObjects` refuses mesh references past the last slot and mesh buffers past ten, and reads past what it refuses so the entries after stay in line. Textures must hold an XPR header and a resource header, and their sizes must fall inside the entry. -- Reload refuses buffers `CreateObjects` refused, and `CMeshRef` refuses an index past the buffers. +- `Load` also refuses tables that don't hold together: a name outside the pool or an unterminated + pool, a directory record naming an entry that doesn't exist, an entry whose bytes run past the + data, or a vertex buffer too short for its count and FVF. +- `Seek` refuses an offset past the end of the file, which would leave `Read` spinning, and reload + skips a buffer it can't seek to. +- Reload refuses buffers `CreateObjects` refused. `CMeshRef` refuses an index past the buffers and + a slice that runs past its index buffer. +- `FindObject` skips the `AddRef` on a texture that failed to load, `FindInXIPAndDetach` hands out + raw entries only, and `LoadXIP` refuses a path with no extension or one too long to build. - Buffers are made through the device (bgfx on desktop) and locked to write. - Desktop: loads and reloads run inline, textures are decoded in software, raw entries get a terminating zero, a refused read is refused instead of rebooting, and there is no edge AA swap. diff --git a/theseus/render/xip_archive.cpp b/theseus/render/xip_archive.cpp index e59265e..cb1b231 100644 --- a/theseus/render/xip_archive.cpp +++ b/theseus/render/xip_archive.cpp @@ -94,6 +94,8 @@ static BYTE* AllocBlockBuffer() // index = pos % 0x10000; return 1; // current ends up one past the block just read, so seeking back into it reads // it again. That is the binary's behavior and costs nothing that matters. +// Refusing an offset past the end of the file is Theseus's: Read would spin +// forever on it. bool CFileBuffer::Seek(int nPos) { if (m_buffer == NULL) @@ -116,7 +118,10 @@ bool CFileBuffer::Seek(int nPos) m_bufferSize = (int)cbGot; } - m_readIndex = nPos % XIP_BLOCK; + const int nIndex = nPos % XIP_BLOCK; + if (nIndex > m_bufferSize) + return false; + m_readIndex = nIndex; return true; } @@ -225,20 +230,24 @@ static void RunLoader(LPTHREAD_START_ROUTINE pfnLoad, CXipFile* pXip) // xip->Load(); return xip; // The key keeps "default" while Open drops it, so default.xip never matches // itself here; nothing loads it twice. Theseus refuses when the pool is -// full, where 5960 writes past it, and a sync caller learns about a failed -// load. isSkin is Theseus's and unused here. +// full, where 5960 writes past it, a sync caller learns about a failed load, +// and a name too long or with no extension is refused. isSkin is Theseus's +// and unused here. CXipFile* LoadXIP(const TCHAR* szURL, bool bSync, bool isSkin) { + const TCHAR* szPrefix = (szURL[0] != 0 && szURL[1] != ':') ? _T("Q:/Xips/") : _T(""); + if (_tcslen(szPrefix) + _tcslen(szURL) >= MAX_PATH) + return NULL; TCHAR szPath[MAX_PATH]; - if (szURL[0] != 0 && szURL[1] != ':') - _tcscpy(szPath, _T("Q:/Xips/")); - else - szPath[0] = 0; + _tcscpy(szPath, szPrefix); _tcscat(szPath, szURL); char szKey[MAX_PATH]; CleanFilePath(szKey, szPath); - *strrchr(szKey, '.') = 0; + char* pchExt = strrchr(szKey, '.'); + if (pchExt == NULL) + return NULL; + *pchExt = 0; for (int i = 0; i < c_nXipFileCount; i++) { @@ -289,7 +298,9 @@ CXipFile* LoadXIP(const TCHAR* szURL, bool bSync, bool isSkin) // } // } // return 0; -// The caller owns the bytes from here on; newer archives win. +// The caller owns the bytes from here on; newer archives win. Only raw +// entries are handed out (Theseus's check): a texture or mesh ref isn't +// m_size bytes long, and a bad directory can point a name at one. bool FindInXIPAndDetach(const TCHAR* szURL, BYTE*& pbContent, DWORD& cbContent) { char szPath[MAX_PATH]; @@ -304,6 +315,9 @@ bool FindInXIPAndDetach(const TCHAR* szURL, BYTE*& pbContent, DWORD& cbContent) const int n = xip.Find(szPath); if (n < 0 || xip.m_objects[n] == NULL) continue; + const DWORD type = xip.m_filedata[n].m_type; + if (type == XIP_TYPE_TEXTURE || type == XIP_TYPE_MESH_REFERENCE) + continue; pbContent = (BYTE*)xip.m_objects[n]; cbContent = xip.m_filedata[n].m_size; @@ -459,7 +473,10 @@ bool CXipFile::Open(const TCHAR* szXipFileName) char szDir[MAX_PATH]; CleanFilePath(szDir, szXipFileName); - *strrchr(szDir, '.') = 0; + char* pchExt = strrchr(szDir, '.'); + if (pchExt == NULL) + return false; + *pchExt = 0; char* pszBase = strrchr(szDir, '\\'); pszBase = pszBase != NULL ? pszBase + 1 : szDir; @@ -516,6 +533,35 @@ static bool XipSizeMatches(CFileBuffer& file, const XIPHEADER& hdr) return cbFile != (DWORD)-1 && cbFile == hdr.m_dataStart + hdr.m_dataSize; } +// The tables come from the file, so check them before anything indexes with +// them: every name inside the pool and terminated, every directory entry +// naming a real entry, and every entry that gets read lying inside the data. +// 5960 trusts all of it. +static bool XipTablesValid(const XIPHEADER& hdr, const FILEDATA* pFiles, const FILENAME* pNames, + const char* pchPool, int cbPool) +{ + if (pchPool[cbPool - 1] != 0) + return false; + + for (int i = 0; i < hdr.m_nameCount; i++) + { + if (pNames[i].m_fileDataIndex >= hdr.m_fileCount || pNames[i].m_nameOffset >= cbPool) + return false; + } + + for (int i = 0; i < hdr.m_fileCount; i++) + { + const FILEDATA& fd = pFiles[i]; + if (fd.m_type == XIP_TYPE_MESH || fd.m_type == XIP_TYPE_MESH_REFERENCE) + continue; + if ((unsigned long long)fd.m_dataOffset + fd.m_size > hdr.m_dataSize) + return false; + if (fd.m_type == XIP_TYPE_VERTEXBUFFER && fd.m_size < 8) + return false; + } + return true; +} + // An archive that fails to load is never read again. static void XipClose(CFileBuffer& file) { @@ -539,8 +585,8 @@ static void XipClose(CFileBuffer& file) // names = new(cbNames); read; // CreateObjects(); loaded = 1; return 1; // Theseus opens through its own path layer and leaves the y: to x: swap out. -// Every read must be whole, the header has to describe this exact file, and -// a failed load closes its handle. +// Every read must be whole, the header has to describe this exact file, the +// tables have to hold together, and a failed load closes its handle. bool CXipFile::Load() { HANDLE hFile = TheseusCreateFile(m_fileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, @@ -582,6 +628,13 @@ bool CXipFile::Load() return false; } + if (!XipTablesValid(m_header, m_filedata, m_directory, m_names, cbNames)) + { + TRACE(_T("\001CXipFile::Load: %s has a broken table\n"), m_fileName); + XipClose(m_file); + return false; + } + CreateObjects(); m_loaded = true; return true; @@ -808,7 +861,8 @@ int CXipFile::Find(const char* szURL) // if (n == -1 || (type != -1 && filedata[n].type != type)) return 0; // if (type == 2) objects[n]->AddRef(); // inlined D3DResource_AddRef // return objects[n]; -// A texture handed out is the caller's reference. +// A texture handed out is the caller's reference. One that failed to load is +// NULL, which the binary would AddRef anyway. void* CXipFile::FindObject(const char* szURL, int nType) { const int n = Find(szURL); @@ -817,7 +871,7 @@ void* CXipFile::FindObject(const char* szURL, int nType) if (nType != -1 && m_filedata[n].m_type != (DWORD)nType) return NULL; - if (nType == XIP_TYPE_TEXTURE) + if (nType == XIP_TYPE_TEXTURE && m_objects[n] != NULL) ((LPDIRECT3DTEXTURE8)m_objects[n])->AddRef(); return m_objects[n]; } @@ -832,16 +886,22 @@ void* CXipFile::FindObject(const char* szURL, int nType) // read filedata[n].size bytes into buf->ib's data; // buf->indexCount = filedata[n].size >> 1; // The binary builds the resource by hand and writes into it; Theseus goes -// through the device so desktop gets a bgfx buffer, and locks to write. +// through the device so desktop gets a bgfx buffer, locks to write, and +// skips the bytes if the device can't give it a buffer. void CXipFile::ReadIndexBuffer(int nFileIndex, int nBuffer) { CMeshBuffer& buf = m_meshBuffers[nBuffer]; const int cb = m_filedata[nFileIndex].m_size; + buf.m_indexBuffer = NULL; TheseusCreateIndexBuffer(cb, D3DUSAGE_DYNAMIC, D3DFMT_INDEX16, D3DPOOL_DEFAULT, &buf.m_indexBuffer); - BYTE* pb; - buf.m_indexBuffer->Lock(0, cb, &pb, XIP_LOCK); + BYTE* pb = NULL; + if (buf.m_indexBuffer == NULL || FAILED(buf.m_indexBuffer->Lock(0, cb, &pb, XIP_LOCK))) + { + XipDiscard(m_file, cb); + return; + } m_file.Read(pb, cb); buf.m_indexBuffer->Unlock(); @@ -885,10 +945,15 @@ void CXipFile::ReadVertexBuffer(int nFileIndex, int nBuffer) #endif buf.m_vertexStride = cbVerts / nVerts; + buf.m_vertexBuffer = NULL; TheseusCreateVertexBuffer(cbVerts, D3DUSAGE_DYNAMIC, fvf, D3DPOOL_DEFAULT, &buf.m_vertexBuffer); - BYTE* pb; - buf.m_vertexBuffer->Lock(0, 0, &pb, XIP_LOCK); + BYTE* pb = NULL; + if (buf.m_vertexBuffer == NULL || FAILED(buf.m_vertexBuffer->Lock(0, 0, &pb, XIP_LOCK))) + { + XipDiscard(m_file, cbVerts); + return; + } m_file.Read(pb, cbVerts); buf.m_vertexBuffer->Unlock(); @@ -963,7 +1028,8 @@ bool CleanupMeshCache() // locked = 0; reloading = 0; // Brings back what CleanupMeshCache freed, seeking to each buffer since the // rest of the archive is still in memory. The CHECKs are Theseus's: an entry -// CreateObjects refused has no slot to come back to. +// CreateObjects refused has no slot to come back to. So is skipping a buffer +// whose offset can't be reached. void CXipFile::ReloadMeshBuffers() { m_locked = true; @@ -977,15 +1043,17 @@ void CXipFile::ReloadMeshBuffers() { if (!CHECK(nIndexBuffer < m_indexBufferCount)) continue; - m_file.Seek(m_header.m_dataStart + fd.m_dataOffset); - ReadIndexBuffer(i, nIndexBuffer++); + const int nSlot = nIndexBuffer++; + if (m_file.Seek(m_header.m_dataStart + fd.m_dataOffset)) + ReadIndexBuffer(i, nSlot); } else if (fd.m_type == XIP_TYPE_VERTEXBUFFER) { if (!CHECK(nVertexBuffer < m_vertexBufferCount)) continue; - m_file.Seek(m_header.m_dataStart + fd.m_dataOffset); - ReadVertexBuffer(i, nVertexBuffer++); + const int nSlot = nVertexBuffer++; + if (m_file.Seek(m_header.m_dataStart + fd.m_dataOffset)) + ReadVertexBuffer(i, nSlot); } } @@ -1034,8 +1102,8 @@ bool CXipFile::IsReloading() const // DrawIndexedVertices(TRIANGLELIST, count * 3, indices + first); // Theseus sets those shader matrices on every world change in shape_render // instead (SetFalloffShaderFrameValues), and skips the AA swap on desktop -// where the shim has no edge AA. The index guard and the draw counter are -// Theseus's. +// where the shim has no edge AA. The index guard, the check that the slice +// fits its index buffer, and the draw counter are Theseus's. void CMeshRef::Render(bool bSetFVF) { if (m_meshBufferIndex >= m_xipFile->m_vertexBufferCount) @@ -1052,6 +1120,10 @@ void CMeshRef::Render(bool bSetFVF) return; } + if (buf.m_indexBuffer == NULL || m_firstIndex < 0 || m_primitiveCount < 0 + || (long long)m_firstIndex + 3LL * m_primitiveCount > buf.m_indexCount) + return; + if (bSetFVF) TheseusSetVertexShader(GetFixedFunctionShader(buf.m_fvf)); From ae8b726411a97a37f4464a18d31fc86306b81702 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:53:45 -0500 Subject: [PATCH 103/167] Fixed dashboard launches never finding the game on Windows The launch path comes back with forward slashes but the games list built its match from the Windows user folder, which has backslashes, so no game ever matched and nothing started. Title Maker's test launch skips that lookup, which is why it worked. Both sides get the same slashes now. Needs a Windows test. Fixes #83 --- theseus/desktop/virtual_games.h | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/theseus/desktop/virtual_games.h b/theseus/desktop/virtual_games.h index 28be62c..9234c93 100644 --- a/theseus/desktop/virtual_games.h +++ b/theseus/desktop/virtual_games.h @@ -257,17 +257,24 @@ inline void VGames_DeleteByName(const char* name) { fprintf(stderr, "[VGames] Deleted: %s\n", name); } +// Forward slashes, no doubled or trailing separator. The Windows user data +// root comes back with backslashes while translated Xbox paths use '/'. +inline void VGames_NormPath(const char* in, char* out, size_t outSize) { + size_t j = 0; + for (size_t k = 0; in[k] && j + 1 < outSize; k++) { + char c = (in[k] == '\\') ? '/' : in[k]; + if (c == '/' && j > 0 && out[j-1] == '/') continue; + out[j++] = c; + } + if (j > 0 && out[j-1] == '/') j--; + out[j] = 0; +} + // Match a disk path like "Library/Games/Road Rage" to a game index, or -1. inline int VGames_MatchFolder(const char* localPath) { VGames_Load(); char norm[512]; - int j = 0; - for (int k = 0; localPath[k] && j < 510; k++) { - if (localPath[k] == '/' && j > 0 && norm[j-1] == '/') continue; - norm[j++] = localPath[k]; - } - if (j > 0 && norm[j-1] == '/') j--; - norm[j] = 0; + VGames_NormPath(localPath, norm, sizeof(norm)); for (int i = 0; i < g_vgames.count; i++) { if (!g_vgames.games[i].valid) continue; @@ -276,9 +283,10 @@ inline int VGames_MatchFolder(const char* localPath) { const char* prefix = (g_vgames.games[i].drive[0]) ? XboxFS_DriveToPrefix(g_vgames.games[i].drive[0]) : 0; if (!prefix) continue; - char expected[512]; - snprintf(expected, sizeof(expected), "%s/%s/%s", + char raw[512], expected[512]; + snprintf(raw, sizeof(raw), "%s/%s/%s", prefix, g_vgames.games[i].category, g_vgames.games[i].name); + VGames_NormPath(raw, expected, sizeof(expected)); if (strcasecmp(norm, expected) == 0) return i; } From a67de4a11aec2e42b2a406f0eda76e0c0dec2830 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:55:16 -0500 Subject: [PATCH 104/167] The XIP pool grows instead of running out The pool held twenty archives like 5960, and a long desktop session visits more scenes than that, so later Inlines came up empty. It grows twenty slots at a time now. All 32 archives in Data/Xips load in one session, three rounds over. --- docs/decomp/XIP.md | 4 +-- theseus/render/xip_archive.cpp | 55 +++++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/docs/decomp/XIP.md b/docs/decomp/XIP.md index 5226983..861ecce 100644 --- a/docs/decomp/XIP.md +++ b/docs/decomp/XIP.md @@ -192,8 +192,8 @@ on every world matrix change instead (see `SetFalloffShaderFrameValues`, `0x0005 ## Where Theseus differs - No `XIPS` signature check and no `y:` to `x:` swap; files open through Theseus's own path layer. -- A twenty first archive is refused instead of overrunning the pool; a sync `LoadXIP` that fails - returns NULL. +- The pool grows twenty slots at a time instead of overrunning at twenty one, so a long desktop + session never runs out. Archives still never leave it. A sync `LoadXIP` that fails returns NULL. - `Load` checks every read, refuses a header whose name pool would be empty or whose `dataStart + dataSize` isn't the file size, and closes a refused file. - `CreateObjects` refuses mesh references past the last slot and mesh buffers past ten, and reads diff --git a/theseus/render/xip_archive.cpp b/theseus/render/xip_archive.cpp index cb1b231..67b4665 100644 --- a/theseus/render/xip_archive.cpp +++ b/theseus/render/xip_archive.cpp @@ -193,11 +193,28 @@ int CFileBuffer::Read(void* pv, int cb) // ===== The archive pool ===================================================== -// Twenty archives, the 5960 pool (c_rgXipFile @ 0x001804e0, 0x150 bytes each; -// the count is at 0x0017e734). Nothing ever leaves it. +// 5960 keeps twenty archives in a fixed array (0x001804e0, 0x150 bytes a slot, +// count at 0x0017e734) and nothing ever leaves it, since scene nodes and mesh +// refs keep pointing at them. Theseus has more scenes than that, so the pool +// grows twenty slots at a time. Each archive is its own allocation and never +// moves, and an outgrown slot table is kept: the Xbox loader thread can be +// walking it from CleanupMeshCache while LoadXIP grows it. -CXipFile c_rgXipFile[20]; -int c_nXipFileCount = 0; +static const int XIP_POOL_STEP = 20; + +static CXipFile** volatile c_rgXipFile = NULL; +static int c_nXipFileSlots = 0; +static volatile int c_nXipFileCount = 0; + +static void GrowXipPool() +{ + const int nSlots = c_nXipFileSlots + XIP_POOL_STEP; + CXipFile** rgNew = new CXipFile*[nSlots]; + for (int i = 0; i < c_nXipFileCount; i++) + rgNew[i] = c_rgXipFile[i]; + c_rgXipFile = rgNew; + c_nXipFileSlots = nSlots; +} // 5960 starts loads on a worker thread and runs them inline if no thread // starts. Desktop always runs them inline: scripts read the scene as soon as @@ -229,8 +246,8 @@ static void RunLoader(LPTHREAD_START_ROUTINE pfnLoad, CXipFile* pXip) // if (!bSync) { start the loader thread; return xip; } // xip->Load(); return xip; // The key keeps "default" while Open drops it, so default.xip never matches -// itself here; nothing loads it twice. Theseus refuses when the pool is -// full, where 5960 writes past it, a sync caller learns about a failed load, +// itself here; nothing loads it twice. Theseus grows the pool where 5960 +// would write past it, a sync caller learns about a failed load, // and a name too long or with no extension is refused. isSkin is Theseus's // and unused here. CXipFile* LoadXIP(const TCHAR* szURL, bool bSync, bool isSkin) @@ -251,7 +268,7 @@ CXipFile* LoadXIP(const TCHAR* szURL, bool bSync, bool isSkin) for (int i = 0; i < c_nXipFileCount; i++) { - CXipFile* pXip = &c_rgXipFile[i]; + CXipFile* pXip = c_rgXipFile[i]; if (_stricmp(szKey, pXip->m_dirPath) != 0) continue; @@ -261,15 +278,16 @@ CXipFile* LoadXIP(const TCHAR* szURL, bool bSync, bool isSkin) return pXip; } - if (c_nXipFileCount >= (int)countof(c_rgXipFile)) - { - TRACE(_T("\001LoadXIP: XIP pool full (%d), cannot load %s\n"), (int)countof(c_rgXipFile), szPath); - return NULL; - } + if (c_nXipFileCount == c_nXipFileSlots) + GrowXipPool(); - CXipFile* pXip = &c_rgXipFile[c_nXipFileCount]; + CXipFile* pXip = new CXipFile; if (!pXip->Open(szPath)) + { + delete pXip; return NULL; + } + c_rgXipFile[c_nXipFileCount] = pXip; c_nXipFileCount++; if (!bSync) @@ -308,7 +326,7 @@ bool FindInXIPAndDetach(const TCHAR* szURL, BYTE*& pbContent, DWORD& cbContent) for (int i = c_nXipFileCount - 1; i >= 0; i--) { - CXipFile& xip = c_rgXipFile[i]; + CXipFile& xip = *c_rgXipFile[i]; if (xip.m_locked || !xip.m_loaded) continue; @@ -387,7 +405,7 @@ void* FindObjectInXIP(const TCHAR* szURL, const TCHAR* szFilename, int nType) for (int i = c_nXipFileCount - 1; i >= 0; i--) { - CXipFile& xip = c_rgXipFile[i]; + CXipFile& xip = *c_rgXipFile[i]; if (xip.m_locked || !xip.m_loaded) continue; @@ -1000,10 +1018,13 @@ void CXipFile::DeleteMeshBuffers() // The out of memory path calls this to free the archive drawn longest ago. bool CleanupMeshCache() { + // Count first: a slot under the count is always in the table. + const int nXips = c_nXipFileCount; + CXipFile** rgXips = c_rgXipFile; CXipFile* pOldest = NULL; - for (int i = 0; i < c_nXipFileCount; i++) + for (int i = 0; i < nXips; i++) { - CXipFile* pXip = &c_rgXipFile[i]; + CXipFile* pXip = rgXips[i]; if (pXip->m_locked || !pXip->m_loaded) continue; if (pXip->m_vertexBufferCount == 0 || pXip->m_meshBuffers[0].m_vertexBuffer == NULL) From 1b93d3291cdcff4df1787875854142c263c888a1 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:57:19 -0500 Subject: [PATCH 105/167] Fixed the first game in a list always showing a blank icon The list was filled before it was marked as a title list, so the row the cursor lands on skipped the games library lookup and fell back to the old Icons.ini path. Icons only turned up once you moved off it. Repacked default.xip. Fixes #85 --- Data/Xips/default.xip | Bin 1686934 -> 1687007 bytes xips-source/default-desktop/harddrive.xap | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Data/Xips/default.xip b/Data/Xips/default.xip index 08f55fe33e5b16777450dd39e98daca585fa4574..a3063b873f9449f661762778cb080e0b12b3c02c 100644 GIT binary patch delta 407 zcmbRCJMI4OG`q)f~67i{mp@l?SYIS%ml>DK+FQftU$~L#Oy%K0mPg@%mu{UK+FTg zyg83h7*-1YSpf>TQrl2X(1i&7OzGEx=NGK-2!fMiaNLUF!AQGU6CfkI+& zb}?8oIX@>SHMt}+KTiRuK_RoGSRpexKTmJEVxNHc^u}#$qSLqb3gk{d{gs7h`v2K% jtnHqC0zfPX#6mzU48$TpEDFS8Kr9Z#65BodBz^S(aW!J2 delta 335 zcmccrJ8jzUG`JtEBK_C_aVqqW_ V0b)@g76W2&AePuZsZY{Z9{?NhOo#vg diff --git a/xips-source/default-desktop/harddrive.xap b/xips-source/default-desktop/harddrive.xap index 95d6fc4..2008f8e 100644 --- a/xips-source/default-desktop/harddrive.xap +++ b/xips-source/default-desktop/harddrive.xap @@ -288,11 +288,12 @@ function StartTitleList() c.audio_panel.SetTranslation(-45.549999, 55.07, -204.5); nPlayScroll0 = 0; nPlayCursor0 = 0; + // Set before the first fill so row 0 asks the collection for its icon. + bInTitleList = true; UpdateTrackList(); theTitleListViewpoint.isBound = true; CurrentViewpoint = theTitleListViewpoint; bLauncherVisible0 = false; - bInTitleList = true; } function EnterLauncherMenu() From 2adcc499e871f8facfc4df5566036ce61e75ae64 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:59:12 -0500 Subject: [PATCH 106/167] Brought the desktop docs up to date with the code They still described the OpenGL renderer, the old xboxfs paths and a DVD scene the media player no longer uses. Rewrote them against bgfx, the host layer and the user data folder, and moved the build steps out of the README into building.md. --- docs/desktop/README.md | 18 +- docs/desktop/building.md | 133 ++++++++++++++ docs/desktop/d3d8-translation.md | 204 ++++++---------------- docs/desktop/dependencies.md | 92 +++++++++- docs/desktop/desktop-features.md | 220 +++++++---------------- docs/desktop/media-player-design.md | 129 ++++---------- docs/desktop/porting-to-pc.md | 260 ++++++---------------------- 7 files changed, 427 insertions(+), 629 deletions(-) create mode 100644 docs/desktop/building.md diff --git a/docs/desktop/README.md b/docs/desktop/README.md index 3855a9b..07c6cf6 100644 --- a/docs/desktop/README.md +++ b/docs/desktop/README.md @@ -1,12 +1,12 @@ -# Desktop Port Documentation +# Desktop Documentation -Technical documentation for the desktop port of Theseus: the Xbox dashboard engine running natively on macOS, Linux, and Windows via SDL2 + OpenGL. +How the Xbox dashboard engine runs on macOS, Linux and Windows as UIX Desktop: SDL2 for windows, input and sound, bgfx for rendering (Metal, Direct3D 11, Vulkan), libmpv for video. -For the engine itself (Script VM, scene graph, XIP archives), see [`docs/decomp/`](../decomp/). For the XAP script API contract that both Xbox and desktop builds must honor, see [`docs/xap-contract.md`](../xap-contract.md). +For the engine itself (script VM, scene graph, XIP archives) see [`docs/decomp/`](../decomp/). For the script API both builds have to honor, see [`docs/xap-contract.md`](../xap-contract.md). -## Contents - -- **[Porting to PC](porting-to-pc.md)**: How Xbox D3D8, kernel APIs, and filesystem calls were replaced with SDL2/OpenGL for macOS, Linux, and Windows -- **[D3D8 to OpenGL Translation](d3d8-translation.md)**: The rendering layer that makes Xbox Direct3D 8 calls work on modern OpenGL 3.2 -- **[Desktop Features](desktop-features.md)**: Game launching, Steam integration, Title Maker, development tools, media player -- **[Media Player Design](media-player-design.md)**: libmpv integration with the Xbox DVD player XAP scene +- **[Building](building.md)**: every desktop target, the one time bgfx and libdatachannel steps, Windows cross builds, packaging +- **[Dependencies](dependencies.md)**: packages per OS, minimum library versions, the Windows cross deps +- **[Desktop Features](desktop-features.md)**: the games grid and launching, Title Maker, media, music, xCloud, skins, CRT, the tool windows, settings files +- **[Porting to PC](porting-to-pc.md)**: the platform layer, drive letters and the user data folder, audio, input, nodes, logs +- **[D3D8 to bgfx](d3d8-translation.md)**: how the dashboard's Direct3D 8 calls are drawn through bgfx +- **[Media Player](media-player-design.md)**: the media library, libmpv playback, and what became of the DVD player diff --git a/docs/desktop/building.md b/docs/desktop/building.md new file mode 100644 index 0000000..7e56ec2 --- /dev/null +++ b/docs/desktop/building.md @@ -0,0 +1,133 @@ +# Building UIX Desktop + +Everything builds from one Makefile in `build/`. The libraries each OS needs, and the versions they have to clear, are in [dependencies.md](dependencies.md). The CI workflows in `.github/workflows/` build every desktop target on every push, so when this page and the workflows disagree, trust the workflows. + +| Target | Command (from `build/`) | Output | +|---|---|---| +| macOS / Linux, native | `make desktop` | `~/builds/theseus/desktop/theseus` | +| Windows x64, cross from macOS or Linux | `make desktop-win64` | `~/builds/theseus/desktop-win64/theseus.exe` | +| Linux ARM64, cross from x86_64 Debian/Ubuntu | `make desktop-linux-arm64` | `~/builds/theseus/desktop-linux-arm64/theseus` | + +`BUILDS_ROOT=/some/dir` moves the output somewhere other than `~/builds/theseus`. `DEBUG=1` builds with `-g -O0`, and `ASAN=1` adds AddressSanitizer to a native build. + +## 1. Clone with submodules + +bgfx, bx, bimg, projectM and libdatachannel are submodules under `theseus/third-party/`. + +``` +git clone --recursive https://github.com/MrMilenko/Theseus +# or, in an existing clone: +git submodule update --init --recursive +``` + +## 2. Install the packages + +See [dependencies.md](dependencies.md) for the per OS package lists. The build finds every library through `pkg-config`, so the package manager doesn't matter. If something sits off the default search path, point `PKG_CONFIG_PATH` at its `.pc` files. On macOS with Homebrew, curl is keg only: + +``` +export PKG_CONFIG_PATH="$(brew --prefix curl)/lib/pkgconfig:$PKG_CONFIG_PATH" +``` + +## 3. Build the bgfx runtime libraries (once per platform) + +Only `bx`, `bimg` and `bgfx` are needed. bgfx's own top level targets also build `shaderc`, `geometryc` and a debug config, which takes far longer for nothing this build uses. + +``` +cd theseus/third-party/bgfx + +# macOS (Apple Silicon) +../bx/tools/bin/darwin/genie --gcc=osx-arm64 gmake +make -C .build/projects/gmake-osx-arm64 -j bx bimg bgfx config=release + +# Linux +../bx/tools/bin/linux/genie --gcc=linux-gcc gmake +make -C .build/projects/gmake-linux-gcc -j bx bimg bgfx config=release64 + +# Windows, cross from Linux +../bx/tools/bin/linux/genie --gcc=mingw-gcc gmake +make -C .build/projects/gmake-mingw-gcc -j bx bimg bgfx config=release64 \ + CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ +``` + +The `genie` that ships prebuilt in bx needs a recent glibc. On an older distro it fails with `GLIBC_2.38 not found`; build it from source and carry on: + +``` +git clone --depth 1 https://github.com/bkaradzic/GENie.git /tmp/genie +make -C /tmp/genie +cp /tmp/genie/bin/linux/genie theseus/third-party/bx/tools/bin/linux/genie +``` + +The compiled shaders are checked in under `Data/shaders/` for every backend (Metal, SPIR-V, Direct3D 11, GLSL, ESSL), so a normal build never runs `shaderc`. Only if you edit a `.sc` file in `theseus/desktop/shaders/` do you need the `shaders-bgfx*` targets, and those want a built `shaderc`. + +## 4. Build libdatachannel (once per platform) + +This is the WebRTC transport behind Xbox Cloud Gaming and remote play. It's easy to skip and the failure is quiet: the Makefile only defines `THESEUS_HAVE_WEBRTC` when it finds the static library, so without this step everything compiles and links and xCloud is simply missing. The folder name matters: `build-mac`, `build-linux` or `build-win64`, because that's where the Makefile looks. + +``` +cd theseus/third-party/libdatachannel + +# macOS +cmake -S . -B build-mac -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ + -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 \ + -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" +cmake --build build-mac -j + +# Linux +cmake -S . -B build-linux -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ + -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 +cmake --build build-linux -j +``` + +The Windows cross build of it is in [Windows](#windows) below. + +## 5. Build and run + +``` +cd build +make desktop +~/builds/theseus/desktop/theseus +``` + +The first `make desktop` also builds the vendored projectM into a local prefix and drops its shared library in `lib/` beside the binary. Each build copies `Configs/`, `Data/` and `Library/` next to the binary and stamps `Configs/version` with the build date and commit. + +When the app runs, the copies beside the binary are only the starting point. Your settings and library live in the per user data folder (see the main README), seeded from those copies on first run. + +## Windows + +The release is cross compiled from Linux with mingw-w64 (`x86_64-w64-mingw32-g++`, posix thread model). The pieces mingw doesn't ship: + +- **SDL2 and SDL2_mixer** mingw devel tarballs from libsdl.org, unpacked into `~/cross/sdl2-mingw`. The preamble of the `desktop-win64` section in `build/Makefile` has the exact commands. +- **libmpv** dev archive (shinchiro's mpv-winbuild-cmake releases) into `~/cross/mpv-mingw`, and the **curl** mingw zip from curl.se into `~/cross/curl-mingw`. +- **OpenSSL, opus and a trimmed FFmpeg**: `build/fetch-mingw-deps.sh` puts them in `~/cross/mingw64`. [dependencies.md](dependencies.md) explains why FFmpeg is built rather than fetched. +- **libdatachannel**, cross built against that prefix: + +``` +cmake -S theseus/third-party/libdatachannel \ + -B theseus/third-party/libdatachannel/build-win64 \ + -DCMAKE_TOOLCHAIN_FILE="$PWD/build/cmake-toolchains/mingw-w64-x86_64.cmake" \ + -DCMAKE_FIND_ROOT_PATH="$HOME/cross/mingw64" \ + -DOPENSSL_ROOT_DIR="$HOME/cross/mingw64" \ + -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ + -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 \ + -DENABLE_WARNINGS_AS_ERRORS=OFF +cmake --build theseus/third-party/libdatachannel/build-win64 -j +``` + +Then `make desktop-win64`. The output folder is a complete drop: the exe, every runtime DLL, and Configs/Data/Library. The `release-win64` job in `.github/workflows/release.yml` is the whole recipe end to end. + +`make desktop` under an MSYS2 MinGW64 shell is also detected, but the cross build is the one CI tests. + +## Linux ARM64 + +The release ARM64 build runs natively on an ARM64 runner with the same steps as x64, plus a GENie built from source. To cross compile from an x86_64 Debian or Ubuntu host instead, enable the arm64 multiarch packages listed at the top of `build/Makefile` and run `make desktop-linux-arm64`. The binary links against the build host's glibc, so build on a host whose glibc is no newer than the target's. + +## Packaging + +- **macOS app**: `build/mk-macapp.sh` wraps a desktop build into `~/builds/theseus/Theseus.app`. It copies the Homebrew dylibs into `Contents/Frameworks`, rewrites their install names to `@rpath` so the bundle runs without your Homebrew layout, builds the icon, and ad hoc signs the result. Ad hoc is enough to run it; handing it to other people properly needs a Developer ID and notarization. +- **Linux AppImage**: `build/mk-appimage.sh [build dir] [output]` bundles the binary with every library it pulls in, except the ones that have to come from the host (glibc, GPU drivers, X11, Wayland, audio servers), and needs `patchelf`. + +## Before you send a change + +- `make lint-platform`: only `theseus/desktop/host_*` may include OS headers or test OS macros. `_WIN32` is true on the Xbox too, so shared code goes through `host.h` instead. +- `make win64-import-guard`: compiles the Windows objects and fails if anything outside `host_win*` calls the Win32 file, clock or sleep functions directly. +- Build every desktop target you can, and the Xbox build if you touched `engine/`, `shared/` or `render/`. diff --git a/docs/desktop/d3d8-translation.md b/docs/desktop/d3d8-translation.md index 484a74b..4ea650c 100644 --- a/docs/desktop/d3d8-translation.md +++ b/docs/desktop/d3d8-translation.md @@ -1,184 +1,80 @@ -# D3D8 to OpenGL Translation +# D3D8 to bgfx -## The Problem +## The problem -The Xbox dashboard renders everything through Direct3D 8, Microsoft's graphics API from 2000. D3D8 predates programmable shaders (it has vertex shaders but no pixel shaders in the modern sense), uses a fixed-function texture blending pipeline, and has a completely different state model from modern OpenGL. +The Xbox dashboard draws everything through the Xbox flavor of Direct3D 8, on an NV2A GPU with a fixed function texture combiner and a handful of custom vertex shaders. The scene graph code calls `SetRenderState`, `SetTextureStageState`, `SetTransform` and `DrawIndexedPrimitive` like any 2001 game, and it expects Xbox texture formats and Xbox vertex layouts. -The desktop port doesn't use a general-purpose D3D8-to-OpenGL translator. Instead, `d3d8_sdl.h` implements just enough of the D3D8 interface to render the dashboard, approximately 2,600 lines of purpose-built translation code. +The desktop build doesn't carry a general D3D8 emulator. `theseus/desktop/d3d8_sdl.h` implements the slice of the D3D8 interface the dashboard actually uses and backs it with [bgfx](https://github.com/bkaradzic/bgfx), which picks Metal on macOS, Direct3D 11 on Windows and Vulkan on Linux (overridable in Settings > Display > Renderer). The dashboard code above it doesn't know the difference. -## Architecture +## Shape of the shim -The translation layer provides stub classes that mirror the D3D8 COM interfaces: - -``` -D3D8 Interface Desktop Implementation -───────────────── ────────────────────── -IDirect3D8 → Returns a single "SDL OpenGL" adapter -IDirect3DDevice8 → OpenGL state machine + GLSL shaders -IDirect3DTexture8 → GL texture objects -IDirect3DVertexBuffer8 → CPU-side vertex arrays with transform caching -IDirect3DIndexBuffer8 → CPU-side index arrays -IDirect3DSurface8 → GL framebuffer attachments -``` - -## IDirect3DDevice8: The Core - -Every D3D8 call in the dashboard goes through `CDashApp` wrapper functions that call into `IDirect3DDevice8`. On desktop, this class manages: - -### Render State Tracking - -D3D8 uses `SetRenderState()` with ~200 possible state flags. The translation layer tracks the ones the dashboard actually uses: - -| D3D8 Render State | OpenGL Equivalent | -|-------------------|-------------------| -| `D3DRS_ZENABLE` | `glEnable/glDisable(GL_DEPTH_TEST)` | -| `D3DRS_ALPHABLENDENABLE` | `glEnable/glDisable(GL_BLEND)` | -| `D3DRS_SRCBLEND` / `D3DRS_DESTBLEND` | `glBlendFunc()` | -| `D3DRS_CULLMODE` | `glEnable(GL_CULL_FACE)` + `glCullFace()` | -| `D3DRS_LIGHTING` | Tracked as a flag, applied in vertex shader | -| `D3DRS_FILLMODE` | `glPolygonMode()` | -| `D3DRS_ALPHATESTENABLE` / `D3DRS_ALPHAREF` | Fragment shader `discard` | -| `D3DRS_TEXTUREFACTOR` | Uniform passed to fragment shader | - -The dashboard sets ~40 different render states during initialization. Most are Xbox-specific display modes (flicker filter, soft display filter, swath width) or features not needed on desktop (stencil, fog, point sprites). These are accepted by `SetRenderState` without error but have no GPU-side effect. - -### Texture Stage States - -D3D8 uses a multi-stage texture blending model. Each stage can have a texture bound and configure how its color and alpha are combined with previous stages: - -```cpp -// Dashboard typically does: -SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); -SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); -SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); -SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE); -``` - -The translation layer tracks stage 0 and stage 1 configurations and generates GLSL fragment shader logic to match. The most common patterns: -- **MODULATE**: `texture_color * vertex_color` (the default) -- **SELECTARG1**: Use texture color only -- **SELECTARG2**: Use vertex color only -- **ADD**: `texture_color + vertex_color` - -### Transform Pipeline - -D3D8 uses separate World, View, and Projection matrices set via `SetTransform()`. The translation layer captures these and combines them into an MVP (Model-View-Projection) matrix for the GLSL vertex shader: - -```cpp -// D3D8 code sets: -SetTransform(D3DTS_WORLD, &matWorld); -SetTransform(D3DTS_VIEW, &matView); -SetTransform(D3DTS_PROJECTION, &matProjection); - -// Translation layer combines to: -mat4 MVP = Projection * View * World; -// Passed to vertex shader as uniform ``` - -## GLSL Shaders - -The dashboard's rendering is handled by two GLSL shaders: - -### Vertex Shader - -```glsl -// Simplified. Actual shader handles more cases. -uniform mat4 u_MVP; -uniform vec4 u_falloffCenter; -uniform float u_falloffRadius; -uniform float u_falloffIntensity; - -in vec3 a_position; -in vec4 a_color; -in vec2 a_texcoord; - -out vec4 v_color; -out vec2 v_texcoord; - -void main() { - gl_Position = u_MVP * vec4(a_position, 1.0); - v_color = a_color; - v_texcoord = a_texcoord; -} +D3D8 interface Desktop implementation +IDirect3D8 One adapter, one device +IDirect3DDevice8 Render state mirror, packed into bgfx state and shader uniforms +IDirect3DTexture8 CPU side RGBA pixels, uploaded to a bgfx texture on first use +IDirect3DVertexBuffer8 Vertex data plus a bgfx layout built from the FVF on first draw +IDirect3DIndexBuffer8 WORD indices, expanded to triangle lists at draw time +D3DX math Matrices, vectors, quaternions, inline ``` -### Fragment Shader - -```glsl -// Simplified. Actual shader handles TSS state combinations. -uniform sampler2D u_texture; -uniform bool u_hasTexture; -uniform float u_alphaRef; - -in vec4 v_color; -in vec2 v_texcoord; - -out vec4 fragColor; +Everything the dashboard draws in a frame lands in bgfx view 0. The CRT effect, video and the ImGui tools layer on after. -void main() { - vec4 texColor = u_hasTexture ? texture(u_texture, v_texcoord) : vec4(1.0); - fragColor = texColor * v_color; - if (fragColor.a < u_alphaRef) discard; -} -``` - -The actual shaders are more complex, handling independent color and alpha operations, multiple texture stages, and various blend modes configured by the TSS tracking. +## Render state -## Per-Vertex Falloff Lighting +D3D8 has a couple hundred render states. The shim tracks the ones the dashboard sets and folds them into a `BGFX_STATE_*` bitmask that rides along with each draw: -The dashboard's signature visual effect is a radial falloff, a spotlight-like effect where objects near the camera's focus are bright and objects further away fade to dark. On Xbox, this was implemented as a custom vertex shader (`effect.vsh`) running on the NV2A GPU. +| D3D8 | bgfx | +|---|---| +| `D3DRS_ZENABLE`, `D3DRS_ZFUNC`, `D3DRS_ZWRITEENABLE` | depth test bits and `BGFX_STATE_WRITE_Z` | +| `D3DRS_ALPHABLENDENABLE`, `D3DRS_SRCBLEND`, `D3DRS_DESTBLEND` | blend function bits | +| `D3DRS_CULLMODE` | cull bits (bgfx's default winding is clockwise, same as D3D) | +| `D3DRS_ALPHATESTENABLE`, `D3DRS_ALPHAREF` | a shader uniform, `discard` in the fragment shader | +| `D3DRS_TEXTUREFACTOR` | a shader uniform | -On desktop, this is computed on the CPU during vertex transformation: +The rest are accepted and dropped: NV2A display tricks (flicker filter, soft display, swath width), fog, stencil, point sprites, vertex blending. The dashboard sets them during init and never relies on them. -``` -For each vertex: - 1. Transform position by World-View-Projection matrix - 2. Compute distance from vertex to falloff center (in view space) - 3. Calculate attenuation: brightness = 1.0 - saturate(distance / radius) - 4. Multiply vertex color by attenuation - 5. Store result in vertex diffuse color -``` +Sampler state (`D3DTSS_ADDRESSU/V`, `MINFILTER`, `MAGFILTER`) becomes `BGFX_SAMPLER_*` flags passed with the texture on each draw. -The falloff parameters (center, radius, intensity) are extracted from the D3D8 vertex shader constants that the dashboard sets. Multiple shader variants exist for different numbers of lights and different vertex formats. +F8 dumps a count of how often each render and texture stage state is set, and how many of those sets changed nothing. Shift+F8 resets the counters. -## Matrix Caching +## The fixed function emulator -The device caches the combined World-View-Projection (`m_matWVP`) and World-View (`m_matWV`) matrices, recomputing them only when a dirty flag is set by `SetTransform()`. This avoids redundant matrix multiplications when the same camera/world state is used across multiple draw calls in a frame. +Two bgfx shaders stand in for the whole fixed function pipeline: `vs_ff.sc` and `fs_ff.sc` in `theseus/desktop/shaders/`. Rather than compiling a shader per state combination, the shim packs the current state into a few `vec4` flag uniforms and the shaders branch on them. -## Texture Handling +**Vertex side** (`vs_ff`): -### XBX Textures (Xbox native) +- Transforms by the world view projection matrix, or passes pretransformed `XYZRHW` vertices straight through for 2D overlays +- Picks the vertex color from falloff, vertex diffuse, texture factor, material diffuse or plain white +- Reads Xbox `NORMPACKED3` normals (11:11:10 in four bytes) as well as float normals +- Does spherical environment mapping for the materials that use it -Xbox textures use DXT compression with swizzled pixel layout: +**Falloff** is the dashboard's signature look: the faint glow along the edges of the green shell and the brighter faces pointing at you. On the Xbox it's `effect.vsh`. The desktop shader does the same math on the GPU: side color plus (front color minus side color) times the absolute dot product of the view direction and the normal. Faces turned toward the camera get the front color and edges fade to the side color. Both colors come from the material, through the same vertex shader constants the Xbox code sets. -1. **Read XBX header**: Extract width, height, format (DXT1/DXT3/DXT5), mipmap count -2. **Deswizzle**: Convert from Morton order to linear scanline order -3. **Upload to OpenGL**: `glCompressedTexImage2D()` with the appropriate GL format: - - DXT1 → `GL_COMPRESSED_RGBA_S3TC_DXT1_EXT` - - DXT3 → `GL_COMPRESSED_RGBA_S3TC_DXT3_EXT` - - DXT5 → `GL_COMPRESSED_RGBA_S3TC_DXT5_EXT` +**Fragment side** (`fs_ff`) covers the texture stage setups the dashboard uses: modulate, select texture, select diffuse and add for color; modulate or select for alpha; an optional second stage that adds color or multiplies alpha; and the alpha test. With no texture bound, a 1x1 white texture keeps the sample well defined. -### Standard Textures (Desktop) +## Draw calls -PNG, JPG, and BMP textures (used by skins and the Title Maker) are loaded via `stb_image` and uploaded as uncompressed RGBA. +- `DrawIndexedPrimitive` submits through a dynamic vertex buffer and a transient index buffer. +- `DrawPrimitiveUP` and `DrawIndexedPrimitiveUP` copy the caller's data into transient buffers each draw. +- Strips, fans and the Xbox only `D3DPT_QUADLIST` are expanded to triangle lists, so bgfx only ever sees triangles. The scratch buffer for that lives for the whole run. +- Vertex layouts come from the FVF on first use. Float normals map to bgfx's normal attribute; packed normals ride in a color attribute and get unpacked in the shader. -### Texture Cache +## Textures -The dashboard loads textures frequently as scenes change. A texture cache (`TXTCACHE[100]`) stores recently-used GL textures by name. On skin changes, the cache is flushed and all `CImageTexture` nodes are marked dirty for reload. +Dashboard textures are `.xbx` files: Xbox XPR0 resources holding DXT1, DXT3, DXT5 or linear data, usually swizzled into Morton (Z) order for the NV2A. `xbx_texture.h` parses the header, decodes DXT blocks on the CPU and unswizzles to plain RGBA. PNG, JPG and BMP (skins, Steam icons, box art) go through `stb_image`. Either way the result is RGBA pixels on the CPU side of `IDirect3DTexture8`. -## What's NOT Translated +Upload to bgfx happens the first time a texture is drawn. Textures the dashboard rewrites every frame (the music visualizer, dynamic textures) are created with `BGFX_TEXTURE_BLIT_DST`. Without it, Metal can put the texture in GPU only memory and silently ignore later CPU updates, and the visualizer freezes on its first frame. -The translation layer deliberately skips D3D8 features the dashboard doesn't use: +## Things drawn outside the scene -- **Pixel shaders**: D3D8 pixel shaders aren't used by the dashboard (it uses TSS instead) -- **Stencil buffer**: Not used -- **Multi-render-targets**: Not used -- **Fog**: Set during init but not rendered (states accepted, no GL calls) -- **Point sprites**: Not used -- **Vertex declarations** (beyond FVF): The dashboard uses FVF exclusively +A few things skip the D3D8 shim entirely: -This keeps the translation layer small and focused. A general-purpose D3D8 wrapper would be 10x larger; this is purpose-built for one application. +- **CRT effect**: when it's on, view 0 renders into an offscreen framebuffer and `fs_crt.sc` draws that to the backbuffer with scanlines, curvature, phosphor mask, vignette, bloom, flicker and color bleed. +- **Video**: libmpv renders each frame into a CPU buffer that's uploaded to a bgfx texture and drawn fullscreen with `vs_blit` and `fs_blit` (see [media-player-design.md](media-player-design.md)). It goes through the CRT pass too. +- **Boot animation and xCloud streams**: the same blit path. +- **ImGui tools**: their own bgfx backend (`imgui_impl_bgfx.cpp`) with `vs_imgui` and `fs_imgui`. -## Coordinate System Differences +## Shaders on disk -D3D8 uses a left-handed coordinate system (Y up, Z into screen). OpenGL uses right-handed (Y up, Z out of screen). The translation layer handles this by negating the Z component in the projection matrix, which flips the winding order. `glFrontFace(GL_CW)` compensates for the reversed triangle winding. +Shader sources are `theseus/desktop/shaders/*.sc`. The compiled binaries for every backend are checked in under `Data/shaders/` (`metal`, `spirv`, `dx11`, `glsl`, `essl`), and the app loads the folder matching the renderer bgfx picked. They're the one part of `Data/` that stays with the app instead of moving to the user data folder. If that folder is missing, the log says so near the top, because otherwise the only symptom is a black window. diff --git a/docs/desktop/dependencies.md b/docs/desktop/dependencies.md index 11834b6..4a6e233 100644 --- a/docs/desktop/dependencies.md +++ b/docs/desktop/dependencies.md @@ -1,7 +1,81 @@ # Desktop dependencies What the desktop build links against on each platform, the floor each library -has to clear, and where the Windows cross-deps come from. +has to clear, and where the Windows cross deps come from. The build steps +themselves are in [building.md](building.md). + +## Packages + +Everything is found through `pkg-config`, and nothing in the build shells out +to a package manager. If `pkg-config --exists mpv libcurl libavcodec opus` +succeeds, the build will find them, however they got there. Prefixes off the +default search path just need `PKG_CONFIG_PATH`. + +"Run" is what a downloaded release needs. "Build" is that plus the toolchain +and headers. The Windows zip and the Linux AppImage carry their own libraries, +so they need nothing installed. + +**macOS (Homebrew)** + +``` +# Run the bare binary release (the .app needs nothing) +brew install sdl2 sdl2_mixer mpv curl ffmpeg opus + +# Build +brew install sdl2 sdl2_mixer mpv curl ffmpeg opus pkg-config cmake openssl@3 +``` + +**macOS (MacPorts)** + +``` +# Run +sudo port install libsdl2 libsdl2_mixer mpv curl ffmpeg libopus + +# Build +sudo port install libsdl2 libsdl2_mixer mpv curl ffmpeg libopus \ + pkgconfig cmake openssl3 +``` + +**Debian / Ubuntu** + +``` +# Run the tarball release +sudo apt install libsdl2-2.0-0 libsdl2-mixer-2.0-0 libmpv2 libcurl4 libopus0 + +# Build +sudo apt install build-essential pkg-config cmake \ + libsdl2-dev libsdl2-mixer-dev \ + libvulkan-dev libx11-dev libgl-dev \ + libmpv-dev libcurl4-openssl-dev libssl-dev \ + libavcodec-dev libavutil-dev libswscale-dev libopus-dev +``` + +**Fedora** + +``` +sudo dnf install gcc-c++ pkgconf-pkg-config cmake SDL2-devel SDL2_mixer-devel \ + mpv-libs-devel libcurl-devel openssl-devel \ + ffmpeg-devel opus-devel vulkan-loader-devel libX11-devel +``` + +**Arch** + +``` +sudo pacman -S base-devel pkgconf cmake sdl2 sdl2_mixer mpv curl openssl \ + ffmpeg opus vulkan-icd-loader libx11 +``` + +Notes: + +- Some distros ship `libmpv1` instead of `libmpv2`. Either works. +- The FFmpeg runtime libraries come in with libmpv. FFmpeg and Opus also + cover the xCloud video and audio decoders. +- OpenSSL is a runtime dependency of xCloud. On macOS it arrives alongside + curl, mpv and FFmpeg, and on Debian `libssl3` is in the base install, so + neither run list names it. Building libdatachannel does need the headers, + which is why `openssl@3` and `libssl-dev` are in the build lists. +- projectM is built from the submodule on the first `make desktop` and ships + in `lib/` beside the binary, so it isn't a package you install. ## Minimum versions @@ -36,11 +110,13 @@ should link and run. | glibc | n/a | 2.41 | n/a | The Linux binary links libavcodec 61 and glibc 2.41, and a tester on -libavcodec 62 could not load a build made against 61. That is why the Linux -tarball bundles its whole `ldd` closure except the host-tied libraries -(glibc, GPU driver GL/EGL/Vulkan/DRM, X11/xcb/Wayland/xkb). +libavcodec 62 could not load a build made against 61. That is why the +AppImage bundles the binary's whole `ldd` closure except the libraries that +have to come from the host (glibc, GPU driver GL/EGL/Vulkan/DRM, X11, xcb, +Wayland, audio servers). The tarball only carries projectM in `lib/` and +uses the system's copies of everything else. -## Windows cross-deps +## Windows cross deps `build/fetch-mingw-deps.sh` populates `~/cross/mingw64` and both CI workflows call it. Two different strategies in there: @@ -54,10 +130,10 @@ call it. Two different strategies in there: HEVC is enabled even though xCloud sends H.264. `h2645_sei.o` calls `ff_aom_uninit_film_grain_params`, and libavcodec only compiles -`aom_film_grain.o` when an HEVC config is on, so an h264-only build leaves that +`aom_film_grain.o` when an HEVC config is on, so an H.264 only build leaves that symbol dangling. -The other cross-deps (SDL2, SDL2_mixer, libmpv, libcurl) are fetched by the +The other cross deps (SDL2, SDL2_mixer, libmpv, libcurl) are fetched by the workflows into `~/cross/{sdl2-mingw,mpv-mingw,curl-mingw}`. ### Two traps worth knowing @@ -72,7 +148,7 @@ against the static archive. Its libsrtp also needs `-DENABLE_WARNINGS_AS_ERRORS=OFF`, because `u_long` is 32 bit under LLP64 and its `%x` format checks trip `-Werror=format`. -## Streaming is opt-in at build time +## Streaming is optional at build time xCloud and Xbox remote play only compile in when libdatachannel is present: `build-mac`, `build-linux`, or `build-win64` under diff --git a/docs/desktop/desktop-features.md b/docs/desktop/desktop-features.md index d8209ed..d943cde 100644 --- a/docs/desktop/desktop-features.md +++ b/docs/desktop/desktop-features.md @@ -1,196 +1,108 @@ # Desktop Features -## Beyond a Port +UIX Desktop is the Xbox dashboard first and a PC launcher second. The scene scripts drive everything you see; the desktop build adds what a PC needs underneath them, plus a set of ImGui tool windows for managing your library and building skins and scenes. This page is the tour. Setup steps for users are in the main [README](../../README.md). -UIX Desktop isn't just the Xbox dashboard running on PC. It adds desktop-native features that turn it into a usable game launcher and development tool. The dashboard's scripting system handles the UI, while the platform layer adds PC-specific functionality underneath. +## The games grid -## Game Launching +On the Xbox, the dashboard found games by walking `E:\Games` and reading each `default.xbe` for its title and icon. The desktop build keeps that exact flow and feeds it entries that mostly aren't folders at all: -Games are added and managed through the **Title Maker** (F3). You don't need to create files or edit configs by hand: the Title Maker handles everything: game entries, icons, launch commands, and categories. +- **`Configs/games.ini`**: everything added in Title Maker, including Steam and RetroArch imports. One section per entry with `TitleID`, `Launch`, `Drive` and `Category`. +- **ES-DE**: read live from your ES-DE gamelists every launch, under one ES-DE category with a folder per system. +- **Xbox Cloud Gaming**: your cloud library and consoles, fetched after sign in and injected for the session. +- **Real folders** under `Library/Games`, `Library/Applications` and `Library/Emulators`, the same as on an Xbox. -### Adding Games - -- **Manually**: Enter a name, launch command, and optional icon -- **Steam import**: One-click scan of your Steam library: discovers installed games, downloads icons from Steam CDN, and creates entries with `steam://` launch URIs -- **Xbox ISOs**: Browse for an ISO file: the Title Maker extracts the game title and cover art automatically, then creates an xemu launch entry -- **xemu setup**: A "Find" button checks common install paths on each platform (`/Applications/xemu.app`, `/usr/bin/xemu`, `C:\Program Files\xemu\`, etc.) - -Games appear on the dashboard organized by category (Games, Applications, Homebrew, Emulators, etc.) and drive letter, just like the Xbox. - -### How It Works (Under the Hood) - -The Title Maker writes to `games.ini`, which the virtual filesystem layer reads at runtime. When the dashboard scans for games (via `FindFirstFile`), the platform layer injects virtual directory entries from `games.ini` alongside any real folders in `Library/Games/`. The dashboard doesn't know the difference: it sees what looks like real Xbox game folders. - -Icons are stored in `Configs/icons/` and mapped via `Icons.ini`. The dashboard's icon selector searches both UDATA title images and the desktop icon database. +The file layer makes the first three list like folders, and their `default.xbe` opens as a synthesized header carrying the name and title ID. Icons come from `Configs/icons/.jpg` or `.png`. For each Title Maker entry the desktop also writes a small `Library/UDATA//TitleMeta.xbx`, so the Memory screen shows a title pod per game the way the Xbox did. ### Launching -When you select a game, the dashboard: -1. Hides the SDL window -2. Spawns the game process (`fork`/`exec` on POSIX, `CreateProcess` on Windows) -3. Waits for the process to exit -4. Restarts itself via `execv()` (POSIX) or relaunches via `CreateProcess` (Windows) with `--preview` to skip the preloader - -Launch commands can be direct executables, Steam URIs (`steam://rungameid/...`), xemu with an ISO path, or any shell command. The dispatch logic lives in `theseus/desktop/launch.cpp` and follows three rules: +`theseus/desktop/launch.cpp` takes the launch line from the entry: -- **`steam://...`**: invokes the `steam` binary directly on Linux to bypass KIO. KDE/Plasma's URL chain often has no registered handler for the `steam` scheme; calling `steam` directly avoids the "Unknown protocol" error path entirely. macOS and Windows still go through their native URL handlers, which work fine there. -- **Other URLs** (`http://`, `https://`, `file://`, ...): system URL opener (`xdg-open` / `open` / `ShellExecute`). -- **Anything else**: runs through the shell (`/bin/sh -c` on POSIX, `cmd /C` on Windows). Raw executables, paths with arguments, bash scripts, multi-token commands like `firefox --new-window URL` all work. Quote any path containing spaces. +- `xcloud://` and `xhome://` start a cloud or remote play stream inside the dashboard window. +- `steam://` links go to Steam. On Linux the `steam` binary is called directly, because some desktop environments have no handler registered for the scheme. +- Other URLs go to the system URL opener. +- Anything else runs as a shell command from the program's own folder, so executables, scripts, commands with arguments and `$VARIABLES` all work. Quote paths with spaces. -The same dispatcher is used by Title Maker's "Test Launch" button (fire-and-forget, no window hide) and by the dashboard's `.uixshortcut` / virtual-game launches (full hide + wait + restart pipeline). +A real launch fades the screen to black the way the Xbox's launch level did, spawns the game, and minimizes the dashboard so the game gets focus. Title Maker's Test Launch uses the same path without the fade or the minimize, and shows how the spawn went. The full command line is always in the log. ## Title Maker (F3) -The Title Maker is the central tool for managing your game library. It supports adding games manually, importing from Steam, and adding Xbox ISOs with automatic metadata extraction. Each entry gets a name, launch command, icon, category, and drive letter. Icons can be PNG, JPG, or BMP: Steam imports download icons automatically from Steam CDN. - -## Save Game Browser - -The dashboard's Memory section is fully functional on desktop. It reads real Xbox save data from `Library/UDATA/` (or from a mounted qcow2 FATX image) and renders the same save game browser as the original Xbox, complete with: - -- **Title pods** with game icons loaded from `TitleImage.xbx` -- **Title headers** with game names parsed from `TitleMeta.xbx` (UTF-16LE with localization support) -- **Save icon rows** with per-save images (falls back to title-level `SaveImage.xbx` when a save doesn't have its own icon) -- **Save metadata** (name, date, block count) exposed via node functions for the XAP script to display -- **Smooth scrolling** between titles with animated transitions -- **D-pad navigation** to browse titles and individual saves - -The rendering is adapted from the Xbox source's custom `RenderLoop`: it positions pods, headers, and icon rows using D3D transformation matrices rather than the normal scene graph. This matches the Xbox's approach where `SavedGameGrid` was a separate rendering subsystem. - -To test with your own saves, copy Xbox `UDATA` folders into `Library/UDATA/`. Each title folder should contain `TitleMeta.xbx` (the game name) and optionally `TitleImage.xbx` (the icon), with save subfolders containing `SaveMeta.xbx`. - -## Development Tools - -Development tools are gated behind **Development Mode** (set via startup chooser or Settings > General > Startup Mode). In Dashboard Mode, only user-facing features (Title Maker, HDD Browser) appear in the Tools menu. In Development Mode, the Inspector and XAP Editor become available. - -### XAP Script Editor (F2) - -A live script editor for dashboard development, built on ImGuiColorTextEdit: - -- **Syntax highlighting** for VRML/XAP keywords, node types, strings, numbers, and comments -- **Undo/redo** (Ctrl+Z / Ctrl+Y) with full edit history -- **Live reload**: Edit a script, save, and the dashboard reloads the scene without restarting -- **Scene browser**: Navigate inline scenes and browse .xap files on disk -- **Error reporting**: Compiler errors show line numbers and descriptions -- **Floating ImGui panel**: Independent from all other tools, resizable and repositionable - -### Scene Inspector (F1) - -A read-only scene graph browser for understanding what the dashboard is doing: - -- **Scene tree**: Expandable hierarchy of all nodes with search/filter -- **Visibility toggles**: Click to hide/show any node or subtree -- **3D click-to-select**: Click any object in the viewport to select it in the tree (pulsing green highlight) -- **Right-click context menu**: Solo/unsolo, show/hide, recursive operations on subtrees -- **Node properties** (read-only): Transform (position, scale, rotation, alpha), material (diffuse, emissive, transparency), texture (name, dimensions, thumbnail preview), geometry info, mesh file -- **Script variables**: All CInstance members shown with types and current values -- **Stats bar**: Draw call count, visible/total nodes, group count, wireframe toggle - -Property editing is currently read-only. A future update will wire edits back to the XAP source for round-trip editing. - -### Xbox HDD Browser (F5) - -Browse and manage Xbox hard drive images: - -- **qcow2 support**: Mount Xbox HDD images (the format xemu uses) -- **FATX filesystem**: Full read-write access to the Xbox file system -- **Title enumeration**: Browse installed games, saves, and system files -- **Integration**: SavedGameGrid reads titles and icons from mounted images - -## CRT Post-Processing - -An optional retro display filter accessible from Settings (F4) or the View menu, implemented as a full-screen GLSL fragment shader with 8 independent parameters: +The central tool for the library. Tabs: -- **Scanlines**: Horizontal darkened lines simulating a CRT display -- **Curvature**: Barrel distortion simulating a curved CRT screen -- **Phosphor mask**: RGB sub-pixel pattern -- **Vignette**: Corner and edge darkening -- **Bloom**: Glow/light bleed from bright areas -- **Flicker**: Brightness variation simulating unstable signal -- **Color bleed**: Horizontal color smear -- **Brightness**: Overall intensity adjustment +- **Main**: every entry, with search and a category filter. Edit the name, launch line, icon and category; Test Launch; Delete. Create entries by hand, add Xbox ISOs as xemu entries (the title and art are read from the ISO), and Fix Names to tidy up imported titles. +- **Steam**: finds the Steam install (or browse to it), imports every installed game with icons from Steam, adds betas and demos by App ID, opens store pages. +- **RetroArch**: finds the install, imports recently played titles with the right core and box art from RetroArch's thumbnails, or adds a ROM with a core picked from the list. Launch fullscreen is a toggle. +- **ES-DE**: point it at the ES-DE data folder. Rescan rereads it; Compress Art re-encodes everything in `Configs/icons` to 256 pixel JPEGs, which keeps big collections light. -**Presets** (Off, Subtle, Classic, Heavy) set all 8 parameters at once, or each can be tuned individually in the Settings window. Settings persist in `desktop.ini`. CRT effects also apply to media playback. +Steam, RetroArch and ES-DE can each be hidden under Optional Tabs. Hiding one only hides the tab. -## Audio +## Media -The desktop build uses SDL_mixer for audio playback: +- **Local library**: Movies and TV folders, with TMDB posters and synopses if you add a key. +- **Plex**: sign in with a PIN, browse and stream your server. +- **Jellyfin**: server URL plus Quick Connect. +- **Playlist Maker (F6)**: build playlists from the library. -- **Dashboard sounds**: Button clicks, transitions, navigation feedback -- **Background music**: Soundtrack playback from the music directory -- **Mute toggle**: Ctrl+M to mute/unmute all audio (persistent toast overlay) -- **Auto-mute during game launch**: Audio pauses when a game is running -- **IMA ADPCM decoding**: Xbox audio format decoded in software +How the player is wired is in [media-player-design.md](media-player-design.md). -## Media Player +## Music -The dashboard's original Xbox DVD player UI (`dvd.xap`) has been wired to libmpv for file-based media playback. When a media file is opened (File > Open Media), the DVD player scene loads and renders video through the XAP's `texture_panel` mesh while the OSD overlay shows playback info. +The dashboard's music screen reads from your music folder (Settings > Media > Local Library > Music Root): every subfolder is a soundtrack, `.m3u` files become soundtracks, loose files collect in one called Music. Formats: MP3, WAV, OGG, FLAC, WMA, M4A, AAC. Tags and durations are read once and cached. -See [Media Player Design](media-player-design.md) for the full technical breakdown. +With **Use projectM visualizer** on (Settings > Audio), X + Y on the keyboard or a pad opens [projectM](https://github.com/projectM-visualizer/projectm), the open source MilkDrop, in its own window, fed from whatever the dashboard is playing. F or F11 makes it fullscreen. Configure projectM picks presets. -## Preloader +## Xbox Cloud Gaming and remote play -When the application starts (without a startup-mode CLI flag), a startup screen lets you choose: +Sign in under Settings > Media > Xbox / xCloud with a device code. Your Game Pass cloud titles and your consoles appear in the games grid; launching one streams it inside the dashboard window over WebRTC (libdatachannel), with FFmpeg decoding the video and Opus the audio. The same panel lists your consoles with a Stream button for each. -- **Dashboard Mode**: Uses compiled `.xip` binary archives (production mode, user-facing features only) -- **Development Mode**: Uses extracted script directories (supports live editing, enables Inspector and XAP Editor) +## Skins -The choice is saved to `desktop.ini` and can be changed later in Settings > General > Startup Mode. +A skin is a folder in `Data/Skins/`: a `.xbx` file listing material colors, plus any textures and meshes it replaces. The Stock skin is the retail 5960 dashboard, and its colors live in the source (`theseus/render/materials_retail.inl`), so `Stock.xbx` lists nothing. Other skins only list the materials they change. -## CLI Flags +The **Skin Editor** (Tools > Skin Editor) lists your skins, applies one live, creates a new one from Stock, and edits material colors with a picker before saving them to the skin. UIX Lite community skins work as they are. -Pass these on the command line when launching the binary: +## CRT effect -| Flag | What it does | -|------------------|-------------------------------------------------------------------------------| -| `--scale N` | UI scale factor for the ImGui debug overlay (`1.5`, `2.0` for high-DPI / 4K) | -| `--fullscreen` | Borderless fullscreen at native resolution | -| `--4k` | Convenience: `--fullscreen` + `--scale 2.0` | -| `--no-toolbar` | Start with the menu bar hidden (toggle back with F10) | -| `--dashboard` | Start in dashboard mode (skip the preloader picker) | -| `--development` | Start in development mode (enables Inspector, XAP Editor) | -| `--muted` | Start with audio muted | -| `--preview` | Legacy alias for `--dashboard` | +A post process pass over the finished frame, video included, with eight controls: scanlines, curvature, phosphor mask, vignette, bloom, flicker, color bleed and brightness. Subtle, Classic, Heavy and Off presets set them all at once. It's in Settings > Display and View > CRT Effect, and saved in `desktop.ini`. -`--scale` writes to `ImGui::GetIO().FontGlobalScale`. ImGui's `imgui.ini` only stores window layout (positions, sizes, docking state); UI scale is not persisted there and must be re-supplied each launch. +## Development tools -## Menu Bar Visibility +- **Scene inspector (F1)**: the live scene graph as a tree with search. Click in the viewport to select what's under the cursor, hide or solo nodes and subtrees, and read transforms, materials, textures (with a thumbnail), meshes and script variables. A stats bar shows draw calls and node counts, and there's a wireframe toggle. +- **XAP editor (F2)**: a script editor with XAP syntax highlighting, undo and redo, and compile errors with line numbers. Save and the scene reloads in place. +- **Xbox HDD browser (F5)**: opens an xemu qcow2 hard drive image read only, lists its FATX partitions, browses them and exports files. Set the image in Settings > General and the Memory screen reads saves from it too. -The top menu bar can be toggled three ways: +**Development Mode** (chosen at launch when Startup Mode is Ask on launch, or set in Settings > General) unpacks the shipped `.xip` archives into folders under `Data/Xips/` in your user data folder and runs the scenes from those loose files, so script edits show up without repacking. The Development menu only appears in that mode. -- **F10**: global hotkey, toggles in and out -- **File menu, Hide Menu Bar**: discoverable menu item (gone with the bar; F10 brings it back) -- **`--no-toolbar`**: CLI flag, starts hidden +## Display and window -Useful when running fullscreen and you want a clean viewport. +Settings > Display covers display mode (windowed, borderless, fullscreen), resolution, vsync, FPS cap, MSAA, renderer, hardware video decode and Emulate Xbox Resolution, which keeps scenes framed in the original 720x480 design space. F11 toggles fullscreen, F12 a borderless window, F10 the menu bar (it can also auto hide). -## Configuration +### Command line -Desktop-specific settings are stored in `Configs/desktop.ini`: +| Flag | What it does | +|---|---| +| `--fullscreen` | Start fullscreen | +| `--4k` | Fullscreen plus 2x UI scale | +| `--scale N` | Scale the tool windows (for example `1.5` or `2.0` on a high DPI screen) | +| `--no-toolbar` | Start with the menu bar hidden | +| `--muted` | Start muted | +| `--minimized` | Start minimized | +| `--dashboard` | Skip the startup choice and run the dashboard | +| `--development` | Start in Development Mode | +| `--graphics-debug` | Extra renderer diagnostics | -```ini -XemuPath=/Applications/xemu.app -QcowPath=/path/to/xbox.qcow2 -StartupMode=1 -CRT_Enabled=0 -CRT_Scanlines=0.3 -CRT_Curvature=0.2 -CRT_Phosphor=0.3 -CRT_Vignette=0.3 -CRT_Bloom=0.15 -CRT_Flicker=0.2 -CRT_ColorBleed=1.0 -CRT_Brightness=1.05 -``` +## Controllers -Dashboard settings (skins, video mode, language, etc.) use the same `config.ini` as the Xbox version. The configuration system is shared between platforms. +SDL2's GameController API covers Xbox, PlayStation, Switch Pro and most other pads. The dashboard reads them exactly like the Xbox's four ports: D-pad and stick to move, A to select, B to back out, the triggers and Black and White where the scenes use them. LT + B hands the pad to the tool windows, with a prompt bar showing what each button does and an on screen keyboard for text fields. -## Controller Support +## Settings files -SDL2's GameController API provides native support for: +All in `Configs/` inside the user data folder: -- Xbox controllers (wired and wireless via Bluetooth/USB) -- PlayStation DualShock 4 and DualSense controllers -- Nintendo Switch Pro Controller -- Any SDL2-compatible gamepad +| File | What it holds | +|---|---| +| `desktop.ini` | Desktop settings: tool paths (xemu, Steam, RetroArch, ES-DE, qcow2 image), display and CRT, audio, library folders, and Plex, Jellyfin and Xbox sign in tokens | +| `config.ini` | The dashboard's own settings, the same file the Xbox build uses (current skin, main menu wiring, clock and so on) | +| `games.ini`, `Icons.ini`, `icons/` | The launcher library | +| `playlists.ini` | Playlists from the Playlist Maker | -The dashboard maps controller input to the same navigation model as the Xbox: D-pad for movement, A for select, B for back, triggers for page scrolling. All XAP-visible callbacks, the secret key sequence system, screen saver reset, and typomatic repeat logic are preserved. +Every save writes a new file, flushes it to disk and swaps it in, keeping the previous one as `.old`. diff --git a/docs/desktop/media-player-design.md b/docs/desktop/media-player-design.md index 9f9d622..202358d 100644 --- a/docs/desktop/media-player-design.md +++ b/docs/desktop/media-player-design.md @@ -1,119 +1,58 @@ -# Media Player (libmpv + DVD XAP) +# Media Player -The dashboard's original Xbox DVD player UI (`dvd.xap`) is wired to libmpv for file-based media playback on desktop. Video renders through the XAP scene's `texture_panel` mesh while the OSD overlay shows playback info using the same script that ran on the Xbox. +The Xbox could play DVDs, provided you bought the little IR remote dongle that unlocked it. On desktop there's no disc to spin, so UIX Desktop plays your files and your Plex and Jellyfin libraries instead, through libmpv, behind scenes styled like the rest of the dashboard. -## How Xbox DVD Playback Worked - -### XAP Script Flow - -1. `theDiscDrive` (DiscDrive node) has property `discType` (string) -2. When `discType` changes, XAP callbacks fire: - - `OnDiscInserted()` checks `discType`: "Audio", "Video", "Title", "unknown" - - `OnDiscRemoved()` stops playback, returns to launcher -3. For "Video": calls `StartDVDPlayer()` which sets `theDVDPlayerInline.visible = true` -4. This loads `dvd.xap` via Inline node (lazy load) -5. dvd.xap's `onLoad()` calls `theDVDLevel.GoTo()` to transition to the player scene -6. Script calls `theDVDPlayer.play()`, `.stop()`, `.pause()`, etc. on the DVDPlayer node - -On the original Xbox, video frames came from a hardware MPEG-2 decoder that wrote to a D3D overlay surface. The `texture_panel` mesh was a black placeholder that the decoder composited over. On desktop, libmpv renders frames to an OpenGL FBO texture that gets bound to the `texture_panel` material during rendering. - -### dvd.xap Scene Structure - -Pure 3D scene with these key elements: -- `texture_panel`: flat mesh with material "Black80", no ImageTexture. This is where the video frame appears. -- `text_panel`: flat mesh for time/chapter text overlay -- `theText`: Text node for elapsed/chapter display -- `DVD_buttons` group with 6 button meshes (rewind, play, stop, fast-forward, prev chapter, next chapter) -- `dvd_shell`: panel frame meshes -- `icons` group: transport control icon meshes (play/pause/stop symbols as geometry) - -## Desktop Implementation - -### Architecture +## The pieces ``` -File > Open Media - | - v -MediaPlayer_Open(path) --> mpv_command("loadfile", path) - | - v -Set theDiscDrive.discType = "Video" - | - v -XAP reacts: dvd.xap loads, DVDPlayer.play() called - | - v -Each frame: - MediaPlayer_Update() --> mpv_render_context_render() to FBO - CDVDPlayer::Advance() --> reads mpv position/chapter/track info - updates XAP properties (hours, minutes, etc.) - D3D8 draw call --> detects "Black80" material on texture_panel - binds video FBO texture instead of black +Main menu button -> mediaplayer.xap scenes (browse, action menu, seasons and episodes) + | + v + MediaCollection node (media_db.cpp: scan, cache, TMDB, queries) + | PlayMovie / PlayEpisode + v + media_ui.cpp (fullscreen video, OSD, track menu, queue) + | + v + media_player.cpp (libmpv, software render, bgfx texture) ``` -Three files handle the integration: +Plex and Jellyfin follow the same shape with their own nodes (`PlexLibrary`, `JellyfinLibrary`) and scenes (`plex.xip`, `jellyfin.xip`), streaming from the server instead of reading files. -- **media_player.h/.cpp**: libmpv wrapper. Creates an mpv instance with OpenGL render context, renders video frames to an FBO, exposes transport controls (play/pause/stop/seek), chapter navigation, audio/subtitle track cycling, zoom/pan, A-B looping, frame stepping, and playback speed. The FBO texture ID is returned via `MediaPlayer_GetVideoTexture()` for the renderer to bind. +## Getting there -- **CDVDPlayer** (desktop_nodes.cpp): The XAP node implementation. Bridges all 34 DVDPlayer script functions to `MediaPlayer_*` calls. The `Advance()` method runs every frame, querying mpv for current position, chapter, track info, and updating the node's properties so the XAP OSD text stays current. +On the desktop build the main menu's Xbox Live button is pointed at the media library. `Configs/config.ini` holds the main menu wiring, and its `Button3Action=GoToMedia()` makes that button run `GoToMedia()` from the desktop `default.xap`, which loads the media player scenes. The scripts are in `xips-source/mediaplayer/` and the packed archives are `mediaplayer.xip` and `mediaplayer_action.xip` in `Data/Xips/`. -- **dvd.xap** (original Xbox script): Defines the joystick handlers that map controller buttons to DVDPlayer functions, the OSD layout, and the menu overlay. Unmodified from the Xbox version. +## The library: MediaCollection -### Video Rendering +`CMediaCollection` (in `theseus/desktop/media_db.cpp`) owns the library. It scans the Movies and TV roots set in Settings > Media > Local Library: -The video texture injection happens in the D3D8 translation layer. When the renderer encounters the `texture_panel` mesh (identified by its "Black80" material), it checks if `MediaPlayer_GetVideoTexture()` returns a valid GL texture. If so, it binds that texture instead of the material's default. When playback stops, the override is removed and the mesh goes back to black. +- **Movies**: one folder per movie (the first video file in it, or one level down), or video files straight in the root +- **TV**: one folder per show, with `Season N` or `Specials` folders inside, or episodes straight in the show folder. Episodes sort by `S01E02` style numbers, then by name +- **Video types**: `.mkv`, `.mp4`, `.m4v`, `.avi`, `.mov`, `.webm` -The CRT post-processing shader works on top of this: scanlines and curvature are applied to the final composited frame including video, giving media playback the same retro look as the rest of the dashboard. +Results are cached in `Library/MediaDB.cache` so the next launch doesn't rescan. With a TMDB API key the scan also fetches posters, synopses and ratings; without one the library still works, just with plain titles. -### Playback Modes +The scenes talk to it through script functions: `GetMovieCount`, `GetMovieTitle`, `SetCurrentShow`, `GetSeasonCount`, `GetEpisodeTitle`, `GetSelectedPlot`, `PlayMovie`, `PlayEpisode`, `RefreshLibrary` and friends. The scripts never touch libmpv directly. -The DVDPlayer node's `playbackMode` property maps to these states (from the Xbox DVD navigator): +## Playback: media_ui and media_player -| Mode | Value | Meaning | -|------|-------|---------| -| DPM_STOPPED | 0 | Stopped / no media | -| DPM_PAUSED | 1 | Paused | -| DPM_STILL | 2 | Still frame | -| DPM_PLAYING | 3 | Normal playback | -| DPM_NONE | 4 | No mode | -| DPM_SCANNING | 5 | Fast forward / rewind | -| DPM_TRICKPLAY | 8 | Frame stepping | +`PlayMovie` and `PlayEpisode` hand a path and a title to `media_ui.cpp`, which takes over the screen: video fullscreen, an on screen display in the dashboard's style, an audio and subtitle track picker, and a play queue for playlists. Esc, Q or Backspace stops and returns you to the library scene. -### Scan Speed Ramp +`media_player.cpp` wraps libmpv. Every play creates a fresh mpv instance and every stop tears it down completely, because partial stops left the render pipeline wedged on Apple Silicon. mpv renders each frame in software into a CPU buffer, which is uploaded to a bgfx texture and drawn with a fullscreen blit. That keeps video independent of whichever GPU backend bgfx picked, and lets the CRT effect apply to video too. Hardware decoding is a toggle in Settings > Display. -Forward/backward scan cycles through speed multipliers on each press: -- **Playing + forward scan**: 2x, 4x, 8x, 16x, 32x (via mpv `speed` property) -- **Paused + forward scan**: Slow motion 1/2x, 1/4x, 1/8x, 1/16x -- **Backward scan**: Simulated via periodic reverse seeking in Advance() (mpv doesn't support native reverse playback) +mpv brings its own audio output (CoreAudio, PulseAudio or ALSA, WASAPI). Dashboard sounds and music are muted while video plays and come back when it stops. -### Track Info Mapping +## Playlists -| Xbox property | mpv source | -|---------------|------------| -| audioFormat | `audio-codec-name` mapped to enum (0=AC3, 1=MPEG1, 2=MPEG2, 3=PCM/Stereo, 4=DTS) | -| audioChannels | `audio-params/channel-count` | -| audioLanguage | `current-tracks/audio/lang` | -| subTitleLanguage | `current-tracks/sub/lang` | -| subTitle | `sid` (subtitle track index) | +The Playlist Maker (F6) builds playlists out of the media library. They're stored as file paths in `Configs/playlists.ini`, so they survive rescans and renamed metadata, and `PlaylistCollection` exposes them to scripts. During playback `[` and `]` step through the queue. -### Function Mapping +## The DVDPlayer node -| Xbox function | mpv equivalent | -|---------------|---------------| -| play/pause/stop | `pause` property, `stop` command | -| forwardScan/backwardScan | `speed` property (forward), periodic seeking (backward) | -| stopScan | Reset `speed` to 1.0 | -| frameAdvance/frameReverse | `frame-step` / `frame-back-step` commands | -| nextChapter/prevChapter | `add chapter +/-1` | -| nextAudioStream | `cycle audio` | -| nextSubtitle | `cycle sub` | -| setScale | `video-zoom` (log2 scale) | -| setZoomPos | `video-pan-x` / `video-pan-y` | -| abRepeat | `ab-loop-a` / `ab-loop-b` properties | +The dashboard's own DVD scene, `dvd.xap`, drives a `DVDPlayer` node: play, pause, scan, chapters, angles, subtitles, A/B repeat, zoom, and a stream of callbacks (`OnTimeChange`, `OnChapterChange` and so on) that the on screen display listens to. The desktop `DVDPlayer` still honors that whole contract and maps it onto libmpv: scans become playback speed (reverse scan is periodic seeking backward, since mpv can't play in reverse), chapters map to mpv chapters, and audio and subtitle stream changes cycle mpv's tracks. Its per frame update fires the callbacks in the same order the 5960 binary does. -Functions that are no-ops for file playback: menu, titleMenu, selectUp/Down/Left/Right, activate, goUp, nextAngle, enableWideScreen, disableWideScreen, refreshAudioSettings. +On desktop the disc drive only ever reports audio CDs, though, so in normal use nothing opens `dvd.xap`. The node is there so the scene and its scripts keep loading and stay correct; the media library above is how you actually watch things. -### Audio +## Audio CDs -mpv has its own audio output (CoreAudio on macOS, ALSA/PulseAudio on Linux, WASAPI on Windows). Dashboard audio uses SDL_mixer separately. When media playback starts, dashboard background music is muted via `DashAudio_MuteAll()` and restored on stop via `DashAudio_UnmuteAll()`. +`DiscDrive` on desktop polls the first optical drive for an audio CD and reports it to the dashboard, which opens the music screen the way the Xbox did. Detection works on Linux and Windows (not macOS yet), and playback goes through libmpv. diff --git a/docs/desktop/porting-to-pc.md b/docs/desktop/porting-to-pc.md index 4b68c70..e668239 100644 --- a/docs/desktop/porting-to-pc.md +++ b/docs/desktop/porting-to-pc.md @@ -1,249 +1,91 @@ # Porting to PC -## The Challenge +## The challenge -The Xbox dashboard source code is approximately 25,000 lines of C++ that assumes it's running on an Xbox. Every file I/O call uses `CreateFile`/`ReadFile`. Every draw call goes through Direct3D 8. Every input read polls Xbox gamepads via XInput. The filesystem uses drive letter mappings (`Q:\Xips\default.xip`) from the modded Xbox ecosystem. Types are Windows-specific (`DWORD`, `HANDLE`, `BYTE`). +The dashboard was written to run on exactly one machine. Files open through `CreateFile` on drive letters (`Q:\Xips\default.xip`, `C:\UIX Configs\config.ini`). Every pixel goes through Xbox Direct3D 8. Input comes from four controller ports and a DVD remote. Sound goes through DirectSound and the console's audio DSP. Types are Win32 types (`DWORD`, `HANDLE`, `TCHAR`), and the kernel answers questions no PC has an answer for, like which memory units are plugged in. -The goal: make all of this run on macOS, Linux, and Windows with SDL2 and OpenGL: then strip out the Xbox scaffolding entirely. +UIX Desktop keeps the dashboard's engine code and its scripts as they are, and gives them an Xbox shaped world to live in on macOS, Linux and Windows. -## Strategy: One Tree, Targeted Divergence - -The initial UIX Desktop port lived in a separate repo and forked aggressively to escape the `#ifdef` swamp. That made desktop a clean codebase but meant every Xbox-side fix had to be re-ported by hand. The two trees were merged into the Theseus repo on 2026-04-10 and have been converging ever since. - -The current shape: one repo, four source roots that share whatever doesn't need to diverge. +## One tree, two builds ``` theseus/ - engine/ Pure logic, no platform deps (VM, nodes, math, settings, dotfield, locale) - shared/ Cross-platform with Win32 types (file I/O, titles, audio, screen, settings file) - render/ D3D8/graphics (scene graph, textures, materials, shape rendering, hud) - xbox/ Xbox-only (XTL init, modchip, kernel APIs, main.cpp) - desktop/ Desktop-only (SDL/OpenGL platform layer, ImGui tools, stubs, platform_shim.h) -``` - -Every file that compiles cleanly on both targets lives in `engine`/`shared`/`render`. Files that genuinely diverge (joystick goes to XInput on Xbox vs. SDL2 on desktop, audio goes to DirectSound vs. SDL_mixer, etc.) live under `xbox/` or `desktop/` and are picked up by their target's build only. The Xbox build is `make` (cross-compiles via clang+lld+cxbe to a bootable XBE); the desktop build is `make desktop` (native macOS/Linux) or `make desktop-win64` (mingw cross-compile to a Windows .exe). Same Makefile. - -The migration from "everything under `desktop/`" to "shared canonical source where possible" is ongoing. As of 2026-05-02 the engine, scene graph, rendering, and asset-loading code (including `xap_compile.cpp`, `xap_vm.cpp`, `tmap_system.cpp`, `asset_loader.cpp`, the node system, and the leaf nodes for screen, date, settings, hud, dotfield, fileutil, activefile, etc.) lives in `engine/`, `shared/`, and `render/` and is consumed by both builds. The remaining drift between the two builds sits in the desktop-only platform tooling (SDL window, ImGui developer tools, libmpv media player, HDD image browser) and a small set of files still being moved. - -The port is a mix of techniques depending on what each subsystem required: - -- **Virtualization**: The filesystem layer intercepts Win32 file I/O calls and redirects them to a local directory structure. The engine calls `CreateFile("Q:\\Xips\\default.xip")` and gets back a valid handle without knowing it's actually opening `./Data/Xips/default.xip` via `fopen()`. -- **Reimplementation**: D3D8 rendering was completely rebuilt on top of OpenGL. The engine calls the same `IDirect3DDevice8` interface, but every method is backed by OpenGL state and GLSL shaders. Audio was fully replaced with SDL_mixer. -- **Type compatibility**: Win32 types (`DWORD`, `HANDLE`, `BYTE`, etc.) are typedef'd to standard C types so the code compiles on non-Windows platforms. -- **Stubbing**: Xbox-only subsystems (Xbox Live, hardware management, etc.) are replaced with no-op stubs that satisfy the linker. Node types referenced by XAP scripts but irrelevant on desktop get stub classes so the script VM doesn't fail on unknown types. -- **Direct rewrite**: The input system (CJoystick) was rewritten from XInput to direct SDL2 calls. The EEPROM config system was replaced with hardcoded desktop defaults. Debug infrastructure (TRACE/ASSERT) was rewired to stderr. All dead Xbox code paths were removed. - -``` -Xbox Code: CreateFile("Q:\\Xips\\default.xip", ...) - | - v -Platform Layer: XboxFS_CreateFileA() translates path to "./Data/Xips/default.xip" - Opens with fopen(), returns a HANDLE-compatible wrapper - | - v -Xbox Code: ReadFile(handle, buffer, bytesToRead, &bytesRead, NULL) - | - v -Platform Layer: Calls fread() on the underlying FILE* -``` - -The filesystem example above is genuine virtualization: the engine doesn't know it's not on an Xbox. But other parts of the port are more hands-on. The D3D8 translation layer, for instance, isn't just intercepting calls. It's a from-scratch OpenGL renderer that happens to expose a D3D8-shaped interface. - -## The Platform Layer - -### sdl_platform.h: The Type Foundation - -This header makes non-Windows platforms speak Windows. It defines every Win32 type the Xbox code uses: - -```cpp -typedef uint32_t DWORD; -typedef uint8_t BYTE; -typedef uint16_t WORD; -typedef int32_t LONG; -typedef int BOOL; -typedef void* HANDLE; -typedef void* LPVOID; -typedef const char* LPCSTR; -// ~100 more type definitions + engine/ Pure logic: script compiler, math, locale, dotfield, settings nodes + shared/ Both builds: script VM, file and title handling, settings files, keyboard, screen + render/ Both builds: scene graph, materials, shapes, text, HUD, dynamic textures + xbox/ Xbox only: XTL init, kernel and hardware glue, DirectSound, XInput + desktop/ Desktop only: SDL main loop, D3D8 to bgfx shim, host layer, tools, media, launchers + toolbox/ Xbox only: FTP, drive and network tools from the PrometheOS lineage ``` -It also implements Win32 API functions using POSIX/SDL equivalents: -- `CreateFileA()` / `ReadFile()` / `WriteFile()` / `CloseHandle()`: file I/O via `fopen`/`fread`/`fwrite`/`fclose` -- `FindFirstFileA()` / `FindNextFileA()` / `FindClose()`: directory enumeration via `opendir`/`readdir`, with virtual game injection from `games.ini` -- `GetFileAttributesA()` / `GetFileAttributesExA()`: file stat via `stat()` -- `GetTickCount()`: millisecond timer via `SDL_GetTicks()` -- `Sleep()`: via `SDL_Delay()` -- `GlobalMemoryStatus()`: real host memory via `sysctl` (macOS) or `sysinfo` (Linux) -- `OutputDebugString()`: via `fprintf(stderr, ...)` - -Xbox-specific APIs that originally lived here have been removed entirely. The Xbox EEPROM configuration system (`XQueryValue`/`XSetValue`) was replaced by hardcoding sensible defaults directly in `config.cpp` (stereo audio, no parental controls, English language). The XInput gamepad bridge was replaced by direct SDL2 calls in `joystick.cpp`. Hardware monitoring (SMBus), memory unit management, soundtrack database, power management, and DirectSound were all removed after confirming zero callers. - -On Windows, most of these types already exist (they're the real Win32 types). The challenge there is that `DWORD` on MSVC is `unsigned long` while our typedef makes it `uint32_t` (`unsigned int`). Same size, but MSVC considers them different types for pointer compatibility. This required a `LPDW()` macro to bridge the gap at API boundaries. - -### xboxfs.h: The Virtual Filesystem - -The Theseus codebase uses drive letter mappings from the modded Xbox ecosystem: `C:\`, `E:\`, `Q:\Xips\`, etc. (These are scene conventions, not how retail Xbox addresses partitions internally.) On desktop, these collapse to three top-level folders that live next to the binary: - -``` -Configs/ (Xbox C:\UIX Configs\) - version - config.ini - games.ini - desktop.ini -Data/ (Xbox Q:\) - Xips/ - default.xip - games.xip - settings.xip - ... - Skins/ - Stock/ - Fonts/ - xbox.xtf - tahoma.ttf - System/ -Library/ (Xbox E:\) - Games/ - UDATA/ - Applications/ - Emulators/ -``` - -`F:\`, `G:\`, `R:\` (extra HDD partitions on real Xbox) silently fail to resolve on desktop -- there's only one library partition. XAP scripts that walk multiple drives just see empty results for the missing ones, so no duplicates. - -`xboxfs.h` provides: - -1. **Path translation**: `XboxFS_TranslatePath("Q:\\Xips\\default.xip")` returns `"Data/Xips/default.xip"` -2. **Case-insensitive lookup**: Xbox filenames are case-insensitive (NTFS). macOS is too (by default), but Linux is case-sensitive. The filesystem layer does case-insensitive directory scanning when a direct match fails. -3. **API interception**: `#define CreateFile CreateFileA` and then `#define CreateFileA XboxFS_CreateFileA`, so every file open in the Xbox code goes through the virtual filesystem without changing a single line of Xbox source. -4. **FATX fallback**: For UDATA/TDATA paths, the filesystem can read directly from a qcow2 Xbox HDD image via the FATX reader. - -On Windows, this required extra care: Win32's `CreateFile` is a real function, so the wrapper must strip Xbox-specific flags (`FILE_FLAG_NO_BUFFERING`, `FILE_FLAG_OVERLAPPED`) and convert forward slashes back to backslashes before calling the real Win32 API. - -### d3d8_sdl.h: D3D8 to OpenGL - -This is the largest and most complex part of the platform layer (~2,600 lines). It provides full implementations of: - -- `IDirect3D8`: device enumeration (returns one "SDL OpenGL" adapter) -- `IDirect3DDevice8`: the main rendering interface with GLSL shaders, render state tracking, vertex transforms, lighting, fog -- `IDirect3DTexture8`: texture objects backed by OpenGL textures -- `IDirect3DVertexBuffer8`: vertex data with CPU-side falloff computation -- `IDirect3DIndexBuffer8`: index buffers for mesh rendering -- D3DX math library: matrix, vector, and quaternion operations - -See [D3D8 to OpenGL Translation](d3d8-translation.md) for the full technical details. - -### desktop_stubs.cpp: The Missing Modules - -Many Xbox modules are irrelevant on desktop and are excluded from compilation entirely. But the remaining compiled code still references their symbols. `desktop_stubs.cpp` provides no-op implementations for: title collection management (`CTitleArray`), disc management (`DiscManager`), the dashboard's `net::` networking wrapper, Discord RPC configuration, and the XBE executable reader (which serves virtual game metadata from `games.ini` on desktop). - -### desktop_nodes.cpp: Script-Visible Nodes - -The XAP scripts reference node types that don't exist natively on desktop. If the script VM encounters an unknown node type, it fails. `desktop_nodes.cpp` provides implementations using the same `DECLARE_NODE`/`IMPLEMENT_NODE` pattern as the Xbox source. - -**Fully functional nodes** (real native implementations): -- `CAudioClip`: sound effects and music via SDL_mixer, with Xbox IMA ADPCM decoding -- `CSavedGameGrid`: enumerates real Xbox save data from xboxfs and qcow2 FATX images, renders the memory browser with scrolling title pods and save icons -- `CDVDPlayer`: full media playback via libmpv with transport controls, chapter navigation, A-B loop, frame stepping, zoom, and CRT post-processing -- `CMusicCollection`: soundtrack browsing from `Data/Music/` -- `CAudioVisualizer`: FFT visualization from PCM audio capture -- `CMemoryMonitor`: reports real host memory -- `CHardDrive`: drive info with virtual game library integration -- `CTheseusLauncher`: game launching via `system()` / `execv()` - -**Stub nodes** (registered so XAP scripts don't crash, return defaults): -- `CXboxLive`, `CLiveAccounts`: Xbox Live account management UI -- `CRecovery`: Xbox system recovery -- `CCopyDestination`: memory unit save data copy -- `CDiscDrive`: physical disc detection - -### audio_sdl.cpp: Sound Without DirectSound - -The Xbox used DirectSound with a custom DSP for audio. The desktop port replaces this entirely with SDL_mixer: - -- `.wav` files are loaded directly via `Mix_LoadWAV()` -- Xbox IMA ADPCM audio (the format used in XIP archives) is decoded in software to PCM before playback -- Music playback uses `Mix_PlayMusic()` for background tracks -- Sound effects use `Mix_PlayChannel()` with mixing -- A PostMix callback captures PCM samples for the audio visualizer +`build/Makefile` builds the desktop app from `desktop/` plus the shared roots. `build/Makefile.xbox` builds the XBE from `xbox/` plus the same shared roots. A file lives in a shared root when one copy can serve both builds; where the two genuinely differ (input, audio, file I/O, a handful of nodes whose Xbox version talks to hardware), each build has its own. `shared/platform.h` says which build is compiling: exactly one of `_XBOX` or `THESEUS_DESKTOP`, and on desktop exactly one host OS. `_WIN32` is true on the Xbox too, so nothing outside the host layer is allowed to test it. -The IMA ADPCM decoder was written from the format specification. It's a standard codec, just packaged in Xbox's custom container format. +## The platform layer -## Input: From XInput to SDL2 +### platform_shim.h: Xbox names, no OS headers -The Xbox dashboard used Microsoft's XInput API to handle gamepads (4-port, pressure-sensitive face buttons, analog triggers) and the DVD IR remote control kit. `CJoystick::Advance()` looped over all 4 ports every frame, polling device state and firing XAP callbacks (OnADown, OnMoveUp, OnReverse, OnPlay, etc.). +Dashboard code expects Win32 and XDK names: `HANDLE`, `ReadFile`, `GetTickCount`, `FILETIME`, `FindFirstFile`, `OutputDebugString` and friends. `platform_shim.h` provides them on every desktop OS, backed by SDL, the C runtime and the host layer. It's the same file on macOS, Linux and Windows, and it never includes an OS header. That last part matters most on Windows, where the real `CreateFile` exists and would happily accept `Q:\Xips\default.xip`. -On desktop, `CJoystick` talks directly to SDL2. A single `JoySnapshot` struct captures the merged state of any connected SDL_GameController plus the keyboard. The keyboard mapping (arrows, enter, backspace, WASD, etc.) is applied with OR logic so both input sources work simultaneously. Media keys (`[`, `]`, `I`, `;`, `'`, `0-9`) use poll-based edge detection to fire the same callbacks the IR remote did on Xbox. +### host.h and host_*: the only code that talks to the OS -All XAP-visible callbacks, the secret key sequence system, screen saver reset, and typomatic repeat logic are preserved identically. +`host.h` is a narrow door with plain types: make a folder, stat a file, list a directory, flush to disk, rename over, spawn a process, open a URL, find the executable, show a folder in the file manager, read the CD table of contents. `host_posix.cpp` and `host_posix_app.cpp` implement it for macOS and Linux, `host_win.cpp` and `host_win_app.cpp` for Windows. Those four files are the only ones that include OS headers or test OS macros. -## What Works vs What's Stubbed +Two build targets keep it that way: -**Fully functional on desktop:** -- XIP/XAP engine (scripting VM, scene graph, asset loading) -- 3D rendering (D3D8 reimplemented on OpenGL) -- Audio (SDL_mixer with Xbox IMA ADPCM decoder) -- Media playback (libmpv through the Xbox DVD player UI) -- Skin system (switchable from dashboard settings) -- Filesystem (virtualized to local directory with FATX fallback) -- Input (keyboard + controller via SDL2 GameController) -- Configuration (same config.ini as Xbox, desktop defaults for hardware settings) -- Game launching, Steam import, Title Maker, XAP editor, scene inspector, HDD browser -- Debug infrastructure (TRACE/ASSERT always active, no gating) +- `make lint-platform` fails on a Windows header, an OS macro or a host test anywhere else. CI runs it on every push. +- `make win64-import-guard` compiles the Windows objects and fails if anything outside `host_win*` imports kernel32's file, clock or sleep calls, which would mean real Win32 answered where the shim was supposed to. -**Stubbed (exists so XAP scripts don't crash, returns defaults):** -- Xbox Live / Live Accounts, Recovery, Copy Destination, Disc Drive +### xbox_path.h: drive letters -## The Windows Problem +Every Xbox path goes through one translator. Drive letters map onto the per user data folder: -On macOS and Linux, the platform layer works cleanly because none of the Win32 APIs exist; everything goes through the stubs. On Windows, it's a different story. +| Xbox | Desktop | +|---|---| +| `C:` (`C:\UIX Configs\`) | `Configs/` | +| `Q:` (the dashboard's own data) | `Data/` | +| `E:` | `Library/` | +| `X:` | `Library/` too: a desktop only drive that can never collide with a real Windows `C:\` path | +| `F:`, `G:`, `R:` and the rest | nothing | -Windows actually has `CreateFile`, `ReadFile`, `DWORD`, `HANDLE`, and all the other Win32 types. The Xbox code compiles against the *real* Win32 implementations instead of going through the virtualization layer. This causes: +Scripts that walk every partition just find the extra ones empty. Linux cares about case and the Xbox didn't, so `path_ci.h` resolves paths case insensitively when the exact spelling isn't on disk; skins and XAPs written on one OS keep working on another. -1. **Type mismatches**: `DWORD` is `unsigned long` on MSVC but `uint32_t` (unsigned int) in our typedefs. Same size, but MSVC treats them as incompatible types for pointers and references. +### app_paths: the user data folder -2. **Path translation bypass**: `CreateFile("Q:\\Xips\\default.xip")` calls the real Win32 `CreateFile`, which doesn't know about the xboxfs directory mapping. +The data lives in a per user folder (`~/Library/Application Support/Theseus` on macOS, `$XDG_DATA_HOME/theseus` or `~/.local/share/theseus` on Linux, `%APPDATA%\Theseus` on Windows). On first run it's seeded from the `Configs/`, `Data/` and `Library/` that ship beside the binary (inside a macOS `.app`, from `Contents/Resources`). Only `Data/shaders/` stays with the app, so a read only install works. -3. **Flag incompatibility**: Xbox-specific `CreateFile` flags (`FILE_FLAG_NO_BUFFERING`, `FILE_FLAG_OVERLAPPED`) get passed to the real Win32 API and cause unexpected behavior. +Updates to shipped `Data/` reach existing installs. `Data/.shipped` remembers the size and timestamp of every file it copied, both as shipped and as it looked right after. A copy that still matches is replaced when the shipped file changes; a copy the user edited is theirs and stays. For installs from before the manifest existed, the engine content (`Xips`, `System`, `Fonts`, `Language`) is replaced when it differs and the old copy is kept as `.old`; skins, music and the rest are left alone. -4. **Shell behavior**: `system("program.exe \"arg with spaces\"")` goes through `cmd.exe` on Windows, which strips the inner quotes. Replaced with `CreateProcess` for game launching. +Settings writes go through `safe_save.h`: write `.new`, flush it to disk, keep the current file as `.old`, then rename the new one into place. A crash or power cut mid save leaves either the old file or the new one, never half of one. -The fix was to intercept Win32 APIs on Windows too (`#define CreateFileA XboxFS_CreateFileA`), so even on Windows, file operations go through the virtual filesystem. A `LPDW()` macro bridges `uint32_t*` to `DWORD*` at Win32 API call sites. +### xbox_file.cpp: files that only exist in games.ini -## Debugging +The dashboard finds games by listing folders under `E:\Games` and reading each `default.xbe`. On desktop most entries aren't folders at all: they're lines in `Configs/games.ini`, or live entries from ES-DE and Xbox Cloud Gaming. `xbox_file.cpp` makes them list like folders anyway, and their `default.xbe` opens as a synthesized header carrying the title name and ID. The dashboard's game grid never knows. -The original Xbox dashboard had debug infrastructure (`TRACE`, `ASSERT`, `DumpStack`) that relied on MSVC's CRT debug runtime (`_CrtDbgBreak`, `_CrtSetAllocHook`, `_heapchk`, etc.). The desktop port replaces these with portable equivalents: +## Rendering -- `TRACE()` outputs to stderr (stripping the Xbox `\001` severity prefix) -- `ASSERT()` prints file and line on failure -- `DumpHex()` prints hex dumps -- The script VM stores function names for stack traces +The dashboard's D3D8 calls land on a purpose built shim over bgfx: Metal on macOS, Direct3D 11 on Windows, Vulkan on Linux. That's its own document: [d3d8-translation.md](d3d8-translation.md). -Debug output is always active: no build-type gating. The `_DEBUG` preprocessor gates that used to control this throughout the codebase have been removed. Console output has been cleaned up to only show errors on startup (~8 lines vs the original ~100). +## Audio -## Build System +`audio_sdl.cpp` replaces DirectSound with SDL_mixer. Sound effects play as mixer chunks and background music as mixer music. Many dashboard sounds are Xbox IMA ADPCM WAVs (format tag `0x0069`), which SDL can't read, so they're decoded to 16 bit PCM at load time. A post mix hook feeds the PCM to the audio visualizer and projectM. Master volume and mute sit on top of all of it, and dashboard audio goes quiet while a video or stream plays (those have their own audio output) and while the window is in the background. -A single Makefile at `build/Makefile` drives every target. Different invocations select different output: +## Input -- **`make`**: Xbox debug XBE via clang + lld-link + cxbe (cross-compile from macOS/Linux). Reads from `theseus/xbox/` + the shared roots. -- **`make retail`**: same shape but `CONFIG=retail`. -- **`make desktop`**: native macOS or Linux desktop binary, GCC or Clang. Reads from `theseus/desktop/` + the shared roots. Uses system SDL2/SDL2_mixer/OpenGL. -- **`make desktop-win64`**: Windows .exe via mingw-w64 cross-compile from macOS/Linux. Same source list as `make desktop`. Pulls SDL2 + SDL2_mixer + GLEW from `~/cross/` (one-time setup is in the target preamble). -- **Native Windows / Visual Studio 2019/2022**: open `build/theseus-desktop.sln` and build x64 / Release. Mirrors the cross-compile output but built with `cl.exe`. Setup in [`build/README-vs.md`](../../build/README-vs.md). +`desktop/joystick.cpp` implements the dashboard's `Joystick` node on SDL2's GameController API, so Xbox, PlayStation, Switch Pro and other mapped pads all work. The keyboard is merged into the same snapshot: arrows for the D-pad, WASD for the left stick, Enter for A, Backspace for B, Tab and backtick for White and Black. The DVD remote's buttons (play, pause, skip, digits) map to keys and fire the same script callbacks the IR receiver did. Typematic repeat, the screen saver reset and the secret button sequences behave like the Xbox. -The codebase compiles with permissive flags (`-fpermissive`, `-fms-extensions` on Clang/GCC; `/permissive-` and a comprehensive `DisableSpecificWarnings` list on MSVC) because the original Xbox code uses legacy C++ patterns that modern compilers reject. +## Nodes -All source filenames are lowercase for cross-platform compatibility on case-sensitive filesystems (Linux). +The scripts reference node types by name, and an unknown node type fails the whole scene, so every node the XIPs use has to exist on desktop. -## Code Organization +- **Shared with the Xbox build**: the scene graph, animation, materials and shapes, text, the keyboard, settings, screen, date, and the script built ins. +- **Desktop versions of Xbox nodes**: `AudioClip`, `MusicCollection`, `SavedGameGrid` (reads real Xbox saves from `Library/UDATA` and from qcow2 images), `HardDrive`, `MemoryMonitor`, `DiscDrive` (audio CDs), `DVDPlayer` (libmpv), `Joystick`, `Config` and `TheseusLauncher`. +- **Kept so scripts load**: `XboxLive`, `LiveAccounts` and `CopyDestination` answer with harmless defaults; there's no Xbox Live or memory unit on a PC. +- **Desktop additions**: `MediaCollection`, `PlaylistCollection`, `PlexLibrary`, `JellyfinLibrary`, `XcloudLibrary`, `GamesCollection` and `Display`. They're new names alongside the originals, never replacements; see [xap-contract.md](../xap-contract.md). -The dashboard engine lives in four roots that share whatever doesn't need to diverge per platform: +## Logs and crashes -- `theseus/engine/`: pure logic, no platform dependencies (script VM internals, math, locale, dotfield, settings nodes) -- `theseus/shared/`: cross-platform with Win32 types (file I/O, titles, audio, screen, settings file parser, savegame grid) -- `theseus/render/`: D3D8/graphics nodes (scene groups, shape rendering, dynamic textures, materials, text, hud) -- `theseus/desktop/`: desktop-only code: SDL main loop, D3D8-to-OpenGL translation, ImGui tools (inspector, XAP editor, Title Maker, HDD browser, menu bar, media player), SDL audio backend, filesystem virtualization, the unified launch dispatcher +Everything the app prints goes to the terminal and to `theseus.log` in the user data folder, with the previous run kept as `theseus.prev.log`, so a launch from Finder, a desktop entry or Explorer still leaves a log someone can send. On Windows a launch from a terminal prints there instead of the file. Near the top, the log names the shipped folder, the user data folder and the `games.ini` it's reading, and warns if the shader folder is missing. A crash handler prints a stack trace on the way down. -Within each root, every major tool is its own `.cpp`/`.h` pair. Shared state between ImGui panels is declared in `theseus/desktop/panel_shared.h`. +## Building -The Xbox build (`make`) reads from `xbox/` + the shared roots; the desktop build (`make desktop`) reads from `desktop/` + the shared roots. Files migrate from `desktop/` into the shared roots whenever a desktop file converges with the canonical decompiled form: one source serves both targets where the platform divergence allows it. +See [building.md](building.md) and [dependencies.md](dependencies.md). From 74a67863432423a4411f32878f3bac19384cced4 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:59:12 -0500 Subject: [PATCH 107/167] Rewrote the README around the desktop release Leads with UIX Desktop now: what's in each download, where your games list, settings and logs live, and what to put in a bug report. The Xbox build moved down to its own section, since console work lives in UIX Lite for now. --- README.md | 520 +++++++++++++++++------------------------------------- 1 file changed, 159 insertions(+), 361 deletions(-) diff --git a/README.md b/README.md index 0ec9e65..946707c 100644 --- a/README.md +++ b/README.md @@ -2,191 +2,153 @@ [![build](https://github.com/MrMilenko/Theseus/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/MrMilenko/Theseus/actions/workflows/build.yml) [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-blue.svg)](LICENSE) -[![Platform](https://img.shields.io/badge/platform-Xbox%20%7C%20macOS%20%7C%20Linux%20%7C%20Windows-lightgrey.svg)](#) +[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows%20%7C%20Xbox-lightgrey.svg)](#get-it) [![Discord](https://img.shields.io/discord/1391105896179957830)](https://discord.gg/qfVHTYD4xX) -

- Vulkan -      - Metal -

- -Six years of reverse engineering the original Xbox dashboard. This repo is the result. - -Theseus boots on modded Xbox hardware as a drop in replacement for the stock dashboard. The same engine compiles natively on macOS, Linux, and Windows, where it doubles as **UIX Desktop**: a 3D launcher and media center. The desktop build renders through [bgfx](https://github.com/bkaradzic/bgfx): Metal on macOS, Vulkan on Linux and Windows. +The original Xbox shipped with a dashboard nobody has ever quite gotten over: the green glow, the orbs, the hum. Theseus is that dashboard rebuilt plank by plank from years of reverse engineering the retail binaries, and it runs on your computer. -The split is intentional. The Xbox build stays faithful to what you'd expect from the Xbox dashboard (or UIX Lite, if you've used a custom dashboard before). Everything that doesn't belong on an Xbox (Steam libraries, modern video playback, emulator-hosted ISOs, playlists, skin authoring tools) lives on the desktop side instead. Two projects, one engine. +On macOS, Linux and Windows it goes by **UIX Desktop**: a launcher and media center that looks and feels like the 2002 dashboard. Your Steam library, emulators, xemu, Xbox Cloud Gaming, movies, TV and music all live in the games grid and the orbs, driven by the same scene scripts the Xbox ran.

- Xbox dashboard - UIX Desktop launcher -

-

+ UIX Desktop games grid Media library - Custom skin

- Steam tab in Title Maker - RetroArch tab in Title Maker + A custom skin + Steam import in Title Maker

---- - -**Running it:** [Quick Start](#quick-start) · [Features](#features) · [Adding games](#adding-games) · [Where your stuff lives](#where-your-stuff-lives) · [Customization](#customization) · [Controls](#controls-desktop) - -**Working on it:** [Dependencies](#dependencies) · [Building](#building) · [How it works](#how-it-works) · [Heritage](#heritage) · [Credits](#credits) · [Third-party](#third-party-libraries) · [License](#license) - ---- - -## Quick Start - -### Xbox - -1. Grab the latest Xbox release (`default.xbe` + `uixdata/`) -2. Drop the XBE somewhere on the Xbox HDD, e.g. `E:\Dashboards\Theseus\default.xbe` -3. Copy `uixdata/` next to it -4. Copy `Configs/` to `C:\UIX Configs\` -5. Boot it. If something's missing, the panic screen tells you what. - -[Download for Xbox ->](https://github.com/MrMilenko/Theseus/releases) +**Jump to:** [Get it](#get-it) · [Set it up](#set-it-up) · [Controls](#controls) · [Getting help](#getting-help) · [Build from source](#build-from-source) · [How it works](#how-it-works) · [The Xbox build](#the-xbox-build) · [Heritage](#heritage) · [Credits](#credits) · [License](#license) -### Desktop +## What's in it -1. Grab the release for your OS -2. Install the runtime libraries for your platform (see [Dependencies](#dependencies)) -3. Run it +- **Games grid** for anything you can launch: native PC games, scripts, Steam, RetroArch, [ES-DE](https://es-de.org/) collections, Xbox ISOs through [xemu](https://xemu.app), and your Xbox Cloud Gaming library +- **Title Maker** (F3) for adding and editing entries by hand, with import tabs for Steam, RetroArch and ES-DE +- **Media library** for your Movies and TV folders, with posters from [TMDB](https://www.themoviedb.org/), plus [Plex](https://www.plex.tv/) and [Jellyfin](https://jellyfin.org/) servers. Playback runs through libmpv +- **Music** from your own folders, with a [projectM](https://github.com/projectM-visualizer/projectm) (MilkDrop) visualizer +- **Xbox Cloud Gaming and remote play**: sign in with your Xbox account and your cloud games and consoles show up in the grid +- **Skins**: UIX Lite community skins drop in as is, and the Skin Editor helps you make your own +- **CRT effect** for that living room TV look: scanlines, curvature, phosphor mask, bloom, all tunable +- **Controllers**: Xbox, PlayStation and other SDL2 gamepads, plus full keyboard control +- **Xbox HDD browser** (F5) for xemu qcow2 images and their FATX partitions -Windows is the exception: that release ships with the DLLs it needs, so step 2 is already done for you. macOS and Linux link against system libraries. +## Get it -[Download for desktop ->](https://github.com/MrMilenko/Theseus/releases) +Grab the build for your OS from the [Releases page](https://github.com/MrMilenko/Theseus/releases). -Building from source instead? Jump to [Building](#building). +| OS | Download | Notes | +|---|---|---| +| macOS (Apple Silicon) | `Theseus--macos-app.zip` | Self contained `Theseus.app`. It's ad hoc signed, not notarized, so the first launch needs right click > Open, or **System Settings > Privacy & Security > Open Anyway** on newer macOS. | +| macOS (Apple Silicon) | `Theseus--macos.zip` | Bare binary for people who already live in Homebrew. Needs `brew install sdl2 sdl2_mixer mpv curl ffmpeg opus`. | +| Linux x64 | `UIX-Desktop--x86_64.AppImage` | One file with its libraries bundled. `chmod +x` it and run. This is the one for a Steam Deck. | +| Linux x64 / ARM64 | `Theseus--linux-x64.tar.gz`, `Theseus--linux-arm64.tar.gz` | Binary plus data. Needs the runtime packages listed in [docs/desktop/dependencies.md](docs/desktop/dependencies.md). | +| Windows x64 | `Theseus--windows-x64.zip` | Unzip anywhere and run `theseus.exe`. Every DLL it needs is in the zip. The exe isn't code signed, so SmartScreen may ask first (More info > Run anyway). | -## Features +Rendering goes through [bgfx](https://github.com/bkaradzic/bgfx): Metal on macOS, Direct3D 11 on Windows, Vulkan on Linux. If the default misbehaves on your machine, pick another backend under **Settings > Display > Renderer** and restart. -### Xbox +## Set it up -A drop-in replacement for the stock Xbox dashboard on modded consoles. Same look and behavior, because that's what it is. Rebuilt plank by plank and still going. +### First run -- Every original scene, animation, and skin slot -- UIX Lite skins drop in unchanged. Skin authors don't have to do anything -- Hot swap skins from settings, no reboot -- ISO / CCI launching from the harddrive menu, plus the original XBE flow -- Hundreds of titles scan in milliseconds -- Title icons auto populate from each game's XBE certificate -- Quick overlay (LT + B) for ISO loader, file manager, FTP / drive widgets -- FTP server, recovery / panic screen, MP3 soundtrack playback +Launch it and you land on the dashboard. On first run UIX Desktop creates your **user data folder** and copies the starter Configs, Data and Library into it. Everything you change lives there, not next to the app, so updating means replacing the app and nothing else. -### UIX Desktop +| OS | User data folder | +|---|---| +| macOS | `~/Library/Application Support/Theseus/` | +| Linux | `$XDG_DATA_HOME/theseus/`, which is usually `~/.local/share/theseus/` | +| Windows | `%APPDATA%\Theseus\` | -The Theseus engine compiled for your computer, with the modern features bolted on. macOS, Linux, Windows, Steam Deck friendly. +**Settings (F4) > General > Open User Directory** takes you straight there. Inside: -- **3D launcher** for native PC games, Steam libraries, RetroArch ROMs, and Xbox ISOs via [xemu](https://xemu.app) -- **xCloud / Game Pass streaming** signs in with your Xbox account and drops your whole cloud library into the games grid, box art and all. Remote play for your own console shows up under "Your Xbox" -- **Media library** that scans your Movies and TV folders, pulls posters from [TMDB](https://www.themoviedb.org/), and plays back through libmpv. [Plex](https://www.plex.tv/) and [Jellyfin](https://jellyfin.org/) servers browse and stream in the same UI (Jellyfin signs in with Quick Connect) -- **Music visualizer** with in-scene [projectM](https://github.com/projectM-visualizer/projectm) (MilkDrop) presets reacting to whatever's playing -- **Skin editor** with live XAP scripting and a scene inspector. Change a skin, see it instantly -- **Title Maker** for adding games and apps, with per launcher import flows for Steam and RetroArch -- **Xbox HDD browser** that opens qcow2 and FATX images -- **CRT post process** for the old TV look. Scanlines, curvature, phosphor, bloom, all tunable -- **Graphics knobs** in Settings > Display: vsync mode, FPS cap, MSAA, hardware video decode -- **Controllers**: Xbox and PlayStation pads via SDL2 +| Path | What's there | +|---|---| +| `Configs/games.ini` | Every launcher entry you add: name, title ID, launch command, category | +| `Configs/Icons.ini` | Which icon each entry uses | +| `Configs/icons/` | The icon images, named by title ID (`.jpg` or `.png`) | +| `Configs/desktop.ini` | Desktop settings: display, audio, CRT, library folders, sign ins | +| `Configs/config.ini` | Dashboard settings shared with the Xbox build (skin, clock, language) | +| `Data/` | Skins, orbs, music, fonts, screenshots and the dashboard's scene archives | +| `Library/` | The virtual Xbox `E:` drive: `Games`, `Applications`, `Emulators`, plus title and save data in `TDATA` and `UDATA` | +| `theseus.log`, `theseus.prev.log` | This run's log and the one before it | -## Adding games +When an update ships new Data (scene archives, fonts, skins), it reaches your copy on the next launch, and anything you've edited is left alone. Only the compiled shaders stay with the app itself. Settings files are written atomically, with the previous version kept beside them as `.old`, so a crash mid save can't eat your games list. -Title Maker (F3 from the dashboard) is where you connect games to dashboard tiles. Three tabs: +### Adding games and apps -**Main** is the catch all. Every title you've added shows up here, regardless of which tab created it. This is also where you add the weird stuff that doesn't belong to a launcher: a Windows .exe, a .bat script, a macOS .command file, a shell one liner, anything that takes a path or command. Edit names, swap icons, tweak the launch line. Most of your time managing the library happens here. +Open **Title Maker** with F3 (or Tools > Title Maker). -**Steam** auto detects your Steam install (Find button), or you point at it once. Hit Import Steam Library and your installed games come in with icons fetched from Valve's CDN. There's also a manual "Add by App ID" form for launching betas, demos, or games not in your normal library scan. +- **Main** lists every entry, whatever added it. Create one by hand with a name and a launch line: a path to an executable, a script, a `steam://` link, or any shell command. Quote paths with spaces. **Test Launch** tries it without leaving the dashboard. **Add ISO** turns an Xbox ISO into an xemu entry once the xemu path is set (Find checks the usual install spots). +- **Steam** finds your Steam install, and **Import Steam Library** brings in every installed game with its icon. Add by App ID covers betas and demos. +- **RetroArch** finds your install, and **Import Recent Titles** pulls in what you've played lately with the core and box art already sorted. Or pick a ROM and a core by hand. +- **ES-DE** points at your ES-DE data folder (the one holding `gamelists/`). Your systems and games show up under an ES-DE category, read live on every launch. -**RetroArch** detects your RetroArch install the same way. Import Recent Titles pulls in everything you've recently played in RetroArch, with the right core auto resolved and boxart copied from RetroArch's thumbnail packs. You can also add manually: pick a ROM, pick a core from the dropdown, done. +Entries from Main, Steam and RetroArch are stored in **`Configs/games.ini`** inside your user data folder, with their icons in `Configs/icons/` and the icon mapping in `Configs/Icons.ini`. Back those up and you've backed up your launcher. ES-DE and Xbox Cloud Gaming entries are never written there; they're rebuilt each launch from their source. -If you don't use Steam or RetroArch, you can turn either tab off under Optional Tabs (top of Main). Anything you've already added stays in Main either way. +The Steam, RetroArch and ES-DE tabs can be hidden under Optional Tabs at the top of Main. Hiding a tab keeps its entries. When you launch something, the dashboard fades to black and minimizes so the game gets the screen; switch back to it when you're done. Cloud and remote play streams run inside the dashboard window instead. -## Where your stuff lives +### Xbox Cloud Gaming -Everything you can customize sits in a per OS user directory, not next to the -binary. It's created and seeded on first run, and an existing side by side -install is migrated across once. +**Settings > Media > Xbox / xCloud > Sign in to Xbox**, then enter the code it shows on any device. From then on your cloud games and your consoles (for remote play) appear in the games grid with their box art. -| | | -|---|---| -| macOS | `~/Library/Application Support/Theseus/` | -| Linux | `$XDG_DATA_HOME/theseus`, else `~/.local/share/theseus` | -| Windows | `%APPDATA%\Theseus\` | - -Configs, Library and most of Data live there: skins, orbs, music, screenshots, -fonts, saves and settings. Only the compiled shaders stay with the binary, so a -signed .app, a .deb or a flatpak can ship a read only payload without breaking -anything you own. +### Movies, TV and music -**Settings > General > Open User Directory** takes you straight there, which is -the quickest way to check what the app is actually reading. +**Settings > Media > Local Library** takes three folders: -On Xbox nothing changed: `C:\UIX Configs\`, `uixdata\` and the rest are where -they always were. +- **Movies**: one folder per movie, or video files straight in the folder +- **TV Shows**: `Show Name/Season 1/episodes` (or `Specials`), or episodes straight in the show folder. `S01E02` style names set the order +- **Music**: each subfolder becomes a soundtrack, `.m3u` playlists in the root become soundtracks too, and loose files land in one called Music -## Customization +A free [TMDB API key](https://www.themoviedb.org/settings/api) is optional; with one, posters and synopses fill in. Hit **Refresh Media Library** after changing folders. -**Skins.** Drop them into `Data/Skins/` inside the user directory above (Xbox: `uixdata\Skins\`) and pick from settings. UIX Lite community skins work as is, no conversion needed. Hot swap, no reboot. +Plex signs in with a PIN code. Jellyfin takes your server URL and signs in with Quick Connect. Both live under **Settings > Media**. -**Scene authoring** (for the people building dashboards from scratch). Scenes are XAP scripts packed into `.xip` archives. The desktop build has a live XAP editor (F2), scene inspector (F1), and asset reload so you can tweak and see results immediately. The XAP node interface is the contract; the C++ behind it can change but the node API is treated as sacred. Full reference in [`docs/xap-contract.md`](docs/xap-contract.md). +### Skins -## Controls (desktop) +Skins live in `Data/Skins//` in your user data folder. Pick one from the dashboard's own settings and it swaps live, no restart. UIX Lite community skins work without conversion. A skin's `.xbx` only lists the materials it changes; anything it leaves out keeps the retail 5960 color. **Tools > Skin Editor** can start a new skin from Stock and tweak colors as you watch. -### Controller +### Handy settings -Xbox and PlayStation pads work via SDL2 GameController. The dashboard itself is -pad driven as you'd expect; the tool windows have their own mode. +- **Display**: fullscreen (F11), borderless (F12), resolution, vsync, FPS cap, MSAA, renderer, hardware video decode, and Emulate Xbox Resolution, which frames scenes in the original 720x480 design space +- **CRT Effect**: Settings > Display, or View > CRT Effect. Four presets or eight sliders +- **Audio**: master volume, mute (Ctrl+M), and the projectM visualizer. With it on, X + Y (keys or pad) opens and closes the visualizer window -| Input | Action | -|---|---| -| **LT + B** | Toggle pad mode. Same chord as the Xbox overlay | -| Stick / D-pad | Move | -| A | Select | -| B | Back out one layer, then close pad mode | -| Y | Open a text field, which brings up the on screen keyboard | -| Hold X | Move and resize windows, for when one outgrows the screen | +Command line flags: `--fullscreen`, `--4k` (fullscreen plus 2x UI scale), `--scale N`, `--no-toolbar`, `--muted`, `--minimized`, `--dashboard`, `--development`. -In pad mode the menu bar steps aside for a controller menu, the pad drives the -tools instead of the dashboard, and a prompt bar along the bottom says what the -buttons currently do. On screen keyboard has letters, numbers and a symbol page -for paths. Turn the whole thing off with `PadMode=0` in `Configs/desktop.ini`. +## Controls -Skin Editor stays mouse and keyboard only: colour pickers and direct -manipulation don't translate to a stick. Keyboard equivalents: +The dashboard drives like the Xbox did: D-pad or stick to move, A to select, B to back out.
-Dashboard navigation +Keyboard -| Key | Xbox button | Action | -|---|---|---| -| Arrow keys | D-pad | Navigate | -| Enter / Space | A | Select | -| Backspace | B | Back | -| X / Y | X / Y | Context actions | -| Tab | White | Play / Pause | -| ` (backtick) | Black | Stop | -| WASD | Left stick | Analog navigation | +| Key | Xbox button | +|---|---| +| Arrow keys | D-pad | +| WASD | Left stick | +| Enter / Space | A | +| Backspace / Esc / B | B | +| X / Y | X / Y | +| Tab | White | +| ` (backtick) | Black | +| Q / E | Left / right trigger |
-Media playback +Video playback | Key | Action | |---|---| -| Esc / Q | Stop, return to dashboard | -| Space | Pause / Resume | -| Left / Right | Seek 5s | -| `[` / `]` | Previous / Next in playlist | -| T | Track picker (audio + subtitles) | +| Esc / Q / Backspace | Stop and return to the dashboard | +| Space | Pause / resume | +| Left / Right | Seek 5 seconds | +| `[` / `]` | Previous / next in playlist | +| T | Audio and subtitle tracks |
-Tools (desktop only) +Tools and windows | Key | Action | |---|---| @@ -196,283 +158,119 @@ manipulation don't translate to a stick. Keyboard equivalents: | F4 | Settings | | F5 | Xbox HDD browser | | F6 | Playlist Maker | -| F10 | Toggle menu bar | -| F11 | Toggle fullscreen | +| F10 | Show or hide the menu bar | +| F11 | Fullscreen | +| F12 | Borderless window | | Ctrl+M | Mute | -| Ctrl+R | Restart dashboard | - -
- ---- - -# For developers - -The rest is build instructions, architecture notes, and the lineage. Skip if you just want to run it. - -## Dependencies - -One list, used by both the [Quick Start](#quick-start) and [Building](#building). "Run" is what a downloaded release needs; "Build" is that plus the toolchain and headers. - -The build finds all of these through `pkg-config`, so no particular package -manager is required. Pick whichever one you already use, or install the -libraries by hand and point `PKG_CONFIG_PATH` at their `.pc` files. - -**macOS (Homebrew):** - -``` -# Run a release -brew install sdl2 sdl2_mixer mpv curl ffmpeg opus +| Ctrl+R | Restart the dashboard | -# Build from source (adds the toolchain + libdatachannel's deps) -brew install sdl2 sdl2_mixer mpv curl ffmpeg opus pkg-config cmake openssl@3 -``` - -**macOS (MacPorts):** - -``` -# Run a release -sudo port install libsdl2 libsdl2_mixer mpv curl ffmpeg libopus - -# Build from source -sudo port install libsdl2 libsdl2_mixer mpv curl ffmpeg libopus \ - pkgconfig cmake openssl3 -``` - -**Linux (Debian / Ubuntu):** +On a Mac, Cmd works in place of Ctrl. -``` -# Run a release -sudo apt install libsdl2-2.0-0 libsdl2-mixer-2.0-0 libmpv2 libcurl4 libopus0 - -# Build from source -sudo apt install build-essential pkg-config cmake \ - libsdl2-dev libsdl2-mixer-dev \ - libvulkan-dev libx11-dev libgl-dev \ - libmpv-dev libcurl4-openssl-dev libssl-dev \ - libavcodec-dev libavutil-dev libswscale-dev libopus-dev -``` - -**Windows (MSYS2 / MinGW64):** - -``` -pacman -S make pkg-config mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_mixer \ - mingw-w64-x86_64-mpv mingw-w64-x86_64-curl \ - mingw-w64-x86_64-vulkan-headers mingw-w64-x86_64-vulkan-loader -``` - -**Anything else (Fedora, Arch, nix, source builds):** - -``` -# Fedora -sudo dnf install gcc-c++ pkgconf-pkg-config cmake SDL2-devel SDL2_mixer-devel \ - mpv-libs-devel libcurl-devel openssl-devel \ - ffmpeg-devel opus-devel vulkan-loader-devel libX11-devel - -# Arch -sudo pacman -S base-devel pkgconf cmake sdl2 sdl2_mixer mpv curl openssl \ - ffmpeg opus vulkan-icd-loader libx11 -``` - -Notes: - -- Nothing in the build shells out to a package manager. If `pkg-config --exists - mpv libcurl libavcodec opus` succeeds, the build will find them, however they - got there. Prefixes off the default search path just need `PKG_CONFIG_PATH`. -- Some distros ship `libmpv1` instead of `libmpv2`. Either works. -- The FFmpeg runtime libs come in with libmpv. `ffmpeg` / `libopus0` cover the xCloud video and audio decoders. -- OpenSSL is a runtime dependency of xCloud. On macOS it arrives alongside curl / mpv / ffmpeg, and on Debian `libssl3` is already in the base install, so neither needs it listed for the run case. Building libdatachannel does need the headers, which is why `openssl@3` and `libssl-dev` show up above. - -## Building - -Both builds live in this repo and use the same Makefile. - -### Xbox - -Cross-compiles from macOS or Linux using clang + lld-link + cxbe. Requires: -- clang and lld-link (`apt install clang lld` on Debian, `dnf install clang lld` on Fedora, `pacman -S clang lld` on Arch, `brew install llvm` or `port install clang-17 lld-17` on macOS) -- [OXDK](https://github.com/MrMilenko/OXDK) cloned and built -- An Xbox SDK source tree (path passed as `XDK_BASE`) - -``` -git clone https://github.com/MrMilenko/OXDK ~/OXDK -cd ~/OXDK/xbox/tools/cxbe && make -cd /path/to/Theseus/build -make CONFIG=retail XDK_BASE=/path/to/xbox -``` - -Output lands at `~/builds/theseus/xbox-retail/default.xbe`. - -The Xbox target is not built in CI, because the SDK tree it needs isn't something this repo can ship. Verify it locally before you ship anything. - -### Desktop - -Same source tree, different Makefile target. Needs C++17 and the packages in [Dependencies](#dependencies). Rendering goes through bgfx: Metal on macOS, Vulkan on Linux and Windows. The compiled shader binaries live in-tree under `Data/shaders/`, so a normal build never touches `shaderc`. - -**1. Init the submodules** (once): - -``` -git submodule update --init --recursive -``` + -**2. Build the bgfx runtime libraries** (once per platform): +
+Controller in the tool windows -Only three libs are needed. bgfx's own top level targets also build `shaderc`, -`geometryc` and a debug config you'll never load, which takes far longer and -drags in tools this build doesn't use. +**LT + B** (the same chord as the Xbox overlay) hands the pad to the tool windows. A prompt bar along the bottom shows what each button does right now. -``` -cd theseus/third-party/bgfx +| Input | Action | +|---|---| +| Stick / D-pad | Move | +| A | Select | +| B | Back out one layer, then leave pad mode | +| Y | Open a text field with the on screen keyboard | +| Hold X | Move and resize windows | -# macOS (Apple Silicon) -../bx/tools/bin/darwin/genie --gcc=osx-arm64 gmake -make -C .build/projects/gmake-osx-arm64 -j bx bimg bgfx config=release +Turn it off with `PadMode=0` in `Configs/desktop.ini`. The Skin Editor stays mouse and keyboard only. -# Linux -../bx/tools/bin/linux/genie --gcc=linux-gcc gmake -make -C .build/projects/gmake-linux-gcc -j bx bimg bgfx config=release64 +
-# Windows (MSYS2 mingw shell, or cross-compiled from Linux) -../bx/tools/bin/windows/genie.exe --gcc=mingw-gcc gmake -make -C .build/projects/gmake-mingw-gcc -j bx bimg bgfx config=release64 -``` +## Getting help -bx ships `genie` prebuilt, and on an older distro it fails with -`GLIBC_2.38 not found`. Build it from source and carry on: +Found a bug? Open an [issue](https://github.com/MrMilenko/Theseus/issues) or find us on the TeamUIX [Discord](https://discord.gg/qfVHTYD4xX). Please include: -``` -git clone --depth 1 https://github.com/bkaradzic/GENie.git /tmp/genie -make -C /tmp/genie -cp /tmp/genie/bin/linux/genie theseus/third-party/bx/tools/bin/linux/genie -``` +1. **Your OS and version**, and your GPU if it's a display problem +2. **Which download you're running**: the file name carries the version and package. Help > About UIX Desktop shows it too +3. **What you did, what you expected, and what happened instead** +4. **`theseus.log`** from your user data folder (see [First run](#first-run)). If it crashed on the previous launch, attach `theseus.prev.log` as well -**3. Build libdatachannel** (once per platform, macOS and Linux). +On Windows, starting `theseus.exe` from a terminal sends the log to that terminal instead of the file. -This one is easy to skip and the failure is quiet: the Makefile only defines `THESEUS_HAVE_WEBRTC` when it finds the static lib, so without this step everything compiles and links fine and xCloud is simply gone from the build. The directory name matters, `build-mac` on macOS and `build-linux` on Linux, because that's where the Makefile looks. +Please don't post `Configs/desktop.ini` as is: it holds your Plex, Jellyfin and Xbox sign in tokens. -``` -cd theseus/third-party/libdatachannel - -# macOS -cmake -S . -B build-mac -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ - -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 \ - -DOPENSSL_ROOT_DIR="$OPENSSL_ROOT" # only if cmake can't find OpenSSL itself -cmake --build build-mac -j - -# Linux -cmake -S . -B build-linux -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ - -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 -cmake --build build-linux -j -``` +## Build from source -Windows cross builds stub xCloud out, so this step doesn't apply there. +The full walkthrough is in [docs/desktop/building.md](docs/desktop/building.md), and the libraries per OS are in [docs/desktop/dependencies.md](docs/desktop/dependencies.md). The shape of it on macOS or Linux: -**4. Build the dashboard:** +1. `git clone --recursive https://github.com/MrMilenko/Theseus` +2. Install the build packages for your OS. Everything is found through `pkg-config`, so any package manager works +3. Build the bgfx runtime libraries and libdatachannel once. Skip libdatachannel and xCloud quietly drops out of the build +4. `cd build && make desktop`, then run `~/builds/theseus/desktop/theseus` -``` -# macOS / Linux -cd build && make desktop -~/builds/theseus/desktop/theseus +Windows builds cross compile with mingw (`make desktop-win64`), and there's a Linux ARM64 cross target (`make desktop-linux-arm64`). Scripts for the macOS `.app` and the Linux AppImage live in `build/`. -# Windows (MSYS2 / MinGW64) -cd build && make desktop-win64 -``` +For contributors: only `theseus/desktop/host_*` talks to the operating system. `make lint-platform` enforces that, and `make win64-import-guard` catches stray Win32 calls. CI runs the lint and builds macOS, Linux and Windows on every push. -`make desktop` also builds the vendored projectM (MilkDrop visualizer) into a local prefix on first run. Only re-run the `shaders-bgfx*` Makefile targets if you edit a `.sc` shader source; the checked-in `.bin` files cover every backend. +## How it works -### macOS .app +The dashboard is a scene graph driven by XAP, a small scripting language compiled to bytecode, with scenes, meshes and textures packed into `.xip` archives. Theseus reimplements the engine underneath: the script VM, the VRML flavored scene graph with its FND/PRD reflection tables, asset loading, text, materials, audio and input. The scripts and archives run unmodified, which is why the scene node interface is treated as a contract ([docs/xap-contract.md](docs/xap-contract.md)). -`build/mk-macapp.sh` wraps a desktop build into `Theseus.app`: it copies the -Homebrew dylibs into `Contents/Frameworks`, rewrites their install names to -`@rpath` so the bundle runs on a machine without your brew layout, builds the -icon set, and ad-hoc signs the result. +On desktop, the dashboard's Direct3D 8 calls land on a thin shim that drives bgfx, Xbox drive paths map onto your user data folder, and SDL2 handles windows, input and sound. ``` -cd build && make desktop -./build/mk-macapp.sh +theseus/ + engine/ Pure logic: script compiler, math, locale + shared/ Both builds: script VM, files and titles, settings, keyboard + render/ Both builds: scene graph, materials, shapes, text + desktop/ Desktop only: SDL main loop, bgfx shim, host_* OS layer, tools, media, launchers + xbox/ Xbox only: kernel and hardware glue + toolbox/ Xbox only: FTP and drive tools from the PrometheOS lineage +theseuslib/ C library: XISO and XIP parsers +xips-source/ Editable source for the scene archives Theseus adds or changes ``` -Lands at `~/builds/theseus/Theseus.app`. Ad-hoc signing is enough to run it -locally; handing it to anyone else needs a Developer ID and notarization. - -### Steam Deck and AppImage +Documentation index: [docs/README.md](docs/README.md). What we found in the dashboard binary, subsystem by subsystem: [docs/decomp/](docs/decomp/). -Currently, AppImages are provided for SteamDeck/SteamOS users. This hasn't been fully tested. Please reach out on the TeamUIX [Discord](https://discord.gg/qfVHTYD4xX). +## The Xbox build -Cross-compiling for Windows from macOS / Linux, ARM64 Linux, or any of the more involved setups is in [`docs/desktop/`](docs/desktop/). The CI workflow builds all four desktop targets (macOS, Linux x64, Linux ARM64, Windows) on every push, which is the closest thing to executable docs for the one-time setup. +The same engine still compiles for the original Xbox as a replacement dashboard, built with [OXDK](https://github.com/MrMilenko/OXDK) through `build/Makefile.xbox`. It's a work in progress and isn't part of the desktop releases. If you want a custom dashboard on your console today, use [UIX Lite](https://github.com/OfficialTeamUIX/UIX-Lite), where TeamUIX's console work currently lives. -## How it works - -The engine is approximately 50 source files reconstructed from the retail and patched XBE's spanning 4920 to 5960, organized the same way the original dashboard was: script VM, scene graph, rendering, asset loading, UI framework, system integration, and launcher. Per-subsystem reverse engineering notes are in [`docs/decomp/`](docs/decomp/). - -The XAP scripting layer is a custom JS-like bytecode VM. The scene graph is VRML97-inspired with runtime reflection via FND/PRD property tables. On the desktop side, D3D8 calls translate through a thin shim into bgfx, which targets Metal on macOS and Vulkan on Linux and Windows. Everything else compiles for both targets from the same shared source. +

+ Theseus on the Xbox +

-If you're poking around the source, the high-level layout: +Building it needs clang and lld-link, OXDK, and an Xbox SDK tree of your own linked at `theseus/xdk` (it isn't in this repo): ``` -theseus/ - engine/ Pure logic (VM, nodes, math) - shared/ Cross-platform with Win32 types (file I/O, audio, settings) - render/ Scene graph, materials, shapes - xbox/ Xbox-only (XTL, modchip, kernel APIs) - desktop/ Desktop-only (SDL, bgfx, ImGui tools) - toolbox/ PrometheOS-derived FTP / drive / network (Xbox-only) -theseuslib/ Shared C library (xiso parser, xip parser) +cd build +make -f Makefile.xbox CONFIG=retail # or CONFIG=debug ``` -Heavier docs index lives at [`docs/README.md`](docs/README.md). +The XBE lands in `~/builds/theseus/xbox-retail/` (or `xbox-debug/`). On the console it expects `uixdata\` beside the XBE, mounted as `Q:`, and its configs in `C:\UIX Configs\`. If something's missing at boot, the panic screen tells you what. ## Heritage -Theseus is part of the TeamUIX lineage. JbOnE created *User.Interface.X* (UIX), a source level modification of the original Xbox Dashboard, which we also poked around in via Ghidra to figure certain things out. Modern TeamUIX continues that tradition with [UIX Lite](https://github.com/OfficialTeamUIX/UIX-Lite) (a heavily patched retail XBE) and Theseus (this repository). +Theseus is part of the TeamUIX lineage. JbOnE created *User.Interface.X* (UIX), a source level modification of the original Xbox dashboard, and a lot of what we know about how the dashboard behaves came from years of living with it. Modern TeamUIX carries that on with [UIX Lite](https://github.com/OfficialTeamUIX/UIX-Lite), a heavily patched retail dashboard, and with Theseus. -There's a circularity to it. UIX modified the dashboard at the source level. Theseus reaches the same destination from the other side of the river, rebuilding the codebase from binary analysis and untangling changes made to XIPs over 25 years of community modification. +UIX changed the dashboard from the inside. Theseus reaches the same place from the other side of the river: Ghidra analysis of the retail dashboard binaries from 4920 through 5960, custom Ghidra scripts that pull the dashboard's own FND/PRD descriptor tables out of the binary, disassembly where the decompiler gets it wrong, every name checked against the XAP scripts that call it, and two decades of community changes to the XIPs untangled along the way. The findings are written up per subsystem in [docs/decomp/](docs/decomp/). -For the broader UIX project narrative, see [UIX History](https://github.com/MrMilenko/UIX-History). +For the wider UIX story, see [UIX History](https://github.com/MrMilenko/UIX-History). ## Credits -**Team UIX:** -- **Milenko**: primary RE and development, UIX Desktop port +**TeamUIX** +- **Milenko**: reverse engineering and development, UIX Desktop - **BigJx**: UIX Lite XIPs (XAP scripts, skins, scene assets), testing, bug reports -- **Rocky5**: skin presets and the Colourizer XBE color patcher (technique descends from **ZogoChieftan**'s in-dashboard color patching in BlackStormX, circa 2004) -- **JbOnE**: original UIX, the lineage Theseus continues +- **Rocky5**: skin presets and the Colourizer XBE color patcher (the technique descends from **ZogoChieftan**'s in dashboard color patching in BlackStormX, circa 2004) +- **JbOnE**: the original UIX, the lineage Theseus continues -**Upstream code:** -- [Team Resurgent](https://github.com/Team-Resurgent): [PrometheOS](https://github.com/Team-Resurgent/PrometheOS-Firmware) (the toolbox is forked from here via [UIX Lite Toolbox](https://github.com/OfficialTeamUIX/UIX-Lite-Toolbox)) and [Hermes](https://github.com/Team-Resurgent/Hermes) (ISO/CCI mount support) +**Upstream code** +- [Team Resurgent](https://github.com/Team-Resurgent): [PrometheOS](https://github.com/Team-Resurgent/PrometheOS-Firmware) (the Xbox toolbox is forked from it via [UIX Lite Toolbox](https://github.com/OfficialTeamUIX/UIX-Lite-Toolbox)) and [Hermes](https://github.com/Team-Resurgent/Hermes) (ISO/CCI mounting on Xbox) -## Third-party libraries - -Xbox build: - -| Library | License | Purpose | -|---|---|---| -| [minimp3](https://github.com/lieff/minimp3) | CC0 | MP3 decoder for the music system | - -Desktop build: - -| Library | License | Purpose | -|---|---|---| -| [SDL2](https://www.libsdl.org/) | zlib | Window, input, audio | -| [SDL2_mixer](https://github.com/libsdl-org/SDL_mixer) | zlib | Sound playback | -| [libmpv](https://mpv.io/) | LGPL 2.1+ | Video playback | -| [libcurl](https://curl.se/libcurl/) | curl | HTTPS for TMDB metadata | -| [Dear ImGui](https://github.com/ocornut/imgui) | MIT | Developer tool UI | -| [ImGuiColorTextEdit](https://github.com/BalazsJako/ImGuiColorTextEdit) | MIT | XAP script editor with syntax highlighting | -| [stb_image](https://github.com/nothings/stb) | Public Domain | Image loading | -| [bgfx](https://github.com/bkaradzic/bgfx) | BSD 2-Clause | Cross-platform render abstraction (Metal, Vulkan) | -| [bx](https://github.com/bkaradzic/bx) | BSD 2-Clause | bgfx base library | -| [bimg](https://github.com/bkaradzic/bimg) | BSD 2-Clause | bgfx image utility library | -| [projectM](https://github.com/projectM-visualizer/projectm) | LGPL 2.1+ | MilkDrop music visualizer | -| [FFmpeg](https://ffmpeg.org/) (libavcodec / libswscale / libavutil) | LGPL 2.1+ | xCloud video decode | -| [Opus](https://opus-codec.org/) | BSD 3-Clause | xCloud audio decode | -| [libdatachannel](https://github.com/paullouisageneau/libdatachannel) | MPL 2.0 | WebRTC transport for xCloud / remote play (one-time local build, see [Building](#desktop-1)) | -| [GLEW](https://glew.sourceforge.net/) | Modified BSD / MIT | OpenGL extension loader (projectM / milkdrop visualizer, Windows only) | - -Full catalog with attributions is in [`LICENSE-THIRD-PARTY.md`](LICENSE-THIRD-PARTY.md). +**Libraries**: SDL2, SDL2_mixer, libmpv, libcurl, FFmpeg, Opus, OpenSSL, libdatachannel, bgfx, bx, bimg, projectM, Dear ImGui, ImGuiColorTextEdit, imgui-filebrowser, stb and minimp3. Licenses and links are in [LICENSE-THIRD-PARTY.md](LICENSE-THIRD-PARTY.md). ## License -Theseus is licensed under the **GNU General Public License, version 3 or later** (`GPL-3.0-or-later`). Full license text in [`LICENSE`](LICENSE). +Theseus is licensed under the **GNU General Public License, version 3 or later** (`GPL-3.0-or-later`). Full text in [LICENSE](LICENSE). -Inherited code keeps its origin license intact (`theseus/toolbox/` from PrometheOS via UIX Lite Toolbox is GPL-3.0; Hermes is GPL-3.0). The XIPs and skin assets shipped in `Data/` are TeamUIX's UIX Lite work and ship under GPL-3.0-or-later. +Inherited code keeps its own license (`theseus/toolbox/` from PrometheOS via UIX Lite Toolbox is GPL-3.0; Hermes is GPL-3.0). The XIPs and skin assets shipped in `Data/` are TeamUIX's UIX Lite work and ship under GPL-3.0-or-later. From 5771c4f73703a5fa5c573abd7e4c315a7eea34c3 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:59:12 -0500 Subject: [PATCH 108/167] Fixed stale paths and gaps in the other docs The docs index was missing two decomp pages, xips-source still pointed at xboxfs, and the third party list was missing half the desktop libraries. Also listed the nodes Theseus adds in the XAP contract. --- LICENSE-THIRD-PARTY.md | 22 +++++++---- Library/Applications/README.md | 4 +- Library/Dashboards/README.md | 6 ++- Library/Emulators/README.md | 6 ++- Library/Games/README.md | 6 ++- Library/TDATA/README.md | 4 +- docs/README.md | 61 +++++++++++++++++-------------- docs/xap-contract.md | 21 ++++++++++- xips-source/README.md | 48 +++++++++--------------- xips-source/mediaplayer/README.md | 36 +++++++++--------- 10 files changed, 121 insertions(+), 93 deletions(-) diff --git a/LICENSE-THIRD-PARTY.md b/LICENSE-THIRD-PARTY.md index 2a686b1..425dcd1 100644 --- a/LICENSE-THIRD-PARTY.md +++ b/LICENSE-THIRD-PARTY.md @@ -28,23 +28,31 @@ Used by the Xbox build: |---|---|---| | [minimp3](https://github.com/lieff/minimp3) | CC0 (Public Domain) | Single-header MP3 decoder for streaming music playback | -Used by the desktop port: +Used by the desktop build: | Library | License | Purpose | |---|---|---| | [SDL2](https://www.libsdl.org/) | zlib | Window, input, audio platform layer | | [SDL2_mixer](https://github.com/libsdl-org/SDL_mixer) | zlib | Sound playback | -| [libmpv](https://mpv.io/) | LGPL-2.1+ | Video playback for the DVD player node | -| [Dear ImGui](https://github.com/ocornut/imgui) | MIT | Developer tool UI | +| [libmpv](https://mpv.io/) | LGPL-2.1+ | Video playback for the media library and the DVD player node, audio CD playback | +| [libcurl](https://curl.se/libcurl/) | curl (MIT style) | HTTPS for TMDB, Plex, Jellyfin and Xbox sign in | +| [FFmpeg](https://ffmpeg.org/) (libavcodec, libavutil, libswscale) | LGPL-2.1+ | xCloud video decode | +| [Opus](https://opus-codec.org/) | BSD-3-Clause | xCloud audio decode | +| [OpenSSL](https://www.openssl.org/) | Apache-2.0 | TLS and DTLS for the xCloud transport | +| [libdatachannel](https://github.com/paullouisageneau/libdatachannel) (submodule) | MPL-2.0 | WebRTC transport for xCloud and remote play | +| [bgfx](https://github.com/bkaradzic/bgfx), [bx](https://github.com/bkaradzic/bx), [bimg](https://github.com/bkaradzic/bimg) (submodules) | BSD-2-Clause | Rendering on Metal, Direct3D 11 and Vulkan | +| [projectM](https://github.com/projectM-visualizer/projectm) (submodule) | LGPL-2.1+ | MilkDrop music visualizer, linked as a shared library | +| [Dear ImGui](https://github.com/ocornut/imgui) | MIT | Tool window UI | | [ImGuiColorTextEdit](https://github.com/BalazsJako/ImGuiColorTextEdit) | MIT | XAP script editor with syntax highlighting | -| [stb_image](https://github.com/nothings/stb) | Public Domain (or MIT, dual-licensed) | Image loading on the desktop side | -| [GLEW](https://glew.sourceforge.net/) | Modified BSD / MIT | OpenGL extension loader (Windows desktop build only) | +| [imgui-filebrowser](https://github.com/AirGuanZ/imgui-filebrowser) | MIT | File picker in the tool windows | +| [stb_image, stb_image_write](https://github.com/nothings/stb) | Public Domain (or MIT, dual licensed) | Image loading, re-encoding icons and box art | +| [GLEW](https://glew.sourceforge.net/) | Modified BSD / MIT | OpenGL extension loader, linked only by a native MSYS2 Windows build. The release builds don't use it | All of the above are GPL-3.0-compatible, either by being permissive (MIT, BSD, zlib, public domain) or by being LGPL (linkable from GPL). -## Xbox Dashboard Assets (`xboxfs/`) +## Xbox Dashboard Assets (`Data/`) -The XIPs and skin assets in `xboxfs/` are authored by [UIX Lite](https://github.com/OfficialTeamUIX/UIX-Lite) and contain TeamUIX modifications of dashboard scripts present on every retail Xbox console. The `uixdata` directory layout is a convention established by JbOnE's earlier source-level modification, *User.Interface.X* (UIX); Theseus follows that convention so the same content runs on both projects. UIX Lite's modifications are licensed under GPL-3.0-or-later. The underlying script formats and any unmodified original components are referenced for compatibility and preservation; Microsoft retains its rights to its original work. Every Xbox owner already possesses the equivalent original files on their console. +The XIPs and skin assets in `Data/` are authored by [UIX Lite](https://github.com/OfficialTeamUIX/UIX-Lite) and contain TeamUIX modifications of dashboard scripts present on every retail Xbox console. The `uixdata` directory layout is a convention established by JbOnE's earlier source-level modification, *User.Interface.X* (UIX); Theseus follows that convention so the same content runs on both projects. UIX Lite's modifications are licensed under GPL-3.0-or-later. The underlying script formats and any unmodified original components are referenced for compatibility and preservation; Microsoft retains its rights to its original work. Every Xbox owner already possesses the equivalent original files on their console. ## Microsoft XDK Headers (`theseus/xdk/`) diff --git a/Library/Applications/README.md b/Library/Applications/README.md index b577f90..3103e0f 100644 --- a/Library/Applications/README.md +++ b/Library/Applications/README.md @@ -2,4 +2,6 @@ On Xbox hardware, this is where homebrew applications live (file managers, tools, utilities), each in its own subfolder containing a `default.xbe`. -This folder is intentionally empty in the public repo. It exists so the desktop build's `E:\` filesystem virtualization has the standard Xbox layout to enumerate at boot. Drop application folders here on desktop to see them in the launcher. On Xbox hardware your real `E:\Applications\` is what gets read. +The folder is empty in the repo. It exists so the desktop build's `E:` drive has the standard Xbox layout to list at boot. + +On desktop, the copy of this folder that counts is the one in your user data folder (`Library/` inside `~/Library/Application Support/Theseus/` on macOS, `~/.local/share/theseus/` on Linux, `%APPDATA%\Theseus\` on Windows), which is seeded from this one on first run. Most entries on desktop come from Title Maker and live in `Configs/games.ini`, so this folder can stay empty. On Xbox hardware your real `E:\Applications\` is what gets read. diff --git a/Library/Dashboards/README.md b/Library/Dashboards/README.md index 969e1e2..b7c56e5 100644 --- a/Library/Dashboards/README.md +++ b/Library/Dashboards/README.md @@ -1,5 +1,7 @@ # Dashboards -On Xbox hardware, this is where alternate dashboards (UnleashX, Evolution X, XBMC4Xbox, etc.) live, each in its own subfolder containing a `default.xbe`. Theseus itself can also live under here. +On Xbox hardware, this is where alternate dashboards (UnleashX, Evolution X, XBMC4Xbox and friends) live, each in its own subfolder containing a `default.xbe`. Theseus itself can live here too. -This folder is intentionally empty in the public repo. It exists so the desktop build's `E:\` filesystem virtualization has the standard Xbox layout to enumerate at boot. Drop dashboard folders here on desktop to see them in the launcher. On Xbox hardware your real `E:\Dashboards\` is what gets read. +The folder is empty in the repo. It exists so the desktop build's `E:` drive has the standard Xbox layout to list at boot. + +On desktop, the copy of this folder that counts is the one in your user data folder (`Library/` inside `~/Library/Application Support/Theseus/` on macOS, `~/.local/share/theseus/` on Linux, `%APPDATA%\Theseus\` on Windows), which is seeded from this one on first run. Most entries on desktop come from Title Maker and live in `Configs/games.ini`, so this folder can stay empty. On Xbox hardware your real `E:\Dashboards\` is what gets read. diff --git a/Library/Emulators/README.md b/Library/Emulators/README.md index 02b66f5..d0f15a6 100644 --- a/Library/Emulators/README.md +++ b/Library/Emulators/README.md @@ -1,5 +1,7 @@ # Emulators -On Xbox hardware, this is where emulator apps (Surreal64, FCE Ultra, ScummVM-X, etc.) live, each in its own subfolder containing a `default.xbe` plus its ROM library. +On Xbox hardware, this is where emulator apps (Surreal64, FCE Ultra, ScummVM and friends) live, each in its own subfolder containing a `default.xbe` plus its ROM library. -This folder is intentionally empty in the public repo. It exists so the desktop build's `E:\` filesystem virtualization has the standard Xbox layout to enumerate at boot. Drop emulator folders here on desktop to see them in the launcher. On Xbox hardware your real `E:\Emulators\` is what gets read. +The folder is empty in the repo. It exists so the desktop build's `E:` drive has the standard Xbox layout to list at boot. + +On desktop, the copy of this folder that counts is the one in your user data folder (`Library/` inside `~/Library/Application Support/Theseus/` on macOS, `~/.local/share/theseus/` on Linux, `%APPDATA%\Theseus\` on Windows), which is seeded from this one on first run. Most entries on desktop come from Title Maker and live in `Configs/games.ini`, so this folder can stay empty. On Xbox hardware your real `E:\Emulators\` is what gets read. diff --git a/Library/Games/README.md b/Library/Games/README.md index c2209f7..8f58dd2 100644 --- a/Library/Games/README.md +++ b/Library/Games/README.md @@ -1,5 +1,7 @@ # Games -On Xbox hardware, this is where game rips and homebrew games live, each in its own subfolder containing a `default.xbe`. The launcher's title scanner walks this tree at boot, parses each XBE's certificate for its title ID, and populates the games panel. +On Xbox hardware, this is where game rips and homebrew games live, each in its own subfolder containing a `default.xbe`. The title scanner walks this tree at boot, reads each XBE's certificate for its title ID and name, and fills the games panel. -This folder is intentionally empty in the public repo. It exists so the desktop build's `E:\` filesystem virtualization has the standard Xbox layout to enumerate. Drop game folders here on desktop to see them in the launcher. On Xbox hardware your real `E:\Games\` is what gets read. +The folder is empty in the repo. It exists so the desktop build's `E:` drive has the standard Xbox layout to list at boot. A folder here on desktop can carry a `default.uixshortcut` file with a `Launch=` line, which the launcher uses when the folder has no `games.ini` entry. + +On desktop, the copy of this folder that counts is the one in your user data folder (`Library/` inside `~/Library/Application Support/Theseus/` on macOS, `~/.local/share/theseus/` on Linux, `%APPDATA%\Theseus\` on Windows), which is seeded from this one on first run. Most entries on desktop come from Title Maker and live in `Configs/games.ini`, so this folder can stay empty. On Xbox hardware your real `E:\Games\` is what gets read. diff --git a/Library/TDATA/README.md b/Library/TDATA/README.md index 39913fa..96a0267 100644 --- a/Library/TDATA/README.md +++ b/Library/TDATA/README.md @@ -1,5 +1,5 @@ # TDATA -Title data. Each Xbox title gets a folder here named by its 8-character title ID, holding save games, per-title configuration, and any persistent state the title writes through `T:\`. On Xbox hardware the kernel maps `T:\` to `E:\TDATA\\` automatically when a title is launched. +Title data. Each Xbox title gets a folder here named by its eight character title ID, holding the persistent state it writes through `T:\` (settings, caches and the like). On Xbox hardware the kernel maps `T:\` to `E:\TDATA\\` when a title launches. Saved games live next door in `UDATA`. -This folder is mostly empty in the public repo. The dashboard's own state lives under `fffe0000` (the dashboard's title ID); see that subfolder's README. On Xbox hardware your real `E:\TDATA\` is what gets read. +The folder is mostly empty in the repo. The dashboard's own state lives under `fffe0000`, the dashboard's title ID; see that subfolder's README. On desktop the live copy is in your user data folder's `Library/TDATA/`. On Xbox hardware your real `E:\TDATA\` is what gets read. diff --git a/docs/README.md b/docs/README.md index 6099714..3c1895d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,41 +1,48 @@ # Theseus Documentation -This directory contains the technical documentation for Theseus: the Xbox dashboard engine reverse-engineered from the 5960 retail XBE. +Technical documentation for Theseus, the Xbox dashboard engine rebuilt from binary analysis of the retail dashboard, and for UIX Desktop, the build of it that runs on macOS, Linux and Windows. If you just want to run it, the main [README](../README.md) is the place to start. -## Contents +## Reference -### Reference +- **[XAP Script Contract](xap-contract.md)**: every node type, function, property and callback the dashboard's XAP scripts use. The "do not break" surface any implementation (Xbox, desktop, future ports) has to honor. +- **[xips-source](../xips-source/README.md)**: the editable source for the scene archives Theseus adds or changes. -- **[XAP Script Contract](xap-contract.md)**: Complete catalog of every node type, function, property, and callback that XAP scripts in the dashboard XIPs reference. The "do not break" API surface that any implementation (Xbox, desktop, future ports) must honor. +## What we found in the dashboard binary (`decomp/`) -### Binary Analysis (`decomp/`) +Per subsystem notes from analyzing the 5960 retail dashboard XBE in Ghidra. Each file documents the structures, function tables, properties and behavior identified for one part of the dashboard. [decomp/README.md](decomp/README.md) is the address by address index. -Per-subsystem reverse engineering notes derived from analysis of the retail 5960 XBE in Ghidra. Each file documents the structures, function tables, properties, and behavior identified for one subsystem of the dashboard. - -- **[VM](decomp/VM.md)**: XAP bytecode VM: opcode set, operator table, built-in functions, variable resolution -- **[Node](decomp/Node.md)**: Scene graph reflection, FND and PRD struct layouts, how the structures were identified from the binary -- **[XIP](decomp/XIP.md)**: Archive format, file layout, texture loading, mesh buffer layout +- **[VM](decomp/VM.md)**: XAP bytecode VM: opcode set, operator table, built in functions, variable resolution +- **[Compiler](decomp/Compiler.md)**: how XAP text becomes bytecode and scene nodes +- **[Node](decomp/Node.md)**: scene graph reflection, FND and PRD struct layouts, how the structures were identified in the binary +- **[XIP](decomp/XIP.md)**: archive format, file layout, texture loading, mesh buffer layout - **[AudioSystem](decomp/AudioSystem.md)**: DirectSound pipeline, CAudioBuf, CAudioPump, CFilePump, CAudioClip, CMusicCollection -- **[SceneGroups](decomp/SceneGroups.md)**: Group, Layer, Background, Level grouping nodes +- **[SceneGroups](decomp/SceneGroups.md)**: Group, Layer, Background and Level grouping nodes - **[ShapeRender](decomp/ShapeRender.md)**: Shape, Appearance, Material, Box, Sphere, falloff shading, mesh normal smoothing -- **[TmapSystem](decomp/TmapSystem.md)**: Dynamic textures, delta field warping, audio visualizer, FFT, particle dot field +- **[HudNode](decomp/HudNode.md)**: the HUD and Picture screen space overlay nodes +- **[TmapSystem](decomp/TmapSystem.md)**: dynamic textures, delta field warping, audio visualizer, FFT, particle dot field - **[AnimationNodes](decomp/AnimationNodes.md)**: TimeSensor, interpolators, Viewpoint, NavigationInfo -- **[FileOps](decomp/FileOps.md)**: File and directory copy, save game grid backend -- **[Keyboard](decomp/Keyboard.md)**: On-screen keyboard layout and input handling -- **[Camera](decomp/Camera.md)**: View matrix construction -- **[Text](decomp/Text.md)**: Font rendering, glyph layout, color formatting +- **[FileOps](decomp/FileOps.md)**: file and directory copy, save game grid backend +- **[Keyboard](decomp/Keyboard.md)**: on screen keyboard layout and input handling +- **[Camera](decomp/Camera.md)**: view matrix construction +- **[Text](decomp/Text.md)**: font rendering, glyph layout, color formatting - **[NtIoSvc](decomp/NtIoSvc.md)**: CD-ROM IOCTL service -- **[TitleCollection](decomp/TitleCollection.md)**: Saved game and title enumeration backend -- **[Settings](decomp/Settings.md)**: System settings, screen saver, INI-style config file parser -- **[Date](decomp/Date.md)** and **[Math](decomp/Math.md)**: Built-in date and math objects -- **[StringObject](decomp/StringObject.md)**: String built-in -- **[Util](decomp/Util.md)**: Utility helpers +- **[TitleCollection](decomp/TitleCollection.md)**: saved game and title enumeration backend +- **[Settings](decomp/Settings.md)**: system settings, screen saver, INI style config file parser +- **[Date](decomp/Date.md)** and **[Math](decomp/Math.md)**: built in date and math objects +- **[StringObject](decomp/StringObject.md)**: string built in +- **[Util](decomp/Util.md)**: utility helpers + +## Desktop (`desktop/`) + +How the same engine runs on macOS, Linux and Windows. The Xbox build doesn't need any of this. -### Desktop Port (`desktop/`) +- **[Building](desktop/building.md)**: every desktop target, the one time library steps, Windows cross builds, packaging +- **[Dependencies](desktop/dependencies.md)**: packages per OS, minimum library versions, Windows cross deps +- **[Desktop Features](desktop/desktop-features.md)**: games grid and launching, Title Maker, media, music, xCloud, skins, CRT, tools +- **[Porting to PC](desktop/porting-to-pc.md)**: the platform layer, drive letters and the user data folder, audio, input, nodes +- **[D3D8 to bgfx](desktop/d3d8-translation.md)**: drawing the dashboard's Direct3D 8 calls through bgfx +- **[Media Player](desktop/media-player-design.md)**: the media library, libmpv playback, and the DVD player node -Documentation specific to building Theseus for desktop platforms (macOS, Linux, in-progress Windows). The Xbox build does not need any of this. +## The Xbox build -- **[Porting to PC](desktop/porting-to-pc.md)**: How Xbox APIs were replaced with SDL2 and OpenGL -- **[D3D8 to OpenGL Translation](desktop/d3d8-translation.md)**: Rendering layer translation -- **[Desktop Features](desktop/desktop-features.md)**: Game launching, Steam, Title Maker, tools -- **[Media Player Design](desktop/media-player-design.md)**: libmpv integration with the DVD player XAP scene +Built with OXDK through `build/Makefile.xbox` (`make -f Makefile.xbox CONFIG=retail` or `CONFIG=debug` from `build/`). It needs an Xbox SDK tree of your own at `theseus/xdk`. Console work currently ships as [UIX Lite](https://github.com/OfficialTeamUIX/UIX-Lite); the Theseus XBE is a work in progress. diff --git a/docs/xap-contract.md b/docs/xap-contract.md index 4dc1af5..59ed8e1 100644 --- a/docs/xap-contract.md +++ b/docs/xap-contract.md @@ -2,7 +2,9 @@ This document catalogs every node type, function, property, and callback that XAP scripts in the dashboard XIPs reference. This is the "do not break" API surface; the C++ implementation behind these interfaces can be rewritten freely, but the names and behaviors must remain stable. -Generated by scanning all 39 .xap files across the dashboard XIP set. +Generated by scanning all 39 .xap files across the dashboard XIP set. Everything below is the retail dashboard's surface; the nodes Theseus adds are listed separately in [Nodes Theseus adds](#nodes-theseus-adds). + +New behavior never renames or rewrites one of these. It goes in alongside, as a new node or a new function, so stock XIPs and community skins keep loading. ## Node Types (32 unique) @@ -424,3 +426,20 @@ DisappearAfter(seconds) ### Box (inline geometry, no DEF'd attributes) + +## Nodes Theseus adds + +These are new node types alongside the retail set, used by Theseus's own scenes in [`xips-source/`](../xips-source/README.md). A stock XIP never references them, and a scene that does only loads on a build that has them. + +| Node type | Build | Source | What it's for | +|-----------|-------|--------|---------------| +| TheseusLauncher | both | `xbox/theseus_launcher.cpp`, `desktop/desktop_nodes.cpp` | Script callable launcher for XBEs, `.uixshortcut` files and `games.ini` entries | +| TitleScanner | Xbox | `xbox/title_scanner.cpp` | Native title scan across drives, feeding the title menu | +| StarField | both | `render/starfield.cpp` | Parallax 3D star backdrop | +| GamesCollection | desktop | `desktop/desktop_nodes.cpp` | The `games.ini` library by category, refreshed as Title Maker edits it | +| MediaCollection | desktop | `desktop/media_db.cpp` | Movie and TV library, TMDB metadata, playback | +| PlaylistCollection | desktop | `desktop/media_db.cpp` | Playlists from the Playlist Maker | +| PlexLibrary | desktop | `desktop/desktop_nodes.cpp` | Plex browsing and playback | +| JellyfinLibrary | desktop | `desktop/desktop_nodes.cpp` | Jellyfin browsing and playback | +| XcloudLibrary | desktop | `desktop/xcloud_node.cpp` | Xbox Cloud Gaming titles and remote play consoles | +| Display | desktop | `desktop/desktop_nodes.cpp` | Window resolution and display mode, shared with Settings > Display | diff --git a/xips-source/README.md b/xips-source/README.md index 064ba95..1be0168 100644 --- a/xips-source/README.md +++ b/xips-source/README.md @@ -1,40 +1,28 @@ # xips-source -Theseus-original XAP and XIP source. This directory holds the editable scene scripts and asset manifests for new dashboard content that ships alongside Theseus. Compiled `.xip` archives live under [`xboxfs/Q/Xips/`](../xboxfs/Q/Xips/); this is where their plain-text source comes from. +Editable source for the scene archives Theseus adds or changes. The dashboard's scenes are XAP scripts plus meshes (`.xm`) and textures (`.xbx`), packed into `.xip` archives. The packed archives that ship are in [`Data/Xips/`](../Data/Xips/); this folder holds their plain text source so changes can be read, diffed and reviewed. -## Why this directory exists +## Why it exists -The shipped `xboxfs/Q/Xips/` tree is mostly binary XIP archives carried forward from the UIX Lite release. Theseus runs those archives unmodified to preserve compatibility with community skins and the dashboard's existing scene contract. When Theseus needs to *add* its own scenes (a new launcher tab, the media player, future Theseus-only features), the source for those scenes goes here. The binary output gets packed into `xboxfs/Q/Xips/` separately. - -We are deliberately not yet doing a full extract of every shipped XIP into this tree. UIX Lite's XIPs remain the upstream canonical source for that content, and we don't want to fork them in the public repo until the divergence story is settled. +Most of `Data/Xips/` is UIX Lite's XIP set, carried forward so community skins and the dashboard's scene contract keep working. When Theseus needs its own scenes (the media player, Plex and Jellyfin, desktop settings, orbs) or a change to a stock one, the source lands here. UIX Lite's XIPs still run on Theseus unchanged; Theseus's own archives can carry scenes UIX Lite's dashboard wouldn't load. ## Layout -``` -xips-source/ - README.md this file - mediaplayer/ Theseus media player (replaces dashboard DVD player on desktop) -``` - -Each subdirectory holds one logical unit of work: typically one or more `.xap` script files plus any geometry, texture, or layout source they reference. A subdirectory is roughly one shipping `.xip` (with the option to inject loose `.xap` files into an existing archive like `default.xip`). - -## Workflow - -1. **Write loose `.xap` source here.** The XAP language is documented in [`docs/xap-contract.md`](../docs/xap-contract.md). Reference the existing dashboard XAPs (extracted via `xiptool`, see below) for scene patterns. -2. **Iterate against a dev-mode build.** Theseus's desktop build supports loose-XAP loading at runtime when the dashboard's loose-load mode is enabled, which lets you edit-and-reload without repacking. -3. **Pack to `.xip` when shipping.** The packing step uses `xiptool` from the UIX Desktop tools (`UIX-Desktop/tools/xiptool/`). Once a Python `xiptool.py` port lands in this repo, that becomes the canonical packer. -4. **Drop the packed `.xip` into `xboxfs/Q/Xips/`** so the runtime build picks it up. For new entries injected into `default.xip`, the packer needs to merge into the existing archive rather than create a new one. - -## Current units - -### `mediaplayer/` - -Generic libmpv-backed video media player. Replaces the dashboard's DVD player on desktop. Repurposes the XONLINE main-menu tab via `xboxfs/C/UIX Configs/config.ini` (`Button3Action=GoToMedia()`). - -Backed by C++ node `CMediaCollection` (new, in `theseus/desktop/desktop_nodes.cpp`). `CDVDPlayer` is preserved as a no-op contract surface so XAPs still referencing it compile clean. Library roots + TMDB API key live in `xboxfs/C/UIX Configs/desktop.ini` under `[Library]`, editable in-app via the Settings window. +| Folder | Packs into | What it is | +|---|---|---| +| `default-desktop/` | `default.xip` (desktop) | The top level scripts for the desktop build: main menu, settings, hard drive, music, media player, Plex, Jellyfin, orbs | +| `default-xbox/` | the Xbox `default.xip` (shipped here as `default-xbox.xip`) | The same top level scripts for the console build | +| `default/` | | The earlier common copy both of those were forked from | +| `mainmenu5/` | `mainmenu5.xip` | Main menu scene: the game pods and their meshes | +| `settings3/` | `settings3.xip` | Settings scene | +| `music/` | `music2.xip`, `music_copy3.xip`, `music_playedit2.xip` | Music screens | +| `mediaplayer/` | `mediaplayer.xip`, `mediaplayer_action.xip` | The media library browser and action menu ([README](mediaplayer/README.md)) | +| `plex/`, `jellyfin/` | `plex.xip`, `jellyfin.xip` | Plex and Jellyfin browsers | -Three-scene state machine (browser → action menu → seasons/episodes), then standalone fullscreen mpv playback layered on top of the dashboard. See `xips-source/mediaplayer/README.md` for the directory layout. +The desktop and Xbox `default.xip` are separate because a few scenes reference nodes that only exist on one build, and a scene that names an unknown node fails to load. When you change one, check whether the other needs the same change. -## Note on UIX Lite divergence +## Working on scenes -When Theseus's `xboxfs/Q/Xips/` content begins to diverge from UIX Lite's upstream archives (a Theseus-original `mediaplayer.xip`, modifications to `default.xip` to add `mediaplayer.xap`, etc.), the changes are intentionally Theseus-side and do not flow back to UIX Lite. The divergence is a deliberate split: UIX Lite's XIPs continue to work on a Theseus-powered dashboard unchanged, but Theseus's shipped XIPs may carry Theseus-only scenes that UIX Lite's dashboard binary would not load. +1. **Read the contract.** Node types, functions, properties and callbacks are catalogued in [`docs/xap-contract.md`](../docs/xap-contract.md). The stock XAPs here are the best examples of scene patterns. +2. **Iterate in Development Mode.** Pick Development Mode at launch (or set Startup Mode in Settings > General). It unpacks the shipped archives into folders under `Data/Xips/` in your user data folder and runs the scenes from those loose files. Edit there, or in the XAP editor (F2), and the scene reloads without repacking. +3. **Bring the change back here** so it's reviewable, then repack the archive into `Data/Xips/`. There isn't an XIP packer in this repo yet, so repacking is done with an external tool for now. diff --git a/xips-source/mediaplayer/README.md b/xips-source/mediaplayer/README.md index 0cdae4c..e7af717 100644 --- a/xips-source/mediaplayer/README.md +++ b/xips-source/mediaplayer/README.md @@ -1,37 +1,35 @@ # mediaplayer -Theseus media player. libmpv-backed movie/TV browser + standalone fullscreen video playback. Replaces the dashboard's DVD player on the desktop build and ships in place of the XONLINE button on the main menu. +The media library: a movie and TV browser backed by libmpv, styled like the dashboard's own memory and music screens. On the desktop build it takes the place of the Xbox Live button on the main menu. -## What's in this directory +## What's here -- `mediaplayer.xap` — wrapper script. Defines `GoToMedia()`, the three-scene state machine (browser, action menu, episode picker), and binds to the C++ `theMediaCollection` node. Gets injected into `default.xip` as a loose XAP alongside the other top-level dashboard scenes. -- `mediaplayer/` — extracted source for `mediaplayer.xip` (browser scene assets: meshes, textures, default.xap layer). -- `mediaplayer_action/` — extracted source for `mediaplayer_action.xip` (action-menu scene assets: cloned from `music_playedit2`). +- `mediaplayer.xap`: the wrapper script. Defines `GoToMedia()`, runs the three scene flow (browser, action menu, seasons and episodes), and talks to the `theMediaCollection` node. The desktop `default.xip` carries its own copy (`default-desktop/MediaPlayer.xap`). +- `mediaplayer/`: source for `mediaplayer.xip`, the browser scene (meshes, textures, its `default.xap`). +- `mediaplayer_action/`: source for `mediaplayer_action.xip`, the action menu scene, built from the music screen's `music_playedit2`. -The packed `.xip` archives live in `../../xboxfs/Q/Xips/` so runtime builds pick them up directly. Repacking is only needed if you edit the scene assets here. +The packed archives are in [`Data/Xips/`](../../Data/Xips/). -## Backing C++ node +## The node behind it -`CMediaCollection` in [`theseus/desktop/desktop_nodes.cpp`](../../theseus/desktop/desktop_nodes.cpp) owns the media database, scanning, TMDB enrichment, and playback handoff. The XAP wrapper queries it via FND-bound methods (`GetMovieCount`, `PlayMovie`, `SetSelectedShow`, etc.) and never reaches into libmpv directly. +`CMediaCollection` in [`theseus/desktop/media_db.cpp`](../../theseus/desktop/media_db.cpp) owns the library: scanning, the `Library/MediaDB.cache` cache, TMDB lookups and the handoff to fullscreen playback. The scripts query it through node functions (`GetMovieCount`, `GetShowTitle`, `SetSelectedShow`, `PlayMovie`, `PlayEpisode` and so on) and never touch libmpv themselves. The whole path is described in [`docs/desktop/media-player-design.md`](../../docs/desktop/media-player-design.md). -`CDVDPlayer` is preserved as a no-op contract surface so XAPs that still DEF a DVDPlayer node compile clean. +## Wiring and settings -## Configuration +The main menu button runs whatever `Button3Action` says in `Configs/config.ini`; the desktop ships it as `GoToMedia()`. -Library roots and the optional TMDB API key live in [`xboxfs/C/UIX Configs/desktop.ini`](../../xboxfs/C/UIX%20Configs/desktop.ini): +Library folders and the optional TMDB key are set in Settings > Media > Local Library and saved in `Configs/desktop.ini` in the user data folder: ```ini [Library] -MusicRoot=/path/to/your/music -MoviesRoot=/path/to/your/movies -TvRoot=/path/to/your/tv +MusicRoot=/path/to/music +MoviesRoot=/path/to/movies +TvRoot=/path/to/tv TMDBKey= ``` -Empty `TMDBKey` disables online metadata enrichment; the catalog still works, posters/synopses just won't populate. Settings can also be edited in-app via the Settings window's Media Library tab. +With no TMDB key the library still works; posters and synopses just stay empty. -## Repackaging +## Editing -If you edit anything inside `mediaplayer/` or `mediaplayer_action/`, repack with `xiptool` (currently lives in the UIX Desktop tooling repo) and drop the resulting `.xip` files into `../../xboxfs/Q/Xips/`. A Python port of `xiptool` is on the roadmap. - -`mediaplayer.xap` is loose-loaded at runtime in dev mode, so you can iterate on the wrapper script without repacking. Final ship form bakes it into `default.xip`. +In Development Mode the scenes run from loose files, so script changes show up without repacking. See [`xips-source/README.md`](../README.md) for the workflow. From 9408e809be230d36c5792801caa209e98bcb0116 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:00:48 -0500 Subject: [PATCH 109/167] About shows the version you're running after an update It read Configs/version from the user folder, which is copied once and never refreshed, so it kept showing the first version you installed. It reads the stamp that ships with the build now. --- theseus/desktop/menu_bar.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/theseus/desktop/menu_bar.cpp b/theseus/desktop/menu_bar.cpp index 58fedd5..b608f1e 100644 --- a/theseus/desktop/menu_bar.cpp +++ b/theseus/desktop/menu_bar.cpp @@ -4,6 +4,14 @@ #include "std.h" #include "dashapp.h" #include "app_paths.h" + +// The version stamp belongs to the build, not the user folder: Configs there is +// seeded once and never refreshed, so it would show the first version forever. +static const char* ShippedVersionPath() { + static char path[1024]; + snprintf(path, sizeof(path), "%s/Configs/version", Plat_ShippedDir()); + return path; +} #include "panel_shared.h" #include "title_maker.h" #include "hdd_browser.h" @@ -234,7 +242,7 @@ void RenderControllerMenu() { static bool s_verRead = false; if (!s_verRead) { s_verRead = true; - FILE* vf = fopen(AppPath("Configs/version"), "r"); + FILE* vf = fopen(ShippedVersionPath(), "r"); if (vf) { if (fgets(s_ver, sizeof(s_ver), vf)) s_ver[strcspn(s_ver, "\r\n")] = 0; fclose(vf); } } const float tw = ImGui::CalcTextSize(s_ver[0] ? s_ver : "").x; @@ -1244,7 +1252,7 @@ void RenderAboutWindow() { static char s_version[64] = ""; if (!s_version[0]) { - FILE* vf = fopen(AppPath("Configs/version"), "r"); + FILE* vf = fopen(ShippedVersionPath(), "r"); if (vf) { char line[128]; while (fgets(line, sizeof(line), vf)) { From ac080715f706205c4dede2cdb9c4d2be5c6c09b0 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:02:32 -0500 Subject: [PATCH 110/167] Fixed skin editor egg glow colors snapping back to yellow EggGlow and EggGlowPulse rebuild their colors every frame from their own stored pair, but the editor read and wrote the falloff colors underneath, so edits got overwritten right away and Save wrote the old pair back out. The editor reads and writes the stored pair now. Fixes #86 --- theseus/desktop/materials_desktop.cpp | 18 ++++++++++++++++++ theseus/render/materials.cpp | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/theseus/desktop/materials_desktop.cpp b/theseus/desktop/materials_desktop.cpp index 9ba7d0a..7da9085 100755 --- a/theseus/desktop/materials_desktop.cpp +++ b/theseus/desktop/materials_desktop.cpp @@ -1015,6 +1015,14 @@ DWORD MatInfo_GetColor(CMatInfo* p, int idx) return D3DCOLOR_RGBA(m->m_r, m->m_g, m->m_b, m->m_a); if (CModulateTextureMatInfo* m = dynamic_cast(p)) return D3DCOLOR_RGBA(m->m_r, m->m_g, m->m_b, m->m_a); + // The egg glows rebuild their falloff colors every frame from these, so + // that's where the skin's ColorA / ColorB actually live. + if (CEggGlowPulseMatInfo* m = dynamic_cast(p)) { + const CustomColor& c = (idx == 0) ? m->Color1 : m->Color2; + return D3DCOLOR_RGBA(c.r, c.g, c.b, c.a); + } + if (CEggGlowFadeMatInfo* m = dynamic_cast(p)) + return (idx == 0) ? m->m_colorSide : D3DCOLOR_RGBA(m->Color1.r, m->Color1.g, m->Color1.b, m->Color1.a); if (CInnerWallMatInfo* m = dynamic_cast(p)) return (idx == 0) ? m->m_colorSide : m->m_colorFront; if (CFalloffMatInfo* m = dynamic_cast(p)) @@ -1035,6 +1043,16 @@ void MatInfo_SetColor(CMatInfo* p, int idx, DWORD c) { m->m_r=r; m->m_g=g; m->m_b=b; m->m_a=a; return; } if (CModulateTextureMatInfo* m = dynamic_cast(p)) { m->m_r=r; m->m_g=g; m->m_b=b; m->m_a=a; return; } + CustomColor cc; + cc.r = r; cc.g = g; cc.b = b; cc.a = a; + if (CEggGlowPulseMatInfo* m = dynamic_cast(p)) { + if (idx == 0) m->Color1 = cc; else m->Color2 = cc; + return; + } + if (CEggGlowFadeMatInfo* m = dynamic_cast(p)) { + if (idx == 0) m->m_colorSide = c; else m->Color1 = cc; + return; + } if (CInnerWallMatInfo* m = dynamic_cast(p)) { if (idx == 0) m->m_colorSide = c; else m->m_colorFront = c; return; diff --git a/theseus/render/materials.cpp b/theseus/render/materials.cpp index 3ba2120..a46fa0e 100755 --- a/theseus/render/materials.cpp +++ b/theseus/render/materials.cpp @@ -1015,6 +1015,14 @@ DWORD MatInfo_GetColor(CMatInfo* p, int idx) return D3DCOLOR_RGBA(m->m_r, m->m_g, m->m_b, m->m_a); if (CModulateTextureMatInfo* m = dynamic_cast(p)) return D3DCOLOR_RGBA(m->m_r, m->m_g, m->m_b, m->m_a); + // The egg glows rebuild their falloff colors every frame from these, so + // that's where the skin's ColorA / ColorB actually live. + if (CEggGlowPulseMatInfo* m = dynamic_cast(p)) { + const CustomColor& c = (idx == 0) ? m->Color1 : m->Color2; + return D3DCOLOR_RGBA(c.r, c.g, c.b, c.a); + } + if (CEggGlowFadeMatInfo* m = dynamic_cast(p)) + return (idx == 0) ? m->m_colorSide : D3DCOLOR_RGBA(m->Color1.r, m->Color1.g, m->Color1.b, m->Color1.a); if (CInnerWallMatInfo* m = dynamic_cast(p)) return (idx == 0) ? m->m_colorSide : m->m_colorFront; if (CFalloffMatInfo* m = dynamic_cast(p)) @@ -1035,6 +1043,16 @@ void MatInfo_SetColor(CMatInfo* p, int idx, DWORD c) { m->m_r=r; m->m_g=g; m->m_b=b; m->m_a=a; return; } if (CModulateTextureMatInfo* m = dynamic_cast(p)) { m->m_r=r; m->m_g=g; m->m_b=b; m->m_a=a; return; } + CustomColor cc; + cc.r = r; cc.g = g; cc.b = b; cc.a = a; + if (CEggGlowPulseMatInfo* m = dynamic_cast(p)) { + if (idx == 0) m->Color1 = cc; else m->Color2 = cc; + return; + } + if (CEggGlowFadeMatInfo* m = dynamic_cast(p)) { + if (idx == 0) m->m_colorSide = c; else m->Color1 = cc; + return; + } if (CInnerWallMatInfo* m = dynamic_cast(p)) { if (idx == 0) m->m_colorSide = c; else m->m_colorFront = c; return; From 9ecf2a82154762d52a51267ef75bd4b03ce7cde7 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:05:02 -0500 Subject: [PATCH 111/167] Fixed the OpenGL ES renderer loading the wrong shaders Renderer=opengles never mapped to the essl shaders we ship, so it fell back to spirv (or metal on a Mac) and handed those to the GL driver, which can't build them and nothing draws. Needs a test on a Pi or other GLES box. See #59. --- theseus/desktop/imgui_impl_bgfx.cpp | 1 + theseus/desktop/sdl_main.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/theseus/desktop/imgui_impl_bgfx.cpp b/theseus/desktop/imgui_impl_bgfx.cpp index 012e036..1249165 100644 --- a/theseus/desktop/imgui_impl_bgfx.cpp +++ b/theseus/desktop/imgui_impl_bgfx.cpp @@ -45,6 +45,7 @@ static bgfx::ShaderHandle LoadShader(const char* name) case bgfx::RendererType::Metal: sub = "metal"; break; case bgfx::RendererType::Vulkan: sub = "spirv"; break; case bgfx::RendererType::OpenGL: sub = "glsl"; break; + case bgfx::RendererType::OpenGLES: sub = "essl"; break; default: break; } char path[512]; diff --git a/theseus/desktop/sdl_main.cpp b/theseus/desktop/sdl_main.cpp index 77412b9..f960cb0 100644 --- a/theseus/desktop/sdl_main.cpp +++ b/theseus/desktop/sdl_main.cpp @@ -45,6 +45,7 @@ bgfx::ShaderHandle theseus_bgfx_load_shader(const char* name) case bgfx::RendererType::Metal: sub = "metal"; break; case bgfx::RendererType::Vulkan: sub = "spirv"; break; case bgfx::RendererType::OpenGL: sub = "glsl"; break; + case bgfx::RendererType::OpenGLES: sub = "essl"; break; default: break; } char path[512]; From 786b982b7610f06134cce6c874ff6781cb68fc3c Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:06:44 -0500 Subject: [PATCH 112/167] Version 0.4.0 Configs/version still carried UIX Lite's 0.5.250605 from the forked XIPs. This release is 0.4.0; builds append the date and commit as before. --- Configs/version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configs/version b/Configs/version index 33196e8..085201d 100644 --- a/Configs/version +++ b/Configs/version @@ -1,2 +1,2 @@ [default] -version=0.5.250605 +version=0.4.0 From 61718d65c5e3d8ca0054c97a971c09ccccbe0626 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:08:45 -0500 Subject: [PATCH 113/167] Fixed Ctrl+Q not quitting The File menu showed Ctrl+Q but nothing listened for it, so it only worked where the OS sends its own quit (Cmd+Q on a Mac). It now quits the same way File > Quit does. Fixes #37 --- theseus/desktop/sdl_main.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/theseus/desktop/sdl_main.cpp b/theseus/desktop/sdl_main.cpp index f960cb0..df27738 100644 --- a/theseus/desktop/sdl_main.cpp +++ b/theseus/desktop/sdl_main.cpp @@ -1753,6 +1753,13 @@ int main(int argc, char* argv[]) { Keyboard_InsertText(g_pActiveKeyboard, event.text.text); break; case SDL_KEYDOWN: + // Ctrl+Q quits from anywhere, same as File > Quit. + if (event.key.keysym.sym == SDLK_q && (event.key.keysym.mod & (KMOD_CTRL | KMOD_GUI))) { + SDL_Event quit; + quit.type = SDL_QUIT; + SDL_PushEvent(&quit); + break; + } // Esc / Q / Backspace quits the xCloud stream and drops // back to the dashboard. if (g_xcloudFullscreen) { From efdd766382b74fe1b5f5453294ce1c4ed0bf705f Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:11:31 -0500 Subject: [PATCH 114/167] Fixed the White skin's wall showing up as big stripes White ships its own cell wall mesh but no shell.xbx, so the wall fell back to the stock texture, which doesn't fit that mesh. Falling back to Stock can't fix it since Stock has no shell either. Gave White the Carbon Fiber-White wall texture, the only one we ship that lines up with its mesh. Fixes #71 --- Data/Skins/White/shell.xbx | Bin 0 -> 34816 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Data/Skins/White/shell.xbx diff --git a/Data/Skins/White/shell.xbx b/Data/Skins/White/shell.xbx new file mode 100644 index 0000000000000000000000000000000000000000..072108ef95599ba2327af1f061de839e2247f4b4 GIT binary patch literal 34816 zcmeI5J&O}j6oyB!5VaKC!atymf?0ouh>NDOx7Wf(?feB178Yy*8W~art7zqx=}eHw zVx?d&EP@~B$#8JJIJ*{to9uI%H}?)RaPQ1LU+=vqx2|2kTs^L;>SR?Nt4_p^MlPIr z6g^e-BcxWG0cXG&a0Z+KXJDmf;QhyUlR-!up8qu8xg9d!*_=KN0e3^d2O*o&?&M(z z_&uaMS=;^)(w)r4Ja_laxmV!Y_H4{^UXG_D+Ou(Q#Qh-sz0pSfHDoVl|ILu~`bMAo zHtMteXCa;bdQH2;D_d6=&rj@Zbr$V5Y#Vp>JAU6k-=W{X>tFq+{@4C#|Mh?L|MY+L z|BZi)|BQc)|IL4x|1tk%{?GiU`Cs$j=KtkC%|J}xmXMghO zi6!5jZFu>MA<5IjQVfv(;>uJuS3A&A>?7X zAG{O-o(h4t=Q+vaw_%R64RhRyIo=W-zwh7g-}SHlYxZfW|FwVGfBhf*KmA|)@bvo= z_5Y23_%4k9jDL;)&3~BxY0kx%|1$rFT{QpM@CN388y?O4xA}kh5BVSYFZn{zKe6j;Qf%^ z=*fsWOXNkVYjC%|x2RV@JL?~S>@VQHtbdUE4a$88xlbXnyk55RJ-6XO<7eEfbpFBj z|9d?U-~aygFI@kwfA#S`}`5*H?@;|Fl|Ht^x`0psie{JX9vp;#{#F9VGcJk3ncmB2I ze*|(|Iqt;lPb~R(+SDI_zh@tB;$B{#bK=SSvk!njlmNMpeFA)cAp8B;N5J_AkopGL zndc$+oi9Sj$8jC}Q~d+<8P`GfACx?QzUMZ~aVO@uOZ5FW^$&diOLqR-^}p}`qvX0z zEZ2RuUH`8CrL2GA`gi?DeXi7FC8pj>EU%;Y*tePbPy4U^*Z&_3hSAHqu#Cd-~6xspZS0N|3(kU{}}(JU6lVD4&giD aM?v_>|5pF2@aN0?UgrH0%j?VT|NjKKHWYFI literal 0 HcmV?d00001 From e344325e083deabd058fc206756b4c4607be96bf Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:17:30 -0500 Subject: [PATCH 115/167] Fixed garbled language names and the Dolby sign in settings On desktop \u escapes were dropped to a bare "u", so Japanese showed as u65e5u672cu8a9e and Dolby as DOLBYu00AE. They turn into UTF-8 now. The accented letters in FRANCAIS, ESPANOL and PORTUGUES had been saved as replacement characters in settings3.xap, so I put the real letters back and repacked default.xip. The Xbox copy of settings3.xap has the same broken letters. Fixes #44 --- Data/Xips/default.xip | Bin 1687007 -> 1687001 bytes theseus/engine/xap_compile.cpp | 52 +++++++++++++++++++++- xips-source/default-desktop/settings3.xap | 12 ++--- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/Data/Xips/default.xip b/Data/Xips/default.xip index a3063b873f9449f661762778cb080e0b12b3c02c..96c35d119b33b4487bd210dff4aedb888fa0d641 100644 GIT binary patch delta 358 zcmccrJMHH0G~Ni$0D}We3=E<`C^?aLd;R?xj0`Za_L2|-0|z4mP#z4{-V$b*tN`Tz zDTm*}3|rSC_+M9uFuYbn@E@EPVfc;2zx_>wA-WkMZ&oD=G@k`-zFCtbA49M-LP2M9 zAY*$VBM37AF*6Xe05K~NvjH(X5OV-AClGT1F*gwN05LBR^8qnG5DNgY;Pya9Ax9t9 z!|jfq!P66cg~X??_Z5<4IXuzd2gH@0{=!%22-D%N?d$!74){aWDa;p=oZdH2$Q+^$ u$Q9fEV4jdCBlF>|;ORiFXuHz_As`k8Vi6z)@x_2x9Ec^hJ1vl$D+>TaoLFE0 delta 379 zcmcclJMI4OG~Ni$0D}We3=E<`C_j;Rd;QZHj0`Za`H~O=0|z4mP#z36-x6k+tN`Tz zDYxIk3|rSC_`g?(FuYbn@SmL*Vfc;2fA~#=A-WkMZ&M`-G@k`-zD<)PA49M-LP39X zAY*$VBM37AF*6Xe05K~NvjH(X5OV-AClGT1F*gwN05LBR^8qnG5DNgY;Pya9Ax9s! z_xtxcdInEt^b?YrzR_1mj1?&0?=wBoS4evL{dq!M(?9qM?P3QC1}iCOPA{A*Bs|?_ zfe_F3^L|2e{h5K{)AQyD1w(Ze&Jz-wZZKbnWjj!-D> 6)); + utf8[1] = (char)(0x80 | (cp & 0x3F)); + nBytes = 2; + } + else + { + utf8[0] = (char)(0xE0 | (cp >> 12)); + utf8[1] = (char)(0x80 | ((cp >> 6) & 0x3F)); + utf8[2] = (char)(0x80 | (cp & 0x3F)); + nBytes = 3; + } + if (pchOut + nBytes > pchEnd) + { + SyntaxError(_T("string constant is too long")); + return -1; + } + for (int i = 0; i < nBytes; i++) + *pchOut++ = utf8[i]; + } + continue; #endif default: diff --git a/xips-source/default-desktop/settings3.xap b/xips-source/default-desktop/settings3.xap index f086266..4656031 100644 --- a/xips-source/default-desktop/settings3.xap +++ b/xips-source/default-desktop/settings3.xap @@ -2812,12 +2812,12 @@ function InitializeLanguageMenu() astrLanguageButtonText = new Array( "ENGLISH", // English "\u65e5\u672c\u8a9e", // Japanese "DEUTSCH", // German - "FRAN�AIS", // French - "ESPA�OL", // Spanish + "FRANÇAIS", // French + "ESPAÑOL", // Spanish "ITALIANO", // Italian "\uD55C\uAD6D\uC5B4", // Korean "\u7E41\u9AD4\u4E2D\u6587", // Traditional Chinese - "PORTUGU�S" ); // Portuguese + "PORTUGUÊS" ); // Portuguese } function StartLanguageMenu() @@ -3703,9 +3703,9 @@ function UpdateRootMeta() else if (nLanguage == 3) metaString = "DEUTSCH"; // German else if (nLanguage == 4) - metaString = "FRAN�AIS"; // French + metaString = "FRANÇAIS"; // French else if (nLanguage == 5) - metaString = "ESPA�OL"; // Spanish + metaString = "ESPAÑOL"; // Spanish else if (nLanguage == 6) metaString = "ITALIANO"; // Italian else if (nLanguage == 7) @@ -3713,7 +3713,7 @@ function UpdateRootMeta() else if (nLanguage == 8) metaString = "\u7E41\u9AD4\u4E2D\u6587"; // Traditional Chinese else if (nLanguage == 9) - metaString = "PORTUGU�S"; // Portugues + metaString = "PORTUGUÊS"; // Portugues else metaString = ""; } From 77c9edb770bc63eb53d97315e1f7754e8cedeab7 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:21:49 -0500 Subject: [PATCH 116/167] Fixed "." and ".." showing up as skins The desktop directory listing handed the dashboard the . and .. entries, which the Xbox never has, so the skins menu listed them as skins. They're skipped now, same as on the Xbox. --- theseus/desktop/xbox_file.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/theseus/desktop/xbox_file.cpp b/theseus/desktop/xbox_file.cpp index 0eb2026..4899be8 100644 --- a/theseus/desktop/xbox_file.cpp +++ b/theseus/desktop/xbox_file.cpp @@ -354,6 +354,8 @@ BOOL NextReal(FindHandle* ffh, WIN32_FIND_DATAA* fd) { char name[260]; HostFileInfo info; while (Host_ReadDir(ffh->real, name, sizeof(name), &info)) { + // FATX has no . or .. entries, so Xbox code never expects them. + if (!strcmp(name, ".") || !strcmp(name, "..")) continue; if (!PatternMatch(ffh->pattern, name)) continue; memset(fd, 0, sizeof(*fd)); strncpy(fd->cFileName, name, sizeof(fd->cFileName) - 1); From a2a11479d000139fd72a95692fe0acea5298c55f Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:22:32 -0500 Subject: [PATCH 117/167] Fixed a script error every time the skins menu opens The skins menu called EnableAudio, which doesn't exist, so opening it threw a runtime error box. It calls EnableAmbientAudio like the other settings menus now. Repacked default.xip. The Xbox copies of skins.xap have the same call. --- Data/Xips/default.xip | Bin 1687001 -> 1687008 bytes xips-source/default-desktop/skins.xap | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Data/Xips/default.xip b/Data/Xips/default.xip index 96c35d119b33b4487bd210dff4aedb888fa0d641..8ace72d614ad7e57a16ddd3e57a39464ab09e821 100644 GIT binary patch delta 154 zcmcclJMF>mG~Ni$0D}We3=E<`sIZaOoRK$5ngI?bGzT)a2Qq>%6A&{4F$)m00x=s9 zvjZ^)5OV@C7Z7s;F%J;)0x=&D^8>K}5DNma5D*Iku?P^00oRK$JngI?vn*$l!0~tY>35c12m<5PgftU@5 z*@2h?h&h3n3y8Uamv`Lq J7f8;P1prOGBpLt! diff --git a/xips-source/default-desktop/skins.xap b/xips-source/default-desktop/skins.xap index f00e5f4..4c73f3b 100644 --- a/xips-source/default-desktop/skins.xap +++ b/xips-source/default-desktop/skins.xap @@ -297,6 +297,6 @@ DEF theSkinsMenu Level function OnActivate() { - EnableAudio(); + EnableAmbientAudio(); } } From c2652f27e27622a6ad0d3ac969b314ea42b318b7 Mon Sep 17 00:00:00 2001 From: Milenko <39630670+MrMilenko@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:26:58 -0500 Subject: [PATCH 118/167] Crashes leave a report file you can send Each crash now writes crashes/