Files
skate-3-rust-engine/tools/setup.py
T
Hails 18f41a33c5 Tooling: streaming map validator (--validate-maps) wired into setup
One skate3rust process validates TEST_WORLD and every converted map over
stdin/stdout (SKATE_MAP_CHECK lines). Existing checks still fail a map;
new spawn-support / neutral-startup / invisible-collision checks report
warnings. Setup falls back to per-map --check-assets with an older exe.
See docs/hails-additions/06-map-validator.md (PR F).

Note: install.py also carries PR G's customiser-overlap hunks
(available_memory, overlap_customiser, Background); they are interleaved
with the validator wiring in _install and are documented in doc 08.
2026-10-01 00:13:53 -06:00

144 lines
8.0 KiB
Python

"""First-run setup window and packaged Python conversion worker."""
from pathlib import Path
import argparse,os,queue,runpy,sys,threading,traceback
ROOT=Path(getattr(sys,'_MEIPASS',Path(__file__).resolve().parents[1]))
sys.path.insert(0,str(ROOT))
def saved_source(marker):
path=marker.get('source')
if not isinstance(path,str):return None
selected=Path(path)
if selected.is_file() or selected.is_dir():return selected
return None
LONG_PATHS_KEY=r'SYSTEM\CurrentControlSet\Control\FileSystem'
def long_paths_enabled():
import winreg
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,LONG_PATHS_KEY) as key:
return winreg.QueryValueEx(key,'LongPathsEnabled')[0]==1
except OSError:return False
def enable_long_paths():
"""Ask for elevation once to enable Win32 long paths; True only if this call enabled them."""
# Character roster work paths below data/installations exceed MAX_PATH for
# installs in deep folders. Declining UAC keeps setup running; only the
# affected optional characters are then reported unavailable.
if os.name!='nt' or long_paths_enabled():return False
import subprocess
command=("Start-Process reg.exe -Verb RunAs -Wait -WindowStyle Hidden -ArgumentList "
f"'add','HKLM\\{LONG_PATHS_KEY}','/v','LongPathsEnabled','/t','REG_DWORD','/d','1','/f'")
subprocess.run(['powershell.exe','-NoProfile','-NonInteractive','-Command',command],
creationflags=subprocess.CREATE_NO_WINDOW)
return long_paths_enabled()
def restart():
# Windows reads LongPathsEnabled once at process start.
import subprocess
env=os.environ.copy()
if getattr(sys,'frozen',False):
env['PYINSTALLER_RESET_ENVIRONMENT']='1'
command=[sys.executable,*sys.argv[1:]]
else:command=[sys.executable,str(Path(__file__).resolve()),*sys.argv[1:]]
return subprocess.call(command,env=env)
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
script=script.resolve()
if not script.is_relative_to((ROOT/'tools').resolve()):raise RuntimeError('Invalid conversion script')
sys.path.insert(0,str(script.parent))
sys.argv=[str(script),*sys.argv[3:]]
runpy.run_path(str(script),run_name='__main__')
return 0
parser=argparse.ArgumentParser()
parser.add_argument('--base',type=Path,required=True)
parser.add_argument('--game-exe',type=Path,required=True)
parser.add_argument('--refresh',action='store_true')
args=parser.parse_args()
if enable_long_paths():return restart()
import tkinter as tk
from tkinter import filedialog,messagebox,ttk
from tools.asset_pipeline.customiser_setup import install
from tools.asset_pipeline.versions import installed, fingerprints, changed_groups
from tools.asset_pipeline.group_receipts import damaged
previous=installed(args.base) if args.refresh else None
changed=changed_groups(previous[1].get('pipelines',{}),fingerprints()) if previous else set()
if previous:changed.update(damaged(*previous,exclude=changed))
updating=previous is not None
reuse=saved_source(previous[1]) if previous else None
window=tk.Tk()
window.title('Skate 3 Rust Engine setup')
window.geometry('700x420');window.resizable(False,False)
icon=ROOT/'docs/images/skating-crab.ico'
if icon.is_file():window.iconbitmap(str(icon))
frame=ttk.Frame(window,padding=24);frame.pack(fill='both',expand=True)
ttk.Label(frame,text='Update game assets' if updating else 'Set up Skate 3 Rust Engine',font=('Segoe UI',20)).pack(anchor='w',pady=(0,16))
ttk.Label(frame,text=('Asset version changes: '+(', '.join(sorted(changed)) or 'none')+'.\nOnly changed or incomplete groups will be prepared again.\nYour previous character data remains until preparation succeeds.\n'
+('Reusing the Xbox source from the previous setup when possible.\nKeep the game data beside default.xex.' if reuse else 'Select your Skate 3 default.xex (or ISO) to continue.\nKeep the game data beside default.xex.')) if updating else 'Select your Skate 3 Xbox 360 ISO, or default.xex inside an\nextracted game folder. Keep the game data beside default.xex.\nSetup prepares the skater, customiser, animations and disc maps.\nNo other apps need installing.\n\nISO extraction needs internet access. Allow free disk space\nand time for the first conversion.',
font=('Segoe UI',11),justify='left').pack(anchor='w')
status=tk.StringVar(value=('Reusing your previous Xbox source…' if reuse else 'Choose your game to begin.') if updating else 'Choose your game to begin.')
ttk.Label(frame,textvariable=status,wraplength=600).pack(anchor='w',pady=(18,8))
progress=ttk.Progressbar(frame,mode='indeterminate');progress.pack(fill='x')
messages=queue.Queue();running=False;success=False
def begin(iso):
nonlocal running
button.config(state='disabled');running=True;progress.start()
def work():
try:
installed_root=install(Path(iso),args.base,args.game_exe,lambda text:messages.put(('progress',text)),refresh=updating)
from tools.asset_pipeline.validation_report import summary
warnings=summary(installed_root)
messages.put(('done',f'Ready with {len(warnings)} warnings or unavailable components. Details: {installed_root / "setup-report.json"}' if warnings else 'Ready'))
except Exception as error:
args.base.mkdir(parents=True,exist_ok=True)
(args.base/'setup-error.log').write_text(traceback.format_exc(),encoding='utf-8')
messages.put(('error',str(error)))
threading.Thread(target=work,daemon=True).start()
def start():
nonlocal running
if running:return
iso=filedialog.askopenfilename(parent=window,title='Select your Skate 3 default.xex or Xbox 360 ISO',
filetypes=[('Skate 3 game','default.xex *.iso'),('Skate 3 executable','default.xex'),('Xbox 360 ISO','*.iso')])
if not iso:return
begin(iso)
def close():
if running:
messagebox.showinfo('Setup running','Wait for the current conversion to finish. Your source game files are not modified.',parent=window)
else:window.destroy()
buttons=ttk.Frame(frame);buttons.pack(anchor='e',pady=18)
button=ttk.Button(buttons,text='Select ISO or default.xex',command=start);button.pack(side='left')
def poll():
nonlocal running,success
while not messages.empty():
kind,text=messages.get_nowait();status.set(text)
if kind=='done':
if text!='Ready':messagebox.showwarning('Setup completed with warnings',text,parent=window)
running=False;success=True;progress.stop();window.destroy();return
if kind=='error':
running=False;progress.stop();button.config(state='normal')
if reuse and text:
status.set('Could not reuse the previous Xbox source. Select default.xex or an ISO.')
messagebox.showerror('Setup could not finish',text+'\n\nDetails: '+str(args.base/'setup-error.log'),parent=window)
window.after(100,poll)
window.protocol('WM_DELETE_WINDOW',close)
window.after(100,poll)
# Asset refreshes reuse the recorded disc path when it still exists, so
# program updates do not force another ISO picker for matching installs.
if updating and reuse is not None:
window.after(200,lambda:begin(reuse) if not running else None)
window.mainloop()
return 0 if success else 2
if __name__=='__main__':raise SystemExit(main())