diff --git a/crates/skate-game/src/custom_models.rs b/crates/skate-game/src/custom_models.rs index d647a16f..475a597a 100644 --- a/crates/skate-game/src/custom_models.rs +++ b/crates/skate-game/src/custom_models.rs @@ -348,9 +348,9 @@ fn start_import(directory: &Path, reference: &Path) -> Result { .map_err(|e| e.to_string())? .parent() .ok_or("Missing game directory")? - .join("support/custom-models/Mixamo to Skate.exe"); + .join("support/skate3setup.exe"); if !executable.is_file() { - return Err("Character importer is missing. Use the complete Custom Models build.".into()); + return Err("Character importer is missing. Restore support/skate3setup.exe from the complete Windows package.".into()); } let jobs = directory.join("jobs"); std::fs::create_dir_all(&jobs).map_err(|e| e.to_string())?; @@ -359,7 +359,13 @@ fn start_import(directory: &Path, reference: &Path) -> Result { .unwrap_or_default() .as_nanos(); let result = jobs.join(format!("{}-{nonce}.json", std::process::id())); - let mut command = Command::new(executable); + let mut command = Command::new(&executable); + // Preserve optional calibration from earlier local Custom Models builds. + let profile = executable.parent().unwrap().join("custom-models/calibration.json"); + command.arg("--character-import"); + if profile.is_file() { + command.arg("--profile").arg(profile); + } command .arg("--library-import") .arg(directory) diff --git a/docs/THIRD_PARTY_NOTICES.md b/docs/THIRD_PARTY_NOTICES.md index e5bdd7fb..6401bcb1 100644 --- a/docs/THIRD_PARTY_NOTICES.md +++ b/docs/THIRD_PARTY_NOTICES.md @@ -30,3 +30,10 @@ from this project. The setup executable bundles Python, NumPy, Pillow, Tcl/Tk and PyInstaller's bootloader. Their license information is retained by the packager alongside its bundled libraries. No game content is included in the release archive. + +Character import also bundles Facebook FBX2glTF 0.9.7, including its Autodesk +FBX SDK dependency, and the Microsoft Visual C++ redistributable runtime. +FBX2glTF's BSD notice is in `licenses/FBX2glTF.txt` and inside the setup payload. +The native converter is fetched from its official release and checked against +the pinned SHA-256 before packaging. No private calibration or skater reference +is distributed. diff --git a/docs/custom-models.md b/docs/custom-models.md index ae179698..ebfa499c 100644 --- a/docs/custom-models.md +++ b/docs/custom-models.md @@ -2,7 +2,9 @@ Open the pause menu, choose **Custom models**, then **Import model...**. The native file picker accepts Mixamo FBX and self-contained GLB characters. Successful imports generate textured thumbnail cards and equip automatically. Click any saved card to swap, or **Use stock skater** to restore the regular customiser. Selection persists between sessions. The three-column grid scrolls vertically with the mouse wheel. Keyboard: arrow keys and Enter; controller: D-pad and A; Escape/B returns to the pause menu. Keyboard/controller selection scrolls into view. The Show filter switches between all models, native pros, native specials, and imports. -The game runs `support/custom-models/Mixamo to Skate.exe` as a hidden child process; its file picker remains visible. Bundle the complete PyInstaller onedir output, FBX2glTF under `tools`, licenses, and optionally a locally generated `calibration.json`. Python and Blender are not required on the player's machine. Conversion instructions and restrictions are in `mixamo-to-skate.md`. +The game runs `support/skate3setup.exe --character-import` as a hidden child process; its file picker remains visible. The Windows release bundles the importer, Python, NumPy, Pillow, Tcl/Tk, FBX2glTF and its Visual C++ runtime inside that executable. No Blender, Python installation, development tools or import-time download is required. The game passes its active installation's stock skater reference explicitly. Conversion instructions and restrictions are in `mixamo-to-skate.md`. + +The existing updater already hashes and replaces the setup executable alongside the game. This also delivers the importer to older releases whose updater only knows those original program files. No separate helper tree needs installing or updating. Importer-only changes do not change owned-disc extractor fingerprints or require map conversion. User library entries, selection, settings and optional legacy `support/custom-models/calibration.json` are not updater targets. That calibration remains optional and is never included in public packages. The library lives in `%LOCALAPPDATA%/Skate3RustEngine/custom-characters`. Each content-addressed entry contains `manifest.json`, `character.glb`, `source.glb`, `preview.png`, and a conversion report when converted. Imports publish through an atomic directory rename, so failed imports never appear as completed entries. Saved assets do not depend on the original FBX location. Import failures/cancellation preserve the existing character. @@ -10,6 +12,8 @@ The game loads each candidate as a hidden scene, waits for its dependencies and The paired calibration reproduces the supplied stock character's bind geometry; arbitrary humanoids use the converter's rig fitting and may need adjustment for unusual proportions. This is not a promise of original weight recovery or identical deformation on every model. -Build the separate executable with `cargo build --release --locked --target x86_64-pc-windows-msvc -p skate-game --bin skate3-custom-models --no-default-features`, using `RUSTFLAGS=-C target-feature=+crt-static`. Stage beside `support/custom-models` and launch with `--assets` pointing at the prepared owned asset directory. +Build the complete package with `scripts/Build-Release.ps1`. It obtains the pinned, checksum-verified FBX2glTF binary and stages the redistributable runtime from the build machine's Visual Studio installation. Packaging fails unless the frozen helper can import a procedural character, generate its thumbnail and report invalid FBX input without changing existing library data. Checks run without the game or a graphical window. + +Manual release checks: open Custom models and import a supported skinned Mixamo FBX; verify the picker opens, the thumbnail appears and the character equips. Repeat with a supported self-contained GLB. Cancel the picker and try an unsupported model; the current character and saved cards should remain. Restart and check selection persistence. Switch to a native pro/special and back to the stock skater; verify animation, board, outfit customisation and contacts. Finally update an older portable copy and repeat import while checking existing models and settings remain. These visual/gameplay checks must be performed by the player. Verification: procedural Python conversion/library tests, Rust library discovery and candidate binding tests, release compilation, packaged importer conversion of the supplied FBX, thumbnail inspection, and independent Khronos glTF validation. No game launch or gameplay testing was performed; menu rendering and live visual swapping still need user playtesting. diff --git a/docs/mixamo-to-skate.md b/docs/mixamo-to-skate.md index 2d19c2db..9516f804 100644 --- a/docs/mixamo-to-skate.md +++ b/docs/mixamo-to-skate.md @@ -7,14 +7,12 @@ Use owned/local assets as the stock reference. Blender is not required. ## Player workflow -The Windows package contains `Mixamo to Skate.exe` and `tools/FBX2glTF.exe`. -Drag one or more FBX files onto the executable, or double-click it and choose -files. Results appear in a new `-skate` directory next to the input; -existing results get a numeric suffix. The directory contains `character.glb`, -`report.json`, and a short readme. The stock reference is found in the local -Skate3RustEngine installation; multiple/missing installations prompt for it. +In the Windows game package, use **Custom models → Import model...**. The +importer and FBX2glTF are embedded in `support/skate3setup.exe`; the game +provides the current copy's prepared stock reference. Successful imports become +saved thumbnail cards and equip automatically. No additional software is needed. -No settings are modified. Output publication is transactional: failure leaves +Output publication is transactional: failure leaves existing characters and source files unchanged. No assets are uploaded. ## Source use @@ -31,8 +29,12 @@ python tools/mixamo_to_skate/main.py model.glb --reference path/to/private/skate The input GLB option is for an already normalized Mixamo conversion, not an arbitrary rig. Arguments are passed directly to FBX2glTF without a shell. Embedded FBX media extraction goes to a disposable temporary directory. +The in-game library also accepts an already converted stock-rig GLB when it +passes validation against the active installation's stock reference. -To package with an existing Python/NumPy/PyInstaller environment: +The complete game release is built with `scripts/Build-Release.ps1`, including +a frozen importer smoke check. The following optional standalone developer +package is separate from the in-game release path: ```text python -m PyInstaller --onedir --console --name "Mixamo to Skate" --paths tools/mixamo_to_skate --distpath /app --workpath /build --specpath /build tools/mixamo_to_skate/main.py @@ -64,7 +66,8 @@ proportions require visual review; success is a structural validation result, not a guarantee that every skating motion looks correct. Generic fitting aligns limb directions and positions to the stock rig. An -optional private `calibration.json` next to the executable improves fitting +optional private `calibration.json` (legacy in-game location: +`support/custom-models/calibration.json`, or explicit CLI `--profile`) improves fitting using the user's round-trip pair: the original stock GLB and a Mixamo-rigged copy of the same neutral geometry, normalized to GLB by FBX2glTF. Generate it with `--calibrate --profile calibration.json` and the normalized source GLB. diff --git a/docs/updates.md b/docs/updates.md index 4c4c8d01..d36cc9cc 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -25,6 +25,11 @@ unchanged groups are retained. Freshly unpacked copies run setup independently. Explicit `--assets` developer launches bypass this management. Packaged releases are required; developer binaries do not silently adopt a downloaded build. +Character import and its FBX/native dependencies are embedded in the setup +executable. These are delivered by the original update protocol too, including +to copies that never had a separate Custom Models helper. Importer-only changes +do not require owned-disc extraction. Optional local calibration is preserved. + # Package protocol and release maintenance `release.json` accompanies the ZIP and is also inside it. Schema 1 identifies the diff --git a/scripts/Build-Release.ps1 b/scripts/Build-Release.ps1 index 746f794b..91665ddb 100644 --- a/scripts/Build-Release.ps1 +++ b/scripts/Build-Release.ps1 @@ -49,6 +49,8 @@ try { if ($source.Extension -notin '.py','.json','.txt','.md','.toml','.rs' -and $source.Name -ne 'LICENSE') { continue } $relative = [IO.Path]::GetRelativePath($toolsRoot, $source.FullName) $portableName = $relative.Replace('\','/') + # Calibration is derived from private models, never a release resource. + if ($portableName -like 'mixamo_to_skate/*.json') { continue } if ($portableName -match '(^|/)blender[^/]*(/|$)' -or $portableName -in @('asset_pipeline/build_map.py','asset_pipeline/finish_character.py', 'add_onboard_ik_targets.py','apply_default_skater_materials.py','export_bevy_glb.py')) { continue } @@ -56,15 +58,21 @@ try { New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null Copy-Item -LiteralPath $source.FullName -Destination $destination } + $importerBinaries = Join-Path $ProjectRoot 'target/importer-runtime' + & "$PSScriptRoot/Prepare-CharacterImporter.ps1" -Destination $importerBinaries & $packagePython -m PyInstaller --noconfirm --clean --onefile --name skate3setup ` --icon "$ProjectRoot/docs/images/skating-crab.ico" --paths $ProjectRoot ` --hidden-import numpy --hidden-import PIL.Image --hidden-import tkinter ` --add-binary "$ProjectRoot/target/native/refpack.dll;tools/asset_pipeline" ` + --add-binary "$importerBinaries/FBX2glTF.exe;tools/mixamo_to_skate/tools" ` + --add-binary "$importerBinaries/*.dll;tools/mixamo_to_skate/tools" ` --exclude-module bpy --exclude-module mathutils ` --copy-metadata numpy --copy-metadata Pillow --copy-metadata PyInstaller ` --add-data "$sourceStage/tools;tools" --add-data "$ProjectRoot/docs/images/skating-crab.ico;docs/images" ` --distpath "$stage/support" --workpath target/setup-build/work --specpath target/setup-build tools/setup.py if ($LASTEXITCODE -ne 0) { throw 'Setup packaging failed' } + & $packagePython tools/mixamo_to_skate/check_package.py --setup "$stage/support/skate3setup.exe" + if ($LASTEXITCODE -ne 0) { throw 'Packaged character importer verification failed' } & $packagePython -m PyInstaller --noconfirm --clean --onefile --windowed --name skate3update ` --distpath "$stage/support" --workpath target/updater-build/work --specpath target/updater-build tools/updater.py if ($LASTEXITCODE -ne 0) { throw 'Updater packaging failed' } @@ -91,7 +99,9 @@ try { Copy-Item -LiteralPath docs/retail-renderer.md -Destination "$stage/docs/retail-renderer.md" Copy-Item -LiteralPath docs/crash-reports.md -Destination "$stage/docs/crash-reports.md" Copy-Item -LiteralPath docs/updates.md -Destination "$stage/docs/updates.md" + Copy-Item -LiteralPath docs/custom-models.md,docs/mixamo-to-skate.md -Destination "$stage/docs" New-Item -ItemType Directory -Path "$stage/licenses" -Force | Out-Null + Copy-Item -LiteralPath tools/mixamo_to_skate/licenses/FBX2glTF.txt -Destination "$stage/licenses/FBX2glTF.txt" Copy-Item -LiteralPath tools/vendor/utt/LICENSE -Destination "$stage/licenses/UTT.txt" Copy-Item -LiteralPath tools/vendor/university/LICENSE-PROJECT.md -Destination "$stage/licenses/CustomEngineLayer.txt" Copy-Item -LiteralPath vendor/bevy_pbr/LICENSE-MIT -Destination "$stage/licenses/Bevy-MIT.txt" diff --git a/scripts/Prepare-CharacterImporter.ps1 b/scripts/Prepare-CharacterImporter.ps1 new file mode 100644 index 00000000..493780bd --- /dev/null +++ b/scripts/Prepare-CharacterImporter.ps1 @@ -0,0 +1,24 @@ +param([Parameter(Mandatory)][string]$Destination) +$ErrorActionPreference = 'Stop' +$root = Split-Path $PSScriptRoot -Parent +$cache = Join-Path $root 'target/importer-deps' +New-Item -ItemType Directory -Path $cache,$Destination -Force | Out-Null +$exe = Join-Path $cache 'FBX2glTF.exe' +$sha = '8d90fb5e0a8d186a3d9a7ff8c75eaee541c3975ce4df0d80351f20092ae0877f' +if (-not (Test-Path -LiteralPath $exe)) { + Invoke-WebRequest 'https://github.com/facebookincubator/FBX2glTF/releases/download/v0.9.7/FBX2glTF-windows-x64.exe' -OutFile $exe +} +if ((Get-FileHash -LiteralPath $exe -Algorithm SHA256).Hash.ToLower() -ne $sha) { + throw 'FBX2glTF checksum mismatch; remove target/importer-deps/FBX2glTF.exe and retry.' +} +Copy-Item -LiteralPath $exe -Destination $Destination +# FBX2glTF uses the VC runtime. Ship the redistributable DLLs beside it so +# players do not need Visual Studio or a separate runtime installer. +$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio/Installer/vswhere.exe' +$vs = (& $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath) +$crt = Get-ChildItem -Path "$vs/VC/Redist/MSVC/*/x64/Microsoft.VC*.CRT" -Directory | + Sort-Object FullName -Descending | Select-Object -First 1 +if (-not $crt) { throw 'Visual C++ x64 redistributable files are required to package the importer.' } +foreach ($name in @('msvcp140.dll','vcruntime140.dll','vcruntime140_1.dll')) { + Copy-Item -LiteralPath (Join-Path $crt.FullName $name) -Destination $Destination +} diff --git a/tools/mixamo_to_skate/check_package.py b/tools/mixamo_to_skate/check_package.py new file mode 100644 index 00000000..159693b6 --- /dev/null +++ b/tools/mixamo_to_skate/check_package.py @@ -0,0 +1,96 @@ +"""Exercise the shipped payload without UI, a game process, or private assets.""" +import argparse +import json +import os +from pathlib import Path +import subprocess +import shutil +import sys +import tempfile + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--setup', type=Path) + parser.add_argument('--inside', action='store_true') + args = parser.parse_args() + if args.inside: + import hashlib + import tkinter + from PIL import Image + from main import app_root + tool = app_root()/'tools/FBX2glTF.exe' + assert hashlib.sha256(tool.read_bytes()).hexdigest() == '8d90fb5e0a8d186a3d9a7ff8c75eaee541c3975ce4df0d80351f20092ae0877f' + for name in ('msvcp140.dll', 'vcruntime140.dll', 'vcruntime140_1.dll'): + assert tool.with_name(name).is_file(), name + result = subprocess.run([str(tool), '--version'], capture_output=True, timeout=30) + assert result.returncode == 0, result.stderr + assert b'0.9.7' in result.stdout + result.stderr + return + from test_converter import fixture + from converter import validate_output + from PIL import Image + setup = args.setup.resolve() + # Child must use only its packaged Python/native dependencies, never PATH. + env = {k: v for k, v in os.environ.items() if not k.startswith('PYTHON')} + env['PATH'] = str(Path(os.environ['SystemRoot'])/'System32') + with tempfile.TemporaryDirectory(prefix='character package ') as temp: + root = Path(temp) + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + import updater + install = root/'old portable copy' + tx = install/'.update-transaction' + # Protocol 1 knows only these paths. Begin with no separate importer, + # replace setup through the real transaction, then test the delivered EXE. + assert updater.FILES == ('skate3rust.exe', 'support/skate3setup.exe', + 'support/skate3update.exe', 'release.json') + for name in updater.FILES: + (install/name).parent.mkdir(parents=True, exist_ok=True) + (install/name).write_bytes(b'old program') + (tx/'new'/name).parent.mkdir(parents=True, exist_ok=True) + (tx/'new'/name).write_bytes(b'new program') + preserved = ('data/installation.json', 'settings/graphics.json', + 'support/custom-models/calibration.json') + for name in preserved: + (install/name).parent.mkdir(parents=True, exist_ok=True) + (install/name).write_bytes(b'preserve') + shutil.copy2(setup, tx/'new/support/skate3setup.exe') + updater.install(install, tx) + assert all((install/name).read_bytes() == b'preserve' for name in preserved) + setup = install/'support/skate3setup.exe' + def run(*arguments): + return subprocess.run([str(setup), *map(str, arguments)], cwd=root, + env=env, capture_output=True, timeout=180) + probe = run('--task', 'tools/mixamo_to_skate/check_package.py', '--inside') + assert probe.returncode == 0, (probe.stdout, probe.stderr) + source, reference = root/'source.glb', root/'reference.glb' + fixture(source, True) + fixture(reference, False) + library, reply = root/'library', root/'reply.json' + library.mkdir() + selection = library/'selection.json' + selection.write_text('{"selected": "preserve"}') + arguments = ('--character-import', '--library-import', library, + '--reference', reference, '--result', reply, '--noninteractive') + result = run(*arguments, source) + response = json.loads(reply.read_text()) + assert result.returncode == 0 and response['status'] == 'ready', response + entry = library/'entries'/response['id'] + validate_output(entry/'character.glb', reference) + with Image.open(entry/'preview.png') as preview: + preview.verify() + before = {p.relative_to(library): p.read_bytes() for p in library.rglob('*') if p.is_file()} + # Invoke the real bundled FBX parser on invalid data and verify failure + # is reported without publishing a broken entry or changing selection. + invalid = root/'invalid.fbx' + invalid.write_bytes(b'not an FBX') + result = run(*arguments, invalid) + response = json.loads(reply.read_text()) + assert result.returncode == 1 and response['status'] == 'error', response + assert 'FBX conversion failed' in response['message'], response + assert before == {p.relative_to(library): p.read_bytes() for p in library.rglob('*') if p.is_file()} + print('Packaged importer: protocol-1 migration, native runtime, GLB import, thumbnail, FBX failure and data preservation passed.') + + +if __name__ == '__main__': + main() diff --git a/tools/mixamo_to_skate/licenses/FBX2glTF.txt b/tools/mixamo_to_skate/licenses/FBX2glTF.txt new file mode 100644 index 00000000..28015369 --- /dev/null +++ b/tools/mixamo_to_skate/licenses/FBX2glTF.txt @@ -0,0 +1,30 @@ +BSD License + +For FBX2glTF software + +Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tools/mixamo_to_skate/main.py b/tools/mixamo_to_skate/main.py index 539f70a2..c0716924 100644 --- a/tools/mixamo_to_skate/main.py +++ b/tools/mixamo_to_skate/main.py @@ -10,7 +10,12 @@ from glb import ConversionError, require def app_root(): - return Path(sys.executable).parent if getattr(sys,'frozen',False) else Path(__file__).resolve().parent + # Release setup carries source + native resources under its extraction root. + # A standalone frozen converter retains its legacy adjacent resource layout. + bundled = Path(__file__).resolve().parent + if (bundled/'tools/FBX2glTF.exe').is_file(): + return bundled + return Path(sys.executable).parent if getattr(sys,'frozen',False) else bundled def find_reference(expected_hash=None): diff --git a/tools/setup.py b/tools/setup.py index 122c0070..2a9ec881 100644 --- a/tools/setup.py +++ b/tools/setup.py @@ -6,6 +6,13 @@ ROOT=Path(getattr(sys,'_MEIPASS',Path(__file__).resolve().parents[1])) sys.path.insert(0,str(ROOT)) def main(): + if len(sys.argv)>1 and sys.argv[1]=='--character-import': + # Keep the importer inside the already versioned setup payload: even + # protocol-1 updaters deliver it atomically with the game executable. + importer=ROOT/'tools/mixamo_to_skate' + sys.path.insert(0,str(importer)) + from main import main as import_character + return import_character(sys.argv[2:]) if len(sys.argv)>2 and sys.argv[1]=='--task': script=Path(sys.argv[2]) if not script.is_absolute():script=ROOT/script diff --git a/tools/updater.py b/tools/updater.py index 3532d633..31b6a351 100644 --- a/tools/updater.py +++ b/tools/updater.py @@ -17,6 +17,8 @@ import zipfile REPO = 'SK8-ENGINE/skate-3-rust-engine' API = f'https://api.github.com/repos/{REPO}/releases' PACKAGE = 'skate3rust-windows-x64.zip' +# Keep protocol 1's paths stable: setup embeds character import and its native +# resources, so even older updaters deliver the complete importer in-place. FILES = ('skate3rust.exe', 'support/skate3setup.exe', 'support/skate3update.exe', 'release.json') PREFIX = 'skate3rust-windows-x64/'