Add new modules for Crusader map rendering and processing
- Implemented `formats.py` to define data structures and functions for handling map data, including reading and decoding shape and map items. - Created `png.py` for generating PNG images from shape frames and pixel data. - Developed `sorting.py` to manage the sorting and rendering order of map items based on their properties and spatial relationships. - Introduced `render_all_maps.py` to facilitate the rendering of all maps for specified games, including command-line argument parsing and subprocess management for rendering tasks.
1
.gitignore
vendored
|
|
@ -43,3 +43,4 @@ tools/pyghidra_crusader/__pycache__/**
|
|||
bin/**
|
||||
USECODE/REGRET/REGRET_USECODE_extracted/chunks/**
|
||||
exports/**
|
||||
out/**
|
||||
|
|
|
|||
|
|
@ -4,494 +4,12 @@
|
|||
<SAVE_STATE>
|
||||
<ARRAY NAME="EXPANDED_PATHS" TYPE="string">
|
||||
<A VALUE="Crusader:" />
|
||||
<A VALUE="Crusader:es:" />
|
||||
</ARRAY>
|
||||
<STATE NAME="SHOW_TABLE" TYPE="boolean" VALUE="false" />
|
||||
</SAVE_STATE>
|
||||
</PROJECT_DATA_XML_NAME>
|
||||
<TOOL_MANAGER ACTIVE_WORKSPACE="Workspace">
|
||||
<WORKSPACE NAME="Workspace" ACTIVE="true">
|
||||
<RUNNING_TOOL TOOL_NAME="CodeBrowser">
|
||||
<ROOT_NODE X_POS="3" Y_POS="78" WIDTH="1815" HEIGHT="1110" EX_STATE="0">
|
||||
<SPLIT_NODE WIDTH="100" HEIGHT="100" DIVIDER_LOCATION="0" ORIENTATION="VERTICAL">
|
||||
<SPLIT_NODE WIDTH="1801" HEIGHT="1014" DIVIDER_LOCATION="774" ORIENTATION="HORIZONTAL">
|
||||
<SPLIT_NODE WIDTH="100" HEIGHT="100" DIVIDER_LOCATION="0" ORIENTATION="VERTICAL">
|
||||
<SPLIT_NODE WIDTH="1801" HEIGHT="1014" DIVIDER_LOCATION="880" ORIENTATION="VERTICAL">
|
||||
<SPLIT_NODE WIDTH="1801" HEIGHT="889" DIVIDER_LOCATION="863" ORIENTATION="VERTICAL">
|
||||
<SPLIT_NODE WIDTH="100" HEIGHT="100" DIVIDER_LOCATION="0" ORIENTATION="VERTICAL">
|
||||
<SPLIT_NODE WIDTH="1621" HEIGHT="816" DIVIDER_LOCATION="148" ORIENTATION="VERTICAL">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Entropy" OWNER="EntropyPlugin" TITLE="Entropy" ACTIVE="false" GROUP="Header" INSTANCE_ID="3207819926581772885" />
|
||||
<COMPONENT_INFO NAME="Overview" OWNER="OverviewPlugin" TITLE="Overview" ACTIVE="false" GROUP="Header" INSTANCE_ID="3207819926581772883" />
|
||||
</COMPONENT_NODE>
|
||||
<SPLIT_NODE WIDTH="1801" HEIGHT="764" DIVIDER_LOCATION="174" ORIENTATION="HORIZONTAL">
|
||||
<SPLIT_NODE WIDTH="313" HEIGHT="764" DIVIDER_LOCATION="640" ORIENTATION="VERTICAL">
|
||||
<SPLIT_NODE WIDTH="313" HEIGHT="486" DIVIDER_LOCATION="502" ORIENTATION="VERTICAL">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Program Tree" OWNER="ProgramTreePlugin" TITLE="Program Trees" ACTIVE="true" GROUP="Default" INSTANCE_ID="3721499797143378251" />
|
||||
</COMPONENT_NODE>
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Symbol Tree" OWNER="SymbolTreePlugin" TITLE="Symbol Tree" ACTIVE="true" GROUP="Default" INSTANCE_ID="3721499797143378246" />
|
||||
</COMPONENT_NODE>
|
||||
</SPLIT_NODE>
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="DataTypes Provider" OWNER="DataTypeManagerPlugin" TITLE="Data Type Manager" ACTIVE="true" GROUP="Default" INSTANCE_ID="3721499813509552472" />
|
||||
</COMPONENT_NODE>
|
||||
</SPLIT_NODE>
|
||||
<SPLIT_NODE WIDTH="1484" HEIGHT="764" DIVIDER_LOCATION="785" ORIENTATION="VERTICAL">
|
||||
<SPLIT_NODE WIDTH="1386" HEIGHT="638" DIVIDER_LOCATION="705" ORIENTATION="VERTICAL">
|
||||
<SPLIT_NODE WIDTH="1484" HEIGHT="597" DIVIDER_LOCATION="490" ORIENTATION="HORIZONTAL">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Listing" OWNER="CodeBrowserPlugin" TITLE="Listing: CRUSADER.EXE" ACTIVE="true" GROUP="Core" INSTANCE_ID="3721499797143378261" />
|
||||
</COMPONENT_NODE>
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Decompiler" OWNER="DecompilePlugin" TITLE="Decompile: Key_HandleOptionKeys" ACTIVE="true" GROUP="Default" INSTANCE_ID="3721499797143378252" />
|
||||
<COMPONENT_INFO NAME="Bytes" OWNER="ByteViewerPlugin" TITLE="Bytes: CRUSADER.EXE" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499797143378254" />
|
||||
<COMPONENT_INFO NAME="Data Window" OWNER="DataWindowPlugin" TITLE="Defined Data" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499813509552477" />
|
||||
<COMPONENT_INFO NAME="Defined Strings" OWNER="ViewStringsPlugin" TITLE="Defined Strings" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499813597632833" />
|
||||
<COMPONENT_INFO NAME="Equates Table" OWNER="EquateTablePlugin" TITLE="Equates Table" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499797143378258" />
|
||||
<COMPONENT_INFO NAME="External Programs" OWNER="ReferencesPlugin" TITLE="External Programs" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499797143378262" />
|
||||
<COMPONENT_INFO NAME="Functions Window" OWNER="FunctionWindowPlugin" TITLE="Functions" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499797143378265" />
|
||||
<COMPONENT_INFO NAME="Relocation Table" OWNER="RelocationTablePlugin" TITLE="Relocation Table" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499813597632832" />
|
||||
</COMPONENT_NODE>
|
||||
</SPLIT_NODE>
|
||||
<SPLIT_NODE WIDTH="1386" HEIGHT="189" DIVIDER_LOCATION="495" ORIENTATION="HORIZONTAL">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Data Type Preview" OWNER="DataTypePreviewPlugin" TITLE="Data Type Preview" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499813509552470" />
|
||||
</COMPONENT_NODE>
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Virtual Disassembler - Current Instruction" OWNER="DisassembledViewPlugin" TITLE="Disassembled View" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499797143378257" />
|
||||
</COMPONENT_NODE>
|
||||
</SPLIT_NODE>
|
||||
</SPLIT_NODE>
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Console" OWNER="ConsolePlugin" TITLE="Console" ACTIVE="true" GROUP="Default" INSTANCE_ID="3721499797143378253" />
|
||||
<COMPONENT_INFO NAME="Bookmarks" OWNER="BookmarkPlugin" TITLE="Bookmarks" ACTIVE="false" GROUP="Core.Bookmarks" INSTANCE_ID="3721499797143378250" />
|
||||
</COMPONENT_NODE>
|
||||
</SPLIT_NODE>
|
||||
</SPLIT_NODE>
|
||||
</SPLIT_NODE>
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Function Call Trees" OWNER="CallTreePlugin" TITLE="Function Call Trees" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499797143378247" />
|
||||
</COMPONENT_NODE>
|
||||
</SPLIT_NODE>
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Domain Events" OWNER="DomainEventDisplayPlugin" TITLE="Domain Object Event Display" ACTIVE="true" GROUP="Default" INSTANCE_ID="3721499797143378259" />
|
||||
</COMPONENT_NODE>
|
||||
</SPLIT_NODE>
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Plugin Event Display" OWNER="EventDisplayPlugin" TITLE="Plugin Event Display" ACTIVE="true" GROUP="Default" INSTANCE_ID="3721499797143378256" />
|
||||
</COMPONENT_NODE>
|
||||
</SPLIT_NODE>
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Database Viewer" OWNER="DbViewerPlugin" TITLE="Database Viewer" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499814153378120" />
|
||||
</COMPONENT_NODE>
|
||||
</SPLIT_NODE>
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Decompiler" OWNER="DecompilePlugin" TITLE="[Decompile: FUN_0000_2b36]" ACTIVE="false" GROUP="disconnected" INSTANCE_ID="3720776277651544989" />
|
||||
</COMPONENT_NODE>
|
||||
</SPLIT_NODE>
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Diff Location Details" OWNER="ProgramDiffPlugin" TITLE="Diff Details" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721161170088475892" />
|
||||
</COMPONENT_NODE>
|
||||
</SPLIT_NODE>
|
||||
<WINDOW_NODE X_POS="426" Y_POS="178" WIDTH="1033" HEIGHT="689">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Script Manager" OWNER="GhidraScriptMgrPlugin" TITLE="Script Manager" ACTIVE="false" GROUP="Script Group" INSTANCE_ID="3721499797143378248" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="423" Y_POS="144" WIDTH="927" HEIGHT="695">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Memory Map" OWNER="MemoryMapPlugin" TITLE="Memory Map" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499797143378243" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="383" Y_POS="7" WIDTH="1020" HEIGHT="1038">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Function Graph" OWNER="FunctionGraphPlugin" TITLE="Function Graph" ACTIVE="false" GROUP="Function Graph" INSTANCE_ID="3721499813597632834" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="550" Y_POS="206" WIDTH="655" HEIGHT="509">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Register Manager" OWNER="RegisterPlugin" TITLE="Register Manager" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499797143378264" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="287" Y_POS="186" WIDTH="1424" HEIGHT="666">
|
||||
<SPLIT_NODE WIDTH="1408" HEIGHT="559" DIVIDER_LOCATION="573" ORIENTATION="HORIZONTAL">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Symbol Table" OWNER="SymbolTablePlugin" TITLE="Symbol Table" ACTIVE="false" GROUP="symbolTable" INSTANCE_ID="3721499813509552478" />
|
||||
</COMPONENT_NODE>
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Symbol References" OWNER="SymbolTablePlugin" TITLE="Symbol References" ACTIVE="false" GROUP="symbolTable" INSTANCE_ID="3721499813509552479" />
|
||||
</COMPONENT_NODE>
|
||||
</SPLIT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="-1" Y_POS="-1" WIDTH="0" HEIGHT="0">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Checksum Generator" OWNER="ComputeChecksumsPlugin" TITLE="Checksum Generator" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499797143378260" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="-1" Y_POS="-1" WIDTH="0" HEIGHT="0">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Function Tags" OWNER="FunctionTagPlugin" TITLE="Function Tags" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499813509552473" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="-1" Y_POS="-1" WIDTH="0" HEIGHT="0">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Comment Window" OWNER="CommentWindowPlugin" TITLE="Comments" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499813509552476" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="-1" Y_POS="-1" WIDTH="0" HEIGHT="0">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Python" OWNER="InterpreterPanelPlugin" TITLE="Python" ACTIVE="false" GROUP="Default" INSTANCE_ID="3207819978370941531" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="0" Y_POS="0" WIDTH="0" HEIGHT="0">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Jython" OWNER="Jython" TITLE="Jython" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499813509552474" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="0" Y_POS="0" WIDTH="0" HEIGHT="0">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Bundle Manager" OWNER="GhidraScriptMgrPlugin" TITLE="Bundle Manager" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499797143378249" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="0" Y_POS="0" WIDTH="0" HEIGHT="0">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="PyGhidra" OWNER="PyGhidra" TITLE="PyGhidra" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499813509552475" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="0" Y_POS="0" WIDTH="0" HEIGHT="0">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Source Files and Transforms" OWNER="SourceFilesTablePlugin" TITLE="Source Files and Transforms" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499797143378263" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="0" Y_POS="0" WIDTH="0" HEIGHT="0">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Function Call Graph" OWNER="FunctionCallGraphPlugin" TITLE="Function Call Graph" ACTIVE="false" GROUP="Function Call Graph" INSTANCE_ID="3721499813509552471" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="588" Y_POS="50" WIDTH="1018" HEIGHT="1087">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Graph" OWNER="DefaultGraphDisplay" TITLE="AST Data Flow Graph For entity_state_tick_dispatch" ACTIVE="false" GROUP="ProgramGraph" INSTANCE_ID="3720233517670421199" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="0" Y_POS="0" WIDTH="0" HEIGHT="0">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Window Locations" OWNER="WindowLocationPlugin" TITLE="Window Locations" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721499797143378244" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
<WINDOW_NODE X_POS="890" Y_POS="456" WIDTH="729" HEIGHT="566">
|
||||
<COMPONENT_NODE TOP_INFO="0">
|
||||
<COMPONENT_INFO NAME="Location References Provider" OWNER="LocationReferencesPlugin" TITLE="References to g_jassica16Offset - 7 locations" ACTIVE="false" GROUP="Default" INSTANCE_ID="3721161260863700211" />
|
||||
</COMPONENT_NODE>
|
||||
</WINDOW_NODE>
|
||||
</ROOT_NODE>
|
||||
<DATA_STATE>
|
||||
<PLUGIN NAME="NavigationHistoryPlugin">
|
||||
<XML NAME="HISTORY_LIST_0">
|
||||
<SAVE_STATE>
|
||||
<STATE NAME="CURRENT_LOC_INDEX" TYPE="int" VALUE="3" />
|
||||
<STATE NAME="LOCATION_COUNT" TYPE="int" VALUE="4" />
|
||||
<STATE NAME="MEMENTO_CLASS0" TYPE="string" VALUE="ghidra.app.plugin.core.gotoquery.DefaultNavigatableLocationMemento" />
|
||||
<STATE NAME="MEMENTO_CLASS1" TYPE="string" VALUE="ghidra.app.plugin.core.gotoquery.DefaultNavigatableLocationMemento" />
|
||||
<STATE NAME="MEMENTO_CLASS2" TYPE="string" VALUE="ghidra.app.plugin.core.gotoquery.DefaultNavigatableLocationMemento" />
|
||||
<STATE NAME="MEMENTO_CLASS3" TYPE="string" VALUE="ghidra.app.plugin.core.gotoquery.DefaultNavigatableLocationMemento" />
|
||||
<XML NAME="MEMENTO_DATA0">
|
||||
<SAVE_STATE>
|
||||
<XML NAME="MEMENTO0">
|
||||
<SAVE_STATE>
|
||||
<STATE NAME="CURSOR_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="MEMENTO_CLASS" TYPE="string" VALUE="ghidra.app.plugin.core.codebrowser.CodeViewerLocationMemento" />
|
||||
<STATE NAME="NAV_ID" TYPE="long" VALUE="3721499797143378261" />
|
||||
<STATE NAME="PROGRAM_ID" TYPE="long" VALUE="3721129048497902283" />
|
||||
<STATE NAME="PROGRAM_PATH_" TYPE="string" VALUE="Crusader:/CRUSADER.EXE" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1000:0000" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1000:0000" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.program.util.MemoryBlockStartFieldLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<ARRAY NAME="_COMMENT" TYPE="string">
|
||||
<A VALUE="//" />
|
||||
<A VALUE="// Code1" />
|
||||
<A VALUE="// ram:1000:0000-ram:1000:83ff" />
|
||||
<A VALUE="//" />
|
||||
</ARRAY>
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_TYPE" TYPE="int" VALUE="-1" />
|
||||
</SAVE_STATE>
|
||||
</XML>
|
||||
<STATE NAME="MEMENTO_CLASS" TYPE="string" VALUE="ghidra.app.plugin.core.gotoquery.DefaultNavigatableLocationMemento" />
|
||||
<STATE NAME="NUM_MEMENTOS" TYPE="int" VALUE="1" />
|
||||
<STATE NAME="PROGRAM_ID" TYPE="long" VALUE="3721129048497902283" />
|
||||
<STATE NAME="PROGRAM_PATH_" TYPE="string" VALUE="Crusader:/CRUSADER.EXE" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1000:0000" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1000:0000" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.program.util.MemoryBlockStartFieldLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<ARRAY NAME="_COMMENT" TYPE="string">
|
||||
<A VALUE="//" />
|
||||
<A VALUE="// Code1" />
|
||||
<A VALUE="// ram:1000:0000-ram:1000:83ff" />
|
||||
<A VALUE="//" />
|
||||
</ARRAY>
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_TYPE" TYPE="int" VALUE="-1" />
|
||||
</SAVE_STATE>
|
||||
</XML>
|
||||
<XML NAME="MEMENTO_DATA1">
|
||||
<SAVE_STATE>
|
||||
<XML NAME="MEMENTO0">
|
||||
<SAVE_STATE>
|
||||
<STATE NAME="CURSOR_OFFSET" TYPE="int" VALUE="283" />
|
||||
<STATE NAME="MEMENTO_CLASS" TYPE="string" VALUE="ghidra.app.plugin.core.codebrowser.CodeViewerLocationMemento" />
|
||||
<STATE NAME="NAV_ID" TYPE="long" VALUE="3721499797143378261" />
|
||||
<STATE NAME="PROGRAM_ID" TYPE="long" VALUE="3721129048497902283" />
|
||||
<STATE NAME="PROGRAM_PATH_" TYPE="string" VALUE="Crusader:/CRUSADER.EXE" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.program.util.FunctionReturnTypeFieldLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_FUNC_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_RETURN_TYPE" TYPE="string" VALUE="byte" />
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_SIGNATURE" TYPE="string" VALUE="byte __cdecl16far Key_HandleOptionKeys(struct KeyEvent * keyevent)" />
|
||||
</SAVE_STATE>
|
||||
</XML>
|
||||
<STATE NAME="MEMENTO_CLASS" TYPE="string" VALUE="ghidra.app.plugin.core.gotoquery.DefaultNavigatableLocationMemento" />
|
||||
<STATE NAME="NUM_MEMENTOS" TYPE="int" VALUE="1" />
|
||||
<STATE NAME="PROGRAM_ID" TYPE="long" VALUE="3721129048497902283" />
|
||||
<STATE NAME="PROGRAM_PATH_" TYPE="string" VALUE="Crusader:/CRUSADER.EXE" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.program.util.FunctionReturnTypeFieldLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_FUNC_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_RETURN_TYPE" TYPE="string" VALUE="byte" />
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_SIGNATURE" TYPE="string" VALUE="byte Key_HandleOptionKeys(struct KeyEvent * keyevent)" />
|
||||
</SAVE_STATE>
|
||||
</XML>
|
||||
<XML NAME="MEMENTO_DATA2">
|
||||
<SAVE_STATE>
|
||||
<STATE NAME="FOCUSED_NAV" TYPE="long" VALUE="3721499797143378252" />
|
||||
<XML NAME="MEMENTO0">
|
||||
<SAVE_STATE>
|
||||
<STATE NAME="CURSOR_OFFSET" TYPE="int" VALUE="278" />
|
||||
<STATE NAME="MEMENTO_CLASS" TYPE="string" VALUE="ghidra.app.plugin.core.codebrowser.CodeViewerLocationMemento" />
|
||||
<STATE NAME="NAV_ID" TYPE="long" VALUE="3721499797143378261" />
|
||||
<STATE NAME="PROGRAM_ID" TYPE="long" VALUE="3721129048497902283" />
|
||||
<STATE NAME="PROGRAM_PATH_" TYPE="string" VALUE="Crusader:/CRUSADER.EXE" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="18" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.program.util.FunctionNameFieldLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_FUNCTION_NAME" TYPE="string" VALUE="Key_HandleOptionKeys" />
|
||||
<STATE NAME="_FUNC_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_SIGNATURE" TYPE="string" VALUE="byte __cdecl16far Key_HandleOptionKeys(struct KeyEvent * keyevent)" />
|
||||
</SAVE_STATE>
|
||||
</XML>
|
||||
<XML NAME="MEMENTO1">
|
||||
<SAVE_STATE>
|
||||
<STATE NAME="INDEX" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="MEMENTO_CLASS" TYPE="string" VALUE="ghidra.app.plugin.core.decompile.DecompilerLocationMemento" />
|
||||
<STATE NAME="NAV_ID" TYPE="long" VALUE="3721499797143378252" />
|
||||
<STATE NAME="PROGRAM_ID" TYPE="long" VALUE="3721129048497902283" />
|
||||
<STATE NAME="PROGRAM_PATH_" TYPE="string" VALUE="Crusader:/CRUSADER.EXE" />
|
||||
<STATE NAME="X_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="Y_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_CHAR_POS" TYPE="int" VALUE="30" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.app.decompiler.location.FunctionNameDecompilerLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_FUNCTION_ENTRY" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_FUNCTION_NAME" TYPE="string" VALUE="Key_HandleOptionKeys" />
|
||||
<STATE NAME="_FUNC_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_LINE_NUM" TYPE="int" VALUE="3" />
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_SIGNATURE" TYPE="string" VALUE="" />
|
||||
<STATE NAME="_TOKEN_TEXT" TYPE="string" VALUE="Key_HandleOptionKeys" />
|
||||
</SAVE_STATE>
|
||||
</XML>
|
||||
<STATE NAME="MEMENTO_CLASS" TYPE="string" VALUE="ghidra.app.plugin.core.gotoquery.DefaultNavigatableLocationMemento" />
|
||||
<STATE NAME="NUM_MEMENTOS" TYPE="int" VALUE="2" />
|
||||
<STATE NAME="PROGRAM_ID" TYPE="long" VALUE="3721129048497902283" />
|
||||
<STATE NAME="PROGRAM_PATH_" TYPE="string" VALUE="Crusader:/CRUSADER.EXE" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_CHAR_POS" TYPE="int" VALUE="30" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.app.decompiler.location.FunctionNameDecompilerLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_FUNCTION_ENTRY" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_FUNCTION_NAME" TYPE="string" VALUE="Key_HandleOptionKeys" />
|
||||
<STATE NAME="_FUNC_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_LINE_NUM" TYPE="int" VALUE="3" />
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_SIGNATURE" TYPE="string" VALUE="" />
|
||||
<STATE NAME="_TOKEN_TEXT" TYPE="string" VALUE="Key_HandleOptionKeys" />
|
||||
</SAVE_STATE>
|
||||
</XML>
|
||||
<XML NAME="MEMENTO_DATA3">
|
||||
<SAVE_STATE>
|
||||
<STATE NAME="FOCUSED_NAV" TYPE="long" VALUE="3721499797143378252" />
|
||||
<XML NAME="MEMENTO0">
|
||||
<SAVE_STATE>
|
||||
<STATE NAME="CURSOR_OFFSET" TYPE="int" VALUE="278" />
|
||||
<STATE NAME="MEMENTO_CLASS" TYPE="string" VALUE="ghidra.app.plugin.core.codebrowser.CodeViewerLocationMemento" />
|
||||
<STATE NAME="NAV_ID" TYPE="long" VALUE="3721499797143378261" />
|
||||
<STATE NAME="PROGRAM_ID" TYPE="long" VALUE="3721129048497902283" />
|
||||
<STATE NAME="PROGRAM_PATH_" TYPE="string" VALUE="Crusader:/CRUSADER.EXE" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_ADDR_REP" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.program.util.AddressFieldLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
</SAVE_STATE>
|
||||
</XML>
|
||||
<XML NAME="MEMENTO1">
|
||||
<SAVE_STATE>
|
||||
<STATE NAME="INDEX" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="MEMENTO_CLASS" TYPE="string" VALUE="ghidra.app.plugin.core.decompile.DecompilerLocationMemento" />
|
||||
<STATE NAME="NAV_ID" TYPE="long" VALUE="3721499797143378252" />
|
||||
<STATE NAME="PROGRAM_ID" TYPE="long" VALUE="3721129048497902283" />
|
||||
<STATE NAME="PROGRAM_PATH_" TYPE="string" VALUE="Crusader:/CRUSADER.EXE" />
|
||||
<STATE NAME="X_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="Y_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_CHAR_POS" TYPE="int" VALUE="30" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.app.decompiler.location.FunctionNameDecompilerLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_FUNCTION_ENTRY" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_FUNCTION_NAME" TYPE="string" VALUE="Key_HandleOptionKeys" />
|
||||
<STATE NAME="_FUNC_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_LINE_NUM" TYPE="int" VALUE="3" />
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_SIGNATURE" TYPE="string" VALUE="" />
|
||||
<STATE NAME="_TOKEN_TEXT" TYPE="string" VALUE="Key_HandleOptionKeys" />
|
||||
</SAVE_STATE>
|
||||
</XML>
|
||||
<STATE NAME="MEMENTO_CLASS" TYPE="string" VALUE="ghidra.app.plugin.core.gotoquery.DefaultNavigatableLocationMemento" />
|
||||
<STATE NAME="NUM_MEMENTOS" TYPE="int" VALUE="2" />
|
||||
<STATE NAME="PROGRAM_ID" TYPE="long" VALUE="3721129048497902283" />
|
||||
<STATE NAME="PROGRAM_PATH_" TYPE="string" VALUE="Crusader:/CRUSADER.EXE" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.program.util.ProgramLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
</SAVE_STATE>
|
||||
</XML>
|
||||
<STATE NAME="NAV_ID" TYPE="long" VALUE="-1" />
|
||||
</SAVE_STATE>
|
||||
</XML>
|
||||
<STATE NAME="LIST_COUNT" TYPE="int" VALUE="1" />
|
||||
</PLUGIN>
|
||||
<PLUGIN NAME="ProgramTreePlugin">
|
||||
<STATE NAME="Current Viewname" TYPE="string" VALUE="Program Tree" />
|
||||
<ARRAY NAME="GroupNameProgram Tree0" TYPE="string">
|
||||
<A VALUE="CRUSADER.EXE" />
|
||||
</ARRAY>
|
||||
<STATE NAME="NavigationToggleState" TYPE="boolean" VALUE="false" />
|
||||
<STATE NAME="NumberOfGroupsProgram Tree" TYPE="int" VALUE="1" />
|
||||
<STATE NAME="NumberOfViews" TYPE="int" VALUE="1" />
|
||||
<STATE NAME="TreeName-0" TYPE="string" VALUE="Program Tree" />
|
||||
</PLUGIN>
|
||||
<PLUGIN NAME="DecompilePlugin">
|
||||
<STATE NAME="INDEX" TYPE="int" VALUE="115" />
|
||||
<STATE NAME="NAV_ID" TYPE="long" VALUE="3721499797143378252" />
|
||||
<STATE NAME="Num Disconnected" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="Y_OFFSET" TYPE="int" VALUE="-13" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_CHAR_POS" TYPE="int" VALUE="26" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.app.decompiler.location.FunctionNameDecompilerLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_FUNCTION_ENTRY" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_FUNCTION_NAME" TYPE="string" VALUE="Key_HandleOptionKeys" />
|
||||
<STATE NAME="_FUNC_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_LINE_NUM" TYPE="int" VALUE="3" />
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_SIGNATURE" TYPE="string" VALUE="" />
|
||||
<STATE NAME="_TOKEN_TEXT" TYPE="string" VALUE="Key_HandleOptionKeys" />
|
||||
</PLUGIN>
|
||||
<PLUGIN NAME="ByteViewerPlugin">
|
||||
<STATE NAME="Block Column" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="Block Num" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="Block Offset" TYPE="string" VALUE="0" />
|
||||
<STATE NAME="Index" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="NAV_ID" TYPE="long" VALUE="3721499797143378254" />
|
||||
<STATE NAME="Num Disconnected" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="X Offset" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="Y Offset" TYPE="int" VALUE="0" />
|
||||
</PLUGIN>
|
||||
<PLUGIN NAME="ProgramManagerPlugin">
|
||||
<STATE NAME="CURRENT_FILE" TYPE="string" VALUE="CRUSADER.EXE" />
|
||||
<STATE NAME="LOCATION_0" TYPE="string" VALUE="/K:/ghidra/Crusader_Decomp/" />
|
||||
<STATE NAME="NUM_PROGRAMS" TYPE="int" VALUE="1" />
|
||||
<STATE NAME="PATHNAME_0" TYPE="string" VALUE="/CRUSADER.EXE" />
|
||||
<STATE NAME="PROJECT_NAME_0" TYPE="string" VALUE="Crusader" />
|
||||
<STATE NAME="VERSION_0" TYPE="int" VALUE="-1" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_CHAR_POS" TYPE="int" VALUE="26" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.app.decompiler.location.FunctionNameDecompilerLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_FUNCTION_ENTRY" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_FUNCTION_NAME" TYPE="string" VALUE="Key_HandleOptionKeys" />
|
||||
<STATE NAME="_FUNC_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_LINE_NUM" TYPE="int" VALUE="3" />
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_SIGNATURE" TYPE="string" VALUE="" />
|
||||
<STATE NAME="_TOKEN_TEXT" TYPE="string" VALUE="Key_HandleOptionKeys" />
|
||||
</PLUGIN>
|
||||
<PLUGIN NAME="FunctionGraphPlugin">
|
||||
<SAVE_STATE NAME="COMPLEX_LAYOUT_NAME" TYPE="SaveState">
|
||||
<COMPLEX_LAYOUT_NAME>
|
||||
<STATE NAME="LAYOUT_CLASS_NAME" TYPE="string" VALUE="ghidra.app.plugin.core.functiongraph.graph.layout.DecompilerNestedLayoutProvider" />
|
||||
<STATE NAME="LAYOUT_NAME" TYPE="string" VALUE="Nested Code Layout" />
|
||||
</COMPLEX_LAYOUT_NAME>
|
||||
</SAVE_STATE>
|
||||
<STATE NAME="DISPLAY_POPUPS" TYPE="boolean" VALUE="true" />
|
||||
<STATE NAME="DISPLAY_SATELLITE" TYPE="boolean" VALUE="true" />
|
||||
<STATE NAME="DOCK_SATELLITE" TYPE="boolean" VALUE="true" />
|
||||
<STATE NAME="DOCK_SATELLITE_POSITION" TYPE="string" VALUE="LOWER_RIGHT" />
|
||||
<STATE NAME="Disconnected Count" TYPE="int" VALUE="0" />
|
||||
<ENUM NAME="EDGE_HOVER_HIGHLIGHT" TYPE="enum" CLASS="ghidra.app.plugin.core.functiongraph.EdgeDisplayType" VALUE="ScopedFlowsFromVertex" />
|
||||
<ENUM NAME="EDGE_SELECTION_HIGHLIGHT" TYPE="enum" CLASS="ghidra.app.plugin.core.functiongraph.EdgeDisplayType" VALUE="AllCycles" />
|
||||
<STATE NAME="NAV_ID" TYPE="long" VALUE="3721499813597632834" />
|
||||
</PLUGIN>
|
||||
<PLUGIN NAME="CodeBrowserPlugin">
|
||||
<STATE NAME="INDEX" TYPE="int" VALUE="190205" />
|
||||
<STATE NAME="NAV_ID" TYPE="long" VALUE="3721499797143378261" />
|
||||
<STATE NAME="Num Disconnected" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="Y_OFFSET" TYPE="int" VALUE="-28" />
|
||||
<STATE NAME="_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_BYTE_ADDR" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_CHAR_OFFSET" TYPE="int" VALUE="18" />
|
||||
<STATE NAME="_CLASSNAME" TYPE="string" VALUE="ghidra.program.util.FunctionNameFieldLocation" />
|
||||
<STATE NAME="_COLUMN" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_FUNCTION_NAME" TYPE="string" VALUE="Key_HandleOptionKeys" />
|
||||
<STATE NAME="_FUNC_ADDRESS" TYPE="string" VALUE="1130:0896" />
|
||||
<STATE NAME="_ROW" TYPE="int" VALUE="0" />
|
||||
<STATE NAME="_SIGNATURE" TYPE="string" VALUE="byte __cdecl16far Key_HandleOptionKeys(struct KeyEvent * keyevent)" />
|
||||
</PLUGIN>
|
||||
</DATA_STATE>
|
||||
</RUNNING_TOOL>
|
||||
</WORKSPACE>
|
||||
<WORKSPACE NAME="Workspace" ACTIVE="true" />
|
||||
</TOOL_MANAGER>
|
||||
</PROJECT>
|
||||
|
||||
|
|
|
|||
BIN
STATIC_REGRET/ANIM.DAT
Normal file
BIN
STATIC_REGRET/COMBAT.DAT
Normal file
BIN
STATIC_REGRET/CRED.DAT
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
STATIC_REGRET/CRED.PAL
Normal file
BIN
STATIC_REGRET/CREDITS.DAT
Normal file
BIN
STATIC_REGRET/DAMAGE.FLX
Normal file
BIN
STATIC_REGRET/DIFF.PAL
Normal file
BIN
STATIC_REGRET/DTABLE.FLX
Normal file
BIN
STATIC_REGRET/FIXED.DAT
Normal file
BIN
STATIC_REGRET/FONTS.DAT
Normal file
BIN
STATIC_REGRET/FONTS.FLX
Normal file
BIN
STATIC_REGRET/GAMEPAL.PAL
Normal file
BIN
STATIC_REGRET/GLOB.FLX
Normal file
BIN
STATIC_REGRET/GUMPS.FLX
Normal file
BIN
STATIC_REGRET/HELP1.BMP
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
STATIC_REGRET/HELP2.BMP
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
STATIC_REGRET/HELP3.BMP
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
STATIC_REGRET/HELP4.BMP
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
STATIC_REGRET/HELP5.BMP
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
STATIC_REGRET/ICONS.DAT
Normal file
BIN
STATIC_REGRET/LOAD.BMP
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
STATIC_REGRET/MISC.PAL
Normal file
BIN
STATIC_REGRET/MISC2.PAL
Normal file
BIN
STATIC_REGRET/MOUSE.SHP
Normal file
BIN
STATIC_REGRET/MUSIC.AMF
Normal file
BIN
STATIC_REGRET/PALETTE.DAT
Normal file
BIN
STATIC_REGRET/SAVE.BMP
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
STATIC_REGRET/SHAPES.FLX
Normal file
BIN
STATIC_REGRET/STAR.PAL
Normal file
BIN
STATIC_REGRET/STUFF.DAT
Normal file
BIN
STATIC_REGRET/TRIG.DAT
Normal file
BIN
STATIC_REGRET/TYPEFLAG.DAT
Normal file
BIN
STATIC_REGRET/WPNOVLAY.DAT
Normal file
BIN
STATIC_REGRET/XFORMPAL.DAT
Normal file
|
|
@ -11,96 +11,59 @@ function bart_enterFastArea() /* entry=117 class_id=0x01F5 slot=0x0F */
|
|||
process_exclude();
|
||||
|
||||
block_01E2:
|
||||
while (true) {
|
||||
suspend;
|
||||
FREE.slot_20(100);
|
||||
if (retval > 50) goto block_0318;
|
||||
|
||||
block_0205:
|
||||
if (retval <= 50) {
|
||||
FREE.slot_20(pid, 120);
|
||||
spawn FREE.waitNTimerTicks((retval + 60), 0x00000000);
|
||||
suspend;
|
||||
FREE.slot_20(5);
|
||||
rndNum = (retval + 4);
|
||||
counter = 0;
|
||||
|
||||
block_025C:
|
||||
if (counter <= rndNum) goto block_0315;
|
||||
|
||||
block_0267:
|
||||
while (counter > rndNum) {
|
||||
counter2 = 1;
|
||||
|
||||
block_026E:
|
||||
if (counter2 <= 7) goto block_02B6;
|
||||
|
||||
block_0276:
|
||||
while (counter2 > 7) {
|
||||
spawn FREE.waitNTimerTicks(pid, 10, 0x00000000);
|
||||
suspend;
|
||||
counter2 = (1 + counter2);
|
||||
goto block_026E;
|
||||
|
||||
block_02B6:
|
||||
}
|
||||
counter2 = 1;
|
||||
|
||||
block_02BD:
|
||||
if (counter2 <= 7) goto block_0308;
|
||||
|
||||
block_02C5:
|
||||
while (counter2 > 7) {
|
||||
spawn FREE.waitNTimerTicks(pid, 10, 0x00000000);
|
||||
suspend;
|
||||
counter2 = (1 + counter2);
|
||||
goto block_02BD;
|
||||
|
||||
block_0308:
|
||||
}
|
||||
counter = (1 + counter);
|
||||
goto block_025C;
|
||||
|
||||
block_0315:
|
||||
goto block_046D;
|
||||
|
||||
block_0318:
|
||||
}
|
||||
}
|
||||
else {
|
||||
counter = 1;
|
||||
|
||||
block_031F:
|
||||
if (counter <= 16) goto block_0367;
|
||||
|
||||
block_0327:
|
||||
while (counter > 16) {
|
||||
spawn FREE.waitNTimerTicks(pid, 10, 0x00000000);
|
||||
suspend;
|
||||
counter = (1 + counter);
|
||||
goto block_031F;
|
||||
|
||||
block_0367:
|
||||
}
|
||||
FREE.slot_20(pid, 60);
|
||||
spawn FREE.waitNTimerTicks((retval + 60), 0x00000000);
|
||||
suspend;
|
||||
counter = 0;
|
||||
|
||||
block_039F:
|
||||
if (counter <= 3) goto block_03EA;
|
||||
|
||||
block_03A7:
|
||||
while (counter > 3) {
|
||||
spawn FREE.waitNTimerTicks(pid, 10, 0x00000000);
|
||||
suspend;
|
||||
counter = (1 + counter);
|
||||
goto block_039F;
|
||||
|
||||
block_03EA:
|
||||
}
|
||||
FREE.slot_20(pid, 120);
|
||||
spawn FREE.waitNTimerTicks((retval + 60), 0x00000000);
|
||||
suspend;
|
||||
counter = 0;
|
||||
|
||||
block_0422:
|
||||
if (counter <= 14) goto block_046D;
|
||||
|
||||
block_042A:
|
||||
while (counter > 14) {
|
||||
spawn FREE.waitNTimerTicks(pid, 10, 0x00000000);
|
||||
suspend;
|
||||
counter = (1 + counter);
|
||||
goto block_0422;
|
||||
|
||||
block_046D:
|
||||
goto block_01E2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
block_0470:
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,22 @@ This note starts a dedicated lane for offline Crusader map extraction and PNG re
|
|||
Current implementation entry point:
|
||||
|
||||
- `tools/render_crusader_map.py`
|
||||
- `render_maps.bat` for whole-game batch runs into `out/remorse` and `out/regret`
|
||||
|
||||
Current renderer diagnostics:
|
||||
|
||||
- large maps now emit progress checkpoints during item collection, dependency sorting, paint-order resolution, and blitting
|
||||
- `collect_render_items()` only expands `FIXED.DAT` glob eggs once, instead of recursively re-expanding glob-emitted glob eggs
|
||||
- metadata now records sampled invalid shape/frame references plus a conservative map-usage hint block
|
||||
- roofs/exploration obscurers are now optional and disabled by default
|
||||
- editor/debug/marker-style map content is now enabled by default instead of being silently discarded
|
||||
|
||||
Internal package layout:
|
||||
|
||||
- `tools/crusader_map/formats.py` for Crusader archive and record parsing
|
||||
- `tools/crusader_map/sorting.py` for the dependency-graph overlap sorter
|
||||
- `tools/crusader_map/png.py` for PNG buffer/blit helpers
|
||||
- `tools/crusader_map/cli.py` for command-line orchestration
|
||||
|
||||
Current supported data roots:
|
||||
|
||||
|
|
@ -146,6 +162,11 @@ This is enough for:
|
|||
- expanding `SF_GLOBEGG`
|
||||
- documenting future work toward a better sorter
|
||||
|
||||
Current offline rendering policy differs from the live game intentionally:
|
||||
|
||||
- `SI_ROOF` shapes are hidden by default because they commonly act as exploration obscurers or roof covers that gameplay later removes or pops
|
||||
- editor/debug/marker-style content is shown by default so offline renders expose more of what the shipped data actually contains
|
||||
|
||||
The current tool does not yet use the footpad values for full ItemSorter-equivalent overlap resolution.
|
||||
|
||||
## Current Projection And Painting Rules
|
||||
|
|
@ -197,6 +218,58 @@ Render a bounded world-space region only:
|
|||
c:/Users/Maddo/.PYENV/PYENV-WIN/versions/3.14.3/python.exe tools/render_crusader_map.py --game remorse --map 0 --world-rect 0 0 4096 4096 --output out/map0-quarter.png
|
||||
```
|
||||
|
||||
Render with roofs restored:
|
||||
|
||||
```powershell
|
||||
c:/Users/Maddo/.PYENV/PYENV-WIN/versions/3.14.3/python.exe tools/render_crusader_map.py --game remorse --map 1 --include-roofs --output out/map1-with-roofs.png
|
||||
```
|
||||
|
||||
Render without the extra hidden/editor marker content:
|
||||
|
||||
```powershell
|
||||
c:/Users/Maddo/.PYENV/PYENV-WIN/versions/3.14.3/python.exe tools/render_crusader_map.py --game remorse --map 1 --no-include-hidden-markers --no-include-editor --output out/map1-minimal.png
|
||||
```
|
||||
|
||||
Render every No Remorse map to `out/remorse`:
|
||||
|
||||
```cmd
|
||||
render_maps.bat remorse
|
||||
```
|
||||
|
||||
Render every No Regret map to `out/regret`:
|
||||
|
||||
```cmd
|
||||
render_maps.bat regret
|
||||
```
|
||||
|
||||
The batch runner also accepts an optional `start_map end_map` range for partial runs while validating changes:
|
||||
|
||||
```cmd
|
||||
render_maps.bat remorse 1 3
|
||||
```
|
||||
|
||||
You can also forward extra renderer arguments through the `RENDER_ARGS` environment variable, for example a bounded validation run:
|
||||
|
||||
```cmd
|
||||
set RENDER_ARGS=--world-rect 0 0 16384 16384
|
||||
render_maps.bat regret 5 5
|
||||
```
|
||||
|
||||
Batch behavior notes:
|
||||
|
||||
- empty maps are skipped in batch mode and do not produce PNG or JSON outputs
|
||||
- there is no default max-pixel cap anymore; full-map renders are attempted unless you pass `--max-pixels`
|
||||
- batch item-count skipping is now opt-in only; set `BATCH_MAX_ITEMS` to a positive value if you want the batch runner to skip very large full maps
|
||||
- the renderer emits progress by default every 2000 items; pass `--progress-every 0` through `RENDER_ARGS` to silence it
|
||||
- batch runs now default to `include_editor=true`, `include_hidden_markers=true`, and `include_roofs=false`
|
||||
|
||||
Metadata notes:
|
||||
|
||||
- `invalid_items` contains a capped sample of bad `(shape, frame, x, y, z, source, reason)` records so broken `FIXED.DAT` references can be inspected without rerunning a scan
|
||||
- `usage` is conservative: it reports known reference-backed map hints when available and otherwise stays `unknown`; it does not yet prove orphan status
|
||||
- `base_item_summary` reports how many roof, editor, egg-family, invisible, and NPC-linked records were present in the raw map payload
|
||||
- `filters` records whether the render included roofs, editor shapes, and hidden marker content
|
||||
|
||||
## Current Deliberate Limits
|
||||
|
||||
This tool is a start, not a complete engine clone.
|
||||
|
|
@ -210,6 +283,7 @@ Current gaps:
|
|||
5. It does not yet consume `ANIM.DAT`, `DAMAGE.FLX`, `DTABLE.FLX`, `WPNOVLAY.DAT`, or palette transforms such as `XFORMPAL.DAT`.
|
||||
6. It uses `GAMEPAL.PAL` directly and does not yet model alternate or transformed palettes.
|
||||
7. It writes a plain RGBA PNG using only the standard library; there is no zoomed viewer, tile atlas exporter, or sprite manifest yet.
|
||||
8. Some maps still contain invalid shape/frame references in `FIXED.DAT`; the renderer now skips those items instead of aborting the whole map, but that means some broken placements remain missing until the source of those references is understood.
|
||||
|
||||
## Immediate Follow-Ups
|
||||
|
||||
|
|
|
|||
84
render_maps.bat
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
@echo off
|
||||
setlocal EnableExtensions
|
||||
|
||||
pushd "%~dp0" >nul
|
||||
|
||||
set "PYTHON_EXE=%PYTHON_EXE%"
|
||||
if not defined PYTHON_EXE if exist "C:\Users\Maddo\.PYENV\PYENV-WIN\versions\3.14.3\python.exe" set "PYTHON_EXE=C:\Users\Maddo\.PYENV\PYENV-WIN\versions\3.14.3\python.exe"
|
||||
if not defined PYTHON_EXE set "PYTHON_EXE=python"
|
||||
set "RENDER_ARGS=%RENDER_ARGS%"
|
||||
|
||||
if /I "%~1"=="remorse" goto remorse_cli
|
||||
if /I "%~1"=="regret" goto regret_cli
|
||||
if /I "%~1"=="all" goto all_cli
|
||||
if "%~1"=="" goto menu
|
||||
|
||||
echo Unknown option: %~1
|
||||
echo Usage: render_maps.bat [remorse^|regret^|all] [start_map] [end_map]
|
||||
goto end
|
||||
|
||||
:menu
|
||||
cls
|
||||
echo Crusader Map Renderer
|
||||
echo.
|
||||
echo 1. Render all No Remorse maps
|
||||
echo 2. Render all No Regret maps
|
||||
echo 3. Render all maps for both games
|
||||
echo 4. Exit
|
||||
echo.
|
||||
set /p choice=Choose an option:
|
||||
|
||||
if "%choice%"=="1" goto remorse_menu
|
||||
if "%choice%"=="2" goto regret_menu
|
||||
if "%choice%"=="3" goto all_menu
|
||||
if "%choice%"=="4" goto end
|
||||
|
||||
echo.
|
||||
echo Invalid choice.
|
||||
pause
|
||||
goto menu
|
||||
|
||||
:remorse_menu
|
||||
"%PYTHON_EXE%" tools\render_all_maps.py --game remorse %RENDER_ARGS%
|
||||
goto after_run
|
||||
|
||||
:regret_menu
|
||||
"%PYTHON_EXE%" tools\render_all_maps.py --game regret %RENDER_ARGS%
|
||||
goto after_run
|
||||
|
||||
:all_menu
|
||||
"%PYTHON_EXE%" tools\render_all_maps.py --game all %RENDER_ARGS%
|
||||
goto after_run
|
||||
|
||||
:remorse_cli
|
||||
if "%~2"=="" (
|
||||
"%PYTHON_EXE%" tools\render_all_maps.py --game remorse %RENDER_ARGS%
|
||||
) else (
|
||||
"%PYTHON_EXE%" tools\render_all_maps.py --game remorse --start %~2 --end %~3 %RENDER_ARGS%
|
||||
)
|
||||
goto end
|
||||
|
||||
:regret_cli
|
||||
if "%~2"=="" (
|
||||
"%PYTHON_EXE%" tools\render_all_maps.py --game regret %RENDER_ARGS%
|
||||
) else (
|
||||
"%PYTHON_EXE%" tools\render_all_maps.py --game regret --start %~2 --end %~3 %RENDER_ARGS%
|
||||
)
|
||||
goto end
|
||||
|
||||
:all_cli
|
||||
if "%~2"=="" (
|
||||
"%PYTHON_EXE%" tools\render_all_maps.py --game all %RENDER_ARGS%
|
||||
) else (
|
||||
"%PYTHON_EXE%" tools\render_all_maps.py --game all --start %~2 --end %~3 %RENDER_ARGS%
|
||||
)
|
||||
goto end
|
||||
|
||||
:after_run
|
||||
echo.
|
||||
pause
|
||||
goto menu
|
||||
|
||||
:end
|
||||
popd >nul
|
||||
endlocal
|
||||
3
tools/crusader_map/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .cli import main
|
||||
|
||||
__all__ = ["main"]
|
||||
261
tools/crusader_map/cli.py
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from .formats import (
|
||||
FLAG_FLIPPED,
|
||||
ShapeArchive,
|
||||
collect_render_items,
|
||||
load_globs,
|
||||
load_map_items,
|
||||
load_palette,
|
||||
load_typeflags,
|
||||
parse_world_rect,
|
||||
resolve_fixed_dat,
|
||||
resolve_static_dir,
|
||||
)
|
||||
from .png import DEFAULT_BACKGROUND, blit_frame, rgba_buffer, write_png_rgba
|
||||
from .sorting import prepare_sorted_items
|
||||
|
||||
|
||||
KNOWN_MAP_USAGE_HINTS = {
|
||||
"remorse": {
|
||||
0: [
|
||||
"ScummVM CruGame::startGame() calls World::switchMap(0) for a new No Remorse game.",
|
||||
"The same startup path comments the initial player placement as 'Map 1 (mission 1)', so this is a confirmed mission-start map anchor.",
|
||||
],
|
||||
},
|
||||
"regret": {},
|
||||
}
|
||||
|
||||
|
||||
def summarize_render_classes(base_items: list, shape_infos: list) -> dict[str, int]:
|
||||
summary = {
|
||||
"roof_items": 0,
|
||||
"editor_items": 0,
|
||||
"egg_family_items": 0,
|
||||
"invisible_flagged_items": 0,
|
||||
"npc_linked_items": 0,
|
||||
}
|
||||
for item in base_items:
|
||||
if item.flags & 0x0010:
|
||||
summary["invisible_flagged_items"] += 1
|
||||
if item.npc_num != 0:
|
||||
summary["npc_linked_items"] += 1
|
||||
if item.shape >= len(shape_infos):
|
||||
continue
|
||||
info = shape_infos[item.shape]
|
||||
if info.is_roof:
|
||||
summary["roof_items"] += 1
|
||||
if info.is_editor:
|
||||
summary["editor_items"] += 1
|
||||
if info.family in (3, 4, 7, 8):
|
||||
summary["egg_family_items"] += 1
|
||||
return summary
|
||||
|
||||
|
||||
def map_usage_info(game: str, map_index: int, base_items: list, render_items: list) -> dict[str, object]:
|
||||
hints = KNOWN_MAP_USAGE_HINTS.get(game, {}).get(map_index, [])
|
||||
item_map_nums = sorted({item.map_num for item in base_items})
|
||||
nonzero_item_map_nums = [value for value in item_map_nums if value != 0]
|
||||
npc_count = sum(1 for item in base_items if item.npc_num != 0)
|
||||
return {
|
||||
"status": "known_used" if hints else "unknown",
|
||||
"confidence": "commented_reference" if hints else "unknown",
|
||||
"known_hints": hints,
|
||||
"item_map_nums": item_map_nums,
|
||||
"nonzero_item_map_nums": nonzero_item_map_nums,
|
||||
"npc_linked_item_count": npc_count,
|
||||
"note": "No authoritative mission-to-map ownership table has been extracted yet. Unknown does not imply orphaned.",
|
||||
"has_renderable_content": bool(render_items),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Render a Crusader fixed map to PNG.")
|
||||
parser.add_argument("--game", choices=("remorse", "regret"), default="remorse")
|
||||
parser.add_argument("--static-dir", help="Override the static asset directory.")
|
||||
parser.add_argument("--fixed-dat", help="Override the FIXED.DAT path when it does not live under the selected static directory.")
|
||||
parser.add_argument("--map", dest="map_index", type=int, required=True, help="Map index to render.")
|
||||
parser.add_argument("--output", required=True, help="PNG output path.")
|
||||
parser.add_argument("--metadata", help="Optional JSON metadata output path.")
|
||||
parser.add_argument("--no-globs", action="store_true", help="Disable GLOB.FLX expansion.")
|
||||
parser.add_argument(
|
||||
"--include-editor",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="Render editor-only shapes. Enabled by default to keep debug/editor map content visible.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-roofs",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="Render roof/exploration-obscurer shapes. Disabled by default.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-hidden-markers",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="Render hidden markers such as egg-family placements, editor/debug objects, and invisible marker shapes when they have visible frames.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--world-rect",
|
||||
nargs=4,
|
||||
metavar=("MIN_X", "MIN_Y", "MAX_X", "MAX_Y"),
|
||||
help="Restrict rendering to a world-space rectangle.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-pixels",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Fail if the output image would exceed this many pixels. Non-positive values disable the limit.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--progress-every",
|
||||
type=int,
|
||||
default=2000,
|
||||
help="Emit collection and sorting progress every N items. Non-positive values disable progress logging.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--invalid-detail-limit",
|
||||
type=int,
|
||||
default=20,
|
||||
help="Maximum number of invalid shape/frame records to include in metadata.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
static_dir = resolve_static_dir(repo_root, args.game, args.static_dir)
|
||||
fixed_dat_path = resolve_fixed_dat(static_dir, args.fixed_dat)
|
||||
world_rect = parse_world_rect(args.world_rect)
|
||||
|
||||
shape_infos = load_typeflags(static_dir / "TYPEFLAG.DAT")
|
||||
palette = load_palette(static_dir / "GAMEPAL.PAL")
|
||||
globs = load_globs(static_dir / "GLOB.FLX")
|
||||
shape_archive = ShapeArchive(static_dir / "SHAPES.FLX")
|
||||
progress_enabled = args.progress_every > 0
|
||||
start_time = time.monotonic()
|
||||
|
||||
def log_progress(message: str) -> None:
|
||||
if not progress_enabled:
|
||||
return
|
||||
elapsed = time.monotonic() - start_time
|
||||
print(f"[map {args.map_index} +{elapsed:7.1f}s] {message}", file=sys.stderr, flush=True)
|
||||
|
||||
if not fixed_dat_path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"missing FIXED.DAT at {fixed_dat_path}; copy the matching game map file into place or pass --fixed-dat"
|
||||
)
|
||||
base_items = load_map_items(fixed_dat_path, args.map_index)
|
||||
log_progress(f"loaded {len(base_items)} fixed records from {fixed_dat_path}")
|
||||
base_item_summary = summarize_render_classes(base_items, shape_infos)
|
||||
render_items = collect_render_items(
|
||||
base_items,
|
||||
shape_infos,
|
||||
globs,
|
||||
include_editor=args.include_editor,
|
||||
expand_globs=not args.no_globs,
|
||||
world_rect=world_rect,
|
||||
include_roofs=args.include_roofs,
|
||||
include_hidden_markers=args.include_hidden_markers,
|
||||
progress=log_progress if progress_enabled else None,
|
||||
checkpoint_every=args.progress_every,
|
||||
)
|
||||
if not render_items:
|
||||
raise ValueError("no renderable items were found for the selected map")
|
||||
|
||||
usage_info = map_usage_info(args.game, args.map_index, base_items, render_items)
|
||||
|
||||
min_left, min_top, max_right, max_bottom, prepared, occluded_count, invalid_item_count, invalid_items = prepare_sorted_items(
|
||||
render_items,
|
||||
shape_archive,
|
||||
shape_infos,
|
||||
progress=log_progress if progress_enabled else None,
|
||||
checkpoint_every=args.progress_every,
|
||||
max_invalid_details=args.invalid_detail_limit,
|
||||
)
|
||||
if not prepared:
|
||||
raise ValueError("no valid shape/frame pairs were renderable for the selected map")
|
||||
width = max_right - min_left
|
||||
height = max_bottom - min_top
|
||||
if width <= 0 or height <= 0:
|
||||
raise ValueError("computed image bounds are invalid")
|
||||
if args.max_pixels > 0 and width * height > args.max_pixels:
|
||||
raise ValueError(
|
||||
f"image would be {width}x{height} = {width * height} pixels; use --world-rect or raise --max-pixels"
|
||||
)
|
||||
|
||||
output_path = Path(args.output)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
buffer = rgba_buffer(width, height, DEFAULT_BACKGROUND)
|
||||
for node_index, node in enumerate(prepared, start=1):
|
||||
blit_frame(
|
||||
buffer,
|
||||
width,
|
||||
height,
|
||||
node.left - min_left,
|
||||
node.top - min_top,
|
||||
node.frame,
|
||||
node.pixels,
|
||||
palette,
|
||||
flipped=bool(node.item.flags & FLAG_FLIPPED),
|
||||
)
|
||||
if progress_enabled and args.progress_every > 0 and node_index % args.progress_every == 0:
|
||||
log_progress(f"blit painted={node_index} of {len(prepared)}")
|
||||
write_png_rgba(output_path, width, height, buffer)
|
||||
log_progress(f"wrote PNG {output_path} ({width}x{height})")
|
||||
|
||||
used_shapes = sorted({item.shape for item in render_items})
|
||||
metadata = {
|
||||
"game": args.game,
|
||||
"static_dir": str(static_dir),
|
||||
"fixed_dat": str(fixed_dat_path),
|
||||
"map": args.map_index,
|
||||
"raw_item_count": len(base_items),
|
||||
"item_count": len(render_items),
|
||||
"painted_item_count": len(prepared),
|
||||
"occluded_item_count": occluded_count,
|
||||
"invalid_item_count": invalid_item_count,
|
||||
"invalid_items": [
|
||||
{
|
||||
"shape": item.shape,
|
||||
"frame": item.frame,
|
||||
"x": item.x,
|
||||
"y": item.y,
|
||||
"z": item.z,
|
||||
"source": item.source,
|
||||
"reason": item.reason,
|
||||
}
|
||||
for item in invalid_items
|
||||
],
|
||||
"used_shape_count": len(used_shapes),
|
||||
"used_shapes": used_shapes,
|
||||
"usage": usage_info,
|
||||
"base_item_summary": base_item_summary,
|
||||
"sorter": "scummvm_dependency_graph",
|
||||
"filters": {
|
||||
"glob_expansion": not args.no_globs,
|
||||
"editor_shapes_included": args.include_editor,
|
||||
"roofs_included": args.include_roofs,
|
||||
"hidden_markers_included": args.include_hidden_markers,
|
||||
},
|
||||
"bounds": {
|
||||
"screen_left": min_left,
|
||||
"screen_top": min_top,
|
||||
"screen_right": max_right,
|
||||
"screen_bottom": max_bottom,
|
||||
"width": width,
|
||||
"height": height,
|
||||
},
|
||||
"world_rect": list(world_rect) if world_rect else None,
|
||||
}
|
||||
if args.metadata:
|
||||
metadata_path = Path(args.metadata)
|
||||
metadata_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8")
|
||||
print(json.dumps(metadata, indent=2))
|
||||
return 0
|
||||
516
tools/crusader_map/formats.py
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
|
||||
FLEX_TABLE_OFFSET = 0x80
|
||||
FLEX_COUNT_OFFSET = 0x54
|
||||
FIXED_MAP_COUNT_OFFSET = 0x54
|
||||
FIXED_MAP_TABLE_OFFSET = 0x80
|
||||
CRUSADER_COORD_SCALE = 2
|
||||
GLOB_COORD_MASK = ~0x3FF
|
||||
GLOB_COORD_SHIFT = 2
|
||||
GLOB_COORD_OFFSET = 2
|
||||
FLAG_INVISIBLE = 0x0010
|
||||
FLAG_FLIPPED = 0x0020
|
||||
EGG_FAMILIES = {3, 4, 7, 8}
|
||||
|
||||
SI_FIXED = 0x0001
|
||||
SI_SOLID = 0x0002
|
||||
SI_LAND = 0x0008
|
||||
SI_OCCL = 0x0010
|
||||
SI_NOISY = 0x0080
|
||||
SI_DRAW = 0x0100
|
||||
SI_ROOF = 0x0400
|
||||
SI_TRANSL = 0x0800
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FlexEntry:
|
||||
offset: int
|
||||
size: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ShapeInfo:
|
||||
family: int
|
||||
flags: int
|
||||
x: int
|
||||
y: int
|
||||
z: int
|
||||
anim_type: int
|
||||
|
||||
@property
|
||||
def is_editor(self) -> bool:
|
||||
return bool(self.flags & 0x1000)
|
||||
|
||||
@property
|
||||
def is_fixed(self) -> bool:
|
||||
return bool(self.flags & SI_FIXED)
|
||||
|
||||
@property
|
||||
def is_solid(self) -> bool:
|
||||
return bool(self.flags & SI_SOLID)
|
||||
|
||||
@property
|
||||
def is_land(self) -> bool:
|
||||
return bool(self.flags & SI_LAND)
|
||||
|
||||
@property
|
||||
def is_occl(self) -> bool:
|
||||
return bool(self.flags & SI_OCCL)
|
||||
|
||||
@property
|
||||
def is_noisy(self) -> bool:
|
||||
return bool(self.flags & SI_NOISY)
|
||||
|
||||
@property
|
||||
def is_draw(self) -> bool:
|
||||
return bool(self.flags & SI_DRAW)
|
||||
|
||||
@property
|
||||
def is_roof(self) -> bool:
|
||||
return bool(self.flags & SI_ROOF)
|
||||
|
||||
@property
|
||||
def is_translucent(self) -> bool:
|
||||
return bool(self.flags & SI_TRANSL)
|
||||
|
||||
@property
|
||||
def is_invitem(self) -> bool:
|
||||
return self.family == 13
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GlobItem:
|
||||
x: int
|
||||
y: int
|
||||
z: int
|
||||
shape: int
|
||||
frame: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MapItem:
|
||||
x: int
|
||||
y: int
|
||||
z: int
|
||||
shape: int
|
||||
frame: int
|
||||
flags: int
|
||||
quality: int
|
||||
npc_num: int
|
||||
map_num: int
|
||||
next_item: int
|
||||
source: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ShapeFrame:
|
||||
compressed: bool
|
||||
width: int
|
||||
height: int
|
||||
xoff: int
|
||||
yoff: int
|
||||
line_offsets: tuple[int, ...]
|
||||
rle_data: bytes
|
||||
|
||||
|
||||
def read_u16_le(data: bytes, offset: int) -> int:
|
||||
return struct.unpack_from("<H", data, offset)[0]
|
||||
|
||||
|
||||
def read_u24_le(data: bytes, offset: int) -> int:
|
||||
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16)
|
||||
|
||||
|
||||
def read_u32_le(data: bytes, offset: int) -> int:
|
||||
return struct.unpack_from("<I", data, offset)[0]
|
||||
|
||||
|
||||
class FlexArchive:
|
||||
def __init__(self, path: Path) -> None:
|
||||
self.path = path
|
||||
self.data = path.read_bytes()
|
||||
self.entries = self._read_entries(self.data)
|
||||
|
||||
@staticmethod
|
||||
def _read_entries(data: bytes) -> list[FlexEntry]:
|
||||
count = read_u32_le(data, FLEX_COUNT_OFFSET)
|
||||
entries: list[FlexEntry] = []
|
||||
for index in range(count):
|
||||
base = FLEX_TABLE_OFFSET + index * 8
|
||||
entries.append(FlexEntry(read_u32_le(data, base), read_u32_le(data, base + 4)))
|
||||
return entries
|
||||
|
||||
def get(self, index: int) -> bytes:
|
||||
entry = self.entries[index]
|
||||
if entry.size == 0:
|
||||
return b""
|
||||
return self.data[entry.offset : entry.offset + entry.size]
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.entries)
|
||||
|
||||
|
||||
class ShapeArchive:
|
||||
def __init__(self, path: Path) -> None:
|
||||
self.archive = FlexArchive(path)
|
||||
self._shape_cache: dict[int, tuple[ShapeFrame, ...]] = {}
|
||||
self._decoded_frame_cache: dict[tuple[int, int], list[int]] = {}
|
||||
|
||||
def get_frame(self, shape_index: int, frame_index: int) -> ShapeFrame:
|
||||
frames = self._get_shape(shape_index)
|
||||
if frame_index < 0 or frame_index >= len(frames):
|
||||
raise IndexError(f"shape {shape_index} frame {frame_index} out of range")
|
||||
return frames[frame_index]
|
||||
|
||||
def decode_frame(self, shape_index: int, frame_index: int) -> tuple[ShapeFrame, list[int]]:
|
||||
cache_key = (shape_index, frame_index)
|
||||
decoded = self._decoded_frame_cache.get(cache_key)
|
||||
frame = self.get_frame(shape_index, frame_index)
|
||||
if decoded is None:
|
||||
decoded = self._decode_pixels(frame)
|
||||
self._decoded_frame_cache[cache_key] = decoded
|
||||
return frame, decoded
|
||||
|
||||
def _get_shape(self, shape_index: int) -> tuple[ShapeFrame, ...]:
|
||||
cached = self._shape_cache.get(shape_index)
|
||||
if cached is not None:
|
||||
return cached
|
||||
raw = self.archive.get(shape_index)
|
||||
if not raw:
|
||||
raise ValueError(f"shape {shape_index} has no data")
|
||||
frames = self._parse_shape(raw)
|
||||
self._shape_cache[shape_index] = frames
|
||||
return frames
|
||||
|
||||
@staticmethod
|
||||
def _parse_shape(data: bytes) -> tuple[ShapeFrame, ...]:
|
||||
frame_count = read_u16_le(data, 4)
|
||||
frames: list[ShapeFrame] = []
|
||||
for index in range(frame_count):
|
||||
header_offset = 6 + index * 8
|
||||
frame_offset = read_u24_le(data, header_offset)
|
||||
frame_size = read_u32_le(data, header_offset + 4)
|
||||
frame_data = data[frame_offset : frame_offset + frame_size]
|
||||
if len(frame_data) < 28:
|
||||
raise ValueError(f"frame {index} too small: {len(frame_data)}")
|
||||
compressed = bool(read_u32_le(frame_data, 8))
|
||||
width = read_u32_le(frame_data, 12)
|
||||
height = read_u32_le(frame_data, 16)
|
||||
xoff = struct.unpack_from("<i", frame_data, 20)[0]
|
||||
yoff = struct.unpack_from("<i", frame_data, 24)[0]
|
||||
line_offsets = tuple(read_u32_le(frame_data, 28 + row * 4) - ((height - row) * 4) for row in range(height))
|
||||
rle_offset = 28 + height * 4
|
||||
frames.append(
|
||||
ShapeFrame(
|
||||
compressed=compressed,
|
||||
width=width,
|
||||
height=height,
|
||||
xoff=xoff,
|
||||
yoff=yoff,
|
||||
line_offsets=line_offsets,
|
||||
rle_data=frame_data[rle_offset:],
|
||||
)
|
||||
)
|
||||
return tuple(frames)
|
||||
|
||||
@staticmethod
|
||||
def _decode_pixels(frame: ShapeFrame) -> list[int]:
|
||||
pixels = [-1] * (frame.width * frame.height)
|
||||
rle = frame.rle_data
|
||||
for row in range(frame.height):
|
||||
pos = frame.line_offsets[row]
|
||||
xpos = 0
|
||||
while xpos < frame.width:
|
||||
if pos >= len(rle):
|
||||
raise ValueError(f"row {row} overran RLE data")
|
||||
xpos += rle[pos]
|
||||
pos += 1
|
||||
if xpos == frame.width:
|
||||
break
|
||||
if pos >= len(rle):
|
||||
raise ValueError(f"row {row} missing run header")
|
||||
dlen = rle[pos]
|
||||
pos += 1
|
||||
run_type = 0
|
||||
if frame.compressed:
|
||||
run_type = dlen & 1
|
||||
dlen >>= 1
|
||||
if dlen <= 0 or xpos + dlen > frame.width:
|
||||
raise ValueError(f"invalid run length {dlen} at row {row}")
|
||||
row_base = row * frame.width + xpos
|
||||
if run_type == 0:
|
||||
end = pos + dlen
|
||||
if end > len(rle):
|
||||
raise ValueError(f"row {row} literal run overruns RLE data")
|
||||
run = rle[pos:end]
|
||||
for index, color in enumerate(run):
|
||||
pixels[row_base + index] = color
|
||||
pos = end
|
||||
else:
|
||||
if pos >= len(rle):
|
||||
raise ValueError(f"row {row} repeated-color run missing color byte")
|
||||
color = rle[pos]
|
||||
pos += 1
|
||||
for index in range(dlen):
|
||||
pixels[row_base + index] = color
|
||||
xpos += dlen
|
||||
return pixels
|
||||
|
||||
|
||||
def load_palette(path: Path) -> list[tuple[int, int, int]]:
|
||||
data = path.read_bytes()
|
||||
if len(data) < 768:
|
||||
raise ValueError(f"palette too small: {path}")
|
||||
palette: list[tuple[int, int, int]] = []
|
||||
for index in range(256):
|
||||
r = (data[index * 3] * 255) // 63
|
||||
g = (data[index * 3 + 1] * 255) // 63
|
||||
b = (data[index * 3 + 2] * 255) // 63
|
||||
palette.append((r, g, b))
|
||||
return palette
|
||||
|
||||
|
||||
def load_typeflags(path: Path) -> list[ShapeInfo]:
|
||||
data = path.read_bytes()
|
||||
infos: list[ShapeInfo] = []
|
||||
for base in range(0, len(data), 9):
|
||||
block = data[base : base + 9]
|
||||
if len(block) < 9:
|
||||
break
|
||||
flags = 0
|
||||
if block[0] & 0x01:
|
||||
flags |= 0x0001
|
||||
if block[0] & 0x02:
|
||||
flags |= 0x0002
|
||||
if block[0] & 0x04:
|
||||
flags |= 0x0004
|
||||
if block[0] & 0x08:
|
||||
flags |= 0x0008
|
||||
if block[0] & 0x10:
|
||||
flags |= 0x0010
|
||||
if block[0] & 0x20:
|
||||
flags |= 0x0020
|
||||
if block[0] & 0x40:
|
||||
flags |= 0x0040
|
||||
if block[0] & 0x80:
|
||||
flags |= 0x0080
|
||||
if block[1] & 0x01:
|
||||
flags |= 0x0100
|
||||
if block[1] & 0x02:
|
||||
flags |= 0x0200
|
||||
if block[1] & 0x04:
|
||||
flags |= 0x0400
|
||||
if block[1] & 0x08:
|
||||
flags |= 0x0800
|
||||
if block[6] & 0x01:
|
||||
flags |= 0x1000
|
||||
if block[6] & 0x02:
|
||||
flags |= 0x2000
|
||||
if block[6] & 0x04:
|
||||
flags |= 0x4000
|
||||
if block[6] & 0x08:
|
||||
flags |= 0x8000
|
||||
if block[6] & 0x10:
|
||||
flags |= 0x10000
|
||||
if block[6] & 0x20:
|
||||
flags |= 0x20000
|
||||
if block[6] & 0x40:
|
||||
flags |= 0x40000
|
||||
if block[6] & 0x80:
|
||||
flags |= 0x80000
|
||||
family = (block[1] >> 4) + ((block[2] & 1) << 4)
|
||||
x = ((block[3] << 3) | (block[2] >> 5)) & 0x1F
|
||||
y = (block[3] >> 2) & 0x1F
|
||||
z = ((block[4] << 1) | (block[3] >> 7)) & 0x1F
|
||||
anim_type = block[4] >> 4
|
||||
infos.append(ShapeInfo(family=family, flags=flags, x=x, y=y, z=z, anim_type=anim_type))
|
||||
return infos
|
||||
|
||||
|
||||
def load_globs(path: Path) -> list[list[GlobItem]]:
|
||||
archive = FlexArchive(path)
|
||||
globs: list[list[GlobItem]] = []
|
||||
for index in range(len(archive)):
|
||||
raw = archive.get(index)
|
||||
if not raw:
|
||||
globs.append([])
|
||||
continue
|
||||
count = read_u16_le(raw, 0)
|
||||
items: list[GlobItem] = []
|
||||
for item_index in range(count):
|
||||
base = 2 + item_index * 6
|
||||
items.append(
|
||||
GlobItem(
|
||||
x=raw[base],
|
||||
y=raw[base + 1],
|
||||
z=raw[base + 2],
|
||||
shape=read_u16_le(raw, base + 3),
|
||||
frame=raw[base + 5],
|
||||
)
|
||||
)
|
||||
globs.append(items)
|
||||
return globs
|
||||
|
||||
|
||||
def load_map_items(path: Path, map_index: int) -> list[MapItem]:
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(path)
|
||||
data = path.read_bytes()
|
||||
map_count = read_u16_le(data, FIXED_MAP_COUNT_OFFSET)
|
||||
if map_index < 0 or map_index >= map_count:
|
||||
raise ValueError(f"map index {map_index} out of range 0..{map_count - 1}")
|
||||
table_offset = FIXED_MAP_TABLE_OFFSET + map_index * 8
|
||||
map_offset = read_u32_le(data, table_offset)
|
||||
map_size = read_u32_le(data, table_offset + 4)
|
||||
payload = data[map_offset : map_offset + map_size]
|
||||
if len(payload) != map_size:
|
||||
raise ValueError(f"map {map_index} payload truncated")
|
||||
items: list[MapItem] = []
|
||||
for base in range(0, len(payload), 16):
|
||||
record = payload[base : base + 16]
|
||||
if len(record) < 16:
|
||||
break
|
||||
x = read_u16_le(record, 0) * CRUSADER_COORD_SCALE
|
||||
y = read_u16_le(record, 2) * CRUSADER_COORD_SCALE
|
||||
items.append(
|
||||
MapItem(
|
||||
x=x,
|
||||
y=y,
|
||||
z=record[4],
|
||||
shape=read_u16_le(record, 5),
|
||||
frame=record[7],
|
||||
flags=read_u16_le(record, 8),
|
||||
quality=read_u16_le(record, 10),
|
||||
npc_num=record[12],
|
||||
map_num=record[13],
|
||||
next_item=read_u16_le(record, 14),
|
||||
source="fixed",
|
||||
)
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
def expand_glob_item(item: MapItem, globs: list[list[GlobItem]]) -> list[MapItem]:
|
||||
if item.quality < 0 or item.quality >= len(globs):
|
||||
return []
|
||||
expanded: list[MapItem] = []
|
||||
for glob_item in globs[item.quality]:
|
||||
expanded.append(
|
||||
MapItem(
|
||||
x=(item.x & GLOB_COORD_MASK) + (glob_item.x << GLOB_COORD_SHIFT) + GLOB_COORD_OFFSET,
|
||||
y=(item.y & GLOB_COORD_MASK) + (glob_item.y << GLOB_COORD_SHIFT) + GLOB_COORD_OFFSET,
|
||||
z=item.z + glob_item.z,
|
||||
shape=glob_item.shape,
|
||||
frame=glob_item.frame,
|
||||
flags=0,
|
||||
quality=0,
|
||||
npc_num=0,
|
||||
map_num=item.map_num,
|
||||
next_item=0,
|
||||
source="glob",
|
||||
)
|
||||
)
|
||||
return expanded
|
||||
|
||||
|
||||
def collect_render_items(
|
||||
base_items: list[MapItem],
|
||||
shape_infos: list[ShapeInfo],
|
||||
globs: list[list[GlobItem]],
|
||||
include_editor: bool,
|
||||
expand_globs: bool,
|
||||
world_rect: tuple[int, int, int, int] | None,
|
||||
include_roofs: bool = True,
|
||||
include_hidden_markers: bool = False,
|
||||
progress: Callable[[str], None] | None = None,
|
||||
checkpoint_every: int = 0,
|
||||
) -> list[MapItem]:
|
||||
render_items: list[MapItem] = []
|
||||
pending = list(base_items)
|
||||
index = 0
|
||||
skipped_invisible = 0
|
||||
skipped_world_rect = 0
|
||||
skipped_invalid_shape = 0
|
||||
skipped_editor = 0
|
||||
skipped_egg = 0
|
||||
skipped_roof = 0
|
||||
skipped_hidden = 0
|
||||
expanded_globs = 0
|
||||
while index < len(pending):
|
||||
item = pending[index]
|
||||
index += 1
|
||||
if item.flags & FLAG_INVISIBLE:
|
||||
if not include_hidden_markers:
|
||||
skipped_hidden += 1
|
||||
continue
|
||||
skipped_invisible += 1
|
||||
if world_rect is not None:
|
||||
min_x, min_y, max_x, max_y = world_rect
|
||||
if item.x < min_x or item.y < min_y or item.x > max_x or item.y > max_y:
|
||||
skipped_world_rect += 1
|
||||
continue
|
||||
if item.shape >= len(shape_infos):
|
||||
skipped_invalid_shape += 1
|
||||
continue
|
||||
info = shape_infos[item.shape]
|
||||
if info.is_editor and not include_editor:
|
||||
skipped_editor += 1
|
||||
continue
|
||||
if info.is_roof and not include_roofs:
|
||||
skipped_roof += 1
|
||||
continue
|
||||
if expand_globs and info.family == 3 and item.source == "fixed":
|
||||
pending.extend(expand_glob_item(item, globs))
|
||||
expanded_globs += 1
|
||||
if not include_hidden_markers:
|
||||
continue
|
||||
if info.family in EGG_FAMILIES and not include_hidden_markers:
|
||||
skipped_egg += 1
|
||||
continue
|
||||
render_items.append(item)
|
||||
if progress is not None and checkpoint_every > 0 and index % checkpoint_every == 0:
|
||||
progress(
|
||||
"collect "
|
||||
f"processed={index} pending={len(pending)} rendered={len(render_items)} "
|
||||
f"expanded_globs={expanded_globs} skipped=(invisible:{skipped_invisible}, world:{skipped_world_rect}, "
|
||||
f"shape:{skipped_invalid_shape}, editor:{skipped_editor}, roof:{skipped_roof}, hidden:{skipped_hidden}, egg:{skipped_egg})"
|
||||
)
|
||||
if progress is not None:
|
||||
progress(
|
||||
"collect complete "
|
||||
f"processed={index} pending={len(pending)} rendered={len(render_items)} "
|
||||
f"expanded_globs={expanded_globs} skipped=(invisible:{skipped_invisible}, world:{skipped_world_rect}, "
|
||||
f"shape:{skipped_invalid_shape}, editor:{skipped_editor}, roof:{skipped_roof}, hidden:{skipped_hidden}, egg:{skipped_egg})"
|
||||
)
|
||||
return render_items
|
||||
|
||||
|
||||
def parse_world_rect(values: list[str] | None) -> tuple[int, int, int, int] | None:
|
||||
if values is None:
|
||||
return None
|
||||
if len(values) != 4:
|
||||
raise ValueError("--world-rect expects four integers: min_x min_y max_x max_y")
|
||||
min_x, min_y, max_x, max_y = (int(value, 0) for value in values)
|
||||
if min_x > max_x or min_y > max_y:
|
||||
raise ValueError("invalid --world-rect bounds")
|
||||
return min_x, min_y, max_x, max_y
|
||||
|
||||
|
||||
def resolve_fixed_dat(static_dir: Path, fixed_dat: str | None) -> Path:
|
||||
if fixed_dat:
|
||||
return Path(fixed_dat)
|
||||
return static_dir / "FIXED.DAT"
|
||||
|
||||
|
||||
def resolve_static_dir(repo_root: Path, game: str, static_dir: str | None) -> Path:
|
||||
if static_dir:
|
||||
return Path(static_dir)
|
||||
if game == "regret":
|
||||
return repo_root / "STATIC_REGRET"
|
||||
return repo_root / "STATIC"
|
||||
67
tools/crusader_map/png.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
from .formats import ShapeFrame
|
||||
|
||||
|
||||
DEFAULT_BACKGROUND = (10, 12, 18, 255)
|
||||
|
||||
|
||||
def rgba_buffer(width: int, height: int, color: tuple[int, int, int, int]) -> bytearray:
|
||||
r, g, b, a = color
|
||||
row = bytes((r, g, b, a)) * width
|
||||
return bytearray(row * height)
|
||||
|
||||
|
||||
def blit_frame(
|
||||
buffer: bytearray,
|
||||
canvas_width: int,
|
||||
canvas_height: int,
|
||||
left: int,
|
||||
top: int,
|
||||
frame: ShapeFrame,
|
||||
pixels: list[int],
|
||||
palette: list[tuple[int, int, int]],
|
||||
flipped: bool,
|
||||
) -> None:
|
||||
for src_y in range(frame.height):
|
||||
dst_y = top + src_y
|
||||
if dst_y < 0 or dst_y >= canvas_height:
|
||||
continue
|
||||
row_base = src_y * frame.width
|
||||
for src_x in range(frame.width):
|
||||
color_index = pixels[row_base + (frame.width - 1 - src_x if flipped else src_x)]
|
||||
if color_index < 0:
|
||||
continue
|
||||
dst_x = left + src_x
|
||||
if dst_x < 0 or dst_x >= canvas_width:
|
||||
continue
|
||||
pixel_base = (dst_y * canvas_width + dst_x) * 4
|
||||
r, g, b = palette[color_index]
|
||||
buffer[pixel_base : pixel_base + 4] = bytes((r, g, b, 255))
|
||||
|
||||
|
||||
def write_png_rgba(path: Path, width: int, height: int, pixels: bytearray) -> None:
|
||||
def chunk(chunk_type: bytes, payload: bytes) -> bytes:
|
||||
return (
|
||||
struct.pack(">I", len(payload))
|
||||
+ chunk_type
|
||||
+ payload
|
||||
+ struct.pack(">I", zlib.crc32(chunk_type + payload) & 0xFFFFFFFF)
|
||||
)
|
||||
|
||||
rows = bytearray()
|
||||
stride = width * 4
|
||||
for row in range(height):
|
||||
rows.append(0)
|
||||
start = row * stride
|
||||
rows.extend(pixels[start : start + stride])
|
||||
|
||||
payload = bytearray(b"\x89PNG\r\n\x1a\n")
|
||||
payload.extend(chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)))
|
||||
payload.extend(chunk(b"IDAT", zlib.compress(bytes(rows), level=9)))
|
||||
payload.extend(chunk(b"IEND", b""))
|
||||
path.write_bytes(payload)
|
||||
418
tools/crusader_map/sorting.py
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable
|
||||
|
||||
from .formats import FLAG_FLIPPED, MapItem, ShapeArchive, ShapeFrame, ShapeInfo
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InvalidRenderItem:
|
||||
shape: int
|
||||
frame: int
|
||||
x: int
|
||||
y: int
|
||||
z: int
|
||||
source: str
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SortNode:
|
||||
item: MapItem
|
||||
info: ShapeInfo
|
||||
frame: ShapeFrame
|
||||
pixels: list[int]
|
||||
left: int
|
||||
top: int
|
||||
right: int
|
||||
bottom: int
|
||||
x: int
|
||||
x_left: int
|
||||
y: int
|
||||
y_far: int
|
||||
z: int
|
||||
z_top: int
|
||||
sx_left: int
|
||||
sx_right: int
|
||||
sx_top: int
|
||||
sy_top: int
|
||||
sx_bot: int
|
||||
sy_bot: int
|
||||
fbigsq: bool
|
||||
flat: bool
|
||||
occl: bool
|
||||
solid: bool
|
||||
draw: bool
|
||||
roof: bool
|
||||
noisy: bool
|
||||
anim: bool
|
||||
trans: bool
|
||||
fixed: bool
|
||||
land: bool
|
||||
sprite: bool
|
||||
invitem: bool
|
||||
occluded: bool = False
|
||||
order: int = -1
|
||||
depends: list["SortNode"] = field(default_factory=list)
|
||||
|
||||
def list_less_than(self, other: "SortNode") -> bool:
|
||||
if self.sprite != other.sprite:
|
||||
return self.sprite < other.sprite
|
||||
if self.z != other.z:
|
||||
return self.z < other.z
|
||||
return self.flat > other.flat
|
||||
|
||||
def overlap(self, other: "SortNode") -> bool:
|
||||
if not rect_intersects(self, other):
|
||||
return False
|
||||
|
||||
point_top_diff = (self.sx_top - other.sx_bot, self.sy_top - other.sy_bot)
|
||||
point_bot_diff = (self.sx_bot - other.sx_top, self.sy_bot - other.sy_top)
|
||||
|
||||
dot_top_left = point_top_diff[0] + point_top_diff[1] * 2
|
||||
dot_top_right = -point_top_diff[0] + point_top_diff[1] * 2
|
||||
dot_bot_left = point_bot_diff[0] - point_bot_diff[1] * 2
|
||||
dot_bot_right = -point_bot_diff[0] - point_bot_diff[1] * 2
|
||||
|
||||
right_clear = self.sx_right <= other.sx_left
|
||||
left_clear = self.sx_left >= other.sx_right
|
||||
top_left_clear = dot_top_left >= 0
|
||||
top_right_clear = dot_top_right >= 0
|
||||
bot_left_clear = dot_bot_left >= 0
|
||||
bot_right_clear = dot_bot_right >= 0
|
||||
|
||||
clear = right_clear or left_clear or (bot_right_clear or bot_left_clear) or (top_right_clear or top_left_clear)
|
||||
return not clear
|
||||
|
||||
def occludes(self, other: "SortNode") -> bool:
|
||||
if not rect_contains(self, other):
|
||||
return False
|
||||
|
||||
point_top_diff = (self.sx_top - other.sx_top, self.sy_top - other.sy_top)
|
||||
point_bot_diff = (self.sx_bot - other.sx_bot, self.sy_bot - other.sy_bot)
|
||||
|
||||
dot_top_left = point_top_diff[0] + point_top_diff[1] * 2
|
||||
dot_top_right = -point_top_diff[0] + point_top_diff[1] * 2
|
||||
dot_bot_left = point_bot_diff[0] - point_bot_diff[1] * 2
|
||||
dot_bot_right = -point_bot_diff[0] - point_bot_diff[1] * 2
|
||||
|
||||
right_res = self.sx_right >= other.sx_right
|
||||
left_res = self.sx_left <= other.sx_left
|
||||
top_left_res = dot_top_left <= 0
|
||||
top_right_res = dot_top_right <= 0
|
||||
bot_left_res = dot_bot_left <= 0
|
||||
bot_right_res = dot_bot_right <= 0
|
||||
return right_res and left_res and bot_right_res and bot_left_res and top_right_res and top_left_res
|
||||
|
||||
def below(self, other: "SortNode") -> bool:
|
||||
if self.sprite != other.sprite:
|
||||
return self.sprite < other.sprite
|
||||
|
||||
if self.flat and other.flat:
|
||||
if self.z != other.z:
|
||||
return self.z < other.z
|
||||
elif self.invitem == other.invitem:
|
||||
if self.z_top <= other.z:
|
||||
return True
|
||||
if self.z >= other.z_top:
|
||||
return False
|
||||
|
||||
y_flat_self = self.y_far == self.y
|
||||
y_flat_other = other.y_far == other.y
|
||||
if y_flat_self and y_flat_other:
|
||||
if self.y // 32 != other.y // 32:
|
||||
return self.y < other.y
|
||||
else:
|
||||
if self.y <= other.y_far:
|
||||
return True
|
||||
if self.y_far >= other.y:
|
||||
return False
|
||||
|
||||
x_flat_self = self.x_left == self.x
|
||||
x_flat_other = other.x_left == other.x
|
||||
if x_flat_self and x_flat_other:
|
||||
if self.x // 32 != other.x // 32:
|
||||
return self.x < other.x
|
||||
else:
|
||||
if self.x <= other.x_left:
|
||||
return True
|
||||
if self.x_left >= other.x:
|
||||
return False
|
||||
|
||||
if self.z_top - 8 <= other.z and self.z < other.z_top - 8:
|
||||
return True
|
||||
if self.z >= other.z_top - 8 and self.z_top - 8 > other.z:
|
||||
return False
|
||||
|
||||
if y_flat_self != y_flat_other:
|
||||
if self.y // 32 <= other.y_far // 32:
|
||||
return True
|
||||
if self.y_far // 32 >= other.y // 32:
|
||||
return False
|
||||
y_center_self = (self.y_far // 32 + self.y // 32) // 2
|
||||
y_center_other = (other.y_far // 32 + other.y // 32) // 2
|
||||
if y_center_self != y_center_other:
|
||||
return y_center_self < y_center_other
|
||||
|
||||
if x_flat_self != x_flat_other:
|
||||
if self.x // 32 <= other.x_left // 32:
|
||||
return True
|
||||
if self.x_left // 32 >= other.x // 32:
|
||||
return False
|
||||
x_center_self = (self.x_left // 32 + self.x // 32) // 2
|
||||
x_center_other = (other.x_left // 32 + other.x // 32) // 2
|
||||
if x_center_self != x_center_other:
|
||||
return x_center_self < x_center_other
|
||||
|
||||
if self.flat or other.flat:
|
||||
if self.z != other.z:
|
||||
return self.z < other.z
|
||||
if self.invitem != other.invitem:
|
||||
return self.invitem < other.invitem
|
||||
if self.flat != other.flat:
|
||||
return self.flat > other.flat
|
||||
if self.trans != other.trans:
|
||||
return self.trans < other.trans
|
||||
if self.anim != other.anim:
|
||||
return self.anim < other.anim
|
||||
if self.draw != other.draw:
|
||||
return self.draw > other.draw
|
||||
if self.solid != other.solid:
|
||||
return self.solid > other.solid
|
||||
if self.occl != other.occl:
|
||||
return self.occl > other.occl
|
||||
if self.fbigsq != other.fbigsq:
|
||||
return self.fbigsq > other.fbigsq
|
||||
|
||||
if self.x == other.x and self.y == other.y and self.trans != other.trans:
|
||||
return self.trans < other.trans
|
||||
|
||||
if self.land and other.land and self.roof != other.roof:
|
||||
return self.roof < other.roof
|
||||
if self.roof != other.roof:
|
||||
return self.roof > other.roof
|
||||
if self.z != other.z:
|
||||
return self.z < other.z
|
||||
|
||||
if x_flat_self or x_flat_other or y_flat_self or y_flat_other:
|
||||
if self.sx_left != other.sx_left:
|
||||
return self.sx_left > other.sx_left
|
||||
if self.sy_bot != other.sy_bot:
|
||||
return self.sy_bot < other.sy_bot
|
||||
|
||||
if self.x + self.y != other.x + other.y:
|
||||
return self.x + self.y < other.x + other.y
|
||||
if self.x_left + self.y_far != other.x_left + other.y_far:
|
||||
return self.x_left + self.y_far < other.x_left + other.y_far
|
||||
if self.y != other.y:
|
||||
return self.y < other.y
|
||||
if self.x != other.x:
|
||||
return self.x < other.x
|
||||
if self.item.shape != other.item.shape:
|
||||
return self.item.shape < other.item.shape
|
||||
return self.item.frame < other.item.frame
|
||||
|
||||
|
||||
def rect_intersects(left: SortNode, right: SortNode) -> bool:
|
||||
return left.left < right.right and left.right > right.left and left.top < right.bottom and left.bottom > right.top
|
||||
|
||||
|
||||
def rect_contains(outer: SortNode, inner: SortNode) -> bool:
|
||||
return outer.left <= inner.left and outer.top <= inner.top and outer.right >= inner.right and outer.bottom >= inner.bottom
|
||||
|
||||
|
||||
def build_sort_node(item: MapItem, info: ShapeInfo, frame: ShapeFrame, pixels: list[int]) -> SortNode:
|
||||
flipped = bool(item.flags & FLAG_FLIPPED)
|
||||
xdim = info.y * 32 if flipped else info.x * 32
|
||||
ydim = info.x * 32 if flipped else info.y * 32
|
||||
zdim = info.z * 8
|
||||
|
||||
x = item.x
|
||||
y = item.y
|
||||
z = item.z
|
||||
x_left = x - xdim
|
||||
y_far = y - ydim
|
||||
z_top = z + zdim
|
||||
|
||||
sx_left = x_left // 4 - y // 4
|
||||
sx_right = x // 4 - y_far // 4
|
||||
sx_top = x_left // 4 - y_far // 4
|
||||
sy_top = x_left // 8 + y_far // 8 - z_top
|
||||
sx_bot = x // 4 - y // 4
|
||||
sy_bot = x // 8 + y // 8 - z
|
||||
|
||||
left = sx_bot + frame.xoff - frame.width if flipped else sx_bot - frame.xoff
|
||||
top = sy_bot - frame.yoff
|
||||
right = left + frame.width
|
||||
bottom = top + frame.height
|
||||
|
||||
return SortNode(
|
||||
item=item,
|
||||
info=info,
|
||||
frame=frame,
|
||||
pixels=pixels,
|
||||
left=left,
|
||||
top=top,
|
||||
right=right,
|
||||
bottom=bottom,
|
||||
x=x,
|
||||
x_left=x_left,
|
||||
y=y,
|
||||
y_far=y_far,
|
||||
z=z,
|
||||
z_top=z_top,
|
||||
sx_left=sx_left,
|
||||
sx_right=sx_right,
|
||||
sx_top=sx_top,
|
||||
sy_top=sy_top,
|
||||
sx_bot=sx_bot,
|
||||
sy_bot=sy_bot,
|
||||
fbigsq=xdim == ydim and xdim >= 128,
|
||||
flat=zdim == 0,
|
||||
occl=info.is_occl and not info.is_translucent,
|
||||
solid=info.is_solid,
|
||||
draw=info.is_draw,
|
||||
roof=info.is_roof,
|
||||
noisy=info.is_noisy,
|
||||
anim=info.anim_type != 0,
|
||||
trans=info.is_translucent,
|
||||
fixed=info.is_fixed,
|
||||
land=info.is_land,
|
||||
sprite=False,
|
||||
invitem=info.is_invitem,
|
||||
)
|
||||
|
||||
|
||||
def insert_dependency_sorted(depends: list[SortNode], node: SortNode) -> bool:
|
||||
for index, current in enumerate(depends):
|
||||
if current is node:
|
||||
return False
|
||||
if node.list_less_than(current):
|
||||
depends.insert(index, node)
|
||||
return True
|
||||
depends.append(node)
|
||||
return True
|
||||
|
||||
|
||||
def resolve_paint_order(
|
||||
ordered: list[SortNode],
|
||||
progress: Callable[[str], None] | None = None,
|
||||
checkpoint_every: int = 0,
|
||||
) -> list[SortNode]:
|
||||
painted: list[SortNode] = []
|
||||
|
||||
def visit(node: SortNode) -> None:
|
||||
if node.occluded or node.order >= 0:
|
||||
return
|
||||
node.order = -2
|
||||
for dependency in node.depends:
|
||||
if dependency.order == -2:
|
||||
break
|
||||
if dependency.order == -1:
|
||||
visit(dependency)
|
||||
node.order = painted[-1].order + 1 if painted else 0
|
||||
painted.append(node)
|
||||
if progress is not None and checkpoint_every > 0 and len(painted) % checkpoint_every == 0:
|
||||
progress(f"paint resolved={len(painted)} of {len(ordered)}")
|
||||
|
||||
for node in ordered:
|
||||
if node.order == -1:
|
||||
visit(node)
|
||||
if progress is not None:
|
||||
progress(f"paint complete resolved={len(painted)} of {len(ordered)}")
|
||||
return painted
|
||||
|
||||
|
||||
def prepare_sorted_items(
|
||||
items: list[MapItem],
|
||||
archive: ShapeArchive,
|
||||
shape_infos: list[ShapeInfo],
|
||||
progress: Callable[[str], None] | None = None,
|
||||
checkpoint_every: int = 0,
|
||||
max_invalid_details: int = 20,
|
||||
) -> tuple[int, int, int, int, list[SortNode], int, int, list[InvalidRenderItem]]:
|
||||
ordered: list[SortNode] = []
|
||||
min_left = sys.maxsize
|
||||
min_top = sys.maxsize
|
||||
max_right = -sys.maxsize
|
||||
max_bottom = -sys.maxsize
|
||||
occluded_count = 0
|
||||
invalid_item_count = 0
|
||||
invalid_items: list[InvalidRenderItem] = []
|
||||
dependency_count = 0
|
||||
|
||||
for item_index, item in enumerate(items, start=1):
|
||||
try:
|
||||
frame, pixels = archive.decode_frame(item.shape, item.frame)
|
||||
except (IndexError, ValueError) as error:
|
||||
invalid_item_count += 1
|
||||
if len(invalid_items) < max_invalid_details:
|
||||
invalid_items.append(
|
||||
InvalidRenderItem(
|
||||
shape=item.shape,
|
||||
frame=item.frame,
|
||||
x=item.x,
|
||||
y=item.y,
|
||||
z=item.z,
|
||||
source=item.source,
|
||||
reason=str(error),
|
||||
)
|
||||
)
|
||||
continue
|
||||
node = build_sort_node(item, shape_infos[item.shape], frame, pixels)
|
||||
|
||||
min_left = min(min_left, node.left)
|
||||
min_top = min(min_top, node.top)
|
||||
max_right = max(max_right, node.right)
|
||||
max_bottom = max(max_bottom, node.bottom)
|
||||
|
||||
insert_at = len(ordered)
|
||||
for index, other in enumerate(ordered):
|
||||
if insert_at == len(ordered) and node.list_less_than(other):
|
||||
insert_at = index
|
||||
if other.occluded:
|
||||
continue
|
||||
if not node.overlap(other):
|
||||
continue
|
||||
if node.below(other):
|
||||
if other.occl and other.occludes(node):
|
||||
node.occluded = True
|
||||
occluded_count += 1
|
||||
break
|
||||
if insert_dependency_sorted(other.depends, node):
|
||||
dependency_count += 1
|
||||
else:
|
||||
if node.occl and node.occludes(other):
|
||||
if not other.occluded:
|
||||
other.occluded = True
|
||||
occluded_count += 1
|
||||
else:
|
||||
if insert_dependency_sorted(node.depends, other):
|
||||
dependency_count += 1
|
||||
ordered.insert(insert_at, node)
|
||||
if progress is not None and checkpoint_every > 0 and item_index % checkpoint_every == 0:
|
||||
progress(
|
||||
"sort "
|
||||
f"processed={item_index} valid={len(ordered)} occluded={occluded_count} invalid={invalid_item_count} "
|
||||
f"dependencies={dependency_count}"
|
||||
)
|
||||
|
||||
if progress is not None:
|
||||
progress(
|
||||
"sort complete "
|
||||
f"processed={len(items)} valid={len(ordered)} occluded={occluded_count} invalid={invalid_item_count} "
|
||||
f"dependencies={dependency_count}"
|
||||
)
|
||||
|
||||
return (
|
||||
min_left,
|
||||
min_top,
|
||||
max_right,
|
||||
max_bottom,
|
||||
resolve_paint_order(ordered, progress=progress, checkpoint_every=checkpoint_every),
|
||||
occluded_count,
|
||||
invalid_item_count,
|
||||
invalid_items,
|
||||
)
|
||||
121
tools/render_all_maps.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
if __package__ in (None, ""):
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from tools.crusader_map.formats import collect_render_items, load_globs, load_map_items, load_typeflags
|
||||
|
||||
|
||||
def get_map_count(fixed_dat: Path) -> int:
|
||||
data = fixed_dat.read_bytes()
|
||||
return struct.unpack_from("<H", data, 0x54)[0]
|
||||
|
||||
|
||||
def has_world_rect(extra_args: list[str]) -> bool:
|
||||
return "--world-rect" in extra_args
|
||||
|
||||
|
||||
def render_game(repo_root: Path, python_exe: str, game: str, start: int | None, end: int | None, extra_args: list[str]) -> int:
|
||||
out_dir = repo_root / "out" / game
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if game == "regret":
|
||||
static_dir = repo_root / "STATIC_REGRET"
|
||||
else:
|
||||
static_dir = repo_root / "STATIC"
|
||||
|
||||
fixed_dat = static_dir / "FIXED.DAT"
|
||||
if not fixed_dat.exists():
|
||||
print(f"Missing {fixed_dat}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
map_count = get_map_count(fixed_dat)
|
||||
shape_infos = load_typeflags(static_dir / "TYPEFLAG.DAT")
|
||||
globs = load_globs(static_dir / "GLOB.FLX")
|
||||
batch_max_items = int(os.environ.get("BATCH_MAX_ITEMS", "0"))
|
||||
world_rect_requested = has_world_rect(extra_args)
|
||||
start_index = 0 if start is None else max(0, start)
|
||||
end_index = map_count - 1 if end is None else min(end, map_count - 1)
|
||||
if start_index > end_index:
|
||||
print(f"Invalid map range {start_index}..{end_index} for {game} ({map_count} maps)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Rendering {game} maps {start_index}..{end_index} into {out_dir}")
|
||||
failed = False
|
||||
script_path = repo_root / "tools" / "render_crusader_map.py"
|
||||
for map_index in range(start_index, end_index + 1):
|
||||
print(f"[{game}] Rendering map {map_index}...")
|
||||
output_png = out_dir / f"map-{map_index}.png"
|
||||
output_json = out_dir / f"map-{map_index}.json"
|
||||
base_items = load_map_items(fixed_dat, map_index)
|
||||
render_items = collect_render_items(
|
||||
base_items,
|
||||
shape_infos,
|
||||
globs,
|
||||
include_editor=True,
|
||||
expand_globs=True,
|
||||
world_rect=None,
|
||||
include_roofs=False,
|
||||
include_hidden_markers=True,
|
||||
)
|
||||
if not render_items:
|
||||
print(f"[{game}] Skipping empty map {map_index}.")
|
||||
output_png.unlink(missing_ok=True)
|
||||
output_json.unlink(missing_ok=True)
|
||||
continue
|
||||
if batch_max_items > 0 and not world_rect_requested and len(render_items) > batch_max_items:
|
||||
print(
|
||||
f"[{game}] Skipping map {map_index}: {len(render_items)} render items exceed batch threshold {batch_max_items}. "
|
||||
"Set BATCH_MAX_ITEMS=0 to disable or use RENDER_ARGS=--world-rect ... for bounded runs.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
output_png.unlink(missing_ok=True)
|
||||
output_json.unlink(missing_ok=True)
|
||||
continue
|
||||
command = [
|
||||
python_exe,
|
||||
str(script_path),
|
||||
"--game",
|
||||
game,
|
||||
"--map",
|
||||
str(map_index),
|
||||
"--output",
|
||||
str(output_png),
|
||||
"--metadata",
|
||||
str(output_json),
|
||||
*extra_args,
|
||||
]
|
||||
result = subprocess.run(command, cwd=repo_root)
|
||||
if result.returncode != 0:
|
||||
print(f"[{game}] Map {map_index} failed.", file=sys.stderr)
|
||||
failed = True
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Render every Crusader fixed map for one or both games.")
|
||||
parser.add_argument("--game", choices=("remorse", "regret", "all"), required=True)
|
||||
parser.add_argument("--start", type=int, help="Optional starting map index.")
|
||||
parser.add_argument("--end", type=int, help="Optional ending map index.")
|
||||
args, extra_args = parser.parse_known_args()
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
python_exe = os.environ.get("PYTHON_EXE") or sys.executable
|
||||
|
||||
games = [args.game] if args.game != "all" else ["remorse", "regret"]
|
||||
exit_code = 0
|
||||
for game in games:
|
||||
exit_code |= render_game(repo_root, python_exe, game, args.start, args.end, extra_args)
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||