From 2d35e423af895f4f7433c370ca270d5ddc9ec251 Mon Sep 17 00:00:00 2001 From: chestahh Date: Sat, 10 Oct 2026 14:38:06 +0100 Subject: [PATCH 1/8] Add independent Halo 1 MCC custom-map support Extract the MCC implementation, tests, inspection tools and documentation from GulchCE main 7877f5bd2d8f12a0e025e698f46233b8254c72f8 onto OpenCE 9da2a45ffa364b720ae9faab6a1dee876926fe42. Keep OpenCE branding, updater, release workflow and existing Custom Edition implementation intact. MCC uses its own catalog, resources, converters, script extensions and save state, with additive dispatch into the native engine. Include focused asset-free CI, generated menu strings and the unchanged licensed bcdec library. Carry the decoder notice in release archives and Android assets. Raise the shared network compatibility version to 26 because MCC adds an inventory message and extra grenade action values. Existing Xbox/CE packet layouts are retained; every peer must run a matching protocol version. Validation: Windows x86 Release build; 647 synthetic/regression checks; 154 fixture-enabled checks with three unavailable-fixture skips; source isolation, generated-menu and dependency-checksum audits; packaging checks. Android rebuild unavailable locally (no NDK), Linux unavailable (no WSL). Previous precursor platform CI is documented separately from this branch. Prepared locally for review; not pushed or submitted upstream. --- .gitattributes | 3 + .github/workflows/mcc.yml | 77 + .gitignore | 3 + README.md | 6 + docs/mcc_maps.md | 460 +++++ .../menus/ce/main_menu.new_select.port.xml | 2 + port/linux/NETCODE.md | 8 + port/linux/game/mcc_audio.c | 266 +++ port/linux/game/mcc_bitmaps.c | 638 +++++++ port/linux/game/mcc_cache.c | 350 ++++ port/linux/game/mcc_cache.h | 28 + port/linux/game/mcc_cache_format.c | 305 ++++ port/linux/game/mcc_cache_format.h | 93 + port/linux/game/mcc_checkpoint.c | 190 +++ port/linux/game/mcc_checkpoint.h | 11 + port/linux/game/mcc_geometry.c | 420 +++++ port/linux/game/mcc_grenades.c | 320 ++++ port/linux/game/mcc_grenades.h | 27 + port/linux/game/mcc_hud.c | 211 +++ port/linux/game/mcc_hud_draw.c | 53 + port/linux/game/mcc_hud_draw.h | 17 + port/linux/game/mcc_main.c | 20 + port/linux/game/mcc_main.h | 4 + port/linux/game/mcc_maps.c | 282 +++ port/linux/game/mcc_maps.h | 23 + port/linux/game/mcc_network.c | 149 ++ port/linux/game/mcc_network.h | 11 + port/linux/game/mcc_network_damage.inc | 45 + port/linux/game/mcc_objects.c | 36 + port/linux/game/mcc_objects.h | 8 + port/linux/game/mcc_pause.c | 453 +++++ port/linux/game/mcc_pause.h | 45 + port/linux/game/mcc_pause_runtime.c | 207 +++ port/linux/game/mcc_pause_runtime.h | 9 + port/linux/game/mcc_player.c | 44 + port/linux/game/mcc_player.h | 9 + port/linux/game/mcc_runtime.h | 60 + port/linux/game/mcc_script_parameters.c | 192 +++ port/linux/game/mcc_script_parameters.h | 15 + port/linux/game/mcc_script_runtime.inl | 159 ++ port/linux/game/mcc_scripts.c | 203 +++ port/linux/game/mcc_scripts.h | 18 + port/linux/game/mcc_syntax.c | 153 ++ port/linux/game/mcc_syntax.h | 29 + port/linux/game/mcc_tag_validate.c | 604 +++++++ port/linux/game/mcc_tag_validate.h | 41 + port/linux/game/mcc_tags.c | 139 ++ port/linux/game/mcc_texture_cache.c | 64 + port/linux/game/mcc_texture_cache.h | 6 + port/linux/game/mcc_ui.c | 349 ++++ port/linux/game/mcc_ui.h | 36 + port/linux/game/mcc_ui_network.h | 10 + port/linux/game/mcc_ui_network.inl | 96 ++ port/linux/game/mcc_ui_teams.c | 154 ++ port/linux/game/mcc_ui_teams.h | 11 + port/linux/game/mcc_vorbis.c | 216 +++ port/linux/game/mcc_vorbis.h | 11 + port/linux/game/menu_functions.c | 278 +++ port/linux/game/menu_tags.c | 22 + port/linux/game/network_damage.c | 9 + port/linux/game/network_distributed.c | 6 + port/linux/game/network_distributed.h | 2 + port/linux/game/network_objects.c | 4 + port/linux/game/tag_validate.c | 36 + port/linux/include/halo_port_limits.h | 3 +- port/linux/src/d3d8_gl.c | 9 + port/linux/src/mcc_memory.c | 30 + port/linux/src/mcc_texture_bridge.c | 120 ++ port/linux/src/mcc_texture_bridge.h | 10 + port/linux/src/xbox_textures.c | 113 ++ port/linux/src/xgpu.h | 8 + port/third_party/bcdec/LICENSE | 52 + port/third_party/bcdec/README.md | 11 + port/third_party/bcdec/bcdec.h | 1512 +++++++++++++++++ port/tools/mcc_cache_report.c | 50 + port/tools/mcc_geometry_check.c | 160 ++ source/ai/actors.c | 3 + source/cache/cache_files.c | 74 + source/cache/cache_files_windows.c | 11 + source/cache/xbox_texture_cache.c | 15 + source/game/game_engine.c | 17 + source/game/player_queues_new.c | 6 + source/game/players.c | 4 + source/hs/hs.c | 49 + source/hs/hs_compile.c | 28 + source/hs/hs_runtime.c | 34 + source/interface/hud.c | 6 + source/interface/hud_draw.c | 13 + source/interface/hud_messaging.c | 3 + source/interface/hud_nav_points.c | 2 + source/interface/hud_unit.c | 3 + source/interface/hud_weapon.c | 8 + source/interface/player_ui.c | 10 + source/interface/ui_widget.c | 110 ++ source/interface/ui_widget.h | 4 + .../ui_widget_event_handler_functions.c | 12 + source/main/main.c | 7 + source/networking/network_server_manager.c | 7 + .../network_server_message_handler.c | 5 + source/objects/damage.c | 6 + source/objects/object_types.c | 5 + source/rasterizer/xbox/rasterizer_xbox.c | 6 + source/saved games/game_state.c | 18 + source/scenario/scenario.c | 18 + source/text/text_group.c | 13 + source/units/unit_scripting_commands.c | 3 + source/units/units.c | 32 + tools/android_build.py | 7 +- tools/ci_build.py | 2 + tools/harness/tests/mcc_network.c | 137 ++ tools/harness/tests/mcc_objects.c | 46 + tools/harness/tests/mcc_player.c | 66 + tools/harness/tests/mcc_script_parameters.c | 195 +++ tools/harness/tests/mcc_scripts.c | 259 +++ tools/harness/tests/mcc_syntax.c | 203 +++ tools/harness/tests/mcc_tag_validation.c | 295 ++++ tools/harness/tests/server_teardown.c | 4 + tools/port_settings.py | 2 +- tools/test_mcc_bitmap_flags.py | 183 ++ tools/test_mcc_cache_format.py | 128 ++ tools/test_mcc_checkpoint.py | 191 +++ tools/test_mcc_geometry.py | 150 ++ tools/test_mcc_grenades.py | 123 ++ tools/test_mcc_hud.py | 143 ++ tools/test_mcc_lifecycle.py | 255 +++ tools/test_mcc_maps.py | 353 ++++ tools/test_mcc_media.py | 506 ++++++ tools/test_mcc_menu.py | 264 +++ tools/test_mcc_network.py | 50 + tools/test_mcc_parameters.py | 20 + tools/test_mcc_pause.py | 436 +++++ tools/test_mcc_pause_runtime.py | 313 ++++ tools/test_mcc_saved_games.py | 169 ++ tools/test_mcc_scripts.py | 60 + tools/test_mcc_shader_channels.py | 156 ++ tools/test_mcc_syntax.py | 23 + tools/test_mcc_tag_validation.py | 88 + tools/test_mcc_texture_bridge.py | 221 +++ tools/test_mcc_ui.py | 637 +++++++ tools/test_mcc_ui_network.py | 208 +++ tools/test_mcc_ui_teams.py | 249 +++ tools/test_mcc_vorbis.py | 339 ++++ 142 files changed, 16843 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/mcc.yml create mode 100644 docs/mcc_maps.md create mode 100644 port/linux/game/mcc_audio.c create mode 100644 port/linux/game/mcc_bitmaps.c create mode 100644 port/linux/game/mcc_cache.c create mode 100644 port/linux/game/mcc_cache.h create mode 100644 port/linux/game/mcc_cache_format.c create mode 100644 port/linux/game/mcc_cache_format.h create mode 100644 port/linux/game/mcc_checkpoint.c create mode 100644 port/linux/game/mcc_checkpoint.h create mode 100644 port/linux/game/mcc_geometry.c create mode 100644 port/linux/game/mcc_grenades.c create mode 100644 port/linux/game/mcc_grenades.h create mode 100644 port/linux/game/mcc_hud.c create mode 100644 port/linux/game/mcc_hud_draw.c create mode 100644 port/linux/game/mcc_hud_draw.h create mode 100644 port/linux/game/mcc_main.c create mode 100644 port/linux/game/mcc_main.h create mode 100644 port/linux/game/mcc_maps.c create mode 100644 port/linux/game/mcc_maps.h create mode 100644 port/linux/game/mcc_network.c create mode 100644 port/linux/game/mcc_network.h create mode 100644 port/linux/game/mcc_network_damage.inc create mode 100644 port/linux/game/mcc_objects.c create mode 100644 port/linux/game/mcc_objects.h create mode 100644 port/linux/game/mcc_pause.c create mode 100644 port/linux/game/mcc_pause.h create mode 100644 port/linux/game/mcc_pause_runtime.c create mode 100644 port/linux/game/mcc_pause_runtime.h create mode 100644 port/linux/game/mcc_player.c create mode 100644 port/linux/game/mcc_player.h create mode 100644 port/linux/game/mcc_runtime.h create mode 100644 port/linux/game/mcc_script_parameters.c create mode 100644 port/linux/game/mcc_script_parameters.h create mode 100644 port/linux/game/mcc_script_runtime.inl create mode 100644 port/linux/game/mcc_scripts.c create mode 100644 port/linux/game/mcc_scripts.h create mode 100644 port/linux/game/mcc_syntax.c create mode 100644 port/linux/game/mcc_syntax.h create mode 100644 port/linux/game/mcc_tag_validate.c create mode 100644 port/linux/game/mcc_tag_validate.h create mode 100644 port/linux/game/mcc_tags.c create mode 100644 port/linux/game/mcc_texture_cache.c create mode 100644 port/linux/game/mcc_texture_cache.h create mode 100644 port/linux/game/mcc_ui.c create mode 100644 port/linux/game/mcc_ui.h create mode 100644 port/linux/game/mcc_ui_network.h create mode 100644 port/linux/game/mcc_ui_network.inl create mode 100644 port/linux/game/mcc_ui_teams.c create mode 100644 port/linux/game/mcc_ui_teams.h create mode 100644 port/linux/game/mcc_vorbis.c create mode 100644 port/linux/game/mcc_vorbis.h create mode 100644 port/linux/src/mcc_memory.c create mode 100644 port/linux/src/mcc_texture_bridge.c create mode 100644 port/linux/src/mcc_texture_bridge.h create mode 100644 port/third_party/bcdec/LICENSE create mode 100644 port/third_party/bcdec/README.md create mode 100644 port/third_party/bcdec/bcdec.h create mode 100644 port/tools/mcc_cache_report.c create mode 100644 port/tools/mcc_geometry_check.c create mode 100644 tools/harness/tests/mcc_network.c create mode 100644 tools/harness/tests/mcc_objects.c create mode 100644 tools/harness/tests/mcc_player.c create mode 100644 tools/harness/tests/mcc_script_parameters.c create mode 100644 tools/harness/tests/mcc_scripts.c create mode 100644 tools/harness/tests/mcc_syntax.c create mode 100644 tools/harness/tests/mcc_tag_validation.c create mode 100644 tools/test_mcc_bitmap_flags.py create mode 100644 tools/test_mcc_cache_format.py create mode 100644 tools/test_mcc_checkpoint.py create mode 100644 tools/test_mcc_geometry.py create mode 100644 tools/test_mcc_grenades.py create mode 100644 tools/test_mcc_hud.py create mode 100644 tools/test_mcc_lifecycle.py create mode 100644 tools/test_mcc_maps.py create mode 100644 tools/test_mcc_media.py create mode 100644 tools/test_mcc_menu.py create mode 100644 tools/test_mcc_network.py create mode 100644 tools/test_mcc_parameters.py create mode 100644 tools/test_mcc_pause.py create mode 100644 tools/test_mcc_pause_runtime.py create mode 100644 tools/test_mcc_saved_games.py create mode 100644 tools/test_mcc_scripts.py create mode 100644 tools/test_mcc_shader_channels.py create mode 100644 tools/test_mcc_syntax.py create mode 100644 tools/test_mcc_tag_validation.py create mode 100644 tools/test_mcc_texture_bridge.py create mode 100644 tools/test_mcc_ui.py create mode 100644 tools/test_mcc_ui_network.py create mode 100644 tools/test_mcc_ui_teams.py create mode 100644 tools/test_mcc_vorbis.py diff --git a/.gitattributes b/.gitattributes index 3773b56307..bbb518d608 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,6 @@ # Windows checkout (core.autocrlf): a CRLF shebang line does not start. *.sh text eol=lf port/android/gradlew text eol=lf + +# Preserve the pinned upstream decoder byte-for-byte, including its whitespace. +port/third_party/bcdec/bcdec.h -text -whitespace diff --git a/.github/workflows/mcc.yml b/.github/workflows/mcc.yml new file mode 100644 index 0000000000..e9123e9016 --- /dev/null +++ b/.github/workflows/mcc.yml @@ -0,0 +1,77 @@ +name: MCC map validation + +# Asset-free checks only. Real map files remain outside the repository and +# require explicit MCC_TEST_MAP/HALO_CUSTOM_EDITION_MAPS locally. +on: + push: + paths: + - "port/linux/game/**" + - "port/linux/src/**" + - "port/tools/**" + - "port/third_party/bcdec/**" + - "source/**" + - "tools/test_mcc_*.py" + - "tools/test_cache_file_formats.py" + - "tools/harness/**" + - ".github/workflows/mcc.yml" + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: mcc-validation-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + reader-and-validator: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Install native test dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang lld gcc-multilib python3-pytest + - name: Check MCC parsing, validation, codecs and existing CE parsing + run: >- + python3 -m pytest -q + tools/test_mcc_cache_format.py + tools/test_mcc_tag_validation.py + tools/test_mcc_media.py + tools/test_mcc_bitmap_flags.py + tools/test_mcc_shader_channels.py + tools/test_mcc_hud.py + tools/test_mcc_vorbis.py + tools/test_mcc_texture_bridge.py + tools/test_mcc_maps.py + tools/test_mcc_menu.py + tools/test_mcc_pause.py + tools/test_mcc_pause_runtime.py + tools/test_mcc_ui.py + tools/test_mcc_ui_teams.py + tools/test_mcc_ui_network.py + tools/test_mcc_lifecycle.py + tools/test_mcc_saved_games.py + tools/test_mcc_network.py + tools/test_mcc_checkpoint.py + tools/test_mcc_grenades.py + tools/test_cache_file_formats.py + + windows-adapters: + runs-on: windows-2025 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Install Python test runner + run: python -m pip install pytest + - name: Check native x86 MCC adapters and legacy isolation + shell: cmd + run: | + for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "MCC_VS_INSTALL=%%i" + if not defined MCC_VS_INSTALL exit /b 1 + call "%MCC_VS_INSTALL%\Common7\Tools\VsDevCmd.bat" -host_arch=x64 -arch=x86 + if errorlevel 1 exit /b 1 + clang --version + python -m pytest -q tools/test_mcc_cache_format.py tools/test_mcc_tag_validation.py tools/test_mcc_media.py tools/test_mcc_bitmap_flags.py tools/test_mcc_shader_channels.py tools/test_mcc_hud.py tools/test_mcc_vorbis.py tools/test_mcc_texture_bridge.py tools/test_mcc_geometry.py tools/test_mcc_scripts.py tools/test_mcc_parameters.py tools/test_mcc_syntax.py tools/test_mcc_maps.py tools/test_mcc_menu.py tools/test_mcc_pause.py tools/test_mcc_pause_runtime.py tools/test_mcc_ui.py tools/test_mcc_ui_network.py tools/test_mcc_ui_teams.py tools/test_mcc_lifecycle.py tools/test_mcc_saved_games.py tools/test_mcc_network.py tools/test_mcc_checkpoint.py tools/test_mcc_grenades.py tools/test_cache_file_formats.py diff --git a/.gitignore b/.gitignore index e04521b7d5..60270e7d47 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ original/ # CI build outputs (tools/ci_build.py) dist/ + +# User-supplied Halo 1 MCC maps and resources +mcc_maps/ diff --git a/README.md b/README.md index 59ba93b670..6c0b964332 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,12 @@ so players of the two versions can play together. 2. At the first start, the game asks for the disc image. Select it. 3. The game extracts the `maps/` folder. Then the game starts. +Halo 1 MCC custom maps use a separate `mcc_maps/` directory and the +**MCC SINGLEPLAYER** and **MCC MULTIPLAYER** map categories. The experimental +adapter supports modern, self-contained version-13 maps; it does not need +Custom Edition's shared resource files. See [MCC map support](docs/mcc_maps.md) +for supported features, installation and compatibility limits. + On Linux and Windows, the game puts `maps/` next to the executable. On Android, copy the disc image to the phone first. The app puts `maps/` in its data folder. Refer to [port/android/README.md](port/android/README.md). diff --git a/docs/mcc_maps.md b/docs/mcc_maps.md new file mode 100644 index 0000000000..82ec3b40d4 --- /dev/null +++ b/docs/mcc_maps.md @@ -0,0 +1,460 @@ +# Halo 1 MCC custom maps + +This implementation adds an independent loader for modern Halo 1 MCC version-13 +caches and **MCC SINGLEPLAYER** / **MCC MULTIPLAYER** choices in the map +menus. It is an experimental +runtime adapter, with explicit format limits below. A successful menu +probe or file audit is not proof that a map plays correctly. + +## Installation and menus + +Put the map in the game data root's `mcc_maps` directory, beside `maps` and +`custom_maps`. For example: + +```text +maps/ Xbox game data +custom_maps/ Halo Custom Edition data +mcc_maps/a10.map Halo 1 MCC custom map +mcc_maps/a10.txt Optional plain-text description +``` + +The level identity is `mcc_maps\a10`. The filename is retained even when +the internal scenario name differs. The supplied Mercury Rising file is +an example: its filename is `a10.map`, while its header names +`mercury_falling`. The namespaces prevent it replacing either Xbox `a10` +or Custom Edition `custom_maps\a10`. + +Open the map-kind chooser and select **MCC SINGLEPLAYER** for campaign +maps or **MCC MULTIPLAYER** for multiplayer maps. The cache header's scenario +type determines the category, independently of the filename; both lists +read the same `mcc_maps` folder. New Game's campaign maps proceed to difficulty +selection, and multiplayer maps can be explored alone. The network host +map screen routes campaign maps to cooperative setup and multiplayer maps +to gametypes. Split-screen multiplayer skips **MCC SINGLEPLAYER**, just as +it skips the existing singleplayer categories. These +are menu routes in the implementation; multiplayer interoperability and +complete campaigns require gameplay testing. +Each selected MCC campaign map is treated as an independent scenario. +Automatic progression through a package's campaign map sequence is not +implemented; the MCC package manifest is not interpreted. + +MCC checkpoints retain the canonical `mcc_maps\` scenario +identity, including when the original tag names an Xbox campaign level. +Continue and Load Game have MCC-specific routes with separate saved-name +storage; they do not turn an MCC checkpoint into a stock campaign level. + +The catalog scans only `mcc_maps`, reads each candidate's 2,048-byte header, +and refreshes when a map menu opens. It accepts at most 1,024 entries, with +filenames of at most 54 characters before `.map`. A description is optional; +the default identifies the scenario type. MCC thumbnail sidecars, Workshop +downloads and MCC package manifests are not implemented. Supported maps +contain their own resources; do not copy `bitmaps.map`, +`sounds.map` or `loc.map` from Custom Edition into `mcc_maps`. +There is no dependency on the Custom Edition setting or its resource +directories. + +The loader opens source maps read-only. A rejected map reports the failing +stage in `debug.txt` and returns a menu error. Matching the header checksum +can detect that a network host selected a different map version; it is not +a recomputation or authentication of the map's contents. Network support +uses OpenCE's game protocol, not the MCC game client or its matchmaking. +This contribution uses network protocol 26 for the additional MCC inventory +message and grenade action values. Every participant needs a matching build +and map data; older builds are rejected even when playing non-MCC maps. + +### In-game pause menus + +MCC maps use a generated, stock-style pause menu for campaign, cooperative +and multiplayer games. The full, half and quarter viewport layouts follow +the local player arrangement. They use separate MCC-owned widgets, strings, +panels and selection highlights; the default pause route does not open the +map's embedded pause or Settings widgets. Loaded font tags are referenced +without changing their contents. + +| Context | Available actions | +| --- | --- | +| Local campaign | Resume Game, Revert to Saved, Restart Level, Save and Quit. | +| Cooperative host | Resume Game, Revert to Saved, Restart Level, Leave Game. | +| Cooperative client | Resume and Leave Game. | +| Local multiplayer | Resume, Restart Game and Leave Game. | +| Multiplayer host | Resume, Restart Game, End Game and Leave Game; Choose Team in team games. | +| Multiplayer client | Resume and Leave Game; Choose Team in team games. | + +Multiplayer additionally offers **Settings** when `display.menus=pc` and +there is one local player. It opens the trusted native player Settings +screen for the initiating controller. Split-screen players use Settings +from the main menu, since the native editor needs a full viewport. +Campaign and cooperative pause menus do not offer Settings. Restart, +revert and leaving/end-game actions use confirmation +dialogs with Cancel selected initially. B returns from a confirmation to +the pause screen; Start closes the current player's menu and its navigation +history. At the main pause screen, either resumes play. Resume and team +selection also close only the initiating player's menu, preserving other +local players' screens. Each pause layout uses the native A/B button-icon +tokens in its footer, drawing the map's HUD button glyphs beside Select/Back. +Only local campaign pauses +the simulation; network games continue while a menu is open. + +Save and Quit preserves the last valid local campaign checkpoint. Revert +requires a valid checkpoint, and cooperative restart/revert remain +host-controlled. A network restart retains the selected map, variant, +options and players through the native lobby/countdown route. End Game is +host-only and ends the round. Team changes remain subject to authenticated +player identity, host authority and balance checks. + +The MCC Settings route uses the initiating controller's profile and widget +history. It does not replace another active profile editor. Its ownership +ends on native Save/Cancel, menu closure, replacement editing or map unload, +so a later native editor cannot be closed by stale MCC state. + +Campaign layouts display the scenario's current mission objectives through +the native objective-text callback. Owned objective and confirmation text +boxes wrap their instance copies using the current font's metrics and clip +to their bounds. The map's objective data, custom HUD +and embedded widgets remain present. Scripts that explicitly open embedded +widgets can still use the MCC callback adapter described below. The new +pause labels and confirmation text are currently English; localization of +these generated strings is not implemented. + +## Repository architecture and isolation + +The existing OpenCE engine remains authoritative. `source/` contains the reconstructed +Xbox game systems, including scenario/tag access, cache I/O, the script +interpreter, rendering, audio, objects, game variants and UI. The native +platform layers under `port/linux/`, `port/windows/` and `port/android/` +provide the host services used by those systems. The desktop renderer +implements the game's Xbox interfaces using the existing native graphics +backend. Menu XML and generated settings define the native map chooser. +The `tools/` tests and native harnesses exercise both portable utilities and +selected game subsystems. + +Existing Custom Edition support has its own catalog, cache reader, +conversion, resource and behavior modules. Those modules and their state +are not an MCC implementation dependency. The independent components are: + +| Component | Responsibility | +| --- | --- | +| `port/linux/game/mcc_maps.c` | Catalog, display identities, names and descriptions. | +| `port/linux/game/mcc_cache_format.c` | Portable little-endian version-13 reader and file-range audit. | +| `port/linux/game/mcc_cache.c` | MCC path admission, read-only file lifetime, conversion and streaming dispatch. | +| `port/linux/game/mcc_main.c` | MCC load-failure recovery through the native main-menu lifecycle. | +| `port/linux/game/mcc_ui.c` and `mcc_ui_network.inl` | MCC in-map widget actions, settings routing, checkpoint/quit behavior and synchronized network restarts. | +| `port/linux/game/mcc_pause.c` and `mcc_pause_runtime.c` | Generated stock-style pause layouts, objective display, independent UI art and atomic publication/lifetime of the appended tags. | +| `port/linux/game/mcc_ui_teams.c` | Authenticated team-only requests during MCC multiplayer rounds, with host balance checks and normal respawn replication. | +| `port/linux/src/mcc_memory.c` | Separate 64 MiB linked tag window; never replaces an existing allocation. | +| `port/linux/game/mcc_geometry.c` | New model descriptors, model vertex/index conversion, external BSP vertex streams and node palettes. | +| `port/linux/game/mcc_audio.c` | MCC sound decoding, resampling and Xbox ADPCM encoding into an MCC virtual stream. | +| `port/linux/game/mcc_vorbis.c` | Private namespaced Vorbis decoder, bounded Ogg validation and empty residue vector compatibility. | +| `port/linux/game/mcc_bitmaps.c` | MCC pixel layout, BC7 decoding and shader/HUD channel normalization into an MCC virtual stream. | +| `port/linux/game/mcc_texture_cache.c` and `port/linux/src/mcc_texture_bridge.c` | Direct bindings of checked MCC pixel allocations and lazy GPU textures, independent of the Xbox/CE staging cache. | +| `port/linux/game/mcc_tags.c` | MCC metadata, shader-type, HUD placement and widget normalization. | +| `port/linux/game/mcc_hud.c` | MCC bitmap/placement scaling, including nested weapon and grenade HUD items. | +| `port/linux/game/mcc_hud_draw.c` | MCC canvas, glyph geometry and text advances; neutral dispatch for Xbox/CE. | +| `port/linux/game/mcc_scripts.c` | Name-based MCC function/global linking and supported MCC native functions. | +| `port/linux/game/mcc_syntax.c` | MCC's 32,767-slot syntax validation and traversal workspace. | +| `port/linux/game/mcc_script_parameters.c` and `mcc_script_runtime.inl` | MCC parameter metadata/scopes and interpreter frames inside the existing HS stack. | +| `port/linux/game/mcc_objects.c` | MCC multiplayer vehicle placement masks. | +| `port/linux/game/mcc_grenades.c` | MCC slots 2/3, salted unit inventories, pickup/throw/drop behavior and HUD. | +| `port/linux/game/mcc_player.c` | MCC grenade selection/action checks and starting-profile counts. | +| `port/linux/game/mcc_network.c` | MCC-only host inventory messages and client reconciliation. | +| `port/linux/game/mcc_checkpoint.c` | MCC inventory snapshot inside proven-unused CPU save memory. | +| `port/linux/game/mcc_tag_validate.c` | Independent range/ownership state while applying stock tag schemas and their validators. | + +Shared engine entry points have additive MCC dispatch for an MCC-namespaced +level or active MCC state. They route cache lifetime, raw resources, BSPs, +script extensions, validation callbacks, menu text and model palettes to +the new modules. For other levels those MCC checks do not select a loader. +The new code does not call the Custom Edition parser, converter, allocator, +script adapter, bitmap converter, sound encoder or behavior table. MCC +conversion writes only MCC-owned allocations, and unload releases them. + +The pause builder appends its definitions and art to a copy of the live +tag index only after construction succeeds. It preserves the existing tag +records and restores the original index on unload. Its action IDs use the +separate `0x7000` range and require exact ownership of a generated widget; +an embedded map widget cannot acquire those permissions by copying an ID. +Allocation or graphics-resource failure releases the partial construction +without publishing it. Xbox and Custom Edition pause selection remains on +the existing route. + +Maps with two through four grenade definitions are supported. The original +unit datum and player-action layouts are retained; the extra two counts +live in a separately owned table keyed by full salted unit handles. +Selection remains in the native unit fields. MCC profile, input, HUD, +pickup, throwing and inventory-network routes handle these slots. + +MCC saves put the additional counts into a 65,640-byte slot at the CPU +arena's end only if the allocator's current high-water mark leaves that +entire slot unused. No `game_state_malloc` call, pool capacity, native +allocation checksum or save-image size changes. Native checkpoint, core +and persistent writers already serialize the complete CPU/GPU arena; +the persistent image checksum therefore includes this snapshot. An MCC +footer adds version, canonical map identity, cache checksum and payload +CRC32. Before accepting the image, the adapter checks the payload and +remaps incoming object-header pointers to verify every salted unit record. +Unload restores the original unused bytes, including nonzero contents. + +This is an independently written implementation of documented wire formats, +informed by reading the existing engine's public interfaces and schemas. +No Custom Edition implementation was copied or renamed. Generic engine +operations, including vertex compression and GPU buffer construction, are +shared. MCC compiles a private namespaced instance of the unchanged generic +`stb_vorbis` library to decode Ogg; Xbox/CE retain their existing instance. The +MIT-licensed `port/third_party/bcdec` dependency decodes BC7 and DXT blocks; +its accompanying license and provenance are retained. This statement does +not claim the legal two-team meaning of formal clean-room reverse +engineering. + +## Wire format and conversions + +The reader admits little-endian `head`/`foot` caches with version 13 and +singleplayer or multiplayer scenario type. Tag memory uses the base +inferred from the tag-array pointer minus the 40-byte MCC tag header. +Each index entry is 32 bytes. Linked pointers are checked against the +loaded data before access. File and multiplication ranges use subtraction +or wider arithmetic to avoid wrapping. + +Models use the MCC `mod2` part layout and uncompressed 68-byte vertices. +The adapter creates new 104-byte Xbox part descriptors, compresses vertices +to 32 bytes and uploads the index strips. Local node indices are mapped to +global nodes when possible; models requiring more engine palette slots +use an MCC-owned palette per part. No CE palette storage is consulted. + +Version-13 BSP vertex data is external to the BSP tag allocation but can be +embedded in the same map file. The adapter follows the BSP header's vertex +byte count and file offset, reads the material's 56-byte environment and +20-byte lightmap streams, and compresses them into adjacent 32-byte and +8-byte streams. The converted streams have a separate tracked lifetime; +BSP changes release only the previous BSP's buffers. + +Bitmap conversion supports the common Halo pixel formats, including P8, +DXT1/3/5 and MCC's BC7 format 18. BC7 is decoded to ARGB8888 for the existing +renderer. Pixel rows, Morton ordering and cube/mip layout are packed by +the independent MCC adapter. The MCC environment bitmap flag is metadata, so it is cleared +when constructing the Xbox descriptor. Compressed and palette flags are +derived from the resulting format. Model flag `0x40` and HUD meter flag +`0x20` identify resources already in Xbox channel order. Those channels +are preserved; references with the flags clear receive Gearbox-to-Xbox +channel conversion. A bitmap used by consumers +that require different channel layouts gets separate MCC-owned tag and +pixel copies, with the relevant consumer's reference redirected. The tag +index can grow into a separate checked allocation for those copies. + +MCC HUD placements use a 960p canvas, converted to the native 480p canvas +in MCC-owned tags. Bitmap half-scale flags are a separate factor, so high +resolution images retain their original texels. Anchors, number advances, +waypoint/damage margins and direct icon geometry are normalized; the +motion-sensor radius and font-relative icon offsets/advances already use +native units and are retained. Five native +HUD/UI files contain additive dispatch only: every pre-existing statement +is retained. Xbox/CE dispatch is neutral before any bitmap lookup or draw. +The pause A/B glyphs and pickup icons use their own bitmap flags, with +matching cursor advances so text remains beside the correctly sized icons. +Pause glyphs align to the active UI font's capitals instead of inheriting +a map's custom HUD icon baseline. + +MCC textures bind their immutable converted allocations directly through an +MCC-owned hardware-header registry. Exact registered header identity grants +access to the pixels; a copied descriptor or arbitrary physical-address word +does not. GPU textures upload lazily and remain owned until MCC unload, +which unbinds and deletes them before freeing their source pixels. This +avoids another pixel copy and allocation in the Xbox/CE texture staging +cache, whose capacity and policy remain unchanged. BC7 decoding retains +the full decoded texels; there is no additional lossy recompression or +resolution reduction. Host memory and GPU texture capacity still bound +the maps the machine can render. The same owned pixels remain available +to CPU object-lighting samples. Two-dimensional DXT resources preserve +their complete 2-by-2 and 1-by-1 source mip blocks for those samples, while +the GPU descriptor retains its native mip limit. Unload clears only CPU +base pointers that still name the MCC-owned allocation. + +The audio adapter accepts embedded PCM16, Xbox ADPCM and Ogg Vorbis +permutations. It uses independent bounds checks and codec state. Mono +output uses the game's 22,050 Hz format; stereo output follows the admitted +22,050/44,100 Hz tag rate. Necessary conversions produce an MCC-only ADPCM +stream. Unsupported or damaged audio rejects the map instead of silently +removing sound. + +Compiled script function and engine-global indices are linked by the +names retained in the script strings. The adapter provides +`objects_distance_to_object` and `mcc_mission_segment`, and maps +`player_effect_set_max_vibrate` to the engine's rumble operation. +Other unresolved names reject the map. `mcc_mission_segment` evaluates its +string argument normally, records a bounded local diagnostic event and +returns true for a supplied string. This acknowledges the event locally; +it does not implement the MCC telemetry service. The boolean result retains +the conditional `sleep` calls used in shipped and Ruby campaign scripts. +It does not call `core_save_name`: that Xbox function writes a raw debug +save, and remains unchanged and unavailable to map scripts. +Compiled MCC static/stub scripts support up to 16 typed parameters. +Signatures, scoped local references and call arguments are checked before +loading. The MCC interpreter extension keeps arguments, mutable local +values and the evaluation cursor in the existing thread stack; object-list +references survive sleeps and release on return or thread termination. +Nested and recursive calls retain separate scopes. The existing 512-byte +stack remains the recursion/depth bound. Dynamic console calls with +parameters are not supported by this extension; the cached map's calls +are the supported path. MCC syntax now retains its full 32,767-slot arena, +including unused slots for dynamic console expressions. This is the Halo 1 +MCC format's signed 16-bit capacity, not 65,535. MCC's independent validator +checks the complete node span, occupied count, salted references and cycles +before the interpreter follows links. Its traversal and parameter workspaces +cover the full arena. Three additive dispatches in `hs.c` select MCC's +validation/allocation-admission path; Xbox and Custom Edition keep their +original 19,001-slot constants, validation storage and statements. +Syntax remains in MCC-owned tag memory, with unchanged datum handles and +thread/checkpoint layouts. No shared data-array structure or allocator changes. +The MCC tag walker also retains the full syntax byte span during its later +schema pass; the shared Xbox/CE schema still specifies 19,001 slots. + +Ruby's Rebalanced was audited across all ten campaign maps: each reserves +32,767 slots. `a10` has a high-water count of 19,723, including 18,480 occupied +nodes; the other nine high-water counts are below 19,001. Its existing holes +are retained rather than compacting or renumbering the graph. Across these +maps, the only unsupported native name was `mcc_mission_segment` (225 calls, +42 in boolean contexts and 183 with a discarded return value). + +Native Ruby loading exposed two additional MCC bitmap layouts. RGB565 +lightmaps with flags `0x1281` are normalized only when their owning group is +a lightmap, they have no mipmaps, and the pixel range is complete and tightly +packed. Bit 12's broader meaning is not inferred. The existing descriptor +verifier still checks the normalized result. Four maps also contain a Wraith +HUD DXT1 texture whose tiny mip tail is sized using unrounded pixel counts. +For that exact full-chain size pattern, the converter retains every complete +mip level and logs the omitted tail; it never reads beyond the declared range. +Arbitrary truncation, missing base images and other unknown flags still fail. + +Ruby's `sound\\music\\spooky1\\in` (including `b30` tag #3583) uses an +empty residue vector codebook. The generic decoder aborted the rest of the +residue after encountering it, producing an error and incorrect PCM. MCC's +private decoder normalizes only empty additive residue vector references +before the first overlap packet, matching Xiph's zero-contribution behavior. +Empty scalar floor/classification books are not covered by that rule. +Ogg checksums, page continuity, EOS, sample count and remaining decoder errors +are still checked. Decoder buffers return to their own allocator, independently +of the game's debug allocator and the later ADPCM format conversion. + +An independent PCM comparison across all ten Ruby maps covered 50,186 Vorbis +references and 8,325 distinct streams: all decoded successfully and every +sample differed from libsndfile/libvorbis by at most one signed-16-bit unit. +These codec checks do not establish a complete campaign playthrough. + +## Current boundaries + +- Modern uncompressed version-13 maps are the target. Earlier Anniversary + chunk-compressed caches, Halo 2 or later MCC maps, external indexed tags, + and shared external bitmap/sound resource files are not supported. +- The portable inspector accepts files up to `INT32_MAX`; runtime raw files + must be smaller than `0x50000000` bytes because higher offsets identify + MCC virtual resources. Runtime tag windows must be free, 64 KiB-aligned, + 64 MiB allocations based between `0x42000000` and `0x70000000` inclusive. + The observed MCC base is `0x50000000`. +- There are at most 65,535 tags and 32 BSP references. Runtime conversion + descriptors must fit below the lowest BSP in the tag window. Geometry + currently admits at most 64 nodes per model, 22 palette nodes per part, + 256 geometries per model and 128 parts per geometry. Each part's vertex + and triangle counts must be nonzero and at most 65,535. +- Non-volume bitmap dimensions are limited to 4,096 by 4,096 with depth one; + volumes are limited to 512 by 512 by 256. There are at most 12 + mip levels after the base level. Individual raw/decoded allocations are + capped at 128 MiB; the normalized bitmap stream is smaller than 512 MiB. + Linear textures must be two-dimensional, unmipped and uncompressed. + Audio permutations are capped at 64 MiB of input and 16,777,216 decoded + frames, with at most 256 MiB of converted audio. +- Reusing stock schemas and gameplay/render systems does not implement + every MCC engine extension. MCC-only object behaviors, new shader + semantics and UI callbacks require separate + semantic work and representative maps. Embedded MCC widget event bytes + are preserved: compatible callback IDs use the existing engine functions, + while identified MCC-specific actions dispatch through the MCC adapter. + This remains relevant to script-opened custom interfaces; the default + pause menus use the separately generated definitions above. Unsupported + embedded callbacks fail without performing the event's close/open actions. + Native Settings is available only in MCC multiplayer with + `display.menus=pc` and one local player. + HUD placement normalization and overlay handling + still require visual comparison against the map's intended appearance. +- No claim is made here of complete campaign/gameplay fidelity or + synchronization under all conditions, performance parity, or tested + Android/Linux runtime behavior. See the bounded test status below. + +## Validation and contribution scope + +The contribution includes the MCC runtime modules, additive engine/menu +connections, the menu generator and its generated map-category strings, +the licensed texture decoder, and focused tests and inspection tools. +The MCC validation workflow runs synthetic-data checks without downloading +game assets. Existing build/release workflows and updater settings are not +replaced. No map, commercial font, save, binary or local playtest directory +is part of the contribution. + +The compatibility layer is independently written, with the generic library +reuse described above. Research included public schemas and documentation, +existing OpenCE interfaces, and inspection of user-supplied maps. Shipped MCC +campaign scripts were consulted to understand the mission-segment call's +behavior (linked below); this is not a claim of formal clean-room certification +or a legal conclusion about the project's distribution. + +The following checks were performed during development of the implementation: + +| Area | Evidence and limits | +| --- | --- | +| Map formats | Mercury Rising and Nitra v2 pass parser and geometry checks. All ten Ruby's Rebalanced campaign caches were inspected for script capacity and audio compatibility. | +| Audio | 50,186 Vorbis references / 8,325 distinct streams from Ruby's ten maps decoded; PCM differed from the independent libvorbis comparison by at most one signed-16-bit sample unit. This does not prove an entire campaign plays correctly. | +| Windows gameplay | Bounded rendered loads, pause actions, checkpoint/core restoration, MCC-to-Xbox-to-CE-to-MCC transitions, Nitra multiplayer hits, and two-instance Mercury network co-op were exercised. These are not full campaign playthroughs or broad internet/cross-platform tests. | +| Platform builds | Windows, Linux and Android Debug/Release builds of the precursor implementation passed CI. Linux/Android gameplay was not established, and those runs predate this contribution's protocol-version and packaging changes. | +| Regression coverage | Generated cache data, malformed resources, codecs, HUD/shader conversions, menus, saved games, scripting, network messages and MCC unload/isolation have focused tests. Real-map checks are opt-in. | + +Use a C compiler and Python/pytest for the tests below. The Windows CI job +sets up an x86 MSVC environment with Clang; Linux needs Clang and a 32-bit +C runtime (`gcc-multilib`). The existing `tools/harness` suite also checks +legacy engine behavior. A passing parser or build alone does not establish +map playability. + +Run the isolated tests with a C compiler and Python/pytest available: + +```text +python -m pytest -q tools/test_mcc_cache_format.py +python -m pytest -q tools/test_mcc_geometry.py +python -m pytest -q tools/test_mcc_media.py tools/test_mcc_tag_validation.py +python -m pytest -q tools/test_mcc_hud.py tools/test_mcc_shader_channels.py tools/test_mcc_vorbis.py +python -m pytest -q tools/test_mcc_texture_bridge.py +python -m pytest -q tools/test_mcc_ui.py tools/test_mcc_ui_teams.py tools/test_mcc_ui_network.py +python -m pytest -q tools/test_mcc_pause.py tools/test_mcc_pause_runtime.py +python -m pytest -q tools/test_mcc_maps.py +python -m pytest -q tools/test_mcc_menu.py +python -m pytest -q tools/test_mcc_saved_games.py +python -m pytest -q tools/test_mcc_checkpoint.py tools/test_mcc_grenades.py tools/test_mcc_network.py +python -m pytest -q tools/test_mcc_parameters.py tools/test_mcc_scripts.py tools/test_mcc_syntax.py +python -m pytest -q tools/test_mcc_lifecycle.py +``` + +Set `MCC_TEST_MAP` to an existing map path to opt into the real-fixture +checks. The parser tests are portable; the geometry harness currently +requires the x86 Windows game headers/toolchain and skips elsewhere. +`port/tools/mcc_cache_report.c` builds with `mcc_cache_format.c` and only +the C standard library, so files can also be audited outside the game. +None of these commands downloads or bundles a game map. + +## Public format evidence + +These are wire-format references, not copied implementation code. Current +version-13 fields were checked against the supplied file where available; +older CEA documentation sometimes describes different compression and +memory limits and must not be applied indiscriminately. + +| Fact | Primary source / observed check | +| --- | --- | +| Version 13, 40-byte PC tag header, 64 MiB tag capacity | [Invader map definitions](https://github.com/SnowyMouse/invader/blob/master/include/invader/hek/map.hpp), [engine configuration](https://github.com/SnowyMouse/invader/blob/master/src/hek/map.cpp); fixture tag pointer `0x50000028`. | +| Infer linked base from tag array; current v13 is uncompressed | [Invader map reader](https://github.com/SnowyMouse/invader/blob/master/src/map/map.cpp); header and index read directly from fixture. | +| MCC BSP external environment/lightmap vertex streams | [Invader BSP compiler](https://github.com/SnowyMouse/invader/blob/master/src/tag/parser/compile/scenario_structure_bsp.cpp), [build layout](https://github.com/SnowyMouse/invader/blob/master/src/build/build_workload.cpp), [BSP schema](https://github.com/SnowyMouse/invader/blob/master/src/tag/hek/definition/scenario_structure_bsp.json); fixture header has `0x2B5D0C` bytes at file offset `0x800`. | +| BC7 format 18, environment flag bit 9 and external resource flag bit 8 | [Invader bitmap schema](https://github.com/SnowyMouse/invader/blob/master/src/tag/hek/definition/bitmap.json); fixture contains 38 BC7 records and environment-flagged bitmaps. | +| MCC script parameters, expanded syntax capacity | [Invader scenario schema](https://github.com/SnowyMouse/invader/blob/master/src/tag/hek/definition/scenario.json), [MCC editing kit change documentation](https://c20.reclaimers.net/h1/h1-ek/); fixture syntax is `56 + 32767 * 20` bytes. | +| MCC Halo 1 syntax limit is 32,767 | [Invader engine configuration](https://github.com/SnowyMouse/invader/blob/696830ff80af227e84e7237c2ef26eb2301ed110/src/hek/map.cpp#L94) uses `INT16_MAX`; all ten Ruby map headers reserve exactly this count. | +| Mission-segment boolean/string contract | [Sapien's generated function documentation](https://github.com/Sigmmma/c20/blob/master/src/data/hs_docs/h1/hs_doc_sapien.txt) and [shipped MCC c10 scripts](https://github.com/NervyDestroyer/Halo-MCC-Scripts/blob/main/H1/levels/c10/scripts/mission_c10.hsc); Ruby's 225 calls also take one string, with boolean conditions used before `sleep`. | +| Parameter wire records and local references | [Scenario schema](https://github.com/SnowyMouse/invader/blob/master/src/tag/hek/definition/scenario.json) defines 36-byte records, maximum 16 and local-variable flag bit 4; [public script compiler](https://github.com/SnowyMouse/invader/blob/master/src/tag/parser/compile/scenario/pre_compile.cpp) confirms primitive/global/local flags and the parameter slot in node data. | +| Sound compression formats and cached sound data | [Invader sound compiler](https://github.com/SnowyMouse/invader/blob/master/src/tag/parser/compile/sound.cpp), existing Xbox sound definitions; fixture samples and tag fields checked directly. | +| MCC HUD canvas and independent bitmap half-scale | [Restored tagset author's format notes](https://github.com/Aerocatia/halopc-restored#hud-scale-is-still-480p); Ruby shield offsets and number advances are exactly twice their stock Xbox counterparts. | +| Xbox-order model and HUD meter flags | [Invader model shader schema](https://github.com/SnowyMouse/invader/blob/master/src/tag/hek/definition/shader_model.json), [HUD meter schema](https://github.com/SnowyMouse/invader/blob/master/src/tag/hek/definition/hud_interface_types.json); Ruby and Mercury contain mixed flag states. | +| Empty Vorbis residue vectors contribute zero | [Xiph Vorbis 1.3.7 codebook implementation](https://github.com/xiph/vorbis/blob/v1.3.7/lib/codebook.c), `vorbis_book_decodevs_add`, `vorbis_book_decodev_add`, `vorbis_book_decodevv_add`; synthetic multi-pass streams and independent Ruby PCM comparison. | +| Header and historical format differences | [Reclaimers map documentation](https://c20.reclaimers.net/h1/maps/), [SnowyMouse CEA format research](https://gist.github.com/SnowyMouse/39168bddd597549038a35d78aee39513); historical chunk compression is outside this implementation's scope. | diff --git a/port/assets/menus/ce/main_menu.new_select.port.xml b/port/assets/menus/ce/main_menu.new_select.port.xml index 006ab4bbd2..58ec627a49 100644 --- a/port/assets/menus/ce/main_menu.new_select.port.xml +++ b/port/assets/menus/ce/main_menu.new_select.port.xml @@ -12,5 +12,7 @@ + + diff --git a/port/linux/NETCODE.md b/port/linux/NETCODE.md index 890a1e4fce..4defe212d7 100644 --- a/port/linux/NETCODE.md +++ b/port/linux/NETCODE.md @@ -180,6 +180,14 @@ sends each killing blow again reliably and an object come to rest three times (a client waits for a player's blow before its body dies without one), and switches co-op's BSP on the host's crossing alone. +Version 26 adds MCC custom-map support, including a separate message for +the extra grenade inventories and the corresponding grenade action values. +All participants need the same protocol version, including on Xbox or +Custom Edition maps; their existing inventory packet layouts are unchanged. +MCC sessions use this netcode and the existing host-controlled co-op systems, +not the official MCC client or matchmaking. See [MCC map support](../../docs/mcc_maps.md) +for map requirements and the bounds of gameplay testing. + A client plays by its host's rules: in another's game (searching for it, in its lobby, or playing it) the developer console, the telnet console and the cheat buttons run only commands that change nothing of the game diff --git a/port/linux/game/mcc_audio.c b/port/linux/game/mcc_audio.c new file mode 100644 index 0000000000..4ccdd5fe6c --- /dev/null +++ b/port/linux/game/mcc_audio.c @@ -0,0 +1,266 @@ +/* MCC audio normalization. An MCC-owned Vorbis adapter handles codec differences; + * no Custom Edition state or sound conversion routine is used. A damaged or + * unsupported permutation rejects the map, rather than quietly silencing it. */ +#include "cseries.h" +#include "errors.h" +#include "sound/sound_definitions.h" +#include "mcc_runtime.h" +#include "mcc_vorbis.h" +#include +#include + +#define MCC_AUDIO_BASE 0x50000000u +#define MCC_AUDIO_LIMIT 0x10000000u +#define MCC_AUDIO_FRAME_LIMIT 0x1000000u + +struct mcc_audio_storage { unsigned char *bytes; uint32_t used, capacity; }; +struct mcc_pcm { short *samples; uint32_t frames, rate; int channels, vorbis_owned; }; +struct mcc_ima { int value, step; }; + +/* The IMA ADPCM quantizer tables are part of the public codec format. */ +static int const mcc_ima_steps[89] = { + 7,8,9,10,11,12,13,14,16,17,19,21,23,25,28,31,34,37,41,45,50,55,60,66,73, + 80,88,97,107,118,130,143,157,173,190,209,230,253,279,307,337,371,408,449, + 494,544,598,658,724,796,876,963,1060,1166,1282,1411,1552,1707,1878,2066, + 2272,2499,2749,3024,3327,3660,4026,4428,4871,5358,5894,6484,7132,7845, + 8630,9493,10442,11487,12635,13899,15289,16818,18500,20350,22385,24623, + 27086,29794,32767 +}; +static int const mcc_ima_changes[8] = {-1,-1,-1,-1,2,4,6,8}; + +static int mcc_ima_advance(struct mcc_ima *state, unsigned code) +{ + int quantizer=mcc_ima_steps[state->step]; + int delta=quantizer/8; + if (code&1) delta+=quantizer/4; + if (code&2) delta+=quantizer/2; + if (code&4) delta+=quantizer; + state->value+=(code&8) ? -delta : delta; + state->value=PIN(state->value,-32768,32767); + state->step=PIN(state->step+mcc_ima_changes[code&7],0,88); + return state->value; +} + +static unsigned mcc_ima_quantize(struct mcc_ima *state, int sample) +{ + int error=sample-state->value, divisor=mcc_ima_steps[state->step]; + unsigned code=error<0 ? 8 : 0, bit; + if (error<0) error=-error; + for (bit=4; bit; bit>>=1,divisor>>=1) { + if (error>=divisor) {code|=bit; error-=divisor;} + } + mcc_ima_advance(state,code); + return code; +} + +static int mcc_pcm_open(unsigned char const *bytes, uint32_t size, int compression, + int channels, uint32_t rate, struct mcc_pcm *pcm) +{ + uint32_t i; + memset(pcm,0,sizeof(*pcm)); + pcm->channels=channels; pcm->rate=rate; + if (compression==3) { + pcm->vorbis_owned=1; + return mcc_vorbis_decode(bytes,size,MCC_AUDIO_FRAME_LIMIT, + &pcm->samples,&pcm->frames,&pcm->rate,&pcm->channels); + } + if (compression==0) { + if (size%(2*channels)) return 0; + pcm->frames=size/(2*channels); + } else if (compression==1) { + if (size%(36*channels)) return 0; + pcm->frames=size/(36*channels)*64; + } else return 0; + if (!pcm->frames || pcm->frames>MCC_AUDIO_FRAME_LIMIT) return 0; + pcm->samples=malloc(pcm->frames*channels*sizeof(short)); + if (!pcm->samples) return 0; + if (!compression) { + for (i=0;iframes*(uint32_t)channels;i++) + pcm->samples[i]=(short)(bytes[i*2]|bytes[i*2+1]<<8); + } else { + for (i=0;iframes/64;i++) { + int channel; + unsigned char const *block=bytes+i*36*channels; + for (channel=0;channel88 || block[channel*4+3]) return 0; + for (frame=0;frame<64;frame++) { + unsigned char packed=block[channels*4+(frame/8*channels+channel)*4+frame%8/2]; + unsigned nibble=(packed>>(frame%2*4))&15; + pcm->samples[(i*64+frame)*channels+channel]=(short)mcc_ima_advance(&state,nibble); + } + } + } + } + return 1; +} + +static void mcc_pcm_close(struct mcc_pcm *pcm) +{ + /* mcc_vorbis uses the C runtime allocator; cseries remaps this module's free + * to debug_free. Preserve ownership even after the permutation is converted + * to ADPCM, so each allocation returns to the allocator that created it. */ + if (pcm->samples) { + if (pcm->vorbis_owned) mcc_vorbis_free(pcm->samples); + else free(pcm->samples); + } + pcm->samples=NULL; +} + +static int mcc_pcm_sample(struct mcc_pcm const *pcm, uint32_t frame, int channel, int channels) +{ + if (pcm->channels==1) return pcm->samples[frame]; + if (channels==1) return ((int)pcm->samples[frame*2]+pcm->samples[frame*2+1])/2; + return pcm->samples[frame*2+channel]; +} + +static int mcc_resampled(struct mcc_pcm const *pcm, uint32_t frame, int channel, int channels, uint32_t rate) +{ + uint64_t position=(uint64_t)frame*pcm->rate; + uint32_t first=(uint32_t)(position/rate), fraction=(uint32_t)(position%rate), next; + int a,b; + if (first>=pcm->frames) first=pcm->frames-1; + next=first+1frames ? first+1 : first; + a=mcc_pcm_sample(pcm,first,channel,channels); + b=mcc_pcm_sample(pcm,next,channel,channels); + return a+(int)((int64_t)(b-a)*fraction/rate); +} + +static int mcc_audio_store(struct mcc_audio_storage *storage, struct mcc_pcm const *pcm, + int channels, uint32_t rate, struct sound_permutation *permutation) +{ + uint64_t frames64=((uint64_t)pcm->frames*rate+pcm->rate-1)/pcm->rate; + uint32_t frames,blocks,size,block; + struct mcc_ima states[2]={{0,0},{0,0}}; + if (!frames64 || frames64>MCC_AUDIO_FRAME_LIMIT) return 0; + frames=(uint32_t)frames64; blocks=(frames+63)/64; size=blocks*36*channels; + if (size>MCC_AUDIO_LIMIT-storage->used) return 0; + if (storage->used+size>storage->capacity) { + uint32_t capacity=(storage->used+size+0xFFFFF)&~0xFFFFFu; + unsigned char *larger=realloc(storage->bytes,capacity); + if (!larger) return 0; + storage->bytes=larger; storage->capacity=capacity; + } + for (block=0;blockbytes+storage->used+block*36*channels; + int channel; + for (channel=0;channel>8); + out[channel*4+2]=(unsigned char)states[channel].step; + out[channel*4+3]=0; + for (pair=0;pair<32;pair++) { + uint32_t f=block*64+pair*2; + unsigned lo=mcc_ima_quantize(&states[channel],mcc_resampled(pcm,MIN(f,frames-1),channel,channels,rate)); + unsigned hi=mcc_ima_quantize(&states[channel],mcc_resampled(pcm,MIN(f+1,frames-1),channel,channels,rate)); + out[channels*4+(pair/4*channels+channel)*4+pair%4]=(unsigned char)(lo|(hi<<4)); + } + } + } + permutation->samples.file_offset=MCC_AUDIO_BASE+storage->used; + permutation->samples.size=size; + permutation->sample_buffer_size=0; + permutation->compression=1; + storage->used+=size; + return 1; +} + +int mcc_audio_prepare(struct mcc_runtime *runtime) +{ + struct mcc_audio_storage *storage; + uint32_t index,converted=0; + if (runtime->source.size>=MCC_AUDIO_BASE) return 0; + storage=malloc(sizeof(*storage)); + if (!storage) return 0; + memset(storage,0,sizeof(*storage)); + runtime->audio=storage; + for (index=0;indexreport.tag_count;index++) { + uint32_t *entry=(uint32_t *)(runtime->tag_index+index*0x20); + struct sound_definition *sound; + uint32_t range_index,rate,output_rate; + int channels; + if (entry[0]!=(uint32_t)SOUND_DEFINITION_TAG) continue; + sound=mcc_runtime_pointer(runtime,entry[5],sizeof(*sound)); + if (!sound || sound->encoding<0 || sound->encoding>1 || sound->sample_rate<0 || sound->sample_rate>1) goto failed; + channels=sound->encoding+1; rate=sound->sample_rate ? 44100 : 22050; + output_rate=channels==1 ? 22050 : rate; + for (range_index=0;range_index<(uint32_t)sound->pitch_ranges.count;range_index++) { + struct sound_pitch_range *range=mcc_runtime_pointer(runtime, + (uint32_t)sound->pitch_ranges.address+range_index*sizeof(*range),sizeof(*range)); + uint32_t permutation_index; + if (!range) goto failed; + for (permutation_index=0;permutation_index<(uint32_t)range->permutations.count;permutation_index++) { + struct sound_permutation *permutation=mcc_runtime_pointer(runtime, + (uint32_t)range->permutations.address+permutation_index*sizeof(*permutation),sizeof(*permutation)); + unsigned char *data; + struct mcc_pcm pcm; + int ok; + if (!permutation || permutation->samples.size<0 || permutation->samples.size>0x4000000 || + (permutation->samples.pad&1)) goto failed; + permutation->unknown0=NONE; + permutation->unknown1=0; + permutation->unknown2=entry[3]; + permutation->unknown3=entry[3]; + permutation->samples.address=NULL; + permutation->sample_buffer_size=0; + if (!permutation->samples.size) continue; + data=malloc(permutation->samples.size); + if (!data) goto failed; + if (!mcc_runtime_read(runtime,permutation->samples.file_offset,permutation->samples.size,data)) { + free(data); goto failed; + } + ok=mcc_pcm_open(data,permutation->samples.size,permutation->compression,channels,rate,&pcm); + free(data); + if (ok && (permutation->compression!=1 || rate!=output_rate)) { + ok=mcc_audio_store(storage,&pcm,channels,output_rate,permutation); + if (ok) converted++; + } + mcc_pcm_close(&pcm); + if (!ok) goto failed; + } + } + sound->compression=1; + sound->sample_rate=output_rate==44100 ? 1 : 0; + } + error(_error_silent,"mcc audio: %lu permutations converted, %lu ADPCM bytes",converted,storage->used); + return 1; +failed: + error(_error_silent,"mcc audio: unsupported or damaged sound tag #%lu; map refused",index); + return 0; +} + +int mcc_audio_read(struct mcc_runtime *runtime,uint32_t offset,uint32_t bytes,void *out) +{ + struct mcc_audio_storage *storage=runtime->audio; + if (offset=MCC_AUDIO_BASE+MCC_AUDIO_LIMIT) return -1; + offset-=MCC_AUDIO_BASE; + if (!storage || offset>storage->used || bytes>storage->used-offset) return 0; + memcpy(out,storage->bytes+offset,bytes); + return 1; +} + +int mcc_audio_contains(struct mcc_runtime *runtime,uint32_t offset,uint32_t bytes) +{ + struct mcc_audio_storage *storage=runtime->audio; + return storage && offset>=MCC_AUDIO_BASE && offset-MCC_AUDIO_BASE<=storage->used && + bytes<=storage->used-(offset-MCC_AUDIO_BASE); +} + +uint32_t mcc_audio_stream_end(struct mcc_runtime *runtime) +{ + struct mcc_audio_storage *storage=runtime->audio; + return storage && storage->used ? MCC_AUDIO_BASE+storage->used : (uint32_t)runtime->source.size; +} + +void mcc_audio_dispose(struct mcc_runtime *runtime) +{ + struct mcc_audio_storage *storage=runtime->audio; + if (storage) { + if (storage->bytes) free(storage->bytes); + free(storage); runtime->audio=NULL; + } +} diff --git a/port/linux/game/mcc_bitmaps.c b/port/linux/game/mcc_bitmaps.c new file mode 100644 index 0000000000..f88757100d --- /dev/null +++ b/port/linux/game/mcc_bitmaps.c @@ -0,0 +1,638 @@ +/* MCC bitmap resources. Convert and pack each resource in MCC-owned memory, + * then stream its native hardware layout. The common rasterizer supplies + * descriptor size limits; no CE bitmap state or conversion API is used. */ +#include "cseries.h" +#include "errors.h" +#include "bitmaps/bitmaps.h" +#include "bitmaps/bitmap_group.h" +#include "interface/unit_hud_interface_definition.h" +#include "rasterizer/rasterizer_swizzle.h" +#include "mcc_runtime.h" +#include +#include +#define BCDEC_STATIC +#define BCDEC_IMPLEMENTATION +#include "../../third_party/bcdec/bcdec.h" + +#define MCC_BITMAP_BASE 0x60000000u +#define MCC_BITMAP_LIMIT 0x1FFFFFFFu +#define MCC_BITMAP_ALLOCATION_LIMIT 0x08000000u + +struct mcc_texture { + struct bitmap_data *bitmap; + uint32_t handle, offset, size; + unsigned char *pixels; + unsigned channels; +}; +struct mcc_bitmap_clone { uint32_t source, target; unsigned channels; }; +struct mcc_textures { + struct mcc_texture *items; + struct mcc_bitmap_clone *clones; + uint32_t count, end, capacity, clone_count, tag_capacity; +}; + +static void *mcc_bitmap_tag(struct mcc_runtime *runtime,uint32_t handle,uint32_t group,uint32_t size) +{ + uint32_t *entry; + if (handle==0xFFFFFFFFu || (handle&0xFFFF)>=runtime->report.tag_count) return NULL; + entry=(uint32_t *)(runtime->tag_index+(handle&0xFFFF)*0x20); + if (entry[0]!=group || entry[3]!=handle) return NULL; + return mcc_runtime_pointer(runtime,entry[5],size); +} + +static void *mcc_bitmap_block(struct mcc_runtime *runtime,struct tag_block const *block,uint32_t size) +{ + if (block->count<0 || (uint32_t)block->count>UINT32_MAX/size) return NULL; + if (!block->count) return runtime->tags; + return mcc_runtime_pointer(runtime,(uint32_t)block->address,(uint32_t)block->count*size); +} + +static void mcc_bitmap_mark_one(struct mcc_textures *textures,struct bitmap_data *bitmap,unsigned channels) +{ + uint32_t index; + for (index=0;indexcount;index++) { + if (textures->items[index].bitmap==bitmap) textures->items[index].channels|=channels; + } +} + +static int mcc_bitmap_clone_block(struct mcc_runtime *runtime,struct tag_block *block,uint32_t stride) +{ + void *source,*copy; + uint32_t bytes; + if (!block->count) {block->address=NULL;return 1;} + source=mcc_bitmap_block(runtime,block,stride); + if (!source) return 0; + bytes=(uint32_t)block->count*stride; + copy=mcc_runtime_allocate(runtime,bytes); + if (!copy) return 0; + memcpy(copy,source,bytes);block->address=copy; + return 1; +} + +/* A bitmap can be used as both color and a packed material mask. Each + * specialized consumer gets its own tag and descriptors, preserving the + * ordinary source tag and every other consumer's channels. */ +static int mcc_bitmap_specialize(struct mcc_runtime *runtime,struct mcc_textures *textures, + uint32_t *handle,unsigned channels) +{ + struct bitmap_group *source,*copy; + struct bitmap_group_sequence *sequences; + struct bitmap_data *bitmaps; + uint32_t i,*entry,new_handle; + char *name; + if (*handle==0xFFFFFFFFu || channels==1) return 1; + for (i=0;iclone_count;i++) { + struct mcc_bitmap_clone const *clone=&textures->clones[i]; + if (clone->source==*handle && clone->channels==channels) {*handle=clone->target;return 1;} + } + if (runtime->report.tag_count>=textures->tag_capacity) return 0; + source=mcc_bitmap_tag(runtime,*handle,BITMAP_GROUP_TAG,sizeof(*source)); + if (!source) return 0; + copy=mcc_runtime_allocate(runtime,sizeof(*copy)); + if (!copy) return 0; + *copy=*source; + if (!mcc_bitmap_clone_block(runtime,©->sequences,sizeof(*sequences)) || + !mcc_bitmap_clone_block(runtime,©->bitmaps,sizeof(*bitmaps))) return 0; + sequences=copy->sequences.address; + for (i=0;i<(uint32_t)copy->sequences.count;i++) { + if (!mcc_bitmap_clone_block(runtime,&sequences[i].sprites,sizeof(struct bitmap_group_sprite))) return 0; + } + /* Import/pixel tag_data are editor storage. Runtime pixels use each + * descriptor's file range, so these never alias a cloned tag allocation. */ + memset(©->import_bitmap,0,sizeof(copy->import_bitmap)); + memset(©->pixel_data,0,sizeof(copy->pixel_data)); + name=mcc_runtime_allocate(runtime,48); + if (!name) return 0; + snprintf(name,48,"mcc_runtime\\bitmap_%08lx_%u",(unsigned long)*handle,channels); + entry=(uint32_t *)(runtime->tag_index+runtime->report.tag_count*32); + memcpy(entry,runtime->tag_index+(*handle&0xFFFF)*32,32); + new_handle=(*handle&0xFFFF0000u)|runtime->report.tag_count; + entry[3]=new_handle;entry[4]=(uint32_t)name;entry[5]=(uint32_t)copy; + bitmaps=copy->bitmaps.address; + for (i=0;i<(uint32_t)copy->bitmaps.count;i++) { + struct mcc_texture *texture; + if (textures->count>=textures->capacity) return 0; + texture=&textures->items[textures->count++]; + texture->bitmap=&bitmaps[i];texture->handle=new_handle;texture->channels=channels; + } + textures->clones[textures->clone_count].source=*handle; + textures->clones[textures->clone_count].target=new_handle; + textures->clones[textures->clone_count++].channels=channels; + runtime->report.tag_count++; + *handle=new_handle; + return 1; +} + +/* Channels is a usage mask: ordinary=1, multipurpose=2, HUD meter=4. */ +static int mcc_bitmap_mark(struct mcc_runtime *runtime,struct mcc_textures *textures, + uint32_t *reference,short sequence,unsigned channels) +{ + struct bitmap_group *group; + struct bitmap_data *bitmaps; + uint32_t index; + uint32_t handle; + if (!mcc_bitmap_specialize(runtime,textures,reference,channels)) return 0; + handle=*reference; + if (handle==0xFFFFFFFFu) return 1; + group=mcc_bitmap_tag(runtime,handle,BITMAP_GROUP_TAG,sizeof(*group)); + if (!group) return 0; + bitmaps=mcc_bitmap_block(runtime,&group->bitmaps,sizeof(*bitmaps)); + if (!bitmaps) return 0; + if (sequence>=0 && sequencesequences.count) { + struct bitmap_group_sequence *sequences=mcc_bitmap_block(runtime,&group->sequences,sizeof(*sequences)); + struct bitmap_group_sequence *selected; + if (!sequences) return 0; + selected=&sequences[sequence]; + if (selected->sprites.count) { + struct bitmap_group_sprite *sprites=mcc_bitmap_block(runtime,&selected->sprites,sizeof(*sprites)); + if (!sprites) return 0; + for (index=0;index<(uint32_t)selected->sprites.count;index++) { + short n=sprites[index].bitmap_index; + if (n<0 || n>=group->bitmaps.count) return 0; + mcc_bitmap_mark_one(textures,&bitmaps[n],channels); + } + } else { + if (selected->first_bitmap_index<0 || selected->bitmap_count<0 || + selected->first_bitmap_index>group->bitmaps.count-selected->bitmap_count) return 0; + for (index=0;index<(uint32_t)selected->bitmap_count;index++) + mcc_bitmap_mark_one(textures,&bitmaps[selected->first_bitmap_index+index],channels); + } + } else { + for (index=0;index<(uint32_t)group->bitmaps.count;index++) mcc_bitmap_mark_one(textures,&bitmaps[index],channels); + } + return 1; +} + +static int mcc_meter_usage(struct mcc_runtime *runtime,struct mcc_textures *textures, + struct meter_hud_element_definition *meter) +{ + /* MCC meter flag 5 keeps Xbox fill/shape channels. The native meter + * already consumes that order; only Gearbox-style meters need a clone + * with luminance and alpha exchanged. */ + unsigned channels=meter->meter_flags&0x20 ? 1 : 4; + return mcc_bitmap_mark(runtime,textures,(uint32_t *)&meter->meter_bitmap.index,meter->sequence_index,channels); +} + +static int mcc_static_usage(struct mcc_runtime *runtime,struct mcc_textures *textures, + struct static_hud_element_definition *element) +{ + return mcc_bitmap_mark(runtime,textures,(uint32_t *)&element->interface_bitmap.index,element->sequence_index,1); +} + +static int mcc_bitmap_model_usage(struct mcc_runtime *runtime,struct mcc_textures *textures, + unsigned char *shader) +{ + static unsigned const offsets[]={0xB0,0xC8,0xE8,0x170}; + uint16_t flags; + unsigned n; + memcpy(&flags,shader+0x28,sizeof(flags)); + /* MCC's model flag 6 selects original Xbox mask channels. These masks + * already have reflection in red and change color in blue; applying the + * Gearbox swizzle would turn opaque DXT1 alpha into full-surface tint. + * Select per reference: another shader may use this same bitmap with + * the flag clear and must still receive its own reordered clone. */ + for (n=0;n<4;n++) { + unsigned channels=n==1 && !(flags&0x40) ? 2 : 1; + if (!mcc_bitmap_mark(runtime,textures,(uint32_t *)(shader+offsets[n]),NONE,channels)) return 0; + } + return 1; +} + +static int mcc_bitmap_usages(struct mcc_runtime *runtime,struct mcc_textures *textures) +{ + uint32_t index; + for (index=0;indexreport.tag_count;index++) { + uint32_t *entry=(uint32_t *)(runtime->tag_index+index*0x20); + if (entry[0]=='soso') { + unsigned char *shader=mcc_runtime_pointer(runtime,entry[5],0x174); + if (!shader || !mcc_bitmap_model_usage(runtime,textures,shader)) return 0; + } else if (entry[0]==UNIT_HUD_INTERFACE_DEFINITION_TAG) { + struct unit_hud_interface_definition *hud=mcc_runtime_pointer(runtime,entry[5],sizeof(*hud)); + struct auxilary_meter_definition *meters; + struct auxilary_overlay_definition *overlays; + uint32_t n; + if (!hud || !mcc_meter_usage(runtime,textures,&hud->shield_meter.meter) || + !mcc_meter_usage(runtime,textures,&hud->health_meter.meter) || + !mcc_static_usage(runtime,textures,&hud->background) || + !mcc_static_usage(runtime,textures,&hud->shield_meter.background) || + !mcc_static_usage(runtime,textures,&hud->health_meter.background) || + !mcc_static_usage(runtime,textures,&hud->motion_sensor.background) || + !mcc_static_usage(runtime,textures,&hud->motion_sensor.foreground)) return 0; + meters=mcc_bitmap_block(runtime,&hud->auxilary_meters,sizeof(*meters)); + overlays=mcc_bitmap_block(runtime,&hud->auxilary_panel.auxilary_overlays,sizeof(*overlays)); + if (!meters || !overlays) return 0; + for (n=0;n<(uint32_t)hud->auxilary_meters.count;n++) { + if (!mcc_meter_usage(runtime,textures,&meters[n].panel.meter) || + !mcc_static_usage(runtime,textures,&meters[n].panel.background)) return 0; + } + for (n=0;n<(uint32_t)hud->auxilary_panel.auxilary_overlays.count;n++) + if (!mcc_static_usage(runtime,textures,&overlays[n].static_element)) return 0; + } else if (entry[0]=='wphi') { + unsigned char *hud=mcc_runtime_pointer(runtime,entry[5],0x78); + unsigned which; + if (!hud) return 0; + for (which=0;which<2;which++) { + struct tag_block *block=(struct tag_block *)(hud+0x60+which*12); + unsigned char *elements=mcc_bitmap_block(runtime,block,0xB4); + uint32_t n; + if (!elements) return 0; + for (n=0;n<(uint32_t)block->count;n++) { + void *element=elements+n*0xB4+0x24; + if (!(which ? mcc_meter_usage(runtime,textures,element) : mcc_static_usage(runtime,textures,element))) return 0; + } + } + } + } + return 1; +} + +static int mcc_bitmap_bytes(short format) +{ + switch (format) { + case 0: case 1: case 2: case 17: return 1; + case 3: case 6: case 8: case 9: return 2; + case 10: case 11: return 4; + default: return 0; + } +} + +static uint32_t mcc_pixel(unsigned char const *source,int format) +{ + unsigned v=source[0], a=255,r=v,g=v,b=v; + if (format==0) {a=v; r=g=b=255;} + else if (format==2) a=v; + else if (format==3) a=source[1]; + else if (format==6 || format==8 || format==9) { + v|=(unsigned)source[1]<<8; + if (format==6) {r=(v>>11)*255/31; g=((v>>5)&63)*255/63; b=(v&31)*255/31;} + else if (format==8) {a=(v&0x8000)?255:0; r=((v>>10)&31)*255/31; g=((v>>5)&31)*255/31; b=(v&31)*255/31;} + else {a=(v>>12)*17; r=((v>>8)&15)*17; g=((v>>4)&15)*17; b=(v&15)*17;} + } else if (format==10 || format==11) { + b=source[0];g=source[1];r=source[2];a=format==10 ? 255 : source[3]; + } else if (format==17) return global_vector_palette[v]; + return b|(g<<8)|(r<<16)|(a<<24); +} + +static uint32_t mcc_channels(uint32_t color,unsigned usage) +{ + unsigned b=color&255,g=(color>>8)&255,r=(color>>16)&255,a=color>>24; + if (usage==2) return a|(g<<8)|(b<<16)|(r<<24); + if (usage==4) return a|(a<<8)|(a<<16)|(r<<24); + return color; +} + +/* Decode one slice to BGRA. Compressed blocks remain 4x4 even for tiny mips. */ +static void mcc_bitmap_decode(unsigned char const *input,uint32_t *output, + unsigned width,unsigned height,short format,unsigned usage) +{ + unsigned x,y; + if (format>=14 && format!=17) { + unsigned step=format==14 ? 8 : 16; + for (y=0;ytype==2 ? 6 : 1; + unsigned maximum=rasterizer_xbox_bitmap_get_max_mipmap_count(bitmap); + unsigned bytes=mcc_bitmap_bytes(bitmap->format); + /* CPU object lighting can select the final 2x2/1x1 DXT mips even though + * the GPU descriptor stops at 4 texels. A 2D chain has identical CPU/GPU + * prefix layout, so retain its complete source tail in owned storage. */ + if (bitmap->type==0 && (bitmap->flags&2)) maximum=bitmap->mipmap_count; + memset(target,0,target_size); + for (face=0;facewidth>>level,1),height=MAX(bitmap->height>>level,1),depth=MAX(bitmap->depth>>level,1); + uint32_t size=bytes ? width*height*depth*bytes : ((width+3)/4)*((height+3)/4)*depth*(bitmap->format==14 ? 8 : 16); + uint32_t at=input+(faces==6 ? cube_face[face] : 0)*size; + unsigned x,y,z; + if (at>source_size || size>source_size-at || out>target_size) return 0; + if (bitmap->flags&16) { + unsigned pitch=(width*bytes+63)&~63u; + if (pitch*height>target_size-out) return 0; + for (y=0;ytarget_size-out) return 0; + if (!bytes) memcpy(target+out,source+at,size); + else { + for (x=0;xflags&(2|16))) bitmap->flags|=8; + return out==target_size; +} + +/* Recent MCC campaign lightmaps carry bit 12 in addition to the environment + * flag. Its wider MCC meaning is not assumed here: admit only the observed + * lightmap-group RGB565 layout with a complete, tightly packed pixel range. + * The normal descriptor verifier still checks every other bit and field. */ +static int mcc_bitmap_lightmap_flags(struct bitmap_data *bitmap,short usage) +{ + if (!(bitmap->flags&0x1000)) return 1; + if (usage!=4 || bitmap->flags!=0x1281 || bitmap->type!=0 || bitmap->format!=6 || + bitmap->width<=0 || bitmap->height<=0 || bitmap->depth!=1 || bitmap->mipmap_count || + (uint64_t)(unsigned)bitmap->width*(unsigned)bitmap->height*2!=(uint32_t)bitmap->pixels_size) return 0; + bitmap->flags&=~0x1000; + return 1; +} + +/* Some MCC HUD textures declare a complete DXT1 chain but size its sub-4x4 + * tail as width*height/2 instead of whole compression blocks. Recognize only + * that exact size formula and retain the complete prefix. Arbitrary short + * ranges, missing base levels and other formats remain load failures. */ +static void mcc_bitmap_complete_mips(struct bitmap_data *bitmap) +{ + uint64_t packed=0; + unsigned level,maximum,complete=0; + if (bitmap->type!=0 || bitmap->format!=14 || bitmap->flags!=0x81 || bitmap->depth!=1 || + bitmap->width<4 || bitmap->height<4 || + (bitmap->width&(bitmap->width-1)) || (bitmap->height&(bitmap->height-1))) return; + maximum=(unsigned)floor_log2(MAX(bitmap->width,bitmap->height)); + if (bitmap->mipmap_count!=(short)maximum) return; + for (level=0;level<=maximum;level++) { + unsigned width=MAX(bitmap->width>>level,1),height=MAX(bitmap->height>>level,1); + packed+=(uint64_t)width*height/2; + if (width>=4 && height>=4) complete=level; + } + if (packed==(uint32_t)bitmap->pixels_size) bitmap->mipmap_count=(short)complete; +} + +static int mcc_texture_prepare(struct mcc_runtime *runtime,struct mcc_textures *textures,struct mcc_texture *texture) +{ + struct bitmap_data *bitmap=texture->bitmap, normalized=*bitmap; + unsigned level,faces=bitmap->type==2 ? 6 : 1; + uint64_t raw_size=0,decoded_size=0; + unsigned char *raw=NULL,*pixels=NULL,*hardware=NULL; + int convert=bitmap->format==18 || texture->channels==2 || texture->channels==4 || + ((bitmap->flags&16) && bitmap->format>=14 && bitmap->format<=17); + int bytes_per_pixel=mcc_bitmap_bytes(bitmap->format); + uint32_t input_offset=0,output_offset=0,hardware_size,allocation; + if (bitmap->width<=0 || bitmap->height<=0 || bitmap->depth<=0 || bitmap->type<0 || bitmap->type>2 || + bitmap->width>4096 || bitmap->height>4096 || bitmap->depth>512 || bitmap->mipmap_count<0 || bitmap->mipmap_count>12 || + bitmap->pixels_size<=0 || (bitmap->flags&0x100) || (!bytes_per_pixel && (bitmap->format<14 || bitmap->format>18))) return 0; + if ((bitmap->type==1 && (bitmap->width>512 || bitmap->height>512 || bitmap->depth>256)) || + (bitmap->type!=1 && bitmap->depth!=1)) return 0; + if (texture->channels && (texture->channels&(texture->channels-1))) { + error(_error_silent,"mcc bitmap: tag %08lx has conflicting pixel-channel uses",texture->handle); + return 0; + } + mcc_bitmap_complete_mips(&normalized); + for (level=0;level<=(unsigned)normalized.mipmap_count;level++) { + unsigned w=MAX(bitmap->width>>level,1),h=MAX(bitmap->height>>level,1),d=MAX(bitmap->depth>>level,1); + uint64_t slice=bytes_per_pixel ? (uint64_t)w*h*bytes_per_pixel : + (uint64_t)((w+3)/4)*((h+3)/4)*(bitmap->format==14 ? 8 : 16); + raw_size+=slice*d*faces; + decoded_size+=(uint64_t)w*h*d*faces*4; + } + if (raw_size>(uint32_t)bitmap->pixels_size || raw_size>MCC_BITMAP_ALLOCATION_LIMIT || + (convert && decoded_size>MCC_BITMAP_ALLOCATION_LIMIT)) return 0; + if (normalized.flags&0x1000) { + struct bitmap_group *group=mcc_bitmap_tag(runtime,texture->handle,BITMAP_GROUP_TAG,sizeof(*group)); + if (!group || !mcc_bitmap_lightmap_flags(&normalized,group->usage)) return 0; + } + if (convert) { + normalized.format=11; + normalized.flags&=~(2|4|8|32|0x100); + } else normalized.flags&=~(8|0x100); + /* CEA bit 9 marks environment/lightmap use (Invader's public bitmap + * schema). It is not a pixel-layout flag and has no Xbox counterpart. */ + normalized.flags&=~0x200; + normalized.flags=(normalized.flags&~64)|128; + /* MCC raw flags are not an Xbox hardware descriptor. Derive the + * compressed/palette bits from the normalized pixel format. */ + normalized.flags&=~(2|4); + if (normalized.format>=14 && normalized.format<=16) normalized.flags|=2; + if (normalized.format==17) normalized.flags|=4; + if (!bitmap_verify(&normalized,FALSE)) return 0; + if (!(normalized.flags&16) && + ((normalized.width&(normalized.width-1)) || (normalized.height&(normalized.height-1)) || + (normalized.depth&(normalized.depth-1)))) return 0; + if ((normalized.flags&16) && (normalized.type!=0 || normalized.mipmap_count || (normalized.flags&2))) return 0; + hardware_size=(uint32_t)rasterizer_xbox_bitmap_get_pixel_data_size(&normalized); + /* The generic bitmap verifier includes the tiny DXT mip tail, whereas + * native hardware stops at a 4-texel dimension. Own enough storage for + * both contracts, including the 2D tail sampled by CPU object lighting. */ + allocation=(MAX(hardware_size,(uint32_t)(convert ? decoded_size : raw_size))+127u)&~127u; + if (!hardware_size || allocation>MCC_BITMAP_ALLOCATION_LIMIT || allocation>MCC_BITMAP_LIMIT-textures->end) return 0; + raw=malloc((size_t)raw_size); + pixels=malloc(allocation); + if (pixels) memset(pixels,0,allocation); + if (!raw || !pixels || !mcc_runtime_read(runtime,bitmap->pixels_offset,(uint32_t)raw_size,raw)) goto failed; + if (convert) { + for (level=0;level<=(unsigned)normalized.mipmap_count;level++) { + unsigned w=MAX(bitmap->width>>level,1),h=MAX(bitmap->height>>level,1),d=MAX(bitmap->depth>>level,1),slice; + unsigned source_bytes=bytes_per_pixel ? w*h*bytes_per_pixel : ((w+3)/4)*((h+3)/4)*(bitmap->format==14 ? 8 : 16); + for (slice=0;sliceformat,texture->channels); + input_offset+=source_bytes;output_offset+=w*h*4; + } + } + } else memcpy(pixels,raw,(size_t)raw_size); + free(raw);raw=NULL; + hardware=malloc(allocation); + if (hardware) memset(hardware,0,allocation); + if (!hardware || !mcc_bitmap_pack(&normalized,pixels,(uint32_t)(convert ? decoded_size : raw_size),hardware, + normalized.type==0 && (normalized.flags&2) ? allocation : hardware_size)) goto failed; + free(pixels);pixels=hardware;hardware=NULL; + if (bitmap->mipmap_count!=normalized.mipmap_count) + error(_error_silent,"mcc bitmap: tag %08lx omitted incomplete DXT1 mip tail (levels %d through %d)", + texture->handle,normalized.mipmap_count+1,bitmap->mipmap_count); + bitmap->format=normalized.format; + bitmap->flags=normalized.flags; + bitmap->mipmap_count=normalized.mipmap_count; + bitmap->pixels_offset=MCC_BITMAP_BASE+textures->end; + bitmap->pixels_size=allocation; + bitmap->tag_index=texture->handle; + bitmap->cache_block_index=NONE; + bitmap->hardware_format=NULL; + bitmap->base_address=NULL; + texture->offset=bitmap->pixels_offset;texture->size=allocation;texture->pixels=pixels; + textures->end+=allocation; + return 1; +failed: + if (raw) free(raw); + if (pixels) free(pixels); + if (hardware) free(hardware); + return 0; +} + +int mcc_bitmaps_prepare(struct mcc_runtime *runtime) +{ + struct mcc_textures *textures; + uint32_t index,original_count=runtime->report.tag_count; + unsigned char *new_index; + textures=malloc(sizeof(*textures)); + if (!textures) return 0; + memset(textures,0,sizeof(*textures)); + runtime->bitmaps=textures; + textures->capacity=runtime->report.bitmap_count*3; + textures->tag_capacity=MIN(original_count*3,0xFFFFu); + new_index=mcc_runtime_allocate(runtime,textures->tag_capacity*32); + if (!new_index) return 0; + memcpy(new_index,runtime->tag_index,original_count*32); + runtime->tag_index=new_index; + textures->clones=malloc(textures->tag_capacity*sizeof(*textures->clones)); + if (!textures->clones) return 0; + if (runtime->report.bitmap_count) { + textures->items=malloc(textures->capacity*sizeof(*textures->items)); + if (!textures->items) return 0; + memset(textures->items,0,textures->capacity*sizeof(*textures->items)); + } + for (index=0;indextag_index+index*0x20), n; + struct bitmap_group *group; + struct bitmap_data *bitmaps; + if (entry[0]!=BITMAP_GROUP_TAG) continue; + group=mcc_runtime_pointer(runtime,entry[5],sizeof(*group)); + if (!group || !(bitmaps=mcc_bitmap_block(runtime,&group->bitmaps,sizeof(*bitmaps)))) return 0; + for (n=0;n<(uint32_t)group->bitmaps.count;n++) { + struct mcc_texture *texture; + if (textures->count>=runtime->report.bitmap_count) return 0; + texture=&textures->items[textures->count++]; + texture->bitmap=&bitmaps[n];texture->handle=entry[3]; + } + } + if (!mcc_bitmap_usages(runtime,textures)) { + error(_error_silent,"mcc bitmaps: invalid bitmap reference in shader or HUD"); return 0; + } + for (index=0;indexcount;index++) { + if (!mcc_texture_prepare(runtime,textures,&textures->items[index])) { + error(_error_silent,"mcc bitmaps: cannot convert texture %lu of tag %08lx; map refused",index,textures->items[index].handle); + return 0; + } + } + error(_error_silent,"mcc bitmaps: %lu textures converted to %lu bytes",textures->count,textures->end); + return 1; +} + +int mcc_bitmaps_read(struct mcc_runtime *runtime,long tag,uint32_t offset,uint32_t bytes,void *out) +{ + struct mcc_textures *textures=runtime->bitmaps; + uint32_t index; + if (offsetcount;index++) { + struct mcc_texture *texture=&textures->items[index]; + uint32_t position=offset-texture->offset; + if ((uint32_t)tag==texture->handle && offset>=texture->offset && position<=texture->size && bytes<=texture->size-position) { + memcpy(out,texture->pixels+position,bytes); return 1; + } + } + return 0; +} + +uint32_t mcc_bitmaps_stream_end(struct mcc_runtime *runtime) +{ + struct mcc_textures *textures=runtime->bitmaps; + return textures && textures->end ? MCC_BITMAP_BASE+textures->end : (uint32_t)runtime->source.size; +} + +int mcc_bitmaps_contains(struct mcc_runtime *runtime,uint32_t offset,uint32_t bytes) +{ + struct mcc_textures *textures=runtime->bitmaps; + uint32_t index; + for (index=0;textures && indexcount;index++) { + struct mcc_texture const *texture=&textures->items[index]; + if (texture->pixels && offset>=texture->offset && offset-texture->offset<=texture->size && + bytes<=texture->size-(offset-texture->offset)) return 1; + } + return 0; +} + +int mcc_bitmaps_valid(struct mcc_runtime *runtime,struct bitmap_data *bitmap) +{ + struct mcc_textures *textures=runtime->bitmaps; + uint32_t index; + for (index=0;textures && indexcount;index++) { + struct mcc_texture const *texture=&textures->items[index]; + if (texture->bitmap!=bitmap) continue; + return texture->pixels && (uint32_t)bitmap->tag_index==texture->handle && + (uint32_t)bitmap->pixels_offset==texture->offset && (uint32_t)bitmap->pixels_size==texture->size && + bitmap_verify(bitmap,FALSE) && rasterizer_xbox_bitmap_get_pixel_data_size(bitmap)<=(long)texture->size; + } + return 0; +} + +/* The renderer may bind the immutable MCC allocation directly. A copied + * descriptor, another map's bitmap, or a changed stream range cannot borrow + * this storage. The caller receives bytes only after ownership validation. */ +void const *mcc_bitmaps_pixels(struct mcc_runtime *runtime,struct bitmap_data const *bitmap,uint32_t *bytes) +{ + struct mcc_textures *textures=runtime ? runtime->bitmaps : NULL; + uint32_t index; + if (bytes) *bytes=0; + if (!textures || !bitmap || !mcc_bitmaps_valid(runtime,(struct bitmap_data *)bitmap)) return NULL; + for (index=0;indexcount;index++) { + struct mcc_texture const *texture=&textures->items[index]; + if (texture->bitmap!=bitmap) continue; + if (bytes) *bytes=texture->size; + return texture->pixels; + } + return NULL; +} + +void mcc_bitmaps_dispose(struct mcc_runtime *runtime) +{ + struct mcc_textures *textures=runtime->bitmaps; + uint32_t index; + if (!textures) return; + for (index=0;indexcount;index++) { + struct mcc_texture *texture=&textures->items[index]; + if (texture->bitmap && texture->bitmap->base_address==texture->pixels) + texture->bitmap->base_address=NULL; + if (texture->pixels) free(texture->pixels); + } + if (textures->items) free(textures->items); + if (textures->clones) free(textures->clones); + free(textures);runtime->bitmaps=NULL; +} diff --git a/port/linux/game/mcc_cache.c b/port/linux/game/mcc_cache.c new file mode 100644 index 0000000000..0da5717b65 --- /dev/null +++ b/port/linux/game/mcc_cache.c @@ -0,0 +1,350 @@ +/* Independent MCC cache lifetime and I/O. Source files are opened read-only. + * Conversion uses exclusively MCC-owned memory and a separate tag window. */ +#include "cseries.h" +#include "errors.h" +#include "cache/cache_files.h" +#include "scenario/scenario_definitions.h" +#include "interface/ui_widget.h" +#include "mcc_cache.h" +#include "mcc_maps.h" +#include "mcc_runtime.h" +#include "mcc_tag_validate.h" +#include "mcc_checkpoint.h" +#include "mcc_grenades.h" +#include "mcc_script_parameters.h" +#include "mcc_texture_cache.h" +#include "mcc_ui_teams.h" +#include +#include +#include +#include + +void *mcc_memory_reserve(uint32_t address, uint32_t size); +void mcc_memory_release(void *allocation, uint32_t size); +int mcc_tags_prepare(struct mcc_runtime *runtime); +int mcc_scripts_prepare(struct mcc_runtime *runtime); +short mcc_geometry_part_palette(struct mcc_runtime *runtime, void const *buffer, byte const **nodes); + +static struct mcc_runtime mcc_loaded_cache; +static FILE *mcc_stream; +static boolean mcc_loaded; + +static int mcc_prefix(char const *name) +{ + static char const prefix[] = "mcc_maps"; + size_t i; + if (!name) + return FALSE; + for (i = 0; i < sizeof(prefix) - 1; ++i) { + unsigned char c = (unsigned char)name[i]; + if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; + if (c != (unsigned char)prefix[i]) return FALSE; + } + return name[i] == '\\' || name[i] == '/'; +} + +boolean mcc_level_name(char const *name) +{ + return mcc_prefix(name); +} + +static int mcc_path(char const *name, char path[128]) +{ + size_t n, i; + char const *stem; + if (!mcc_prefix(name)) return FALSE; + stem = name + sizeof(MCC_MAPS_LEVEL_PREFIX) - 1; + n = strlen(stem); + if (!n || n > 63 - (sizeof(MCC_MAPS_LEVEL_PREFIX) - 1) || + stem[n - 1] == '.' || stem[n - 1] == ' ') + return FALSE; + for (i = 0; i < n; ++i) { + unsigned char c = (unsigned char)stem[i]; + if (c < 32 || strchr("/\\:*?\"<>|", c)) return FALSE; + } + snprintf(path, 128, "%s%s.map", MCC_MAPS_DIRECTORY, stem); + return TRUE; +} + +/* Checkpoints store the scenario tag's name, not the menu's level name. + * Give this MCC instance its canonical namespace so Continue/reload resolves + * the same file even when its internal scenario path names a stock level. */ +static int mcc_scenario_identity(struct mcc_runtime *runtime, char const *name) +{ + char checked_path[128]; + char *identity; + uint32_t address, index; + size_t length; + if (!runtime || !runtime->tag_index || !mcc_path(name, checked_path)) return FALSE; + index = runtime->report.scenario_handle & 0xFFFFu; + if (index >= runtime->report.tag_count) return FALSE; + length = strlen(name + sizeof(MCC_MAPS_LEVEL_PREFIX) - 1); + identity = mcc_runtime_allocate(runtime, (uint32_t)(sizeof(MCC_MAPS_LEVEL_PREFIX) + length)); + if (!identity) return FALSE; + memcpy(identity, MCC_MAPS_LEVEL_PREFIX, sizeof(MCC_MAPS_LEVEL_PREFIX) - 1); + memcpy(identity + sizeof(MCC_MAPS_LEVEL_PREFIX) - 1, + name + sizeof(MCC_MAPS_LEVEL_PREFIX) - 1, length + 1); + address = (uint32_t)(uintptr_t)identity; + memcpy(runtime->tag_index + index * 32 + 16, &address, sizeof(address)); + return TRUE; +} + +static int mcc_file_read(void *context, uint32_t offset, uint32_t bytes, void *out) +{ + FILE *stream = context; + return stream && offset <= 0x7FFFFFFFu && + !fseek(stream, (long)offset, SEEK_SET) && fread(out, 1, bytes, stream) == bytes; +} + +static FILE *mcc_open(char const *name, struct mcc_cache_source *source, + struct mcc_cache_identity *identity) +{ + char path[128]; + unsigned char header[MCC_CACHE_HEADER_BYTES]; + FILE *stream; + long size; + if (!mcc_path(name, path)) return NULL; + stream = fopen(path, "rb"); + if (!stream) return NULL; + if (fseek(stream, 0, SEEK_END) || (size = ftell(stream)) < 0 || + !mcc_file_read(stream, 0, sizeof(header), header) || + mcc_cache_identify(header, sizeof(header), (uint64_t)size, identity) != MCC_CACHE_OK) { + fclose(stream); + return NULL; + } + source->context = stream; + source->read = mcc_file_read; + source->size = (uint64_t)size; + return stream; +} + +boolean mcc_cache_available(char const *name) +{ + struct mcc_cache_identity identity; + struct mcc_cache_source source; + FILE *stream = mcc_open(name, &source, &identity); + if (!stream) return FALSE; + fclose(stream); + return TRUE; +} + +unsigned long mcc_cache_checksum(char const *name) +{ + struct mcc_cache_identity identity; + struct mcc_cache_source source; + FILE *stream = mcc_open(name, &source, &identity); + if (!stream) return 0; + fclose(stream); + return identity.checksum; +} + +boolean mcc_cache_present(char const *name, unsigned long checksum, char *message, long size) +{ + struct mcc_cache_identity identity; + struct mcc_cache_source source; + FILE *stream = mcc_open(name, &source, &identity); + if (!stream) { + snprintf(message, (size_t)size, "The MCC map %.63s is missing or is not a valid version-13 cache. Copy its .map file into mcc_maps.", name ? name : ""); + return FALSE; + } + fclose(stream); + if (checksum && identity.checksum != checksum) { + snprintf(message, (size_t)size, "The MCC map %.63s differs from the host's copy. Put the same version in mcc_maps.", name); + return FALSE; + } + return TRUE; +} + +boolean mcc_cache_require(char const *name, unsigned long checksum) +{ + char message[256]; + wchar_t text[256]; + size_t i; + if (mcc_cache_present(name, checksum, message, sizeof(message))) return TRUE; + error(_error_silent, "mcc: %s", message); + for (i = 0; message[i] && i < NUMBEROF(text) - 1; ++i) text[i] = (unsigned char)message[i]; + text[i] = 0; + display_error_text_when_main_menu_loaded(text); + return FALSE; +} + +void *mcc_runtime_pointer(struct mcc_runtime *runtime, uint32_t address, uint32_t bytes) +{ + uint32_t offset = address - runtime->report.tag_base; + if (address < runtime->report.tag_base || offset > runtime->used || bytes > runtime->used - offset) + return NULL; + return runtime->tags + offset; +} + +int mcc_runtime_read(struct mcc_runtime *runtime, uint32_t offset, uint32_t bytes, void *out) +{ + return (uint64_t)offset <= runtime->source.size && + bytes <= runtime->source.size - offset && + runtime->source.read(runtime->source.context, offset, bytes, out); +} + +void *mcc_runtime_allocate(struct mcc_runtime *runtime, uint32_t bytes) +{ + uint32_t limit = runtime->capacity, index; + uint32_t start = (runtime->used + 15u) & ~15u; + void *result; + for (index = 0; index < runtime->report.bsp_count; ++index) { + uint32_t bsp_offset = runtime->report.bsps[index].address - runtime->report.tag_base; + if (bsp_offset < limit) limit = bsp_offset; + } + if (start < runtime->used || start > limit || bytes > limit - start) return NULL; + result = runtime->tags + start; + memset(result, 0, bytes); + runtime->used = start + bytes; + return result; +} + +boolean mcc_cache_tags_loaded(void) { return mcc_loaded; } + +boolean mcc_cache_contains(void const *address, long bytes) +{ + uintptr_t offset = (uintptr_t)address - (uintptr_t)mcc_loaded_cache.tags; + return mcc_loaded && bytes > 0 && offset <= mcc_loaded_cache.capacity && + (uint32_t)bytes <= mcc_loaded_cache.capacity - offset; +} + +void mcc_cache_tags_unload(void) +{ + mcc_ui_teams_reset(); + mcc_checkpoint_dispose(); + mcc_grenades_reset(); + mcc_parameters_dispose(); + mcc_loaded = FALSE; + mcc_texture_cache_dispose(); + mcc_geometry_dispose(&mcc_loaded_cache); + mcc_audio_dispose(&mcc_loaded_cache); + mcc_bitmaps_dispose(&mcc_loaded_cache); + mcc_validation_dispose(&mcc_loaded_cache); + mcc_memory_release(mcc_loaded_cache.tags, mcc_loaded_cache.capacity); + if (mcc_stream) fclose(mcc_stream); + mcc_stream = NULL; + memset(&mcc_loaded_cache, 0, sizeof(mcc_loaded_cache)); +} + +struct cache_file_tag_header *mcc_cache_tags_load(char const *name, void *header) +{ + struct mcc_runtime *runtime = &mcc_loaded_cache; + struct mcc_cache_identity identity; + unsigned char *raw = NULL; + enum mcc_cache_status status; + char const *problem = "the map could not be opened"; + mcc_cache_tags_unload(); + mcc_stream = mcc_open(name, &runtime->source, &identity); + if (!mcc_stream) goto failed; + /* The virtual streams are disjoint from the on-disk file. */ + problem = "the file overlaps the MCC virtual resource range"; + if (identity.file_length >= 0x50000000u) goto failed; + raw = malloc(identity.tag_data_size); + problem = "not enough memory to inspect tags"; + if (!raw) goto failed; + status = mcc_cache_load(&runtime->source, raw, identity.tag_data_size, &runtime->report); + problem = mcc_cache_status_string(status); + if (status != MCC_CACHE_OK) goto failed; + runtime->capacity = MCC_CACHE_TAG_CAPACITY; + runtime->tags = mcc_memory_reserve(runtime->report.tag_base, runtime->capacity); + problem = "the MCC tag address window is unavailable"; + if (!runtime->tags) goto failed; + memcpy(runtime->tags, raw, identity.tag_data_size); + runtime->used = identity.tag_data_size; + runtime->tag_index = runtime->tags + 40; + free(raw); + raw = NULL; + problem = "the MCC scenario identity could not be retained for saved games"; + if (!mcc_scenario_identity(runtime, name)) goto failed; + problem = "MCC tag conversion failed (see the preceding diagnostic)"; + if (!mcc_hud_bitmaps_prepare(runtime) || !mcc_tags_prepare(runtime) || !mcc_scripts_prepare(runtime) || + !mcc_geometry_prepare(runtime) || !mcc_audio_prepare(runtime) || + !mcc_bitmaps_prepare(runtime)) goto failed; + /* The runtime tag table is stock-shaped. The independent wire reader's + * report retains model offsets; no other loader sees an MCC header. */ + memset(runtime->tags + 16, 0, 20); + memcpy(runtime->tags, &runtime->tag_index, 4); + memcpy(runtime->tags + 12, &runtime->report.tag_count, 4); + memcpy(runtime->tags + 32, "sgat", 4); + problem = "converted MCC tags did not pass engine validation"; + if (!mcc_tags_validate(runtime)) goto failed; + if (!mcc_runtime_read(runtime, 0, MCC_CACHE_HEADER_BYTES, header)) { + problem = "the map header could not be read"; + goto failed; + } + mcc_loaded = TRUE; + error(_error_silent, "mcc: loaded %s (%lu tags, %lu bytes of runtime tags)", name, + (unsigned long)runtime->report.tag_count, (unsigned long)runtime->used); + return (struct cache_file_tag_header *)runtime->tags; +failed: + error(_error_silent, "mcc: refused %s: %s", name ? name : "(unnamed)", problem); + { + wchar_t text[256]; + char message[256]; + size_t i; + snprintf(message, sizeof(message), "MCC map could not load: %s. See debug.txt for details.", problem); + for (i = 0; message[i] && i < NUMBEROF(text) - 1; ++i) text[i] = (unsigned char)message[i]; + text[i] = 0; + display_error_text_when_main_menu_loaded(text); + } + if (raw) free(raw); + mcc_cache_tags_unload(); + return NULL; +} + +boolean mcc_cache_read(long tag, long offset, long bytes, void *out) +{ + int result; + if (!mcc_loaded || offset < 0 || bytes < 0 || !out) return FALSE; + result = mcc_bitmaps_read(&mcc_loaded_cache, tag, (uint32_t)offset, (uint32_t)bytes, out); + if (result < 0) result = mcc_audio_read(&mcc_loaded_cache, (uint32_t)offset, (uint32_t)bytes, out); + if (result < 0) result = mcc_runtime_read(&mcc_loaded_cache, (uint32_t)offset, (uint32_t)bytes, out); + if (!result) { + memset(out, 0, (size_t)bytes); + error(_error_silent, "mcc: I/O failed at %08lx (%ld bytes)", (unsigned long)offset, bytes); + } + return result ? TRUE : FALSE; +} + +boolean mcc_cache_bsp_load(struct scenario_structure_bsp_reference const *reference, + void **header, void **structure) +{ + uint32_t i; + for (i = 0; mcc_loaded && i < mcc_loaded_cache.report.bsp_count; ++i) { + struct mcc_cache_bsp const *bsp = &mcc_loaded_cache.report.bsps[i]; + if (bsp->tag_handle != (uint32_t)reference->structure_bsp.index) continue; + if (bsp->address != (uint32_t)(uintptr_t)reference->base_address || + bsp->file_offset != (uint32_t)reference->file_offset || bsp->size != (uint32_t)reference->file_size) + return FALSE; + if (!mcc_geometry_bsp_load(&mcc_loaded_cache, bsp->tag_handle, bsp->file_offset, + bsp->size, reference->base_address, structure)) return FALSE; + *header = reference->base_address; + if (!mcc_bsp_validate(&mcc_loaded_cache, (long)bsp->tag_handle, *header, (long)bsp->size)) { + mcc_geometry_bsp_unload(&mcc_loaded_cache); + return FALSE; + } + return TRUE; + } + return FALSE; +} + +void mcc_cache_bsp_unload(void) { mcc_geometry_bsp_unload(&mcc_loaded_cache); } + +short mcc_cache_part_palette(void const *buffer, byte const **nodes) +{ + return mcc_loaded ? mcc_geometry_part_palette(&mcc_loaded_cache, buffer, nodes) : 0; +} + +boolean mcc_cache_bitmap_valid(struct bitmap_data *bitmap) +{ + extern int mcc_bitmaps_valid(struct mcc_runtime *runtime, struct bitmap_data *bitmap); + return mcc_loaded && mcc_bitmaps_valid(&mcc_loaded_cache, bitmap); +} + +void const *mcc_cache_bitmap_pixels(struct bitmap_data const *bitmap,unsigned long *bytes) +{ + uint32_t size=0; + void const *pixels=mcc_loaded ? mcc_bitmaps_pixels(&mcc_loaded_cache,bitmap,&size) : NULL; + if (bytes) *bytes=size; + return pixels; +} diff --git a/port/linux/game/mcc_cache.h b/port/linux/game/mcc_cache.h new file mode 100644 index 0000000000..205beb22e0 --- /dev/null +++ b/port/linux/game/mcc_cache.h @@ -0,0 +1,28 @@ +#ifndef MCC_CACHE_H +#define MCC_CACHE_H + +struct cache_file_tag_header; +struct scenario_structure_bsp_reference; +struct bitmap_data; + +/* Path ownership is decided before stripping a filename. MCC never searches + * maps/, custom_maps/ or the optional Custom Edition installation. */ +boolean mcc_level_name(char const *name); +boolean mcc_cache_available(char const *name); +boolean mcc_cache_present(char const *name, unsigned long checksum, + char *message, long message_size); +boolean mcc_cache_require(char const *name, unsigned long checksum); +unsigned long mcc_cache_checksum(char const *name); +struct cache_file_tag_header *mcc_cache_tags_load(char const *name, void *header); +boolean mcc_cache_tags_loaded(void); +void mcc_cache_tags_unload(void); +boolean mcc_cache_contains(void const *address, long bytes); +boolean mcc_cache_read(long tag, long offset, long bytes, void *out); +boolean mcc_cache_bsp_load(struct scenario_structure_bsp_reference const *reference, + void **header, void **structure); +void mcc_cache_bsp_unload(void); +short mcc_cache_part_palette(void const *buffer, byte const **nodes); +boolean mcc_cache_bitmap_valid(struct bitmap_data *bitmap); +void const *mcc_cache_bitmap_pixels(struct bitmap_data const *bitmap, unsigned long *bytes); + +#endif diff --git a/port/linux/game/mcc_cache_format.c b/port/linux/game/mcc_cache_format.c new file mode 100644 index 0000000000..756acbc3cc --- /dev/null +++ b/port/linux/game/mcc_cache_format.c @@ -0,0 +1,305 @@ +/* Halo 1 MCC cache reader. All file integers are explicitly little-endian. + * Public format references and observed-fixture details: docs/mcc_maps.md. */ +#include "mcc_cache_format.h" +#include +#include + +#define MCC_FOURCC(a,b,c,d) (((uint32_t)(a)<<24)|((uint32_t)(b)<<16)|((uint32_t)(c)<<8)|(uint32_t)(d)) +#define MCC_SCENARIO MCC_FOURCC('s','c','n','r') +#define MCC_BSP MCC_FOURCC('s','b','s','p') + +static uint16_t mcc_u16(void const *p) { + uint8_t const *b = (uint8_t const *)p; + return (uint16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)); +} +static uint32_t mcc_u32(void const *p) { + uint8_t const *b = (uint8_t const *)p; + return (uint32_t)b[0] | ((uint32_t)b[1]<<8) | ((uint32_t)b[2]<<16) | ((uint32_t)b[3]<<24); +} +static int mcc_range(uint64_t offset, uint64_t bytes, uint64_t limit) { + return offset <= limit && bytes <= limit - offset; +} +static enum mcc_cache_status mcc_fail(struct mcc_cache_report *r, + enum mcc_cache_status status, uint32_t location) { + r->status = status; + r->problem_location = location; + return status; +} + +enum mcc_cache_status mcc_cache_identify(void const *header, size_t header_bytes, + uint64_t file_size, struct mcc_cache_identity *out) { + uint8_t const *h = (uint8_t const *)header; + struct mcc_cache_identity v; + if (!h || !out) return MCC_CACHE_BAD_ARGUMENT; + memset(out, 0, sizeof(*out)); + if (header_bytes < MCC_CACHE_HEADER_BYTES) return MCC_CACHE_BAD_HEADER; + if (mcc_u32(h) != MCC_FOURCC('h','e','a','d') || + mcc_u32(h+0x7FC) != MCC_FOURCC('f','o','o','t')) return MCC_CACHE_BAD_HEADER; + if (mcc_u32(h+4) != MCC_CACHE_VERSION) return MCC_CACHE_WRONG_VERSION; + if (file_size < MCC_CACHE_HEADER_BYTES || file_size > 0x7FFFFFFFu) return MCC_CACHE_BAD_FILE_SIZE; + memset(&v, 0, sizeof(v)); + v.file_size = file_size; + v.file_length = mcc_u32(h+8); + if (v.file_length && (v.file_length < MCC_CACHE_HEADER_BYTES || v.file_length > file_size)) + return MCC_CACHE_BAD_FILE_SIZE; + if (!v.file_length) v.file_length = (uint32_t)file_size; + if (!memchr(h+0x20, 0, 32) || !memchr(h+0x40, 0, 32)) return MCC_CACHE_BAD_STRING; + memcpy(v.name, h+0x20, 32); + memcpy(v.build, h+0x40, 32); + v.scenario_type = mcc_u16(h+0x60); + if (v.scenario_type > 1) return MCC_CACHE_BAD_SCENARIO_TYPE; + v.flags = mcc_u16(h+0x68); + v.checksum = mcc_u32(h+0x64); + v.tag_data_offset = mcc_u32(h+0x10); + v.tag_data_size = mcc_u32(h+0x14); + if (v.tag_data_offset < MCC_CACHE_HEADER_BYTES || v.tag_data_size < 0x28 || + v.tag_data_size > MCC_CACHE_TAG_CAPACITY || + !mcc_range(v.tag_data_offset, v.tag_data_size, v.file_length)) return MCC_CACHE_BAD_TAG_RANGE; + *out = v; + return MCC_CACHE_OK; +} + +void *mcc_cache_pointer(void *data, struct mcc_cache_report const *r, + uint32_t address, uint32_t bytes) { + if (!data || !r || address < r->tag_base || + !mcc_range(address-r->tag_base, bytes, r->identity.tag_data_size)) return NULL; + return (uint8_t *)data + (address-r->tag_base); +} + +static void *mcc_block(void *data, struct mcc_cache_report const *r, + uint8_t const *block, uint32_t stride) { + uint32_t count = mcc_u32(block); + if (count > UINT32_MAX / stride) return NULL; + if (!count) return data; + return mcc_cache_pointer(data, r, mcc_u32(block+4), count*stride); +} + +static uint8_t *mcc_bsp_pointer(uint8_t *data, struct mcc_cache_bsp const *b, + uint32_t address, uint32_t bytes) { + if (address < b->address || !mcc_range(address-b->address, bytes, b->size)) return NULL; + return data + (address-b->address); +} + +static enum mcc_cache_status mcc_check_bsp(struct mcc_cache_source const *source, + struct mcc_cache_report *r, struct mcc_cache_bsp *b) { + uint8_t *data, *structure, *lightmaps; + uint32_t i, count; + enum mcc_cache_status status = MCC_CACHE_BAD_BSP; + if (b->size < 0x18 || b->size > MCC_CACHE_TAG_CAPACITY || b->file_offset < MCC_CACHE_HEADER_BYTES || + !mcc_range(b->file_offset, b->size, r->identity.file_length) || + b->address < r->tag_base || + !mcc_range(b->address-r->tag_base, b->size, MCC_CACHE_TAG_CAPACITY) || + b->address-r->tag_base < r->identity.tag_data_size) return MCC_CACHE_BAD_BSP; + data = (uint8_t *)malloc(b->size); + if (!data) return MCC_CACHE_NO_MEMORY; + if (!source->read(source->context,b->file_offset,b->size,data)) { + status = MCC_CACHE_READ_FAILED; + goto done; + } + b->structure_address = mcc_u32(data); + /* Version 13 stores uncompressed environment/lightmap vertices in a + * separate file section, named by the first pair after the BSP pointer. */ + b->vertex_bytes = mcc_u32(data+4); + b->vertex_file_offset = mcc_u32(data+8); + if (mcc_u32(data+0x14) != MCC_BSP || + !mcc_range(b->vertex_file_offset,b->vertex_bytes,r->identity.file_length)) goto done; + structure = mcc_bsp_pointer(data,b,b->structure_address,0x288); + if (!structure) goto done; + count = mcc_u32(structure+0x104); + if (count > b->size/0x20) goto done; + lightmaps = mcc_bsp_pointer(data,b,mcc_u32(structure+0x108),count*0x20); + if (count && !lightmaps) goto done; + for (i=0;i b->size/0x100) goto done; + materials = mcc_bsp_pointer(data,b,mcc_u32(lm+0x18),material_count*0x100); + if (material_count && !materials) goto done; + for (j=0;jvertex_bytes) || + !mcc_range(mcc_u32(m+0xCC),(uint64_t)lit*20,b->vertex_bytes) || + (lit && lit != rendered)) goto done; + r->bsp_material_count++; + } + } + status = MCC_CACHE_OK; +done: + free(data); + return status; +} + +static enum mcc_cache_status mcc_check_assets(void *data, struct mcc_cache_report *r, + uint32_t group, uint32_t address) { + uint8_t *tag; + uint32_t i, count; + if (group == MCC_FOURCC('b','i','t','m')) { + uint8_t *bitmaps; + tag = mcc_cache_pointer(data,r,address,0x6C); + if (!tag) return MCC_CACHE_BAD_BITMAP; + count = mcc_u32(tag+0x60); + bitmaps = mcc_block(data,r,tag+0x60,0x30); + if (!bitmaps) return MCC_CACHE_BAD_BITMAP; + for (i=0;i18) + return MCC_CACHE_BAD_BITMAP; + if (flags & 0x100) r->external_bitmap_count++; + else if (!mcc_range(mcc_u32(b+0x18),mcc_u32(b+0x1C),r->identity.file_length)) + return MCC_CACHE_BAD_BITMAP; + r->bitmap_count++; + if (format==18) r->bc7_bitmap_count++; + } + } + if (group == MCC_FOURCC('s','n','d','!')) { + uint8_t *ranges; + tag = mcc_cache_pointer(data,r,address,0xA4); + if (!tag) return MCC_CACHE_BAD_SOUND; + count = mcc_u32(tag+0x98); + ranges = mcc_block(data,r,tag+0x98,0x48); + if (!ranges) return MCC_CACHE_BAD_SOUND; + for (i=0;iexternal_sound_count++; + else if (!mcc_range(mcc_u32(p+0x48),mcc_u32(p+0x40),r->identity.file_length)) + return MCC_CACHE_BAD_SOUND; + r->sound_permutation_count++; + } + } + } + return MCC_CACHE_OK; +} + +enum mcc_cache_status mcc_cache_load(struct mcc_cache_source const *source, + void *data, size_t capacity, struct mcc_cache_report *r) { + uint8_t header[MCC_CACHE_HEADER_BYTES], *tags = (uint8_t *)data, *scenario, *bsp_refs; + uint32_t i, index_address; + enum mcc_cache_status status; + if (!source || !source->read || !data || !r) return MCC_CACHE_BAD_ARGUMENT; + memset(r,0,sizeof(*r)); + r->problem_tag = UINT32_MAX; + if (source->sizeread(source->context,0,sizeof(header),header)) + return mcc_fail(r,MCC_CACHE_READ_FAILED,0); + status = mcc_cache_identify(header,sizeof(header),source->size,&r->identity); + if (status) return mcc_fail(r,status,0); + if (capacityidentity.tag_data_size) return mcc_fail(r,MCC_CACHE_NO_MEMORY,0); + if (!source->read(source->context,r->identity.tag_data_offset,r->identity.tag_data_size,data)) + return mcc_fail(r,MCC_CACHE_READ_FAILED,r->identity.tag_data_offset); + index_address = mcc_u32(tags); + if (index_address<0x28 || (index_address&3) || mcc_u32(tags+0x24)!=MCC_FOURCC('t','a','g','s')) + return mcc_fail(r,MCC_CACHE_BAD_TAG_INDEX,r->identity.tag_data_offset); + r->tag_base = index_address-0x28; + r->tag_count = mcc_u32(tags+0xC); + r->scenario_handle = mcc_u32(tags+4); + if (!r->tag_count || r->tag_count>65535 || + !mcc_range(0x28,(uint64_t)r->tag_count*0x20,r->identity.tag_data_size) || + !mcc_range(r->tag_base,MCC_CACHE_TAG_CAPACITY,UINT64_C(0x100000000)) || + (r->scenario_handle&0xFFFF)>=r->tag_count) + return mcc_fail(r,MCC_CACHE_BAD_TAG_INDEX,index_address); + r->model_file_offset=mcc_u32(tags+0x14); + r->model_vertex_bytes=mcc_u32(tags+0x1C); + r->model_bytes=mcc_u32(tags+0x20); + if (r->model_vertex_bytes>r->model_bytes || + !mcc_range(r->model_file_offset,r->model_bytes,r->identity.file_length)) + return mcc_fail(r,MCC_CACHE_BAD_MODEL_RANGE,r->model_file_offset); + for (i=0;itag_count;i++) { + uint8_t *entry=tags+0x28+i*0x20; + uint32_t group=mcc_u32(entry), address=mcc_u32(entry+0x14), name_address=mcc_u32(entry+0x10); + char *name=mcc_cache_pointer(data,r,name_address,1); + r->problem_tag=i; + if ((mcc_u32(entry+0xC)&0xFFFF)!=i || !name || + !memchr(name,0,r->identity.tag_data_size-(name_address-r->tag_base))) + return mcc_fail(r,MCC_CACHE_BAD_TAG,index_address+i*0x20); + if (mcc_u32(entry+0x18)) return mcc_fail(r,MCC_CACHE_EXTERNAL_TAG,index_address+i*0x20); + if (group!=MCC_BSP && !mcc_cache_pointer(data,r,address,1)) + return mcc_fail(r,MCC_CACHE_BAD_TAG,address); + if (group==MCC_FOURCC('m','o','d','2')) r->model_count++; + if (group==MCC_FOURCC('s','o','t','r')) r->generic_shader_count++; + status=mcc_check_assets(data,r,group,address); + if (status) return mcc_fail(r,status,address); + } + r->problem_tag=r->scenario_handle&0xFFFF; + { + uint8_t *entry=tags+0x28+r->problem_tag*0x20; + if (mcc_u32(entry)!=MCC_SCENARIO || mcc_u32(entry+0xC)!=r->scenario_handle) + return mcc_fail(r,MCC_CACHE_BAD_SCENARIO,index_address+r->problem_tag*0x20); + scenario=mcc_cache_pointer(data,r,mcc_u32(entry+0x14),0x5B0); + if (!scenario || mcc_u16(scenario+0x3C)!=r->identity.scenario_type) + return mcc_fail(r,MCC_CACHE_BAD_SCENARIO,mcc_u32(entry+0x14)); + } + r->script_count=mcc_u32(scenario+0x49C); + r->global_count=mcc_u32(scenario+0x4A8); + { + uint32_t syntax_size=mcc_u32(scenario+0x474); + uint8_t *syntax=mcc_cache_pointer(data,r,mcc_u32(scenario+0x480),syntax_size); + uint8_t *scripts=mcc_block(data,r,scenario+0x49C,0x5C); + if ((syntax_size && (!syntax || syntax_size<0x38 || (syntax_size-0x38)%0x14)) || !scripts || + !mcc_block(data,r,scenario+0x4A8,0x5C)) return mcc_fail(r,MCC_CACHE_BAD_SCRIPT,0); + if (syntax_size) r->script_node_count=(syntax_size-0x38)/0x14; + for (i=0;iscript_count;i++) { + if (mcc_u32(scripts+i*0x5C+0x50)) r->parameterized_script_count++; + } + } + r->bsp_count=mcc_u32(scenario+0x5A4); + bsp_refs=mcc_block(data,r,scenario+0x5A4,0x20); + if (!r->bsp_count || r->bsp_count>MCC_CACHE_MAX_BSPS || !bsp_refs) + return mcc_fail(r,MCC_CACHE_BAD_BSP,0); + for (i=0;ibsp_count;i++) { + uint8_t *ref=bsp_refs+i*0x20; + struct mcc_cache_bsp *b=&r->bsps[i]; + b->file_offset=mcc_u32(ref); b->size=mcc_u32(ref+4); + b->address=mcc_u32(ref+8); b->tag_handle=mcc_u32(ref+0x1C); + r->problem_tag=b->tag_handle&0xFFFF; + if (r->problem_tag>=r->tag_count || + mcc_u32(tags+0x28+r->problem_tag*0x20)!=MCC_BSP || + mcc_u32(tags+0x34+r->problem_tag*0x20)!=b->tag_handle) + return mcc_fail(r,MCC_CACHE_BAD_BSP,b->file_offset); + status=mcc_check_bsp(source,r,b); + if (status) return mcc_fail(r,status,b->file_offset); + if (b->size>r->largest_bsp_bytes) r->largest_bsp_bytes=b->size; + } + r->problem_tag=UINT32_MAX; + r->status=MCC_CACHE_OK; + return MCC_CACHE_OK; +} + +enum mcc_cache_status mcc_cache_inspect(struct mcc_cache_source const *source, + struct mcc_cache_report *r) { + uint8_t header[MCC_CACHE_HEADER_BYTES]; + struct mcc_cache_identity identity; + enum mcc_cache_status status; + void *tags; + if (!source || !source->read || !r) return MCC_CACHE_BAD_ARGUMENT; + memset(r,0,sizeof(*r)); r->problem_tag=UINT32_MAX; + if (source->sizeread(source->context,0,sizeof(header),header)) + return mcc_fail(r,MCC_CACHE_READ_FAILED,0); + status=mcc_cache_identify(header,sizeof(header),source->size,&identity); + if (status) return mcc_fail(r,status,0); + tags=malloc(identity.tag_data_size); + if (!tags) return mcc_fail(r,MCC_CACHE_NO_MEMORY,0); + status=mcc_cache_load(source,tags,identity.tag_data_size,r); + free(tags); + return status; +} + +char const *mcc_cache_status_string(enum mcc_cache_status s) { + static char const *const text[]={ + "ok", "invalid reader argument", "map read failed", "invalid MCC cache header", + "not a Halo 1 MCC version-13 cache", "invalid MCC map file length", + "unterminated MCC header string", "MCC map is not singleplayer or multiplayer", + "invalid MCC tag data range", "insufficient memory for MCC tags", + "invalid MCC tag index", "invalid MCC tag entry", "invalid MCC scenario", + "invalid MCC model data range", "invalid MCC BSP or vertex range", + "invalid MCC bitmap data", "invalid MCC sound data", "invalid MCC script data", + "external indexed MCC tags require a resource provider" + }; + return (unsigned)s +#include + +#define MCC_CACHE_HEADER_BYTES 0x800u +#define MCC_CACHE_VERSION 13u +#define MCC_CACHE_TAG_CAPACITY 0x04000000u +#define MCC_CACHE_MAX_BSPS 32u + +enum mcc_cache_status { + MCC_CACHE_OK = 0, + MCC_CACHE_BAD_ARGUMENT, + MCC_CACHE_READ_FAILED, + MCC_CACHE_BAD_HEADER, + MCC_CACHE_WRONG_VERSION, + MCC_CACHE_BAD_FILE_SIZE, + MCC_CACHE_BAD_STRING, + MCC_CACHE_BAD_SCENARIO_TYPE, + MCC_CACHE_BAD_TAG_RANGE, + MCC_CACHE_NO_MEMORY, + MCC_CACHE_BAD_TAG_INDEX, + MCC_CACHE_BAD_TAG, + MCC_CACHE_BAD_SCENARIO, + MCC_CACHE_BAD_MODEL_RANGE, + MCC_CACHE_BAD_BSP, + MCC_CACHE_BAD_BITMAP, + MCC_CACHE_BAD_SOUND, + MCC_CACHE_BAD_SCRIPT, + MCC_CACHE_EXTERNAL_TAG +}; + +struct mcc_cache_identity { + uint64_t file_size; + uint32_t file_length, tag_data_offset, tag_data_size, checksum; + uint16_t scenario_type, flags; + char name[32], build[32]; +}; + +/* All reads are absolute file offsets; a successful read fills every byte. */ +struct mcc_cache_source { + void *context; + int (*read)(void *context, uint32_t offset, uint32_t size, void *out); + uint64_t size; +}; + +struct mcc_cache_bsp { + uint32_t file_offset, size, address, tag_handle; + uint32_t structure_address; + uint32_t vertex_file_offset, vertex_bytes; +}; + +struct mcc_cache_report { + struct mcc_cache_identity identity; + enum mcc_cache_status status; + uint32_t problem_tag, problem_location; + uint32_t tag_base, tag_count, scenario_handle; + uint32_t model_file_offset, model_vertex_bytes, model_bytes; + uint32_t bsp_count, largest_bsp_bytes, bsp_material_count; + uint32_t bitmap_count, bc7_bitmap_count, external_bitmap_count; + uint32_t sound_permutation_count, external_sound_count; + uint32_t model_count, generic_shader_count; + uint32_t script_count, global_count, script_node_count; + uint32_t parameterized_script_count; + struct mcc_cache_bsp bsps[MCC_CACHE_MAX_BSPS]; +}; + +/* Fast menu probe: needs only the first 2048 bytes and the actual file length. + * A successful probe identifies a map; it does NOT certify it can run. */ +enum mcc_cache_status mcc_cache_identify(void const *header, size_t header_bytes, + uint64_t file_size, struct mcc_cache_identity *identity); + +/* Load raw tags without changing their wire contents. Caller owns the buffer; + * it need not be at the map's linked address for inspection. The index, + * scenario, BSP geometry and raw asset ranges are checked independently. + * Runtime conversion and engine tag validation remain separate obligations. */ +enum mcc_cache_status mcc_cache_load(struct mcc_cache_source const *source, + void *tag_data, size_t capacity, struct mcc_cache_report *report); + +/* Same audit using a temporary tag buffer, freed before this call returns. */ +enum mcc_cache_status mcc_cache_inspect(struct mcc_cache_source const *source, + struct mcc_cache_report *report); + +/* Return a validated slice within loaded raw tags, or NULL. */ +void *mcc_cache_pointer(void *tag_data, struct mcc_cache_report const *report, + uint32_t address, uint32_t bytes); +char const *mcc_cache_status_string(enum mcc_cache_status status); + +#endif diff --git a/port/linux/game/mcc_checkpoint.c b/port/linux/game/mcc_checkpoint.c new file mode 100644 index 0000000000..16b7e8a079 --- /dev/null +++ b/port/linux/game/mcc_checkpoint.c @@ -0,0 +1,190 @@ +/* MCC grenade counts travel inside the complete native game-state image. + * The CPU allocator's high-water mark proves this fixed tail is unused; + * no game_state_malloc call, pool size, or allocation checksum is changed. + * Original tail bytes are retained and restored when MCC ownership ends. */ +#include "cseries.h" +#include "errors.h" +#include "saved games/game_state.h" +#include "memory/data.h" +#include "objects/objects.h" +#include "mcc_cache.h" +#include "mcc_checkpoint.h" +#include "mcc_grenades.h" +#include +#include +#include + +#define MCC_CHECKPOINT_PAYLOAD_LIMIT (8u + 8192u * 8u) +#define MCC_CHECKPOINT_FOOTER_BYTES 96u +#define MCC_CHECKPOINT_SLOT_BYTES (MCC_CHECKPOINT_PAYLOAD_LIMIT + MCC_CHECKPOINT_FOOTER_BYTES) + +static struct { + unsigned char *original; + unsigned char *address; +} mcc_checkpoint_owner; + +static uint32_t mcc_checkpoint_word(void const *data) +{ + unsigned char const *p = data; + return p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static void mcc_checkpoint_put(void *data, uint32_t value) +{ + unsigned char *p = data; + p[0] = (unsigned char)value; p[1] = (unsigned char)(value >> 8); + p[2] = (unsigned char)(value >> 16); p[3] = (unsigned char)(value >> 24); +} + +static uint32_t mcc_checkpoint_checksum(unsigned char const *data, uint32_t bytes) +{ + uint32_t result = 0xFFFFFFFFu, i, bit; + for (i = 0; i < bytes; ++i) { + result ^= data[i]; + for (bit = 0; bit < 8; ++bit) + result = (result >> 1) ^ (0xEDB88320u & (0u - (result & 1u))); + } + return ~result; +} + +static unsigned char *mcc_checkpoint_tail(void const *image, unsigned long total, + unsigned long used, unsigned long capacity) +{ + if (!image || total < sizeof(struct game_state_header) || capacity > total || + used < sizeof(struct game_state_header) || used > capacity || + MCC_CHECKPOINT_SLOT_BYTES > capacity - used) return NULL; + return (unsigned char *)image + capacity - MCC_CHECKPOINT_SLOT_BYTES; +} + +static int mcc_checkpoint_identity(struct game_state_header const *header) +{ + return memchr(header->map_name, 0, 64) && mcc_level_name(header->map_name); +} + +static int mcc_checkpoint_claim(unsigned char *tail) +{ + if (mcc_checkpoint_owner.original) return mcc_checkpoint_owner.address == tail; + mcc_checkpoint_owner.original = malloc(MCC_CHECKPOINT_SLOT_BYTES); + if (!mcc_checkpoint_owner.original) return 0; + memcpy(mcc_checkpoint_owner.original, tail, MCC_CHECKPOINT_SLOT_BYTES); + mcc_checkpoint_owner.address = tail; + return 1; +} + +/* Saved native pointers still address the fixed live arena. Translate them + * only after proving the full prospective object/header lies in CPU memory. */ +static void const *mcc_checkpoint_incoming(void const *image, void const *current, + unsigned long used, void const *address, unsigned long bytes) +{ + uintptr_t offset = (uintptr_t)address - (uintptr_t)current; + if (offset > used || bytes > used - offset) return NULL; + return (unsigned char const *)image + offset; +} + +static int mcc_checkpoint_units(void const *image, void const *current, + unsigned long used, unsigned char const *payload) +{ + unsigned count = payload[6] | ((unsigned)payload[7] << 8), i; + struct data_array const *array; + struct object_header_datum const *headers; + if (!count) return 1; + array = mcc_checkpoint_incoming(image, current, used, object_header_data, sizeof(*array)); + if (!array || !array->valid || array->size != sizeof(*headers) || array->count < 0 || + array->count > 8192 || array->count > array->maximum_count) return 0; + headers = mcc_checkpoint_incoming(image, current, used, array->data, + (unsigned long)array->count * sizeof(*headers)); + if (!headers) return 0; + for (i = 0; i < count; ++i) { + unsigned char const *record = payload + 8 + i * 8; + uint32_t handle = mcc_checkpoint_word(record), index = handle & 0xFFFFu; + struct object_header_datum const *object; + if (index >= (unsigned)array->count) return 0; + object = headers + index; + if ((uint16_t)object->identifier != (uint16_t)(handle >> 16) || + object->type >= 32 || !(_object_mask_unit & (1u << object->type)) || + !mcc_checkpoint_incoming(image, current, used, object->datum, sizeof(struct object_datum)) || + (record[4] && mcc_grenades_type_count() < 3) || + (record[5] && mcc_grenades_type_count() < 4)) return 0; + } + return 1; +} + +int mcc_checkpoint_capture(void *image, unsigned long total, + unsigned long used, unsigned long capacity) +{ + struct game_state_header const *header = image; + unsigned char *tail, *footer; + uint32_t bytes; + if (!mcc_cache_tags_loaded()) return 1; + tail = mcc_checkpoint_tail(image, total, used, capacity); + if (!tail || !mcc_checkpoint_identity(header) || !mcc_checkpoint_claim(tail)) goto failed; + bytes = mcc_grenades_snapshot(NULL, 0); + if (bytes < 8 || bytes > MCC_CHECKPOINT_PAYLOAD_LIMIT) goto failed; + footer = tail + MCC_CHECKPOINT_PAYLOAD_LIMIT; + /* Invalidate the previous footer before any partial payload write. */ + memset(footer, 0, MCC_CHECKPOINT_FOOTER_BYTES); + if (mcc_grenades_snapshot(tail, bytes) != bytes || !mcc_grenades_snapshot_validate(tail, bytes)) goto failed; + memcpy(footer, "MCCS", 4); + mcc_checkpoint_put(footer + 4, 1); + mcc_checkpoint_put(footer + 8, bytes); + mcc_checkpoint_put(footer + 12, mcc_checkpoint_checksum(tail, bytes)); + mcc_checkpoint_put(footer + 16, (uint32_t)header->cache_file_checksum); + memcpy(footer + 24, header->map_name, strlen(header->map_name) + 1); + return 1; +failed: + error(_error_silent, "mcc checkpoint: the extra state could not be captured in unused CPU memory"); + return 0; +} + +int mcc_checkpoint_validate(void const *image, unsigned long total, + unsigned long used, unsigned long capacity, void const *current_image) +{ + struct game_state_header const *header = image, *current = current_image; + unsigned char *tail, *current_tail, *footer; + uint32_t bytes; + if (!mcc_cache_tags_loaded()) return 1; + tail = mcc_checkpoint_tail(image, total, used, capacity); + current_tail = mcc_checkpoint_tail(current_image, total, used, capacity); + if (!tail || !current_tail || !mcc_checkpoint_identity(header) || + !mcc_checkpoint_identity(current) || strcmp(header->map_name, current->map_name) || + header->cache_file_checksum != current->cache_file_checksum) goto failed; + footer = tail + MCC_CHECKPOINT_PAYLOAD_LIMIT; + bytes = mcc_checkpoint_word(footer + 8); + if (memcmp(footer, "MCCS", 4) || mcc_checkpoint_word(footer + 4) != 1 || + bytes < 8 || bytes > MCC_CHECKPOINT_PAYLOAD_LIMIT || + mcc_checkpoint_word(footer + 12) != mcc_checkpoint_checksum(tail, bytes) || + mcc_checkpoint_word(footer + 16) != (uint32_t)header->cache_file_checksum || + mcc_checkpoint_word(footer + 20) || mcc_checkpoint_word(footer + 88) || + mcc_checkpoint_word(footer + 92) || !memchr(footer + 24, 0, 64) || + strcmp((char const *)footer + 24, header->map_name) || + !mcc_grenades_snapshot_validate(tail, bytes) || + !mcc_checkpoint_units(image, current_image, used, tail) || + !mcc_checkpoint_claim(current_tail)) goto failed; + return 1; +failed: + error(_error_silent, "mcc checkpoint: missing, mismatched or damaged extra state; image refused"); + return 0; +} + +int mcc_checkpoint_restore(void const *image, unsigned long total, + unsigned long used, unsigned long capacity) +{ + unsigned char *tail; + uint32_t bytes; + if (!mcc_cache_tags_loaded()) return 1; + if (!mcc_checkpoint_validate(image, total, used, capacity, image)) return 0; + tail = mcc_checkpoint_tail(image, total, used, capacity); + bytes = mcc_checkpoint_word(tail + MCC_CHECKPOINT_PAYLOAD_LIMIT + 8); + if (mcc_grenades_restore(tail, bytes)) return 1; + error(_error_silent, "mcc checkpoint: grenade records do not match restored units"); + return 0; +} + +void mcc_checkpoint_dispose(void) +{ + if (mcc_checkpoint_owner.original) { + memcpy(mcc_checkpoint_owner.address, mcc_checkpoint_owner.original, MCC_CHECKPOINT_SLOT_BYTES); + free(mcc_checkpoint_owner.original); + } + memset(&mcc_checkpoint_owner, 0, sizeof(mcc_checkpoint_owner)); +} diff --git a/port/linux/game/mcc_checkpoint.h b/port/linux/game/mcc_checkpoint.h new file mode 100644 index 0000000000..9963932f27 --- /dev/null +++ b/port/linux/game/mcc_checkpoint.h @@ -0,0 +1,11 @@ +/* MCC-only data carried in the checked, otherwise unused CPU arena tail. */ +#ifndef MCC_CHECKPOINT_H +#define MCC_CHECKPOINT_H +int mcc_checkpoint_capture(void *image, unsigned long total_bytes, + unsigned long cpu_used, unsigned long cpu_capacity); +int mcc_checkpoint_validate(void const *image, unsigned long total_bytes, + unsigned long cpu_used, unsigned long cpu_capacity, void const *current_image); +int mcc_checkpoint_restore(void const *image, unsigned long total_bytes, + unsigned long cpu_used, unsigned long cpu_capacity); +void mcc_checkpoint_dispose(void); +#endif diff --git a/port/linux/game/mcc_geometry.c b/port/linux/game/mcc_geometry.c new file mode 100644 index 0000000000..b380fd1a98 --- /dev/null +++ b/port/linux/game/mcc_geometry.c @@ -0,0 +1,420 @@ +/* Geometry adapter for MCC caches. Wire model and BSP streams are read only; + * Xbox render descriptors are constructed in MCC-owned allocations. */ +#include "cseries.h" +#include "errors.h" +#include "tag_files/tag_groups.h" +#include "models/model_definitions.h" +#include "rasterizer/rasterizer.h" +#include "rasterizer/rasterizer_geometry.h" +#include "rasterizer/rasterizer_model_types.h" +#include "structures/structure_bsp_definitions.h" +#include "mcc_runtime.h" + +#include +#include +#include + +struct mcc_render_part { + unsigned long flags; + short shader; + signed char previous, next; + short centroid_nodes[2]; + float centroid_weights[2]; + real_point3d centroid; + struct tag_block uncompressed, compressed, triangles; + struct triangle_buffer indices; + struct vertex_buffer vertices; +}; + +struct mcc_geometry_buffer { + struct mcc_geometry_buffer *next; + void *descriptor; + void *payload; + uint32_t bytes; + void *allocation; + uint32_t allocation_bytes; + int is_index, is_bsp; + unsigned char palette[22]; + unsigned char palette_count; +}; + +struct mcc_converted_model { + struct mcc_converted_model *next; + void *address; +}; + +struct mcc_geometry_state { + struct mcc_geometry_buffer *buffers; + struct mcc_converted_model *models; +}; + +typedef char mcc_render_part_size_check[sizeof(struct mcc_render_part)==0x68 ? 1 : -1]; + +static uint16_t mg_u16(void const *p) { + unsigned char const *v=p; + return (uint16_t)(v[0]|((uint16_t)v[1]<<8)); +} +static uint32_t mg_u32(void const *p) { + unsigned char const *v=p; + return v[0]|((uint32_t)v[1]<<8)|((uint32_t)v[2]<<16)|((uint32_t)v[3]<<24); +} +static int mg_range(uint32_t offset,uint64_t length,uint32_t capacity) { + return offset<=capacity && length<=capacity-offset; +} +static int mg_error(char const *text) { + error(_error_silent,"MCC geometry: %s",text); + return 0; +} + +static void *mg_block(struct mcc_runtime *r, struct tag_block const *b, uint32_t stride) { + if (b->count<0 || (uint32_t)b->count>UINT32_MAX/stride) return NULL; + if (!b->count) return r->tags; + return mcc_runtime_pointer(r,(uint32_t)(uintptr_t)b->address,(uint32_t)b->count*stride); +} + +static struct mcc_geometry_buffer *mg_record(struct mcc_runtime *r,void *descriptor, + void *payload,uint32_t bytes,void *allocation,uint32_t allocation_bytes,int index,int bsp) { + struct mcc_geometry_state *state=r->geometry; + struct mcc_geometry_buffer *b=malloc(sizeof(*b)); + if (!b) return NULL; + memset(b,0,sizeof(*b)); + b->descriptor=descriptor; b->payload=payload; b->bytes=bytes; + b->allocation=allocation; b->allocation_bytes=allocation_bytes; + b->is_index=index; b->is_bsp=bsp; + b->next=state->buffers; state->buffers=b; + return b; +} + +static struct mcc_geometry_buffer *mg_vertices(struct mcc_runtime *r, + struct vertex_buffer *descriptor,short type,uint32_t count,void *payload,uint32_t bytes, + void *allocation,uint32_t allocation_bytes,int bsp) { + struct mcc_geometry_buffer *b=mg_record(r,descriptor,payload,bytes,allocation,allocation_bytes,0,bsp); + if (!b) { if (allocation) free(allocation); return NULL; } + if (!rasterizer_vertex_buffer_new(descriptor,type,(long)count,payload,(long)bytes)) return NULL; + return b; +} + +static int mg_indices(struct mcc_runtime *r,struct triangle_buffer *descriptor, + uint32_t triangles,void *payload,uint32_t bytes) { + if (!mg_record(r,descriptor,payload,bytes,payload,bytes,1,0)) { free(payload); return 0; } + return rasterizer_triangle_buffer_new(descriptor,_triangle_buffer_type_precompiled_strip, + (long)triangles,payload); +} + +/* Check every floating-point attribute before calling the engine compressor; + * it assumes finite normals and asserts on components outside [-1.005,1.005]. + * The compiler builtin is available even with the Linux port's C89 headers. */ +static int mg_float_vertices(void const *data,uint32_t count,uint32_t stride, + uint32_t float_count,uint32_t vector_start,uint32_t vector_components) { + uint32_t i,j; + for (i=0;i=vector_start && j1.005f) return 0; + } + } + return 1; +} + +static int mg_palette_index(unsigned char *palette,unsigned char *count,short node) { + unsigned char i; + for (i=0;i<*count;i++) if (palette[i]==node) return i; + if (*count==22) return -1; + palette[*count]=(unsigned char)node; + return (*count)++; +} + +static int mg_part(struct mcc_runtime *r,struct model const *model,unsigned char const *wire, + uint32_t part_index,uint32_t part_count,struct mcc_render_part *part) { + uint32_t count=mg_u32(wire+0x58), triangles=mg_u32(wire+0x48); + uint32_t vertex_offset=mg_u32(wire+0x64), index_offset=mg_u32(wire+0x4C); + uint32_t i,raw_bytes,vertex_bytes,index_bytes; + struct model_vertex_uncompressed *vertices=NULL; + struct model_vertex_compressed *compressed=NULL; + unsigned short *indices=NULL; + unsigned char palette[22],palette_count=0; + int local=(model->flags&2)!=0, many=model->nodes.count>=RASTERIZER_MAXIMUM_NODES_PER_MODEL; + struct mcc_geometry_buffer *record; + (void)part_index; + memset(part,0,sizeof(*part)); + if (!count || count>65535 || !triangles || triangles>65535 || mg_u16(wire+0x44)!=1 || + mg_u16(wire+0x54)!=4 || model->nodes.count<1 || model->nodes.count>64 || + mg_u16(wire+4)>=(uint32_t)model->shaders.count || + ((signed char)wire[6]!=-1 && (unsigned char)wire[6]>=part_count) || + ((signed char)wire[7]!=-1 && (unsigned char)wire[7]>=part_count)) + return mg_error("invalid model part descriptor"); + raw_bytes=count*68; vertex_bytes=count*32; index_bytes=(triangles+2)*2; + if (!mg_range(vertex_offset,raw_bytes,r->report.model_vertex_bytes) || + !mg_range(index_offset,index_bytes,r->report.model_bytes-r->report.model_vertex_bytes)) + return mg_error("model stream exceeds its declared range"); + if (local) { + palette_count=wire[0x6B]; + if (!palette_count || palette_count>22) return mg_error("invalid model node palette size"); + memcpy(palette,wire+0x6C,palette_count); + for (i=0;i=model->nodes.count) + return mg_error("model node palette references a missing node"); + } + vertices=malloc(raw_bytes); compressed=malloc(vertex_bytes); indices=malloc(index_bytes); + if (!vertices || !compressed || !indices) goto fail; + if (!mcc_runtime_read(r,r->report.model_file_offset+vertex_offset,raw_bytes,vertices) || + !mcc_runtime_read(r,r->report.model_file_offset+r->report.model_vertex_bytes+index_offset,index_bytes,indices)) goto fail; + if (!mg_float_vertices(vertices,count,68,14,3,9)) goto fail; + for (i=0;i=(local ? palette_count : model->nodes.count)) goto fail; + if (local && !many) vertices[i].nodes[j]=palette[node]; + else if (many && !local) { + int remap=mg_palette_index(palette,&palette_count,node); + if (remap<0) goto fail; + vertices[i].nodes[j]=(short)remap; + } + } + } + for (i=0;i=count) goto fail; + /* Only the common 32-byte material/centroid prefix is shared by the two + * formats. Blocks and hardware descriptors are new runtime objects. */ + memcpy(part,wire,0x20); + part->flags&=~2u; + if (local) { + for (i=0;i<2;i++) { + short node=part->centroid_nodes[i]; + if (node!=-1 && (node<0 || node>=palette_count)) goto fail; + if (node!=-1) part->centroid_nodes[i]=palette[node]; + } + } + rasterizer_geometry_compress_vertices(_rasterizer_vertex_type_model_uncompressed, + (long)count,compressed,(long)vertex_bytes,vertices,(long)raw_bytes); + free(vertices); vertices=NULL; + record=mg_vertices(r,&part->vertices,_rasterizer_vertex_type_model_compressed,count, + compressed,vertex_bytes,compressed,vertex_bytes,0); + compressed=NULL; + if (!record) goto fail; + if (many) { memcpy(record->palette,palette,palette_count); record->palette_count=palette_count; } + if (!mg_indices(r,&part->indices,triangles,indices,index_bytes)) { indices=NULL; goto fail; } + return 1; +fail: + if (vertices) free(vertices); + if (compressed) free(compressed); + if (indices) free(indices); + return mg_error("model vertices, node indices or hardware buffers could not be converted"); +} + +int mcc_geometry_prepare(struct mcc_runtime *r) { + uint32_t i; + struct mcc_geometry_state *state; + if (!r || r->geometry) return 0; + state=malloc(sizeof(*state)); + if (!state) return 0; + memset(state,0,sizeof(*state)); + r->geometry=state; + for (i=0;ireport.tag_count;i++) { + unsigned char *entry=r->tag_index+i*0x20; + struct model *model; + struct mcc_converted_model *known; + unsigned char *geometries,*new_geometries; + long g; + if (mg_u32(entry)!='mod2') continue; + model=mcc_runtime_pointer(r,mg_u32(entry+0x14),sizeof(*model)); + if (!model) return mg_error("model header lies outside loaded tags"); + for (known=state->models;known;known=known->next) if (known->address==model) break; + if (known) { *(uint32_t *)entry='mode'; continue; } + geometries=mg_block(r,&model->geometries,0x30); + if (!geometries || model->geometries.count>256) return mg_error("invalid model geometry block"); + new_geometries=mcc_runtime_allocate(r,(uint32_t)model->geometries.count*0x30); + if (model->geometries.count && !new_geometries) return mg_error("no room for model geometry descriptors"); + if (model->geometries.count) memcpy(new_geometries,geometries,(uint32_t)model->geometries.count*0x30); + for (g=0;ggeometries.count;g++) { + struct tag_block *block=(struct tag_block *)(geometries+g*0x30+0x24); + struct tag_block *destination=(struct tag_block *)(new_geometries+g*0x30+0x24); + unsigned char *parts=mg_block(r,block,0x84); + struct mcc_render_part *new_parts; + long p; + if (!parts || block->count>128) return mg_error("invalid model parts block"); + new_parts=mcc_runtime_allocate(r,(uint32_t)block->count*sizeof(*new_parts)); + if (block->count && !new_parts) return mg_error("no room for model part descriptors"); + for (p=0;pcount;p++) if (!mg_part(r,model,parts+p*0x84,(uint32_t)p,(uint32_t)block->count,&new_parts[p])) return 0; + destination->address=new_parts; + destination->definition=NULL; + } + model->geometries.address=new_geometries; + model->geometries.definition=NULL; + model->flags&=~2u; + known=malloc(sizeof(*known)); + if (!known) return 0; + known->address=model; known->next=state->models; state->models=known; + *(uint32_t *)entry='mode'; + } + return 1; +} + +static void *mg_bsp_pointer(void *base,uint32_t size,uint32_t address,uint32_t bytes) { + uintptr_t start=(uintptr_t)base; + if (addressvertices.count, lit=(uint32_t)m->lightmap_vertices.count; + uint32_t vo=(uint32_t)m->vertices.offset, lo=(uint32_t)m->lightmap_vertices.offset; + uint32_t raw_bytes,compressed_bytes; + unsigned char *raw,*compressed; + if (count>65535 || lit>65535 || (lit && lit!=count) || + !mg_range(vo,(uint64_t)count*56,b->vertex_bytes) || + !mg_range(lo,(uint64_t)lit*20,b->vertex_bytes)) return mg_error("invalid BSP material vertex range"); + memset(&m->vertices,0,sizeof(m->vertices)); + memset(&m->lightmap_vertices,0,sizeof(m->lightmap_vertices)); + m->vertices.type=_rasterizer_vertex_type_environment_compressed; + m->lightmap_vertices.type=_rasterizer_vertex_type_environment_lightmap_compressed; + memset(&m->uncompressed_vertex_data,0,sizeof(m->uncompressed_vertex_data)); + memset(&m->compressed_vertex_data,0,sizeof(m->compressed_vertex_data)); + if (!count) return 1; + raw_bytes=count*56+lit*20; + compressed_bytes=count*32+lit*8; + raw=malloc(raw_bytes); compressed=malloc(compressed_bytes); + if (!raw || !compressed) { + if (raw) free(raw); + if (compressed) free(compressed); + return 0; + } + if (!mcc_runtime_read(r,b->vertex_file_offset+vo,count*56,raw) || + (lit && !mcc_runtime_read(r,b->vertex_file_offset+lo,lit*20,raw+count*56)) || + !mg_float_vertices(raw,count,56,14,3,9) || + !mg_float_vertices(raw+count*56,lit,20,5,0,3)) { + free(raw); free(compressed); return mg_error("invalid BSP vertex payload"); + } + rasterizer_geometry_compress_vertices(_rasterizer_vertex_type_environment_uncompressed, + (long)count,compressed,(long)(count*32),raw,(long)(count*56)); + if (lit) rasterizer_geometry_compress_vertices(_rasterizer_vertex_type_environment_lightmap_uncompressed, + (long)lit,compressed+count*32,(long)(lit*8),raw+count*56,(long)(lit*20)); + free(raw); + /* CPU BSP queries expect the lightmap vertices immediately after the + * environment vertices, so both streams have one lifetime/allocation. */ + if (!mg_vertices(r,&m->vertices,_rasterizer_vertex_type_environment_compressed,count, + compressed,count*32,compressed,compressed_bytes,1)) return 0; + if (lit && !mg_vertices(r,&m->lightmap_vertices,_rasterizer_vertex_type_environment_lightmap_compressed, + lit,compressed+count*32,lit*8,NULL,0,1)) return 0; + m->compressed_vertex_data.size=(long)compressed_bytes; + m->compressed_vertex_data.address=compressed; + return 1; +} + +int mcc_geometry_bsp_load(struct mcc_runtime *r,uint32_t handle,uint32_t offset, + uint32_t size,void *address,void **result) { + struct mcc_cache_bsp const *b=NULL; + struct structure_bsp *structure; + struct structure_lightmap *lightmaps; + uint32_t i; + long l; + if (!r || !r->geometry || !result) return 0; + *result=NULL; + mcc_geometry_bsp_unload(r); + for (i=0;ireport.bsp_count;i++) { + struct mcc_cache_bsp const *candidate=&r->report.bsps[i]; + if (candidate->tag_handle==handle && candidate->file_offset==offset && candidate->size==size && + candidate->address==(uint32_t)(uintptr_t)address) { b=candidate; break; } + } + if (!b || !mcc_runtime_read(r,offset,size,address)) return mg_error("BSP request differs from validated reference"); + structure=mg_bsp_pointer(address,size,b->structure_address,sizeof(*structure)); + if (!structure || structure->lightmaps.count<0 || (uint32_t)structure->lightmaps.count>size/0x20) + return mg_error("invalid BSP lightmaps"); + lightmaps=mg_bsp_pointer(address,size,(uint32_t)(uintptr_t)structure->lightmaps.address, + (uint32_t)structure->lightmaps.count*0x20); + if (structure->lightmaps.count && !lightmaps) return mg_error("BSP lightmaps point outside BSP"); + for (l=0;llightmaps.count;l++) { + struct tag_block const *block=&lightmaps[l].materials; + struct structure_material *materials; + long m; + if (block->count<0 || (uint32_t)block->count>size/sizeof(*materials)) goto fail; + materials=mg_bsp_pointer(address,size,(uint32_t)(uintptr_t)block->address, + (uint32_t)block->count*sizeof(*materials)); + if (block->count && !materials) goto fail; + for (m=0;mcount;m++) if (!mg_bsp_material(r,b,&materials[m])) goto fail; + } + /* The MCC file stream descriptors are not Xbox vertex-buffer tables. */ + memset((unsigned char *)address+4,0,16); + *result=structure; + return 1; +fail: + mcc_geometry_bsp_unload(r); + return mg_error("BSP material conversion failed"); +} + +static void mg_release(struct mcc_geometry_buffer *b) { + if (b->is_index) rasterizer_triangle_buffer_delete((struct triangle_buffer *)b->descriptor); + else rasterizer_vertex_buffer_delete((struct vertex_buffer *)b->descriptor); + if (b->allocation) free(b->allocation); + free(b); +} + +void mcc_geometry_bsp_unload(struct mcc_runtime *r) { + struct mcc_geometry_state *state=r ? r->geometry : NULL; + struct mcc_geometry_buffer **link; + if (!state) return; + link=&state->buffers; + while (*link) { + struct mcc_geometry_buffer *b=*link; + if (b->is_bsp) { *link=b->next; mg_release(b); } + else link=&b->next; + } +} + +void mcc_geometry_dispose(struct mcc_runtime *r) { + struct mcc_geometry_state *state=r ? r->geometry : NULL; + if (!state) return; + while (state->buffers) { + struct mcc_geometry_buffer *b=state->buffers; + state->buffers=b->next; mg_release(b); + } + while (state->models) { + struct mcc_converted_model *m=state->models; + state->models=m->next; free(m); + } + free(state); r->geometry=NULL; +} + +void *mcc_geometry_buffer_data(struct mcc_runtime *r,void const *hardware, + int index_buffer,unsigned long *bytes) { + struct mcc_geometry_state *state=r ? r->geometry : NULL; + struct mcc_geometry_buffer *b; + if (bytes) *bytes=0; + if (!state || !hardware) return NULL; + for (b=state->buffers;b;b=b->next) { + void *current=b->is_index ? ((struct triangle_buffer *)b->descriptor)->hardware_format : + ((struct vertex_buffer *)b->descriptor)->hardware_format; + if (b->is_index==index_buffer && current==hardware) { + if (bytes) *bytes=b->bytes; + return b->payload; + } + } + return NULL; +} + +int mcc_geometry_contains(struct mcc_runtime *r,void const *address,unsigned long bytes) { + struct mcc_geometry_state *state=r ? r->geometry : NULL; + struct mcc_geometry_buffer *b; + uintptr_t wanted=(uintptr_t)address; + if (!state) return 0; + for (b=state->buffers;b;b=b->next) { + uintptr_t start=(uintptr_t)b->allocation; + if (b->allocation && wanted>=start && wanted-start<=b->allocation_bytes && + bytes<=b->allocation_bytes-(wanted-start)) return 1; + } + return 0; +} + +short mcc_geometry_part_palette(struct mcc_runtime *r,void const *vertex_buffer, + unsigned char const **nodes) { + struct mcc_geometry_state *state=r ? r->geometry : NULL; + struct mcc_geometry_buffer *b; + if (nodes) *nodes=NULL; + if (!state || !nodes) return 0; + for (b=state->buffers;b;b=b->next) if (!b->is_index && b->descriptor==vertex_buffer && b->palette_count) { + *nodes=b->palette; return b->palette_count; + } + return 0; +} diff --git a/port/linux/game/mcc_grenades.c b/port/linux/game/mcc_grenades.c new file mode 100644 index 0000000000..71e099f299 --- /dev/null +++ b/port/linux/game/mcc_grenades.c @@ -0,0 +1,320 @@ +/* MCC adds two grenade/ability slots without changing the native unit or + * save-game layout. Every extra count belongs to a full salted object ID. */ +#include "cseries.h" +#include "errors.h" +#include "mcc_cache.h" +#include "mcc_grenades.h" +#include "ai/actors.h" +#include "game/cheats.h" +#include "game/game.h" +#include "game/game_engine.h" +#include "game/game_globals.h" +#include "game/players.h" +#include "interface/hud_draw.h" +#include "interface/unit_hud_interface_definition.h" +#include "items/equipment.h" +#include "items/equipment_definitions.h" +#include "items/items.h" +#include "items/weapons.h" +#include "objects/objects.h" +#include "scenario/scenario.h" +#include "units/units.h" +#include "units/unit_definitions.h" +#include + +struct mcc_grenade_inventory {long handle;unsigned char counts[2];}; +static struct mcc_grenade_inventory mcc_grenade_inventories[MAXIMUM_OBJECTS_PER_MAP]; +static long mcc_grenade_flash_time[MAXIMUM_NUMBER_OF_LOCAL_PLAYERS]; + +boolean network_game_distributed_client(void); +void network_damage_note_grenade(long unit,short type); + +boolean mcc_grenades_active(void) {return mcc_cache_tags_loaded();} + +short mcc_grenades_type_count(void) +{ + struct game_globals *globals; + if (!mcc_grenades_active()) return 0; + globals=scenario_get_game_globals(); + return globals ? (short)PIN(globals->grenades.count,0,4) : 0; +} + +static struct game_globals_grenade *mcc_grenade_definition(short type) +{ + if (type<0 || type>=mcc_grenades_type_count()) return NULL; + return (struct game_globals_grenade *)scenario_get_game_globals()->grenades.address+type; +} + +static struct mcc_grenade_inventory *mcc_grenade_inventory(long unit,int create) +{ + unsigned index=(unsigned long)unit&0xFFFF; + struct mcc_grenade_inventory *inventory; + if (unit==NONE || index>=MAXIMUM_OBJECTS_PER_MAP || !unit_try_and_get(unit)) return NULL; + inventory=&mcc_grenade_inventories[index]; + if (inventory->handle!=unit) { + if (!create) return NULL; + inventory->handle=unit;inventory->counts[0]=inventory->counts[1]=0; + } + return inventory; +} + +void mcc_grenades_reset(void) +{ + unsigned i; + memset(mcc_grenade_inventories,0,sizeof(mcc_grenade_inventories)); + for (i=0;i=mcc_grenades_type_count() || !(object=unit_try_and_get(unit))) return 0; + if (type<2) return MAX(object->unit.grenade_counts[type],0); + inventory=mcc_grenade_inventory(unit,0); + return inventory ? inventory->counts[type-2] : 0; +} + +void mcc_grenades_set(long unit,short type,short count) +{ + struct unit_datum *object; + struct mcc_grenade_inventory *inventory; + if (!mcc_grenades_active() || type<0 || type>=mcc_grenades_type_count() || !(object=unit_try_and_get(unit))) return; + count=(short)PIN(count,0,127); + if (type<2) object->unit.grenade_counts[type]=(char)count; + else if ((inventory=mcc_grenade_inventory(unit,1))!=NULL) inventory->counts[type-2]=(unsigned char)count; +} + +short mcc_grenades_total(long unit) +{ + short n,total=0; + for (n=0;n=total) current=0; + for (n=0;n0) return index; + } + return NONE; +} + +short mcc_grenades_add(long unit,short type,short count) +{ + struct unit_datum *object=unit_try_and_get(unit); + if (!object || !mcc_grenade_definition(type) || count<0) return 0; + mcc_grenades_set(unit,type,(short)MIN(mcc_grenades_get(unit,type)+(long)count,127)); + object->unit.current_grenade_index=(char)type; + object->unit.desired_grenade_index=(char)type; + return mcc_grenades_get(unit,type); +} + +void mcc_grenades_initialize_unit(long unit) +{ + struct unit_datum *object=unit_try_and_get(unit); + struct unit_definition *definition; + mcc_grenades_clear(unit); + if (!object) return; + definition=unit_definition_get(object->definition_index); + if (definition->unit.grenade_type>=2) + mcc_grenades_set(unit,definition->unit.grenade_type,definition->unit.grenade_count); +} + +boolean mcc_grenades_pickup(long unit,long equipment) +{ + struct item_datum *item=equipment_get(equipment); + struct equipment_definition *definition=equipment_definition_get(item->definition_index); + short type=definition->equipment.grenade_type,count=mcc_grenades_get(unit,type); + struct game_globals_grenade *grenade=mcc_grenade_definition(type); + long player; + if (definition->equipment.powerup_type!=_equipment_powerup_grenade || !grenade || + count>=MIN(grenade->maximum_count,127) || !unit_try_and_get(unit)) return FALSE; + mcc_grenades_set(unit,type,count+1); + player=player_index_from_unit_index(unit); + if (player!=NONE && player_get(player)->local_player_index!=NONE) equipment_handle_pickup(equipment); + object_delete(equipment); + return TRUE; +} + +void mcc_grenades_move_to_hand(long unit) +{ + struct unit_datum *object=unit_get(unit); + short type=object->unit.current_grenade_index; + struct game_globals_grenade *grenade=mcc_grenade_definition(type); + struct object_marker hand; + struct object_placement_data placement; + long projectile; + boolean unlimited=(object->unit.player_index!=NONE && + (cheat.infinite_ammo || game_engine_infinite_grenades(object->unit.player_index))) || + (object->unit.actor_index!=NONE && actor_has_unlimited_grenades(object->unit.actor_index)); + if (!grenade || grenade->projectile.index==NONE || (!unlimited && !mcc_grenades_get(unit,type) && !network_game_distributed_client())) { + object->unit.grenade_throw_state=_unit_grenade_throw_ending;return; + } + if (!unlimited) mcc_grenades_set(unit,type,mcc_grenades_get(unit,type)-1); + /* The common marker query supplies node zero as its fallback, including + * for ability-bearing creatures without a named left-hand marker. */ + object_get_marker_by_name(unit,"left hand",&hand,1); + object_placement_data_new(&placement,grenade->projectile.index,unit); + placement.flags|=FLAG(_new_object_never_automatically_delete_bit); + placement.position=hand.matrix.position; + placement.forward=object->unit.aiming_vector; + normalize3d(perpendicular3d(&placement.forward,&placement.up)); + projectile=object_new(&placement); + object=unit_get(unit); + if (projectile==NONE) object->unit.grenade_throw_state=_unit_grenade_throw_ending; + else { + object_attach_to_node(unit,projectile,hand.node_index); + object->unit.grenade_object_index=projectile; + object->unit.grenade_throw_state=_unit_grenade_throw_in_hand; + network_damage_note_grenade(unit,type); + } +} + +void mcc_grenades_drop_extra(long unit,void (*drop_item)(long,long)) +{ + short type; + for (type=2;type0) { + struct object_placement_data placement; + long item=NONE; + if (grenade && grenade->item.index!=NONE) { + object_placement_data_new(&placement,grenade->item.index,unit); + item=object_new(&placement); + } + mcc_grenades_set(unit,type,mcc_grenades_get(unit,type)-1); + if (item!=NONE) {object_disconnect_from_map(item);drop_item(unit,item);} + } + } +} + +struct number_hud_element_definition { + struct hud_placement_definition placement;struct hud_color_definition colors; + char digits;unsigned char number_flags;char fractional_digits;unsigned char pad;long unused[3]; +}; +struct weapon_hud_overlay_definition {struct tag_reference bitmap;struct tag_block items;}; +struct mcc_grenade_hud { + struct hud_absolute_placement_definition absolute; + struct static_hud_element_definition background; + struct {struct static_hud_element_definition background;struct number_hud_element_definition numbers;short cutoff,pad;} counter; + struct weapon_hud_overlay_definition overlays; +}; + +void mcc_grenades_hud(short local_player,long unit) +{ + struct unit_datum *object=unit_try_and_get(unit),*parent; + struct game_globals_grenade *grenade; + struct mcc_grenade_hud const *hud; + short count,flags,overlays; + long weapon,*flash; + if (!object || local_player<0 || local_player>=NUMBEROF(mcc_grenade_flash_time)) return; + grenade=mcc_grenade_definition(object->unit.current_grenade_index); + if (!grenade || grenade->hud_interface.index==NONE) return; + weapon=unit_inventory_get_weapon(unit,object->unit.current_weapon_index); + if (weapon_prevents_grenade_throwing(weapon)) return; + parent=unit_try_and_get(object->object.parent_object_index); + if (parent && (parent->unit.driver_object_index==unit || parent->unit.gunner_object_index==unit)) return; + hud=tag_get('grhi',grenade->hud_interface.index); + count=mcc_grenades_get(unit,object->unit.current_grenade_index); + flags=(count<=hud->counter.cutoff ? FLAG(_hud_draw_flashing_bit) : 0) | + (!count ? FLAG(_hud_draw_disabled_bit) : 0) | + (local_player_count()>1 ? FLAG(_hud_draw_in_multiplayer_bit) : 0); + flash=&mcc_grenade_flash_time[local_player]; + if (!(flags&FLAG(_hud_draw_flashing_bit))) *flash=NONE; + else if (*flash==NONE) *flash=game_time_get(); + if (hud->background.interface_bitmap.index!=NONE) + hud_draw_static_element(local_player,&hud->absolute,&hud->background,flags,*flash); + if (hud->counter.background.interface_bitmap.index!=NONE) + hud_draw_static_element(local_player,&hud->absolute,&hud->counter.background,flags,*flash); + if (hud->counter.numbers.digits) + hud_draw_numbers(local_player,&hud->absolute,&hud->counter.numbers,count,NONE,flags,*flash,0.0f); + overlays=(count<=hud->counter.cutoff ? 1 : 0)|(!count ? 2 : 0); + if (!overlays) overlays|=4; + overlays|=8; + if (hud->overlays.bitmap.index!=NONE) + hud_draw_weapon_overlays(local_player,&hud->absolute,&hud->overlays,overlays,*flash,flags,local_player_count()>1); +} + +static uint32_t mcc_grenade_u32(unsigned char const *p) +{ + return p[0]|((uint32_t)p[1]<<8)|((uint32_t)p[2]<<16)|((uint32_t)p[3]<<24); +} + +uint32_t mcc_grenades_snapshot(void *out,uint32_t capacity) +{ + unsigned i,count=0; + unsigned char *bytes=out; + uint32_t size; + for (i=0;icounts[0] || inventory->counts[1]) && unit_try_and_get(inventory->handle)) count++; + } + size=8+count*8; + if (!out) return size; + if (capacity>8); + bytes+=8; + for (i=0;ihandle; + if (!(inventory->counts[0] || inventory->counts[1]) || !unit_try_and_get(inventory->handle)) continue; + bytes[0]=(unsigned char)handle;bytes[1]=(unsigned char)(handle>>8);bytes[2]=(unsigned char)(handle>>16);bytes[3]=(unsigned char)(handle>>24); + bytes[4]=inventory->counts[0];bytes[5]=inventory->counts[1];bytes[6]=bytes[7]=0;bytes+=8; + } + return size; +} + +int mcc_grenades_snapshot_validate(void const *in,uint32_t bytes) +{ + unsigned char const *data=in; + unsigned char seen[MAXIMUM_OBJECTS_PER_MAP]; + unsigned i,count; + if (!in || bytes<8 || memcmp(data,"GMC4",4) || data[4]!=1 || data[5]) return 0; + count=data[6]|((unsigned)data[7]<<8); + if (count>MAXIMUM_OBJECTS_PER_MAP || bytes!=8+count*8) return 0; + memset(seen,0,sizeof(seen)); + for (i=0;i=MAXIMUM_OBJECTS_PER_MAP || seen[index] || + record[4]>127 || record[5]>127 || record[6] || record[7]) return 0; + seen[index]=1; + } + return 1; +} + +int mcc_grenades_restore(void const *in,uint32_t bytes) +{ + unsigned char const *data=in; + unsigned i,count; + if (!mcc_grenades_snapshot_validate(in,bytes)) return 0; + count=data[6]|((unsigned)data[7]<<8); + for (i=0;ihandle=(long)handle;inventory->counts[0]=record[4];inventory->counts[1]=record[5]; + } + return 1; +} diff --git a/port/linux/game/mcc_grenades.h b/port/linux/game/mcc_grenades.h new file mode 100644 index 0000000000..5e3103e4a6 --- /dev/null +++ b/port/linux/game/mcc_grenades.h @@ -0,0 +1,27 @@ +#ifndef MCC_GRENADES_H +#define MCC_GRENADES_H +#include + +boolean mcc_grenades_active(void); +short mcc_grenades_type_count(void); +short mcc_grenades_get(long unit,short type); +void mcc_grenades_set(long unit,short type,short count); +short mcc_grenades_total(long unit); +short mcc_grenades_next(long unit,short current,short delta); +short mcc_grenades_add(long unit,short type,short count); +/* Clear/reset affect only the independently owned extra-slot state. */ +void mcc_grenades_clear(long unit); +void mcc_grenades_reset(void); +void mcc_grenades_initialize_unit(long unit); +boolean mcc_grenades_pickup(long unit,long equipment); +void mcc_grenades_move_to_hand(long unit); +void mcc_grenades_drop_extra(long unit,void (*drop_item)(long,long)); +void mcc_grenades_hud(short local_player,long unit); + +/* Stable little-endian snapshot: GMC4, version u16=1, record count u16; + * records are full salted handle u32, slot2 u8, slot3 u8, zero u16. + * Selection already belongs to the native unit game-state image. */ +uint32_t mcc_grenades_snapshot(void *out,uint32_t capacity); +int mcc_grenades_snapshot_validate(void const *in,uint32_t bytes); +int mcc_grenades_restore(void const *in,uint32_t bytes); +#endif diff --git a/port/linux/game/mcc_hud.c b/port/linux/game/mcc_hud.c new file mode 100644 index 0000000000..ef905d9dd7 --- /dev/null +++ b/port/linux/game/mcc_hud.c @@ -0,0 +1,211 @@ +/* MCC HUD normalization. MCC placements use a 960p canvas; the native + * viewport uses 480p. Bitmap half-HUD flags apply in addition to that canvas + * conversion. Keep the original bitmap pixels and sprite texture coordinates. */ +#include "cseries.h" +#include "bitmaps/bitmap_group.h" +#include "interface/unit_hud_interface_definition.h" +#include "mcc_runtime.h" +#include + +char const *config_string(char const *name); + +/* MCC's PC score hint contains one formatting argument. The native HUD + * copies this string verbatim, so resolve that argument in owned UTF-16 + * storage. Keep the configured keyboard binding and the controller label. */ +static int mcc_hud_score_text(uint16_t const *source,uint32_t characters, + char const *binding,uint16_t *out,uint32_t capacity) +{ + uint32_t length=0,placeholder=UINT32_MAX,i,used=0,key_length=0,needed; + char key[49]; + static char const controller[]="BACK"; + if (!source || !characters) return -1; + while (length=32 && c<127 ? c : '?'); + } + while (key_length && key[key_length-1]==' ') key_length--; + } + /* The additional text, excluding the two source characters and NUL. */ + needed=length-2+4+(key_length ? key_length+3 : 0)+1; + if (!out) return (int)needed; + if (capacityr->used/stride) return NULL; + return *count ? mcc_runtime_pointer(r,mcc_hud_word(p+4),*count*stride) : r->tags; +} + +static int mcc_hud_multiplayer_text(struct mcc_runtime *r,uint32_t const *entry) +{ + static char const name[]="ui\\multiplayer_game_text"; + char const *actual=mcc_runtime_pointer(r,entry[4],sizeof(name)); + unsigned char *root,*strings,*hint; + uint16_t const *source; + uint16_t *replacement; + uint32_t count,bytes,address; + int characters; + if (!actual || memcmp(actual,name,sizeof(name))) return 1; + root=mcc_runtime_pointer(r,entry[5],12); + if (!root) return 0; + strings=mcc_hud_block(r,root,20,&count); + if (!strings) return 0; + if (count<=100) return 1; + hint=strings+100*20;bytes=mcc_hud_word(hint); + if (!bytes) return 1; /* Maps may deliberately omit the score hint. */ + if (bytes<2 || (bytes&1)) return 0; + source=mcc_runtime_pointer(r,mcc_hud_word(hint+12),bytes); + if (!source || ((uintptr_t)source&1)) return 0; + characters=mcc_hud_score_text(source,bytes/2,config_string("controls.scoreboard"),NULL,0); + if (characters<0) return 0; + if (!characters) return 1; + replacement=mcc_runtime_allocate(r,(uint32_t)characters*2); + if (!replacement || mcc_hud_score_text(source,bytes/2,config_string("controls.scoreboard"), + replacement,(uint32_t)characters)!=characters) return 0; + bytes=(uint32_t)characters*2;address=(uint32_t)(uintptr_t)replacement; + memcpy(hint,&bytes,4);memcpy(hint+12,&address,4); + return 1; +} + +static int mcc_hud_half_bitmap(struct mcc_runtime *r,uint32_t handle,int *half) +{ + uint32_t *entry; + struct bitmap_group *bitmap; + *half=0; + if (handle==0xFFFFFFFFu) return 1; + if ((handle&0xFFFF)>=r->report.tag_count) return 0; + entry=(uint32_t *)(r->tag_index+(handle&0xFFFF)*32); + if (entry[0]!='bitm' || entry[3]!=handle) return 0; + bitmap=mcc_runtime_pointer(r,entry[5],sizeof(*bitmap)); + if (!bitmap) return 0; + *half=(bitmap->flags&(0x10|0x80))!=0; + return 1; +} + +static void mcc_hud_offset(struct hud_placement_definition *placement) +{ + placement->offset.x/=2; + placement->offset.y/=2; + /* MCC does not apply the old PC placement high-resolution flag. */ + placement->multiplayer_scaling_flags&=~4; +} + +static void mcc_hud_scale(struct hud_placement_definition *placement,int half) +{ + float factor=half ? 0.25f : 0.5f; + mcc_hud_offset(placement); + placement->scale.i*=factor; + placement->scale.j*=factor; +} + +static int mcc_hud_bitmap_element(struct mcc_runtime *r,unsigned char *element) +{ + int half; + if (!mcc_hud_half_bitmap(r,mcc_hud_word(element+0x30),&half)) return 0; + mcc_hud_scale((void *)element,half); + return 1; +} + +static int mcc_hud_items(struct mcc_runtime *r,unsigned char *definition,uint32_t stride) +{ + uint32_t i,count; + unsigned char *items=mcc_hud_block(r,definition+16,stride,&count); + int half; + if (!items || !mcc_hud_half_bitmap(r,mcc_hud_word(definition+12),&half)) return 0; + for (i=0;ireport.tag_count;i++) { + uint32_t *entry=(uint32_t *)(r->tag_index+i*32),n,count; + unsigned char *p,*block; + if (entry[0]=='unhi') { + static unsigned const elements[]={0x24,0x8C,0xF4,0x17C,0x1E4,0x26C,0x2D4}; + p=mcc_runtime_pointer(r,entry[5],0x56C); + if (!p) return 0; + for (n=0;nmessaging.placement); + globals->waypoint.top_offset*=0.5f; + globals->waypoint.bottom_offset*=0.5f; + globals->waypoint.left_offset*=0.5f; + globals->waypoint.right_offset*=0.5f; + globals->damage_indicators.top_offset/=2; + globals->damage_indicators.bottom_offset/=2; + globals->damage_indicators.left_offset/=2; + globals->damage_indicators.right_offset/=2; + } else if (entry[0]=='hud#') { + /* The number renderer owns bitmap scaling; these are screen + * advances, not sprite texture coordinates. */ + p=mcc_runtime_pointer(r,entry[5],0x64); + if (!p) return 0; + ((signed char *)p)[0x11]/=2; /* screen_width */ + ((signed char *)p)[0x14]/=2; /* decimal_point_width */ + } else if (entry[0]=='bitm') { + struct bitmap_group *bitmap=mcc_runtime_pointer(r,entry[5],sizeof(*bitmap)); + if (!bitmap) return 0; + /* Native number drawing already honors half-HUD-scale; MCC's + * force-highres spelling has the same effect for these digits. */ + if (bitmap->flags&0x80) bitmap->flags|=0x10; + } else if (entry[0]=='ustr') { + if (!mcc_hud_multiplayer_text(r,entry)) return 0; + } + } + return 1; +} diff --git a/port/linux/game/mcc_hud_draw.c b/port/linux/game/mcc_hud_draw.c new file mode 100644 index 0000000000..58fb31ac68 --- /dev/null +++ b/port/linux/game/mcc_hud_draw.c @@ -0,0 +1,53 @@ +/* The MCC canvas is 960p, while native UI/HUD coordinates are 480p. + * Keep full-resolution texture samples and convert only screen geometry. */ +#include "cseries.h" +#include "bitmaps/bitmap_group.h" +#include "game/players.h" +#include "interface/hud_definitions.h" +#include "interface/hud_draw.h" +#include "text/draw_string.h" +#include "mcc_cache.h" +#include "mcc_hud_draw.h" + +real mcc_hud_canvas_scale(void) +{ + return mcc_cache_tags_loaded() ? 0.5f : 1.0f; +} + +real mcc_hud_bitmap_scale(long bitmap_group_index) +{ + struct bitmap_group const *group; + if (!mcc_cache_tags_loaded() || bitmap_group_index==NONE) return 1.0f; + group=bitmap_group_get(bitmap_group_index); + return group->flags&(0x10|0x80) ? 0.25f : 0.5f; +} + +boolean mcc_hud_icon_draw(long bitmap_group_index, struct bitmap_data const *bitmap, + real_rectangle2d const *clip, rectangle2d *cursor, pixel32 color, + struct icon_hud_element_definition const *icon, boolean menu) +{ + real viewport_scale, pixel_scale, offset_scale, advance; + point2d point; + if (!mcc_cache_tags_loaded()) return FALSE; + viewport_scale=!menu && local_player_count()>1 ? 0.75f : 1.0f; + pixel_scale=viewport_scale*mcc_hud_bitmap_scale(bitmap_group_index); + /* Icon offsets and extra text advances are font-relative, unlike the + * HUD placement anchors. MCC tags retain the native values here. */ + offset_scale=viewport_scale; + point.x=(short)(cursor->x0+icon->offset.x*offset_scale+(menu ? 1.0f : 0.0f)); + point.y=(short)(cursor->y1-icon->offset.y*offset_scale-(menu ? 2.0f : 0.0f)); + if (menu) { + /* A map's HUD icon baseline may be custom-authored for its messages. + * Native pause labels use the selected UI font instead. Align their + * glyphs to its capitals, without resizing or changing the font. The + * empty string measures this same line without introducing wrapping. */ + real height=(clip ? clip->y1-clip->y0 : 1.0f)*bitmap->height*pixel_scale; + point.y=(short)(draw_unicode_string_capital_middle(cursor,L"")+height*0.5f); + } + hud_draw_bitmap_direct(bitmap,_hud_anchor_bottom_left,&point,clip,pixel_scale,0.0f, + icon->flags&2 ? icon->color : color,FALSE); + advance=icon->width_offset*offset_scale; + if (!(icon->flags&4)) advance+=(clip ? clip->x1-clip->x0 : 1.0f)*bitmap->width*pixel_scale; + cursor->x0=(short)(point.x+advance); + return TRUE; +} diff --git a/port/linux/game/mcc_hud_draw.h b/port/linux/game/mcc_hud_draw.h new file mode 100644 index 0000000000..bbedce3556 --- /dev/null +++ b/port/linux/game/mcc_hud_draw.h @@ -0,0 +1,17 @@ +#ifndef MCC_HUD_DRAW_H +#define MCC_HUD_DRAW_H + +#include "cseries.h" +#include "math/real_math.h" + +struct bitmap_data; +struct icon_hud_element_definition; + +/* Neutral for every non-MCC cache. No native or CE tag is rewritten. */ +real mcc_hud_canvas_scale(void); +real mcc_hud_bitmap_scale(long bitmap_group_index); +boolean mcc_hud_icon_draw(long bitmap_group_index, struct bitmap_data const *bitmap, + real_rectangle2d const *clip, rectangle2d *cursor, pixel32 color, + struct icon_hud_element_definition const *icon, boolean menu); + +#endif diff --git a/port/linux/game/mcc_main.c b/port/linux/game/mcc_main.c new file mode 100644 index 0000000000..4b9a4c4d3b --- /dev/null +++ b/port/linux/game/mcc_main.c @@ -0,0 +1,20 @@ +/* Local MCC load failures recover through the native UI lifecycle. The + * failed map has already released its tags; no game is initialized for it. */ +#include "cseries.h" +#include "errors.h" +#include "main/main.h" +#include "interface/ui_widget.h" +#include "mcc_cache.h" +#include "mcc_main.h" + +boolean mcc_main_load_failed(char const *name) +{ + if (!mcc_level_name(name)) return FALSE; + error(_error_silent, "mcc: returning to the menu after failed load of %s", name); + display_error_text_when_main_menu_loaded( + L"The MCC map could not be loaded. See debug.txt for the failing stage."); + errors_clear(); + main_goto_main_menu(); + main_menu_load(); + return TRUE; +} diff --git a/port/linux/game/mcc_main.h b/port/linux/game/mcc_main.h new file mode 100644 index 0000000000..491c0d7d57 --- /dev/null +++ b/port/linux/game/mcc_main.h @@ -0,0 +1,4 @@ +#ifndef MCC_MAIN_H +#define MCC_MAIN_H +boolean mcc_main_load_failed(char const *name); +#endif diff --git a/port/linux/game/mcc_maps.c b/port/linux/game/mcc_maps.c new file mode 100644 index 0000000000..2a116c3082 --- /dev/null +++ b/port/linux/game/mcc_maps.c @@ -0,0 +1,282 @@ +/* +MCC_MAPS.C + +The independent MCC menu catalog. Only version 13 Halo 1 MCC caches from +d:\mcc_maps are admitted. The on-disk filename is the level identity; the +cache's internal scenario name may differ. No Xbox or Custom Edition map +list, loader, resource directory or display index is used here. +*/ + +#include "cseries.h" +#include "errors.h" +#include "tag_files/files.h" +#include "tag_files/tag_files.h" +#include "mcc_cache_format.h" +#include "mcc_maps.h" + +#include +#include +#include + +/* main_set_map_name retains 63 characters including the namespace. */ +#define MCC_MAP_FILENAME_MAXIMUM (63 - (sizeof(MCC_MAPS_LEVEL_PREFIX) - 1)) +#define MCC_MAP_DESCRIPTION_MAXIMUM 127 + +struct mcc_menu_map +{ + char file_name[MCC_MAP_FILENAME_MAXIMUM + 1]; + char level_name[64]; + wchar_t name[MCC_MAP_FILENAME_MAXIMUM + 1]; + wchar_t description[MCC_MAP_DESCRIPTION_MAXIMUM + 1]; + boolean campaign; +}; + +static struct +{ + boolean scanned; + short count; + struct mcc_menu_map maps[MCC_MAPS_MAXIMUM]; +} mcc_menu_catalog; + +typedef char mcc_display_indices_are_separate[ + MCC_MAPS_FIRST_DISPLAY_INDEX + MCC_MAPS_MAXIMUM <= 0x3000 ? 1 : -1]; + +static void mcc_map_description_read(struct mcc_menu_map *map) +{ + char path[128]; + unsigned char bytes[1024]; + FILE *file; + size_t length = 0, position; + short used = 0; + + snprintf(path, sizeof(path), "%s%s.txt", MCC_MAPS_DIRECTORY, map->file_name); + file = fopen(path, "rb"); + if (file) + { + length = fread(bytes, 1, sizeof(bytes), file); + fclose(file); + } + position = length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF ? 3 : 0; + for (; position < length && used < MCC_MAP_DESCRIPTION_MAXIMUM; position++) + { + unsigned char value = bytes[position]; + + if (value == '\n') + { + if (used + 2 > MCC_MAP_DESCRIPTION_MAXIMUM) + break; + map->description[used++] = '\r'; + map->description[used++] = '\n'; + } + else if (value == '\t') + map->description[used++] = ' '; + else if (value >= 32 && value < 127) + map->description[used++] = value; + else if (value >= 0xC0) + map->description[used++] = '?'; + } + while (used && (map->description[used - 1] == ' ' || map->description[used - 1] == '\r' || + map->description[used - 1] == '\n')) + used--; + map->description[used] = 0; + if (!used) + { + wchar_t const *fallback = map->campaign ? L"Halo 1 MCC\r\nCampaign map" : L"Halo 1 MCC\r\nMultiplayer map"; + + for (used = 0; fallback[used]; used++) + map->description[used] = fallback[used]; + map->description[used] = 0; + } +} + +static void mcc_map_register(char const *name) +{ + char path[128]; + unsigned char header[MCC_CACHE_HEADER_BYTES]; + struct mcc_cache_identity identity; + struct mcc_menu_map *map; + FILE *file; + long file_size; + size_t name_length = strlen(name), index; + boolean start_word = TRUE; + + if (!name_length || name_length > MCC_MAP_FILENAME_MAXIMUM || mcc_menu_catalog.count == MCC_MAPS_MAXIMUM || + name[name_length - 1] == '.' || name[name_length - 1] == ' ') + return; + for (index = 0; index < name_length; index++) + { + unsigned char character = (unsigned char)name[index]; + + if (character < 32 || strchr("/\\:*?\"<>|", character)) + return; + } + for (index = 0; index < (size_t)mcc_menu_catalog.count; index++) + { + if (!csstrcasecmp(mcc_menu_catalog.maps[index].file_name, name)) + return; + } + snprintf(path, sizeof(path), "%s%s.map", MCC_MAPS_DIRECTORY, name); + file = fopen(path, "rb"); + if (!file) + return; + if (fseek(file, 0, SEEK_END) || (file_size = ftell(file)) < 0 || fseek(file, 0, SEEK_SET) || + fread(header, 1, sizeof(header), file) != sizeof(header)) + { + fclose(file); + return; + } + fclose(file); + if (mcc_cache_identify(header, sizeof(header), (uint64_t)file_size, &identity) != MCC_CACHE_OK) + return; + map = &mcc_menu_catalog.maps[mcc_menu_catalog.count++]; + memset(map, 0, sizeof(*map)); + memcpy(map->file_name, name, name_length + 1); + snprintf(map->level_name, sizeof(map->level_name), "%s%s", MCC_MAPS_LEVEL_PREFIX, name); + map->campaign = identity.scenario_type == 0; + for (index = 0; index < name_length; index++) + { + unsigned char character = (unsigned char)name[index]; + + if (character == '_') + character = ' '; + else if (start_word && character >= 'a' && character <= 'z') + character -= 'a' - 'A'; + map->name[index] = character >= 32 && character < 127 ? character : '?'; + start_word = character == ' '; + } + mcc_map_description_read(map); +} + +static int mcc_map_order(void const *left, void const *right) +{ + return csstrcasecmp(((struct mcc_menu_map const *)left)->file_name, + ((struct mcc_menu_map const *)right)->file_name); +} + +static void mcc_maps_scan(void) +{ + struct file_reference directory, file; + char name[MAXIMUM_FILENAME_LENGTH + 1], extension[MAXIMUM_FILENAME_LENGTH + 1]; + + if (mcc_menu_catalog.scanned) + return; + mcc_menu_catalog.scanned = TRUE; + mcc_menu_catalog.count = 0; + file_reference_create_from_path(&directory, MCC_MAPS_DIRECTORY, TRUE); + find_files_start(0, &directory); + while (find_files_next(&file, NULL)) + { + file_reference_get_name(&file, FLAG(_name_extension_bit), extension); + if (csstrcasecmp(extension, "map")) + continue; + file_reference_get_name(&file, FLAG(_name_filename_bit), name); + mcc_map_register(name); + } + qsort(mcc_menu_catalog.maps, (size_t)mcc_menu_catalog.count, sizeof(mcc_menu_catalog.maps[0]), mcc_map_order); + error(_error_silent, "mcc maps: %d maps found in mcc_maps", mcc_menu_catalog.count); +} + +void mcc_maps_rescan(void) +{ + mcc_menu_catalog.scanned = FALSE; +} + +short mcc_maps_count(boolean multiplayer_only) +{ + short index, count = 0; + + mcc_maps_scan(); + for (index = 0; index < mcc_menu_catalog.count; index++) + { + if (!multiplayer_only || !mcc_menu_catalog.maps[index].campaign) + count++; + } + return count; +} + +short mcc_maps_index(short row, boolean multiplayer_only) +{ + short index; + + mcc_maps_scan(); + if (row < 0) + return NONE; + for (index = 0; index < mcc_menu_catalog.count; index++) + { + if ((!multiplayer_only || !mcc_menu_catalog.maps[index].campaign) && row-- == 0) + return index; + } + return NONE; +} + +short mcc_maps_type_count(boolean campaign) +{ + short index, count = 0; + + mcc_maps_scan(); + for (index = 0; index < mcc_menu_catalog.count; index++) + { + if (mcc_menu_catalog.maps[index].campaign == campaign) + count++; + } + return count; +} + +short mcc_maps_type_index(short row, boolean campaign) +{ + short index; + + mcc_maps_scan(); + if (row < 0) + return NONE; + for (index = 0; index < mcc_menu_catalog.count; index++) + { + if (mcc_menu_catalog.maps[index].campaign == campaign && row-- == 0) + return index; + } + return NONE; +} + +short mcc_maps_find(char const *level_name) +{ + short index; + + if (!level_name || _strnicmp(level_name, MCC_MAPS_LEVEL_PREFIX, sizeof(MCC_MAPS_LEVEL_PREFIX) - 1)) + return NONE; + mcc_maps_scan(); + for (index = 0; index < mcc_menu_catalog.count; index++) + { + if (!csstrcasecmp(level_name, mcc_menu_catalog.maps[index].level_name)) + return index; + } + return NONE; +} + +char const *mcc_maps_level_name(short index) +{ + mcc_maps_scan(); + return index >= 0 && index < mcc_menu_catalog.count ? mcc_menu_catalog.maps[index].level_name : NULL; +} + +wchar_t const *mcc_maps_name(short index) +{ + mcc_maps_scan(); + return index >= 0 && index < mcc_menu_catalog.count ? mcc_menu_catalog.maps[index].name : L""; +} + +wchar_t const *mcc_maps_description(short index) +{ + mcc_maps_scan(); + return index >= 0 && index < mcc_menu_catalog.count ? mcc_menu_catalog.maps[index].description : L""; +} + +boolean mcc_maps_campaign(short index) +{ + mcc_maps_scan(); + return index >= 0 && index < mcc_menu_catalog.count && mcc_menu_catalog.maps[index].campaign; +} + +boolean mcc_maps_level_campaign(char const *level_name) +{ + return mcc_maps_campaign(mcc_maps_find(level_name)); +} diff --git a/port/linux/game/mcc_maps.h b/port/linux/game/mcc_maps.h new file mode 100644 index 0000000000..490ea54bc3 --- /dev/null +++ b/port/linux/game/mcc_maps.h @@ -0,0 +1,23 @@ +/* Halo 1 MCC map discovery. This list never includes the Xbox or CE folders. */ +#ifndef __MCC_MAPS_H +#define __MCC_MAPS_H + +#define MCC_MAPS_DIRECTORY "d:\\mcc_maps\\" +#define MCC_MAPS_LEVEL_PREFIX "mcc_maps\\" +#define MCC_MAPS_FIRST_DISPLAY_INDEX 0x1000 +#define MCC_MAPS_MAXIMUM 1024 + +void mcc_maps_rescan(void); +short mcc_maps_count(boolean multiplayer_only); +short mcc_maps_index(short row, boolean multiplayer_only); +/* Exact scenario-type lists; display indices still use the full catalog. */ +short mcc_maps_type_count(boolean campaign); +short mcc_maps_type_index(short row, boolean campaign); +short mcc_maps_find(char const *level_name); +char const *mcc_maps_level_name(short index); +wchar_t const *mcc_maps_name(short index); +wchar_t const *mcc_maps_description(short index); +boolean mcc_maps_campaign(short index); +boolean mcc_maps_level_campaign(char const *level_name); + +#endif diff --git a/port/linux/game/mcc_network.c b/port/linux/game/mcc_network.c new file mode 100644 index 0000000000..bdb8267134 --- /dev/null +++ b/port/linux/game/mcc_network.c @@ -0,0 +1,149 @@ +#include "cseries.h" +#include "mcc_network.h" +#include "mcc_grenades.h" +#include "game/game.h" +#include "game/players.h" +#include "objects/objects.h" +#include "units/units.h" +#include "network_distributed.h" +#include + +struct mcc_network_inventory { + long unit; + byte counts[2]; + byte reserved[2]; +}; +typedef char mcc_network_inventory_size[sizeof(struct mcc_network_inventory) == 8 ? 1 : -1]; +struct mcc_network_history { + long unit; + byte counts[2]; + long changed[2]; + long sent; +}; +static struct mcc_network_history mcc_network_sent[MAXIMUM_TRACKED_OBJECTS]; +static struct mcc_network_history mcc_network_own[MAXIMUM_TRACKED_OBJECTS]; +static long mcc_network_last_tick = NONE; + +void mcc_network_new_game(void) +{ + long slot; + csmemset(mcc_network_sent, 0, sizeof(mcc_network_sent)); + csmemset(mcc_network_own, 0, sizeof(mcc_network_own)); + mcc_network_last_tick = NONE; + for (slot = 0; slot < MAXIMUM_TRACKED_OBJECTS; ++slot) { + mcc_network_sent[slot].unit = mcc_network_own[slot].unit = NONE; + mcc_network_sent[slot].sent = NONE; + mcc_network_own[slot].changed[0] = mcc_network_own[slot].changed[1] = NONE; + } +} + +static void mcc_network_observe_clock(long now) +{ + /* Checkpoint restores rewind simulation time but not these private caches. */ + if (mcc_network_last_tick != NONE && now < mcc_network_last_tick) + mcc_network_new_game(); + mcc_network_last_tick = now; +} + +word mcc_network_inventory_entry_size(void) { return sizeof(struct mcc_network_inventory); } + +void mcc_network_host_tick(void) +{ + struct { + struct distributed_message_header header; + struct mcc_network_inventory entries[64]; + } message; + struct object_iterator iterator; + short count = 0; + long now = game_time_get(); + short limit = MIN(64, DATAGRAM_ENTRIES(struct mcc_network_inventory)); + if (!mcc_grenades_active() || mcc_grenades_type_count() <= 2 || + game_connection() != _game_connection_network_server || now % 3) return; + mcc_network_observe_clock(now); + object_iterator_new(&iterator, _object_mask_unit, 0); + while (object_iterator_next(&iterator)) { + long slot = DATUM_INDEX_TO_ABSOLUTE_INDEX(iterator.index); + struct mcc_network_history *previous; + byte counts[2]; + if (slot < 0 || slot >= MAXIMUM_TRACKED_OBJECTS || !distributed_object_index_valid(iterator.index)) continue; + counts[0] = (byte)mcc_grenades_get(iterator.index, 2); + counts[1] = (byte)mcc_grenades_get(iterator.index, 3); + previous = &mcc_network_sent[slot]; + if (previous->unit == iterator.index && previous->counts[0] == counts[0] && previous->counts[1] == counts[1] && + previous->sent != NONE && now - previous->sent < TICKS_PER_SECOND) continue; + previous->unit = iterator.index; previous->counts[0] = counts[0]; previous->counts[1] = counts[1]; previous->sent = now; + message.entries[count].unit = iterator.index; + message.entries[count].counts[0] = counts[0]; message.entries[count].counts[1] = counts[1]; + message.entries[count].reserved[0] = message.entries[count].reserved[1] = 0; + if (++count == limit) { + distributed_send(&message, _distributed_message_mcc_grenades, count, + (word)(sizeof(message.header) + count * sizeof(message.entries[0])), _distributed_to_clients); + count = 0; + } + } + if (count) distributed_send(&message, _distributed_message_mcc_grenades, count, + (word)(sizeof(message.header) + count * sizeof(message.entries[0])), _distributed_to_clients); +} + +static struct mcc_network_history *mcc_network_note_own(long unit_index, boolean compare) +{ + long slot = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit_index); + struct mcc_network_history *own; + short type; + if (slot < 0 || slot >= MAXIMUM_TRACKED_OBJECTS) return NULL; + own = &mcc_network_own[slot]; + if (own->unit != unit_index) { + own->unit = unit_index; compare = FALSE; + own->changed[0] = own->changed[1] = NONE; + } + for (type = 0; type < 2; ++type) { + byte count = (byte)mcc_grenades_get(unit_index, (short)(type + 2)); + if (compare && count < own->counts[type]) own->changed[type] = game_time_get(); + own->counts[type] = count; + } + return own; +} + +void mcc_network_client_tick(void) +{ + short local; + if (!mcc_grenades_active() || game_connection() != _game_connection_network_client) return; + mcc_network_observe_clock(game_time_get()); + for (local = 0; local < MAXIMUM_LOCAL_PLAYERS; ++local) { + long player_index = local_player_get_player_index(local); + struct player_datum *player = player_index != NONE ? player_try_and_get(player_index) : NULL; + long unit = distributed_living_unit(player); + if (unit != NONE) mcc_network_note_own(unit, TRUE); + } +} + +void mcc_network_handle_inventories(void const *entries, short count) +{ + short entry; + if (!mcc_grenades_active() || game_connection() != _game_connection_network_client || count < 0 || count > 255) return; + mcc_network_observe_clock(game_time_get()); + for (entry = 0; entry < count; ++entry) { + struct mcc_network_inventory inventory; + struct unit_datum *unit; + struct mcc_network_history *own = NULL; + short type; + real round_trip; + csmemcpy(&inventory, (byte const *)entries + entry * sizeof(inventory), sizeof(inventory)); + if (inventory.reserved[0] || inventory.reserved[1] || inventory.counts[0] > 127 || inventory.counts[1] > 127 || + !distributed_object_index_valid(inventory.unit) || !network_objects_client_has(inventory.unit)) continue; + unit = object_try_and_get_and_verify_type(inventory.unit, _object_mask_unit); + if (!unit) continue; + if (unit->unit.player_index != NONE && distributed_player_is_local(unit->unit.player_index)) + own = mcc_network_note_own(inventory.unit, TRUE); + round_trip = distributed_own_round_trip_ticks(); + if (!(round_trip > 0.0f)) round_trip = 6.0f; + if (round_trip > 60.0f) round_trip = 60.0f; + for (type = 0; type < 2; ++type) { + short current = mcc_grenades_get(inventory.unit, (short)(type + 2)); + if (!own || inventory.counts[type] < current || own->changed[type] == NONE || + game_time_get() - own->changed[type] > (long)ceil(round_trip) + 5) + mcc_grenades_set(inventory.unit, (short)(type + 2), inventory.counts[type]); + } + if (own) mcc_network_note_own(inventory.unit, FALSE); + } +} diff --git a/port/linux/game/mcc_network.h b/port/linux/game/mcc_network.h new file mode 100644 index 0000000000..1380012140 --- /dev/null +++ b/port/linux/game/mcc_network.h @@ -0,0 +1,11 @@ +/* MCC-only extra grenade inventories. The ordinary inventory wire layout + * and the two native grenade fields are unchanged. */ +#ifndef MCC_NETWORK_H +#define MCC_NETWORK_H +#include "cseries.h" +void mcc_network_new_game(void); +void mcc_network_host_tick(void); +void mcc_network_client_tick(void); +word mcc_network_inventory_entry_size(void); +void mcc_network_handle_inventories(void const *entries, short count); +#endif diff --git a/port/linux/game/mcc_network_damage.inc b/port/linux/game/mcc_network_damage.inc new file mode 100644 index 0000000000..ec51317b61 --- /dev/null +++ b/port/linux/game/mcc_network_damage.inc @@ -0,0 +1,45 @@ +/* MCC additions to the generic damage ledger. Included after the common + * projectile/reach/throw validators, so their established policy stays the + * same for all four MCC grenade kinds. Native two-kind paths are untouched. */ +static void mcc_network_note_extra_grenade(long unit_index, short type) +{ + struct unit_datum *unit; + long player; + short slot = 0, index; + if (!mcc_grenades_active() || type < 2 || type >= mcc_grenades_type_count() || + game_connection() != _game_connection_network_server) return; + unit = object_try_and_get_and_verify_type(unit_index, _object_mask_unit); + if (!unit || unit->unit.player_index == NONE) return; + player = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit->unit.player_index); + if (player < 0 || player >= MAXIMUM_TRACKED_PLAYERS) return; + for (index = 0; index < MAXIMUM_GRENADE_THROWS; ++index) { + if (damage_players[player].throw_times[index] == NONE) { slot = index; break; } + if (damage_players[player].throw_times[index] < damage_players[player].throw_times[slot]) slot = index; + } + damage_players[player].throw_times[slot] = game_time_get(); + damage_players[player].throw_types[slot] = type; +} + +static void mcc_network_extra_grenade_damage(short player, long damage_index, + byte *kinds, struct damage_reach *reach, short *grenade_type, real *grenade_rate) +{ + struct game_globals *globals; + short type; + if (!mcc_grenades_active()) return; + globals = scenario_get_game_globals(); + for (type = 2; type < mcc_grenades_type_count() && type < globals->grenades.count; ++type) { + struct game_globals_grenade *grenade = TAG_BLOCK_GET_ELEMENT(&globals->grenades, type, struct game_globals_grenade); + boolean thrown = distributed_grenade_throw(player, type) != NONE; + if (grenade->projectile.index != NONE && (thrown || distributed_explosion_this_tick(player, type, NULL))) { + byte extra_kinds; + struct damage_reach extra_reach; + real rate = distributed_source_deals(grenade->projectile.index, damage_index, TRUE, &extra_kinds, &extra_reach); + if (rate > 0.0f) { + distributed_reach_combine(reach, &extra_reach); + if (*grenade_type == NONE || thrown) *grenade_type = type; + } + *grenade_rate = MAX(*grenade_rate, rate); + *kinds |= extra_kinds; + } + } +} diff --git a/port/linux/game/mcc_objects.c b/port/linux/game/mcc_objects.c new file mode 100644 index 0000000000..9449d803be --- /dev/null +++ b/port/linux/game/mcc_objects.c @@ -0,0 +1,36 @@ +/* MCC scenario vehicles retain their own multiplayer placement masks. These + * rules are active only during an MCC game and never modify a game variant. */ +#include "cseries.h" +#include "game/game_engine.h" +#include "mcc_cache.h" +#include "mcc_objects.h" +#include + +static unsigned short mcc_vehicle_default_mask(void) +{ + if (!mcc_cache_tags_loaded() || !game_engine_running() || + game_engine_get_variant()->universal_variant.vehicle_set == 1) + return 0; + switch (game_engine_get_variant()->game_engine_index) { + case game_engine_slayer: return 0x01; + case game_engine_ctf: return 0x02; + case game_engine_king: return 0x04; + case game_engine_oddball: return 0x08; + default: return 0; + } +} + +boolean mcc_vehicle_placement_rules(void) +{ + return mcc_vehicle_default_mask() != 0; +} + +boolean mcc_vehicle_placement_allowed(struct scenario_object_datum const *placement) +{ + unsigned short mask = mcc_vehicle_default_mask(), flags; + if (!mask) return TRUE; + /* The independent MCC tag schema checks each 0x78-byte vehicle placement + * before this entry point; its spawn flags are the word at 0x5A. */ + memcpy(&flags, (unsigned char const *)placement + 0x5A, sizeof(flags)); + return (flags & mask) != 0; +} diff --git a/port/linux/game/mcc_objects.h b/port/linux/game/mcc_objects.h new file mode 100644 index 0000000000..5cf449acbe --- /dev/null +++ b/port/linux/game/mcc_objects.h @@ -0,0 +1,8 @@ +#ifndef MCC_OBJECTS_H +#define MCC_OBJECTS_H + +struct scenario_object_datum; +boolean mcc_vehicle_placement_rules(void); +boolean mcc_vehicle_placement_allowed(struct scenario_object_datum const *placement); + +#endif diff --git a/port/linux/game/mcc_pause.c b/port/linux/game/mcc_pause.c new file mode 100644 index 0000000000..224a6a6d66 --- /dev/null +++ b/port/linux/game/mcc_pause.c @@ -0,0 +1,453 @@ +/* MCC-owned native pause definitions. The layouts follow the stock Xbox + * logical viewport sizes; no embedded map widget or CE converter is edited. */ +#include "cseries.h" +#include "tag_files/tag_groups.h" +#include "text/text_group.h" +#include "mcc_pause.h" +#include "mcc_ui.h" +#include +#include +#include +#include + +struct mcc_pause_rect { short top, left, bottom, right; }; +struct mcc_pause_event { + long flags; + short event, function; + struct tag_reference widget, sound; + char script[32]; +}; +struct mcc_pause_child { + struct tag_reference widget; + char name[32]; + long flags; + short controller, y, x; + unsigned char padding[0x50 - 0x3A]; +}; +struct mcc_pause_input { short function; unsigned char padding[0x22]; }; +struct mcc_pause_widget { + short type, controller; + char name[32]; + struct mcc_pause_rect bounds; + long flags, close_ms, fade_ms; + struct tag_reference background; + struct tag_block inputs, events, replacements; + unsigned char padding06C[0xEC - 0x6C]; + struct tag_reference text, font; + float color[4]; + short justification; + unsigned short text_flags; + unsigned char padding120[0x12E - 0x120]; + short string_index, text_x, text_y; + unsigned char padding134[0x150 - 0x134]; + long list_flags; + struct tag_reference list_header, list_footer; + struct mcc_pause_rect list_header_bounds, list_footer_bounds; + unsigned char padding184[0x1A4 - 0x184]; + struct tag_reference description; + unsigned char padding1B4[0x2D4 - 0x1B4]; + struct tag_block conditional; + unsigned char padding2E0[0x3E0 - 0x2E0]; + struct tag_block children; +}; +typedef char mcc_pause_widget_size[sizeof(struct mcc_pause_widget) == 0x3EC ? 1 : -1]; +typedef char mcc_pause_event_size[sizeof(struct mcc_pause_event) == 0x48 ? 1 : -1]; +typedef char mcc_pause_child_size[sizeof(struct mcc_pause_child) == 0x50 ? 1 : -1]; +typedef char mcc_pause_child_offset[offsetof(struct mcc_pause_widget, children) == 0x3E0 ? 1 : -1]; + +enum { MCC_PAUSE_MAX_TAGS = 512, MCC_PAUSE_ARENA_BYTES = 2 * 1024 * 1024 }; +enum { SOLO, COOP_HOST, COOP_CLIENT, MP_LOCAL, MP_HOST, MP_HOST_TEAMS, MP_CLIENT, MP_CLIENT_TEAMS, MODE_COUNT }; +enum { LABEL_RESUME, LABEL_REVERT, LABEL_RESTART, LABEL_SAVE_QUIT, LABEL_LEAVE, LABEL_SETTINGS, + LABEL_TEAMS, LABEL_END, LABEL_RED, LABEL_BLUE, LABEL_CANCEL, LABEL_OK, LABEL_OBJECTIVES, + LABEL_CONFIRM_REVERT, LABEL_CONFIRM_RESTART, LABEL_CONFIRM_SAVE_QUIT, LABEL_CONFIRM_LEAVE, + LABEL_CONFIRM_END, LABEL_ATTENTION, LABEL_TEAM_TITLE, LABEL_RESTART_LEVEL, LABEL_FOOTER, LABEL_COUNT }; +enum { CONFIRM_REVERT, CONFIRM_RESTART, CONFIRM_SAVE_QUIT, CONFIRM_LEAVE, CONFIRM_END, CONFIRM_COUNT }; +static char const *const mcc_pause_labels[LABEL_COUNT] = { + "RESUME GAME", "REVERT TO SAVED", "RESTART GAME", "SAVE AND QUIT", "LEAVE GAME", "SETTINGS", + "CHOOSE TEAM", "END GAME", "RED TEAM", "BLUE TEAM", "CANCEL", "OK", "MISSION OBJECTIVES:", + "All progress since the last checkpoint will be lost.", + "Restart this game? Current progress will be lost.", + "Save your last checkpoint and return to the main menu?", + "Are you sure you want to leave this game?", + "End this round and return to the lobby?", "ATTENTION", "CHOOSE TEAM", "RESTART LEVEL", "%a-button SELECT %b-button BACK" +}; + +static struct { + unsigned char *arena; + unsigned long used; + long count, first, strings; + unsigned short salt; + boolean failed; + struct mcc_pause_assets assets; + struct mcc_pause_tag tags[MCC_PAUSE_MAX_TAGS]; + long roots[MODE_COUNT][MCC_PAUSE_LAYOUT_COUNT]; + long confirm[MCC_PAUSE_LAYOUT_COUNT][2][CONFIRM_COUNT]; + long team[MCC_PAUSE_LAYOUT_COUNT]; +} mcc_pause; + +static void *mcc_pause_allocate(unsigned long size) +{ + unsigned long start = (mcc_pause.used + 7u) & ~7u; + void *result; + if (!mcc_pause.arena || start > MCC_PAUSE_ARENA_BYTES || size > MCC_PAUSE_ARENA_BYTES - start) { + mcc_pause.failed = TRUE; + return NULL; + } + result = mcc_pause.arena + start; + mcc_pause.used = start + size; + memset(result, 0, size); + return result; +} + +static void mcc_pause_reference(struct tag_reference *reference, long group, long handle) +{ + reference->group_tag = group; + reference->index = handle; +} + +static long mcc_pause_add(long group, char const *stem, void *data) +{ + struct mcc_pause_tag *tag; + char *name; + long ordinal = mcc_pause.count; + if (!data || ordinal >= MCC_PAUSE_MAX_TAGS || mcc_pause.first + ordinal > 0xFFFFL) { + mcc_pause.failed = TRUE; + return NONE; + } + name = mcc_pause_allocate(96); + if (!name) return NONE; + snprintf(name, 96, "mcc_pause\\%s_%03ld", stem, ordinal); + tag = &mcc_pause.tags[mcc_pause.count++]; + tag->group = group; + tag->handle = (long)(((uint32_t)(unsigned short)(mcc_pause.salt + ordinal) << 16) | + (uint32_t)(mcc_pause.first + ordinal)); + tag->name = name; + tag->data = data; + return tag->handle; +} + +static struct mcc_pause_widget *mcc_pause_definition(long handle) +{ + unsigned long ordinal = ((unsigned long)handle & 0xFFFFu) - (unsigned long)mcc_pause.first; + return ordinal < (unsigned long)mcc_pause.count && mcc_pause.tags[ordinal].handle == handle && + mcc_pause.tags[ordinal].group == 'DeLa' ? mcc_pause.tags[ordinal].data : NULL; +} + +static long mcc_pause_widget(char const *name, short type, short width, short height, short layout, short label) +{ + struct mcc_pause_widget *widget = mcc_pause_allocate(sizeof(*widget)); + if (!widget) return NONE; + widget->type = type; + widget->controller = 4; + strncpy(widget->name, name, sizeof(widget->name) - 1); + widget->bounds.right = width; + widget->bounds.bottom = height; + mcc_pause_reference(&widget->background, 'bitm', NONE); + mcc_pause_reference(&widget->text, 'ustr', label == NONE ? NONE : mcc_pause.strings); + mcc_pause_reference(&widget->font, 'font', type == 1 ? mcc_pause.assets.font[layout] : NONE); + mcc_pause_reference(&widget->list_header, 'bitm', NONE); + mcc_pause_reference(&widget->list_footer, 'bitm', NONE); + mcc_pause_reference(&widget->description, 'DeLa', NONE); + widget->string_index = label == NONE ? 0 : label; + widget->color[0] = 1.0f; + widget->color[1] = 0.156863f; + widget->color[2] = 0.588235f; + widget->color[3] = 1.0f; + return mcc_pause_add('DeLa', name, widget); +} + +static void mcc_pause_background(long handle, short width, short height, enum mcc_pause_art_kind kind) +{ + struct mcc_pause_widget *widget = mcc_pause_definition(handle); + long bitmap; + if (!widget || mcc_pause.failed) return; + bitmap = mcc_pause.assets.art(width, height, kind); + if (bitmap == NONE) { mcc_pause.failed = TRUE; return; } + mcc_pause_reference(&widget->background, 'bitm', bitmap); +} + +static void mcc_pause_events(long handle, short action, long target, long flags, boolean back) +{ + struct mcc_pause_widget *widget = mcc_pause_definition(handle); + struct mcc_pause_event *events; + short i, count = 2; + if (!widget) return; + events = mcc_pause_allocate(count * sizeof(*events)); + if (!events) return; + widget->events.count = count; + widget->events.address = events; + for (i = 0; i < count; ++i) { + events[i].event = back ? (i ? 13 : 1) : (i ? 28 : 0); + events[i].flags = flags; + events[i].function = action; + mcc_pause_reference(&events[i].widget, 'DeLa', target); + mcc_pause_reference(&events[i].sound, 'snd!', NONE); + } +} + +static void mcc_pause_children(long handle, long const *children, short const *x, short const *y, short count) +{ + struct mcc_pause_widget *widget = mcc_pause_definition(handle); + struct mcc_pause_child *references; + short i; + if (!widget || count < 1) return; + references = mcc_pause_allocate(count * sizeof(*references)); + if (!references) return; + widget->children.count = count; + widget->children.address = references; + for (i = 0; i < count; ++i) { + mcc_pause_reference(&references[i].widget, 'DeLa', children[i]); + references[i].x = x[i]; + references[i].y = y[i]; + snprintf(references[i].name, sizeof(references[i].name), "item_%d", (int)i); + } +} + +static long mcc_pause_button(short layout, short width, short height, short label, short action, long target, long flags) +{ + long handle = mcc_pause_widget("button", 1, width, height, layout, label); + struct mcc_pause_widget *widget = mcc_pause_definition(handle); + if (!widget) return NONE; + widget->justification = 2; + widget->text_y = 3; + if (layout == MCC_PAUSE_QUARTER) + widget->font.index = mcc_pause.assets.small_font[layout]; + mcc_pause_background(handle, width, height, MCC_PAUSE_ART_HIGHLIGHT); + mcc_pause_events(handle, action, target, flags, FALSE); + return handle; +} + +static long mcc_pause_panel(short layout, short width, short height) +{ + long panel = mcc_pause_widget("panel", 0, width, height, layout, NONE); + mcc_pause_background(panel, width, height, MCC_PAUSE_ART_PANEL); + return panel; +} + +static long mcc_pause_dialog(short layout, boolean pauses, short kind) +{ + static short const actions[CONFIRM_COUNT] = {MCC_PAUSE_ACTION_REVERT, MCC_PAUSE_ACTION_RESTART, + MCC_PAUSE_ACTION_QUIT, MCC_PAUSE_ACTION_QUIT, MCC_PAUSE_ACTION_END_GAME}; + long *saved = &mcc_pause.confirm[layout][pauses != FALSE][kind]; + long root, children[5], buttons[2], list; + short width = layout == MCC_PAUSE_QUARTER ? 320 : 640, height = layout == MCC_PAUSE_FULL ? 480 : 240; + short panel_width = layout == MCC_PAUSE_QUARTER ? 292 : 360, panel_height = 176; + short px = (short)((width - panel_width) / 2), py = (short)((height - panel_height) / 2); + short xs[5] = {0}, ys[5] = {0}, bx[2] = {0}, by[2] = {0}; + struct mcc_pause_widget *widget; + if (*saved != NONE) return *saved; + root = mcc_pause_widget("confirmation", 0, width, height, layout, NONE); + widget = mcc_pause_definition(root); + if (!widget) return NONE; + widget->flags = 1 | (pauses ? 2 : 0); + mcc_pause_background(root, width, height, MCC_PAUSE_ART_DIM); + mcc_pause_events(root, 0, NONE, 512, TRUE); + children[0] = mcc_pause_panel(layout, panel_width, panel_height); + xs[0] = px; ys[0] = py; + children[1] = mcc_pause_widget("attention", 1, panel_width - 24, 24, layout, LABEL_ATTENTION); + xs[1] = px + 12; ys[1] = py + 8; + children[2] = mcc_pause_widget("confirmation_text", 1, panel_width - 24, 84, layout, LABEL_CONFIRM_REVERT + kind); + widget = mcc_pause_definition(children[2]); + if (widget) widget->font.index = mcc_pause.assets.small_font[layout]; + xs[2] = px + 12; ys[2] = py + 40; + list = mcc_pause_widget("confirmation_choices", 3, panel_width - 16, 30, layout, NONE); + widget = mcc_pause_definition(list); + if (widget) widget->flags = 1 | 64; /* Left/right navigation. */ + /* Cancel is first so an accidental second accept cannot destroy progress. */ + buttons[0] = mcc_pause_button(layout, (panel_width - 20) / 2, 27, LABEL_CANCEL, 0, NONE, 512); + buttons[1] = mcc_pause_button(layout, (panel_width - 20) / 2, 27, LABEL_OK, actions[kind], NONE, + 128 | (kind <= CONFIRM_RESTART ? 4 : 0)); + bx[1] = (panel_width - 16) / 2; + mcc_pause_children(list, buttons, bx, by, 2); + children[3] = list; xs[3] = px + 8; ys[3] = py + 136; + mcc_pause_children(root, children, xs, ys, 4); + *saved = root; + return root; +} + +static long mcc_pause_team_screen(short layout) +{ + long root, children[3], buttons[3], list; + short width = layout == MCC_PAUSE_QUARTER ? 320 : 640, height = layout == MCC_PAUSE_FULL ? 480 : 240; + short x[3] = {0}, y[3] = {0}, bx[3] = {0}, by[3] = {0, 32, 64}; + struct mcc_pause_widget *widget; + if (mcc_pause.team[layout] != NONE) return mcc_pause.team[layout]; + root = mcc_pause_widget("choose_team", 0, width, height, layout, NONE); + widget = mcc_pause_definition(root); + if (!widget) return NONE; + widget->flags = 1; + mcc_pause_background(root, width, height, MCC_PAUSE_ART_DIM); + mcc_pause_events(root, 0, NONE, 512, TRUE); + children[0] = mcc_pause_panel(layout, 226, 156); x[0] = (width - 226) / 2; y[0] = (height - 156) / 2; + children[1] = mcc_pause_widget("choose_team_caption", 1, 202, 24, layout, LABEL_TEAM_TITLE); + x[1] = (width - 202) / 2; y[1] = y[0] + 8; + list = mcc_pause_widget("teams", 3, 202, 96, layout, NONE); + widget = mcc_pause_definition(list); if (widget) widget->flags = 0x21; + buttons[0] = mcc_pause_button(layout, 202, 27, LABEL_RED, MCC_PAUSE_ACTION_RED_TEAM, NONE, 128); + buttons[1] = mcc_pause_button(layout, 202, 27, LABEL_BLUE, MCC_PAUSE_ACTION_BLUE_TEAM, NONE, 128); + buttons[2] = mcc_pause_button(layout, 202, 27, LABEL_CANCEL, 0, NONE, 512); + mcc_pause_children(list, buttons, bx, by, 3); + children[2] = list; x[2] = (width - 202) / 2; y[2] = y[0] + 44; + mcc_pause_children(root, children, x, y, 3); + mcc_pause.team[layout] = root; + return root; +} + +static long mcc_pause_screen(short mode, short layout) +{ + boolean campaign = mode <= COOP_CLIENT, pauses = mode == SOLO; + boolean host = mode == COOP_HOST || mode == MP_HOST || mode == MP_HOST_TEAMS; + boolean teams = mode == MP_HOST_TEAMS || mode == MP_CLIENT_TEAMS; + long root, list, children[6], buttons[6]; + short width = layout == MCC_PAUSE_QUARTER ? 320 : 640, height = layout == MCC_PAUSE_FULL ? 480 : 240; + short count = 0, child_count = 0, xs[6] = {0}, ys[6] = {0}, bx[6] = {0}, by[6] = {0}; + short row = campaign ? (layout == MCC_PAUSE_QUARTER ? 24 : 28) : (layout == MCC_PAUSE_FULL ? 35 : 27); + short button_width = campaign && layout == MCC_PAUSE_QUARTER ? 264 : 202; + short button_height = row < 28 ? row - 1 : 27, x, y, panel_height, i; + struct mcc_pause_widget *widget; + root = mcc_pause_widget("pause", 0, width, height, layout, NONE); + widget = mcc_pause_definition(root); if (!widget) return NONE; + widget->flags = 1 | (pauses ? 2 : 0); + mcc_pause_background(root, width, height, MCC_PAUSE_ART_DIM); + /* The action closes this controller's tree and history. Native close-all + * flags would also dismiss other split-screen players' menus. */ + mcc_pause_events(root, MCC_PAUSE_ACTION_RESUME, NONE, 128, TRUE); + buttons[count++] = mcc_pause_button(layout, button_width, button_height, LABEL_RESUME, MCC_PAUSE_ACTION_RESUME, NONE, 128); + if (campaign && mode != COOP_CLIENT) { + buttons[count++] = mcc_pause_button(layout, button_width, button_height, LABEL_REVERT, 0, + mcc_pause_dialog(layout, pauses, CONFIRM_REVERT), 8); + buttons[count++] = mcc_pause_button(layout, button_width, button_height, LABEL_RESTART_LEVEL, 0, + mcc_pause_dialog(layout, pauses, CONFIRM_RESTART), 8); + } + if (teams) buttons[count++] = mcc_pause_button(layout, button_width, button_height, LABEL_TEAMS, 0, mcc_pause_team_screen(layout), 8); + /* The native profile editor is 640x480. Its lower controls do not fit + * split-screen viewports; those players use Settings from the main menu. */ + if (!campaign && mcc_pause.assets.settings && layout == MCC_PAUSE_FULL) + buttons[count++] = mcc_pause_button(layout, button_width, button_height, LABEL_SETTINGS, MCC_PAUSE_ACTION_SETTINGS, NONE, 128); + if (!campaign && (host || mode == MP_LOCAL)) + buttons[count++] = mcc_pause_button(layout, button_width, button_height, LABEL_RESTART, 0, + mcc_pause_dialog(layout, FALSE, CONFIRM_RESTART), 8); + if (!campaign && host) + buttons[count++] = mcc_pause_button(layout, button_width, button_height, LABEL_END, 0, + mcc_pause_dialog(layout, FALSE, CONFIRM_END), 8); + buttons[count++] = mcc_pause_button(layout, button_width, button_height, mode == SOLO ? LABEL_SAVE_QUIT : LABEL_LEAVE, 0, + mcc_pause_dialog(layout, pauses, mode == SOLO ? CONFIRM_SAVE_QUIT : CONFIRM_LEAVE), 8); + if (campaign) { + x = layout == MCC_PAUSE_QUARTER ? 28 : 72; + y = layout == MCC_PAUSE_FULL ? 171 : (layout == MCC_PAUSE_HALF ? 51 : 26); + children[child_count] = mcc_pause_panel(layout, layout == MCC_PAUSE_QUARTER ? 296 : 218, + layout == MCC_PAUSE_QUARTER ? 228 : 159); + xs[child_count] = layout == MCC_PAUSE_QUARTER ? 12 : 64; + ys[child_count++] = layout == MCC_PAUSE_FULL ? 162 : (layout == MCC_PAUSE_HALF ? 42 : 10); + if (layout != MCC_PAUSE_QUARTER) { + children[child_count] = mcc_pause_panel(layout, 285, 159); + xs[child_count] = 287; ys[child_count++] = y - 9; + } + } else { + panel_height = count * row + 48; + x = (width - 202) / 2; y = (height - panel_height) / 2 + 10; + children[child_count] = mcc_pause_panel(layout, 226, panel_height); + xs[child_count] = x - 8; ys[child_count++] = y - 10; + } + list = mcc_pause_widget("pause_actions", 3, button_width, count * row, layout, NONE); + widget = mcc_pause_definition(list); if (widget) widget->flags = 0x21; + for (i = 0; i < count; ++i) by[i] = i * row; + mcc_pause_children(list, buttons, bx, by, count); + children[child_count] = list; xs[child_count] = x; ys[child_count++] = y; + if (campaign) { + long caption = mcc_pause_widget("objective_caption", 1, layout == MCC_PAUSE_QUARTER ? 264 : 270, 22, layout, LABEL_OBJECTIVES); + long objective = mcc_pause_widget("objective", 1, layout == MCC_PAUSE_QUARTER ? 264 : 270, + layout == MCC_PAUSE_QUARTER ? 56 : 94, layout, NONE); + struct mcc_pause_input *input = mcc_pause_allocate(sizeof(*input)); + widget = mcc_pause_definition(caption); + if (widget && layout == MCC_PAUSE_QUARTER) widget->font.index = mcc_pause.assets.small_font[layout]; + widget = mcc_pause_definition(objective); + if (widget && input) { + widget->font.index = mcc_pause.assets.small_font[layout]; + widget->color[1] = 0.0f; widget->color[2] = 0.5f; + input->function = 18; /* Native objective text, driven by the scenario. */ + widget->inputs.count = 1; widget->inputs.address = input; + } + children[child_count] = caption; xs[child_count] = layout == MCC_PAUSE_QUARTER ? 28 : 302; + ys[child_count++] = layout == MCC_PAUSE_FULL ? 166 : (layout == MCC_PAUSE_HALF ? 46 : 130); + children[child_count] = objective; xs[child_count] = layout == MCC_PAUSE_QUARTER ? 28 : 302; + ys[child_count++] = layout == MCC_PAUSE_FULL ? 195 : (layout == MCC_PAUSE_HALF ? 75 : 154); + } + { + short footer_width = campaign ? (layout == MCC_PAUSE_QUARTER ? 264 : 180) : 202; + long footer = mcc_pause_widget("button_key", 1, footer_width, 18, layout, LABEL_FOOTER); + widget = mcc_pause_definition(footer); + /* Native icon text advances through each token; centering the + * individual fragments would separate the glyphs from their labels. */ + if (widget) { widget->font.index = mcc_pause.assets.small_font[layout]; widget->justification = 0; } + children[child_count] = footer; + xs[child_count] = campaign ? (layout == MCC_PAUSE_QUARTER ? 28 : 87) : x; + ys[child_count++] = campaign ? (layout == MCC_PAUSE_FULL ? 296 : layout == MCC_PAUSE_HALF ? 176 : 216) : + y - 10 + panel_height - 24; + } + mcc_pause_children(root, children, xs, ys, child_count); + return root; +} + +void mcc_pause_dispose(void) +{ + if (mcc_pause.arena) free(mcc_pause.arena); + memset(&mcc_pause, 0, sizeof(mcc_pause)); +} + +boolean mcc_pause_build(long first_index, short first_salt, struct mcc_pause_assets const *assets) +{ + struct string_list *strings; + struct string_list_entry *entries; + short mode, layout, label; + mcc_pause_dispose(); + if (!assets || !assets->art || first_index < 0 || first_index > 0xFFFFL - MCC_PAUSE_MAX_TAGS) return FALSE; + for (layout = 0; layout < MCC_PAUSE_LAYOUT_COUNT; ++layout) + if (assets->font[layout] == NONE || assets->small_font[layout] == NONE) return FALSE; + mcc_pause.first = first_index; mcc_pause.salt = (unsigned short)first_salt; mcc_pause.assets = *assets; + mcc_pause.arena = malloc(MCC_PAUSE_ARENA_BYTES); + if (!mcc_pause.arena) return FALSE; + memset(mcc_pause.confirm, 0xFF, sizeof(mcc_pause.confirm)); + memset(mcc_pause.team, 0xFF, sizeof(mcc_pause.team)); + strings = mcc_pause_allocate(sizeof(*strings)); + entries = mcc_pause_allocate(LABEL_COUNT * sizeof(*entries)); + if (!strings || !entries) goto failed; + strings->strings.count = LABEL_COUNT; strings->strings.address = entries; + for (label = 0; label < LABEL_COUNT; ++label) { + size_t i, length = strlen(mcc_pause_labels[label]); + unsigned short *text = mcc_pause_allocate((unsigned long)(length + 1) * 2); + if (!text) goto failed; + for (i = 0; i < length; ++i) text[i] = (unsigned char)mcc_pause_labels[label][i]; + entries[label].string.size = (long)(length + 1) * 2; entries[label].string.address = text; + } + mcc_pause.strings = mcc_pause_add('ustr', "strings", strings); + for (mode = 0; mode < MODE_COUNT && !mcc_pause.failed; ++mode) + for (layout = 0; layout < MCC_PAUSE_LAYOUT_COUNT && !mcc_pause.failed; ++layout) + mcc_pause.roots[mode][layout] = mcc_pause_screen(mode, layout); + if (mcc_pause.failed) goto failed; + return TRUE; +failed: + mcc_pause_dispose(); + return FALSE; +} + +long mcc_pause_tag_count(void) { return mcc_pause.count; } +struct mcc_pause_tag const *mcc_pause_tag_at(long ordinal) +{ + return ordinal >= 0 && ordinal < mcc_pause.count ? &mcc_pause.tags[ordinal] : NULL; +} +boolean mcc_pause_owns(long handle) { return mcc_pause_definition(handle) != NULL; } +boolean mcc_pause_text_wrap_needed(long handle) +{ + struct mcc_pause_widget const *widget = mcc_pause_definition(handle); + return widget && (!strcmp(widget->name, "objective") || !strcmp(widget->name, "confirmation_text")); +} +long mcc_pause_select(boolean campaign, boolean network, boolean host, boolean teams, short local_count, boolean first_player) +{ + short layout, mode; + if (!mcc_pause.arena || mcc_pause.failed || local_count < 1 || local_count > 4) return NONE; + layout = local_count == 1 ? MCC_PAUSE_FULL : local_count == 2 || (local_count == 3 && first_player) ? MCC_PAUSE_HALF : MCC_PAUSE_QUARTER; + mode = campaign ? (!network ? SOLO : host ? COOP_HOST : COOP_CLIENT) : + !network ? MP_LOCAL : host ? (teams ? MP_HOST_TEAMS : MP_HOST) : (teams ? MP_CLIENT_TEAMS : MP_CLIENT); + return mcc_pause.roots[mode][layout]; +} diff --git a/port/linux/game/mcc_pause.h b/port/linux/game/mcc_pause.h new file mode 100644 index 0000000000..bbf29dfd44 --- /dev/null +++ b/port/linux/game/mcc_pause.h @@ -0,0 +1,45 @@ +#ifndef MCC_PAUSE_H +#define MCC_PAUSE_H + +enum mcc_pause_layout { + MCC_PAUSE_FULL, + MCC_PAUSE_HALF, + MCC_PAUSE_QUARTER, + MCC_PAUSE_LAYOUT_COUNT +}; + +enum mcc_pause_art_kind { + MCC_PAUSE_ART_PANEL, + MCC_PAUSE_ART_HIGHLIGHT, + MCC_PAUSE_ART_DIM +}; + +/* Logical viewport sizes: 640x480, 640x240, 320x240. The art callback owns + * and deduplicates its resources; highlights have normal/focused frames. */ +struct mcc_pause_assets { + long font[MCC_PAUSE_LAYOUT_COUNT]; + long small_font[MCC_PAUSE_LAYOUT_COUNT]; + long (*art)(short width, short height, enum mcc_pause_art_kind kind); + boolean settings; +}; + +struct mcc_pause_tag { + long group; + long handle; + char const *name; + void *data; +}; + +/* Builds independent, owned definitions. The caller appends these records + * to a copied live tag table before selecting/opening any of the widgets. */ +boolean mcc_pause_build(long first_index, short first_salt, struct mcc_pause_assets const *assets); +long mcc_pause_tag_count(void); +struct mcc_pause_tag const *mcc_pause_tag_at(long ordinal); +boolean mcc_pause_owns(long handle); +/* Only these owned paragraph widgets need wrapping within their own bounds. */ +boolean mcc_pause_text_wrap_needed(long handle); +long mcc_pause_select(boolean campaign, boolean network, boolean host, boolean teams, + short local_count, boolean first_player); +void mcc_pause_dispose(void); + +#endif diff --git a/port/linux/game/mcc_pause_runtime.c b/port/linux/game/mcc_pause_runtime.c new file mode 100644 index 0000000000..2336308284 --- /dev/null +++ b/port/linux/game/mcc_pause_runtime.c @@ -0,0 +1,207 @@ +/* MCC-owned stock-style pause resources. The map's widgets, HUD and objective + * tags are never overwritten. A copied tag table is published only once the + * complete menu and its independently allocated art are ready. */ +#include "cseries.h" +#include "bitmaps/bitmap_group.h" +#include "game/game.h" +#include "game/game_engine.h" +#include "networking/network_game_globals.h" +#include "rasterizer/xbox/rasterizer_xbox_hardware_bitmaps.h" +#include "scenario/scenario.h" +#include "scenario/scenario_definitions.h" +#include "tag_files/tag_groups.h" +#include "mcc_cache.h" +#include "mcc_pause.h" +#include "mcc_pause_runtime.h" +#include +#include +#include + +#define MCC_PAUSE_MAX_ART 64 +struct mcc_pause_instance { + long group, parents[2], handle; + char *name; + void *data; + unsigned long unused[2]; +}; +typedef char mcc_pause_instance_size[sizeof(struct mcc_pause_instance)==32?1:-1]; +struct mcc_pause_art { + short width, height; + enum mcc_pause_art_kind kind; + long handle; + char name[64]; + struct bitmap_group group; + struct bitmap_group_sequence sequence; + struct bitmap_data frames[2]; +}; +static struct { + struct mcc_pause_instance *original, *table; + long original_count, art_count; + struct mcc_pause_art art[MCC_PAUSE_MAX_ART]; + boolean failed; +} pause_runtime; + +void *cache_files_tag_instances(long *count); +void cache_files_set_tag_instances(void *instances, long count); +char const *config_string(char const *name); +void platform_log(char const *format, ...); +void xgpu_mcc_texture_unbind(unsigned long const *resource); + +static short mcc_pause_power_two(short value) +{ + short size=1; + while(size=w-inset || y>=h-inset) return FALSE; + if(r<=0) return TRUE; + dx=x<7 ? 7-x : x>=w-7 ? x-(w-8) : 0; + dy=y<7 ? 7-y : y>=h-7 ? y-(h-8) : 0; + return !dx || !dy || dx*dx+dy*dy<=r*r; +} + +static long mcc_pause_art_add(short width, short height, enum mcc_pause_art_kind kind) +{ + struct mcc_pause_art *art; + long i; + if(width<=0 || height<=0 || width>1024 || height>512) goto failed; + for(i=0;iwidth==width && art->height==height && art->kind==kind) return art->handle; + } + if(pause_runtime.art_count==MCC_PAUSE_MAX_ART) goto failed; + art=&pause_runtime.art[pause_runtime.art_count++]; + art->width=width;art->height=height;art->kind=kind; + art->handle=(0x5A00UL<<16)|(pause_runtime.original_count+pause_runtime.art_count-1); + snprintf(art->name,sizeof(art->name),"mcc_pause\\art_%d_%dx%d",(int)kind,(int)width,(int)height); + art->sequence.bitmap_count=kind==MCC_PAUSE_ART_HIGHLIGHT?2:1; + art->group.sequences.count=1;art->group.sequences.address=&art->sequence; + art->group.bitmaps.count=art->sequence.bitmap_count;art->group.bitmaps.address=art->frames; + for(i=0;isequence.bitmap_count;i++) { + struct bitmap_data *bitmap=&art->frames[i]; + unsigned long *pixels; + short x,y; + bitmap->signature='bitm';bitmap->width=kind==MCC_PAUSE_ART_DIM?4:mcc_pause_power_two(width); + bitmap->height=kind==MCC_PAUSE_ART_DIM?4:mcc_pause_power_two(height);bitmap->depth=1; + bitmap->format=11;bitmap->flags=1;bitmap->tag_index=art->handle; + bitmap->cache_block_index=NONE; + bitmap->pixels_size=bitmap->width*bitmap->height*4; + bitmap->base_address=malloc(bitmap->pixels_size); + if(!bitmap->base_address) goto failed; + memset(bitmap->base_address,0,bitmap->pixels_size); + pixels=bitmap->base_address; + for(y=0;y<(kind==MCC_PAUSE_ART_DIM?4:height);y++) for(x=0;x<(kind==MCC_PAUSE_ART_DIM?4:width);x++) { + unsigned long color=0; + if(kind==MCC_PAUSE_ART_DIM) color=0x80000000UL; + else if((kind==MCC_PAUSE_ART_PANEL || i==1) && mcc_pause_inside(x,y,width,height,0)) + color=mcc_pause_inside(x,y,width,height,2)? + (kind==MCC_PAUSE_ART_PANEL?0xC00A2649UL:0x60204A75UL):0xFF2896FFUL; + pixels[y*bitmap->width+x]=color; + } + if(!rasterizer_bitmap_new(bitmap)) goto failed; + rasterizer_bitmap_changed(bitmap); + } + return art->handle; +failed: + pause_runtime.failed=TRUE; + return NONE; +} + +void mcc_pause_runtime_unload(void) +{ + long i,j; + extern void mcc_ui_settings_close(short); + mcc_ui_settings_close(NONE); + if(pause_runtime.table) { + cache_files_set_tag_instances(pause_runtime.original,pause_runtime.original_count); + free(pause_runtime.table); + } + mcc_pause_dispose(); + for(i=0;ihardware_format) { + /* A normal frame end clears texture stages, but partial load + * cleanup must also be safe before the next frame completes. */ + xgpu_mcc_texture_unbind(bitmap->hardware_format); + rasterizer_bitmap_delete(bitmap); + } + if(bitmap->base_address) free(bitmap->base_address); + } + memset(&pause_runtime,0,sizeof(pause_runtime)); +} + +boolean mcc_pause_runtime_load(void) +{ + struct mcc_pause_assets assets; + long i,total,font,small; + if(!mcc_cache_tags_loaded()) return FALSE; + if(pause_runtime.table) return TRUE; + pause_runtime.original=cache_files_tag_instances(&pause_runtime.original_count); + if(!pause_runtime.original || pause_runtime.original_count<0 || pause_runtime.original_count>63000) goto failed; + memset(&assets,0,sizeof(assets)); + font=tag_loaded('font',"ui\\large_ui");small=tag_loaded('font',"ui\\small_ui"); + if(font==NONE) font=small; + if(small==NONE) small=font; + if(font==NONE) { + for(i=0;i=65535) goto failed; + pause_runtime.table=malloc(total*sizeof(*pause_runtime.table)); + if(!pause_runtime.table) goto failed; + memcpy(pause_runtime.table,pause_runtime.original,pause_runtime.original_count*sizeof(*pause_runtime.table)); + for(i=pause_runtime.original_count;iparents[0]=entry->parents[1]=NONE; + if(ordinalgroup='bitm';entry->handle=art->handle;entry->name=art->name;entry->data=&art->group; + } else { + struct mcc_pause_tag const *tag=mcc_pause_tag_at(ordinal-pause_runtime.art_count); + if(!tag) goto failed; + entry->group=tag->group;entry->handle=tag->handle;entry->name=(char *)tag->name;entry->data=tag->data; + } + } + cache_files_set_tag_instances(pause_runtime.table,total); + platform_log("mcc: native pause menus added (%ld widgets/strings, %ld art groups)",mcc_pause_tag_count(),pause_runtime.art_count); + return TRUE; +failed: + platform_log("mcc: native pause menus could not be built"); + mcc_pause_runtime_unload(); + return FALSE; +} + +boolean mcc_pause_runtime_active(void) {return mcc_cache_tags_loaded();} + +long mcc_pause_runtime_screen(short local_count, boolean first_player) +{ + boolean network,host,teams=FALSE; + if(!mcc_cache_tags_loaded() || !pause_runtime.table || !global_scenario) return NONE; + network=game_connection()==_game_connection_network_server || game_connection()==_game_connection_network_client; + host=global_network_game_server_get()!=NULL; + if(global_scenario->type==1) teams=game_engine_get_variant()->universal_variant.teams; + return mcc_pause_select(global_scenario->type==0,network,host,teams,local_count,first_player); +} diff --git a/port/linux/game/mcc_pause_runtime.h b/port/linux/game/mcc_pause_runtime.h new file mode 100644 index 0000000000..0e1e70958f --- /dev/null +++ b/port/linux/game/mcc_pause_runtime.h @@ -0,0 +1,9 @@ +#ifndef MCC_PAUSE_RUNTIME_H +#define MCC_PAUSE_RUNTIME_H + +boolean mcc_pause_runtime_load(void); +void mcc_pause_runtime_unload(void); +boolean mcc_pause_runtime_active(void); +long mcc_pause_runtime_screen(short local_count, boolean first_player); + +#endif diff --git a/port/linux/game/mcc_player.c b/port/linux/game/mcc_player.c new file mode 100644 index 0000000000..06a817c60e --- /dev/null +++ b/port/linux/game/mcc_player.c @@ -0,0 +1,44 @@ +/* MCC player input/profile extensions. Native action and profile layouts + * remain unchanged; extra counts belong to the MCC inventory sidecar. */ +#include "cseries.h" +#include "math/real_math.h" +#include "game/players.h" +#include "units/units.h" +#include "scenario/scenario_definitions.h" +#include "mcc_grenades.h" +#include "mcc_player.h" + +boolean mcc_player_choice_supported(short choice) +{ + return mcc_grenades_active() && + (choice == NONE || (choice >= 0 && choice < mcc_grenades_type_count())); +} + +boolean mcc_player_action_valid(struct player_action const *action) +{ + if (!action || !mcc_player_choice_supported(action->desired_grenade_index)) return FALSE; + if (!valid_real(action->desired_facing.yaw) || !valid_real(action->desired_facing.pitch) || + !valid_real(action->throttle.i) || !valid_real(action->throttle.j) || !valid_real(action->primary_trigger)) + return FALSE; + if (action->desired_weapon_index != NONE && + (action->desired_weapon_index < 0 || action->desired_weapon_index >= MAXIMUM_WEAPONS_PER_UNIT)) return FALSE; + return action->desired_zoom_level >= NONE; +} + +void mcc_player_profile_grenades(long unit, struct scenario_starting_profile const *profile, boolean reset) +{ + short slot; + unsigned char const *counts; + if (!mcc_grenades_active() || !profile) return; + counts = (unsigned char const *)profile + offsetof(struct scenario_starting_profile, grenade_counts); + if (reset) mcc_grenades_clear(unit); + /* MCC uses the two bytes following the original pair for slots 2/3. */ + for (slot = 2; slot < mcc_grenades_type_count(); ++slot) + if (counts[slot]) + mcc_grenades_set(unit, slot, (short)MIN(127, mcc_grenades_get(unit, slot) + counts[slot])); +} + +void mcc_player_postspawn_grenades(long unit, boolean disabled) +{ + if (mcc_grenades_active() && disabled) mcc_grenades_clear(unit); +} diff --git a/port/linux/game/mcc_player.h b/port/linux/game/mcc_player.h new file mode 100644 index 0000000000..40fe692393 --- /dev/null +++ b/port/linux/game/mcc_player.h @@ -0,0 +1,9 @@ +#ifndef MCC_PLAYER_H +#define MCC_PLAYER_H +struct player_action; +struct scenario_starting_profile; +boolean mcc_player_choice_supported(short choice); +boolean mcc_player_action_valid(struct player_action const *action); +void mcc_player_profile_grenades(long unit, struct scenario_starting_profile const *profile, boolean reset); +void mcc_player_postspawn_grenades(long unit, boolean disabled); +#endif diff --git a/port/linux/game/mcc_runtime.h b/port/linux/game/mcc_runtime.h new file mode 100644 index 0000000000..6210b315f2 --- /dev/null +++ b/port/linux/game/mcc_runtime.h @@ -0,0 +1,60 @@ +/* MCC-only runtime state. Nothing in this interface aliases a Custom Edition + * cache, allocator, conversion flag, resource offset or map identity. */ +#ifndef MCC_RUNTIME_H +#define MCC_RUNTIME_H + +#include "mcc_cache_format.h" +struct bitmap_data; + +struct mcc_runtime { + unsigned char *tags; + unsigned char *tag_index; + uint32_t used; + uint32_t capacity; + struct mcc_cache_source source; + struct mcc_cache_report report; + /* Owned by the corresponding MCC adapter, released even on failed loads. */ + void *geometry; + void *audio; + void *bitmaps; +}; + +/* Only ranges actually loaded into tags are accessible here. BSP data is + * checked against its own region by the geometry adapter. */ +void *mcc_runtime_pointer(struct mcc_runtime *runtime, uint32_t address, uint32_t bytes); +int mcc_runtime_read(struct mcc_runtime *runtime, uint32_t offset, uint32_t bytes, void *out); + +/* Appends aligned runtime data below the lowest BSP, never overwriting tags. */ +void *mcc_runtime_allocate(struct mcc_runtime *runtime, uint32_t bytes); + +int mcc_geometry_prepare(struct mcc_runtime *runtime); +void mcc_geometry_dispose(struct mcc_runtime *runtime); +int mcc_geometry_bsp_load(struct mcc_runtime *runtime, uint32_t handle, + uint32_t offset, uint32_t size, void *address, void **structure); +void mcc_geometry_bsp_unload(struct mcc_runtime *runtime); +/* Exact MCC-owned hardware/payload identities, used by its tag validator. */ +void *mcc_geometry_buffer_data(struct mcc_runtime *runtime, void const *hardware, + int index_buffer, unsigned long *bytes); +int mcc_geometry_contains(struct mcc_runtime *runtime, void const *address, unsigned long bytes); +short mcc_geometry_part_palette(struct mcc_runtime *runtime, void const *vertex_buffer, + unsigned char const **nodes); + +int mcc_audio_prepare(struct mcc_runtime *runtime); +int mcc_audio_read(struct mcc_runtime *runtime, uint32_t offset, uint32_t bytes, void *out); +int mcc_audio_contains(struct mcc_runtime *runtime, uint32_t offset, uint32_t bytes); +void mcc_audio_dispose(struct mcc_runtime *runtime); +int mcc_bitmaps_prepare(struct mcc_runtime *runtime); +int mcc_hud_bitmaps_prepare(struct mcc_runtime *runtime); +int mcc_bitmaps_read(struct mcc_runtime *runtime, long tag, uint32_t offset, + uint32_t bytes, void *out); +int mcc_bitmaps_contains(struct mcc_runtime *runtime, uint32_t offset, uint32_t bytes); +int mcc_bitmaps_valid(struct mcc_runtime *runtime, struct bitmap_data *bitmap); +void const *mcc_bitmaps_pixels(struct mcc_runtime *runtime, + struct bitmap_data const *bitmap, uint32_t *bytes); +void mcc_bitmaps_dispose(struct mcc_runtime *runtime); + +/* Returns the maximum exclusive virtual stream offset after conversion. */ +uint32_t mcc_audio_stream_end(struct mcc_runtime *runtime); +uint32_t mcc_bitmaps_stream_end(struct mcc_runtime *runtime); + +#endif diff --git a/port/linux/game/mcc_script_parameters.c b/port/linux/game/mcc_script_parameters.c new file mode 100644 index 0000000000..1798ea7018 --- /dev/null +++ b/port/linux/game/mcc_script_parameters.c @@ -0,0 +1,192 @@ +/* MCC wire parameters: script+0x50 block, 36-byte name/type records; + * syntax primitive/global/local flags 0x15 and zero-based slot in data. + * Public schema: Invader scenario.json and scenario/pre_compile.cpp. + * Only signatures/scope are retained here; live values belong to HS stacks. */ +#include "cseries.h" +#include "errors.h" +#include "math/real_math.h" +#include "hs/hs.h" +#include "hs/hs_scenario_definitions.h" +#include "memory/data.h" +#include "scenario/scenario_definitions.h" +#include "mcc_cache.h" +#include "mcc_runtime.h" +#include "mcc_script_parameters.h" +#include "mcc_syntax.h" +#include +#include + +#define MCC_PARAMETER_SCRIPTS 512 +#define MCC_PARAMETER_NODES MCC_SYNTAX_CAPACITY +extern struct data_array *hs_syntax_data; +extern short const hs_external_global_count; +static struct mcc_script_signature mcc_signatures[MCC_PARAMETER_SCRIPTS]; +static short mcc_parameter_owners[MCC_PARAMETER_NODES]; +static short mcc_parameter_types[MCC_PARAMETER_NODES]; +static unsigned short mcc_parameter_seen[MCC_PARAMETER_NODES]; +static long mcc_parameter_walk[MCC_PARAMETER_NODES]; +static unsigned mcc_signature_count, mcc_parameter_node_count; + +/* Preparation precedes publication of active MCC state. Match the specific + * MCC AI conversion here without enabling it for any other map. */ +static int mcc_parameter_cast(short actual, short desired) +{ + return hs_can_cast(actual, desired) || (actual == _hs_type_ai && + (HS_TYPE_IS_OBJECT(desired) || desired == _hs_type_object_list)); +} + +void mcc_parameters_dispose(void) +{ + mcc_signature_count = mcc_parameter_node_count = 0; +} + +struct mcc_script_signature const *mcc_parameters_signature(short script) +{ + return mcc_cache_tags_loaded() && script >= 0 && (unsigned)script < mcc_signature_count ? + mcc_signatures + script : NULL; +} + +int mcc_parameter_type(struct hs_syntax_node const *node, short *owner) +{ + uintptr_t offset; + unsigned index; + if (!mcc_cache_tags_loaded() || !node || !(node->flags & 0x10)) return -1; + if (!hs_syntax_data || !hs_syntax_data->data || (node->flags & 0x17) != 0x15) return -2; + offset = (uintptr_t)node - (uintptr_t)hs_syntax_data->data; + index = (unsigned)(offset / sizeof(*node)); + if (offset % sizeof(*node) || offset / sizeof(*node) >= mcc_parameter_node_count || + mcc_parameter_owners[index] < 0 || node->data < 0 || node->data >= 16) return -2; + if (owner) *owner = mcc_parameter_owners[index]; + return mcc_parameter_types[index]; +} + +int mcc_parameter_compile(struct hs_syntax_node const *node) +{ + int type = mcc_parameter_type(node, NULL); + if (type == -1) return -1; + return hs_type_valid(type) && hs_type_valid(node->type) && hs_can_cast((short)type, node->type); +} + +int mcc_parameter_set_valid(struct hs_syntax_node const *node, struct hs_syntax_node const *value) +{ + int type = mcc_parameter_type(node, NULL); + if (type == -1) return -1; + return hs_type_valid(type) && value && value->type == type && node->type == type; +} + +static struct hs_syntax_node *mcc_parameter_node(struct hs_syntax_node *nodes, unsigned count, long handle) +{ + unsigned index = (uint32_t)handle & 0xFFFFu; + return handle != NONE && index < count && nodes[index].datum_header && + (uint16_t)nodes[index].datum_header == (uint16_t)((uint32_t)handle >> 16) ? nodes + index : NULL; +} + +static int mcc_parameter_scope(struct hs_syntax_node *nodes, unsigned count, long root, short owner, unsigned stamp) +{ + unsigned depth = 0; + struct hs_syntax_node *node; + if (root == NONE) return 1; + if (!(node = mcc_parameter_node(nodes, count, root))) return 0; + mcc_parameter_walk[depth++] = root; + mcc_parameter_seen[(uint32_t)root & 0xFFFFu] = (unsigned short)stamp; + while (depth) { + long handle = mcc_parameter_walk[--depth], links[2]; + unsigned index = (uint32_t)handle & 0xFFFFu, i; + node = nodes + index; + if (node->flags & 0x10) { + if (owner < 0 || (node->flags & 0x17) != 0x15 || node->data < 0 || + node->data >= mcc_signatures[owner].count || + (mcc_parameter_owners[index] != NONE && mcc_parameter_owners[index] != owner)) return 0; + mcc_parameter_owners[index] = owner; + mcc_parameter_types[index] = mcc_signatures[owner].types[node->data]; + if (!hs_type_valid(node->type) || + !mcc_parameter_cast(mcc_parameter_types[index], node->type)) return 0; + } + links[0] = node->next_node_index; + links[1] = node->flags & 1 ? NONE : node->data; + for (i = 0; i < 2; ++i) { + unsigned child = (uint32_t)links[i] & 0xFFFFu; + if (links[i] == NONE) continue; + if (!mcc_parameter_node(nodes, count, links[i])) return 0; + if (mcc_parameter_seen[child] == stamp) continue; + if (depth >= count) return 0; + mcc_parameter_seen[child] = (unsigned short)stamp; + mcc_parameter_walk[depth++] = links[i]; + } + } + return 1; +} + +int mcc_parameters_prepare(struct mcc_runtime *runtime, struct scenario *scenario, struct data_array *syntax) +{ + struct hs_script *scripts; + struct hs_global *globals; + struct hs_syntax_node *nodes; + unsigned i, j, count, has_parameters = 0; + mcc_parameters_dispose(); + if (!syntax || syntax->count < 0 || syntax->count > MCC_PARAMETER_NODES || + scenario->hs_scripts.count < 0 || scenario->hs_scripts.count > MCC_PARAMETER_SCRIPTS || + scenario->hs_globals.count < 0 || scenario->hs_globals.count > 1024 - hs_external_global_count) goto fail; + count = (unsigned)syntax->count; + nodes = (struct hs_syntax_node *)(syntax + 1); + scripts = mcc_runtime_pointer(runtime, (uint32_t)(uintptr_t)scenario->hs_scripts.address, + (uint32_t)scenario->hs_scripts.count * sizeof(*scripts)); + globals = mcc_runtime_pointer(runtime, (uint32_t)(uintptr_t)scenario->hs_globals.address, + (uint32_t)scenario->hs_globals.count * sizeof(*globals)); + if ((scenario->hs_scripts.count && !scripts) || (scenario->hs_globals.count && !globals)) goto fail; + memset(mcc_parameter_owners, 0xFF, sizeof(mcc_parameter_owners)); + memset(mcc_parameter_seen, 0, sizeof(mcc_parameter_seen)); + for (i = 0; i < (unsigned)scenario->hs_scripts.count; ++i) { + uint32_t fields[3]; + unsigned char const *parameters; + memcpy(fields, (unsigned char const *)(scripts + i) + 0x50, sizeof(fields)); + if (fields[0] > 16 || (fields[0] && scripts[i].script_type != _hs_script_static && + scripts[i].script_type != _hs_script_stub)) goto fail; + parameters = mcc_runtime_pointer(runtime, fields[1], fields[0] * 36); + if (fields[0] && !parameters) goto fail; + mcc_signatures[i].count = (short)fields[0]; + has_parameters |= fields[0]; + for (j = 0; j < fields[0]; ++j) { + short type; + memcpy(&type, parameters + j * 36 + 32, 2); + if (!memchr(parameters + j * 36, 0, 32) || !hs_type_valid(type) || type == _hs_type_void) goto fail; + mcc_signatures[i].types[j] = type; + } + } + for (i = 0; i < count; ++i) if (nodes[i].datum_header && (nodes[i].flags & 0x10)) has_parameters = 1; + if (has_parameters) { + for (i = 0; i < (unsigned)scenario->hs_scripts.count; ++i) + if (!mcc_parameter_scope(nodes, count, scripts[i].root_expression_index, (short)i, i + 1)) goto fail; + for (i = 0; i < (unsigned)scenario->hs_globals.count; ++i) + if (!mcc_parameter_scope(nodes, count, globals[i].initialization_expression_index, NONE, + i + MCC_PARAMETER_SCRIPTS + 1)) goto fail; + for (i = 0; i < count; ++i) { + struct hs_syntax_node *call = nodes + i, *argument; + struct mcc_script_signature const *signature; + long next; + if (!call->datum_header) continue; + if ((call->flags & 0x10) && mcc_parameter_owners[i] == NONE) goto fail; + if ((call->flags & 3) != 2) continue; + if (call->script_index < 0 || call->script_index >= scenario->hs_scripts.count || + !(argument = mcc_parameter_node(nodes, count, call->data))) goto fail; + if (argument->type != _hs_function_name || !(argument->flags & 1) || + (scripts[call->script_index].script_type != _hs_script_static && + scripts[call->script_index].script_type != _hs_script_stub)) goto fail; + signature = mcc_signatures + call->script_index; + next = argument->next_node_index; + for (j = 0; j < (unsigned)signature->count; ++j) { + argument = mcc_parameter_node(nodes, count, next); + if (!argument || argument->type != signature->types[j]) goto fail; + next = argument->next_node_index; + } + if (next != NONE) goto fail; + } + } + mcc_signature_count = (unsigned)scenario->hs_scripts.count; + mcc_parameter_node_count = count; + return 1; +fail: + mcc_parameters_dispose(); + error(_error_silent, "mcc: invalid parameter block, scope or script call arguments"); + return 0; +} diff --git a/port/linux/game/mcc_script_parameters.h b/port/linux/game/mcc_script_parameters.h new file mode 100644 index 0000000000..ac95fef697 --- /dev/null +++ b/port/linux/game/mcc_script_parameters.h @@ -0,0 +1,15 @@ +#ifndef MCC_SCRIPT_PARAMETERS_H +#define MCC_SCRIPT_PARAMETERS_H +struct mcc_runtime; +struct scenario; +struct data_array; +struct hs_syntax_node; +struct mcc_script_signature { short count; short types[16]; }; +int mcc_parameters_prepare(struct mcc_runtime *, struct scenario *, struct data_array *); +void mcc_parameters_dispose(void); +struct mcc_script_signature const *mcc_parameters_signature(short script); +/* -1 means an ordinary Xbox/CE node; -2 is a damaged MCC parameter. */ +int mcc_parameter_type(struct hs_syntax_node const *node, short *owner); +int mcc_parameter_compile(struct hs_syntax_node const *node); +int mcc_parameter_set_valid(struct hs_syntax_node const *node, struct hs_syntax_node const *value); +#endif diff --git a/port/linux/game/mcc_script_runtime.inl b/port/linux/game/mcc_script_runtime.inl new file mode 100644 index 0000000000..398aa27a8d --- /dev/null +++ b/port/linux/game/mcc_script_runtime.inl @@ -0,0 +1,159 @@ +/* Included after the native interpreter's declarations. This MCC-only + * adapter uses its existing stack service, so arguments, recursion, sleep + * and checkpoint state remain inside each native 512-byte thread stack. */ +#include "mcc_script_parameters.h" +long hs_cast(long thread_index, short actual_type, short desired_type, long value); + +struct mcc_hs_arguments { + unsigned long magic; + short script, count, next, phase; + unsigned long references; + long expression, result; + long values[1]; +}; +#define MCC_HS_ARGUMENT_MAGIC 0x5043434DUL + +static long mcc_hs_arguments_size(short count) +{ + return (long)offsetof(struct mcc_hs_arguments, values) + count * (long)sizeof(long); +} + +static struct mcc_hs_arguments *mcc_hs_frame(struct hs_stack_frame *frame) +{ + struct hs_syntax_node *call; + struct mcc_hs_arguments *state; + struct mcc_script_signature const *signature; + if (!frame || frame->size < (long)offsetof(struct mcc_hs_arguments, values) || + !(call = hs_syntax_get(frame->expression_index)) || (call->flags & 3) != 2 || + !(signature = mcc_parameters_signature(call->script_index)) || !signature->count) return NULL; + state = (struct mcc_hs_arguments *)frame->data; + return frame->size >= mcc_hs_arguments_size(signature->count) && + state->magic == MCC_HS_ARGUMENT_MAGIC && state->script == call->script_index && + state->count == signature->count ? state : NULL; +} + +static void mcc_hs_release_arguments(struct mcc_hs_arguments *state) +{ + short i; + for (i = 0; i < state->count; ++i) + if (state->references & (1u << i)) object_list_remove_reference(state->values[i]); + state->references = 0; +} + +static void mcc_hs_release_thread(long thread_index) +{ + struct hs_thread_datum *thread = hs_thread_get(thread_index); + struct hs_stack_frame *frame; + if (!mcc_parameters_signature(0)) return; + for (frame = thread->stack; frame && frame != (struct hs_stack_frame *)thread->stack_data; + frame = frame->previous) { + struct mcc_hs_arguments *state = mcc_hs_frame(frame); + if (state) mcc_hs_release_arguments(state); + } +} + +static long *mcc_hs_parameter_address(long thread_index, struct hs_syntax_node *node, short *type) +{ + struct hs_thread_datum *thread = hs_thread_get(thread_index); + struct hs_stack_frame *frame; + short owner; + int value_type = mcc_parameter_type(node, &owner); + if (value_type < 0) return NULL; + *type = (short)value_type; + for (frame = thread->stack; frame && frame != (struct hs_stack_frame *)thread->stack_data; + frame = frame->previous) { + struct mcc_hs_arguments *state = mcc_hs_frame(frame); + /* A nested call still evaluating actual arguments is in its caller's + * lexical scope, even when it recursively names the same script. */ + if (state && state->phase == 1) { + if (state->script != owner || node->data >= state->count) return NULL; + return &state->values[node->data]; + } + } + return NULL; +} + +static boolean mcc_hs_parameter_evaluate(long thread_index, struct hs_syntax_node *node, long *destination) +{ + long *value; + short type; + if (mcc_parameter_type(node, NULL) == -1) return FALSE; + value = mcc_hs_parameter_address(thread_index, node, &type); + if (!value) hs_syntax_error(thread_index); + else *destination = hs_cast(thread_index, type, node->type, *value); + return TRUE; +} + +static boolean mcc_hs_script_evaluate(short script_index, long thread_index, boolean initialize) +{ + struct mcc_script_signature const *signature = mcc_parameters_signature(script_index); + struct hs_thread_datum *thread; + struct hs_script *script; + struct mcc_hs_arguments *state; + struct hs_syntax_node *argument; + if (!signature || !signature->count) return FALSE; + thread = hs_thread_get(thread_index); + script = TAG_BLOCK_GET_ELEMENT(&global_scenario_get()->hs_scripts, script_index, struct hs_script); + state = hs_stack_allocate(thread_index, mcc_hs_arguments_size(signature->count)); + if (!state) return TRUE; + if (initialize) { + struct hs_syntax_node *call = hs_syntax_get(thread->stack->expression_index); + struct hs_syntax_node *predicate = call ? hs_syntax_get(call->data) : NULL; + if (!predicate) { hs_syntax_error(thread_index); return TRUE; } + csmemset(state, 0, mcc_hs_arguments_size(signature->count)); + state->magic = MCC_HS_ARGUMENT_MAGIC; state->script = script_index; + state->count = signature->count; state->expression = predicate->next_node_index; + state->result = hs_type_default_value(script->return_type); + } + if (state->magic != MCC_HS_ARGUMENT_MAGIC || state->script != script_index || + state->count != signature->count || state->next < 0 || state->next > state->count || + state->phase < 0 || state->phase > 1) { hs_syntax_error(thread_index); return TRUE; } + if (!state->phase) { + if (state->next && signature->types[state->next - 1] == _hs_type_object_list && + !(state->references & (1u << (state->next - 1)))) { + object_list_add_reference(state->values[state->next - 1]); + state->references |= 1u << (state->next - 1); + } + if (state->next < state->count) { + long expression = state->expression; + short slot = state->next++; + argument = hs_syntax_get(expression); + if (!argument || argument->type != signature->types[slot]) { hs_syntax_error(thread_index); return TRUE; } + state->expression = argument->next_node_index; + hs_evaluate(thread_index, expression, &state->values[slot]); + return TRUE; + } + if (state->expression != NONE) { hs_syntax_error(thread_index); return TRUE; } + state->phase = 1; + if (hs_scenario_script_disabled(script_index)) { + mcc_hs_release_arguments(state); + hs_return(thread_index, state->result); + } else hs_evaluate(thread_index, script->root_expression_index, &state->result); + } else { + long result = state->result; + mcc_hs_release_arguments(state); + hs_return(thread_index, result); + } + return TRUE; +} + +static boolean mcc_hs_parameter_set(long thread_index, struct hs_syntax_node *variable, boolean initialize) +{ + short type; + long *destination, *value; + if (mcc_parameter_type(variable, NULL) == -1) return FALSE; + destination = mcc_hs_parameter_address(thread_index, variable, &type); + value = hs_stack_allocate(thread_index, sizeof(long)); + if (!value) return TRUE; + if (!destination) { hs_syntax_error(thread_index); return TRUE; } + if (initialize) hs_evaluate(thread_index, variable->next_node_index, value); + else { + if (type == _hs_type_object_list) { + object_list_remove_reference(*destination); + object_list_add_reference(*value); + } + *destination = *value; + hs_return(thread_index, *value); + } + return TRUE; +} diff --git a/port/linux/game/mcc_scripts.c b/port/linux/game/mcc_scripts.c new file mode 100644 index 0000000000..cfbfb44d35 --- /dev/null +++ b/port/linux/game/mcc_scripts.c @@ -0,0 +1,203 @@ +/* MCC script linking and MCC-only native functions. Unknown functions cause + * a load failure; silently removing campaign logic would hide broken maps. */ +#include "cseries.h" +#include "errors.h" +#include "ai/ai_script.h" +#include "hs/hs.h" +#include "hs/hs_scenario_definitions.h" +#include "hs/object_lists.h" +#include "memory/data.h" +#include "objects/objects.h" +#include "scenario/scenario_definitions.h" +#include "scenario/scenario.h" +#include "mcc_runtime.h" +#include "mcc_cache.h" +#include "mcc_scripts.h" +#include "mcc_script_parameters.h" +#include "mcc_syntax.h" +#include + +boolean hs_macro_function_parse(short index, long expression); + +boolean mcc_script_ai_cast(short actual_type, short desired_type) +{ + return mcc_cache_tags_loaded() && actual_type == _hs_type_ai && + (HS_TYPE_IS_OBJECT(desired_type) || desired_type == _hs_type_object_list); +} + +int mcc_script_parse_ai(struct hs_syntax_node *node, char const *text) +{ + if (!node || (node->flags & 5) != 1 || !(node->flags & 8) || + !mcc_script_ai_cast(node->constant_type, node->type)) return -1; + return text && node->source_offset >= 0 && + ai_index_from_string(global_scenario_get(), text + node->source_offset, &node->data); +} + +long mcc_script_cast_ai(short desired_type, long reference) +{ + long list = object_list_from_ai_reference(reference), cursor, result = NONE; + if (desired_type == _hs_type_object_list) return list; + if (list != NONE) { + long first = object_list_get_first(list, &cursor); + struct object_datum *object = object_try_and_get(first); + if (object && HS_TYPE_IS_OBJECT(desired_type) && + TEST_FLAG(hs_object_type_masks[desired_type - _hs_type_object], object->object.type)) + result = first; + object_list_delete(list); + } + return result; +} + +static void mcc_distance_evaluate(short function, long thread, boolean initialize) +{ + long *arguments = hs_macro_function_evaluate(function, thread, initialize); + union { real value; long bits; } result; + long object, reference; + real_point3d destination, origin; + if (!arguments) return; + result.value = -1.0f; + if (object_try_and_get(arguments[1]) && arguments[0] != NONE && + datum_try_and_get(object_list_header_data, arguments[0])) { + object_get_origin(arguments[1], &destination); + object = object_list_get_first(arguments[0], &reference); + while (object != NONE) { + if (object_try_and_get(object)) { + real distance; + object_get_origin(object, &origin); + distance = distance3d(&origin, &destination); + if (result.value < 0 || distance < result.value) result.value = distance; + } + object = object_list_get_next(arguments[0], &reference); + } + } + hs_return(thread, result.bits); +} + +static struct { + struct hs_function_definition definition; + short second_parameter; +} mcc_distance_definition = { + { _hs_type_real, 0, "objects_distance_to_object", hs_macro_function_parse, + mcc_distance_evaluate, "Minimum distance to an object; -1 for no objects.", + NULL, 2, { _hs_type_object_list } }, _hs_type_object +}; + +static void mcc_mission_segment_evaluate(short function, long thread, boolean initialize) +{ + long *arguments = hs_macro_function_evaluate(function, thread, initialize); + char const *segment; + if (!arguments) return; + segment = (char const *)(uintptr_t)arguments[0]; + /* This port acknowledges a local segment event, without MCC's external + * telemetry service. Keep the boolean result: shipped scripts use it in + * conditional expressions before cinematics. core_save_name is unrelated + * in the Xbox runtime and would create a debug save, not a segment event. + * Argument expressions still run normally, including any sleep/yield. */ + if (segment) error(_error_log, "mcc: mission segment '%.128s'", segment); + hs_return(thread, segment != NULL); +} + +static struct hs_function_definition mcc_mission_segment_definition = { + _hs_type_boolean, 0, "mcc_mission_segment", hs_macro_function_parse, + mcc_mission_segment_evaluate, "Records a local mission segment and acknowledges it.", + NULL, 1, { _hs_type_string } +}; + +struct hs_function_definition *mcc_script_function(short index) +{ + if (!mcc_cache_tags_loaded()) return NULL; + if (index == MCC_HS_DISTANCE_TO_OBJECT) return &mcc_distance_definition.definition; + if (index == MCC_HS_MISSION_SEGMENT) return &mcc_mission_segment_definition; + return NULL; +} + +short mcc_script_find(char const *name) +{ + if (!mcc_cache_tags_loaded() || !name) return NONE; + if (!csstrcasecmp(name, "objects_distance_to_object")) return MCC_HS_DISTANCE_TO_OBJECT; + if (!csstrcasecmp(name, "mcc_mission_segment")) return MCC_HS_MISSION_SEGMENT; + if (!csstrcasecmp(name, "player_effect_set_max_vibrate")) + return hs_find_function_by_name("player_effect_set_max_rumble"); + return NONE; +} + +boolean mcc_script_call_valid(struct hs_syntax_node const *call, struct data_array *syntax) +{ + struct hs_function_definition *function = mcc_script_function(call->function_index); + struct hs_syntax_node *node; + long next; + short argument; + if (!function || !(call->flags & 8) || (call->flags & 3) || + !(node = datum_try_and_get(syntax, call->data))) return FALSE; + next = node->next_node_index; + for (argument = 0; argument < function->parameter_count; ++argument) { + node = datum_try_and_get(syntax, next); + if (!node || node->type != function->parameter_types[argument]) return FALSE; + next = node->next_node_index; + } + return next == NONE; +} + +static char const *mcc_script_string(char const *strings, uint32_t bytes, long offset) +{ + return offset >= 0 && (uint32_t)offset < bytes && memchr(strings + offset, 0, bytes - offset) ? + strings + offset : NULL; +} + +int mcc_scripts_prepare(struct mcc_runtime *runtime) +{ + uint32_t address; + struct scenario *scenario; + struct data_array *syntax; + struct hs_syntax_node *nodes; + char const *strings; + long i, missing = 0; + memcpy(&address, runtime->tag_index + (runtime->report.scenario_handle & 0xFFFFu) * 32 + 20, 4); + scenario = mcc_runtime_pointer(runtime, address, sizeof(*scenario)); + if (!scenario) return FALSE; + if (!scenario->hs_syntax_data.size) return runtime->report.parameterized_script_count == 0; + syntax = mcc_runtime_pointer(runtime, (uint32_t)(uintptr_t)scenario->hs_syntax_data.address, + (uint32_t)scenario->hs_syntax_data.size); + strings = mcc_runtime_pointer(runtime, (uint32_t)(uintptr_t)scenario->hs_string_constants.address, + (uint32_t)scenario->hs_string_constants.size); + if (!syntax || !strings || !mcc_syntax_graph_valid(syntax, scenario->hs_syntax_data.size)) { + error(_error_silent, "mcc: invalid script syntax header or graph (MCC capacity: %d nodes)", MCC_SYNTAX_CAPACITY); + return FALSE; + } + nodes = (struct hs_syntax_node *)(syntax + 1); + for (i = 0; i < syntax->count; ++i) { + struct hs_syntax_node *node = &nodes[i]; + char const *name = NULL; + short index = NONE; + if (!node->datum_header) continue; + if (!(node->flags & 3)) { + uint32_t child = (uint32_t)node->data & 0xFFFFu; + if (child >= (uint32_t)syntax->count || !nodes[child].datum_header || + (uint16_t)nodes[child].datum_header != (uint16_t)((uint32_t)node->data >> 16) || + nodes[child].type != _hs_function_name) + return FALSE; + name = mcc_script_string(strings, scenario->hs_string_constants.size, nodes[child].string_offset); + if (!name) return FALSE; + if (!csstrcasecmp(name, "objects_distance_to_object")) index = MCC_HS_DISTANCE_TO_OBJECT; + else if (!csstrcasecmp(name, "mcc_mission_segment")) index = MCC_HS_MISSION_SEGMENT; + else if (!csstrcasecmp(name, "player_effect_set_max_vibrate")) + index = hs_find_function_by_name("player_effect_set_max_rumble"); + else index = hs_find_function_by_name(name); + if (index != NONE) node->function_index = nodes[child].function_index = index; + } else if ((node->flags & 5) == 5 && ((uint16_t)node->data & 0x8000)) { + name = mcc_script_string(strings, scenario->hs_string_constants.size, node->string_offset); + if (!name) return FALSE; + index = hs_find_global_by_name(name); + if (index != NONE && ((uint16_t)index & 0x8000)) node->short_value = index; + else index = NONE; + } + if (name && index == NONE) { + if (missing < 16) error(_error_silent, "mcc: unsupported script function or global '%s'", name); + ++missing; + } + } + /* Retain the MCC arena, including free slots for console expressions. + * Its separate validator owns the larger traversal workspace. */ + syntax->data = nodes; + return missing == 0 && mcc_parameters_prepare(runtime, scenario, syntax); +} diff --git a/port/linux/game/mcc_scripts.h b/port/linux/game/mcc_scripts.h new file mode 100644 index 0000000000..d24e1015d4 --- /dev/null +++ b/port/linux/game/mcc_scripts.h @@ -0,0 +1,18 @@ +#ifndef MCC_SCRIPTS_H +#define MCC_SCRIPTS_H +struct hs_function_definition; +struct hs_syntax_node; +struct data_array; +struct mcc_runtime; +/* These indices are outside the unchanged Xbox/CE function table. */ +#define MCC_HS_DISTANCE_TO_OBJECT 0x7000 +#define MCC_HS_MISSION_SEGMENT 0x7001 +struct hs_function_definition *mcc_script_function(short index); +short mcc_script_find(char const *name); +boolean mcc_script_call_valid(struct hs_syntax_node const *call, struct data_array *syntax); +int mcc_scripts_prepare(struct mcc_runtime *runtime); +/* MCC permits an AI reference in an object/unit or object-list expression. */ +int mcc_script_parse_ai(struct hs_syntax_node *node, char const *text); +boolean mcc_script_ai_cast(short actual_type, short desired_type); +long mcc_script_cast_ai(short desired_type, long reference); +#endif diff --git a/port/linux/game/mcc_syntax.c b/port/linux/game/mcc_syntax.c new file mode 100644 index 0000000000..53f0ee701a --- /dev/null +++ b/port/linux/game/mcc_syntax.c @@ -0,0 +1,153 @@ +/* Independent MCC syntax validation and traversal storage. Runtime datums + * remain in the map's tag allocation, so handles and checkpoint layout keep + * their existing representation. The shared interpreter's signed indices + * support this format's 32767 slots without widening any engine structure. */ +#include "cseries.h" +#include "errors.h" +#include "math/real_math.h" +#include "hs/hs.h" +#include "hs/hs_scenario_definitions.h" +#include "memory/data.h" +#include "scenario/scenario_definitions.h" +#include "mcc_cache.h" +#include "mcc_syntax.h" +#include +#include + +typedef char mcc_syntax_wire_size_matches_runtime[ + sizeof(struct data_array) + MCC_SYNTAX_CAPACITY * sizeof(struct hs_syntax_node) == + MCC_SYNTAX_MAXIMUM_DATA_BYTES ? 1 : -1]; + +static byte mcc_syntax_colors[MCC_SYNTAX_CAPACITY]; +static byte mcc_syntax_edges[MCC_SYNTAX_CAPACITY]; +static unsigned short mcc_syntax_stack[MCC_SYNTAX_CAPACITY]; +static byte mcc_syntax_seen[MCC_SYNTAX_CAPACITY]; + +static struct hs_syntax_node const *mcc_syntax_node( + struct data_array const *syntax, long handle) +{ + unsigned index = (uint32_t)handle & 0xFFFFu; + struct hs_syntax_node const *nodes = (struct hs_syntax_node const *)(syntax + 1); + if (handle == NONE || index >= (unsigned)syntax->count || !nodes[index].datum_header || + (uint16_t)nodes[index].datum_header != (uint16_t)((uint32_t)handle >> 16)) return NULL; + return nodes + index; +} + +boolean mcc_syntax_graph_valid(struct data_array const *syntax, long bytes) +{ + struct hs_syntax_node const *nodes; + unsigned start, depth, live = 0; + if (!syntax || bytes < (long)sizeof(*syntax) || ((uintptr_t)syntax & 3u) || + syntax->signature != 'd@t@' || !syntax->valid || + syntax->size != sizeof(*nodes) || syntax->maximum_count <= 0 || + syntax->maximum_count > MCC_SYNTAX_CAPACITY || syntax->count < 0 || + syntax->count > syntax->maximum_count || syntax->actual_count < 0 || + syntax->actual_count > syntax->count || syntax->first_free_absolute_index < 0 || + syntax->first_free_absolute_index > syntax->maximum_count || + bytes != (long)(sizeof(*syntax) + (unsigned)syntax->maximum_count * sizeof(*nodes))) return FALSE; + nodes = (struct hs_syntax_node const *)(syntax + 1); + memset(mcc_syntax_colors, 0, sizeof(mcc_syntax_colors)); + for (start = 0; start < (unsigned)syntax->count; ++start) { + if (!nodes[start].datum_header) continue; + ++live; + if (mcc_syntax_colors[start]) continue; + depth = 0; + mcc_syntax_stack[depth] = (unsigned short)start; + mcc_syntax_edges[depth++] = 0; + mcc_syntax_colors[start] = 1; + while (depth) { + unsigned index = mcc_syntax_stack[depth - 1], edge = mcc_syntax_edges[depth - 1]++; + long link; + unsigned child; + if (edge == 2) { + mcc_syntax_colors[index] = 2; + --depth; + continue; + } + link = edge == 0 ? nodes[index].next_node_index : + ((nodes[index].flags & 1) ? NONE : nodes[index].data); + if (link == NONE) continue; + if (!mcc_syntax_node(syntax, link)) return FALSE; + child = (uint32_t)link & 0xFFFFu; + if (mcc_syntax_colors[child] == 1) return FALSE; + if (mcc_syntax_colors[child] == 2) continue; + if (depth >= (unsigned)syntax->count) return FALSE; + mcc_syntax_colors[child] = 1; + mcc_syntax_stack[depth] = (unsigned short)child; + mcc_syntax_edges[depth++] = 0; + } + } + return live == (unsigned)syntax->actual_count; +} + +int mcc_syntax_scenario_valid(struct scenario const *scenario) +{ + if (!mcc_cache_tags_loaded()) return -1; + if (!scenario || scenario->hs_string_constants.size < 0x400 || + !mcc_cache_contains(scenario->hs_string_constants.address, scenario->hs_string_constants.size) || + !mcc_cache_contains(scenario->hs_syntax_data.address, scenario->hs_syntax_data.size)) return FALSE; + return mcc_syntax_graph_valid(scenario->hs_syntax_data.address, scenario->hs_syntax_data.size); +} + +static boolean mcc_syntax_expression_valid(struct data_array const *syntax, long root, short type, + short (*node_refusal)(struct hs_syntax_node const *, char const **), char const **name) +{ + struct hs_syntax_node const *nodes = (struct hs_syntax_node const *)(syntax + 1), *node; + unsigned depth = 0; + *name = NULL; + if (root == NONE) return TRUE; + if (!(node = mcc_syntax_node(syntax, root)) || node->type != type) return FALSE; + memset(mcc_syntax_seen, 0, sizeof(mcc_syntax_seen)); + mcc_syntax_seen[(uint32_t)root & 0xFFFFu] = 1; + mcc_syntax_stack[depth++] = (unsigned short)root; + while (depth) { + long child; + node = nodes + mcc_syntax_stack[--depth]; + if (node_refusal(node, name)) return FALSE; + if (node->flags & 1) continue; + child = node->data; + while (child != NONE) { + unsigned index = (uint32_t)child & 0xFFFFu; + if (!(node = mcc_syntax_node(syntax, child)) || mcc_syntax_seen[index] || + depth >= (unsigned)syntax->count) { + *name = NULL; + return FALSE; + } + mcc_syntax_seen[index] = 1; + mcc_syntax_stack[depth++] = (unsigned short)index; + child = node->next_node_index; + } + } + return TRUE; +} + +boolean mcc_syntax_functions_check(struct scenario *scenario, struct data_array *syntax, + short (*node_refusal)(struct hs_syntax_node const *, char const **), + unsigned long *disabled_scripts, unsigned long *disabled_globals) +{ + long i, scripts = 0, globals = 0; + char const *name, *first_name = NULL, *first_owner = NULL; + if (!mcc_cache_tags_loaded()) return FALSE; + for (i = 0; i < scenario->hs_scripts.count; ++i) { + struct hs_script const *script = (struct hs_script const *)scenario->hs_scripts.address + i; + if (!mcc_syntax_expression_valid(syntax, script->root_expression_index, script->return_type, + node_refusal, &name)) { + BIT_VECTOR_SET_FLAG(disabled_scripts, i, TRUE); + if (!first_owner) { first_owner = script->name; first_name = name; } + ++scripts; + } + } + for (i = 0; i < scenario->hs_globals.count; ++i) { + struct hs_global const *global = (struct hs_global const *)scenario->hs_globals.address + i; + if (!mcc_syntax_expression_valid(syntax, global->initialization_expression_index, global->type, + node_refusal, &name)) { + BIT_VECTOR_SET_FLAG(disabled_globals, i, TRUE); + if (!first_owner) { first_owner = global->name; first_name = name; } + ++globals; + } + } + if (first_owner) error(_error_silent, + "mcc: script validation refused %.32s (%s); %ld scripts won't run, %ld globals start at their defaults", + first_owner, first_name ? first_name : "damaged expression", scripts, globals); + return TRUE; +} diff --git a/port/linux/game/mcc_syntax.h b/port/linux/game/mcc_syntax.h new file mode 100644 index 0000000000..0c2b6b4f77 --- /dev/null +++ b/port/linux/game/mcc_syntax.h @@ -0,0 +1,29 @@ +#ifndef MCC_SYNTAX_H +#define MCC_SYNTAX_H + +/* MCC Halo 1's cache syntax array uses signed 16-bit counts. Its final + * addressable slot is 32766; this does not change the Xbox/CE 19001 limit. */ +#define MCC_SYNTAX_CAPACITY 32767 +/* Serialized 56-byte data-array header followed by 20-byte syntax nodes. */ +#define MCC_SYNTAX_MAXIMUM_DATA_BYTES (56L + 20L * MCC_SYNTAX_CAPACITY) + +struct data_array; +struct scenario; +struct hs_syntax_node; + +/* Pre-publication validation: the caller owns and bounds the complete blob. + * This also rejects cycles before the shared compiler follows sibling links. */ +boolean mcc_syntax_graph_valid(struct data_array const *syntax, long bytes); + +/* -1 for other formats; otherwise whether this active MCC scenario is sound. */ +int mcc_syntax_scenario_valid(struct scenario const *scenario); + +/* Return TRUE only when the active MCC scenario was handled. The callback + * applies the engine's existing function allowlist and argument checks. All + * traversal state belongs to MCC, including when a node is above slot 19000. */ +boolean mcc_syntax_functions_check(struct scenario *scenario, + struct data_array *syntax, + short (*node_refusal)(struct hs_syntax_node const *, char const **), + unsigned long *disabled_scripts, unsigned long *disabled_globals); + +#endif diff --git a/port/linux/game/mcc_tag_validate.c b/port/linux/game/mcc_tag_validate.c new file mode 100644 index 0000000000..5b6a8b0870 --- /dev/null +++ b/port/linux/game/mcc_tag_validate.c @@ -0,0 +1,604 @@ +/* MCC tags use the engine's published schema descriptions, with their own + * traversal, ownership bitmap, stream limits and callback context. Neither + * the Xbox validator state nor any Custom Edition validation mode is used. */ +#include "mcc_tag_validate.h" +#include "mcc_runtime.h" +#include "mcc_syntax.h" +#include "tag_schema.h" + +#include +#include +#include +#include + +#define MCC_VALIDATION_DEPTH 32 +#define MCC_VALIDATION_CAPACITY 0x04000000UL + +struct mcc_validation_instance { + unsigned long group, parents[2]; + long handle; + char *name; + void *root; + unsigned long unused[2]; +}; +struct mcc_validation_header { + struct mcc_validation_instance *instances; + long scenario; + unsigned long checksum; + long count; + long vertex_count; + byte *vertices; + long index_count; + byte *indices; + unsigned long signature; +}; +struct mcc_validation_bsp_header { + void *root; + long vertex_count; + byte *vertices; + long index_count; + byte *indices; + unsigned long signature; +}; +struct mcc_validation_frame { + byte *address; + struct tag_schema_definition const *schema; +}; +struct mcc_validation_context { + byte *region; + unsigned long length; + struct mcc_validation_frame stack[MCC_VALIDATION_DEPTH]; + int depth; + byte *structure; + long handle; + char const *field; + boolean rejected; + boolean geometry_access; +}; +static struct { + struct mcc_runtime *runtime; + struct mcc_validation_header *header; + byte *claims; + unsigned long claim_bytes; + long corrections; + struct mcc_validation_context *active; +} mcc_validation; +static char mcc_validation_empty_name[] = ""; +static struct mcc_validation_dispatch const mcc_callbacks = { + mcc_validation_owns, mcc_validation_message, mcc_validation_file_contains, + mcc_validation_root, mcc_validation_contains, mcc_validation_tag_get, + mcc_validation_buffer_data, mcc_validation_any_claimed +}; + +/* MCC retains tag-collection loader lists that the Xbox schema inventory + * does not describe. Wire format: a 12-byte block of 16-byte references. + * Reference: Invader src/tag/hek/definition/tag_collection.json. */ +struct mcc_collection_entry { struct tag_reference reference; }; +struct mcc_collection_root { struct tag_block tags; }; +static struct tag_schema_field const mcc_collection_entry_fields[] = { + TAG_SCHEMA_REFERENCE(struct mcc_collection_entry, reference, NULL), + TAG_SCHEMA_END +}; +static struct tag_schema_definition const mcc_collection_entry_schema = + TAG_SCHEMA_DEFINITION(mcc_collection_entry, struct mcc_collection_entry, mcc_collection_entry_fields); +static struct tag_schema_field const mcc_collection_fields[] = { + TAG_SCHEMA_TOOL_BLOCK(struct mcc_collection_root, tags, mcc_collection_entry_schema, 200), + TAG_SCHEMA_END +}; +static struct tag_schema_definition const mcc_collection_schema = + TAG_SCHEMA_DEFINITION(mcc_collection, struct mcc_collection_root, mcc_collection_fields); +static struct tag_schema_group const mcc_collection_group = {'tagc', {NONE, NONE}, &mcc_collection_schema}; + +/* Input-device defaults are retained PC/MCC menu data: enum16, flags16 and + * two raw-data descriptors (44 bytes total). Format evidence: Reclaimer + * reclaimer/hek/defs/devc.py; payloads never control the native input layer. */ +struct mcc_device_defaults { + short device_kind; + unsigned short flags; + struct tag_data identifier; + struct tag_data profile; +}; +static struct tag_schema_field const mcc_device_fields[] = { + TAG_SCHEMA_ENUM(struct mcc_device_defaults, device_kind, 3, 0), + TAG_SCHEMA_DATA(struct mcc_device_defaults, identifier, 16), + TAG_SCHEMA_DATA(struct mcc_device_defaults, profile, 41984), + TAG_SCHEMA_END +}; +static struct tag_schema_definition const mcc_device_schema = + TAG_SCHEMA_DEFINITION(mcc_device_defaults, struct mcc_device_defaults, mcc_device_fields); +static struct tag_schema_group const mcc_device_group = {'devc', {NONE, NONE}, &mcc_device_schema}; + +static int mcc_range(void const *base, unsigned long length, void const *address, unsigned long bytes) +{ + uintptr_t origin = (uintptr_t)base, point = (uintptr_t)address; + return point >= origin && point - origin <= length && bytes <= length - (unsigned long)(point - origin); +} + +static int mcc_claim(void const *address, unsigned long size) +{ + unsigned long first, bit; + if (!size) return 1; + if (!mcc_range(mcc_validation.runtime->tags, mcc_validation.runtime->capacity, address, size)) return 0; + first = (unsigned long)((uintptr_t)address - (uintptr_t)mcc_validation.runtime->tags); + for (bit = first; bit < first + size; ++bit) { + byte mask = (byte)(1U << (bit & 7)); + if (mcc_validation.claims[bit >> 3] & mask) return 0; + mcc_validation.claims[bit >> 3] |= mask; + } + return 1; +} + +static void mcc_problem(struct mcc_validation_context *context, int rejected, char const *message) +{ + char text[512]; + if (rejected) context->rejected = TRUE; + else ++mcc_validation.corrections; + if (rejected || mcc_validation.corrections <= 64) { + snprintf(text, sizeof(text), "MCC tag %08lx (%s): %s: %s", context->handle, + context->field ? context->field : "root", rejected ? "rejected" : "corrected", message); + tag_validate_report(text); + } +} + +static struct tag_schema_group const *mcc_group(unsigned long group) +{ + struct tag_schema_group const *const *list; + if (group == 'tagc') return &mcc_collection_group; + if (group == 'devc') return &mcc_device_group; + for (list = tag_schema_group_lists; *list; ++list) { + struct tag_schema_group const *entry; + for (entry = *list; entry->group_tag; ++entry) + if (entry->group_tag == group) return entry; + } + return NULL; +} + +static struct mcc_validation_instance *mcc_instance(long handle) +{ + unsigned long slot = (unsigned long)handle & 0xffffUL; + if (handle == NONE || !mcc_validation.header || slot >= (unsigned long)mcc_validation.header->count) return NULL; + return mcc_validation.header->instances[slot].handle == handle ? &mcc_validation.header->instances[slot] : NULL; +} + +static int mcc_is_group(struct mcc_validation_instance const *instance, unsigned long const *groups) +{ + if (!groups) return 1; + for (; *groups; ++groups) + if (instance->group == *groups || instance->parents[0] == *groups || instance->parents[1] == *groups) return 1; + return 0; +} + +static int mcc_name_valid(struct mcc_validation_context *context, char const *name) +{ + unsigned long remaining; + if (name == mcc_validation_empty_name) return 1; + if (mcc_range(context->region, context->length, name, 1)) + remaining = context->length - (unsigned long)((uintptr_t)name - (uintptr_t)context->region); + else if (mcc_range(mcc_validation.runtime->tags, mcc_validation.runtime->used, name, 1)) + remaining = mcc_validation.runtime->used - (unsigned long)((uintptr_t)name - (uintptr_t)mcc_validation.runtime->tags); + else return 0; + return memchr(name, 0, remaining < 256 ? remaining : 256) != NULL; +} + +static long mcc_integer(byte const *address, short size, int unsign) +{ + if (size == 1) return unsign ? (long)*address : (long)*(signed char const *)address; + if (size == 2) { short value; memcpy(&value, address, 2); return unsign ? (long)(unsigned short)value : value; } + { long value; memcpy(&value, address, 4); return value; } +} +static void mcc_set_integer(byte *address, short size, long value) +{ + if (size == 1) *address = (byte)value; + else if (size == 2) { short small = (short)value; memcpy(address, &small, 2); } + else memcpy(address, &value, 4); +} + +static void mcc_walk(struct mcc_validation_context *context, byte *address, + struct tag_schema_definition const *schema, int checks); + +static struct tag_schema_field const *mcc_field(struct tag_schema_definition const *schema, char const *name) +{ + struct tag_schema_field const *field; + for (field = schema->fields; field->type != _tag_schema_terminator; ++field) + if (field->name && !strcmp(field->name, name)) return field; + return NULL; +} + +/* MCC graph indices are signed shorts, rather than the Xbox authoring + * tool's 256-animation limit. Keep node-tree and permutation-list checks + * independent, using the published schema offsets after structural checks. */ +static int mcc_animation_graph_check(struct mcc_validation_context *context, + byte *address, struct tag_schema_definition const *schema) +{ + struct tag_schema_field const *nodes_field = mcc_field(schema, "nodes"); + struct tag_schema_field const *animations_field = mcc_field(schema, "animations"); + struct tag_schema_definition const *nodes_schema, *animations_schema; + struct tag_schema_field const *sibling, *child, *parent, *next; + struct tag_block const *nodes, *animations; + byte reached[64], *states; + short queue[64]; + long head, tail, first; + if (!nodes_field || !animations_field || !nodes_field->definition || !animations_field->definition) return 0; + nodes_schema = nodes_field->definition; animations_schema = animations_field->definition; + sibling = mcc_field(nodes_schema, "next_sibling_node_index"); + child = mcc_field(nodes_schema, "first_child_node_index"); + parent = mcc_field(nodes_schema, "parent_node_index"); + next = mcc_field(animations_schema, "next_animation_index"); + if (!sibling || !child || !parent || !next) return 0; + nodes = (struct tag_block const *)(address + nodes_field->offset); + animations = (struct tag_block const *)(address + animations_field->offset); + if (nodes->count < 0 || nodes->count > 64 || animations->count < 0 || animations->count > 32767) return 0; + memset(reached, 0, sizeof(reached)); head = tail = 0; + if (nodes->count) { + byte const *root = nodes->address; + /* Root parent is unused, and MCC commonly stores zero there. */ + if (mcc_integer(root + sibling->offset, 2, 0) != NONE) return 0; + queue[tail++] = 0; reached[0] = 1; + } + while (head < tail) { + long index = queue[head++]; + byte const *node = (byte const *)nodes->address + index * nodes_schema->size; + long child_index = mcc_integer(node + child->offset, 2, 0); + while (child_index != NONE) { + byte const *descendant; + if (child_index < 0 || child_index >= nodes->count || reached[child_index] || tail >= 64) return 0; + descendant = (byte const *)nodes->address + child_index * nodes_schema->size; + if (mcc_integer(descendant + parent->offset, 2, 0) != index) return 0; + reached[child_index] = 1; queue[tail++] = (short)child_index; + child_index = mcc_integer(descendant + sibling->offset, 2, 0); + } + } + if (tail != nodes->count) return 0; + if (!animations->count) return 1; + states = malloc(animations->count); + if (!states) return 0; + memset(states, 0, animations->count); + for (first = 0; first < animations->count; ++first) { + long index = first; + while (index != NONE && !states[index]) { + byte const *animation = (byte const *)animations->address + index * animations_schema->size; + states[index] = 1; + index = mcc_integer(animation + next->offset, 2, 0); + if (index != NONE && (index < 0 || index >= animations->count)) { free(states); return 0; } + } + if (index != NONE && states[index] == 1) { free(states); return 0; } + index = first; + while (index != NONE && states[index] == 1) { + byte const *animation = (byte const *)animations->address + index * animations_schema->size; + states[index] = 2; index = mcc_integer(animation + next->offset, 2, 0); + } + } + free(states); (void)context; + return 1; +} + +/* Every container's extents are verified before any of its indices or child + * elements are visited. Claims reject overlap and cycles before dereference. */ +static void mcc_fields(struct mcc_validation_context *context, byte *address, + struct tag_schema_definition const *schema, int stage, int nesting) +{ + struct tag_schema_field const *field; + byte *previous_structure = context->structure; + if (!schema || schema->size <= 0 || nesting >= MCC_VALIDATION_DEPTH || + !mcc_range(context->region, context->length, address, (unsigned long)schema->size)) { + mcc_problem(context, 1, "schema structure is outside loaded data or excessively nested"); return; + } + context->structure = address; + for (field = schema->fields; field->type != _tag_schema_terminator && !context->rejected; ++field) { + int index; + uint64_t extent = (uint64_t)(unsigned short)field->size * (unsigned short)field->count; + context->field = field->name; + /* Inline structures may deliberately omit their trailing unused + * fields; size is still the array stride, not the bytes visited. */ + if (field->type == _tag_schema_struct && field->definition && field->count > 0) { + struct tag_schema_definition const *inner = field->definition; + extent = (uint64_t)(unsigned short)field->size * (field->count - 1) + (unsigned long)inner->size; + if (inner->size <= 0 || inner->size > field->size) extent = UINT64_MAX; + } + if (field->count <= 0 || field->size < 0 || field->offset < 0 || field->offset > schema->size || + extent > (unsigned long)(schema->size - field->offset)) { + mcc_problem(context, 1, "schema field leaves its structure"); break; + } + for (index = 0; index < field->count && !context->rejected; ++index) { + byte *member = address + field->offset + index * field->size; + if (field->type == _tag_schema_struct) { + mcc_fields(context, member, field->definition, stage, nesting + 1); + } else if (field->type == _tag_schema_block) { + struct tag_block *block = (struct tag_block *)member; + struct tag_schema_definition const *element = field->definition; + if (stage == 0) { + unsigned long bytes; + long maximum = field->maximum; + block->definition = NULL; + if (!element || element->size <= 0 || block->count < 0 || + (unsigned long)block->count > context->length / (unsigned long)element->size) { + mcc_problem(context, 1, "invalid block count"); break; + } + bytes = (unsigned long)block->count * (unsigned long)element->size; + if (bytes && (!mcc_range(context->region, context->length, block->address, bytes) || + !mcc_claim(block->address, bytes))) { + mcc_problem(context, 1, "block leaves loaded data or overlaps another allocation"); break; + } + if (schema->name && !strcmp(schema->name, "game_globals") && !strcmp(field->name, "grenades")) { + maximum = 4; + if (block->count < 2 || block->count > maximum) { + mcc_problem(context, 1, "MCC requires two to four grenade kinds"); break; + } + } + if (maximum > 0 && block->count > maximum && + !(field->flags & FLAG(_tag_schema_tool_maximum_bit)) && + /* MCC appends first-person animation slots. Native + * callers index known slots, with no fixed storage. */ + !(schema->name && !strcmp(schema->name, "animation_graph_first_person_weapon_animations") && + !strcmp(field->name, "animations"))) { + block->count = maximum; + mcc_problem(context, 0, "block reduced to engine capacity"); + } + if (!block->count) block->address = NULL; + } else { + long item; + for (item = 0; item < block->count && !context->rejected; ++item) + mcc_walk(context, (byte *)block->address + item * element->size, element, stage == 2); + } + } else if (field->type == _tag_schema_data || field->type == _tag_schema_file_data) { + if (stage == 0) { + struct tag_data *data = (struct tag_data *)member; + long maximum = field->maximum; + /* The shared scenario schema describes Xbox/CE's 19001 + * slots. MCC's separately validated syntax arena keeps + * its own complete span through this later schema pass. */ + if (field->type == _tag_schema_data && schema->name && + !strcmp(schema->name, "scenario") && field->name && + !strcmp(field->name, "hs_syntax_data")) + maximum = MCC_SYNTAX_MAXIMUM_DATA_BYTES; + data->definition = NULL; + if (data->size < 0) { mcc_problem(context, 1, "negative data size"); break; } + if (field->type == _tag_schema_file_data) { + if (!mcc_validation_file_contains((struct tag_validation *)context, data->file_offset, data->size)) + mcc_problem(context, 1, "file data leaves MCC stream"); + } else if (data->size) { + if (mcc_range(context->region, context->length, data->address, data->size)) { + if (!mcc_claim(data->address, (unsigned long)data->size)) + mcc_problem(context, 1, "data overlaps another allocation"); + } else if (!mcc_geometry_contains(mcc_validation.runtime, data->address, data->size)) { + mcc_problem(context, 1, "data leaves loaded bytes and owned geometry"); + } + } + if (maximum > 0 && data->size > maximum) { + data->size = maximum; mcc_problem(context, 0, "data reduced to engine capacity"); + } + if (!data->size && field->type == _tag_schema_data) data->address = NULL; + } + } else if (field->type == _tag_schema_check) { + if (stage == 2) { + int accepted; + context->geometry_access = FALSE; + mcc_validation.active = context; + mcc_validation_callbacks = &mcc_callbacks; + if (!strcmp(field->name, "bitmap_data_check")) + accepted = mcc_bitmaps_valid(mcc_validation.runtime, (struct bitmap_data *)address); + else if (!strcmp(field->name, "animation_graph_check")) + accepted = mcc_animation_graph_check(context, address, schema); + else accepted = field->check((struct tag_validation *)context, address); + if (!accepted && !context->rejected) + mcc_problem(context, 1, "schema callback rejected data"); + mcc_validation.active = NULL; + mcc_validation_callbacks = NULL; + } + } else if (stage == 1) { + int unsign = (field->flags & FLAG(_tag_schema_unsigned_bit)) != 0; + int optional = (field->flags & FLAG(_tag_schema_none_bit)) != 0; + if (field->type == _tag_schema_reference || field->type == _tag_schema_tag_index) { + struct tag_reference *reference = (struct tag_reference *)member; + long *handle = field->type == _tag_schema_reference ? &reference->index : (long *)member; + struct mcc_validation_instance *instance = mcc_instance(*handle); + if (*handle != NONE && (!instance || !mcc_is_group(instance, field->definition))) { + *handle = NONE; mcc_problem(context, 0, "invalid tag reference removed"); + } + if (field->type == _tag_schema_reference) { + if (*handle != NONE && instance) { + reference->group_tag = instance->group; + reference->name = instance->name; + reference->name_length = (long)strlen(instance->name); + } else { + reference->name = mcc_validation_empty_name; + reference->name_length = 0; + } + } + } else if (field->type == _tag_schema_enum || field->type == _tag_schema_block_index) { + long value = mcc_integer(member, field->size, unsign); + long absent = unsign && field->size == 1 ? 255 : unsign && field->size == 2 ? 65535 : NONE; + long maximum = field->maximum; + if (field->type == _tag_schema_enum && schema->name && + (!strcmp(schema->name, "unit") || !strcmp(schema->name, "equipment")) && + !strcmp(field->name, "grenade_type")) maximum = 4; + if (field->type == _tag_schema_block_index) { + byte *target = NULL; + if (field->target_level == TAG_SCHEMA_ROOT) target = context->stack[0].address; + else if (field->target_level == TAG_SCHEMA_STRUCTURE) target = context->structure; + else if (field->target_level >= 0 && field->target_level < context->depth) + target = context->stack[context->depth - 1 - field->target_level].address; + if (!target || field->target_offset < 0 || + !mcc_range(context->region, context->length, target + field->target_offset, sizeof(struct tag_block))) { + mcc_problem(context, 1, "invalid schema index target"); break; + } + maximum = ((struct tag_block *)(target + field->target_offset))->count; + if (!maximum) optional = 1; + } + if (!((value >= 0 && value < maximum && value != absent) || (optional && value == absent))) { + mcc_set_integer(member, field->size, optional ? NONE : 0); + mcc_problem(context, 0, "index or enumeration corrected"); + } + } else if (field->type == _tag_schema_string && field->size && !memchr(member, 0, field->size)) { + member[field->size - 1] = 0; mcc_problem(context, 0, "string terminated"); + } else if (field->type == _tag_schema_reset) { + if (field->size <= 4) mcc_set_integer(member, field->size, field->target_offset); + else memset(member, 0, field->size); + } + } + } + } + context->structure = previous_structure; +} + +static void mcc_walk(struct mcc_validation_context *context, byte *address, + struct tag_schema_definition const *schema, int checks) +{ + unsigned long alignment; + if (!schema || schema->size <= 0) { mcc_problem(context, 1, "invalid schema size"); return; } + alignment = schema->size % 4 == 0 ? 4 : schema->size % 2 == 0 ? 2 : 1; + if ((uintptr_t)address & (alignment - 1)) { mcc_problem(context, 1, "misaligned tag or block element"); return; } + if (context->depth >= MCC_VALIDATION_DEPTH) { mcc_problem(context, 1, "block nesting exceeds limit"); return; } + context->stack[context->depth].address = address; + context->stack[context->depth++].schema = schema; + if (checks) mcc_fields(context, address, schema, 2, 0); + else { + mcc_fields(context, address, schema, 0, 0); + if (!context->rejected) mcc_fields(context, address, schema, 1, 0); + } + --context->depth; +} + +int mcc_tags_validate(struct mcc_runtime *runtime) +{ + struct mcc_validation_context context; + struct mcc_validation_header *header; + long i; + int pass; + if (!runtime || !runtime->tags || ((uintptr_t)runtime->tags & 3) || + runtime->used < sizeof(*header) || runtime->used > runtime->capacity || + runtime->capacity > MCC_VALIDATION_CAPACITY) return 0; + mcc_validation_dispose(mcc_validation.runtime); + mcc_validation.runtime = runtime; + header = mcc_validation.header = (struct mcc_validation_header *)runtime->tags; + mcc_validation.claim_bytes = (runtime->capacity + 7) / 8; + mcc_validation.claims = malloc(mcc_validation.claim_bytes); + if (!mcc_validation.claims) return 0; + memset(mcc_validation.claims, 0, mcc_validation.claim_bytes); + memset(&context, 0, sizeof(context)); context.region = runtime->tags; context.length = runtime->used; + context.handle = NONE; + if (header->signature != 'tags' || header->count <= 0 || header->count > 65535 || + ((uintptr_t)header->instances & 3) || + !mcc_range(runtime->tags, runtime->used, header->instances, (unsigned long)header->count * sizeof(*header->instances)) || + !mcc_claim(header, sizeof(*header)) || !mcc_claim(header->instances, header->count * sizeof(*header->instances))) { + mcc_problem(&context, 1, "invalid tag header or instance table"); return 0; + } + for (i = 0; i < header->count && !context.rejected; ++i) { + struct mcc_validation_instance *instance = &header->instances[i]; + struct tag_schema_group const *group = mcc_group(instance->group); + context.handle = instance->handle; + if (((unsigned long)instance->handle & 65535UL) != (unsigned long)i || !group) { + mcc_problem(&context, 1, "invalid tag handle or unsupported tag group"); break; + } + instance->parents[0] = group->parent_group_tags[0]; instance->parents[1] = group->parent_group_tags[1]; + if (!mcc_name_valid(&context, instance->name)) instance->name = mcc_validation_empty_name; + if (instance->group == 'sbsp') { instance->root = NULL; continue; } + if (!group->definition) { + if (!mcc_range(context.region, context.length, instance->root, 1)) mcc_problem(&context, 1, "tag root outside cache"); + continue; + } + if (!mcc_range(context.region, context.length, instance->root, group->definition->size) || + !mcc_claim(instance->root, group->definition->size)) mcc_problem(&context, 1, "tag root outside cache or overlaps allocation"); + } + for (pass = 0; pass < 2 && !context.rejected; ++pass) + for (i = 0; i < header->count && !context.rejected; ++i) { + struct mcc_validation_instance *instance = &header->instances[i]; + struct tag_schema_group const *group = mcc_group(instance->group); + context.handle = instance->handle; + if (instance->root && group && group->definition) mcc_walk(&context, instance->root, group->definition, pass); + } + if (!mcc_instance(header->scenario) || mcc_instance(header->scenario)->group != 'scnr') + mcc_problem(&context, 1, "scenario handle does not identify a scenario"); + return !context.rejected; +} + +int mcc_bsp_validate(struct mcc_runtime *runtime, long handle, void *address, long size) +{ + struct mcc_validation_context context; + struct mcc_validation_bsp_header *header = address; + struct mcc_validation_instance *instance; + struct tag_schema_group const *schema = mcc_group('sbsp'); + unsigned long first, bit; + if (mcc_validation.runtime != runtime || !mcc_validation.claims || ((uintptr_t)address & 3) || + size < (long)sizeof(*header) || + !mcc_range(runtime->tags, runtime->capacity, address, (unsigned long)size)) return 0; + first = (unsigned long)((uintptr_t)address - (uintptr_t)runtime->tags); + if (first < runtime->used) return 0; + instance = mcc_instance(handle); + if (!instance || instance->group != 'sbsp' || !schema || !schema->definition) return 0; + for (bit = runtime->used; bit < runtime->capacity; ++bit) mcc_validation.claims[bit >> 3] &= (byte)~(1U << (bit & 7)); + memset(&context, 0, sizeof(context)); context.region = address; context.length = size; context.handle = handle; + if (header->signature != 'sbsp' || !mcc_claim(header, sizeof(*header)) || + !mcc_range(address, size, header->root, schema->definition->size) || !mcc_claim(header->root, schema->definition->size)) { + mcc_problem(&context, 1, "invalid BSP header or root"); return 0; + } + mcc_walk(&context, header->root, schema->definition, 0); + if (!context.rejected) mcc_walk(&context, header->root, schema->definition, 1); + return !context.rejected; +} + +void mcc_validation_dispose(struct mcc_runtime *runtime) +{ + if (runtime != mcc_validation.runtime) return; + if (mcc_validation.claims) free(mcc_validation.claims); + memset(&mcc_validation, 0, sizeof(mcc_validation)); +} +boolean mcc_validation_owns(struct tag_validation *validation) +{ + return validation && validation == (struct tag_validation *)mcc_validation.active; +} +boolean mcc_validation_callback_active(void) { return mcc_validation.active != NULL; } +void mcc_validation_message(struct tag_validation *validation, boolean refuse, char const *format, va_list arguments) +{ + char text[384]; vsnprintf(text, sizeof(text), format, arguments); + mcc_problem((struct mcc_validation_context *)validation, refuse, text); +} +boolean mcc_validation_file_contains(struct tag_validation *validation, long offset, long size) +{ + struct mcc_runtime *runtime = mcc_validation.runtime; + (void)validation; + if (offset < 0 || size < 0 || !runtime) return FALSE; + return ((unsigned long)offset <= runtime->source.size && + (unsigned long)size <= runtime->source.size - (unsigned long)offset) || + mcc_audio_contains(runtime, (uint32_t)offset, (uint32_t)size) || + mcc_bitmaps_contains(runtime, (uint32_t)offset, (uint32_t)size); +} +void *mcc_validation_root(struct tag_validation *validation) +{ + struct mcc_validation_context *context = (struct mcc_validation_context *)validation; + return context->depth ? context->stack[0].address : NULL; +} +boolean mcc_validation_contains(struct tag_validation *validation, void const *address, unsigned long size) +{ + struct mcc_validation_context *context = (struct mcc_validation_context *)validation; + return mcc_range(context->region, context->length, address, size) || + (context->geometry_access && mcc_geometry_contains(mcc_validation.runtime, address, size)); +} +void *mcc_validation_tag_get(struct tag_validation *validation, long handle, unsigned long group) +{ + struct mcc_validation_instance *instance = mcc_instance(handle); + unsigned long groups[2]; (void)validation; groups[0] = group; groups[1] = 0; + return instance && mcc_is_group(instance, groups) ? instance->root : NULL; +} +void *mcc_validation_buffer_data(struct tag_validation *validation, void const *buffer, boolean indices) +{ + struct mcc_validation_context *context = (struct mcc_validation_context *)validation; + unsigned long bytes; + /* The BSP header's second table contains lightmap vertex buffers; only + * the model tag header's second table contains index buffers. */ + int index_buffer = context->region == mcc_validation.runtime->tags && indices; + void *data = mcc_geometry_buffer_data(mcc_validation.runtime, buffer, index_buffer, &bytes); + if (data) context->geometry_access = TRUE; + return data; +} +boolean mcc_validation_any_claimed(void const *address, unsigned long size) +{ + unsigned long first, bit; + if (!mcc_validation.runtime) return TRUE; + if (mcc_geometry_contains(mcc_validation.runtime, address, size)) return FALSE; + if (!mcc_range(mcc_validation.runtime->tags, mcc_validation.runtime->capacity, address, size)) return TRUE; + first = (unsigned long)((uintptr_t)address - (uintptr_t)mcc_validation.runtime->tags); + for (bit = first; bit < first + size; ++bit) + if (mcc_validation.claims[bit >> 3] & (1U << (bit & 7))) return TRUE; + return FALSE; +} diff --git a/port/linux/game/mcc_tag_validate.h b/port/linux/game/mcc_tag_validate.h new file mode 100644 index 0000000000..f8c1dec6f3 --- /dev/null +++ b/port/linux/game/mcc_tag_validate.h @@ -0,0 +1,41 @@ +/* Independent MCC validation context and dispatch for shared schema callbacks. */ +#ifndef MCC_TAG_VALIDATE_H +#define MCC_TAG_VALIDATE_H + +#include "cseries.h" +#include + +struct mcc_runtime; +struct tag_validation; + +int mcc_tags_validate(struct mcc_runtime *runtime); +int mcc_bsp_validate(struct mcc_runtime *runtime, long handle, void *address, long size); +void mcc_validation_dispose(struct mcc_runtime *runtime); + +/* Identity comparisons only: a legacy validator is never read as an MCC one. */ +boolean mcc_validation_owns(struct tag_validation *validation); +boolean mcc_validation_callback_active(void); +void mcc_validation_message(struct tag_validation *validation, boolean refuse, + char const *format, va_list arguments); +boolean mcc_validation_file_contains(struct tag_validation *validation, long offset, long size); +void *mcc_validation_root(struct tag_validation *validation); +boolean mcc_validation_contains(struct tag_validation *validation, void const *address, unsigned long size); +void *mcc_validation_tag_get(struct tag_validation *validation, long handle, unsigned long group); +void *mcc_validation_buffer_data(struct tag_validation *validation, void const *buffer, boolean indices); +boolean mcc_validation_any_claimed(void const *address, unsigned long size); + +/* Optional MCC callback routing. The stock standalone validator has no MCC + * runtime dependency: this pointer is NULL except inside an MCC callback. */ +struct mcc_validation_dispatch { + boolean (*owns)(struct tag_validation *); + void (*message)(struct tag_validation *, boolean, char const *, va_list); + boolean (*file_contains)(struct tag_validation *, long, long); + void *(*root)(struct tag_validation *); + boolean (*contains)(struct tag_validation *, void const *, unsigned long); + void *(*tag_get)(struct tag_validation *, long, unsigned long); + void *(*buffer_data)(struct tag_validation *, void const *, boolean); + boolean (*any_claimed)(void const *, unsigned long); +}; +extern struct mcc_validation_dispatch const *mcc_validation_callbacks; + +#endif diff --git a/port/linux/game/mcc_tags.c b/port/linux/game/mcc_tags.c new file mode 100644 index 0000000000..199ee8e071 --- /dev/null +++ b/port/linux/game/mcc_tags.c @@ -0,0 +1,139 @@ +/* MCC metadata normalization. All writes are to an MCC-owned, checked slice; + * the original map and both established format implementations are untouched. */ +#include "cseries.h" +#include "errors.h" +#include "mcc_runtime.h" +#include + +static uint32_t mcc_word(void const *p) +{ + uint32_t value; + memcpy(&value, p, 4); + return value; +} + +static void *mcc_block(struct mcc_runtime *r, unsigned char *p, uint32_t stride, uint32_t *count) +{ + *count = mcc_word(p); + if (*count > r->used / stride) return NULL; + return *count ? mcc_runtime_pointer(r, mcc_word(p + 4), *count * stride) : NULL; +} + +static void mcc_hud_placement(unsigned char *p) +{ + uint16_t flags; + float x, y; + memcpy(&flags, p + 12, 2); + if (!(flags & 4)) return; + memcpy(&x, p + 4, 4); + memcpy(&y, p + 8, 4); + x *= 0.5f; + y *= 0.5f; + memcpy(p + 4, &x, 4); + memcpy(p + 8, &y, 4); + flags &= ~4u; + memcpy(p + 12, &flags, 2); +} + +static int mcc_hud(struct mcc_runtime *r, uint32_t group, unsigned char *p) +{ + unsigned char *block; + uint32_t count, i; + if (group == 'unhi') { + static unsigned const placements[] = {0x24, 0x8C, 0xF4, 0x17C, 0x1E4, 0x26C, 0x2D4}; + for (i = 0; i < NUMBEROF(placements); ++i) mcc_hud_placement(p + placements[i]); + memset(p + 0xF4 + 0x58, 0, 12); + memset(p + 0x1E4 + 0x58, 0, 12); + block = mcc_block(r, p + 0x3A4, 0x84, &count); + if (count && !block) return FALSE; + for (i = 0; i < count; ++i) mcc_hud_placement(block + i * 0x84); + block = mcc_block(r, p + 0x3CC, 0x144, &count); + if (count && !block) return FALSE; + for (i = 0; i < count; ++i) { + mcc_hud_placement(block + i * 0x144 + 0x14); + mcc_hud_placement(block + i * 0x144 + 0x7C); + memset(block + i * 0x144 + 0x7C + 0x58, 0, 12); + } + } else if (group == 'wphi') { + unsigned k; + for (k = 0; k < 2; ++k) { + block = mcc_block(r, p + 0x60 + 12 * k, 0xB4, &count); + if (count && !block) return FALSE; + for (i = 0; i < count; ++i) { + mcc_hud_placement(block + i * 0xB4 + 0x24); + if (k) memset(block + i * 0xB4 + 0x24 + 0x58, 0, 12); + } + } + } else if (group == 'grhi') { + mcc_hud_placement(p + 0x24); + mcc_hud_placement(p + 0x8C); + } else if (group == 'hudg') { + mcc_hud_placement(p + 0x24); + } + return TRUE; +} + +int mcc_tags_prepare(struct mcc_runtime *r) +{ + uint32_t i; + for (i = 0; i < r->report.tag_count; ++i) { + unsigned char *entry = r->tag_index + i * 32; + uint32_t group = mcc_word(entry), address = mcc_word(entry + 20), size = 0; + unsigned char *p; + short shader = -1; + switch (group) { + case 'senv': shader = 3; break; + case 'soso': shader = 4; break; + case 'sotr': shader = 5; break; + case 'schi': case 'scex': shader = 6; break; + case 'swat': shader = 7; break; + case 'sgla': shader = 8; break; + case 'smet': shader = 9; break; + case 'spla': shader = 10; break; + case 'unhi': size = 0x56C; break; + case 'wphi': size = 0x17C; break; + case 'grhi': size = 0x1F8; break; + case 'hudg': size = 0x450; break; + case 'weap': size = 0x508; break; + case 'DeLa': size = 0x60; break; + default: break; + } + if (shader >= 0) size = group == 'scex' ? 0x78 : 0x28; + if (!size) continue; + p = mcc_runtime_pointer(r, address, size); + if (!p) { + error(_error_silent, "mcc: metadata for tag %lu is outside the tag data", (unsigned long)i); + return FALSE; + } + if (shader >= 0) memcpy(p + 0x24, &shader, 2); + if (group == 'scex') { + uint32_t flags = mcc_word(p + 0x6C), new_group = 'schi'; + if (!mcc_word(p + 0x54)) memcpy(p + 0x54, p + 0x60, 12); + memset(p + 0x60, 0, 12); + memcpy(p + 0x60, &flags, 4); + memcpy(entry, &new_group, 4); + } + if (group == 'weap') { + unsigned j; + for (j = 0; j < 4; ++j) { + uint16_t value; + memcpy(&value, p + 0x330 + j * 2, 2); + if (value == 17 || value == 18) { + value -= 2; + memcpy(p + 0x330 + j * 2, &value, 2); + } + } + } + if (group == 'DeLa') { + uint32_t count; + unsigned char *events = mcc_block(r, p + 0x54, 0x48, &count); + if (count && !events) return FALSE; + /* The first 102 callback IDs retain their Xbox meanings. MCC's + * additional callbacks are interpreted by mcc_ui.c only for an + * MCC-owned widget. Preserve the handlers, including mouse input, + * confirmation actions, scripts and their navigation flags. */ + } + if (!mcc_hud(r, group, p)) return FALSE; + } + return TRUE; +} diff --git a/port/linux/game/mcc_texture_cache.c b/port/linux/game/mcc_texture_cache.c new file mode 100644 index 0000000000..d5b11fe648 --- /dev/null +++ b/port/linux/game/mcc_texture_cache.c @@ -0,0 +1,64 @@ +/* MCC-only hardware descriptors over immutable converted pixel allocations. + * Neither the Xbox/CE bitmap descriptors nor their LRU state are rewritten. */ +#include "cseries.h" +#include "bitmaps/bitmaps.h" +#include "bitmaps/bitmap_group.h" +#include "rasterizer/rasterizer_swizzle.h" +#include "mcc_cache.h" +#include "mcc_texture_cache.h" +#include "../src/mcc_texture_bridge.h" +#include + +void *mcc_texture_cache_get(struct bitmap_data const *bitmap,boolean load) +{ + static long const formats[2][18]={ + {D3DFMT_A8,D3DFMT_L8,D3DFMT_AL8,D3DFMT_A8L8,-1,-1,D3DFMT_R5G6B5,-1, + D3DFMT_A1R5G5B5,D3DFMT_A4R4G4B4,D3DFMT_X8R8G8B8,D3DFMT_A8R8G8B8, + -1,-1,D3DFMT_DXT1,D3DFMT_DXT3,D3DFMT_DXT5,D3DFMT_P8}, + {D3DFMT_LIN_A8,D3DFMT_LIN_L8,D3DFMT_LIN_AL8,D3DFMT_LIN_A8L8,-1,-1, + D3DFMT_LIN_R5G6B5,-1,D3DFMT_LIN_A1R5G5B5,D3DFMT_LIN_A4R4G4B4, + D3DFMT_LIN_X8R8G8B8,D3DFMT_LIN_A8R8G8B8,-1,-1,-1,-1,-1,-1}}; + unsigned long format,size=0,bytes,pitch; + boolean linear; + void const *pixels; + void *registered; + if (!bitmap || !mcc_cache_tags_loaded()) return NULL; + registered=mcc_texture_bridge_find(bitmap); + if (registered) { + /* CPU object lighting samples the same packed pixels after asking + * the hardware cache whether the lightmap/diffuse map is ready. */ + if (!bitmap->base_address) { + pixels=mcc_cache_bitmap_pixels(bitmap,&bytes); + if (!pixels) return NULL; + ((struct bitmap_data *)bitmap)->base_address=(void *)pixels; + } + return registered; + } + if (!load) return NULL; + pixels=mcc_cache_bitmap_pixels(bitmap,&bytes); + if (!pixels || bitmap->format<0 || bitmap->format>=18) return NULL; + linear=(bitmap->flags&(1<<4))!=0; + if (formats[linear][bitmap->format]<0) return NULL; + format=((unsigned long)formats[linear][bitmap->format]<(D3DSIZE_PITCH_MASK>>D3DSIZE_PITCH_SHIFT)+1) return NULL; + format|=(1<height-1)<width-1); + } else { + format|=(floor_log2(bitmap->depth)<height)<width)<type==1 ? 3 : 2)<type==2 ? D3DFORMAT_CUBEMAP : 0); + } + registered=mcc_texture_bridge_register(bitmap,format,size,pixels,bytes); + if (registered) ((struct bitmap_data *)bitmap)->base_address=(void *)pixels; + return registered; +} + +void mcc_texture_cache_dispose(void) {mcc_texture_bridge_dispose();} diff --git a/port/linux/game/mcc_texture_cache.h b/port/linux/game/mcc_texture_cache.h new file mode 100644 index 0000000000..b8982c0fd9 --- /dev/null +++ b/port/linux/game/mcc_texture_cache.h @@ -0,0 +1,6 @@ +#ifndef MCC_TEXTURE_CACHE_H +#define MCC_TEXTURE_CACHE_H +struct bitmap_data; +void *mcc_texture_cache_get(struct bitmap_data const *bitmap, boolean load); +void mcc_texture_cache_dispose(void); +#endif diff --git a/port/linux/game/mcc_ui.c b/port/linux/game/mcc_ui.c new file mode 100644 index 0000000000..d0b54fe578 --- /dev/null +++ b/port/linux/game/mcc_ui.c @@ -0,0 +1,349 @@ +/* Runtime actions for MCC's own in-map widgets. Callback numbers are wire + * values from the public UIEventHandlerReferenceFunction definition: + * https://github.com/SnowyMouse/invader/blob/master/src/tag/hek/definition/ui_widget_definition.json + * No Xbox/CE tag, callback table or settings implementation is rewritten. */ +#include "cseries.h" +#include "game/game.h" +#include "game/game_engine.h" +#include "game/players.h" +#include "interface/event_manager.h" +#include "interface/player_ui.h" +#include "interface/ui_widget.h" +#include "main/main.h" +#include "networking/network_game_globals.h" +#include "networking/network_game_manager.h" +#include "networking/network_client_manager.h" +#include "saved games/game_state.h" +#include "saved games/player_profile.h" +#include "scenario/scenario.h" +#include "scenario/scenario_definitions.h" +#include "tag_files/tag_groups.h" +#include "tag_files/tag_files.h" +#include "mcc_cache.h" +#include "mcc_maps.h" +#include "mcc_pause.h" +#include "mcc_ui.h" +#include "mcc_ui_network.h" +#include "mcc_ui_teams.h" +#include "network_coop.h" +#include + +/* Only the public prefix shared by every widget is needed here. */ +struct mcc_ui_widget_prefix { + long definition; + char const *name; + short controller; +}; +typedef char mcc_ui_controller_offset[offsetof(struct mcc_ui_widget_prefix, controller) == 8 ? 1 : -1]; + +char const *config_string(char const *name); +boolean pc_menu_tag(long tag_index); +boolean ui_widget_port_dispatch_event(struct widget_instance *widget, short type, short controller, boolean *deleted); + +static short mcc_settings_controller = NONE; + +void mcc_ui_settings_profile_released(void) +{ + mcc_settings_controller = NONE; +} + +void mcc_ui_settings_close(short controller) +{ + if (mcc_settings_controller != NONE && + (controller == NONE || controller == mcc_settings_controller)) { + mcc_settings_controller = NONE; + player_ui_end_editing_profile(); + } +} + +static boolean mcc_ui_settings_begin(short controller) +{ + long index; + boolean fallback = FALSE; + struct player_profile *profile; + if (controller < 0 || controller >= 4 || local_player_count() != 1 || + player_ui_get_edit_player_profile() || player_ui_get_edit_playlist_profile()) return FALSE; + mcc_settings_controller = NONE; + index = player_ui_get_active_player_profile_index(controller); + if (index != NONE) player_ui_begin_editing_profile(index); + /* Retain the native player-1 convenience for console/quickstart games + * without assigning that profile to a different local controller. */ + if (!player_ui_get_edit_player_profile() && controller == 0) { + word count = 1; + fallback = TRUE; + index = player_ui_get_player1_last_used_profile_index(); + if (index != NONE) player_ui_begin_editing_profile(index); + if (!player_ui_get_edit_player_profile()) { + index = NONE; + player_profiles_enumerate_available_to_local_player_index(controller, &count, &index, FALSE); + if (count && index != NONE) player_ui_begin_editing_profile(index); + } + } + profile = player_ui_get_edit_player_profile(); + if (!profile) { + player_ui_end_editing_profile(); + return FALSE; + } + if (fallback) player_ui_set_active_player_profile(controller, index, profile); + mcc_settings_controller = controller; + return TRUE; +} + +static boolean mcc_ui_owns_widget(struct widget_instance *widget) +{ + struct mcc_ui_widget_prefix const *prefix = (void const *)widget; + return widget && mcc_cache_tags_loaded() && prefix->definition != NONE && + (mcc_pause_owns(prefix->definition) || + mcc_cache_contains(tag_get('DeLa', prefix->definition), 0x60)); +} + +boolean mcc_ui_trusted_action(struct widget_instance *widget, word function) +{ + struct mcc_ui_widget_prefix const *prefix = (void const *)widget; + return widget && mcc_cache_tags_loaded() && + function >= MCC_PAUSE_ACTION_RESUME && function <= MCC_PAUSE_ACTION_SETTINGS && + mcc_pause_owns(prefix->definition); +} + +boolean mcc_ui_settings_needed(char const *map_name) +{ + return map_name && mcc_level_name(map_name) && mcc_cache_tags_loaded() && + !mcc_maps_level_campaign(map_name) && + !csstrcasecmp(config_string("display.menus"), "pc"); +} + +static boolean mcc_ui_scenario_type(short type) +{ + /* main_get_map_name is the last solo selection; network rounds load + * their own map without replacing it. Classify live actions using the + * currently loaded MCC scenario, independently of the menu catalog. */ + return mcc_cache_tags_loaded() && global_scenario && global_scenario->type == type; +} + +static short mcc_ui_controller(struct widget_instance *widget, struct event_record *event) +{ + short controller = event ? event->controller_index : + ((struct mcc_ui_widget_prefix const *)widget)->controller; + return controller >= 0 && controller < 4 ? controller : 0; +} + +static boolean mcc_ui_failure(short controller, wchar_t const *text) +{ + display_error_text_deferred(text, controller); + return FALSE; +} + +static boolean mcc_ui_host_required(short controller) +{ + if (game_connection() == _game_connection_network_client) + return mcc_ui_failure(controller, L"Only the host can restart or revert this game."); + return TRUE; +} + +static boolean mcc_ui_restart(short controller) +{ + if (!mcc_ui_host_required(controller)) return FALSE; + if (global_network_game_server_get()) { + if (!mcc_ui_restart_network_game()) + return mcc_ui_failure(controller, L"The game could not be restarted."); + } else main_reset_map(); + return TRUE; +} + +static boolean mcc_ui_end_round(short controller, boolean *deleted) +{ + /* End the round normally: connected players reach the carnage report + * and the host can pick the next map in the native lobby. */ + if (!global_network_game_server_get()) + return mcc_ui_failure(controller, L"Only the host can choose a new game."); + if (!game_engine_running() || !game_engine_can_score()) + return mcc_ui_failure(controller, L"This round is already ending."); + /* game_engine_end_game closes the complete widget tree immediately. + * The dispatcher must never inspect this widget again afterward. */ + *deleted = TRUE; + game_engine_end_game(); + return TRUE; +} + +boolean mcc_ui_new_game(struct widget_instance *widget, boolean *deleted) +{ + struct mcc_ui_widget_prefix const *prefix = (void const *)widget; + char const *name; + if (!mcc_ui_owns_widget(widget)) return FALSE; + name = tag_get_name(prefix->definition); + if (!name || strcmp(name, "ui\\shell\\multiplayer_game\\pause_game\\new_game_button")) return FALSE; + mcc_ui_end_round(mcc_ui_controller(widget, NULL), deleted); + return TRUE; +} + +static boolean mcc_ui_request_team(short team, short controller) +{ + struct network_game *game = network_game_get_game(); + short machine = network_game_client_get_local_machine_index(); + long i; + if (!game || !game->variant.universal_variant.teams) + return mcc_ui_failure(controller, L"This game does not use teams."); + if (!mcc_ui_team_balance_allows(machine, controller, team)) + return mcc_ui_failure(controller, L"The host's automatic team balance prevents this team change."); + for (i = 0; i < (long)NUMBEROF(game->players); i++) { + struct network_player player = game->players[i]; + if (network_player_is_valid(&player) && player.machine_index == machine && + player.controller_index == controller) { + player.team_index = (char)team; + if (network_game_client_update_local_player_data(global_network_game_client_get(), &player)) + return TRUE; + break; + } + } + return mcc_ui_failure(controller, L"The team change could not be sent to the host."); +} + +static boolean mcc_ui_choose_team(struct widget_instance *widget, short controller) +{ + struct mcc_ui_widget_prefix const *prefix = (void const *)widget; + char const *name = tag_get_name(prefix->definition), *leaf; + leaf = name ? strrchr(name, '\\') : NULL; + leaf = leaf ? leaf + 1 : name; + if (leaf && !strcmp(leaf, "red_team_button")) return mcc_ui_request_team(0, controller); + if (leaf && !strcmp(leaf, "blue_team_button")) return mcc_ui_request_team(1, controller); + return mcc_ui_failure(controller, L"This team choice is not supported."); +} + +static void mcc_ui_close_for_controller(short controller, boolean *deleted) +{ + /* A generated confirmation has its pause screen in this controller's + * history. Close both, preserving every other split-screen player's UI. + * Publish deletion before the native helper can free the caller. */ + *deleted = TRUE; + ui_widgets_close_all_for_local_player(controller); +} + +boolean mcc_ui_event_function(struct widget_instance *widget, + struct event_record *event, word function, boolean *deleted, boolean *result) +{ + short controller; + if (!mcc_ui_owns_widget(widget)) return FALSE; + controller = mcc_ui_controller(widget, event); + *result = TRUE; + if (function >= MCC_PAUSE_ACTION_RESUME && function <= MCC_PAUSE_ACTION_SETTINGS) { + if (!mcc_ui_trusted_action(widget, function)) { + *result = FALSE; + return TRUE; + } + switch (function) { + case MCC_PAUSE_ACTION_RESUME: + mcc_ui_close_for_controller(controller, deleted); + return TRUE; + case MCC_PAUSE_ACTION_REVERT: function = 11; break; + case MCC_PAUSE_ACTION_RESTART: function = 12; break; + case MCC_PAUSE_ACTION_SAVE: function = 179; break; + case MCC_PAUSE_ACTION_QUIT: + if (network_coop_active() || mcc_ui_scenario_type(0)) function = 13; + else { + /* Cache all caller state and mark deletion before native + * leave helpers, which may synchronously close widgets. */ + boolean split_screen = local_player_count() > 1; + *deleted = TRUE; + if (global_network_game_client_get()) { + network_game_client_local_player_quit(controller); + if (split_screen) player_ui_local_player_left_multiplayer_game(controller); + } else main_goto_main_menu(); + mcc_ui_close_for_controller(controller, deleted); + return TRUE; + } + break; + case MCC_PAUSE_ACTION_END_GAME: + *result = mcc_ui_end_round(controller, deleted); + return TRUE; + case MCC_PAUSE_ACTION_RED_TEAM: + case MCC_PAUSE_ACTION_BLUE_TEAM: + *result = mcc_ui_request_team(function == MCC_PAUSE_ACTION_RED_TEAM ? 0 : 1, controller); + if (*result) mcc_ui_close_for_controller(controller, deleted); + return TRUE; + case MCC_PAUSE_ACTION_SETTINGS: function = 137; break; + } + } + switch (function) { + case 11: /* Confirm revert to checkpoint. */ + if (!mcc_ui_host_required(controller)) { *result = FALSE; break; } + if (!game_state_port_saved_game_valid()) { + *result = mcc_ui_failure(controller, L"There is no saved checkpoint to revert to."); + break; + } + main_revert_map(); /* Existing engine host-revert path synchronizes co-op. */ + break; + case 12: /* Confirm restart level. */ + case 163: /* PC multiplayer options: restart game. */ + *result = mcc_ui_restart(controller); + break; + case 13: /* Campaign quit: retain this MCC campaign's checkpoint. */ + if (network_coop_active()) { + short i; + for (i = 0; i < 4; i++) network_game_client_local_player_quit(i); + } else { + if (mcc_ui_scenario_type(0) && game_state_port_saved_game_valid()) + game_state_save_to_persistent_storage(); + main_goto_main_menu(); + } + break; + case 108: /* Mouse accept: use the same A/confirmation route as a controller. */ + case 109: + case 110: + case 111: + case 128: + event_manager_post_button(controller, + function == 108 ? 0 : function == 109 ? 1 : function == 110 ? 10 : function == 111 ? 11 : 2); + break; + case 137: /* Open trusted native settings, never the map's PC configuration widgets. */ + if (!mcc_ui_scenario_type(1) || csstrcasecmp(config_string("display.menus"), "pc") || + local_player_count() != 1 || + !pc_menu_tag(tag_loaded('DeLa', "pc\\main_menu\\settings_select\\player_setup\\player_profile_edit\\player_profile_edit_screen")) || + !mcc_ui_settings_begin(controller)) { + *result = mcc_ui_failure(controller, L"Settings could not be opened for this player."); + break; + } + { + struct mcc_ui_widget_prefix const *root = (void const *)widget_instance_get_topmost_parent(widget); + long root_tag = root->definition; + /* The native port-open helper always uses stack zero. Capture + * the caller's history first and open on its actual controller; + * a successful load synchronously frees the old widget tree. */ + *result = ui_widget_load_by_name_or_tag( + "pc\\main_menu\\settings_select\\player_setup\\player_profile_edit\\player_profile_edit_screen", + NONE, NULL, controller, root_tag, NONE, NONE) != NULL; + if (*result) *deleted = TRUE; + else mcc_ui_settings_close(controller); + } + break; + case 155: /* PC pause/options creation has no engine state to initialize. */ + case 156: + break; + case 157: + *result = mcc_ui_choose_team(widget, controller); + break; + case 164: + *result = ui_widget_port_dispatch_event(widget_instance_get_topmost_parent(widget), 32, controller, deleted); + break; + case 179: /* Explicit save checkpoint from the campaign pause screen. */ + if (game_connection() == _game_connection_network_client) { + *result = mcc_ui_failure(controller, L"Only the host can save a checkpoint."); + break; + } + game_state_save(); + if (!game_state_port_saved_game_valid()) { + *result = mcc_ui_failure(controller, L"The checkpoint could not be saved."); + break; + } + if (!network_coop_active() && mcc_ui_scenario_type(0)) + game_state_save_to_persistent_storage(); + break; + default: + /* Native-compatible IDs still pass through the original dispatcher + * and its map-widget permissions. Unknown PC callbacks must fail so + * their open/close flags cannot claim an operation succeeded. */ + if (function < 102) return FALSE; + *result = mcc_ui_failure(controller, L"This map's menu action is not supported by the native game."); + break; + } + return TRUE; +} diff --git a/port/linux/game/mcc_ui.h b/port/linux/game/mcc_ui.h new file mode 100644 index 0000000000..9494730ecf --- /dev/null +++ b/port/linux/game/mcc_ui.h @@ -0,0 +1,36 @@ +#ifndef MCC_UI_H +#define MCC_UI_H + +struct widget_instance; +struct event_record; + +/* Positive signed-short callback IDs reserved for generated MCC pause tags. */ +enum { + MCC_PAUSE_ACTION_RESUME = 0x7000, + MCC_PAUSE_ACTION_REVERT, + MCC_PAUSE_ACTION_RESTART, + MCC_PAUSE_ACTION_SAVE, + MCC_PAUSE_ACTION_QUIT, + MCC_PAUSE_ACTION_END_GAME, + MCC_PAUSE_ACTION_RED_TEAM, + MCC_PAUSE_ACTION_BLUE_TEAM, + MCC_PAUSE_ACTION_SETTINGS +}; + +boolean mcc_ui_trusted_action(struct widget_instance *widget, word function); + +/* TRUE means this MCC-owned callback supplied result. FALSE leaves the + * established callback dispatcher entirely responsible for the widget. */ +boolean mcc_ui_event_function(struct widget_instance *widget, + struct event_record *event, word function, boolean *deleted, boolean *result); +boolean mcc_ui_settings_needed(char const *map_name); +/* Native profile-clear and widget-close lifecycle notifications. These + * affect only the profile editor opened by the MCC action adapter. */ +void mcc_ui_settings_profile_released(void); +void mcc_ui_settings_close(short controller); +/* Handles only the MCC-owned multiplayer New Game button. The caller must + * finish dispatch immediately when handled, including a host-only denial. + * A successful transition synchronously deletes the active widget tree. */ +boolean mcc_ui_new_game(struct widget_instance *widget, boolean *deleted); + +#endif diff --git a/port/linux/game/mcc_ui_network.h b/port/linux/game/mcc_ui_network.h new file mode 100644 index 0000000000..2c1656fb2f --- /dev/null +++ b/port/linux/game/mcc_ui_network.h @@ -0,0 +1,10 @@ +#ifndef MCC_UI_NETWORK_H +#define MCC_UI_NETWORK_H + +struct network_game_server; +boolean mcc_ui_restart_network_game(void); +boolean mcc_ui_network_restart_settings(struct network_game_server *server); +boolean mcc_ui_network_restart_pregame(void); +void mcc_ui_network_server_dispose(struct network_game_server *server); + +#endif diff --git a/port/linux/game/mcc_ui_network.inl b/port/linux/game/mcc_ui_network.inl new file mode 100644 index 0000000000..aee073c231 --- /dev/null +++ b/port/linux/game/mcc_ui_network.inl @@ -0,0 +1,96 @@ +/* MCC pause-menu restarts use the existing reliable round transition, with + * an MCC-owned snapshot instead of advancing the user's playlist. Included + * by the server implementation to keep its private representation private. */ +#include "mcc_cache.h" + +static struct network_game_server *mcc_restart_server; +static struct network_game mcc_restart_game; +static struct network_game_server *mcc_restart_ui_server; +static long mcc_restart_ui_round; + +void mcc_ui_network_server_dispose(struct network_game_server *server) +{ + if (server == mcc_restart_server) mcc_restart_server = NULL; + if (server == mcc_restart_ui_server) mcc_restart_ui_server = NULL; +} + +boolean mcc_ui_network_restart_pregame(void) +{ + struct network_game_server *server = global_network_game_server_get(); + if (!mcc_restart_ui_server) return FALSE; + if (!server || server != mcc_restart_ui_server) { + mcc_restart_ui_server = NULL; + return FALSE; + } + mcc_restart_ui_server = NULL; + if (server->state != _network_game_server_state_pregame || + server->game.number_of_games_played != mcc_restart_ui_round || + server->game.map.version != mcc_restart_game.map.version || + strcmp(server->game.map.name, mcc_restart_game.map.name)) return FALSE; + /* The host's own switch-to-pregame message arrives after reset returns. + * Ordinary postgame UI opens map selection and pauses the countdown. + * Consume this exact restart at that boundary, after the UI map loads; + * MCC tags may already be unloaded, so ownership is the pending token. */ + network_game_server_pause_countdown(server, FALSE); + network_game_server_begin_game_start_countdown(server, 3000); + return TRUE; +} + +boolean mcc_ui_network_restart_settings(struct network_game_server *server) +{ + long i; + if (!server || server != mcc_restart_server) return FALSE; + mcc_restart_server = NULL; + server->game.map = mcc_restart_game.map; + server->game.variant = mcc_restart_game.variant; + server->game.variant_options = mcc_restart_game.variant_options; + server->game.cooperative_flags = mcc_restart_game.cooperative_flags; + server->game.minimum_players = mcc_restart_game.minimum_players; + server->game.maximum_players = mcc_restart_game.maximum_players; + server->game.maximum_teams = mcc_restart_game.maximum_teams; + server->game.difficulty = mcc_restart_game.difficulty; + server->game.number_of_games_played = (long)((unsigned long)mcc_restart_game.number_of_games_played + 1); + csmemcpy(server->game.name, mcc_restart_game.name, sizeof(server->game.name)); + /* A normal next round swaps red and blue. Restart preserves the current + * choices without restoring players who may have left the session. */ + for (i = 0; i < MAXIMUM_NETWORK_PLAYER_COUNT; ++i) { + struct network_player *player = &server->game.players[i]; + struct network_player const *saved = &mcc_restart_game.players[i]; + if (network_player_is_valid(player) && + player->machine_index == saved->machine_index && + player->controller_index == saved->controller_index && + player->player_list_index == saved->player_list_index) + player->team_index = saved->team_index; + } + main_set_multiplayer_map_name(server->game.map.name); + network_game_server_open_game(server); + return TRUE; +} + +boolean mcc_ui_restart_network_game(void) +{ + struct network_game_server *server = global_network_game_server_get(); + boolean success; + if (!server) return FALSE; + if (mcc_restart_server) { + /* A failed reliable transition can leave the session in postgame. + * Retry that transition with its original settings, not the playlist. */ + if (server != mcc_restart_server || server->state != _network_game_server_state_postgame) + return FALSE; + } else { + if (!mcc_cache_tags_loaded() || !mcc_level_name(server->game.map.name) || + server->state != _network_game_server_state_ingame) return FALSE; + mcc_restart_game = server->game; + mcc_restart_server = server; + network_game_server_switch_to_postgame(server); + } + success = network_game_server_reset_to_pregame(server); + if (success) { + /* Start the ordinary precache/countdown path once the host's local + * client receives the reliable reset and opens the pregame UI. */ + mcc_restart_ui_server = server; + mcc_restart_ui_round = server->game.number_of_games_played; + network_event("mcc: restarting the current network map"); + } + return success; +} diff --git a/port/linux/game/mcc_ui_teams.c b/port/linux/game/mcc_ui_teams.c new file mode 100644 index 0000000000..43abc5eae1 --- /dev/null +++ b/port/linux/game/mcc_ui_teams.c @@ -0,0 +1,154 @@ +/* MCC's in-game team picker uses the existing authenticated settings message. + * Only its team byte is accepted; the ordinary pregame handler is untouched. */ +#include "cseries.h" +#include "game/game.h" +#include "game/players.h" +#include "memory/data.h" +#include "networking/network_game_manager.h" +#include "networking/network_messages.h" +#include "networking/network_server_manager_internal.h" +#include "scenario/scenario.h" +#include "scenario/scenario_definitions.h" +#include "units/units.h" +#include "mcc_cache.h" +#include "mcc_ui_teams.h" +#include + +struct mcc_team_respawn { + boolean active; + boolean no_statistics; + long player_index; + long unit_index; +}; +static struct mcc_team_respawn mcc_team_respawns[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; + +void mcc_ui_teams_reset(void) +{ + memset(mcc_team_respawns, 0, sizeof(mcc_team_respawns)); +} + +static void mcc_ui_team_respawn_track(long player_index, long unit_index) +{ + unsigned long slot = DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index); + if (slot >= NUMBEROF(mcc_team_respawns) || unit_index == NONE) return; + mcc_team_respawns[slot].active = TRUE; + mcc_team_respawns[slot].no_statistics = FALSE; + mcc_team_respawns[slot].player_index = player_index; + mcc_team_respawns[slot].unit_index = unit_index; +} + +void mcc_ui_team_respawn_damage(long player_index, long unit_index, boolean active) +{ + unsigned long slot = DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index); + struct mcc_team_respawn *pending; + if (!mcc_cache_tags_loaded() || slot >= NUMBEROF(mcc_team_respawns)) return; + pending = &mcc_team_respawns[slot]; + if (pending->active && pending->player_index == player_index && pending->unit_index == unit_index) + pending->no_statistics = active; +} + +void mcc_ui_team_respawn_death(long *killer, long *object, long dead, boolean *friendly) +{ + unsigned long slot = DATUM_INDEX_TO_ABSOLUTE_INDEX(dead); + struct mcc_team_respawn pending; + struct player_datum *player; + struct unit_datum *unit; + if (!mcc_cache_tags_loaded() || slot >= NUMBEROF(mcc_team_respawns)) return; + pending = mcc_team_respawns[slot]; + /* The next death consumes the record even if an unrelated death won the + * race, or a recycled player/unit slot has a different salt. */ + mcc_team_respawns[slot].active = FALSE; + if (!pending.active || !pending.no_statistics || pending.player_index != dead || + game_connection() != _game_connection_network_server || + !global_scenario || global_scenario->type != 1 || + !killer || !object || !friendly || *killer != dead || + *object != pending.unit_index || !*friendly) return; + player = player_try_and_get(dead); + unit = unit_try_and_get(pending.unit_index); + if (!player || player->unit_index != pending.unit_index || !unit || + !TEST_FLAG(unit->object.damage_flags, _object_die_act_of_god_no_statistics_bit)) return; + /* The native no-statistics death still invokes Slayer's score callback + * as a suicide. Neutral attribution keeps the normal death, objectives + * and respawn processing, and is replicated by the following engine hook. */ + *killer = NONE; + *object = NONE; + *friendly = FALSE; +} + +boolean mcc_ui_team_balance_allows(short machine, short controller, short team) +{ + struct data_iterator iterator; + struct player_datum *player; + long counts[2] = {0, 0}, old_team = NONE; + if (!VALID_INDEX(team, 2)) return FALSE; + if (!game_variant_options_get()->auto_team_balance) return TRUE; + data_iterator_new(&iterator, player_data); + while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL) { + if (player->quit_out_of_game || !VALID_INDEX(player->team_index, 2)) continue; + ++counts[player->team_index]; + if (player->network_player_data.machine_index == machine && + player->network_player_data.controller_index == controller) old_team = player->team_index; + } + return old_team == team || (VALID_INDEX(old_team, 2) && counts[team] < counts[1 - team]); +} + +boolean mcc_ui_team_request(struct network_game_server *server, + struct network_game_server_client_machine *machine, word *message, short size) +{ + struct network_game *game; + struct network_player request, unchanged; + struct network_player *current; + struct player_datum *player; + struct data_iterator iterator; + short packet_type = _message_client_player_settings_request; + short packet_version = HALO_PORT_NETWORK_GAME_MESSAGE_VERSION; + long machine_index, slot; + /* Server state 1 is a running round. No pregame/postgame interception. */ + if (!mcc_cache_tags_loaded() || !server || + network_game_server_get_state(server, NULL) != 1) return FALSE; + game = network_game_server_get_game(server); + if (!game || !mcc_level_name(game->map.name)) return FALSE; + if (!global_scenario || global_scenario->type != 1 || + !game->variant.universal_variant.teams || !machine || !message || size <= (short)sizeof(word)) + return TRUE; + /* Wire version byte + all 32 player bytes + packet type + word header. */ + if (size != sizeof(struct network_player) + 4 || + ((unsigned char const *)message)[size - 1] != _message_client_player_settings_request) return TRUE; + size -= sizeof(word); + if (!decode_network_game_message(&request, message + 1, &size, + &packet_type, &packet_version, 3) || + packet_type != _message_client_player_settings_request) return TRUE; + slot = request.player_list_index; + if (!network_game_server_get_client_machine(server, machine, &machine_index) || + !VALID_INDEX(machine_index, NUMBEROF(game->machines))) return TRUE; + if (!VALID_INDEX(slot, NUMBEROF(game->players)) || request.machine_index != machine_index || + !VALID_INDEX(request.controller_index, 4) || !VALID_INDEX(request.team_index, 2)) return TRUE; + current = &game->players[slot]; + if (!network_player_is_valid(current)) return TRUE; + unchanged = request; + unchanged.team_index = current->team_index; + /* Includes name, icon, color, machine, controller and exact player slot. */ + if (memcmp(&unchanged, current, sizeof(unchanged))) return TRUE; + data_iterator_new(&iterator, player_data); + while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL) { + struct network_player *identity = &player->network_player_data; + if (player->quit_out_of_game || identity->player_list_index != slot || + identity->machine_index != machine_index || + identity->controller_index != request.controller_index) continue; + if (player->team_index == request.team_index) return TRUE; + /* Respect the host's existing automatic balance rule. */ + if (!mcc_ui_team_balance_allows((short)machine_index, request.controller_index, request.team_index)) return TRUE; + current->team_index = request.team_index; + identity->team_index = request.team_index; + player->team_index = request.team_index; + /* A normal new unit carries its team's ownership/color to every + * client. Drop objectives and respawn, without a suicide score. */ + if (player->unit_index != NONE) { + mcc_ui_team_respawn_track(iterator.datum_index, player->unit_index); + unit_kill_no_statistics(player->unit_index); + } + network_event("mcc: player %ld requested team %d", slot, (int)request.team_index); + return TRUE; + } + return TRUE; +} diff --git a/port/linux/game/mcc_ui_teams.h b/port/linux/game/mcc_ui_teams.h new file mode 100644 index 0000000000..f0f7332dd2 --- /dev/null +++ b/port/linux/game/mcc_ui_teams.h @@ -0,0 +1,11 @@ +#ifndef MCC_UI_TEAMS_H +#define MCC_UI_TEAMS_H +struct network_game_server; +struct network_game_server_client_machine; +boolean mcc_ui_team_balance_allows(short machine, short controller, short team); +void mcc_ui_teams_reset(void); +void mcc_ui_team_respawn_damage(long player_index, long unit_index, boolean active); +void mcc_ui_team_respawn_death(long *killer, long *object, long dead, boolean *friendly); +boolean mcc_ui_team_request(struct network_game_server *server, + struct network_game_server_client_machine *machine, word *message, short size); +#endif diff --git a/port/linux/game/mcc_vorbis.c b/port/linux/game/mcc_vorbis.c new file mode 100644 index 0000000000..b7dd63a0c9 --- /dev/null +++ b/port/linux/game/mcc_vorbis.c @@ -0,0 +1,216 @@ +/* MCC's private instance of the unmodified generic Vorbis dependency. Every + * public dependency symbol is namespaced so Xbox/CE keep their existing decoder. + * Empty residue vector books add nothing in Xiph's reference decoder; stb treats + * an attempted read from one as an error and abandons the remaining residue. + * Normalize only those vector-book references before decoding any audio packet. + * Reference: https://github.com/xiph/vorbis/blob/v1.3.7/lib/codebook.c + * (vorbis_book_decodevs_add, vorbis_book_decodev_add, vorbis_book_decodevv_add). + */ +#include "mcc_vorbis.h" +#include +#include +#include + +#define STB_VORBIS_NO_STDIO +#define STB_VORBIS_NO_PUSHDATA_API +#define stb_vorbis_get_info mcc_stb_get_info +#define stb_vorbis_get_comment mcc_stb_get_comment +#define stb_vorbis_get_error mcc_stb_get_error +#define stb_vorbis_close mcc_stb_close +#define stb_vorbis_get_sample_offset mcc_stb_get_sample_offset +#define stb_vorbis_get_file_offset mcc_stb_get_file_offset +#define stb_vorbis_decode_memory mcc_stb_decode_memory +#define stb_vorbis_open_memory mcc_stb_open_memory +#define stb_vorbis_seek_frame mcc_stb_seek_frame +#define stb_vorbis_seek mcc_stb_seek +#define stb_vorbis_seek_start mcc_stb_seek_start +#define stb_vorbis_stream_length_in_samples mcc_stb_stream_length_in_samples +#define stb_vorbis_stream_length_in_seconds mcc_stb_stream_length_in_seconds +#define stb_vorbis_get_frame_float mcc_stb_get_frame_float +#define stb_vorbis_get_frame_short_interleaved mcc_stb_get_frame_short_interleaved +#define stb_vorbis_get_frame_short mcc_stb_get_frame_short +#define stb_vorbis_get_samples_float_interleaved mcc_stb_get_samples_float_interleaved +#define stb_vorbis_get_samples_float mcc_stb_get_samples_float +#define stb_vorbis_get_samples_short_interleaved mcc_stb_get_samples_short_interleaved +#define stb_vorbis_get_samples_short mcc_stb_get_samples_short +#include "stb_vorbis.c" + +static uint32_t mcc_ogg_u32(unsigned char const *p) +{ + return (uint32_t)p[0]|(uint32_t)p[1]<<8|(uint32_t)p[2]<<16|(uint32_t)p[3]<<24; +} + +/* The pull decoder does not verify page checksums. Validate the complete bounded + * permutation first, including EOS, so a truncated/damaged packet is not made + * acceptable by the narrow empty-book compatibility rule. */ +static int mcc_ogg_validate(unsigned char const *bytes, uint32_t size, uint32_t *frames, + uint32_t *payload_end, uint32_t *final_page) +{ + uint32_t table[256], offset=0, serial=0, sequence=0, i, j; + uint32_t last_page=0, last_end=0; + int continued=0, ended=0; + *payload_end=size;*final_page=0; + for (i=0;i<256;i++) { + uint32_t crc=i<<24; + for (j=0;j<8;j++) crc=(crc<<1)^((crc&0x80000000u)?0x04C11DB7u:0); + table[i]=crc; + } + while (offsetsize-offset-length) return 0; + length+=count; + for (i=0;isize-offset || (count && !!(page[5]&1)!=continued)) return 0; + if (!offset) { + if (!(page[5]&2)) return 0; + serial=mcc_ogg_u32(page+14);sequence=mcc_ogg_u32(page+18); + } else if ((page[5]&2) || mcc_ogg_u32(page+14)!=serial || mcc_ogg_u32(page+18)!=sequence) return 0; + for (i=0;i=22 && i<26 ? 0 : page[i]; + crc=(crc<<8)^table[(crc>>24)^value]; + } + if (crc!=mcc_ogg_u32(page+22)) return 0; + /* Ogg permits zero-segment pages (including a separate EOS page). With + * no lacing entries no packet begins or ends, so retain pending state. + * https://xiph.org/ogg/doc/framing.html#page_segments */ + if (count) { + continued=page[26+count]==255; + last_page=offset;last_end=offset+length; + } + ended=(page[5]&4)!=0; + if (ended) { + if (continued || mcc_ogg_u32(page+10)) return 0; + *frames=mcc_ogg_u32(page+6); + if (!count) { + if (!last_end) return 0; + *payload_end=last_end;*final_page=last_page; + } + } + offset+=length;sequence++; + } + return ended; +} + +/* stb's page reader uses a stale lacing entry for an empty terminal page. Give + * only this case a private equivalent framing: finish the last complete packet + * page with the already-validated EOS granule. This also preserves final-block + * trimming. Source bytes and the shared Xbox/CE decoder remain untouched. */ +static unsigned char *mcc_ogg_terminal_page(unsigned char const *bytes, + uint32_t size, uint32_t offset, uint32_t frames) +{ + unsigned char *copy=malloc(size), *page; + uint32_t i,j,crc=0; + if (!copy) return NULL; + memcpy(copy,bytes,size);page=copy+offset; + page[5]|=4; + for (i=0;i<4;i++) page[6+i]=(unsigned char)(frames>>(8*i)); + memset(page+10,0,4);memset(page+22,0,4); + for (i=0;i>(8*i)); + return copy; +} + +static int mcc_vorbis_empty_book(Codebook const *book) +{ + int i; + if (book->sparse) return !book->sorted_entries; + /* stb expands very small sparse books, including an empty 1-3 entry book. */ + for (i=0;ientries;i++) if (book->codeword_lengths[i]!=NO_CODE) return 0; + return 1; +} + +static int mcc_vorbis_residues(vorb *stream) +{ + int index, classification, pass; + /* A floor book supplies scalar values, never an additive vector. Keep that + * distinct from the zero-contribution residue case. */ + for (index=0;indexfloor_count;index++) { + Floor1 const *floor=&stream->floor_config[index].floor1; + int partition; + if (stream->floor_types[index]!=1) return 0; + for (partition=0;partitionpartitions;partition++) { + int kind=floor->partition_class_list[partition]; + int subclasses=floor->class_subclasses[kind]; + if (subclasses && mcc_vorbis_empty_book(stream->codebooks+floor->class_masterbooks[kind])) return 0; + for (classification=0;classification<(1<subclass_books[kind][classification]; + if (number>=0 && mcc_vorbis_empty_book(stream->codebooks+number)) return 0; + } + } + } + for (index=0;indexresidue_count;index++) { + Residue *residue=stream->residue_config+index; + /* Empty scalar classbooks cannot supply classifications. This is not the + * empty additive-vector case, and must not be silently accepted. */ + if (mcc_vorbis_empty_book(stream->codebooks+residue->classbook)) return 0; + for (classification=0;classificationclassifications;classification++) { + for (pass=0;pass<8;pass++) { + int number=residue->residue_books[classification][pass]; + if (number>=0) { + Codebook const *book=stream->codebooks+number; + if (!book->lookup_type) return 0; + if (mcc_vorbis_empty_book(book)) residue->residue_books[classification][pass]=-1; + } + } + } + } + return 1; +} + +int mcc_vorbis_decode(unsigned char const *bytes, uint32_t size, uint32_t frame_limit, + short **samples, uint32_t *frames, uint32_t *rate, int *channels) +{ + vorb stream; + uint32_t expected=0, count=0, payload_end=0, final_page=0; + unsigned char *normalized=NULL; + short *pcm=NULL; + int ok=0; + *samples=NULL;*frames=0;*rate=0;*channels=0; + if (!bytes || !size || size>INT_MAX || !mcc_ogg_validate(bytes,size,&expected,&payload_end,&final_page) || + !expected || expected>frame_limit || expected>INT_MAX/(2*sizeof(short))) return 0; + if (payload_end2 || stream.sample_rate<8000 || stream.sample_rate>192000) goto done; + pcm=malloc((size_t)expected*stream.channels*sizeof(short)); + if (!pcm) goto done; + /* Normalization precedes the discarded overlap packet, which can itself use + * an empty residue vector book. No Xbox/CE decoder state is accessible here. */ + if (!vorbis_pump_first_frame(&stream) || stream.error) goto done; + while (count + +/* MCC-owned decoder. On success release *samples with mcc_vorbis_free(): callers + * using cseries' tracked allocator must not pass it to their free macro. */ +int mcc_vorbis_decode(unsigned char const *bytes, uint32_t size, uint32_t frame_limit, + short **samples, uint32_t *frames, uint32_t *rate, int *channels); +void mcc_vorbis_free(short *samples); + +#endif diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index 7f9cb55ea8..853357b0fc 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -71,6 +71,7 @@ their handlers open opens. #include "main/main.h" #include "networking/network_game_manager.h" #include "saved games/player_profile.h" +#include "saved games/game_state.h" #include "tag_files/tag_groups.h" #include "text/text_group.h" #include "text/unicode.h" @@ -78,6 +79,8 @@ their handlers open opens. #include "halo_menus.h" #include "custom_edition_cache.h" #include "custom_edition_maps.h" +#include "mcc_maps.h" +#include "mcc_cache.h" #include "network_voice.h" #include "text/draw_string.h" /* (internet play's server browser: the platform layer's) */ @@ -562,6 +565,10 @@ static struct short saved_game_to_delete; } campaign; +/* MCC checkpoint identities are independent of the stock campaign's level + * numbers. Only MCC rows use this additional storage. */ +static char mcc_saved_game_names[MAXIMUM_ROWS][256]; + /* the descendant of the widget with the name (a widget's name is its definition's: the last part of ours), the nth of them */ static struct widget_instance *descendant(struct widget_instance *widget, char const *name, long *nth) @@ -858,12 +865,38 @@ static boolean campaign_menu_initialize(short controller) } /* "campaign menu continue": the saved game goes on */ +static boolean mcc_saved_game_read(char name[256], short *difficulty) +{ + boolean corrupted = FALSE; + + name[0] = 0; + if (player_ui_get_active_player_profile_index(0) == NONE || + !game_state_test_persistent_storage(name, difficulty, &corrupted) || + !mcc_level_name(name)) + { + name[0] = 0; + return FALSE; + } + *difficulty = (short)PIN(*difficulty, 0, NUMBER_OF_GAME_DIFFICULTY_LEVELS - 1); + return TRUE; +} + static boolean campaign_continue(short controller) { struct player_profile profile; char const *map_name; short level, difficulty; + { + char mcc_name[256]; + if (mcc_saved_game_read(mcc_name, &difficulty)) + { + if (!campaign_profile(controller, &profile) || !mcc_cache_require(mcc_name, 0)) + return campaign_fail(); + campaign_start(mcc_name, difficulty, controller); + return TRUE; + } + } if (!campaign_profile(controller, &profile) || !ui_widget_port_saved_game(&map_name, &level, &difficulty)) return campaign_fail(); campaign_start(map_name, difficulty, controller); @@ -885,6 +918,8 @@ enum MAP_KIND_MULTIPLAYER, MAP_KIND_CUSTOM_SINGLEPLAYER, MAP_KIND_CUSTOM_MULTIPLAYER, + MAP_KIND_MCC_SINGLEPLAYER, + MAP_KIND_MCC_MULTIPLAYER, NUMBER_OF_MAP_KINDS }; @@ -896,9 +931,16 @@ short ui_widget_port_multiplayer_maps(char const *const **names, short *last_use static boolean map_kind_singleplayer(short kind) { + if (kind == MAP_KIND_MCC_SINGLEPLAYER) + return TRUE; return kind == MAP_KIND_SINGLEPLAYER || kind == MAP_KIND_CUSTOM_SINGLEPLAYER; } +static boolean map_kind_mcc(short kind) +{ + return kind == MAP_KIND_MCC_SINGLEPLAYER || kind == MAP_KIND_MCC_MULTIPLAYER; +} + /* The kind the chooser shows, kept to the multiplayer kinds unless `singleplayer`: a singleplayer kind it was turned to becomes the next multiplayer one the way it turned from `previous`. */ @@ -1044,6 +1086,61 @@ static void map_description_show(struct widget_instance *description, short leve widget->parameters.text_box.string_list_index = map; } +/* MCC has its own catalog, selection state and namespace. Its name and +description use its reserved display indices; an absent screenshot uses +the stock unknown-map image without entering the CE picture provider. */ +static struct +{ + short first, chosen; + boolean campaign; +} mcc_solo_list; + +static void mcc_map_description_show(struct widget_instance *description, short index) +{ + struct widget_instance *picture; + + map_description_show(description, NONE, index == NONE ? NONE : (short)(MCC_MAPS_FIRST_DISPLAY_INDEX + index)); + picture = named(description, "mp_map_right_pic", 0); + if (picture) + picture->animation.current_frame_index = 13; +} + +static void mcc_solo_map_text(short row, wchar_t *text) +{ + ustrncpy(text, mcc_maps_name(mcc_maps_type_index(row, mcc_solo_list.campaign)), ROW_TEXT_LENGTH - 1); + text[ROW_TEXT_LENGTH - 1] = 0; +} + +static void mcc_solo_list_update(struct widget_instance *list) +{ + struct widget_instance *description = list->parameters.list.extended_description; + short count = mcc_maps_type_count(mcc_solo_list.campaign); + short row = map_kind_rows_update(list, &mcc_solo_list.first, count, mcc_solo_map_text); + + if (row != NONE) + mcc_solo_list.chosen = row; + mcc_map_description_show(description, mcc_maps_type_index(mcc_solo_list.chosen, mcc_solo_list.campaign)); + profile_name_show(description); +} + +static boolean mcc_solo_choose(short controller) +{ + short index = mcc_maps_type_index(mcc_solo_list.chosen, mcc_solo_list.campaign); + char const *level_name = mcc_maps_level_name(index); + struct player_profile profile; + + if (!level_name || !campaign_profile(controller, &profile)) + return campaign_fail(); + if (mcc_maps_campaign(index)) + { + main_set_map_name(level_name); + main_defer_map_map_change(); + return TRUE; + } + campaign_start(level_name, main_get_difficulty(), controller); + return FALSE; +} + /* New Game's list: SINGLEPLAYER's levels, the profile's reached, or MULTIPLAYER's maps, played alone to walk around (no game engine: a campaign game on the map); CUSTOM SINGLEPLAYER's maps played as the campaign's levels @@ -1069,6 +1166,10 @@ static short map_kind_count(short kind, short multiplayer_map_count) return xbox_multiplayer_map_count(multiplayer_map_count); case MAP_KIND_CUSTOM_SINGLEPLAYER: return custom_edition_maps_count(TRUE); + case MAP_KIND_MCC_SINGLEPLAYER: + return mcc_maps_type_count(TRUE); + case MAP_KIND_MCC_MULTIPLAYER: + return mcc_maps_type_count(FALSE); default: return custom_edition_maps_count(FALSE); } @@ -1101,6 +1202,9 @@ static boolean level_list_initialize(struct widget_instance *list, short control struct player_profile profile; short level, last_used; + mcc_maps_rescan(); + mcc_solo_list.first = mcc_solo_list.chosen = 0; + if (!campaign_profile(controller, &profile)) return FALSE; campaign_levels_read(&profile); @@ -1132,9 +1236,19 @@ static void level_list_update(struct widget_instance *list) if (kind != level_list.kind) { + if (map_kind_mcc(kind)) + { + mcc_solo_list.first = mcc_solo_list.chosen = 0; + mcc_solo_list.campaign = kind == MAP_KIND_MCC_SINGLEPLAYER; + } level_list.kind = kind; level_list.first = level_list.chosen = 0; } + if (map_kind_mcc(kind)) + { + mcc_solo_list_update(list); + return; + } count = map_kind_count(kind, level_list.map_count); entry = map_kind_rows_update(list, &level_list.first, count, kind == MAP_KIND_SINGLEPLAYER ? level_row_text : @@ -1167,6 +1281,9 @@ static boolean level_choose(short controller) short level = campaign.shown_level; short count = map_kind_count(level_list.kind, level_list.map_count); + if (map_kind_mcc(level_list.kind)) + return mcc_solo_choose(controller); + if (level_list.kind != MAP_KIND_SINGLEPLAYER && (level_list.chosen < 0 || level_list.chosen >= count)) return campaign_fail(); if (level_list.kind == MAP_KIND_MULTIPLAYER || level_list.kind == MAP_KIND_CUSTOM_MULTIPLAYER) @@ -1208,6 +1325,11 @@ static boolean difficulty_start(short difficulty, short controller) if (!campaign_profile(controller, &profile)) return campaign_fail(); + if (mcc_maps_level_campaign(map_name)) + { + campaign_start(map_name, PIN(difficulty, 0, 3), controller); + return TRUE; + } /* (a campaign level, or a Custom Edition campaign map) */ if (!custom_edition_maps_level_campaign(map_name)) map_name = main_get_solo_level_name(0); @@ -1251,6 +1373,7 @@ static void saved_games_read(short controller) short index; campaign.saved_game_count = 0; + memset(mcc_saved_game_names, 0, sizeof(mcc_saved_game_names)); if (!campaign_profile(controller, &active)) return; active_index = player_ui_get_active_player_profile_index(0); @@ -1263,6 +1386,15 @@ static void saved_games_read(short controller) if (!((unsigned long)profiles[index] & PROFILE_VALID_BIT) || !player_profile_get(profiles[index], &profile)) continue; player_ui_set_active_player_profile(0, profiles[index], &profile); + if (mcc_saved_game_read(mcc_saved_game_names[campaign.saved_game_count], &saved_game->difficulty)) + { + saved_game->profile_index = profiles[index]; + saved_game->level = NONE; + ustrncpy(saved_game->profile_name, profile.player_name, MAXIMUM_PLAYER_PROFILE_NAME_LENGTH); + saved_game->profile_name[MAXIMUM_PLAYER_PROFILE_NAME_LENGTH] = 0; + campaign.saved_game_count++; + continue; + } if (ui_widget_port_saved_game(&map_name, &saved_game->level, &saved_game->difficulty)) { saved_game->profile_index = profiles[index]; @@ -1315,6 +1447,17 @@ static void saved_game_list_update(struct widget_instance *list) { struct campaign_saved_game const *saved_game = &campaign.saved_games[campaign.shown_saved_game]; + if (mcc_saved_game_names[campaign.shown_saved_game][0]) + { + short index = mcc_maps_find(mcc_saved_game_names[campaign.shown_saved_game]); + struct widget_instance *picture = named(description, "load_level_right_pic", 0); + level_description(description, "load_level", + index == NONE ? LEVEL_UNAVAILABLE : MCC_MAPS_FIRST_DISPLAY_INDEX + index, + FALSE, NULL, saved_game->difficulty); + if (picture) picture->animation.current_frame_index = LEVEL_UNAVAILABLE; + profile_name_show(description); + return; + } level_description(description, "load_level", saved_game->level, FALSE, NULL, saved_game->difficulty); } else if (row != NONE) @@ -1337,6 +1480,14 @@ static boolean saved_game_continue(short controller) if (!player_profile_get(saved_game->profile_index, &profile)) return campaign_fail(); player_ui_set_active_player_profile(0, saved_game->profile_index, &profile); + if (mcc_saved_game_names[campaign.shown_saved_game][0]) + { + char const *name = mcc_saved_game_names[campaign.shown_saved_game]; + if (!mcc_cache_require(name, 0)) + return campaign_fail(); + campaign_start(name, saved_game->difficulty, controller); + return TRUE; + } campaign_start(main_get_solo_level_name(saved_game->level), saved_game->difficulty, controller); return TRUE; } @@ -2491,6 +2642,64 @@ static void map_difficulty_text(short difficulty, wchar_t *text) string_get("pc\\main_menu\\player_profiles_select\\difficulty_names", difficulty, text); } +static struct +{ + short first, chosen, step, level; + boolean multiplayer_only; + boolean campaign; +} mcc_host_list; + +static void mcc_host_map_text(short row, wchar_t *text) +{ + ustrncpy(text, mcc_maps_name(mcc_maps_type_index(row, mcc_host_list.campaign)), ROW_TEXT_LENGTH - 1); + text[ROW_TEXT_LENGTH - 1] = 0; +} + +static void mcc_host_list_update(struct widget_instance *list) +{ + struct widget_instance *description = list->parameters.list.extended_description; + short count = mcc_host_list.step == MAP_STEP_DIFFICULTIES ? NUMBER_OF_GAME_DIFFICULTY_LEVELS : + mcc_maps_type_count(mcc_host_list.campaign); + short row = map_kind_rows_update(list, &mcc_host_list.first, count, + mcc_host_list.step == MAP_STEP_DIFFICULTIES ? map_difficulty_text : mcc_host_map_text); + short shown; + + if (row != NONE) + mcc_host_list.chosen = row; + shown = mcc_host_list.step == MAP_STEP_DIFFICULTIES ? mcc_host_list.level : + mcc_maps_type_index(mcc_host_list.chosen, mcc_host_list.campaign); + visible_set(named(description, "mp_map_right_item", 0), shown != NONE); + mcc_map_description_show(description, shown); + profile_name_show(description); +} + +static boolean mcc_host_list_choose(struct widget_instance *list, boolean *widget_deleted) +{ + short index = mcc_host_list.step == MAP_STEP_DIFFICULTIES ? mcc_host_list.level : + mcc_maps_type_index(mcc_host_list.chosen, mcc_host_list.campaign); + char const *level_name = mcc_maps_level_name(index); + + if (!level_name) + return campaign_fail(); + if (mcc_host_list.step == MAP_STEP_DIFFICULTIES) + { + if (!ui_widget_port_mcc_cooperative_level_choose(level_name, mcc_host_list.chosen)) + return campaign_fail(); + return ui_widget_port_open(list, SERVER_SETUP_NAME, widget_deleted); + } + if (!mcc_maps_campaign(index)) + return ui_widget_port_mcc_multiplayer_map_choose(level_name); + if (mcc_host_list.multiplayer_only) + return campaign_fail(); + mcc_host_list.level = index; + mcc_host_list.step = MAP_STEP_DIFFICULTIES; + mcc_host_list.chosen = (short)PIN(main_get_difficulty(), 0, NUMBER_OF_GAME_DIFFICULTY_LEVELS - 1); + mcc_host_list.first = 0; + map_kind_focus(list, 0, mcc_host_list.chosen); + ui_play_audio_feedback_sound(SOUND_FORWARD); + return FALSE; +} + /* "mp level list initialize": on the multiplayer map used last */ static boolean map_list_initialize(struct widget_instance *list) { @@ -2498,6 +2707,9 @@ static boolean map_list_initialize(struct widget_instance *list) short last_used = 0; map_list.hosting = global_network_game_server_get() != NULL && !network_game_is_splitscreen_local(); + mcc_maps_rescan(); + csmemset(&mcc_host_list, 0, sizeof(mcc_host_list)); + mcc_host_list.multiplayer_only = !map_list.hosting; /* (the maps folders looked for again as the list opens) */ custom_edition_maps_look_again(); map_list.map_count = ui_widget_port_multiplayer_maps(&names, &last_used); @@ -2523,10 +2735,21 @@ static void map_list_update(struct widget_instance *list) if (kind != map_list.kind) { + if (map_kind_mcc(kind)) + { + csmemset(&mcc_host_list, 0, sizeof(mcc_host_list)); + mcc_host_list.multiplayer_only = !map_list.hosting; + mcc_host_list.campaign = kind == MAP_KIND_MCC_SINGLEPLAYER; + } map_list.kind = kind; map_list.step = MAP_STEP_MAPS; map_list.first = map_list.chosen = 0; } + if (map_kind_mcc(kind)) + { + mcc_host_list_update(list); + return; + } count = map_step_count(); entry = map_kind_rows_update(list, &map_list.first, count, map_list.step == MAP_STEP_DIFFICULTIES ? map_difficulty_text : kind == MAP_KIND_SINGLEPLAYER ? map_level_text : @@ -2558,6 +2781,9 @@ static boolean map_list_choose(struct widget_instance *list, boolean *widget_del { short chosen = map_list.chosen; + if (map_kind_mcc(map_list.kind)) + return mcc_host_list_choose(list, widget_deleted); + if (chosen >= map_step_count()) return campaign_fail(); if (map_list.step == MAP_STEP_DIFFICULTIES) @@ -2586,6 +2812,22 @@ static boolean map_list_choose(struct widget_instance *list, boolean *widget_del level, else out of the Map screen */ static boolean map_list_back(struct widget_instance *list, boolean *widget_deleted) { + if (map_kind_mcc(map_list.kind) && mcc_host_list.step == MAP_STEP_DIFFICULTIES) + { + short row, count = mcc_maps_type_count(mcc_host_list.campaign); + + mcc_host_list.step = MAP_STEP_MAPS; + mcc_host_list.chosen = 0; + for (row = 0; row < count; row++) + { + if (mcc_maps_type_index(row, mcc_host_list.campaign) == mcc_host_list.level) + mcc_host_list.chosen = row; + } + mcc_host_list.first = map_kind_first(mcc_host_list.chosen, count); + map_kind_focus(list, mcc_host_list.first, mcc_host_list.chosen); + ui_play_audio_feedback_sound(SOUND_BACK); + return TRUE; + } ui_play_audio_feedback_sound(SOUND_BACK); if (map_list.step == MAP_STEP_DIFFICULTIES) { @@ -2765,6 +3007,9 @@ ui_widget_port_cooperative_level_choose). A Custom Edition map this machine has not is taken to be one: co-op is the only game with no game engine. */ static boolean game_cooperative(struct network_game const *game) { + if (game && mcc_level_name(game->map.name)) + return !game->variant.game_engine_index && + (mcc_maps_level_campaign(game->map.name) || mcc_maps_find(game->map.name) == NONE); return game && !game->variant.game_engine_index && (custom_edition_maps_level_campaign(game->map.name) || (custom_edition_level_name(game->map.name) && custom_edition_maps_display_index(game->map.name) == NONE)); @@ -3221,6 +3466,9 @@ static short campaign_level_of(char const *map_name) { short level; + if (mcc_level_name(map_name)) + return NONE; + /* (a Custom Edition map, custom_maps\a30, is never one) */ if (custom_edition_level_name(map_name)) return NONE; @@ -3248,6 +3496,22 @@ static void map_display_name(char const *map_name, wchar_t *text) short index; short count = xbox_multiplayer_map_count(ui_widget_port_multiplayer_maps(&names, NULL)); + if (mcc_level_name(map_name)) + { + short mcc_index = mcc_maps_find(map_name); + + if (mcc_index != NONE) + ustrncpy(text, mcc_maps_name(mcc_index), ROW_TEXT_LENGTH - 1); + else + { + for (index = 0; map_name[index] && index < ROW_TEXT_LENGTH - 1; index++) + text[index] = (wchar_t)(unsigned char)map_name[index]; + text[index] = 0; + } + text[ROW_TEXT_LENGTH - 1] = 0; + return; + } + /* (a Custom Edition map's, if this machine has it: custom_edition_maps.c) */ if (custom_edition_level_name(map_name)) { @@ -4218,6 +4482,20 @@ static void lobby_map_show(struct widget_instance *description, char const *map_ short count = xbox_multiplayer_map_count(ui_widget_port_multiplayer_maps(&names, NULL)), map = 19, index; short level = campaign_level_of(map_name); struct widget_instance *widget; + short mcc_index = mcc_maps_find(map_name); + + if (mcc_index != NONE) + { + visible_set(named(description, "lobby_map_pic", 0), TRUE); + visible_set(named(description, "lobby_map_name", 0), TRUE); + visible_set(named(description, "replay_level_right_pic", 0), FALSE); + visible_set(named(description, "replay_level_right_name", 0), FALSE); + if ((widget = named(description, "lobby_map_pic", 0)) != NULL) + widget->animation.current_frame_index = 13; + if ((widget = named(description, "lobby_map_name", 0)) != NULL) + widget->parameters.text_box.string_list_index = MCC_MAPS_FIRST_DISPLAY_INDEX + mcc_index; + return; + } for (index = 0; index < count; index++) { diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c index 5a1e5a7b7a..243e457b3a 100644 --- a/port/linux/game/menu_tags.c +++ b/port/linux/game/menu_tags.c @@ -41,6 +41,8 @@ pause it as its pause menu does. #include "rasterizer/xbox/rasterizer_xbox_hardware_bitmaps.h" #include "halo_menus.h" +#include "mcc_ui.h" +#include "mcc_cache.h" #include #include @@ -1674,6 +1676,9 @@ static void pause_patch(struct cache_file_tag_instance *instances) short grow = 0; boolean box_redrawn = FALSE; + /* MCC supplies its own Settings and game-options buttons. */ + if (mcc_cache_tags_loaded()) + return; if (collection == NONE || quit_function == NONE) return; screens = tag_get('Soul', collection); @@ -1931,6 +1936,17 @@ void menu_tags_loaded( boolean game_map = strcmp(map_name, "ui") != 0; boolean campaign = game_map && single_player_campaign_map(); + /* MCC owns its pause menus; only its multiplayer Settings route needs + these native screens. Do not patch embedded MCC campaign widgets. */ + if (mcc_cache_tags_loaded()) + { + if (mcc_ui_settings_needed(map_name)) + { + campaign = FALSE; + goto mcc_settings; + } + return; + } /* (ui.map, and a multiplayer map: its pause menu's SETTINGS; a single-player campaign's too) */ if ((game_map && tag_loaded('Soul', MULTIPLAYER_COLLECTION) == NONE && !campaign) || @@ -1938,6 +1954,7 @@ void menu_tags_loaded( { return; } +mcc_settings: menus = halo_menus_load(); if (!menus) return; @@ -2105,6 +2122,11 @@ char const *pc_menus_root_name( boolean pc_menu_tag( long tag_index) { + /* MCC's independently generated pause widgets use stock widget behavior. */ + { + extern boolean mcc_pause_owns(long); + if (mcc_pause_owns(tag_index)) return FALSE; + } return menu_tags.loaded && tag_index != NONE && DATUM_INDEX_TO_ABSOLUTE_INDEX(tag_index) >= menu_tags.original_count; } diff --git a/port/linux/game/network_damage.c b/port/linux/game/network_damage.c index 3ed444a5c3..6125d3f523 100644 --- a/port/linux/game/network_damage.c +++ b/port/linux/game/network_damage.c @@ -56,6 +56,7 @@ being hit looks and feels like on the clients). #include "scenario/scenario.h" #include "units/units.h" #include "network_distributed.h" +#include "mcc_grenades.h" #include @@ -1202,6 +1203,8 @@ than one damage), the vehicle they drove lately; 0 when none of them deals it; how they deal it (the _damage_source flags); how far from them what they fire deals it; and the type of grenade when nothing but a grenade of theirs deals it, else NONE */ +#include "mcc_network_damage.inc" + static real distributed_player_deals( short player_index, long damage_index, @@ -1283,6 +1286,7 @@ static real distributed_player_deals( *kinds |= this_kinds; } } + mcc_network_extra_grenade_damage(player_index, damage_index, kinds, reach, grenade_type, &grenade_rate); if (rate > 0.0f) *grenade_type = NONE; rate = MAX(rate, grenade_rate); @@ -2320,6 +2324,11 @@ void network_damage_note_grenade( short index; short slot = 0; + if (mcc_grenades_active() && grenade_type >= 2) + { + mcc_network_note_extra_grenade(unit_index, grenade_type); + return; + } if (game_connection() != _game_connection_network_server || unit->unit.player_index == NONE || grenade_type < 0 || grenade_type >= NUMBER_OF_UNIT_GRENADE_TYPES) { diff --git a/port/linux/game/network_distributed.c b/port/linux/game/network_distributed.c index 46126a4127..05e88b9e67 100644 --- a/port/linux/game/network_distributed.c +++ b/port/linux/game/network_distributed.c @@ -73,6 +73,7 @@ machine (their datum identifiers need not be). #include "network_coop.h" #include "network_distributed.h" #include "network_voice.h" +#include "mcc_network.h" #include "profile_sections.h" #include @@ -3443,6 +3444,7 @@ static boolean distributed_message_stale( case _distributed_message_coop_screen_effect: case _distributed_message_coop_device_states: case _distributed_message_votekick_status: + case _distributed_message_mcc_grenades: break; default: return FALSE; @@ -3989,6 +3991,7 @@ void network_distributed_handle_message( case _distributed_message_unit_states: entry_size = DISTRIBUTED_UNIT_STATE_MINIMUM_SIZE; break; case _distributed_message_player_statistics: entry_size = sizeof(struct distributed_player_statistics); break; case _distributed_message_pings: entry_size = sizeof(struct distributed_player_ping); break; + case _distributed_message_mcc_grenades: entry_size = mcc_network_inventory_entry_size(); break; case _distributed_message_actor_states: entry_size = network_actors_entry_size(); break; case _distributed_message_structure_bsp: entry_size = sizeof(struct distributed_structure_bsp); break; case _distributed_message_coop_presentation: entry_size = network_coop_presentation_entry_size(); break; @@ -4163,6 +4166,9 @@ void network_distributed_handle_message( case _distributed_message_inventories: network_objects_handle_inventories(entries, header.count); break; + case _distributed_message_mcc_grenades: + mcc_network_handle_inventories(entries, header.count); + break; case _distributed_message_object_changes: network_objects_handle_changes(entries, header.count); break; diff --git a/port/linux/game/network_distributed.h b/port/linux/game/network_distributed.h index 0ba551911c..463083c3af 100644 --- a/port/linux/game/network_distributed.h +++ b/port/linux/game/network_distributed.h @@ -118,6 +118,8 @@ enum _distributed_message_voice_up = 80, _distributed_message_voice_down = 81, _distributed_message_voice_config = 82, + /* MCC-only extra grenade slots; ordinary inventory packets stay exact. */ + _distributed_message_mcc_grenades = 83, NUMBER_OF_DISTRIBUTED_MESSAGES }; diff --git a/port/linux/game/network_objects.c b/port/linux/game/network_objects.c index 6427396707..5ab8d48ebd 100644 --- a/port/linux/game/network_objects.c +++ b/port/linux/game/network_objects.c @@ -64,6 +64,7 @@ same datum index (identifier and all), so that any message can name one: #include "cutscene/cinematics.h" #include "network_coop.h" #include "network_distributed.h" +#include "mcc_network.h" #include "profile_sections.h" #include @@ -2015,6 +2016,7 @@ void network_objects_host_tick( if (game_time_get() % INVENTORY_INTERVAL_TICKS == 0) profile_scope(objects_host_tick_send_inventories_section, distributed_host_send_inventories();) profile_scope(objects_host_tick_send_damage_animations_section, distributed_host_send_damage_animations();) + mcc_network_host_tick(); } /* ---------- a client */ @@ -3128,6 +3130,7 @@ void network_objects_client_tick( objects_client_check_all = TRUE; distributed_client_ready_picked_up_weapons(); distributed_client_note_own_inventories(); + mcc_network_client_tick(); distributed_client_send_vehicles(); distributed_client_carry_unsteered_vehicles(); /* (who it is, as its Discord told it: once its ready went, which makes @@ -3146,6 +3149,7 @@ void network_objects_new_game( short local_player_index; short index; + mcc_network_new_game(); csmemset(objects_host_inventories, 0, sizeof(objects_host_inventories)); host_damage_animation_count = 0; csmemset(client_damage_animations, 0, sizeof(client_damage_animations)); diff --git a/port/linux/game/tag_validate.c b/port/linux/game/tag_validate.c index dccfd832e4..306e7a800c 100644 --- a/port/linux/game/tag_validate.c +++ b/port/linux/game/tag_validate.c @@ -42,11 +42,15 @@ data and which the game takes as models (tag_schema_custom_edition_groups). #include "cseries.h" #include "cache/physical_memory_map.h" #include "tag_schema.h" +#include "mcc_tag_validate.h" /* MCC callbacks have an independent context. */ #include #include #include +/* NULL for Xbox/Custom Edition and in the asset-free standalone validator. */ +struct mcc_validation_dispatch const *mcc_validation_callbacks; + /* ---------- constants */ enum @@ -1325,6 +1329,8 @@ boolean tag_validate_any_claimed( void const *address, unsigned long size) { + if (mcc_validation_callbacks) + return mcc_validation_callbacks->any_claimed(address, size); unsigned long offset = (unsigned long)address - (unsigned long)tag_validate_globals.header; unsigned long bit; @@ -1361,6 +1367,14 @@ void tag_validate_refuse( { va_list arguments; + if (mcc_validation_callbacks && mcc_validation_callbacks->owns(validation)) + { + va_start(arguments, format); + mcc_validation_callbacks->message(validation, TRUE, format, arguments); + va_end(arguments); + return; + } + validation->refused = TRUE; va_start(arguments, format); validation_message(validation, "cannot be played", format, arguments); @@ -1390,6 +1404,14 @@ void tag_validate_correct( { va_list arguments; + if (mcc_validation_callbacks && mcc_validation_callbacks->owns(validation)) + { + va_start(arguments, format); + mcc_validation_callbacks->message(validation, FALSE, format, arguments); + va_end(arguments); + return; + } + validation->corrections++; if (validation->corrections <= MAXIMUM_LOGGED_CORRECTIONS) { @@ -1414,6 +1436,8 @@ boolean tag_validate_file_contains( long offset, long size) { + if (mcc_validation_callbacks && mcc_validation_callbacks->owns(validation)) + return mcc_validation_callbacks->file_contains(validation, offset, size); short range_index; (void)validation; @@ -1436,12 +1460,16 @@ boolean tag_validate_file_contains( void *tag_validate_root( struct tag_validation *validation) { + if (mcc_validation_callbacks && mcc_validation_callbacks->owns(validation)) + return mcc_validation_callbacks->root(validation); return validation->depth > 0 ? validation->frames[0].base : NULL; } boolean tag_validate_custom_edition( struct tag_validation *validation) { + if (mcc_validation_callbacks && mcc_validation_callbacks->owns(validation)) + return FALSE; (void)validation; return tag_validate_globals.custom_edition; @@ -1452,6 +1480,8 @@ boolean tag_validate_contains( void const *address, unsigned long size) { + if (mcc_validation_callbacks && mcc_validation_callbacks->owns(validation)) + return mcc_validation_callbacks->contains(validation, address, size); return region_contains(validation, address, size); } @@ -1460,6 +1490,8 @@ void *tag_validate_tag_get( long tag_index, unsigned long group_tag) { + if (mcc_validation_callbacks && mcc_validation_callbacks->owns(validation)) + return mcc_validation_callbacks->tag_get(validation, tag_index, group_tag); struct tag_validate_instance *instance = instance_get(tag_index); unsigned long groups[2]; @@ -1492,6 +1524,8 @@ void *tag_validate_vertex_buffer_data( struct tag_validation *validation, void const *buffer) { + if (mcc_validation_callbacks && mcc_validation_callbacks->owns(validation)) + return mcc_validation_callbacks->buffer_data(validation, buffer, FALSE); return buffer_data(validation->vertex_buffers, validation->vertex_buffer_count, buffer); } @@ -1499,5 +1533,7 @@ void *tag_validate_index_buffer_data( struct tag_validation *validation, void const *buffer) { + if (mcc_validation_callbacks && mcc_validation_callbacks->owns(validation)) + return mcc_validation_callbacks->buffer_data(validation, buffer, TRUE); return buffer_data(validation->index_buffers, validation->index_buffer_count, buffer); } diff --git a/port/linux/include/halo_port_limits.h b/port/linux/include/halo_port_limits.h index 3d2ef473bb..333a173c0d 100644 --- a/port/linux/include/halo_port_limits.h +++ b/port/linux/include/halo_port_limits.h @@ -66,7 +66,8 @@ is newer (network_client_manager.c). A host advertises it, with its netcode, in its game's advertisement's reserved bytes (network_server_message_handler.c), which hosts built before there was a version send as zeros: version 0. Raise it with any change to what the machines send each other. */ -#define HALO_PORT_NETWORK_VERSION 25 +/* MCC adds an inventory message and additional grenade action values. */ +#define HALO_PORT_NETWORK_VERSION 26 /* ... the advertisement's reserved bytes: the version (a little-endian word), then flags */ #define HALO_PORT_ADVERTISED_VERSION_OFFSET 0 diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index 38dac3dfba..7b43061a65 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -2386,6 +2386,15 @@ void WINAPI D3DDevice_SetTexture(DWORD stage, D3DBaseTexture *texture) device.textures[stage] = texture; } +/* Unload can happen before a later draw replaces every texture stage. */ +void xgpu_mcc_texture_unbind(DWORD const *resource) +{ + unsigned long stage; + for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++) + if ((DWORD const *)device.textures[stage] == resource) + device.textures[stage] = NULL; +} + void WINAPI D3DDevice_SetPalette(DWORD stage, D3DPalette *palette) { if (stage < D3DTSS_MAXSTAGES) diff --git a/port/linux/src/mcc_memory.c b/port/linux/src/mcc_memory.c new file mode 100644 index 0000000000..9e12ef35f4 --- /dev/null +++ b/port/linux/src/mcc_memory.c @@ -0,0 +1,30 @@ +/* MCC cache address reservations. A file can request a free 64 MiB window; + * MAP_FIXED_NOREPLACE never replaces an Xbox, CE or host allocation. */ +#include "platform.h" +#include +#include + +void *mcc_memory_reserve(uint32_t address, uint32_t size) +{ + void *wanted = (void *)(uintptr_t)address; + void *allocation; + /* Keep clear of the Xbox arena, Custom Edition window and low addresses. + * MCC Tool's current base is 0x50000000; other non-overlapping linked + * addresses are accepted only if the OS confirms the window is free. */ + if ((address & 0xFFFFu) || size != 0x04000000u || + address < 0x42000000u || address > 0x70000000u) + return NULL; + allocation = mmap(wanted, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0); + if (allocation == wanted) + return allocation; + if (allocation != MAP_FAILED) + munmap(allocation, size); + return NULL; +} + +void mcc_memory_release(void *allocation, uint32_t size) +{ + if (allocation) + munmap(allocation, size); +} diff --git a/port/linux/src/mcc_texture_bridge.c b/port/linux/src/mcc_texture_bridge.c new file mode 100644 index 0000000000..cf1e213acf --- /dev/null +++ b/port/linux/src/mcc_texture_bridge.c @@ -0,0 +1,120 @@ +/* MCC pixels already live in immutable, independently owned storage. Keep + * their GPU copies here rather than staging them through the Xbox LRU heap. + * A header's exact registered identity, never its Data word, grants access. */ +#include "xgpu.h" +#include "mcc_texture_bridge.h" +#include +#include + +#define MCC_TEXTURE_BUCKETS 512 +struct mcc_gpu_texture { + struct mcc_gpu_texture *next; + struct mcc_gpu_texture *hardware_next; + void const *bitmap; + DWORD header[5]; + void const *pixels; + unsigned long bytes; + GLuint texture; + D3DCOLOR palette[256]; + BOOL palette_present; + GLenum target; + struct xgpu_texture_description description; +}; +static struct mcc_gpu_texture *mcc_gpu_textures[MCC_TEXTURE_BUCKETS]; +static struct mcc_gpu_texture *mcc_gpu_hardware[MCC_TEXTURE_BUCKETS]; + +static unsigned long mcc_texture_bucket(void const *bitmap) +{ + return (((unsigned long)bitmap >> 4) * 2654435761UL) % MCC_TEXTURE_BUCKETS; +} + +void *mcc_texture_bridge_find(void const *bitmap) +{ + struct mcc_gpu_texture *entry; + for (entry=mcc_gpu_textures[mcc_texture_bucket(bitmap)];entry;entry=entry->next) + if (entry->bitmap==bitmap) return entry->header; + return NULL; +} + +void *mcc_texture_bridge_register(void const *bitmap,unsigned long format, + unsigned long size,void const *pixels,unsigned long bytes) +{ + struct mcc_gpu_texture *entry; + struct xgpu_texture_description description; + unsigned long face_bytes,faces,bucket; + void *existing=mcc_texture_bridge_find(bitmap); + if (existing) return existing; + if (!bitmap || !pixels || !bytes) return NULL; + xgpu_texture_describe(format,size,&description); + if (!description.width || !description.height || !description.depth || + description.width>4096 || description.height>4096 || description.depth>512 || + (description.depth>1 && (description.width>512 || description.height>512)) || + !description.levels || description.levels>13) return NULL; + face_bytes=xgpu_texture_face_size(&description); + faces=description.cube_map ? 6 : 1; + if (!face_bytes || face_bytes>bytes/faces) return NULL; + entry=calloc(1,sizeof(*entry)); + if (!entry) return NULL; + entry->bitmap=bitmap;entry->pixels=pixels;entry->bytes=bytes; + entry->description=description; + entry->target=description.cube_map ? GL_TEXTURE_CUBE_MAP : + description.depth>1 ? GL_TEXTURE_3D : GL_TEXTURE_2D; + entry->header[0]=D3DCOMMON_TYPE_TEXTURE|1; + /* Outside the guest physical arena and never dereferenced. The device + * requires nonzero Data; the exact header registry resolves this token. */ + entry->header[1]=0xffffffffUL; + entry->header[3]=format;entry->header[4]=size; + bucket=mcc_texture_bucket(bitmap); + entry->next=mcc_gpu_textures[bucket];mcc_gpu_textures[bucket]=entry; + bucket=mcc_texture_bucket(entry->header); + entry->hardware_next=mcc_gpu_hardware[bucket];mcc_gpu_hardware[bucket]=entry; + return entry->header; +} + +BOOL xgpu_mcc_texture_get(DWORD const *resource,D3DCOLOR const *palette, + GLuint *texture,GLenum *target,struct xgpu_texture_description *description) +{ + struct mcc_gpu_texture *entry; + /* Resource identity is checked without dereferencing arbitrary addresses; + * hardware and bitmap lookups both use independent MCC hash tables. */ + for (entry=mcc_gpu_hardware[mcc_texture_bucket(resource)];entry;entry=entry->hardware_next) { + if (entry->header!=resource) continue; + *description=entry->description;*target=entry->target; + if (entry->texture && entry->description.format==0x0b && + (entry->palette_present!=(palette!=NULL) || + (palette && memcmp(entry->palette,palette,sizeof(entry->palette))))) { + glDeleteTextures(1,&entry->texture);entry->texture=0;xgpu_gl_state_invalidate(); + } + if (!entry->texture) { + GLuint created=0; + glGenTextures(1,&created); + if (created && xgpu_mcc_texture_upload(created,entry->target, + &entry->description,entry->pixels,entry->bytes,palette)) { + entry->texture=created; + entry->palette_present=palette!=NULL; + if (palette && entry->description.format==0x0b) + memcpy(entry->palette,palette,sizeof(entry->palette)); + } + else if (created) {glDeleteTextures(1,&created);xgpu_gl_state_invalidate();} + } + *texture=entry->texture; + return TRUE; + } + return FALSE; +} + +void mcc_texture_bridge_dispose(void) +{ + unsigned long bucket; + memset(mcc_gpu_hardware,0,sizeof(mcc_gpu_hardware)); + for (bucket=0;bucketnext; + xgpu_mcc_texture_unbind(entry->header); + if (entry->texture) {glDeleteTextures(1,&entry->texture);xgpu_gl_state_invalidate();} + free(entry);entry=next; + } + } +} diff --git a/port/linux/src/mcc_texture_bridge.h b/port/linux/src/mcc_texture_bridge.h new file mode 100644 index 0000000000..05b364145c --- /dev/null +++ b/port/linux/src/mcc_texture_bridge.h @@ -0,0 +1,10 @@ +/* MCC immutable textures have identities independent of guest physical memory. */ +#ifndef MCC_TEXTURE_BRIDGE_H +#define MCC_TEXTURE_BRIDGE_H + +void *mcc_texture_bridge_find(void const *bitmap); +void *mcc_texture_bridge_register(void const *bitmap, unsigned long format, + unsigned long size, void const *pixels, unsigned long bytes); +void mcc_texture_bridge_dispose(void); + +#endif diff --git a/port/linux/src/xbox_textures.c b/port/linux/src/xbox_textures.c index 5244121e98..1f84e74ef9 100644 --- a/port/linux/src/xbox_textures.c +++ b/port/linux/src/xbox_textures.c @@ -794,6 +794,115 @@ static void upload(GLuint texture, GLenum target, const struct xgpu_texture_desc texture_dump(target, description); } +/* MCC bounded immutable upload. The original guest-memory uploader above + * remains unchanged; native format decoding is shared, ownership is separate. */ +BOOL xgpu_mcc_texture_upload(GLuint texture, GLenum target, const struct xgpu_texture_description *description, + void const *pixels, unsigned long bytes, const D3DCOLOR *palette) +{ + const unsigned char *base = pixels; + struct format_information information = format_information(description->format); + unsigned long face_count = description->cube_map ? 6 : 1; + unsigned long face_size = xgpu_texture_face_size(description); + unsigned long largest = description->width * description->height * description->depth; + BOOL decode_compressed = FALSE; + unsigned long *converted; + unsigned long face, level; + GLenum gl_error; + if (!base || !texture_size_supported(description) || !description->levels || + description->levels > 13 || !face_size || face_size > bytes / face_count || + information.kind == _texel_unknown) return FALSE; + /* Account for this upload's errors independently of earlier rendering. */ + gl_error = glGetError(); + if (gl_error != GL_NO_ERROR) + platform_log("mcc textures: prior GL error %lx", (unsigned long)gl_error); + +#ifdef HALO_GLES + decode_compressed = description->compressed && !xgpu_capabilities.s3tc; +#endif + converted = description->compressed && !decode_compressed ? NULL : malloc(largest * sizeof(unsigned long)); + if (!converted && !(description->compressed && !decode_compressed)) + { + platform_log("mcc textures: no memory to convert a %lux%lux%lu texture; it is not drawn", + description->width, description->height, description->depth); + return FALSE; + } + glBindTexture(target, texture); + xgpu_gl_state_invalidate(); + /* the channel of the texels each channel is sampled from, set on every + upload: a texture object can be reused for other texels */ + { + GLint channels[4] = { GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA }; + +#ifdef HALO_GLES + /* converted texels are BGRA in memory (32-bit ARGB words); ES takes + RGBA */ + if (converted) + { + channels[0] = GL_BLUE; + channels[2] = GL_RED; + } +#endif + + glTexParameteri(target, GL_TEXTURE_SWIZZLE_R, channels[0]); + glTexParameteri(target, GL_TEXTURE_SWIZZLE_G, channels[1]); + glTexParameteri(target, GL_TEXTURE_SWIZZLE_B, channels[2]); + glTexParameteri(target, GL_TEXTURE_SWIZZLE_A, channels[3]); + } + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexParameteri(target, GL_TEXTURE_BASE_LEVEL, 0); + glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, (GLint)description->levels - 1); + for (face = 0; face < face_count; face++) + { + GLenum image_target = description->cube_map ? GL_TEXTURE_CUBE_MAP_POSITIVE_X + face : target; + + for (level = 0; level < description->levels; level++) + { + const unsigned char *source = base + face * face_size + xgpu_texture_level_offset(description, level); + GLsizei width = (GLsizei)level_dimension(description->width, level); + GLsizei height = (GLsizei)level_dimension(description->height, level); + GLsizei depth = (GLsizei)level_dimension(description->depth, level); + + if (description->compressed && !decode_compressed) + { + if (target == GL_TEXTURE_3D) + glCompressedTexImage3D(image_target, (GLint)level, compressed_format(information.kind), width, height, depth, 0, + (GLsizei)level_bytes(description, level), source); + else + glCompressedTexImage2D(image_target, (GLint)level, compressed_format(information.kind), width, height, 0, + (GLsizei)level_bytes(description, level), source); + } + else + { +#ifdef HALO_GLES + if (decode_compressed) + dxt_decode_level(information.kind, source, (unsigned long)width, (unsigned long)height, + (unsigned long)depth, converted); + else +#endif + if (!decode_level(description, level, source, palette, converted)) + { + platform_log("mcc textures: no memory to convert a %lux%lux%lu texture; it is not drawn", + description->width, description->height, description->depth); + free(converted); + return FALSE; + } + if (target == GL_TEXTURE_3D) + glTexImage3D(image_target, (GLint)level, GL_RGBA8, width, height, depth, 0, GL_BGRA, GL_UNSIGNED_BYTE, converted); + else + glTexImage2D(image_target, (GLint)level, GL_RGBA8, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, converted); + } + } + } + free(converted); + gl_error = glGetError(); + if (gl_error != GL_NO_ERROR) { + platform_log("mcc textures: upload failed, GL error %lx", (unsigned long)gl_error); + return FALSE; + } + texture_dump(target, description); + return TRUE; +} + /* ---------- cache */ struct texture_entry @@ -918,6 +1027,10 @@ static GLuint texture_entry_result(struct texture_entry *entry, GLenum *target, GLuint xgpu_texture_get(const DWORD *resource, const D3DCOLOR *palette, GLenum *target, struct xgpu_texture_description *description) { + GLuint mcc_texture; + if (xgpu_mcc_texture_get(resource, palette, &mcc_texture, target, description)) + return mcc_texture; + DWORD data = resource[1], format_word = resource[3], size_word = resource[4]; struct texture_entry **bucket = &texture_buckets[bucket_index(data, format_word, size_word)]; struct texture_entry *entry; diff --git a/port/linux/src/xgpu.h b/port/linux/src/xgpu.h index 982f24b7ac..4485831edd 100644 --- a/port/linux/src/xgpu.h +++ b/port/linux/src/xgpu.h @@ -203,6 +203,14 @@ GLuint xgpu_texture_get(const DWORD *resource, const D3DCOLOR *palette, GLenum * struct xgpu_texture_description *description); void xgpu_texture_cache_begin_frame(void); +/* MCC-owned immutable resources never enter the guest physical texture cache. */ +BOOL xgpu_mcc_texture_get(DWORD const *resource, D3DCOLOR const *palette, + GLuint *texture, GLenum *target, struct xgpu_texture_description *description); +BOOL xgpu_mcc_texture_upload(GLuint texture, GLenum target, + struct xgpu_texture_description const *description, void const *pixels, + unsigned long bytes, D3DCOLOR const *palette); +void xgpu_mcc_texture_unbind(DWORD const *resource); + /* ---------- render targets */ struct xgpu_render_target diff --git a/port/third_party/bcdec/LICENSE b/port/third_party/bcdec/LICENSE new file mode 100644 index 0000000000..78e53f197c --- /dev/null +++ b/port/third_party/bcdec/LICENSE @@ -0,0 +1,52 @@ +This software is available under 2 licenses -- choose whichever you prefer. + +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License + +Copyright (c) 2022 Sergii Kudlai + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------ +ALTERNATIVE B - The Unlicense + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/port/third_party/bcdec/README.md b/port/third_party/bcdec/README.md new file mode 100644 index 0000000000..0dd1ce15d6 --- /dev/null +++ b/port/third_party/bcdec/README.md @@ -0,0 +1,11 @@ +# bcdec + +Unmodified `bcdec.h` and license from [iOrange/bcdec](https://github.com/iOrange/bcdec), +revision `80859ed3b7afb1c527a2a99d70c61457bea72d0c`. + +Header SHA-256: `134520764d96f70a27db814173616c89ad85db95fbd3328417c7a8c77e7ca189`. + +The independent MCC bitmap adapter uses this decoder for BC7 textures and +for compressed multipurpose textures that require channel conversion. +Distributed under the included MIT license option. No build changes are +needed: the adapter includes this header by relative path. diff --git a/port/third_party/bcdec/bcdec.h b/port/third_party/bcdec/bcdec.h new file mode 100644 index 0000000000..553c3ec5a8 --- /dev/null +++ b/port/third_party/bcdec/bcdec.h @@ -0,0 +1,1512 @@ +/* bcdec.h - v0.985 + provides functions to decompress blocks of BC compressed images + written by Sergii "iOrange" Kudlai in 2022 + + This library does not allocate memory and is trying to use as less stack as possible + + This library was never optimized specifically for speed but for the overall size + it has zero external dependencies and is not using any runtime functions + + This library was writtem by humans and will never contain any LLM-generated code + + Supported BC formats: + BC1 (also known as DXT1) + it's "binary alpha" variant BC1A (DXT1A) + BC2 (also known as DXT3) + BC3 (also known as DXT5) + BC4 (also known as ATI1N) + BC5 (also known as ATI2N) + BC6H (HDR format) + BC7 + + BC1/BC2/BC3/BC7 are expected to decompress into 4*4 RGBA blocks 8bit per component (32bit pixel) + BC4/BC5 are expected to decompress into 4*4 R/RG blocks 8bit per component (8bit and 16bit pixel) + BC6H is expected to decompress into 4*4 RGB blocks of either 32bit float or 16bit "half" per + component (96bit or 48bit pixel) + + For more info, issues and suggestions please visit https://github.com/iOrange/bcdec + + Configuration: + #define BCDEC_BC4BC5_PRECISE: + enables more precise but slower BC4/BC5 decoding + signed/unsigned mode + + CREDITS: + Aras Pranckevicius (@aras-p) - BC1/BC3 decoders optimizations (up to 3x the speed) + - BC6H/BC7 bits pulling routines optimizations + - optimized BC6H by moving unquantize out of the loop + - Split BC6H decompression function into 'half' and + 'float' variants + + Michael Schmidt (@RunDevelopment) - Found better "magic" coefficients for integer interpolation + of reference colors in BC1 color block, that match with + the floating point interpolation. This also made it faster + than integer division by 3! + + bugfixes: + @linkmauve + @wkjarosz + + LICENSE: See end of file for license information. +*/ + +#ifndef BCDEC_HEADER_INCLUDED +#define BCDEC_HEADER_INCLUDED + +#define BCDEC_VERSION_MAJOR 0 +#define BCDEC_VERSION_MINOR 985 + +/* if BCDEC_STATIC causes problems, try defining BCDECDEF to 'inline' or 'static inline' */ +#ifndef BCDECDEF +#ifdef BCDEC_STATIC +#define BCDECDEF static +#else +#ifdef __cplusplus +#define BCDECDEF extern "C" +#else +#define BCDECDEF extern +#endif +#endif +#endif + +/* Used information sources: + https://docs.microsoft.com/en-us/windows/win32/direct3d10/d3d10-graphics-programming-guide-resources-block-compression + https://docs.microsoft.com/en-us/windows/win32/direct3d11/bc6h-format + https://docs.microsoft.com/en-us/windows/win32/direct3d11/bc7-format + https://docs.microsoft.com/en-us/windows/win32/direct3d11/bc7-format-mode-reference + + ! WARNING ! Khronos's BPTC partitions tables contain mistakes, do not use them! + https://www.khronos.org/registry/DataFormat/specs/1.1/dataformat.1.1.html#BPTC + + ! Use tables from here instead ! + https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_texture_compression_bptc.txt + + Leaving it here as it's a nice read + https://fgiesen.wordpress.com/2021/10/04/gpu-bcn-decoding/ + + Fast half to float function from here + https://gist.github.com/rygorous/2144712 +*/ + +#define BCDEC_BC1_BLOCK_SIZE 8 +#define BCDEC_BC2_BLOCK_SIZE 16 +#define BCDEC_BC3_BLOCK_SIZE 16 +#define BCDEC_BC4_BLOCK_SIZE 8 +#define BCDEC_BC5_BLOCK_SIZE 16 +#define BCDEC_BC6H_BLOCK_SIZE 16 +#define BCDEC_BC7_BLOCK_SIZE 16 + +#define BCDEC_BC1_COMPRESSED_SIZE(w, h) ((((w)>>2)*((h)>>2))*BCDEC_BC1_BLOCK_SIZE) +#define BCDEC_BC2_COMPRESSED_SIZE(w, h) ((((w)>>2)*((h)>>2))*BCDEC_BC2_BLOCK_SIZE) +#define BCDEC_BC3_COMPRESSED_SIZE(w, h) ((((w)>>2)*((h)>>2))*BCDEC_BC3_BLOCK_SIZE) +#define BCDEC_BC4_COMPRESSED_SIZE(w, h) ((((w)>>2)*((h)>>2))*BCDEC_BC4_BLOCK_SIZE) +#define BCDEC_BC5_COMPRESSED_SIZE(w, h) ((((w)>>2)*((h)>>2))*BCDEC_BC5_BLOCK_SIZE) +#define BCDEC_BC6H_COMPRESSED_SIZE(w, h) ((((w)>>2)*((h)>>2))*BCDEC_BC6H_BLOCK_SIZE) +#define BCDEC_BC7_COMPRESSED_SIZE(w, h) ((((w)>>2)*((h)>>2))*BCDEC_BC7_BLOCK_SIZE) + +BCDECDEF void bcdec_bc1(const void* compressedBlock, void* decompressedBlock, int destinationPitch); +BCDECDEF void bcdec_bc2(const void* compressedBlock, void* decompressedBlock, int destinationPitch); +BCDECDEF void bcdec_bc3(const void* compressedBlock, void* decompressedBlock, int destinationPitch); +#ifndef BCDEC_BC4BC5_PRECISE +BCDECDEF void bcdec_bc4(const void* compressedBlock, void* decompressedBlock, int destinationPitch); +BCDECDEF void bcdec_bc5(const void* compressedBlock, void* decompressedBlock, int destinationPitch); +#else +BCDECDEF void bcdec_bc4(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); +BCDECDEF void bcdec_bc5(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); +BCDECDEF void bcdec_bc4_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); +BCDECDEF void bcdec_bc5_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); +#endif +BCDECDEF void bcdec_bc6h_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); +BCDECDEF void bcdec_bc6h_half(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); +BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, int destinationPitch); + +#endif /* BCDEC_HEADER_INCLUDED */ + +#ifdef BCDEC_IMPLEMENTATION + +static unsigned short bcdec__getu16le(const void* s) { + unsigned short l = (unsigned short)((const unsigned char*)s)[0]; + unsigned short h = (unsigned short)((const unsigned char*)s)[1]; + return l | (h << 8); +} +static unsigned int bcdec__getu32le(const void* s) { + unsigned int l = bcdec__getu16le(s); + unsigned int h = bcdec__getu16le((const unsigned char*)s + 2); + return l | (h << 16); +} +static unsigned long long bcdec__getu64le(const void* s) { + unsigned long long l = bcdec__getu32le(s); + unsigned long long h = bcdec__getu32le((const unsigned char*)s + 4); + return l | (h << 32); +} + +static void bcdec__color_block(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int onlyOpaqueMode) { + unsigned short c0, c1; + unsigned int refColors[4]; /* 0xAABBGGRR */ + unsigned char* dstColors; + unsigned int colorIndices; + int i, j, idx; + unsigned int r0, g0, b0, r1, g1, b1, r, g, b; + + c0 = bcdec__getu16le(compressedBlock); + c1 = bcdec__getu16le((const unsigned char*)compressedBlock + 2); + + /* Unpack 565 ref colors */ + r0 = (c0 >> 11) & 0x1F; + g0 = (c0 >> 5) & 0x3F; + b0 = c0 & 0x1F; + + r1 = (c1 >> 11) & 0x1F; + g1 = (c1 >> 5) & 0x3F; + b1 = c1 & 0x1F; + + /* Expand 565 ref colors to 888 */ + r = (r0 * 527 + 23) >> 6; + g = (g0 * 259 + 33) >> 6; + b = (b0 * 527 + 23) >> 6; + refColors[0] = 0xFF000000 | (b << 16) | (g << 8) | r; + + r = (r1 * 527 + 23) >> 6; + g = (g1 * 259 + 33) >> 6; + b = (b1 * 527 + 23) >> 6; + refColors[1] = 0xFF000000 | (b << 16) | (g << 8) | r; + + if (c0 > c1 || onlyOpaqueMode) { /* Standard BC1 mode (also BC3 color block uses ONLY this mode) */ + /* color_2 = 2/3*color_0 + 1/3*color_1 + color_3 = 1/3*color_0 + 2/3*color_1 */ + r = ((2 * r0 + r1) * 351 + 61) >> 7; + g = ((2 * g0 + g1) * 2763 + 1039) >> 11; + b = ((2 * b0 + b1) * 351 + 61) >> 7; + refColors[2] = 0xFF000000 | (b << 16) | (g << 8) | r; + + r = ((r0 + r1 * 2) * 351 + 61) >> 7; + g = ((g0 + g1 * 2) * 2763 + 1039) >> 11; + b = ((b0 + b1 * 2) * 351 + 61) >> 7; + refColors[3] = 0xFF000000 | (b << 16) | (g << 8) | r; + } else { /* Quite rare BC1A mode */ + /* color_2 = 1/2*color_0 + 1/2*color_1; + color_3 = 0; */ + r = ((r0 + r1) * 1053 + 125) >> 8; + g = ((g0 + g1) * 4145 + 1019) >> 11; + b = ((b0 + b1) * 1053 + 125) >> 8; + refColors[2] = 0xFF000000 | (b << 16) | (g << 8) | r; + + refColors[3] = 0x00000000; + } + + colorIndices = bcdec__getu32le((const unsigned char*)compressedBlock + 4); + + /* Fill out the decompressed color block */ + dstColors = (unsigned char*)decompressedBlock; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + idx = colorIndices & 0x03; + ((unsigned int*)dstColors)[j] = refColors[idx]; + colorIndices >>= 2; + } + + dstColors += destinationPitch; + } +} + +static void bcdec__sharp_alpha_block(const void* compressedBlock, void* decompressedBlock, int destinationPitch) { + unsigned short alpha; + const unsigned char* alphaPtr; + unsigned char* decompressed; + int i, j; + + alphaPtr = (const unsigned char*)compressedBlock; + decompressed = (unsigned char*)decompressedBlock; + + for (i = 0; i < 4; ++i) { + alpha = bcdec__getu16le(alphaPtr); + + for (j = 0; j < 4; ++j) { + decompressed[j * 4] = ((alpha >> (4 * j)) & 0x0F) * 17; + } + + alphaPtr += 2; + decompressed += destinationPitch; + } +} + +static void bcdec__smooth_alpha_block(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int pixelSize) { + unsigned char* decompressed; + unsigned char alpha[8]; + int i, j; + unsigned long long block, indices; + + block = bcdec__getu64le(compressedBlock); + decompressed = (unsigned char*)decompressedBlock; + + alpha[0] = block & 0xFF; + alpha[1] = (block >> 8) & 0xFF; + + if (alpha[0] > alpha[1]) { + /* 6 interpolated alpha values. */ + alpha[2] = (6 * alpha[0] + alpha[1]) / 7; /* 6/7*alpha_0 + 1/7*alpha_1 */ + alpha[3] = (5 * alpha[0] + 2 * alpha[1]) / 7; /* 5/7*alpha_0 + 2/7*alpha_1 */ + alpha[4] = (4 * alpha[0] + 3 * alpha[1]) / 7; /* 4/7*alpha_0 + 3/7*alpha_1 */ + alpha[5] = (3 * alpha[0] + 4 * alpha[1]) / 7; /* 3/7*alpha_0 + 4/7*alpha_1 */ + alpha[6] = (2 * alpha[0] + 5 * alpha[1]) / 7; /* 2/7*alpha_0 + 5/7*alpha_1 */ + alpha[7] = ( alpha[0] + 6 * alpha[1]) / 7; /* 1/7*alpha_0 + 6/7*alpha_1 */ + } + else { + /* 4 interpolated alpha values. */ + alpha[2] = (4 * alpha[0] + alpha[1]) / 5; /* 4/5*alpha_0 + 1/5*alpha_1 */ + alpha[3] = (3 * alpha[0] + 2 * alpha[1]) / 5; /* 3/5*alpha_0 + 2/5*alpha_1 */ + alpha[4] = (2 * alpha[0] + 3 * alpha[1]) / 5; /* 2/5*alpha_0 + 3/5*alpha_1 */ + alpha[5] = ( alpha[0] + 4 * alpha[1]) / 5; /* 1/5*alpha_0 + 4/5*alpha_1 */ + alpha[6] = 0x00; + alpha[7] = 0xFF; + } + + indices = block >> 16; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + decompressed[j * pixelSize] = alpha[indices & 0x07]; + indices >>= 3; + } + + decompressed += destinationPitch; + } +} + +#ifdef BCDEC_BC4BC5_PRECISE +static void bcdec__bc4_block(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int pixelSize, int isSigned) { + signed char* sblock; + unsigned char* ublock; + int alpha[8]; + int i, j; + unsigned long long block, indices; + + static int aWeights4[4] = { 13107, 26215, 39321, 52429 }; + static int aWeights6[6] = { 9363, 18724, 28086, 37450, 46812, 56173 }; + + block = bcdec__getu64le(compressedBlock); + + if (isSigned) { + alpha[0] = (char)(block & 0xFF); + alpha[1] = (char)((block >> 8) & 0xFF); + if (alpha[0] < -127) alpha[0] = -127; /* -128 clamps to -127 */ + if (alpha[1] < -127) alpha[1] = -127; /* -128 clamps to -127 */ + } else { + alpha[0] = block & 0xFF; + alpha[1] = (block >> 8) & 0xFF; + } + + if (alpha[0] > alpha[1]) { + /* 6 interpolated alpha values. */ + alpha[2] = (aWeights6[5] * alpha[0] + aWeights6[0] * alpha[1] + 32768) >> 16; /* 6/7*alpha_0 + 1/7*alpha_1 */ + alpha[3] = (aWeights6[4] * alpha[0] + aWeights6[1] * alpha[1] + 32768) >> 16; /* 5/7*alpha_0 + 2/7*alpha_1 */ + alpha[4] = (aWeights6[3] * alpha[0] + aWeights6[2] * alpha[1] + 32768) >> 16; /* 4/7*alpha_0 + 3/7*alpha_1 */ + alpha[5] = (aWeights6[2] * alpha[0] + aWeights6[3] * alpha[1] + 32768) >> 16; /* 3/7*alpha_0 + 4/7*alpha_1 */ + alpha[6] = (aWeights6[1] * alpha[0] + aWeights6[4] * alpha[1] + 32768) >> 16; /* 2/7*alpha_0 + 5/7*alpha_1 */ + alpha[7] = (aWeights6[0] * alpha[0] + aWeights6[5] * alpha[1] + 32768) >> 16; /* 1/7*alpha_0 + 6/7*alpha_1 */ + } else { + /* 4 interpolated alpha values. */ + alpha[2] = (aWeights4[3] * alpha[0] + aWeights4[0] * alpha[1] + 32768) >> 16; /* 4/5*alpha_0 + 1/5*alpha_1 */ + alpha[3] = (aWeights4[2] * alpha[0] + aWeights4[1] * alpha[1] + 32768) >> 16; /* 3/5*alpha_0 + 2/5*alpha_1 */ + alpha[4] = (aWeights4[1] * alpha[0] + aWeights4[2] * alpha[1] + 32768) >> 16; /* 2/5*alpha_0 + 3/5*alpha_1 */ + alpha[5] = (aWeights4[0] * alpha[0] + aWeights4[3] * alpha[1] + 32768) >> 16; /* 1/5*alpha_0 + 4/5*alpha_1 */ + alpha[6] = isSigned ? -127 : 0; + alpha[7] = isSigned ? 127 : 255; + } + + indices = block >> 16; + if (isSigned) { + sblock = (signed char*)decompressedBlock; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + sblock[j * pixelSize] = (signed char)alpha[indices & 0x07]; + indices >>= 3; + } + sblock += destinationPitch; + } + } else { + ublock = (unsigned char*)decompressedBlock; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + ublock[j * pixelSize] = (unsigned char)alpha[indices & 0x07]; + indices >>= 3; + } + ublock += destinationPitch; + } + } +} + +static void bcdec__bc4_block_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int pixelSize, int isSigned) { + float* decompressed; + float alpha[8]; + int i, j; + unsigned long long block, indices; + + block = bcdec__getu64le(compressedBlock); + decompressed = (float*)decompressedBlock; + + if (isSigned) { + alpha[0] = (float)((char)(block & 0xFF)) / 127.0f; + alpha[1] = (float)((char)((block >> 8) & 0xFF)) / 127.0f; + if (alpha[0] < -1.0f) alpha[0] = -1.0f; /* -128 clamps to -127 */ + if (alpha[1] < -1.0f) alpha[1] = -1.0f; /* -128 clamps to -127 */ + } else { + alpha[0] = (float)(block & 0xFF) / 255.0f; + alpha[1] = (float)((block >> 8) & 0xFF) / 255.0f; + } + + if (alpha[0] > alpha[1]) { + /* 6 interpolated alpha values. */ + alpha[2] = (6.0f * alpha[0] + alpha[1]) / 7.0f; /* 6/7*alpha_0 + 1/7*alpha_1 */ + alpha[3] = (5.0f * alpha[0] + 2.0f * alpha[1]) / 7.0f; /* 5/7*alpha_0 + 2/7*alpha_1 */ + alpha[4] = (4.0f * alpha[0] + 3.0f * alpha[1]) / 7.0f; /* 4/7*alpha_0 + 3/7*alpha_1 */ + alpha[5] = (3.0f * alpha[0] + 4.0f * alpha[1]) / 7.0f; /* 3/7*alpha_0 + 4/7*alpha_1 */ + alpha[6] = (2.0f * alpha[0] + 5.0f * alpha[1]) / 7.0f; /* 2/7*alpha_0 + 5/7*alpha_1 */ + alpha[7] = ( alpha[0] + 6.0f * alpha[1]) / 7.0f; /* 1/7*alpha_0 + 6/7*alpha_1 */ + } else { + /* 4 interpolated alpha values. */ + alpha[2] = (4.0f * alpha[0] + alpha[1]) / 5.0f; /* 4/5*alpha_0 + 1/5*alpha_1 */ + alpha[3] = (3.0f * alpha[0] + 2.0f * alpha[1]) / 5.0f; /* 3/5*alpha_0 + 2/5*alpha_1 */ + alpha[4] = (2.0f * alpha[0] + 3.0f * alpha[1]) / 5.0f; /* 2/5*alpha_0 + 3/5*alpha_1 */ + alpha[5] = ( alpha[0] + 4.0f * alpha[1]) / 5.0f; /* 1/5*alpha_0 + 4/5*alpha_1 */ + alpha[6] = isSigned ? -1.0f : 0.0f; + alpha[7] = 1.0f; + } + + indices = block >> 16; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + decompressed[j * pixelSize] = alpha[indices & 0x07]; + indices >>= 3; + } + decompressed += destinationPitch; + } +} +#endif /* BCDEC_BC4BC5_PRECISE */ + +typedef struct bcdec__bitstream { + unsigned long long low; + unsigned long long high; +} bcdec__bitstream_t; + +static int bcdec__bitstream_read_bits(bcdec__bitstream_t* bstream, int numBits) { + unsigned int mask = (1 << numBits) - 1; + /* Read the low N bits */ + unsigned int bits = (bstream->low & mask); + + bstream->low >>= numBits; + /* Put the low N bits of "high" into the high 64-N bits of "low". */ + bstream->low |= (bstream->high & mask) << (sizeof(bstream->high) * 8 - numBits); + bstream->high >>= numBits; + + return bits; +} + +static int bcdec__bitstream_read_bit(bcdec__bitstream_t* bstream) { + return bcdec__bitstream_read_bits(bstream, 1); +} + +/* reversed bits pulling, used in BC6H decoding + why ?? just why ??? */ +static int bcdec__bitstream_read_bits_r(bcdec__bitstream_t* bstream, int numBits) { + int bits = bcdec__bitstream_read_bits(bstream, numBits); + /* Reverse the bits. */ + int result = 0; + while (numBits--) { + result <<= 1; + result |= (bits & 1); + bits >>= 1; + } + return result; +} + + + +BCDECDEF void bcdec_bc1(const void* compressedBlock, void* decompressedBlock, int destinationPitch) { + bcdec__color_block(compressedBlock, decompressedBlock, destinationPitch, 0); +} + +BCDECDEF void bcdec_bc2(const void* compressedBlock, void* decompressedBlock, int destinationPitch) { + bcdec__color_block(((char*)compressedBlock) + 8, decompressedBlock, destinationPitch, 1); + bcdec__sharp_alpha_block(compressedBlock, ((char*)decompressedBlock) + 3, destinationPitch); +} + +BCDECDEF void bcdec_bc3(const void* compressedBlock, void* decompressedBlock, int destinationPitch) { + bcdec__color_block(((char*)compressedBlock) + 8, decompressedBlock, destinationPitch, 1); + bcdec__smooth_alpha_block(compressedBlock, ((char*)decompressedBlock) + 3, destinationPitch, 4); +} + +#ifndef BCDEC_BC4BC5_PRECISE +BCDECDEF void bcdec_bc4(const void* compressedBlock, void* decompressedBlock, int destinationPitch) { + bcdec__smooth_alpha_block(compressedBlock, decompressedBlock, destinationPitch, 1); +#else +BCDECDEF void bcdec_bc4(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned) { + bcdec__bc4_block(compressedBlock, decompressedBlock, destinationPitch, 1, isSigned); +#endif +} + +#ifndef BCDEC_BC4BC5_PRECISE +BCDECDEF void bcdec_bc5(const void* compressedBlock, void* decompressedBlock, int destinationPitch) { + bcdec__smooth_alpha_block(compressedBlock, decompressedBlock, destinationPitch, 2); + bcdec__smooth_alpha_block(((char*)compressedBlock) + 8, ((char*)decompressedBlock) + 1, destinationPitch, 2); +#else +BCDECDEF void bcdec_bc5(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned) { + bcdec__bc4_block(compressedBlock, decompressedBlock, destinationPitch, 2, isSigned); + bcdec__bc4_block(((char*)compressedBlock) + 8, ((char*)decompressedBlock) + 1, destinationPitch, 2, isSigned); +#endif +} + +#ifdef BCDEC_BC4BC5_PRECISE +BCDECDEF void bcdec_bc4_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned) { + bcdec__bc4_block_float(compressedBlock, decompressedBlock, destinationPitch, 1, isSigned); +} + +BCDECDEF void bcdec_bc5_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned) { + bcdec__bc4_block_float(compressedBlock, decompressedBlock, destinationPitch, 2, isSigned); + bcdec__bc4_block_float(((char*)compressedBlock) + 8, ((float*)decompressedBlock) + 1, destinationPitch, 2, isSigned); +} +#endif /* BCDEC_BC4BC5_PRECISE */ + +/* http://graphics.stanford.edu/~seander/bithacks.html#VariableSignExtend */ +static int bcdec__extend_sign(int val, int bits) { + return (val << (32 - bits)) >> (32 - bits); +} + +static int bcdec__transform_inverse(int val, int a0, int bits, int isSigned) { + /* If the precision of A0 is "p" bits, then the transform algorithm is: + B0 = (B0 + A0) & ((1 << p) - 1) */ + val = (val + a0) & ((1 << bits) - 1); + if (isSigned) { + val = bcdec__extend_sign(val, bits); + } + return val; +} + +/* pretty much copy-paste from documentation */ +static int bcdec__unquantize(int val, int bits, int isSigned) { + int unq, s = 0; + + if (!isSigned) { + if (bits >= 15) { + unq = val; + } else if (!val) { + unq = 0; + } else if (val == ((1 << bits) - 1)) { + unq = 0xFFFF; + } else { + unq = ((val << 16) + 0x8000) >> bits; + } + } else { + if (bits >= 16) { + unq = val; + } else { + if (val < 0) { + s = 1; + val = -val; + } + + if (val == 0) { + unq = 0; + } else if (val >= ((1 << (bits - 1)) - 1)) { + unq = 0x7FFF; + } else { + unq = ((val << 15) + 0x4000) >> (bits - 1); + } + + if (s) { + unq = -unq; + } + } + } + return unq; +} + +static int bcdec__interpolate(int a, int b, int* weights, int index) { + return (a * (64 - weights[index]) + b * weights[index] + 32) >> 6; +} + +static unsigned short bcdec__finish_unquantize(int val, int isSigned) { + int s; + + if (!isSigned) { + return (unsigned short)((val * 31) >> 6); /* scale the magnitude by 31 / 64 */ + } else { + val = (val < 0) ? -(((-val) * 31) >> 5) : (val * 31) >> 5; /* scale the magnitude by 31 / 32 */ + s = 0; + if (val < 0) { + s = 0x8000; + val = -val; + } + return (unsigned short)(s | val); + } +} + +/* modified half_to_float_fast4 from https://gist.github.com/rygorous/2144712 */ +static float bcdec__half_to_float_quick(unsigned short half) { + typedef union { + unsigned int u; + float f; + } FP32; + + static const FP32 magic = { 113 << 23 }; + static const unsigned int shifted_exp = 0x7c00 << 13; /* exponent mask after shift */ + FP32 o; + unsigned int exp; + + o.u = (half & 0x7fff) << 13; /* exponent/mantissa bits */ + exp = shifted_exp & o.u; /* just the exponent */ + o.u += (127 - 15) << 23; /* exponent adjust */ + + /* handle exponent special cases */ + if (exp == shifted_exp) { /* Inf/NaN? */ + o.u += (128 - 16) << 23; /* extra exp adjust */ + } else if (exp == 0) { /* Zero/Denormal? */ + o.u += 1 << 23; /* extra exp adjust */ + o.f -= magic.f; /* renormalize */ + } + + o.u |= (half & 0x8000) << 16; /* sign bit */ + return o.f; +} + +BCDECDEF void bcdec_bc6h_half(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned) { + static char actual_bits_count[4][14] = { + { 10, 7, 11, 11, 11, 9, 8, 8, 8, 6, 10, 11, 12, 16 }, /* W */ + { 5, 6, 5, 4, 4, 5, 6, 5, 5, 6, 10, 9, 8, 4 }, /* dR */ + { 5, 6, 4, 5, 4, 5, 5, 6, 5, 6, 10, 9, 8, 4 }, /* dG */ + { 5, 6, 4, 4, 5, 5, 5, 5, 6, 6, 10, 9, 8, 4 } /* dB */ + }; + + /* There are 32 possible partition sets for a two-region tile. + Each 4x4 block represents a single shape. + Here also every fix-up index has MSB bit set. */ + static unsigned char partition_sets[32][4][4] = { + { {128, 0, 1, 1}, {0, 0, 1, 1}, { 0, 0, 1, 1}, {0, 0, 1, 129} }, /* 0 */ + { {128, 0, 0, 1}, {0, 0, 0, 1}, { 0, 0, 0, 1}, {0, 0, 0, 129} }, /* 1 */ + { {128, 1, 1, 1}, {0, 1, 1, 1}, { 0, 1, 1, 1}, {0, 1, 1, 129} }, /* 2 */ + { {128, 0, 0, 1}, {0, 0, 1, 1}, { 0, 0, 1, 1}, {0, 1, 1, 129} }, /* 3 */ + { {128, 0, 0, 0}, {0, 0, 0, 1}, { 0, 0, 0, 1}, {0, 0, 1, 129} }, /* 4 */ + { {128, 0, 1, 1}, {0, 1, 1, 1}, { 0, 1, 1, 1}, {1, 1, 1, 129} }, /* 5 */ + { {128, 0, 0, 1}, {0, 0, 1, 1}, { 0, 1, 1, 1}, {1, 1, 1, 129} }, /* 6 */ + { {128, 0, 0, 0}, {0, 0, 0, 1}, { 0, 0, 1, 1}, {0, 1, 1, 129} }, /* 7 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, { 0, 0, 0, 1}, {0, 0, 1, 129} }, /* 8 */ + { {128, 0, 1, 1}, {0, 1, 1, 1}, { 1, 1, 1, 1}, {1, 1, 1, 129} }, /* 9 */ + { {128, 0, 0, 0}, {0, 0, 0, 1}, { 0, 1, 1, 1}, {1, 1, 1, 129} }, /* 10 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, { 0, 0, 0, 1}, {0, 1, 1, 129} }, /* 11 */ + { {128, 0, 0, 1}, {0, 1, 1, 1}, { 1, 1, 1, 1}, {1, 1, 1, 129} }, /* 12 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, { 1, 1, 1, 1}, {1, 1, 1, 129} }, /* 13 */ + { {128, 0, 0, 0}, {1, 1, 1, 1}, { 1, 1, 1, 1}, {1, 1, 1, 129} }, /* 14 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, { 0, 0, 0, 0}, {1, 1, 1, 129} }, /* 15 */ + { {128, 0, 0, 0}, {1, 0, 0, 0}, { 1, 1, 1, 0}, {1, 1, 1, 129} }, /* 16 */ + { {128, 1, 129, 1}, {0, 0, 0, 1}, { 0, 0, 0, 0}, {0, 0, 0, 0} }, /* 17 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, {129, 0, 0, 0}, {1, 1, 1, 0} }, /* 18 */ + { {128, 1, 129, 1}, {0, 0, 1, 1}, { 0, 0, 0, 1}, {0, 0, 0, 0} }, /* 19 */ + { {128, 0, 129, 1}, {0, 0, 0, 1}, { 0, 0, 0, 0}, {0, 0, 0, 0} }, /* 20 */ + { {128, 0, 0, 0}, {1, 0, 0, 0}, {129, 1, 0, 0}, {1, 1, 1, 0} }, /* 21 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, {129, 0, 0, 0}, {1, 1, 0, 0} }, /* 22 */ + { {128, 1, 1, 1}, {0, 0, 1, 1}, { 0, 0, 1, 1}, {0, 0, 0, 129} }, /* 23 */ + { {128, 0, 129, 1}, {0, 0, 0, 1}, { 0, 0, 0, 1}, {0, 0, 0, 0} }, /* 24 */ + { {128, 0, 0, 0}, {1, 0, 0, 0}, {129, 0, 0, 0}, {1, 1, 0, 0} }, /* 25 */ + { {128, 1, 129, 0}, {0, 1, 1, 0}, { 0, 1, 1, 0}, {0, 1, 1, 0} }, /* 26 */ + { {128, 0, 129, 1}, {0, 1, 1, 0}, { 0, 1, 1, 0}, {1, 1, 0, 0} }, /* 27 */ + { {128, 0, 0, 1}, {0, 1, 1, 1}, {129, 1, 1, 0}, {1, 0, 0, 0} }, /* 28 */ + { {128, 0, 0, 0}, {1, 1, 1, 1}, {129, 1, 1, 1}, {0, 0, 0, 0} }, /* 29 */ + { {128, 1, 129, 1}, {0, 0, 0, 1}, { 1, 0, 0, 0}, {1, 1, 1, 0} }, /* 30 */ + { {128, 0, 129, 1}, {1, 0, 0, 1}, { 1, 0, 0, 1}, {1, 1, 0, 0} } /* 31 */ + }; + + static int aWeight3[8] = { 0, 9, 18, 27, 37, 46, 55, 64 }; + static int aWeight4[16] = { 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64 }; + + bcdec__bitstream_t bstream; + int mode, partition, numPartitions, i, j, partitionSet, indexBits, index, ep_i, actualBits0Mode; + int r[4], g[4], b[4]; /* wxyz */ + unsigned short* decompressed; + int* weights; + + decompressed = (unsigned short*)decompressedBlock; + + bstream.low = bcdec__getu64le(compressedBlock); + bstream.high = bcdec__getu64le((const unsigned char*)compressedBlock + 8); + + r[0] = r[1] = r[2] = r[3] = 0; + g[0] = g[1] = g[2] = g[3] = 0; + b[0] = b[1] = b[2] = b[3] = 0; + + mode = bcdec__bitstream_read_bits(&bstream, 2); + if (mode > 1) { + mode |= (bcdec__bitstream_read_bits(&bstream, 3) << 2); + } + + /* modes >= 11 (10 in my code) are using 0 one, others will read it from the bitstream */ + partition = 0; + + switch (mode) { + /* mode 1 */ + case 0b00: { + /* Partitition indices: 46 bits + Partition: 5 bits + Color Endpoints: 75 bits (10.555, 10.555, 10.555) */ + g[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gy[4] */ + b[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* by[4] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* bz[4] */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* rw[9:0] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* gw[9:0] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* bw[9:0] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* rx[4:0] */ + g[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gz[4] */ + g[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* gy[3:0] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* gx[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream); /* bz[0] */ + g[3] |= bcdec__bitstream_read_bits(&bstream, 4); /* gz[3:0] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* bx[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 1; /* bz[1] */ + b[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* by[3:0] */ + r[2] |= bcdec__bitstream_read_bits(&bstream, 5); /* ry[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 2; /* bz[2] */ + r[3] |= bcdec__bitstream_read_bits(&bstream, 5); /* rz[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 3; /* bz[3] */ + partition = bcdec__bitstream_read_bits(&bstream, 5); /* d[4:0] */ + mode = 0; + } break; + + /* mode 2 */ + case 0b01: { + /* Partitition indices: 46 bits + Partition: 5 bits + Color Endpoints: 75 bits (7666, 7666, 7666) */ + g[2] |= bcdec__bitstream_read_bit(&bstream) << 5; /* gy[5] */ + g[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gz[4] */ + g[3] |= bcdec__bitstream_read_bit(&bstream) << 5; /* gz[5] */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 7); /* rw[6:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream); /* bz[0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 1; /* bz[1] */ + b[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* by[4] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 7); /* gw[6:0] */ + b[2] |= bcdec__bitstream_read_bit(&bstream) << 5; /* by[5] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 2; /* bz[2] */ + g[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gy[4] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 7); /* bw[6:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 3; /* bz[3] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 5; /* bz[5] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* bz[4] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 6); /* rx[5:0] */ + g[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* gy[3:0] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 6); /* gx[5:0] */ + g[3] |= bcdec__bitstream_read_bits(&bstream, 4); /* gz[3:0] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 6); /* bx[5:0] */ + b[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* by[3:0] */ + r[2] |= bcdec__bitstream_read_bits(&bstream, 6); /* ry[5:0] */ + r[3] |= bcdec__bitstream_read_bits(&bstream, 6); /* rz[5:0] */ + partition = bcdec__bitstream_read_bits(&bstream, 5); /* d[4:0] */ + mode = 1; + } break; + + /* mode 3 */ + case 0b00010: { + /* Partitition indices: 46 bits + Partition: 5 bits + Color Endpoints: 72 bits (11.555, 11.444, 11.444) */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* rw[9:0] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* gw[9:0] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* bw[9:0] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* rx[4:0] */ + r[0] |= bcdec__bitstream_read_bit(&bstream) << 10; /* rw[10] */ + g[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* gy[3:0] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 4); /* gx[3:0] */ + g[0] |= bcdec__bitstream_read_bit(&bstream) << 10; /* gw[10] */ + b[3] |= bcdec__bitstream_read_bit(&bstream); /* bz[0] */ + g[3] |= bcdec__bitstream_read_bits(&bstream, 4); /* gz[3:0] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 4); /* bx[3:0] */ + b[0] |= bcdec__bitstream_read_bit(&bstream) << 10; /* bw[10] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 1; /* bz[1] */ + b[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* by[3:0] */ + r[2] |= bcdec__bitstream_read_bits(&bstream, 5); /* ry[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 2; /* bz[2] */ + r[3] |= bcdec__bitstream_read_bits(&bstream, 5); /* rz[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 3; /* bz[3] */ + partition = bcdec__bitstream_read_bits(&bstream, 5); /* d[4:0] */ + mode = 2; + } break; + + /* mode 4 */ + case 0b00110: { + /* Partitition indices: 46 bits + Partition: 5 bits + Color Endpoints: 72 bits (11.444, 11.555, 11.444) */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* rw[9:0] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* gw[9:0] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* bw[9:0] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 4); /* rx[3:0] */ + r[0] |= bcdec__bitstream_read_bit(&bstream) << 10; /* rw[10] */ + g[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gz[4] */ + g[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* gy[3:0] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* gx[4:0] */ + g[0] |= bcdec__bitstream_read_bit(&bstream) << 10; /* gw[10] */ + g[3] |= bcdec__bitstream_read_bits(&bstream, 4); /* gz[3:0] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 4); /* bx[3:0] */ + b[0] |= bcdec__bitstream_read_bit(&bstream) << 10; /* bw[10] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 1; /* bz[1] */ + b[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* by[3:0] */ + r[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* ry[3:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream); /* bz[0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 2; /* bz[2] */ + r[3] |= bcdec__bitstream_read_bits(&bstream, 4); /* rz[3:0] */ + g[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gy[4] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 3; /* bz[3] */ + partition = bcdec__bitstream_read_bits(&bstream, 5); /* d[4:0] */ + mode = 3; + } break; + + /* mode 5 */ + case 0b01010: { + /* Partitition indices: 46 bits + Partition: 5 bits + Color Endpoints: 72 bits (11.444, 11.444, 11.555) */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* rw[9:0] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* gw[9:0] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* bw[9:0] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 4); /* rx[3:0] */ + r[0] |= bcdec__bitstream_read_bit(&bstream) << 10; /* rw[10] */ + b[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* by[4] */ + g[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* gy[3:0] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 4); /* gx[3:0] */ + g[0] |= bcdec__bitstream_read_bit(&bstream) << 10; /* gw[10] */ + b[3] |= bcdec__bitstream_read_bit(&bstream); /* bz[0] */ + g[3] |= bcdec__bitstream_read_bits(&bstream, 4); /* gz[3:0] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* bx[4:0] */ + b[0] |= bcdec__bitstream_read_bit(&bstream) << 10; /* bw[10] */ + b[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* by[3:0] */ + r[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* ry[3:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 1; /* bz[1] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 2; /* bz[2] */ + r[3] |= bcdec__bitstream_read_bits(&bstream, 4); /* rz[3:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* bz[4] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 3; /* bz[3] */ + partition = bcdec__bitstream_read_bits(&bstream, 5); /* d[4:0] */ + mode = 4; + } break; + + /* mode 6 */ + case 0b01110: { + /* Partitition indices: 46 bits + Partition: 5 bits + Color Endpoints: 72 bits (9555, 9555, 9555) */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 9); /* rw[8:0] */ + b[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* by[4] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 9); /* gw[8:0] */ + g[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gy[4] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 9); /* bw[8:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* bz[4] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* rx[4:0] */ + g[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gz[4] */ + g[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* gy[3:0] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* gx[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream); /* bz[0] */ + g[3] |= bcdec__bitstream_read_bits(&bstream, 4); /* gx[3:0] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* bx[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 1; /* bz[1] */ + b[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* by[3:0] */ + r[2] |= bcdec__bitstream_read_bits(&bstream, 5); /* ry[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 2; /* bz[2] */ + r[3] |= bcdec__bitstream_read_bits(&bstream, 5); /* rz[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 3; /* bz[3] */ + partition = bcdec__bitstream_read_bits(&bstream, 5); /* d[4:0] */ + mode = 5; + } break; + + /* mode 7 */ + case 0b10010: { + /* Partitition indices: 46 bits + Partition: 5 bits + Color Endpoints: 72 bits (8666, 8555, 8555) */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 8); /* rw[7:0] */ + g[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gz[4] */ + b[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* by[4] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 8); /* gw[7:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 2; /* bz[2] */ + g[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gy[4] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 8); /* bw[7:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 3; /* bz[3] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* bz[4] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 6); /* rx[5:0] */ + g[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* gy[3:0] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* gx[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream); /* bz[0] */ + g[3] |= bcdec__bitstream_read_bits(&bstream, 4); /* gz[3:0] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* bx[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 1; /* bz[1] */ + b[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* by[3:0] */ + r[2] |= bcdec__bitstream_read_bits(&bstream, 6); /* ry[5:0] */ + r[3] |= bcdec__bitstream_read_bits(&bstream, 6); /* rz[5:0] */ + partition = bcdec__bitstream_read_bits(&bstream, 5); /* d[4:0] */ + mode = 6; + } break; + + /* mode 8 */ + case 0b10110: { + /* Partitition indices: 46 bits + Partition: 5 bits + Color Endpoints: 72 bits (8555, 8666, 8555) */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 8); /* rw[7:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream); /* bz[0] */ + b[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* by[4] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 8); /* gw[7:0] */ + g[2] |= bcdec__bitstream_read_bit(&bstream) << 5; /* gy[5] */ + g[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gy[4] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 8); /* bw[7:0] */ + g[3] |= bcdec__bitstream_read_bit(&bstream) << 5; /* gz[5] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* bz[4] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* rx[4:0] */ + g[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gz[4] */ + g[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* gy[3:0] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 6); /* gx[5:0] */ + g[3] |= bcdec__bitstream_read_bits(&bstream, 4); /* zx[3:0] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* bx[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 1; /* bz[1] */ + b[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* by[3:0] */ + r[2] |= bcdec__bitstream_read_bits(&bstream, 5); /* ry[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 2; /* bz[2] */ + r[3] |= bcdec__bitstream_read_bits(&bstream, 5); /* rz[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 3; /* bz[3] */ + partition = bcdec__bitstream_read_bits(&bstream, 5); /* d[4:0] */ + mode = 7; + } break; + + /* mode 9 */ + case 0b11010: { + /* Partitition indices: 46 bits + Partition: 5 bits + Color Endpoints: 72 bits (8555, 8555, 8666) */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 8); /* rw[7:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 1; /* bz[1] */ + b[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* by[4] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 8); /* gw[7:0] */ + b[2] |= bcdec__bitstream_read_bit(&bstream) << 5; /* by[5] */ + g[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gy[4] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 8); /* bw[7:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 5; /* bz[5] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* bz[4] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* bw[4:0] */ + g[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gz[4] */ + g[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* gy[3:0] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 5); /* gx[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream); /* bz[0] */ + g[3] |= bcdec__bitstream_read_bits(&bstream, 4); /* gz[3:0] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 6); /* bx[5:0] */ + b[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* by[3:0] */ + r[2] |= bcdec__bitstream_read_bits(&bstream, 5); /* ry[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 2; /* bz[2] */ + r[3] |= bcdec__bitstream_read_bits(&bstream, 5); /* rz[4:0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 3; /* bz[3] */ + partition = bcdec__bitstream_read_bits(&bstream, 5); /* d[4:0] */ + mode = 8; + } break; + + /* mode 10 */ + case 0b11110: { + /* Partitition indices: 46 bits + Partition: 5 bits + Color Endpoints: 72 bits (6666, 6666, 6666) */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 6); /* rw[5:0] */ + g[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gz[4] */ + b[3] |= bcdec__bitstream_read_bit(&bstream); /* bz[0] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 1; /* bz[1] */ + b[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* by[4] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 6); /* gw[5:0] */ + g[2] |= bcdec__bitstream_read_bit(&bstream) << 5; /* gy[5] */ + b[2] |= bcdec__bitstream_read_bit(&bstream) << 5; /* by[5] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 2; /* bz[2] */ + g[2] |= bcdec__bitstream_read_bit(&bstream) << 4; /* gy[4] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 6); /* bw[5:0] */ + g[3] |= bcdec__bitstream_read_bit(&bstream) << 5; /* gz[5] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 3; /* bz[3] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 5; /* bz[5] */ + b[3] |= bcdec__bitstream_read_bit(&bstream) << 4; /* bz[4] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 6); /* rx[5:0] */ + g[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* gy[3:0] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 6); /* gx[5:0] */ + g[3] |= bcdec__bitstream_read_bits(&bstream, 4); /* gz[3:0] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 6); /* bx[5:0] */ + b[2] |= bcdec__bitstream_read_bits(&bstream, 4); /* by[3:0] */ + r[2] |= bcdec__bitstream_read_bits(&bstream, 6); /* ry[5:0] */ + r[3] |= bcdec__bitstream_read_bits(&bstream, 6); /* rz[5:0] */ + partition = bcdec__bitstream_read_bits(&bstream, 5); /* d[4:0] */ + mode = 9; + } break; + + /* mode 11 */ + case 0b00011: { + /* Partitition indices: 63 bits + Partition: 0 bits + Color Endpoints: 60 bits (10.10, 10.10, 10.10) */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* rw[9:0] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* gw[9:0] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* bw[9:0] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 10); /* rx[9:0] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 10); /* gx[9:0] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 10); /* bx[9:0] */ + mode = 10; + } break; + + /* mode 12 */ + case 0b00111: { + /* Partitition indices: 63 bits + Partition: 0 bits + Color Endpoints: 60 bits (11.9, 11.9, 11.9) */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* rw[9:0] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* gw[9:0] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* bw[9:0] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 9); /* rx[8:0] */ + r[0] |= bcdec__bitstream_read_bit(&bstream) << 10; /* rw[10] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 9); /* gx[8:0] */ + g[0] |= bcdec__bitstream_read_bit(&bstream) << 10; /* gw[10] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 9); /* bx[8:0] */ + b[0] |= bcdec__bitstream_read_bit(&bstream) << 10; /* bw[10] */ + mode = 11; + } break; + + /* mode 13 */ + case 0b01011: { + /* Partitition indices: 63 bits + Partition: 0 bits + Color Endpoints: 60 bits (12.8, 12.8, 12.8) */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* rw[9:0] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* gw[9:0] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* bw[9:0] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 8); /* rx[7:0] */ + r[0] |= bcdec__bitstream_read_bits_r(&bstream, 2) << 10;/* rx[10:11] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 8); /* gx[7:0] */ + g[0] |= bcdec__bitstream_read_bits_r(&bstream, 2) << 10;/* gx[10:11] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 8); /* bx[7:0] */ + b[0] |= bcdec__bitstream_read_bits_r(&bstream, 2) << 10;/* bx[10:11] */ + mode = 12; + } break; + + /* mode 14 */ + case 0b01111: { + /* Partitition indices: 63 bits + Partition: 0 bits + Color Endpoints: 60 bits (16.4, 16.4, 16.4) */ + r[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* rw[9:0] */ + g[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* gw[9:0] */ + b[0] |= bcdec__bitstream_read_bits(&bstream, 10); /* bw[9:0] */ + r[1] |= bcdec__bitstream_read_bits(&bstream, 4); /* rx[3:0] */ + r[0] |= bcdec__bitstream_read_bits_r(&bstream, 6) << 10;/* rw[10:15] */ + g[1] |= bcdec__bitstream_read_bits(&bstream, 4); /* gx[3:0] */ + g[0] |= bcdec__bitstream_read_bits_r(&bstream, 6) << 10;/* gw[10:15] */ + b[1] |= bcdec__bitstream_read_bits(&bstream, 4); /* bx[3:0] */ + b[0] |= bcdec__bitstream_read_bits_r(&bstream, 6) << 10;/* bw[10:15] */ + mode = 13; + } break; + + default: { + /* Modes 10011, 10111, 11011, and 11111 (not shown) are reserved. + Do not use these in your encoder. If the hardware is passed blocks + with one of these modes specified, the resulting decompressed block + must contain all zeroes in all channels except for the alpha channel. */ + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + decompressed[j * 3 + 0] = 0; + decompressed[j * 3 + 1] = 0; + decompressed[j * 3 + 2] = 0; + } + decompressed += destinationPitch; + } + + return; + } + } + + numPartitions = (mode >= 10) ? 0 : 1; + + actualBits0Mode = actual_bits_count[0][mode]; + if (isSigned) { + r[0] = bcdec__extend_sign(r[0], actualBits0Mode); + g[0] = bcdec__extend_sign(g[0], actualBits0Mode); + b[0] = bcdec__extend_sign(b[0], actualBits0Mode); + } + + /* Mode 11 (like Mode 10) does not use delta compression, + and instead stores both color endpoints explicitly. */ + if ((mode != 9 && mode != 10) || isSigned) { + for (i = 1; i < (numPartitions + 1) * 2; ++i) { + r[i] = bcdec__extend_sign(r[i], actual_bits_count[1][mode]); + g[i] = bcdec__extend_sign(g[i], actual_bits_count[2][mode]); + b[i] = bcdec__extend_sign(b[i], actual_bits_count[3][mode]); + } + } + + if (mode != 9 && mode != 10) { + for (i = 1; i < (numPartitions + 1) * 2; ++i) { + r[i] = bcdec__transform_inverse(r[i], r[0], actualBits0Mode, isSigned); + g[i] = bcdec__transform_inverse(g[i], g[0], actualBits0Mode, isSigned); + b[i] = bcdec__transform_inverse(b[i], b[0], actualBits0Mode, isSigned); + } + } + + for (i = 0; i < (numPartitions + 1) * 2; ++i) { + r[i] = bcdec__unquantize(r[i], actualBits0Mode, isSigned); + g[i] = bcdec__unquantize(g[i], actualBits0Mode, isSigned); + b[i] = bcdec__unquantize(b[i], actualBits0Mode, isSigned); + } + + weights = (mode >= 10) ? aWeight4 : aWeight3; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + partitionSet = (mode >= 10) ? ((i|j) ? 0 : 128) : partition_sets[partition][i][j]; + + indexBits = (mode >= 10) ? 4 : 3; + /* fix-up index is specified with one less bit */ + /* The fix-up index for subset 0 is always index 0 */ + if (partitionSet & 0x80) { + indexBits--; + } + partitionSet &= 0x01; + + index = bcdec__bitstream_read_bits(&bstream, indexBits); + + ep_i = partitionSet * 2; + decompressed[j * 3 + 0] = bcdec__finish_unquantize( + bcdec__interpolate(r[ep_i], r[ep_i+1], weights, index), isSigned); + decompressed[j * 3 + 1] = bcdec__finish_unquantize( + bcdec__interpolate(g[ep_i], g[ep_i+1], weights, index), isSigned); + decompressed[j * 3 + 2] = bcdec__finish_unquantize( + bcdec__interpolate(b[ep_i], b[ep_i+1], weights, index), isSigned); + } + + decompressed += destinationPitch; + } +} + +BCDECDEF void bcdec_bc6h_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned) { + unsigned short block[16*3]; + float* decompressed; + const unsigned short* b; + int i, j; + + bcdec_bc6h_half(compressedBlock, block, 4*3, isSigned); + b = block; + decompressed = (float*)decompressedBlock; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + decompressed[j * 3 + 0] = bcdec__half_to_float_quick(*b++); + decompressed[j * 3 + 1] = bcdec__half_to_float_quick(*b++); + decompressed[j * 3 + 2] = bcdec__half_to_float_quick(*b++); + } + decompressed += destinationPitch; + } +} + +static void bcdec__swap_values(int* a, int* b) { + a[0] ^= b[0], b[0] ^= a[0], a[0] ^= b[0]; +} + +BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, int destinationPitch) { + static char actual_bits_count[2][8] = { + { 4, 6, 5, 7, 5, 7, 7, 5 }, /* RGBA */ + { 0, 0, 0, 0, 6, 8, 7, 5 }, /* Alpha */ + }; + + /* There are 64 possible partition sets for a two-region tile. + Each 4x4 block represents a single shape. + Here also every fix-up index has MSB bit set. */ + static unsigned char partition_sets[2][64][4][4] = { + { /* Partition table for 2-subset BPTC */ + { {128, 0, 1, 1}, {0, 0, 1, 1}, { 0, 0, 1, 1}, {0, 0, 1, 129} }, /* 0 */ + { {128, 0, 0, 1}, {0, 0, 0, 1}, { 0, 0, 0, 1}, {0, 0, 0, 129} }, /* 1 */ + { {128, 1, 1, 1}, {0, 1, 1, 1}, { 0, 1, 1, 1}, {0, 1, 1, 129} }, /* 2 */ + { {128, 0, 0, 1}, {0, 0, 1, 1}, { 0, 0, 1, 1}, {0, 1, 1, 129} }, /* 3 */ + { {128, 0, 0, 0}, {0, 0, 0, 1}, { 0, 0, 0, 1}, {0, 0, 1, 129} }, /* 4 */ + { {128, 0, 1, 1}, {0, 1, 1, 1}, { 0, 1, 1, 1}, {1, 1, 1, 129} }, /* 5 */ + { {128, 0, 0, 1}, {0, 0, 1, 1}, { 0, 1, 1, 1}, {1, 1, 1, 129} }, /* 6 */ + { {128, 0, 0, 0}, {0, 0, 0, 1}, { 0, 0, 1, 1}, {0, 1, 1, 129} }, /* 7 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, { 0, 0, 0, 1}, {0, 0, 1, 129} }, /* 8 */ + { {128, 0, 1, 1}, {0, 1, 1, 1}, { 1, 1, 1, 1}, {1, 1, 1, 129} }, /* 9 */ + { {128, 0, 0, 0}, {0, 0, 0, 1}, { 0, 1, 1, 1}, {1, 1, 1, 129} }, /* 10 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, { 0, 0, 0, 1}, {0, 1, 1, 129} }, /* 11 */ + { {128, 0, 0, 1}, {0, 1, 1, 1}, { 1, 1, 1, 1}, {1, 1, 1, 129} }, /* 12 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, { 1, 1, 1, 1}, {1, 1, 1, 129} }, /* 13 */ + { {128, 0, 0, 0}, {1, 1, 1, 1}, { 1, 1, 1, 1}, {1, 1, 1, 129} }, /* 14 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, { 0, 0, 0, 0}, {1, 1, 1, 129} }, /* 15 */ + { {128, 0, 0, 0}, {1, 0, 0, 0}, { 1, 1, 1, 0}, {1, 1, 1, 129} }, /* 16 */ + { {128, 1, 129, 1}, {0, 0, 0, 1}, { 0, 0, 0, 0}, {0, 0, 0, 0} }, /* 17 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, {129, 0, 0, 0}, {1, 1, 1, 0} }, /* 18 */ + { {128, 1, 129, 1}, {0, 0, 1, 1}, { 0, 0, 0, 1}, {0, 0, 0, 0} }, /* 19 */ + { {128, 0, 129, 1}, {0, 0, 0, 1}, { 0, 0, 0, 0}, {0, 0, 0, 0} }, /* 20 */ + { {128, 0, 0, 0}, {1, 0, 0, 0}, {129, 1, 0, 0}, {1, 1, 1, 0} }, /* 21 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, {129, 0, 0, 0}, {1, 1, 0, 0} }, /* 22 */ + { {128, 1, 1, 1}, {0, 0, 1, 1}, { 0, 0, 1, 1}, {0, 0, 0, 129} }, /* 23 */ + { {128, 0, 129, 1}, {0, 0, 0, 1}, { 0, 0, 0, 1}, {0, 0, 0, 0} }, /* 24 */ + { {128, 0, 0, 0}, {1, 0, 0, 0}, {129, 0, 0, 0}, {1, 1, 0, 0} }, /* 25 */ + { {128, 1, 129, 0}, {0, 1, 1, 0}, { 0, 1, 1, 0}, {0, 1, 1, 0} }, /* 26 */ + { {128, 0, 129, 1}, {0, 1, 1, 0}, { 0, 1, 1, 0}, {1, 1, 0, 0} }, /* 27 */ + { {128, 0, 0, 1}, {0, 1, 1, 1}, {129, 1, 1, 0}, {1, 0, 0, 0} }, /* 28 */ + { {128, 0, 0, 0}, {1, 1, 1, 1}, {129, 1, 1, 1}, {0, 0, 0, 0} }, /* 29 */ + { {128, 1, 129, 1}, {0, 0, 0, 1}, { 1, 0, 0, 0}, {1, 1, 1, 0} }, /* 30 */ + { {128, 0, 129, 1}, {1, 0, 0, 1}, { 1, 0, 0, 1}, {1, 1, 0, 0} }, /* 31 */ + { {128, 1, 0, 1}, {0, 1, 0, 1}, { 0, 1, 0, 1}, {0, 1, 0, 129} }, /* 32 */ + { {128, 0, 0, 0}, {1, 1, 1, 1}, { 0, 0, 0, 0}, {1, 1, 1, 129} }, /* 33 */ + { {128, 1, 0, 1}, {1, 0, 129, 0}, { 0, 1, 0, 1}, {1, 0, 1, 0} }, /* 34 */ + { {128, 0, 1, 1}, {0, 0, 1, 1}, {129, 1, 0, 0}, {1, 1, 0, 0} }, /* 35 */ + { {128, 0, 129, 1}, {1, 1, 0, 0}, { 0, 0, 1, 1}, {1, 1, 0, 0} }, /* 36 */ + { {128, 1, 0, 1}, {0, 1, 0, 1}, {129, 0, 1, 0}, {1, 0, 1, 0} }, /* 37 */ + { {128, 1, 1, 0}, {1, 0, 0, 1}, { 0, 1, 1, 0}, {1, 0, 0, 129} }, /* 38 */ + { {128, 1, 0, 1}, {1, 0, 1, 0}, { 1, 0, 1, 0}, {0, 1, 0, 129} }, /* 39 */ + { {128, 1, 129, 1}, {0, 0, 1, 1}, { 1, 1, 0, 0}, {1, 1, 1, 0} }, /* 40 */ + { {128, 0, 0, 1}, {0, 0, 1, 1}, {129, 1, 0, 0}, {1, 0, 0, 0} }, /* 41 */ + { {128, 0, 129, 1}, {0, 0, 1, 0}, { 0, 1, 0, 0}, {1, 1, 0, 0} }, /* 42 */ + { {128, 0, 129, 1}, {1, 0, 1, 1}, { 1, 1, 0, 1}, {1, 1, 0, 0} }, /* 43 */ + { {128, 1, 129, 0}, {1, 0, 0, 1}, { 1, 0, 0, 1}, {0, 1, 1, 0} }, /* 44 */ + { {128, 0, 1, 1}, {1, 1, 0, 0}, { 1, 1, 0, 0}, {0, 0, 1, 129} }, /* 45 */ + { {128, 1, 1, 0}, {0, 1, 1, 0}, { 1, 0, 0, 1}, {1, 0, 0, 129} }, /* 46 */ + { {128, 0, 0, 0}, {0, 1, 129, 0}, { 0, 1, 1, 0}, {0, 0, 0, 0} }, /* 47 */ + { {128, 1, 0, 0}, {1, 1, 129, 0}, { 0, 1, 0, 0}, {0, 0, 0, 0} }, /* 48 */ + { {128, 0, 129, 0}, {0, 1, 1, 1}, { 0, 0, 1, 0}, {0, 0, 0, 0} }, /* 49 */ + { {128, 0, 0, 0}, {0, 0, 129, 0}, { 0, 1, 1, 1}, {0, 0, 1, 0} }, /* 50 */ + { {128, 0, 0, 0}, {0, 1, 0, 0}, {129, 1, 1, 0}, {0, 1, 0, 0} }, /* 51 */ + { {128, 1, 1, 0}, {1, 1, 0, 0}, { 1, 0, 0, 1}, {0, 0, 1, 129} }, /* 52 */ + { {128, 0, 1, 1}, {0, 1, 1, 0}, { 1, 1, 0, 0}, {1, 0, 0, 129} }, /* 53 */ + { {128, 1, 129, 0}, {0, 0, 1, 1}, { 1, 0, 0, 1}, {1, 1, 0, 0} }, /* 54 */ + { {128, 0, 129, 1}, {1, 0, 0, 1}, { 1, 1, 0, 0}, {0, 1, 1, 0} }, /* 55 */ + { {128, 1, 1, 0}, {1, 1, 0, 0}, { 1, 1, 0, 0}, {1, 0, 0, 129} }, /* 56 */ + { {128, 1, 1, 0}, {0, 0, 1, 1}, { 0, 0, 1, 1}, {1, 0, 0, 129} }, /* 57 */ + { {128, 1, 1, 1}, {1, 1, 1, 0}, { 1, 0, 0, 0}, {0, 0, 0, 129} }, /* 58 */ + { {128, 0, 0, 1}, {1, 0, 0, 0}, { 1, 1, 1, 0}, {0, 1, 1, 129} }, /* 59 */ + { {128, 0, 0, 0}, {1, 1, 1, 1}, { 0, 0, 1, 1}, {0, 0, 1, 129} }, /* 60 */ + { {128, 0, 129, 1}, {0, 0, 1, 1}, { 1, 1, 1, 1}, {0, 0, 0, 0} }, /* 61 */ + { {128, 0, 129, 0}, {0, 0, 1, 0}, { 1, 1, 1, 0}, {1, 1, 1, 0} }, /* 62 */ + { {128, 1, 0, 0}, {0, 1, 0, 0}, { 0, 1, 1, 1}, {0, 1, 1, 129} } /* 63 */ + }, + { /* Partition table for 3-subset BPTC */ + { {128, 0, 1, 129}, {0, 0, 1, 1}, { 0, 2, 2, 1}, { 2, 2, 2, 130} }, /* 0 */ + { {128, 0, 0, 129}, {0, 0, 1, 1}, {130, 2, 1, 1}, { 2, 2, 2, 1} }, /* 1 */ + { {128, 0, 0, 0}, {2, 0, 0, 1}, {130, 2, 1, 1}, { 2, 2, 1, 129} }, /* 2 */ + { {128, 2, 2, 130}, {0, 0, 2, 2}, { 0, 0, 1, 1}, { 0, 1, 1, 129} }, /* 3 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, {129, 1, 2, 2}, { 1, 1, 2, 130} }, /* 4 */ + { {128, 0, 1, 129}, {0, 0, 1, 1}, { 0, 0, 2, 2}, { 0, 0, 2, 130} }, /* 5 */ + { {128, 0, 2, 130}, {0, 0, 2, 2}, { 1, 1, 1, 1}, { 1, 1, 1, 129} }, /* 6 */ + { {128, 0, 1, 1}, {0, 0, 1, 1}, {130, 2, 1, 1}, { 2, 2, 1, 129} }, /* 7 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, {129, 1, 1, 1}, { 2, 2, 2, 130} }, /* 8 */ + { {128, 0, 0, 0}, {1, 1, 1, 1}, {129, 1, 1, 1}, { 2, 2, 2, 130} }, /* 9 */ + { {128, 0, 0, 0}, {1, 1, 129, 1}, { 2, 2, 2, 2}, { 2, 2, 2, 130} }, /* 10 */ + { {128, 0, 1, 2}, {0, 0, 129, 2}, { 0, 0, 1, 2}, { 0, 0, 1, 130} }, /* 11 */ + { {128, 1, 1, 2}, {0, 1, 129, 2}, { 0, 1, 1, 2}, { 0, 1, 1, 130} }, /* 12 */ + { {128, 1, 2, 2}, {0, 129, 2, 2}, { 0, 1, 2, 2}, { 0, 1, 2, 130} }, /* 13 */ + { {128, 0, 1, 129}, {0, 1, 1, 2}, { 1, 1, 2, 2}, { 1, 2, 2, 130} }, /* 14 */ + { {128, 0, 1, 129}, {2, 0, 0, 1}, {130, 2, 0, 0}, { 2, 2, 2, 0} }, /* 15 */ + { {128, 0, 0, 129}, {0, 0, 1, 1}, { 0, 1, 1, 2}, { 1, 1, 2, 130} }, /* 16 */ + { {128, 1, 1, 129}, {0, 0, 1, 1}, {130, 0, 0, 1}, { 2, 2, 0, 0} }, /* 17 */ + { {128, 0, 0, 0}, {1, 1, 2, 2}, {129, 1, 2, 2}, { 1, 1, 2, 130} }, /* 18 */ + { {128, 0, 2, 130}, {0, 0, 2, 2}, { 0, 0, 2, 2}, { 1, 1, 1, 129} }, /* 19 */ + { {128, 1, 1, 129}, {0, 1, 1, 1}, { 0, 2, 2, 2}, { 0, 2, 2, 130} }, /* 20 */ + { {128, 0, 0, 129}, {0, 0, 0, 1}, {130, 2, 2, 1}, { 2, 2, 2, 1} }, /* 21 */ + { {128, 0, 0, 0}, {0, 0, 129, 1}, { 0, 1, 2, 2}, { 0, 1, 2, 130} }, /* 22 */ + { {128, 0, 0, 0}, {1, 1, 0, 0}, {130, 2, 129, 0}, { 2, 2, 1, 0} }, /* 23 */ + { {128, 1, 2, 130}, {0, 129, 2, 2}, { 0, 0, 1, 1}, { 0, 0, 0, 0} }, /* 24 */ + { {128, 0, 1, 2}, {0, 0, 1, 2}, {129, 1, 2, 2}, { 2, 2, 2, 130} }, /* 25 */ + { {128, 1, 1, 0}, {1, 2, 130, 1}, {129, 2, 2, 1}, { 0, 1, 1, 0} }, /* 26 */ + { {128, 0, 0, 0}, {0, 1, 129, 0}, { 1, 2, 130, 1}, { 1, 2, 2, 1} }, /* 27 */ + { {128, 0, 2, 2}, {1, 1, 0, 2}, {129, 1, 0, 2}, { 0, 0, 2, 130} }, /* 28 */ + { {128, 1, 1, 0}, {0, 129, 1, 0}, { 2, 0, 0, 2}, { 2, 2, 2, 130} }, /* 29 */ + { {128, 0, 1, 1}, {0, 1, 2, 2}, { 0, 1, 130, 2}, { 0, 0, 1, 129} }, /* 30 */ + { {128, 0, 0, 0}, {2, 0, 0, 0}, {130, 2, 1, 1}, { 2, 2, 2, 129} }, /* 31 */ + { {128, 0, 0, 0}, {0, 0, 0, 2}, {129, 1, 2, 2}, { 1, 2, 2, 130} }, /* 32 */ + { {128, 2, 2, 130}, {0, 0, 2, 2}, { 0, 0, 1, 2}, { 0, 0, 1, 129} }, /* 33 */ + { {128, 0, 1, 129}, {0, 0, 1, 2}, { 0, 0, 2, 2}, { 0, 2, 2, 130} }, /* 34 */ + { {128, 1, 2, 0}, {0, 129, 2, 0}, { 0, 1, 130, 0}, { 0, 1, 2, 0} }, /* 35 */ + { {128, 0, 0, 0}, {1, 1, 129, 1}, { 2, 2, 130, 2}, { 0, 0, 0, 0} }, /* 36 */ + { {128, 1, 2, 0}, {1, 2, 0, 1}, {130, 0, 129, 2}, { 0, 1, 2, 0} }, /* 37 */ + { {128, 1, 2, 0}, {2, 0, 1, 2}, {129, 130, 0, 1}, { 0, 1, 2, 0} }, /* 38 */ + { {128, 0, 1, 1}, {2, 2, 0, 0}, { 1, 1, 130, 2}, { 0, 0, 1, 129} }, /* 39 */ + { {128, 0, 1, 1}, {1, 1, 130, 2}, { 2, 2, 0, 0}, { 0, 0, 1, 129} }, /* 40 */ + { {128, 1, 0, 129}, {0, 1, 0, 1}, { 2, 2, 2, 2}, { 2, 2, 2, 130} }, /* 41 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, {130, 1, 2, 1}, { 2, 1, 2, 129} }, /* 42 */ + { {128, 0, 2, 2}, {1, 129, 2, 2}, { 0, 0, 2, 2}, { 1, 1, 2, 130} }, /* 43 */ + { {128, 0, 2, 130}, {0, 0, 1, 1}, { 0, 0, 2, 2}, { 0, 0, 1, 129} }, /* 44 */ + { {128, 2, 2, 0}, {1, 2, 130, 1}, { 0, 2, 2, 0}, { 1, 2, 2, 129} }, /* 45 */ + { {128, 1, 0, 1}, {2, 2, 130, 2}, { 2, 2, 2, 2}, { 0, 1, 0, 129} }, /* 46 */ + { {128, 0, 0, 0}, {2, 1, 2, 1}, {130, 1, 2, 1}, { 2, 1, 2, 129} }, /* 47 */ + { {128, 1, 0, 129}, {0, 1, 0, 1}, { 0, 1, 0, 1}, { 2, 2, 2, 130} }, /* 48 */ + { {128, 2, 2, 130}, {0, 1, 1, 1}, { 0, 2, 2, 2}, { 0, 1, 1, 129} }, /* 49 */ + { {128, 0, 0, 2}, {1, 129, 1, 2}, { 0, 0, 0, 2}, { 1, 1, 1, 130} }, /* 50 */ + { {128, 0, 0, 0}, {2, 129, 1, 2}, { 2, 1, 1, 2}, { 2, 1, 1, 130} }, /* 51 */ + { {128, 2, 2, 2}, {0, 129, 1, 1}, { 0, 1, 1, 1}, { 0, 2, 2, 130} }, /* 52 */ + { {128, 0, 0, 2}, {1, 1, 1, 2}, {129, 1, 1, 2}, { 0, 0, 0, 130} }, /* 53 */ + { {128, 1, 1, 0}, {0, 129, 1, 0}, { 0, 1, 1, 0}, { 2, 2, 2, 130} }, /* 54 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, { 2, 1, 129, 2}, { 2, 1, 1, 130} }, /* 55 */ + { {128, 1, 1, 0}, {0, 129, 1, 0}, { 2, 2, 2, 2}, { 2, 2, 2, 130} }, /* 56 */ + { {128, 0, 2, 2}, {0, 0, 1, 1}, { 0, 0, 129, 1}, { 0, 0, 2, 130} }, /* 57 */ + { {128, 0, 2, 2}, {1, 1, 2, 2}, {129, 1, 2, 2}, { 0, 0, 2, 130} }, /* 58 */ + { {128, 0, 0, 0}, {0, 0, 0, 0}, { 0, 0, 0, 0}, { 2, 129, 1, 130} }, /* 59 */ + { {128, 0, 0, 130}, {0, 0, 0, 1}, { 0, 0, 0, 2}, { 0, 0, 0, 129} }, /* 60 */ + { {128, 2, 2, 2}, {1, 2, 2, 2}, { 0, 2, 2, 2}, {129, 2, 2, 130} }, /* 61 */ + { {128, 1, 0, 129}, {2, 2, 2, 2}, { 2, 2, 2, 2}, { 2, 2, 2, 130} }, /* 62 */ + { {128, 1, 1, 129}, {2, 0, 1, 1}, {130, 2, 0, 1}, { 2, 2, 2, 0} } /* 63 */ + } + }; + + static int aWeight2[] = { 0, 21, 43, 64 }; + static int aWeight3[] = { 0, 9, 18, 27, 37, 46, 55, 64 }; + static int aWeight4[] = { 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64 }; + + static unsigned char sModeHasPBits = 0b11001011; + + bcdec__bitstream_t bstream; + int mode, partition, numPartitions, numEndpoints, i, j, k, rotation, partitionSet; + int indexSelectionBit, indexBits, indexBits2, index, index2; + int endpoints[6][4]; + char indices[4][4]; + int r, g, b, a; + int* weights, * weights2; + unsigned char* decompressed; + + decompressed = (unsigned char*)decompressedBlock; + + bstream.low = bcdec__getu64le(compressedBlock); + bstream.high = bcdec__getu64le((const unsigned char*)compressedBlock + 8); + + for (mode = 0; mode < 8 && (0 == bcdec__bitstream_read_bit(&bstream)); ++mode); + + /* unexpected mode, clear the block (transparent black) */ + if (mode >= 8) { + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + decompressed[j * 4 + 0] = 0; + decompressed[j * 4 + 1] = 0; + decompressed[j * 4 + 2] = 0; + decompressed[j * 4 + 3] = 0; + } + decompressed += destinationPitch; + } + + return; + } + + partition = 0; + numPartitions = 1; + rotation = 0; + indexSelectionBit = 0; + + if (mode == 0 || mode == 1 || mode == 2 || mode == 3 || mode == 7) { + numPartitions = (mode == 0 || mode == 2) ? 3 : 2; + partition = bcdec__bitstream_read_bits(&bstream, (mode == 0) ? 4 : 6); + } + + numEndpoints = numPartitions * 2; + + if (mode == 4 || mode == 5) { + rotation = bcdec__bitstream_read_bits(&bstream, 2); + + if (mode == 4) { + indexSelectionBit = bcdec__bitstream_read_bit(&bstream); + } + } + + /* Extract endpoints */ + /* RGB */ + for (i = 0; i < 3; ++i) { + for (j = 0; j < numEndpoints; ++j) { + endpoints[j][i] = bcdec__bitstream_read_bits(&bstream, actual_bits_count[0][mode]); + } + } + /* Alpha (if any) */ + if (actual_bits_count[1][mode] > 0) { + for (j = 0; j < numEndpoints; ++j) { + endpoints[j][3] = bcdec__bitstream_read_bits(&bstream, actual_bits_count[1][mode]); + } + } + + /* Fully decode endpoints */ + /* First handle modes that have P-bits */ + if (mode == 0 || mode == 1 || mode == 3 || mode == 6 || mode == 7) { + for (i = 0; i < numEndpoints; ++i) { + /* component-wise left-shift */ + for (j = 0; j < 4; ++j) { + endpoints[i][j] <<= 1; + } + } + + /* if P-bit is shared */ + if (mode == 1) { + i = bcdec__bitstream_read_bit(&bstream); + j = bcdec__bitstream_read_bit(&bstream); + + /* rgb component-wise insert pbits */ + for (k = 0; k < 3; ++k) { + endpoints[0][k] |= i; + endpoints[1][k] |= i; + endpoints[2][k] |= j; + endpoints[3][k] |= j; + } + } else if (sModeHasPBits & (1 << mode)) { + /* unique P-bit per endpoint */ + for (i = 0; i < numEndpoints; ++i) { + j = bcdec__bitstream_read_bit(&bstream); + for (k = 0; k < 4; ++k) { + endpoints[i][k] |= j; + } + } + } + } + + for (i = 0; i < numEndpoints; ++i) { + /* get color components precision including pbit */ + j = actual_bits_count[0][mode] + ((sModeHasPBits >> mode) & 1); + + for (k = 0; k < 3; ++k) { + /* left shift endpoint components so that their MSB lies in bit 7 */ + endpoints[i][k] = endpoints[i][k] << (8 - j); + /* Replicate each component's MSB into the LSBs revealed by the left-shift operation above */ + endpoints[i][k] = endpoints[i][k] | (endpoints[i][k] >> j); + } + + /* get alpha component precision including pbit */ + j = actual_bits_count[1][mode] + ((sModeHasPBits >> mode) & 1); + + /* left shift endpoint components so that their MSB lies in bit 7 */ + endpoints[i][3] = endpoints[i][3] << (8 - j); + /* Replicate each component's MSB into the LSBs revealed by the left-shift operation above */ + endpoints[i][3] = endpoints[i][3] | (endpoints[i][3] >> j); + } + + /* If this mode does not explicitly define the alpha component */ + /* set alpha equal to 1.0 */ + if (!actual_bits_count[1][mode]) { + for (j = 0; j < numEndpoints; ++j) { + endpoints[j][3] = 0xFF; + } + } + + /* Determine weights tables */ + indexBits = (mode == 0 || mode == 1) ? 3 : ((mode == 6) ? 4 : 2); + indexBits2 = (mode == 4) ? 3 : ((mode == 5) ? 2 : 0); + weights = (indexBits == 2) ? aWeight2 : ((indexBits == 3) ? aWeight3 : aWeight4); + weights2 = (indexBits2 == 2) ? aWeight2 : aWeight3; + + /* Quite inconvenient that indices aren't interleaved so we have to make 2 passes here */ + /* Pass #1: collecting color indices */ + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + partitionSet = (numPartitions == 1) ? ((i | j) ? 0 : 128) : partition_sets[numPartitions - 2][partition][i][j]; + + indexBits = (mode == 0 || mode == 1) ? 3 : ((mode == 6) ? 4 : 2); + /* fix-up index is specified with one less bit */ + /* The fix-up index for subset 0 is always index 0 */ + if (partitionSet & 0x80) { + indexBits--; + } + + indices[i][j] = bcdec__bitstream_read_bits(&bstream, indexBits); + } + } + + /* Pass #2: reading alpha indices (if any) and interpolating & rotating */ + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + partitionSet = (numPartitions == 1) ? ((i|j) ? 0 : 128) : partition_sets[numPartitions - 2][partition][i][j]; + partitionSet &= 0x03; + + index = indices[i][j]; + + if (!indexBits2) { + r = bcdec__interpolate(endpoints[partitionSet * 2][0], endpoints[partitionSet * 2 + 1][0], weights, index); + g = bcdec__interpolate(endpoints[partitionSet * 2][1], endpoints[partitionSet * 2 + 1][1], weights, index); + b = bcdec__interpolate(endpoints[partitionSet * 2][2], endpoints[partitionSet * 2 + 1][2], weights, index); + a = bcdec__interpolate(endpoints[partitionSet * 2][3], endpoints[partitionSet * 2 + 1][3], weights, index); + } else { + index2 = bcdec__bitstream_read_bits(&bstream, (i|j) ? indexBits2 : (indexBits2 - 1)); + /* The index value for interpolating color comes from the secondary index bits for the texel + if the mode has an index selection bit and its value is one, and from the primary index bits otherwise. + The alpha index comes from the secondary index bits if the block has a secondary index and + the block either doesn’t have an index selection bit or that bit is zero, and from the primary index bits otherwise. */ + if (!indexSelectionBit) { + r = bcdec__interpolate(endpoints[partitionSet * 2][0], endpoints[partitionSet * 2 + 1][0], weights, index); + g = bcdec__interpolate(endpoints[partitionSet * 2][1], endpoints[partitionSet * 2 + 1][1], weights, index); + b = bcdec__interpolate(endpoints[partitionSet * 2][2], endpoints[partitionSet * 2 + 1][2], weights, index); + a = bcdec__interpolate(endpoints[partitionSet * 2][3], endpoints[partitionSet * 2 + 1][3], weights2, index2); + } else { + r = bcdec__interpolate(endpoints[partitionSet * 2][0], endpoints[partitionSet * 2 + 1][0], weights2, index2); + g = bcdec__interpolate(endpoints[partitionSet * 2][1], endpoints[partitionSet * 2 + 1][1], weights2, index2); + b = bcdec__interpolate(endpoints[partitionSet * 2][2], endpoints[partitionSet * 2 + 1][2], weights2, index2); + a = bcdec__interpolate(endpoints[partitionSet * 2][3], endpoints[partitionSet * 2 + 1][3], weights, index); + } + } + + switch (rotation) { + case 1: { /* 01 – Block format is Scalar(R) Vector(AGB) - swap A and R */ + bcdec__swap_values(&a, &r); + } break; + case 2: { /* 10 – Block format is Scalar(G) Vector(RAB) - swap A and G */ + bcdec__swap_values(&a, &g); + } break; + case 3: { /* 11 - Block format is Scalar(B) Vector(RGA) - swap A and B */ + bcdec__swap_values(&a, &b); + } break; + } + + decompressed[j * 4 + 0] = r; + decompressed[j * 4 + 1] = g; + decompressed[j * 4 + 2] = b; + decompressed[j * 4 + 3] = a; + } + + decompressed += destinationPitch; + } +} + +#endif /* BCDEC_IMPLEMENTATION */ + +/* LICENSE: + +This software is available under 2 licenses -- choose whichever you prefer. + +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License + +Copyright (c) 2022 Sergii Kudlai + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------ +ALTERNATIVE B - The Unlicense + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +*/ diff --git a/port/tools/mcc_cache_report.c b/port/tools/mcc_cache_report.c new file mode 100644 index 0000000000..9e975a5066 --- /dev/null +++ b/port/tools/mcc_cache_report.c @@ -0,0 +1,50 @@ +/* Standalone audit of a Halo 1 MCC cache. A successful audit is not a claim + * that the cache has been converted for the Xbox runtime. */ +#include "mcc_cache_format.h" +#include +#include + +static int read_file(void *context, uint32_t offset, uint32_t size, void *out) { + FILE *file=(FILE *)context; + return fseek(file,(long)offset,SEEK_SET)==0 && fread(out,1,size,file)==size; +} + +int main(int argc, char **argv) { + struct mcc_cache_source source; + struct mcc_cache_report report; + enum mcc_cache_status status; + FILE *file; + long length; + unsigned i; + if (argc!=2) { fprintf(stderr,"usage: mcc_cache_report FILE.map\n"); return 2; } + file=fopen(argv[1],"rb"); + if (!file) { fprintf(stderr,"cannot open map\n"); return 2; } + if (fseek(file,0,SEEK_END)!=0 || (length=ftell(file))<0) { + fclose(file); fprintf(stderr,"cannot measure map\n"); return 2; + } + source.context=file; source.read=read_file; source.size=(uint64_t)length; + status=mcc_cache_inspect(&source,&report); + printf("status=%s\n",mcc_cache_status_string(status)); + printf("problem_tag=%u\nproblem_location=0x%08X\n",report.problem_tag,report.problem_location); + if (report.identity.tag_data_size) { + printf("name=%s\nbuild=%s\nscenario_type=%u\nflags=0x%04X\n", + report.identity.name,report.identity.build,report.identity.scenario_type,report.identity.flags); + printf("tag_base=0x%08X\ntag_bytes=%u\ntag_count=%u\n",report.tag_base, + report.identity.tag_data_size,report.tag_count); + printf("models=%u\nmodel_bytes=%u\nbsps=%u\nbsp_materials=%u\n", + report.model_count,report.model_bytes,report.bsp_count,report.bsp_material_count); + printf("bitmaps=%u\nbc7_bitmaps=%u\nexternal_bitmaps=%u\n", + report.bitmap_count,report.bc7_bitmap_count,report.external_bitmap_count); + printf("sound_permutations=%u\nexternal_sounds=%u\ngeneric_shaders=%u\n", + report.sound_permutation_count,report.external_sound_count,report.generic_shader_count); + printf("scripts=%u\nglobals=%u\nscript_node_capacity=%u\nparameterized_scripts=%u\n", + report.script_count,report.global_count,report.script_node_count,report.parameterized_script_count); + for (i=0;ifile_offset,b->size,b->address,b->vertex_file_offset,b->vertex_bytes); + } + } + fclose(file); + return status==MCC_CACHE_OK ? 0 : 1; +} diff --git a/port/tools/mcc_geometry_check.c b/port/tools/mcc_geometry_check.c new file mode 100644 index 0000000000..4b0cda4c23 --- /dev/null +++ b/port/tools/mcc_geometry_check.c @@ -0,0 +1,160 @@ +/* Headless MCC geometry checks. Uses the real engine vertex compressor and + * mock GPU handles; it does not prove rendering or gameplay compatibility. */ +#include "cseries.h" +#include "errors.h" +#include "rasterizer/rasterizer_geometry.h" +#include "mcc_runtime.h" +#include +#include +#include +#include + +/* The game maps allocations and memory copies through cseries. Keep those + * calls present in the adapter under test, backed by CRT storage here. */ +#undef malloc +#undef free +#undef memset +#undef memcpy +#undef fopen +void *debug_malloc(unsigned int bytes,boolean clear,char const *file,long line) { + void *p=malloc(bytes); (void)file; (void)line; + if (p && clear) memset(p,0,bytes); + return p; +} +void debug_free(void *p,char const *file,long line) { + if (!p) { fprintf(stderr,"invalid NULL debug_free (%s:%ld)\n",file,line); exit(12); } + free(p); +} +void *csmemset(void *p,long value,unsigned long bytes) { return memset(p,(int)value,bytes); } +void *csmemcpy(void *p,void const *source,unsigned long bytes) { return memcpy(p,source,bytes); } + +#ifdef _WIN32 +__declspec(dllimport) void *__stdcall VirtualAlloc(void *,size_t,unsigned long,unsigned long); +__declspec(dllimport) int __stdcall VirtualFree(void *,size_t,unsigned long); +#else +#include +#endif + +char temporary[256]; +static int active_buffers,created_buffers,fail_at=-1; +static struct vertex_buffer *model_buffers[8192]; +static unsigned model_buffer_count; + +void error(short priority,char const *format,...) { + va_list args; (void)priority; + va_start(args,format); vfprintf(stderr,format,args); va_end(args); fputc('\n',stderr); +} +char *csprintf(char *buffer,char *format,...) { + va_list args; va_start(args,format); vsnprintf(buffer,256,format,args); va_end(args); return buffer; +} +void display_assert(char *information,char *file,long line,boolean fatal) { + fprintf(stderr,"assertion: %s (%s:%ld)\n",information,file,line); if (fatal) exit(3); +} +void system_exit(long code) { exit((int)code); } + +boolean rasterizer_vertex_buffer_new(struct vertex_buffer *b,long type,long count, + void const *vertices,long bytes) { + if (created_buffers==fail_at) return FALSE; + if (count<=0 || bytes!=count*rasterizer_geometry_get_vertex_size((short)type)) exit(4); + b->hardware_format=malloc(1); if (!b->hardware_format) exit(5); + b->type=(short)type; b->count=count; b->offset=0; b->base_address=(void *)vertices; + if (model_buffer_count==8192) exit(9); + model_buffers[model_buffer_count++]=b; + active_buffers++; created_buffers++; return TRUE; +} +void rasterizer_vertex_buffer_delete(struct vertex_buffer *b) { + if (b->hardware_format) { free(b->hardware_format); b->hardware_format=NULL; active_buffers--; } +} +boolean rasterizer_triangle_buffer_new(struct triangle_buffer *b,short type,long count,void const *data) { + if (created_buffers==fail_at) return FALSE; + if (type!=1 || count<=0 || !data) exit(6); + b->hardware_format=malloc(1); if (!b->hardware_format) exit(5); + b->type=type; b->count=count; b->base_address=(void *)data; + active_buffers++; created_buffers++; return TRUE; +} +void rasterizer_triangle_buffer_delete(struct triangle_buffer *b) { + if (b->hardware_format) { free(b->hardware_format); b->hardware_format=NULL; active_buffers--; } +} + +static int read_file(void *context,uint32_t offset,uint32_t size,void *out) { + return !fseek(context,(long)offset,SEEK_SET) && fread(out,1,size,context)==size; +} +int mcc_runtime_read(struct mcc_runtime *r,uint32_t offset,uint32_t bytes,void *out) { + return offset<=r->source.size && bytes<=r->source.size-offset && r->source.read(r->source.context,offset,bytes,out); +} +void *mcc_runtime_pointer(struct mcc_runtime *r,uint32_t address,uint32_t bytes) { + if (addressreport.tag_base || address-r->report.tag_base>r->used || + bytes>r->used-(address-r->report.tag_base)) return NULL; + return r->tags+(address-r->report.tag_base); +} +void *mcc_runtime_allocate(struct mcc_runtime *r,uint32_t bytes) { + uint32_t i,start=(r->used+3)&~3u,limit=r->capacity; + for (i=0;ireport.bsp_count;i++) { + uint32_t b=r->report.bsps[i].address-r->report.tag_base; + if (blimit || bytes>limit-start) return NULL; + r->used=start+bytes; memset(r->tags+start,0,bytes); return r->tags+start; +} + +int main(int argc,char **argv) { + struct mcc_runtime r; + FILE *file; + long length; + uint32_t i; + int success, palette_parts=0, palette_sum=0; + if (argc<2 || argc>3) return 2; + if (argc==3) fail_at=atoi(argv[2]); + memset(&r,0,sizeof(r)); + file=fopen(argv[1],"rb"); if (!file) return 2; + fseek(file,0,SEEK_END); length=ftell(file); if (length<0) return 2; + r.source.context=file; r.source.read=read_file; r.source.size=(uint64_t)length; + if (mcc_cache_inspect(&r.source,&r.report)!=MCC_CACHE_OK) return 2; + r.capacity=MCC_CACHE_TAG_CAPACITY; +#ifdef _WIN32 + r.tags=VirtualAlloc((void *)(uintptr_t)r.report.tag_base,r.capacity,0x3000,4); +#else + r.tags=mmap((void *)(uintptr_t)r.report.tag_base,r.capacity,PROT_READ|PROT_WRITE, + MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED_NOREPLACE,-1,0); +#endif + if ((uintptr_t)r.tags!=r.report.tag_base) return 2; + if (mcc_cache_load(&r.source,r.tags,r.capacity,&r.report)!=MCC_CACHE_OK) return 2; + r.used=r.report.identity.tag_data_size; + r.tag_index=r.tags+0x28; + success=mcc_geometry_prepare(&r); + /* The validator must see precisely the buffer's original payload and + * reject an index/vertex identity mismatch. Palette indices remain + * inspectable after all models have been converted. */ + for (i=0;success && ihardware_format,0,&bytes); + if (!payload || payload!=b->base_address || bytes!=(unsigned long)b->count*32 || + !mcc_geometry_contains(&r,payload,bytes) || + mcc_geometry_buffer_data(&r,b->hardware_format,1,NULL)) return 10; + n=mcc_geometry_part_palette(&r,b,&nodes); + if (n) palette_parts++; + for (j=0;jtag_handle,b->file_offset,b->size, + (void *)(uintptr_t)b->address,&structure); + if (success && !structure) return 7; + mcc_geometry_bsp_unload(&r); + } + mcc_geometry_dispose(&r); + printf("converted=%d\ncreated_buffers=%d\nactive_buffers=%d\npalette_parts=%d\npalette_sum=%d\n", + success,created_buffers,active_buffers,palette_parts,palette_sum); + if (active_buffers || r.geometry) return 8; +#ifdef _WIN32 + VirtualFree(r.tags,0,0x8000); +#else + munmap(r.tags,r.capacity); +#endif + fclose(file); + return success ? 0 : 1; +} diff --git a/source/ai/actors.c b/source/ai/actors.c index b8a422a316..b980ebfe3e 100644 --- a/source/ai/actors.c +++ b/source/ai/actors.c @@ -281,6 +281,7 @@ symbols in this file: /* ---------- headers */ #include "cseries.h" +#include "mcc_grenades.h" #include "actors.h" #include "actor_definitions.h" #include "actor_placement.h" @@ -3367,6 +3368,8 @@ void actor_died( grenade_roll < actor_variant_definition->items.dont_drop_grenades_chance) { csmemset(unit->unit.grenade_counts, 0, sizeof(unit->unit.grenade_counts)); + if (mcc_grenades_active()) + mcc_grenades_clear(actor->meta.unit_index); } if (weapon_index != NONE) diff --git a/source/cache/cache_files.c b/source/cache/cache_files.c index 9db305c56f..14464e05c7 100644 --- a/source/cache/cache_files.c +++ b/source/cache/cache_files.c @@ -136,6 +136,7 @@ symbols in this file: #include "sound/sound_manager.h" #include "tag_schema.h" #include "custom_edition_cache.h" /* port: port/linux/game/custom_edition_cache.c */ +#include "mcc_cache.h" /* independent MCC dispatch */ #include "cache_file_formats.h" /* port: CUSTOM_EDITION_TAG_CACHE_BYTES */ /* ---------- constants */ @@ -371,6 +372,9 @@ boolean cache_file_tag_cache_contains( void const *tag_cache = physical_memory_get_tag_cache_base_address(); unsigned long tag_cache_size = TAG_CACHE_SIZE; + if (mcc_cache_tags_loaded()) + return mcc_cache_contains(address, size); + if (custom_edition_cache_tags_loaded()) { tag_cache = halo_custom_edition_tag_cache(); @@ -606,6 +610,11 @@ void scenario_tags_unload( loose_sounds_tags_unloaded(); } texture_cache_close(); + /* MCC's appended pause table unwinds before the native menu table. */ + { + extern void mcc_pause_runtime_unload(void); + mcc_pause_runtime_unload(); + } /* port: the menus' tags go, and the map's own table comes back (port/linux/game/menu_tags.c): after the texture cache, which writes to the bitmaps it has loaded as it closes, theirs among them */ @@ -617,6 +626,14 @@ void scenario_tags_unload( cache_file_close(); /* port: a Halo Custom Edition map has no Xbox vertex or index buffers (port/linux/game/custom_edition_cache.c) */ + if (mcc_cache_tags_loaded()) + { + mcc_cache_tags_unload(); + cache_file_globals.tags_loaded = FALSE; + global_tag_instances = NULL; + global_tag_count = 0; + return; + } if (custom_edition_cache_tags_loaded()) custom_edition_cache_tags_unload(); else @@ -984,6 +1001,8 @@ boolean cache_files_map_plays_multiplayer( build[0] = 0; if (!map_name || !map_name[0]) return TRUE; + if (mcc_level_name(map_name)) + return TRUE; /* port: a Halo Custom Edition map (custom_maps\) is converted for this build as it loads (port/linux/game/custom_edition_cache.c): its header's build is Halo PC's, not one to check */ @@ -1051,6 +1070,8 @@ builds of other regions play together. */ unsigned long cache_files_map_version( char const *map_name) { + if (mcc_level_name(map_name)) + return mcc_cache_checksum(map_name); return custom_edition_level_name(map_name) ? custom_edition_map_checksum(map_name) : 0; } @@ -1075,6 +1096,8 @@ boolean cache_files_map_present( if (!map_name || !map_name[0]) return TRUE; + if (mcc_level_name(map_name)) + return mcc_cache_require(map_name, version); if (custom_edition_level_name(map_name)) { if (custom_edition_cache_present(map_name, version, message, sizeof(message))) @@ -1121,6 +1144,9 @@ boolean cache_files_give_time_to_precache( { boolean result = FALSE; + if (mcc_level_name(map_name)) + return mcc_cache_require(map_name, 0); + /* port: no map named yet is nothing to precache. A client joining over the internet asks for its multiplayer map (network_game_client_update_precache_status) before the host's settings name it: an empty name, which matched a cache @@ -1174,6 +1200,35 @@ long scenario_tags_load( result = NONE; texture_cache_open(); sound_cache_open(); + if (mcc_level_name(scenario_name)) + { + cache_file_globals.tag_header = mcc_cache_tags_load(scenario_name, &cache_file_globals.header); + if (cache_file_globals.tag_header) + { + extern void menu_tags_loaded(char const *map_name); + extern void loose_sounds_tags_loaded(void); + global_tag_instances = cache_file_globals.tag_header->tag_instances; + global_tag_count = cache_file_globals.tag_header->tag_count; + cache_file_globals.tags_loaded = TRUE; + menu_tags_loaded(scenario_name); + { + extern boolean mcc_pause_runtime_load(void); + if (!mcc_pause_runtime_load()) + { + scenario_tags_unload(); + return NONE; + } + } + loose_sounds_tags_loaded(); + result = cache_file_globals.tag_header->scenario_tag_index; + } + else + { + sound_cache_close(); + texture_cache_close(); + } + return result; + } /* port: a Halo Custom Edition map (custom_maps\) is read in place into a tag cache of its own, converted for this build and checked as its own maps are (port/linux/game/custom_edition_cache.c). It has no Xbox @@ -1330,6 +1385,17 @@ boolean scenario_structure_bsp_load( /* port: the bsp's header, once read and checked */ struct cache_file_structure_bsp_header *structure_bsp_header; + if (mcc_cache_tags_loaded()) + { + void *mcc_header, *mcc_structure; + if (!cache_file_structure_bsp_tag_valid(reference) || + !mcc_cache_bsp_load(reference, &mcc_header, &mcc_structure)) + return FALSE; + cache_file_globals.structure_bsp_header = mcc_header; + cache_get_tag_instance(reference->structure_bsp.index)->base_address = mcc_structure; + return TRUE; + } + /* port: the tag data's size was checked as the map loaded (cache_file_header_verify); the bsp's reference is the map's, and is checked before anything is read where it says (a Custom Edition map's @@ -1454,6 +1520,14 @@ void scenario_structure_bsp_unload( { struct cache_file_tag_instance *tag_instance; + if (mcc_cache_tags_loaded()) + { + mcc_cache_bsp_unload(); + cache_get_tag_instance(reference->structure_bsp.index)->base_address = NULL; + cache_file_globals.structure_bsp_header = NULL; + return; + } + structure_bsp_header_deregister_vertex_buffers(cache_file_globals.structure_bsp_header); /* port: the buffers a Halo Custom Edition bsp was given (port/linux/game/custom_edition_geometry.c) */ diff --git a/source/cache/cache_files_windows.c b/source/cache/cache_files_windows.c index da92f2654c..0d213e617e 100644 --- a/source/cache/cache_files_windows.c +++ b/source/cache/cache_files_windows.c @@ -191,6 +191,7 @@ symbols in this file: #include "scenario/scenario_definitions.h" #include "rasterizer/rasterizer.h" #include "custom_edition_cache.h" /* port: port/linux/game/custom_edition_cache.c */ +#include "mcc_cache.h" /* independent MCC reads and namespace */ #include @@ -582,6 +583,8 @@ boolean cache_files_precache_is_copying_map( boolean cache_files_precache_map_loaded( const char *map_name) { + if (mcc_level_name(map_name)) + return mcc_cache_available(map_name); /* port: a Halo Custom Edition map (custom_maps\) is read in place and never copied to the cache partition; it is never the game's own map of that file name (port/linux/game/custom_edition_cache.c) */ @@ -599,6 +602,9 @@ boolean cache_files_precache_map_begin( { const char *cache_map_name = tag_name_strip_path(map_name); + if (mcc_level_name(map_name)) + return mcc_cache_require(map_name, 0); + /* port: a Halo Custom Edition map (custom_maps\) this machine has not is missing, as a map not on the DVD is: never the game's own map of its file name (port/linux/game/custom_edition_cache.c) */ @@ -859,6 +865,11 @@ short cache_file_read( /* port: the reads of a Halo Custom Edition map are served in place, at once; the request stays free (port/linux/game/custom_edition_cache.c) */ + if (mcc_cache_tags_loaded()) + { + *completion_flag_reference = mcc_cache_read(tag_index, offset, size, buffer) ? TRUE : (boolean)-1; + return request_index; + } if (custom_edition_cache_tags_loaded()) { custom_edition_cache_read(tag_index, offset, size, buffer); diff --git a/source/cache/xbox_texture_cache.c b/source/cache/xbox_texture_cache.c index 6249b078bf..428aa34f0a 100644 --- a/source/cache/xbox_texture_cache.c +++ b/source/cache/xbox_texture_cache.c @@ -121,6 +121,8 @@ symbols in this file: #include "text/draw_string.h" #include #include "custom_edition_cache.h" /* port: port/linux/game/custom_edition_cache.c */ +#include "mcc_cache.h" +#include "mcc_texture_cache.h" /* ---------- constants */ @@ -758,6 +760,11 @@ static boolean texture_cache_bitmap_valid( bitmap->mipmap_count<=floor_log2(MAX(bitmap->width, MAX(bitmap->height, bitmap->depth))) && bitmap->pixels_size>=0; + /* Only MCC-owned normalized resources take this admission path. Their + linear rows have already been padded by the MCC bitmap adapter. */ + if (mcc_cache_tags_loaded() && mcc_cache_bitmap_valid(bitmap)) + return TRUE; + if (valid) { boolean compressed_format = @@ -995,6 +1002,14 @@ void *_texture_cache_bitmap_get_hardware_format( { void *hardware_format = NULL; + /* MCC owns immutable pixels and independent hardware headers. */ + if (mcc_cache_tags_loaded()) + { + hardware_format = mcc_texture_cache_get(bitmap, load); + if (hardware_format || mcc_cache_bitmap_valid(bitmap)) + return hardware_format; + } + match_assert( "c:\\halo\\SOURCE\\cache\\xbox_texture_cache.c", 210, diff --git a/source/game/game_engine.c b/source/game/game_engine.c index 84bb011c73..8e4e146578 100644 --- a/source/game/game_engine.c +++ b/source/game/game_engine.c @@ -593,6 +593,9 @@ struct network_game *network_game_server_get_game(struct network_game_server *se #include "units/bipeds.h" #include "units/units.h" #include "custom_edition_cache.h" /* port: port/linux/game/custom_edition_cache.c */ +#include "mcc_cache.h" +#include "mcc_objects.h" +#include "mcc_player.h" /* network_game_globals.c's */ boolean network_game_distributed_client(void); @@ -4922,6 +4925,12 @@ void game_engine_player_killed( if (!game_engine) return; + /* MCC team-change respawns carry neutral attribution to the netcode. */ + { + extern void mcc_ui_team_respawn_death(long *, long *, long, boolean *); + mcc_ui_team_respawn_death(&killing_player_index, &killing_object_index, dead_player_index, &friendly_fire); + } + /* the distributed netcode: a client's copy of a death has the host's killer (port/linux/game/network_distributed.c) */ network_distributed_player_killed(&killing_player_index, &killing_object_index, dead_player_index, @@ -7649,6 +7658,9 @@ long game_engine_remap_vehicle( { long result = vehicle_definition_index; + if (mcc_vehicle_placement_rules()) + return result; + /* port: a Halo Custom Edition map's vehicles are chosen by their placements, and its scripts may create any (port/linux/game/custom_edition_objects.c) */ @@ -8821,6 +8833,8 @@ static void game_engine_update_item_spawn( definition_index, NONE); placement_data.position = equipment->position; + if (mcc_cache_tags_loaded()) + vector3d_from_angle(&placement_data.forward, equipment->facing); /* port: Halo PC faces a Custom Edition map's items the way their placements do, where the Xbox copied only the position */ if (custom_edition_cache_tags_loaded()) @@ -9105,6 +9119,9 @@ void game_engine_postspawn_player_update( unit->unit.grenade_counts[ _unit_grenade_covenant_plasma] = (char)starting_plasma_grenade_count; + mcc_player_postspawn_grenades(unit_index, + global_variant.universal_variant.weapon_set == _game_engine_weapons_no_grenades && + !game_engine_infinite_grenades_internal()); } } diff --git a/source/game/player_queues_new.c b/source/game/player_queues_new.c index 4f6317f6ee..7f552f5367 100644 --- a/source/game/player_queues_new.c +++ b/source/game/player_queues_new.c @@ -105,6 +105,8 @@ symbols in this file: /* ---------- headers */ #include "cseries/cseries.h" +#include "mcc_grenades.h" +#include "mcc_player.h" #include "cseries/cseries_windows.h" #include "cseries/errors.h" #include "game/game.h" @@ -774,6 +776,7 @@ boolean update_client_dequeue( "c:\\halo\\SOURCE\\game\\player_queues_new.c", 0x1C9, (NONE == queue->desired_weapon_index) || (queue->desired_weapon_index>=0 && queue->desired_weapon_indexdesired_grenade_index)) match_assert( "c:\\halo\\SOURCE\\game\\player_queues_new.c", 0x1CA, @@ -800,6 +803,7 @@ boolean update_client_dequeue( "c:\\halo\\SOURCE\\game\\player_queues_new.c", 0x1E6, (NONE == actions[queue_index].desired_weapon_index) || (actions[queue_index].desired_weapon_index>=0 && actions[queue_index].desired_weapon_indexdesired_facing.yaw) && valid_real(action->desired_facing.pitch) && valid_real(action->throttle.i) && valid_real(action->throttle.j) && valid_real(action->primary_trigger) && (action->desired_weapon_index == NONE || diff --git a/source/game/players.c b/source/game/players.c index 6d81ff0c87..da2adebc84 100644 --- a/source/game/players.c +++ b/source/game/players.c @@ -228,6 +228,7 @@ symbols in this file: /* ---------- headers */ #include "cseries.h" +#include "mcc_player.h" #include "cseries/errors.h" #include "cseries/profile.h" #include "ai/ai.h" @@ -4586,6 +4587,7 @@ void player_add_equipment( starting_profile->starting_shield_modifier; unit->object.body_vitality += starting_profile->starting_health_modifier; + mcc_player_profile_grenades(unit_index, starting_profile, reset_equipment); unit = (struct unit_datum *)unit->unit.grenade_counts; starting_profile = (struct scenario_starting_profile *)starting_profile->grenade_counts; @@ -4718,6 +4720,7 @@ void players_update_before_game( (NONE == action->desired_weapon_index) || ((action->desired_weapon_index >= 0) && (action->desired_weapon_index <= MAXIMUM_WEAPONS_PER_UNIT))); + if (!mcc_player_choice_supported(action->desired_grenade_index)) match_assert( "c:\\halo\\SOURCE\\game\\players.c", 0x261, @@ -4845,6 +4848,7 @@ void players_update_before_game( (NONE == control_data.weapon_index) || ((control_data.weapon_index >= 0) && (control_data.weapon_index <= MAXIMUM_WEAPONS_PER_UNIT))); + if (!mcc_player_choice_supported(control_data.grenade_index)) match_assert( "c:\\halo\\SOURCE\\game\\players.c", 0x2E4, diff --git a/source/hs/hs.c b/source/hs/hs.c index 213fd10d51..65b888d198 100644 --- a/source/hs/hs.c +++ b/source/hs/hs.c @@ -12790,6 +12790,12 @@ script nodes it has room for. The shipped maps' all are, of 19001 nodes static boolean hs_scenario_syntax_data_valid( struct scenario const *scenario) { + { + extern int mcc_syntax_scenario_valid(struct scenario const *scenario); + int mcc_valid = mcc_syntax_scenario_valid(scenario); + if (mcc_valid >= 0) + return mcc_valid != 0; + } long const syntax_data_size = sizeof(struct data_array)+MAXIMUM_HS_SYNTAX_NODES_PER_SCENARIO*sizeof(struct hs_syntax_node); byte const *address = (byte const *)scenario->hs_syntax_data.address; @@ -12912,6 +12918,16 @@ static short hs_syntax_node_refusal( return _hs_node_refusal_none; function_index = expression->function_index; + { + extern struct hs_function_definition *mcc_script_function(short index); + extern boolean mcc_script_call_valid(struct hs_syntax_node const *call, struct data_array *syntax); + struct hs_function_definition *mcc_function = mcc_script_function(function_index); + if (mcc_function) + { + *name = mcc_function->name; + return mcc_script_call_valid(expression, hs_syntax_data) ? _hs_node_refusal_none : _hs_node_refusal_damaged; + } + } if (function_index<0 || function_index>=(short)NUMBEROF(hs_function_allowed_in_maps)) return _hs_node_refusal_damaged; function = hs_function_get(function_index); @@ -12966,6 +12982,13 @@ static short hs_syntax_node_refusal( if (function_index == _hs_function_set) { short designator = (short)first_argument->data; + { + extern int mcc_parameter_set_valid(struct hs_syntax_node const *node, struct hs_syntax_node const *value); + int mcc_parameter = mcc_parameter_set_valid(first_argument, + hs_syntax_try_get(first_argument->next_node_index)); + if (mcc_parameter >= 0) + return mcc_parameter ? _hs_node_refusal_none : _hs_node_refusal_damaged; + } if (!TEST_FLAG(first_argument->flags, _hs_syntax_node_primitive_bit) || !TEST_FLAG(first_argument->flags, _hs_syntax_node_variable_bit) || @@ -13080,6 +13103,14 @@ console's expressions, compiled later, may call and set anything */ static void hs_scenario_functions_check( struct scenario *scenario) { + { + extern boolean mcc_syntax_functions_check(struct scenario *scenario, struct data_array *syntax, + short (*node_refusal)(struct hs_syntax_node const *, char const **), + unsigned long *disabled_scripts, unsigned long *disabled_globals); + if (mcc_syntax_functions_check(scenario, hs_syntax_data, hs_syntax_node_refusal, + hs_scenario_disabled_scripts, hs_scenario_disabled_globals)) + return; + } long expression_index; short script_index; short global_index; @@ -13242,6 +13273,11 @@ static void hs_allocate( struct scenario *scenario; scenario = global_scenario_index != NONE ? global_scenario_get() : NULL; + { + extern int mcc_syntax_scenario_valid(struct scenario const *scenario); + if (mcc_syntax_scenario_valid(scenario) > 0) + return; + } /* port: as the map holds it only when it is sound */ if (scenario && scenario->hs_syntax_data.size == @@ -13597,6 +13633,12 @@ void hs_recompile( struct hs_function_definition *hs_function_get( short function_index) { + { + extern struct hs_function_definition *mcc_script_function(short index); + struct hs_function_definition *mcc_function = mcc_script_function(function_index); + if (mcc_function) + return mcc_function; + } match_assert( "c:\\halo\\SOURCE\\hs\\hs.c", 522, @@ -13732,6 +13774,13 @@ short hs_find_function_by_name( { short function_index; + { + extern short mcc_script_find(char const *name); + short mcc_index = mcc_script_find(name); + if (mcc_index != NONE) + return mcc_index; + } + for (function_index = 0; function_indexname, name) == 0) diff --git a/source/hs/hs_compile.c b/source/hs/hs_compile.c index e31047e04c..21b4af9654 100644 --- a/source/hs/hs_compile.c +++ b/source/hs/hs_compile.c @@ -2259,6 +2259,21 @@ static boolean hs_parse_primitive( struct hs_syntax_node *expression = hs_syntax_get(expression_index); boolean result = FALSE; + { + extern int mcc_script_parse_ai(struct hs_syntax_node *node, char const *text); + int mcc_result = mcc_script_parse_ai(expression, + hs_compile_globals.compiled_source); + if (mcc_result >= 0) + { + if (!mcc_result) + { + hs_compile_globals.error = "this is not a valid MCC AI encounter or squad."; + hs_compile_globals.error_offset = expression->source_offset; + } + return mcc_result != 0; + } + } + match_assert( "c:\\halo\\SOURCE\\hs\\hs_compile.c", 0x4AF, @@ -2553,6 +2568,19 @@ boolean hs_compile_postprocess( expression_index = data_next_index(hs_syntax_data, expression_index)) { struct hs_syntax_node *expression = hs_syntax_get(expression_index); + { + extern int mcc_parameter_compile(struct hs_syntax_node const *node); + int mcc_parameter = mcc_parameter_compile(expression); + if (mcc_parameter >= 0) + { + if (!mcc_parameter) + { + hs_compile_globals.error = "invalid MCC script parameter."; + success = FALSE; + } + continue; + } + } if (hs_type_valid(expression->type)) { diff --git a/source/hs/hs_runtime.c b/source/hs/hs_runtime.c index f9f27a7564..907c7e38bf 100644 --- a/source/hs/hs_runtime.c +++ b/source/hs/hs_runtime.c @@ -737,6 +737,8 @@ boolean debug_trigger_volumes; /* ---------- public code */ +#include "mcc_script_runtime.inl" + void hs_runtime_initialize( void) { @@ -911,6 +913,12 @@ static char const *expression_get_function_name( expression_index != thread->stack->expression_index) { /* port: an index past the table (a damaged map's) names none */ + { + extern struct hs_function_definition *mcc_script_function(short index); + struct hs_function_definition *mcc_function = mcc_script_function(syntax_node->index); + if (mcc_function) + return mcc_function->name; + } if ((word)syntax_node->index>=hs_function_table_count) return "(corrupt function)"; @@ -1349,6 +1357,7 @@ static long hs_thread_new( static void hs_thread_delete( long thread_index) { + mcc_hs_release_thread(thread_index); match_assert("c:\\halo\\SOURCE\\hs\\hs_runtime.c", 0x290, hs_thread_get(thread_index)->type!=_hs_thread_type_script); @@ -1795,6 +1804,12 @@ boolean hs_can_cast( { short object_type; + { + extern boolean mcc_script_ai_cast(short actual_type, short desired_type); + if (mcc_script_ai_cast(actual_type, desired_type)) + return TRUE; + } + match_assert("c:\\halo\\SOURCE\\hs\\hs_runtime.c", 0x5a4, actual_type==_hs_passthrough || hs_type_valid(actual_type)); match_assert("c:\\halo\\SOURCE\\hs\\hs_runtime.c", 0x5a5, @@ -1847,6 +1862,13 @@ long hs_cast( short desired_type, long value) { + { + extern boolean mcc_script_ai_cast(short actual_type, short desired_type); + extern long mcc_script_cast_ai(short desired_type, long reference); + if (mcc_script_ai_cast(actual_type, desired_type)) + return mcc_script_cast_ai(desired_type, value); + } + match_hs_assert("c:\\halo\\SOURCE\\hs\\hs_runtime.c", 0x5d8, thread_index, hs_can_cast(actual_type, desired_type), "bad typecast."); @@ -2148,6 +2170,8 @@ void hs_evaluate_set( NULL; short type; long global_index; + if (mcc_hs_parameter_set(thread_index, variable, initialize)) + return; /* port: a variable that isn't a global's name ends the thread: its index would pick the global (the external one too) written through. Only @@ -2711,6 +2735,11 @@ static void hs_thread_main( index_count = TEST_FLAG(expression->flags, _hs_syntax_node_script_bit) ? global_scenario_get()->hs_scripts.count : hs_function_table_count; + { + extern struct hs_function_definition *mcc_script_function(short index); + if (!TEST_FLAG(expression->flags, _hs_syntax_node_script_bit) && mcc_script_function(expression->index)) + index_count = expression->index + 1; + } if (expression->index<0 || expression->index>=index_count) { hs_syntax_error(thread_index); @@ -2735,6 +2764,7 @@ static void hs_thread_main( frames are let go, and a script's thread doesn't run again */ if (TEST_FLAG(thread->flags, _hs_thread_stack_overflow_bit)) { + mcc_hs_release_thread(thread_index); thread->stack = (struct hs_stack_frame *)thread->stack_data; thread->flags = 0; thread->sleep_until = thread->type==_hs_thread_type_script ? NONE : 0; @@ -2767,6 +2797,8 @@ static void hs_script_evaluate( long thread_index, boolean initialize) { + if (mcc_hs_script_evaluate(script_index, thread_index, initialize)) + return; struct hs_script *script = TAG_BLOCK_GET_ELEMENT( &global_scenario_get()->hs_scripts, script_index, @@ -2877,6 +2909,8 @@ static void hs_evaluate( return; } + if (mcc_hs_parameter_evaluate(thread_index, expression, destination)) + return; if (TEST_FLAG(hs_syntax_get(expression_index)->flags, _hs_syntax_node_primitive_bit)) { diff --git a/source/interface/hud.c b/source/interface/hud.c index 41485e7caf..029434db02 100644 --- a/source/interface/hud.c +++ b/source/interface/hud.c @@ -76,6 +76,7 @@ symbols in this file: /* ---------- headers */ #include "cseries/cseries.h" +#include "mcc_grenades.h" #include "bitmaps/bitmap_group.h" #include "camera/director.h" #include "cache/texture_cache.h" @@ -1300,6 +1301,11 @@ static void temporary_hud_draw( &scenario_get_game_globals()->grenades, unit->unit.current_grenade_index, struct game_globals_grenade); + if (mcc_grenades_active()) + sprintf(temporary + csstrlen(temporary), "%d %s|n", + mcc_grenades_get(player->unit_index, unit->unit.current_grenade_index), + tag_name_strip_path(tag_get_name(grenade->projectile.index))); + else sprintf( temporary + csstrlen(temporary), "%d %s|n", diff --git a/source/interface/hud_draw.c b/source/interface/hud_draw.c index 4e6436f26e..4f33f8c0c3 100644 --- a/source/interface/hud_draw.c +++ b/source/interface/hud_draw.c @@ -90,6 +90,8 @@ symbols in this file: #include "bitmaps/bitmaps_inlines.h" #include "bitmaps/bitmap_utilities.h" #include "cache/texture_cache.h" +#include "mcc_cache.h" +#include "mcc_hud_draw.h" #include "effects/particles.h" #include "game/game.h" #include "game/players.h" @@ -729,6 +731,12 @@ static void hud_draw_multitexture_overlay( } parameters.map_offset[map_index] = &texture_offset[map_index]; + if (mcc_cache_tags_loaded()) + { + boolean mcc_linear = TEST_FLAG(parameters.map[map_index]->flags, _bitmap_linear_bit); + parameters.map_texture_scale[map_index].i = mcc_linear ? (real)parameters.map[map_index]->width : 1.0f; + parameters.map_texture_scale[map_index].j = mcc_linear ? (real)parameters.map[map_index]->height : 1.0f; + } parameters.map_scale[map_index].i = scale_x; parameters.map_scale[map_index].j = scale_y; parameters.map_wrapped[map_index] = (boolean)overlay->map_clamp[map_index]; @@ -782,6 +790,8 @@ static void hud_draw_multitexture_overlay( reticle added its whole square) */ parameters.alpha_weighted = custom_edition_cache_tags_loaded() && parameters.framebuffer_blend_function == _shader_framebuffer_blend_function_add; + if (mcc_cache_tags_loaded()) + parameters.alpha_weighted = parameters.framebuffer_blend_function == _shader_framebuffer_blend_function_add; for (function_index = 0; function_index < overlay->functions.count; @@ -2047,6 +2057,9 @@ void hud_draw_numbers( TEST_FLAG(bitmap_group->flags, _bitmap_group_half_hud_scale_bit) ? scale*0.5f : scale; + if (mcc_cache_tags_loaded()) + digit_scale *= mcc_hud_canvas_scale(); + if (TEST_FLAG(numbers->number_flags, _hud_number_show_trailing_m_bit)) { digit_count += 1.0f; diff --git a/source/interface/hud_messaging.c b/source/interface/hud_messaging.c index 29248cc42e..3523dc2f6d 100644 --- a/source/interface/hud_messaging.c +++ b/source/interface/hud_messaging.c @@ -114,6 +114,7 @@ symbols in this file: /* ---------- headers */ +#include "mcc_hud_draw.h" #include "cseries/cseries.h" #include "cseries/errors.h" @@ -543,6 +544,8 @@ static void render_state_bitmap( if (bitmap && _texture_cache_bitmap_get_hardware_format( (struct bitmap_data *)bitmap, FALSE, TRUE)) { + if (mcc_hud_icon_draw(bitmap_group_index, bitmap, clip, cursor_bounds, color, icon, FALSE)) + return; scale = local_player_count() > 1 ? 0.75f : 1.0f; point.x = (short)(icon->offset.x * scale + cursor_bounds->x0); point.y = (short)(cursor_bounds->y1 - icon->offset.y * scale); diff --git a/source/interface/hud_nav_points.c b/source/interface/hud_nav_points.c index f30b27054b..fbd54ba945 100644 --- a/source/interface/hud_nav_points.c +++ b/source/interface/hud_nav_points.c @@ -98,6 +98,7 @@ symbols in this file: /* ---------- headers */ +#include "mcc_hud_draw.h" #include "cseries/cseries.h" #include "cseries/errors.h" #include "game_state.h" @@ -817,6 +818,7 @@ void custom_render_nav_point( byte alpha; real fade; + arrow_scale *= mcc_hud_bitmap_scale(bitmap_group_index); point.x = (short)(long)screen_position.x; point.y = (short)(long)screen_position.y; alpha = (byte)PIN(fast_ftol_C(arrow->opacity)*255, 0, 255); diff --git a/source/interface/hud_unit.c b/source/interface/hud_unit.c index 4b13657a78..7401c7432b 100644 --- a/source/interface/hud_unit.c +++ b/source/interface/hud_unit.c @@ -66,6 +66,7 @@ symbols in this file: /* ---------- headers */ +#include "mcc_hud_draw.h" #include "cseries/cseries.h" #include "cseries/errors.h" #include "cutscene/cinematics.h" @@ -613,6 +614,8 @@ void hud_render_damage_indicators( byte damage_indicators[NUMBER_OF_HUD_DAMAGE_INDICATOR_DIRECTIONS]; short direction; + scale *= mcc_hud_bitmap_scale(definition->indicator_bitmap.index); + player_effect_get_damage_indicators( local_player_index, damage_indicators); diff --git a/source/interface/hud_weapon.c b/source/interface/hud_weapon.c index 4462270ef7..48bf27d91b 100644 --- a/source/interface/hud_weapon.c +++ b/source/interface/hud_weapon.c @@ -65,6 +65,7 @@ symbols in this file: #include "interface/hud_weapon.h" #include "interface/unit_hud_interface_definition.h" #include "interface/weapon_hud_interface_definition.h" +#include "mcc_grenades.h" #include "items/weapon_definitions.h" #include "items/weapons.h" #include "objects/objects.h" @@ -554,6 +555,11 @@ static void render_grenade_hud( short local_player_index, long unit_index) { + if (mcc_grenades_active()) + { + mcc_grenades_hud(local_player_index, unit_index); + return; + } long return_eip = get_return_eip(); long stack_buffer[STACK_BUFFER_LENGTH]; struct unit_datum *unit; @@ -841,6 +847,7 @@ static void hud_update_weapon_local_player( { no_grenades = no_grenades && !unit->unit.grenade_counts[grenade_type]; } + if (mcc_grenades_active() && mcc_grenades_total(unit_index)) no_grenades = FALSE; result = (no_grenades && !unit->unit.grenade_throw_state && TEST_FLAG(unit->unit.control_flags, _unit_control_throw_grenade_bit)) || @@ -1137,6 +1144,7 @@ static void crosshairs_draw( { no_grenades = no_grenades && !unit->unit.grenade_counts[grenade_type]; } + if (mcc_grenades_active() && mcc_grenades_total(unit_index)) no_grenades = FALSE; firing_active = no_grenades && !unit->unit.grenade_throw_state && TEST_FLAG(unit->unit.control_flags, _unit_control_throw_grenade_bit); diff --git a/source/interface/player_ui.c b/source/interface/player_ui.c index 76b7d158f8..7220ad886d 100644 --- a/source/interface/player_ui.c +++ b/source/interface/player_ui.c @@ -738,6 +738,11 @@ void player_ui_begin_editing_profile( long type; player_ui_globals.edit_profile_index = NONE; + /* A replacement native editor no longer belongs to MCC's pause UI. */ + { + extern void mcc_ui_settings_profile_released(void); + mcc_ui_settings_profile_released(); + } type = saved_game_file_get_type(profile_index); switch (type) @@ -1268,5 +1273,10 @@ static void clear_profile_edit_data( void) { player_ui_globals.edit_profile_index = NONE; + /* Native Save/Cancel also ends any MCC-owned editor lease. */ + { + extern void mcc_ui_settings_profile_released(void); + mcc_ui_settings_profile_released(); + } return; } diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 23b6ada6f9..b2712a1076 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -627,6 +627,7 @@ symbols in this file: struct widget_instance; +#include "mcc_hud_draw.h" #include "cseries.h" #include "errors.h" #include "bitmaps/bitmap_group.h" @@ -654,6 +655,7 @@ struct widget_instance; #include "interface/progress_bar.h" #include "interface/ui_widget_game_data_input_functions.h" #include "custom_edition_maps.h" /* port: port/linux/game/custom_edition_maps.c */ +#include "mcc_maps.h" #include "interface/ui_widget_event_handler_functions.h" #include "interface/ui_widget_text_search_and_replace_functions.h" #include "interface/virtual_keyboard.h" @@ -1929,6 +1931,41 @@ void *ui_widget_realloc( line); } +/* MCC maps are selected by their full namespaced path. They do not enter +the Xbox/CE multiplayer level-index list. */ +boolean ui_widget_port_mcc_multiplayer_map_choose(char const *level_name) +{ + short index = mcc_maps_find(level_name); + struct network_game_server *server = global_network_game_server_get(); + + if (index == NONE || mcc_maps_campaign(index)) + return FALSE; + main_set_multiplayer_map_name(level_name); + game_engine_override_map_name(level_name); + if (server) + network_game_server_change_map_name(server, level_name); + saved_game_file_remember_last_used_multiplayer_map(level_name); + return TRUE; +} + +boolean ui_widget_port_mcc_cooperative_level_choose(char const *level_name, short difficulty) +{ + struct network_game_server *server = global_network_game_server_get(); + struct game_variant variant; + + if (!server || !mcc_maps_level_campaign(level_name) || + difficulty < 0 || difficulty >= NUMBER_OF_GAME_DIFFICULTY_LEVELS) + return FALSE; + csmemset(&variant, 0, sizeof(variant)); + ustrcpy(variant.human_readable_game_description, L"Co-op"); + main_set_difficulty(difficulty); + main_set_multiplayer_map_name(level_name); + network_game_server_port_set_cooperative(server, difficulty); + network_game_server_change_map_name(server, level_name); + network_game_server_change_game_variant(server, &variant); + return TRUE; +} + void widget_free( void *ptr) { @@ -2929,6 +2966,11 @@ void ui_widgets_close_all( if (widget_globals.widget_stack[local_player_index]) dispose_widget_stack(&widget_globals.widget_stack[local_player_index]); } + /* Release only an editor opened by MCC after its widgets are gone. */ + { + extern void mcc_ui_settings_close(short); + mcc_ui_settings_close(NONE); + } return; } @@ -2956,6 +2998,10 @@ void ui_widgets_close_all_for_local_player( dispose_widget_stack(&widget_globals.widget_stack[widget_index]); } } + { + extern void mcc_ui_settings_close(short); + mcc_ui_settings_close(local_player_index); + } return; } @@ -3391,6 +3437,18 @@ static void event_handler_dispatch( /* port: not from a widget its function deleted (it went back: menu_functions.c's profile_save_changes), which the Xbox's opened from regardless */ + /* MCC New Game can synchronously close every widget. Finish the event + with its deletion result before any navigation can reuse that tree. */ + { + extern boolean mcc_ui_new_game(struct widget_instance *, boolean *); + if (TEST_FLAG(handler->flags, _event_handler_open_widget_bit) && + !widget_deleted && handler->widget_tag.index != NONE && + mcc_ui_new_game(widget, &widget_deleted)) + { + *calling_widget_deleted = widget_deleted; + return; + } + } if (TEST_FLAG(handler->flags, _event_handler_open_widget_bit) && !widget_deleted && handler->widget_tag.index != NONE) @@ -4061,6 +4119,8 @@ static void render_state_bitmap( if (bitmap && _texture_cache_bitmap_get_hardware_format( (struct bitmap_data *)bitmap, FALSE, TRUE)) { + if (mcc_hud_icon_draw(bitmap_group_index, bitmap, clip, cursor_bounds, color, icon, TRUE)) + return; /* port: the sprite from a high-res texture, if it has one */ bitmap = hud_hires_sprite_bitmap(bitmap, icon->sequence_index); scale = hud_globals_get_scale(local_player_count() > 1); @@ -4655,6 +4715,20 @@ void network_game_reset_to_pregame_ui( void) { ui_widgets_close_all(); + /* An MCC restart retains this round's settings and returns directly to + * the ready lobby. The ordinary host map picker pauses its countdown. */ + { + extern boolean mcc_ui_network_restart_pregame(void); + if (mcc_ui_network_restart_pregame()) + { + char const *screen = network_game_is_splitscreen_local() ? + "ui\\shell\\main_menu\\multiplayer_type_select\\split_screen\\pregame\\splitscreen_pregame_wrapper_normal" : + pc_menus_screen("ui\\shell\\main_menu\\multiplayer_type_select\\connected\\pregame\\connected_pregame_screen"); + if (!ui_widget_load_by_name_or_tag(screen, NONE, NULL, NONE, NONE, NONE, NONE)) + error(_error_silent, "mcc: failed to load restart pregame screen"); + return; + } + } if (network_game_is_splitscreen_local()) { if (network_game_is_quickstart_local()) @@ -5378,6 +5452,19 @@ static void widget_instance_render_text_box( } ui_widget_port_text_wrap(*text, &bounds, width); } + /* MCC owns these text boxes; wrap their instance copies to the panel + * width without changing the map's objective strings or other menus. */ + { + extern boolean mcc_pause_text_wrap_needed(long); + if (mcc_pause_text_wrap_needed(widget->definition_tag_index)) + { + ui_widget_port_text_wrap(*text, &bounds, (short)(bounds.x1 - bounds.x0)); + clip.x0 = MAX(clip.x0, bounds.x0); + clip.y0 = MAX(clip.y0, bounds.y0); + clip.x1 = MIN(clip.x1, bounds.x1); + clip.y1 = MIN(clip.y1, bounds.y1); + } + } if (string_has_icons_to_draw(*text)) draw_string_and_hack_in_icons(&bounds, &clip, NULL, 0, *text, FALSE); else @@ -8251,6 +8338,29 @@ static boolean ui_check_for_pause_game( } local_player_count++; } + /* MCC selects its owned stock-style pause tree; embedded map menus + * remain available to custom scripts but are never the pause entry. */ + { + extern boolean mcc_pause_runtime_active(void); + extern long mcc_pause_runtime_screen(short, boolean); + if (mcc_pause_runtime_active()) + { + if ((!network_game || game_engine_allow_pause()) && + pressing_local_player_index == controller_index) + { + if (widget_globals.active_widgets[controller_index]) + ui_widgets_close_all_for_local_player(controller_index); + else if (network_game || local_player_count <= 1 || !game_time_get_paused()) + { + long screen = mcc_pause_runtime_screen(local_player_count, pressed_by_first_local_player); + if (screen == NONE || !ui_widget_load_by_name_or_tag(NULL, screen, NULL, + controller_index, NONE, NONE, NONE)) + error(_error_silent, "mcc: failed to open native pause menu"); + } + } + return pause_pressed; + } + } if (network_game) { if (game_engine_allow_pause() && diff --git a/source/interface/ui_widget.h b/source/interface/ui_widget.h index c021a5e4c8..425370ba13 100644 --- a/source/interface/ui_widget.h +++ b/source/interface/ui_widget.h @@ -66,6 +66,10 @@ enum /* ---------- prototypes/UI_WIDGET.C */ +/* MCC menu actions use level names from their independent catalog. */ +boolean ui_widget_port_mcc_multiplayer_map_choose(char const *level_name); +boolean ui_widget_port_mcc_cooperative_level_choose(char const *level_name, short difficulty); + boolean event_controller_index_compatible_with_widget( struct event_record const *event, struct widget_instance const *widget); diff --git a/source/interface/ui_widget_event_handler_functions.c b/source/interface/ui_widget_event_handler_functions.c index 252c4a4563..71cde69444 100644 --- a/source/interface/ui_widget_event_handler_functions.c +++ b/source/interface/ui_widget_event_handler_functions.c @@ -3413,6 +3413,11 @@ static boolean ui_widget_function_denied( boolean denied = FALSE; short index; + /* Only exact MCC-owned pause widgets may use their private actions. */ + { + extern boolean mcc_ui_trusted_action(struct widget_instance *, word); + if (mcc_ui_trusted_action(widget, function_index)) return FALSE; + } if (pc_menu_tag(widget->definition_tag_index)) return FALSE; if (function_index >= PC_MENU_FUNCTION_BASE && function_index < 0x8000) @@ -3454,6 +3459,13 @@ boolean ui_widget_event_handler_function_invoke( screens after it) */ if (ui_widget_function_denied(widget, function_index)) return FALSE; + /* MCC's additional in-map callbacks have their own dispatcher. */ + { + extern boolean mcc_ui_event_function(struct widget_instance *, struct event_record *, word, + boolean *, boolean *); + if (mcc_ui_event_function(widget, event, function_index, widget_deleted, &result)) + return result; + } /* port: the menus' own functions (port/linux/game/menu_functions.c) */ if (function_index >= PC_MENU_FUNCTION_BASE && function_index < 0x8000) { diff --git a/source/main/main.c b/source/main/main.c index 2189b49aa8..1bcccb40e7 100644 --- a/source/main/main.c +++ b/source/main/main.c @@ -397,6 +397,8 @@ symbols in this file: #include "text/font_group.h" #include "tag_files/files.h" #include "custom_edition_cache.h" /* port: custom_edition_level_name */ +#include "mcc_cache.h" +#include "mcc_main.h" #include "view_fov.h" /* port: display.fov, before the projection and culling */ #if defined(HALO_WINDOWS) || defined(HALO_ANDROID) || defined(__linux__) @@ -1265,6 +1267,9 @@ short main_get_solo_level_from_name( char lower_name[128] = { 0 }; short level; + if (mcc_level_name(name)) + return NONE; + /* port: a Custom Edition map (custom_maps\a30) is never one of the campaign's levels, whatever its name holds (port/linux/game/custom_edition_cache.c) */ @@ -1467,6 +1472,8 @@ static void main_new_map( } else { + if (mcc_main_load_failed(options->map_name)) + return; error(_error_immediate, "game_load() failed."); } diff --git a/source/networking/network_server_manager.c b/source/networking/network_server_manager.c index c9910e2106..f88c6fba15 100644 --- a/source/networking/network_server_manager.c +++ b/source/networking/network_server_manager.c @@ -468,6 +468,7 @@ symbols in this file: #include "networking/network_server_manager.h" #include "networking/network_server_manager_internal.h" #include "networking/network_server_message_handler.h" +#include "mcc_ui_network.h" #include "saved games/player_profile.h" #include "text/unicode.h" @@ -1313,6 +1314,7 @@ struct network_game_server *network_game_server_create( void network_game_server_dispose( struct network_game_server *server) { + mcc_ui_network_server_dispose(server); match_assert(NETWORK_SERVER_MANAGER_FILE, 0x120, server); /* port: a won co-op round's next level belongs to this server alone */ @@ -4143,6 +4145,8 @@ static boolean network_game_server_setup_game_from_playlist( { boolean success = FALSE; + if (mcc_ui_network_restart_settings(server)) return TRUE; + match_assert(NETWORK_SERVER_MANAGER_FILE, 0x961, server); network_event("setting up a net game"); @@ -4952,3 +4956,6 @@ boolean network_game_server_reset_to_pregame( return success; } + +/* Independent MCC pause-menu round restart. */ +#include "mcc_ui_network.inl" diff --git a/source/networking/network_server_message_handler.c b/source/networking/network_server_message_handler.c index a268d3e0ac..cfae14fdd3 100644 --- a/source/networking/network_server_message_handler.c +++ b/source/networking/network_server_message_handler.c @@ -2369,6 +2369,11 @@ static boolean network_game_server_handle_message_client_player_settings_request word *message, short message_size) { + /* MCC's in-game picker admits only an authenticated, team-only change. */ + { + extern boolean mcc_ui_team_request(struct network_game_server *, struct network_game_server_client_machine *, word *, short); + if (mcc_ui_team_request(server, client_machine, message, message_size)) return TRUE; + } /* (port: not once the game has started: every machine loads it from the settings sent with the start) */ if (network_game_server_lobby_is_open(server)) diff --git a/source/objects/damage.c b/source/objects/damage.c index c75e6b5046..f61d1929bd 100644 --- a/source/objects/damage.c +++ b/source/objects/damage.c @@ -1118,11 +1118,17 @@ static void object_damage_aftermath( no statistics on a machine that joined after, is no player's) */ if (player_index != NONE) { + /* An MCC team-change death must come from this exact no-statistics + * cause, not a real suicide racing the pending unit flag. */ + extern void mcc_ui_team_respawn_damage(long, long, boolean); + mcc_ui_team_respawn_damage(player_index, object_index, + TEST_FLAG(damage->flags, _damage_no_statistics_bit) != 0); game_engine_player_killed( player_index, object_index, player_index, TRUE); + mcc_ui_team_respawn_damage(player_index, object_index, FALSE); } } diff --git a/source/objects/object_types.c b/source/objects/object_types.c index d6c9a90f29..70bc876cfa 100644 --- a/source/objects/object_types.c +++ b/source/objects/object_types.c @@ -125,6 +125,7 @@ symbols in this file: #include "cseries.h" #include "errors.h" #include "object_types.h" +#include "mcc_objects.h" #include "cache/cache_files.h" #include "cutscene/cinematics.h" @@ -1220,6 +1221,10 @@ void object_types_place_all( scenario_datum_index, element_size); + if (object_type == _object_type_vehicle && + !mcc_vehicle_placement_allowed(scenario_object)) + continue; + /* port: a Halo Custom Edition map's vehicles are those its placements' multiplayer spawn flags name for the game type, as retail Halo's are (port/linux/game/custom_edition_objects.c); diff --git a/source/rasterizer/xbox/rasterizer_xbox.c b/source/rasterizer/xbox/rasterizer_xbox.c index 30ac278eaf..02e3ea555c 100644 --- a/source/rasterizer/xbox/rasterizer_xbox.c +++ b/source/rasterizer/xbox/rasterizer_xbox.c @@ -1310,6 +1310,12 @@ void rasterizer_model_part_skinning( if (!many_node_matrices) return; node_count = custom_edition_part_palette(vertex_buffer, &nodes); + { + extern short mcc_cache_part_palette(void const *buffer, byte const **nodes); + short mcc_count = mcc_cache_part_palette(vertex_buffer, &nodes); + if (mcc_count) + node_count = mcc_count; + } for (node_index = 0; node_index < node_count && node_index < RASTERIZER_MAXIMUM_NODES_PER_MODEL - 1; node_index++) { node_matrix_constants(vsh_constants__nodematrices[node_index], diff --git a/source/saved games/game_state.c b/source/saved games/game_state.c index 0b5a073bd4..8d379bebac 100644 --- a/source/saved games/game_state.c +++ b/source/saved games/game_state.c @@ -122,6 +122,8 @@ symbols in this file: #include "lruv_cache.h" #include "memory_pool.h" #include "cluster_partitions.h" +#include "mcc_cache.h" +#include "mcc_checkpoint.h" /* port: object_bounds_cache.c's */ void object_bounds_cache_invalidate(void); @@ -323,6 +325,13 @@ void game_state_save( void) { game_state_call_before_save_procs(); + /* MCC extra slots occupy only proven-unused CPU bytes in this image. */ + if (mcc_cache_tags_loaded() && !mcc_checkpoint_capture(game_state_globals.base_address, + GAME_STATE_SIZE, game_state_globals.cpu_allocation_size, GAME_STATE_CPU_SIZE)) + { + game_state_globals.saved_game_valid = FALSE; + return; + } main_stop_time(); game_state_globals.saved_game_valid = (game_state_write_to_file()!=FALSE); @@ -426,6 +435,9 @@ boolean game_state_test_persistent_storage( void game_state_save_core( const char *name) { + if (mcc_cache_tags_loaded() && !mcc_checkpoint_capture(game_state_globals.base_address, + GAME_STATE_SIZE, game_state_globals.cpu_allocation_size, GAME_STATE_CPU_SIZE)) + return; /* the whole of the native builds' larger game state */ if (game_state_write_core(name, game_state_globals.base_address, GAME_STATE_SIZE)) { @@ -850,7 +862,13 @@ boolean game_state_image_accept( if (!valid) return FALSE; } + if (mcc_cache_tags_loaded() && !mcc_checkpoint_validate(image, size, + game_state_globals.cpu_allocation_size, GAME_STATE_CPU_SIZE, game_state_globals.base_address)) + return FALSE; csmemcpy(game_state_globals.base_address, image, size); + if (mcc_cache_tags_loaded() && !mcc_checkpoint_restore(game_state_globals.base_address, size, + game_state_globals.cpu_allocation_size, GAME_STATE_CPU_SIZE)) + return FALSE; error(_error_silent, "the saved game is taken"); return TRUE; diff --git a/source/scenario/scenario.c b/source/scenario/scenario.c index ff88903b06..d806f1ea4e 100644 --- a/source/scenario/scenario.c +++ b/source/scenario/scenario.c @@ -167,6 +167,7 @@ symbols in this file: #include "ai/ai.h" #include "bink/bink_playback.h" #include "cache/cache_files.h" +#include "mcc_cache.h" #include "cseries/errors.h" #include "effects/contrails.h" #include "effects/decals.h" @@ -1111,6 +1112,18 @@ boolean scenario_switch_structure_bsp( checked. (A map's first bsp refused refuses the map: scenario_load) */ if (!loaded) { + /* MCC admission failures must reach scenario_load's cleanup and + main_new_map's menu recovery without entering the modal error loop. */ + if (mcc_cache_tags_loaded() && old_structure_bsp_index == NONE) + { + error(_error_silent, "mcc: failed to load initial structure bsp #%d", structure_bsp_index); + global_structure_bsp = NULL; + global_collision_bsp = NULL; + global_bsp3d = NULL; + collision_log_enable(TRUE); + main_start_time(); + return FALSE; + } error(_error_immediate, "failed to load structure bsp #%d", structure_bsp_index); global_structure_bsp = NULL; global_collision_bsp = NULL; @@ -1239,6 +1252,11 @@ boolean scenario_load( goto missing_tag_loop; } + /* A rejected initial MCC BSP never reaches game_load's map_loaded state. + Release its tags here so the following menu or map has its own I/O owner. */ + if (!result && mcc_cache_tags_loaded()) + scenario_unload(); + return result; } diff --git a/source/text/text_group.c b/source/text/text_group.c index ecb05b5af5..97de032c37 100644 --- a/source/text/text_group.c +++ b/source/text/text_group.c @@ -32,6 +32,7 @@ symbols in this file: #include "tag_files/tag_files.h" #include "custom_edition_maps.h" +#include "mcc_maps.h" #define MULTIPLAYER_GAME_TEXT_TAG_NAME "ui\\multiplayer_game_text" #define FIRST_FALLBACK_MULTIPLAYER_GAME_TEXT_STRING 36 @@ -213,6 +214,18 @@ static wchar_t *fallback_string(long tag_index, short string_index) { char const *tag_name = tag_get_name(tag_index); short fallback_index = string_index - FIRST_FALLBACK_MULTIPLAYER_GAME_TEXT_STRING; + /* MCC owns a separate display-index range; existing map lookups below + are unchanged for every Xbox and Custom Edition index. */ + if (string_index >= MCC_MAPS_FIRST_DISPLAY_INDEX && + string_index < MCC_MAPS_FIRST_DISPLAY_INDEX + MCC_MAPS_MAXIMUM) + { + short mcc_index = string_index - MCC_MAPS_FIRST_DISPLAY_INDEX; + + if (!csstrcasecmp(tag_name, LEVEL_NAMES_TAG_NAME) || !csstrcasecmp(tag_name, PC_LEVEL_NAMES_TAG_NAME)) + return (wchar_t *)mcc_maps_name(mcc_index); + if (!csstrcasecmp(tag_name, LEVEL_DESCRIPTIONS_TAG_NAME) || !csstrcasecmp(tag_name, PC_LEVEL_DESCRIPTIONS_TAG_NAME)) + return (wchar_t *)mcc_maps_description(mcc_index); + } /* (a Custom Edition map's name or description, by its display index) */ if (!csstrcasecmp(tag_name, LEVEL_NAMES_TAG_NAME) || !csstrcasecmp(tag_name, PC_LEVEL_NAMES_TAG_NAME)) diff --git a/source/units/unit_scripting_commands.c b/source/units/unit_scripting_commands.c index 93db429cd2..efe35973e4 100644 --- a/source/units/unit_scripting_commands.c +++ b/source/units/unit_scripting_commands.c @@ -29,6 +29,7 @@ symbols in this file: /* ---------- headers */ #include "cseries/cseries.h" +#include "mcc_grenades.h" #include "hs/object_lists.h" #include "items/weapons.h" #include "objects/damage.h" @@ -212,6 +213,8 @@ short unit_scripting_get_grenade_count( struct unit_datum const *unit; short grenade_count; short grenade_index; + if (mcc_grenades_active()) + return mcc_grenades_total(unit_index); unit = unit_try_and_get(unit_index); grenade_count = 0; if (unit) diff --git a/source/units/units.c b/source/units/units.c index 588e6932a2..1855b1e198 100644 --- a/source/units/units.c +++ b/source/units/units.c @@ -695,6 +695,7 @@ symbols in this file: #include "network_coop.h" /* port: port/linux/game/network_coop.c */ #include "coop_scripts.h" /* port: port/linux/game/coop_scripts.c */ #include "coop_enemies.h" /* port: port/linux/game/coop_enemies.c */ +#include "mcc_grenades.h" /* port: the control and animation impulses the host's actors give their units go to the clients' copies (port/linux/game/network_actors.c) */ @@ -1031,6 +1032,7 @@ void units_initialize( void units_initialize_for_new_map( void) { + if (mcc_grenades_active()) mcc_grenades_reset(); memset(unit_globals, 0, offsetof(struct unit_globals, used_time)); return; @@ -1081,6 +1083,7 @@ void unit_kill_no_statistics( void unit_delete( long unit_index) { + if (mcc_grenades_active()) mcc_grenades_clear(unit_index); return; } @@ -2025,6 +2028,7 @@ short unit_get_grenade_count( long unit_index, short grenade_type) { + if (mcc_grenades_active()) return mcc_grenades_get(unit_index, grenade_type); struct unit_datum *unit = unit_get(unit_index); if (grenade_type!=NONE) @@ -2044,6 +2048,11 @@ short unit_get_grenade_count( short unit_get_current_grenade_type( long unit_index) { + if (mcc_grenades_active()) + { + short mcc_type = unit_get(unit_index)->unit.current_grenade_index; + return mcc_type >= 0 && mcc_type < mcc_grenades_type_count() ? mcc_type : NONE; + } struct unit_datum *unit = unit_get(unit_index); match_assert("c:\\halo\\SOURCE\\units\\units.c", 7864, unit->unit.current_grenade_index==NONE || (unit->unit.current_grenade_index>=0 && unit->unit.current_grenade_index=0); @@ -2144,6 +2155,7 @@ boolean unit_add_grenade_to_inventory( long unit_index, long equipment_index) { + if (mcc_grenades_active()) return mcc_grenades_pickup(unit_index, equipment_index); struct item_datum *equipment = equipment_get(equipment_index); struct equipment_definition *equipment_definition = equipment_definition_get(equipment->definition_index); @@ -2651,6 +2663,7 @@ boolean unit_new( result = TRUE; } + if (result && mcc_grenades_active()) mcc_grenades_initialize_unit(unit_index); return result; } @@ -2861,6 +2874,7 @@ static void unit_drop_grenades( long grenade_type_count; struct game_globals_grenade *grenade; + if (mcc_grenades_active()) mcc_grenades_drop_extra(unit_index, unit_drop_item); unit = unit_get(unit_index); grenade_count = unit->unit.grenade_counts; grenade_count_negative_base = @@ -4765,6 +4779,7 @@ void unit_place( unit->unit.grenade_counts, 0, sizeof(unit->unit.grenade_counts)); + if (mcc_grenades_active()) mcc_grenades_clear(unit_index); if (unit->unit.equipment_object_index!=NONE) { @@ -5764,6 +5779,12 @@ boolean unit_update( if (cheat.infinite_ammo && unit->unit.player_index!=NONE) { short grenade_index; + if (mcc_grenades_active()) + { + short mcc_type; + for (mcc_type = 2; mcc_type < mcc_grenades_type_count(); ++mcc_type) + if (!mcc_grenades_get(unit_index, mcc_type)) mcc_grenades_set(unit_index, mcc_type, 1); + } for (grenade_index = 0; grenade_indexunit.grenade_counts); ++grenade_index) { @@ -7879,6 +7900,11 @@ static short unit_weapon_next_index( static void unit_throw_grenade_move_to_hand( long unit_index) { + if (mcc_grenades_active() && unit_get(unit_index)->unit.current_grenade_index >= 2) + { + mcc_grenades_move_to_hand(unit_index); + return; + } long object_index; struct object_placement_data placement_data; @@ -11755,6 +11781,12 @@ void unit_control( control_data->weapon_index==NONE || (control_data->weapon_index>=0 && control_data->weapon_indexgrenade_index == NONE || + (control_data->grenade_index >= 0 && control_data->grenade_index < mcc_grenades_type_count())); + } + else match_assert( "c:\\halo\\SOURCE\\units\\units.c", 0x5e9, diff --git a/tools/android_build.py b/tools/android_build.py index 7c73739fd3..af61a39ee8 100644 --- a/tools/android_build.py +++ b/tools/android_build.py @@ -610,7 +610,10 @@ def guest_object(source: Path, cflags: str, prefix: str = "") -> Path: # config.toml (port/android/host/host_main.c) staged_brokers = assets_dir / "brokers.txt" n.build(outputs=staged_brokers, rule="android_copy", inputs=Path("port/assets/network/brokers.txt")) - n.build(outputs="android", rule="phony", inputs=[libmain, staged_sdl, staged_image, staged_brokers]) + # Include MCC's decoder notice in the APK as well as the release archive. + staged_bcdec_license = assets_dir / "bcdec-LICENSE.txt" + n.build(outputs=staged_bcdec_license, rule="android_copy", inputs=Path("port/third_party/bcdec/LICENSE")) + n.build(outputs="android", rule="phony", inputs=[libmain, staged_sdl, staged_image, staged_brokers, staged_bcdec_license]) apk = PORT_DIR / "app" / "build" / "outputs" / "apk" / "debug" / "app-debug.apk" sdl_android_mouse_listener = SDL_DIR / SDL_ANDROID_MOUSE_LISTENER @@ -622,7 +625,7 @@ def guest_object(source: Path, cflags: str, prefix: str = "") -> Path: description="ANDROID GRADLE $out", pool="console", ) - n.build(outputs=apk, rule="android_gradle", inputs=[libmain, staged_sdl, staged_image, staged_brokers], + n.build(outputs=apk, rule="android_gradle", inputs=[libmain, staged_sdl, staged_image, staged_brokers, staged_bcdec_license], implicit=[sdl_android_mouse_listener]) n.build(outputs="android_apk", rule="phony", inputs=apk) n.newline() diff --git a/tools/ci_build.py b/tools/ci_build.py index 56a749f0bc..e23385bf26 100644 --- a/tools/ci_build.py +++ b/tools/ci_build.py @@ -118,6 +118,8 @@ def main() -> int: # the menus' XML parser (port/third_party/expat), in every build, whose # MIT license asks copies to carry its notice shutil.copy2(ROOT / "port/third_party/expat/COPYING", dist / "expat-COPYING.txt") + # MCC's texture decoder is distributed under bcdec's MIT option. + shutil.copy2(ROOT / "port/third_party/bcdec/LICENSE", dist / "bcdec-LICENSE.txt") # voice chat's codec (port/third_party/opus), in every build, whose BSD # license asks binaries to carry its notice shutil.copy2(ROOT / "port/third_party/opus/COPYING", dist / "opus-COPYING.txt") diff --git a/tools/harness/tests/mcc_network.c b/tools/harness/tests/mcc_network.c new file mode 100644 index 0000000000..c4f5e79b82 --- /dev/null +++ b/tools/harness/tests/mcc_network.c @@ -0,0 +1,137 @@ +#include +#include +#include +typedef unsigned char byte, boolean; +typedef unsigned short word; +typedef float real; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +#define MIN(a,b) ((a)<(b)?(a):(b)) +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define CHECK(c) do {if(!(c)){fprintf(stderr,"line %d: %s\n",__LINE__,#c);return 1;}}while(0) +#define csmemset memset +#define csmemcpy memcpy +#define DATUM_INDEX_TO_ABSOLUTE_INDEX(i) ((unsigned long)(i)&65535) +#define MAXIMUM_TRACKED_OBJECTS 16 +#define MAXIMUM_TRACKED_PLAYERS 4 +#define MAXIMUM_LOCAL_PLAYERS 1 +#define MAXIMUM_GRENADE_THROWS 8 +#define TICKS_PER_SECOND 30 +#define DATAGRAM_ENTRIES(t) 64 +#define _object_mask_unit 3 +#define _game_connection_network_server 1 +#define _game_connection_network_client 2 +#define _distributed_message_mcc_grenades 83 +#define _distributed_to_clients 0 +struct distributed_message_header {word header;byte type,count;long time;}; +struct object_iterator {long index;int done;}; +struct unit_datum {struct {long player_index;} unit;}; +struct player_datum {long unit;}; +static long now=3; +static int active=1,connection=1,types=4,sends; +static long live_unit=(long)0xe0010001UL; +static short counts[4]={1,2,3,4}; +static struct unit_datum unit={{0}}; +static struct player_datum player; +static byte sent[1024]; +static short sent_count; +static word sent_size; +static int known=1; +static boolean local=0; +static real round_trip=6; +short mcc_grenades_get(long index,short type) {return index==live_unit&&type>=0&&type=0&&typedone=0;it->index=live_unit;} +void *object_iterator_next(struct object_iterator *it){if(it->done++)return NULL;return &unit;} +boolean distributed_object_index_valid(long index){return ((unsigned long)index>>16)!=0 && index!=NONE;} +boolean network_objects_client_has(long index){return known&&index==live_unit;} +void *object_try_and_get_and_verify_type(long index,long mask){(void)mask;return index==live_unit?&unit:NULL;} +boolean distributed_player_is_local(long index){return index==0&&local;} +real distributed_own_round_trip_ticks(void){return round_trip;} +long local_player_get_player_index(short index){return index==0?0:NONE;} +struct player_datum *player_try_and_get(long index){return index==0?&player:NULL;} +long distributed_living_unit(struct player_datum const *p){return p?p->unit:NONE;} +void distributed_send(void *message,byte type,short count,word size,short destination){ + (void)destination;if(type!=83||size>sizeof(sent))return; + memcpy(sent,message,size);sent_count=count;sent_size=size;sends++; +} +/* MCC_INVENTORY_IMPLEMENTATION */ + +struct tag_block {long count;void *address;}; +struct game_globals_grenade {struct {long index;} projectile;}; +struct game_globals {struct tag_block grenades;}; +struct damage_reach {real distance,ticks,carried_ticks;}; +static struct {long throw_times[MAXIMUM_GRENADE_THROWS];short throw_types[MAXIMUM_GRENADE_THROWS];} damage_players[MAXIMUM_TRACKED_PLAYERS]; +static struct game_globals globals; +static struct game_globals_grenade grenades[4]; +static int paid; +#define TAG_BLOCK_GET_ELEMENT(block,index,type) ((type *)(block)->address+(index)) +struct game_globals *scenario_get_game_globals(void){return &globals;} +short distributed_grenade_throw(short p,short type){short i;for(i=0;i<8;i++)if(damage_players[p].throw_times[i]!=NONE&&damage_players[p].throw_types[i]==type)return i;return NONE;} +boolean distributed_explosion_this_tick(short p,short type,void *point){(void)p;(void)type;(void)point;return paid;} +real distributed_source_deals(long projectile,long damage,boolean grenade,byte *kinds,struct damage_reach *reach){ + (void)grenade;*kinds=projectile==damage?2:0;reach->distance=8;reach->ticks=3;reach->carried_ticks=1;return projectile==damage?4:0; +} +void distributed_reach_combine(struct damage_reach *reach,struct damage_reach const *other){reach->distance=MAX(reach->distance,other->distance);} +/* MCC_DAMAGE_IMPLEMENTATION */ + +int main(int argc,char **argv){ + struct mcc_network_inventory packet; + int i; + CHECK(argc==2);player.unit=live_unit;mcc_network_new_game(); + memset(&packet,0,sizeof(packet));packet.unit=live_unit;packet.counts[0]=7;packet.counts[1]=8; + if(!strcmp(argv[1],"host")){ + mcc_network_host_tick();CHECK(sends==1&&sent_count==1&&sent_size==16); + CHECK(((struct mcc_network_inventory *)(sent+8))->counts[0]==3); + now=6;mcc_network_host_tick();CHECK(sends==1); + counts[2]=2;mcc_network_host_tick();CHECK(sends==2); + now=36;mcc_network_host_tick();CHECK(sends==3); + active=0;now=66;mcc_network_host_tick();CHECK(sends==3); + }else if(!strcmp(argv[1],"receive")){ + connection=2;mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==7&&counts[3]==8&&counts[0]==1&&counts[1]==2); + packet.counts[0]=128;mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==7); + packet.counts[0]=4;packet.unit^=0x10000;mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==7); + packet.unit=live_unit;packet.reserved[0]=1;mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==7); + packet.reserved[0]=0;known=0;mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==7); + }else if(!strcmp(argv[1],"prediction")){ + connection=2;local=1;mcc_network_client_tick(); + now=4;counts[2]=2;mcc_network_client_tick(); + mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==2&&counts[3]==8); + now=16;mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==7); + packet.counts[0]=1;mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==1); + live_unit^=0x10000;packet.unit=live_unit;packet.counts[0]=6;mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==6); + }else if(!strcmp(argv[1],"rewind_host")){ + now=300;mcc_network_host_tick();CHECK(sends==1); + now=303;mcc_network_host_tick();CHECK(sends==1); + now=30;mcc_network_host_tick();CHECK(sends==2); + now=33;mcc_network_host_tick();CHECK(sends==2); + }else if(!strcmp(argv[1],"rewind_client")){ + connection=2;local=1;now=300;mcc_network_client_tick(); + now=301;counts[2]=2;mcc_network_client_tick(); + mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==2); + now=30;mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==7); + now=31;counts[2]=6;mcc_network_client_tick(); + mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==6); + now=43;mcc_network_handle_inventories(&packet,1);CHECK(counts[2]==7); + }else if(!strcmp(argv[1],"damage")){ + byte kinds=0;struct damage_reach reach={0,0,0};short type=NONE;real rate=0; + globals.grenades.count=4;globals.grenades.address=grenades;for(i=0;i<4;i++)grenades[i].projectile.index=100+i; + for(i=0;i<8;i++)damage_players[0].throw_times[i]=NONE; + mcc_network_note_extra_grenade(live_unit,4);CHECK(distributed_grenade_throw(0,4)==NONE); + mcc_network_note_extra_grenade(live_unit,2);CHECK(distributed_grenade_throw(0,2)!=NONE); + mcc_network_extra_grenade_damage(0,102,&kinds,&reach,&type,&rate);CHECK(rate==4&&type==2&&kinds==2&&reach.distance==8); + for(i=0;i<8;i++)damage_players[0].throw_times[i]=NONE; + rate=0;type=NONE;kinds=0;mcc_network_extra_grenade_damage(0,102,&kinds,&reach,&type,&rate);CHECK(rate==0&&type==NONE); + paid=1;mcc_network_extra_grenade_damage(0,102,&kinds,&reach,&type,&rate);CHECK(rate==4&&type==2); + connection=2;mcc_network_note_extra_grenade(live_unit,3);CHECK(distributed_grenade_throw(0,3)==NONE); + }else if(!strcmp(argv[1],"legacy")){ + active=0;mcc_network_host_tick();connection=2;mcc_network_client_tick();mcc_network_handle_inventories(&packet,1); + CHECK(sends==0&&counts[2]==3&&counts[3]==4&&counts[0]==1&&counts[1]==2); + }else return 2; + return 0; +} diff --git a/tools/harness/tests/mcc_objects.c b/tools/harness/tests/mcc_objects.c new file mode 100644 index 0000000000..3f76d13b9d --- /dev/null +++ b/tools/harness/tests/mcc_objects.c @@ -0,0 +1,46 @@ +#include "cseries.h" +#include "game/game_engine.h" +#include "mcc_objects.h" +#include +#include +#undef memcpy +#undef memset + +static boolean loaded, running; +static struct game_variant variant; +boolean mcc_cache_tags_loaded(void) { return loaded; } +boolean game_engine_running(void) { return running; } +struct game_variant *game_engine_get_variant(void) { return &variant; } +void *csmemcpy(void *to, void const *from, unsigned long size) { return memcpy(to, from, size); } + +#define CHECK(value) do { if (!(value)) { fprintf(stderr, "failed at %d\n", __LINE__); return 1; } } while (0) +int main(void) { + unsigned char bytes[0x78] = {0}; + struct scenario_object_datum *placement = (void *)bytes; + unsigned short flags; + int engine, enabled; + running = TRUE; + variant.game_engine_index = game_engine_slayer; + CHECK(!mcc_vehicle_placement_rules()); + CHECK(mcc_vehicle_placement_allowed(placement)); + loaded = TRUE; + for (engine = game_engine_ctf; engine <= game_engine_king; ++engine) { + static unsigned short masks[] = {0, 2, 1, 8, 4}; + variant.game_engine_index = engine; + for (enabled = 0; enabled < 16; ++enabled) { + flags = (unsigned short)enabled; + memcpy(bytes + 0x5A, &flags, 2); + CHECK(mcc_vehicle_placement_rules()); + CHECK(mcc_vehicle_placement_allowed(placement) == ((enabled & masks[engine]) != 0)); + } + } + variant.game_engine_index = game_engine_race; + CHECK(!mcc_vehicle_placement_rules() && mcc_vehicle_placement_allowed(placement)); + variant.game_engine_index = game_engine_slayer; + variant.universal_variant.vehicle_set = 1; + CHECK(!mcc_vehicle_placement_rules()); + variant.universal_variant.vehicle_set = 0; + running = FALSE; + CHECK(!mcc_vehicle_placement_rules() && mcc_vehicle_placement_allowed(placement)); + return 0; +} diff --git a/tools/harness/tests/mcc_player.c b/tools/harness/tests/mcc_player.c new file mode 100644 index 0000000000..221ccff0bb --- /dev/null +++ b/tools/harness/tests/mcc_player.c @@ -0,0 +1,66 @@ +#include "cseries.h" +#include "math/real_math.h" +#include "game/players.h" +#include "scenario/scenario_definitions.h" +#include "mcc_player.h" +#include +#include +#undef memset +static boolean active; +static short types = 4, counts[4]; +boolean mcc_grenades_active(void) { return active; } +short mcc_grenades_type_count(void) { return types; } +void mcc_grenades_clear(long unit) { (void)unit; counts[2] = counts[3] = 0; } +short mcc_grenades_get(long unit, short type) { (void)unit; return counts[type]; } +void mcc_grenades_set(long unit, short type, short count) { (void)unit; counts[type] = count; } +#define CHECK(value) do { if (!(value)) { fprintf(stderr,"failed at %d\n",__LINE__); return 1; } } while (0) +int main(void) { + struct player_action action = {0}; + struct scenario_starting_profile profile = {0}; + union { unsigned long bits; float value; } invalid; + short choice; + CHECK(!mcc_player_choice_supported(0)); + mcc_player_profile_grenades(1, &profile, TRUE); + active = TRUE; + for (choice = NONE; choice < 4; ++choice) { + action.desired_grenade_index = choice; + CHECK(mcc_player_action_valid(&action)); + } + action.desired_grenade_index = 4; + CHECK(!mcc_player_action_valid(&action)); + action.desired_grenade_index = -2; + CHECK(!mcc_player_action_valid(&action)); + action.desired_grenade_index = 3; + types = 2; + CHECK(!mcc_player_action_valid(&action)); + types = 4; + invalid.bits = 0x7FC00000; + action.throttle.i = invalid.value; + CHECK(!mcc_player_action_valid(&action)); + action.throttle.i = 0; + action.desired_weapon_index = 100; + CHECK(!mcc_player_action_valid(&action)); + action.desired_weapon_index = NONE; + action.desired_zoom_level = -2; + CHECK(!mcc_player_action_valid(&action)); + profile.pad[0] = 2; profile.pad[1] = 3; + counts[0] = 1; counts[1] = 4; counts[2] = 10; + mcc_player_profile_grenades(1, &profile, TRUE); + CHECK(counts[0] == 1 && counts[1] == 4 && counts[2] == 2 && counts[3] == 3); + mcc_player_profile_grenades(1, &profile, FALSE); + CHECK(counts[2] == 4 && counts[3] == 6); + counts[2] = 126; + mcc_player_profile_grenades(1, &profile, FALSE); + CHECK(counts[2] == 127 && counts[3] == 9); + active = FALSE; + mcc_player_profile_grenades(1, &profile, TRUE); + CHECK(counts[2] == 127 && counts[3] == 9); + mcc_player_postspawn_grenades(1, TRUE); + CHECK(counts[2] == 127 && counts[3] == 9); + active = TRUE; + mcc_player_postspawn_grenades(1, FALSE); + CHECK(counts[2] == 127 && counts[3] == 9); + mcc_player_postspawn_grenades(1, TRUE); + CHECK(counts[0] == 1 && counts[1] == 4 && !counts[2] && !counts[3]); + return 0; +} diff --git a/tools/harness/tests/mcc_script_parameters.c b/tools/harness/tests/mcc_script_parameters.c new file mode 100644 index 0000000000..86203f46c3 --- /dev/null +++ b/tools/harness/tests/mcc_script_parameters.c @@ -0,0 +1,195 @@ +#include "cseries.h" +#include "math/real_math.h" +#include "errors.h" +#include "hs/hs.h" +#include "hs/hs_scenario_definitions.h" +#include "memory/data.h" +#include "scenario/scenario_definitions.h" +#include "mcc_runtime.h" +#include "mcc_script_parameters.h" +#include +#include +#include +#undef malloc +#undef free +#undef memset +#undef memcpy +#undef strcmp +#undef strcpy + +#define CHECK(value) do {if(!(value)){fprintf(stderr,"check failed at %d: %s\n",__LINE__,#value);return 1;}}while(0) +struct hs_stack_frame {struct hs_stack_frame *previous;long expression_index;void *result;short size;byte data[2];}; +struct hs_thread_datum {short identifier;byte type,flags;long script_index,sleep_until,previous_sleep_until; + struct hs_stack_frame *stack;long result;byte stack_data[512];}; +static struct hs_thread_datum thread; +static struct scenario *scenario; +struct data_array *hs_syntax_data; +static int loaded=1,errors,refs,returned,disabled; +short const hs_external_global_count=443; +static long last_result; +static struct hs_thread_datum *hs_thread_get(long index) {(void)index;return &thread;} +boolean mcc_cache_tags_loaded(void) {return loaded;} +void error(short priority,char const *message,...) {(void)priority;(void)message;} +void *csmemset(void *p,long value,unsigned long size) {return memset(p,(int)value,size);} +void *csmemcpy(void *p,void const *from,unsigned long size) {return memcpy(p,from,size);} +boolean hs_can_cast(short a,short b) {return a==b || (a==_hs_type_short_integer && b==_hs_type_long_integer) || + (loaded && a==_hs_type_ai && (HS_TYPE_IS_OBJECT(b)||b==_hs_type_object_list));} +long hs_cast(long index,short from,short to,long value) {(void)index;(void)from;(void)to;return value;} +void object_list_add_reference(long index) {if(index!=NONE)refs++;} +void object_list_remove_reference(long index) {if(index!=NONE){if(!refs)exit(20);refs--;}} +static void hs_syntax_error(long index) {(void)index;errors++;} +static long hs_type_default_value(short type) {(void)type;return NONE;} +boolean hs_scenario_script_disabled(short index) {(void)index;return disabled;} +struct scenario *global_scenario_get(void) {return scenario;} +struct hs_syntax_node *hs_syntax_get(long handle) { + unsigned index=(unsigned)handle&65535; + struct hs_syntax_node *nodes=hs_syntax_data->data; + return handle!=NONE&&index<(unsigned)hs_syntax_data->count&& + (unsigned short)nodes[index].datum_header==(unsigned short)((unsigned)handle>>16)?nodes+index:NULL; +} +static void *hs_stack_allocate(long index,long size) { + void *out;(void)index; + if(size<0||size>thread.stack_data+512-(thread.stack->data+thread.stack->size)){errors++;return NULL;} + out=thread.stack->data+thread.stack->size;thread.stack->size+=(short)size;return out; +} +void hs_return(long index,long value) {(void)index;returned++;last_result=value;} +static void hs_evaluate(long index,long expression,long *out); +#include "mcc_script_runtime.inl" +static void hs_evaluate(long index,long expression,long *out) { + struct hs_syntax_node *node=hs_syntax_get(expression); + if(!node){errors++;return;} + if(!mcc_hs_parameter_evaluate(index,node,out))*out=node->data; +} +void *mcc_runtime_pointer(struct mcc_runtime *runtime,uint32_t address,uint32_t bytes) { + uint32_t offset=address-runtime->report.tag_base; + return address>=runtime->report.tag_base&&offset<=runtime->used&&bytes<=runtime->used-offset? + runtime->tags+offset:NULL; +} +void *tag_block_get_element_with_size(struct tag_block const *block,long index,long size) { + return (unsigned char *)block->address+index*size; +} + +int main(int argc,char **argv) { + struct mcc_runtime runtime={0}; + struct hs_script *scripts; + struct hs_syntax_node *nodes; + struct hs_global *globals; + unsigned char *parameter; + uint32_t *block; + short type=_hs_type_long_integer,owner; + int valid=1,i; + struct hs_stack_frame *frame; + struct mcc_hs_arguments *state; + CHECK(argc==2); + runtime.used=1048576;runtime.tags=malloc(runtime.used);CHECK(runtime.tags); + memset(runtime.tags,0,runtime.used);runtime.report.tag_base=(uint32_t)(uintptr_t)runtime.tags; + scenario=(void *)(runtime.tags+256);scripts=(void *)(runtime.tags+4096); + hs_syntax_data=(void *)(runtime.tags+8192);nodes=(void *)(hs_syntax_data+1); + parameter=runtime.tags+0xE0000;globals=(void *)(runtime.tags+0xF0000); + scenario->hs_scripts.count=2;scenario->hs_scripts.address=scripts; + scenario->hs_globals.address=globals; + scripts[0].script_type=_hs_script_static;scripts[0].return_type=type;scripts[0].root_expression_index=0x10000; + scripts[1].script_type=_hs_script_static;scripts[1].return_type=type;scripts[1].root_expression_index=NONE; + block=(void *)((unsigned char *)scripts+0x50);block[0]=1;block[1]=(uint32_t)(uintptr_t)parameter; + strcpy((char *)parameter,"argument");memcpy(parameter+32,&type,2); + hs_syntax_data->data=nodes;hs_syntax_data->count=5; + for(i=0;i<5;i++){nodes[i].datum_header=1;nodes[i].next_node_index=NONE;nodes[i].type=type;nodes[i].flags=9;} + nodes[0].flags=0x1D;nodes[0].data=0; + nodes[1].datum_header=0; + nodes[2].flags=10;nodes[2].script_index=0;nodes[2].data=0x10003; + nodes[3].type=_hs_function_name;nodes[3].next_node_index=0x10004; + nodes[4].data=42; + if(!strcmp(argv[1],"count")){block[0]=17;valid=0;} + else if(!strcmp(argv[1],"type")){type=_hs_type_void;memcpy(parameter+32,&type,2);valid=0;} + else if(!strcmp(argv[1],"pointer")){block[1]=1;valid=0;} + else if(!strcmp(argv[1],"slot")){nodes[0].data=1;valid=0;} + else if(!strcmp(argv[1],"orphan")){scripts[0].root_expression_index=NONE;valid=0;} + else if(!strcmp(argv[1],"flags")){nodes[0].flags=0x19;valid=0;} + else if(!strcmp(argv[1],"call_arity")){nodes[3].next_node_index=NONE;valid=0;} + else if(!strcmp(argv[1],"call_type")){nodes[4].type=_hs_type_boolean;valid=0;} + else if(!strcmp(argv[1],"predicate")){nodes[3].type=_hs_type_long_integer;valid=0;} + else if(!strcmp(argv[1],"script_kind")){scripts[0].script_type=_hs_script_startup;valid=0;} + else if(!strcmp(argv[1],"global_scope")){scenario->hs_globals.count=1;globals[0].initialization_expression_index=0x10000;valid=0;} + else if(!strcmp(argv[1],"lists")){type=_hs_type_object_list;memcpy(parameter+32,&type,2);nodes[0].type=nodes[4].type=type;} + else if(!strcmp(argv[1],"ai_cast")){type=_hs_type_ai;memcpy(parameter+32,&type,2);nodes[4].type=type;nodes[0].type=_hs_type_unit;} + else if(!strcmp(argv[1],"global_capacity")||!strcmp(argv[1],"global_overflow")) { + scenario->hs_globals.count=581+(!strcmp(argv[1],"global_overflow")); + for(i=0;ihs_globals.count;i++)globals[i].initialization_expression_index=NONE; + valid=scenario->hs_globals.count==581; + } else if(!strcmp(argv[1],"maximum")) { + block[0]=16;hs_syntax_data->count=20; + for(i=0;i<16;i++) { + strcpy((char *)parameter+i*36,"argument");memcpy(parameter+i*36+32,&type,2); + nodes[i+4].datum_header=1;nodes[i+4].flags=9;nodes[i+4].type=type; + nodes[i+4].next_node_index=i==15?NONE:0x10005+i; + } + } else if(!strcmp(argv[1],"high_node")) { + hs_syntax_data->count=32767; + nodes[32766]=nodes[0];nodes[0].datum_header=0; + scripts[0].root_expression_index=0x17FFE; + } else if(!strcmp(argv[1],"negative_count")) { + hs_syntax_data->count=(short)32768;valid=0; + } else if(!strcmp(argv[1],"disabled"))disabled=1; + loaded=0;CHECK(mcc_parameters_prepare(&runtime,scenario,hs_syntax_data)==valid);loaded=1; + if(!valid){CHECK(!mcc_parameters_signature(0));free(runtime.tags);return 0;} + if(!strcmp(argv[1],"high_node")) { + struct hs_thread_datum saved; + CHECK(mcc_parameter_type(nodes+32766,&owner)==type&&owner==0); + CHECK(mcc_parameter_compile(nodes+32766)==1); + memset(&thread,0,sizeof(thread));frame=(void *)(thread.stack_data+32); + frame->previous=(void *)thread.stack_data;frame->expression_index=0x10002;thread.stack=frame; + CHECK(mcc_hs_script_evaluate(0,0,TRUE)); + saved=thread;memset(&thread,0,sizeof(thread));thread=saved; + frame->size=0;CHECK(mcc_hs_script_evaluate(0,0,FALSE)); + CHECK(((struct mcc_hs_arguments *)frame->data)->result==42); + frame->size=0;CHECK(mcc_hs_script_evaluate(0,0,FALSE)); + CHECK(returned==1&&last_result==42&&!errors); + goto done; + } + CHECK(mcc_parameter_type(nodes,&owner)==type&&owner==0); + CHECK(mcc_parameter_compile(nodes)==1); + if(!strcmp(argv[1],"maximum")){CHECK(mcc_parameters_signature(0)->count==16);goto done;} + if(!strcmp(argv[1],"global_capacity"))goto done; + if(!strcmp(argv[1],"metadata")||!strcmp(argv[1],"ai_cast")){CHECK(mcc_parameters_signature(0)->count==1);goto done;} + if(!strcmp(argv[1],"legacy")){loaded=0;CHECK(mcc_parameter_type(nodes,NULL)==-1);CHECK(!mcc_parameters_signature(0));goto done;} + memset(&thread,0,sizeof(thread));frame=(void *)(thread.stack_data+32); + frame->previous=(void *)thread.stack_data;frame->expression_index=0x10002;thread.stack=frame; + CHECK(mcc_hs_script_evaluate(0,0,TRUE)); + state=(void *)frame->data;CHECK(state->values[0]==42&&state->phase==0); + if(!strcmp(argv[1],"snapshot")) { + unsigned char snapshot[sizeof(thread)];memcpy(snapshot,&thread,sizeof(thread)); + memset(&thread,0,sizeof(thread));memcpy(&thread,snapshot,sizeof(thread)); + } + frame->size=0;CHECK(mcc_hs_script_evaluate(0,0,FALSE)); + if(disabled){CHECK(returned==1&&last_result==NONE&&!refs);goto done;} + CHECK(state->phase==1&&state->result==42); + if(!strcmp(argv[1],"lists")){CHECK(refs==1);mcc_hs_release_thread(0);CHECK(!refs);state->references=0;} + if(!strcmp(argv[1],"nested_scope")||!strcmp(argv[1],"recursive_arguments")) { + struct hs_stack_frame *nested=(void *)(frame->data+frame->size+16); + struct mcc_hs_arguments *other; + long *value;short actual; + nested->previous=frame;nested->expression_index=0x10002;thread.stack=nested; + nested->size=0;CHECK(mcc_hs_script_evaluate(0,0,TRUE));other=(void *)nested->data; + other->values[0]=84; + if(!strcmp(argv[1],"nested_scope"))other->phase=1; + value=mcc_hs_parameter_address(0,nodes,&actual); + CHECK(value&&*value==(!strcmp(argv[1],"nested_scope")?84:42));thread.stack=frame; + } + if(!strcmp(argv[1],"set")) { + struct hs_stack_frame *setter=(void *)(frame->data+frame->size+16); + setter->previous=frame;setter->expression_index=0x10003;setter->size=0;thread.stack=setter; + nodes[0].next_node_index=0x10004;nodes[4].data=77; + CHECK(mcc_parameter_set_valid(nodes,nodes+4)==1); + CHECK(mcc_hs_parameter_set(0,nodes,TRUE));setter->size=0; + CHECK(mcc_hs_parameter_set(0,nodes,FALSE));CHECK(state->values[0]==77);thread.stack=frame; + returned=0; + } + if(!strcmp(argv[1],"overflow")) { + struct hs_stack_frame *overflow=(void *)(thread.stack_data+480); + overflow->size=0;overflow->previous=frame;overflow->expression_index=0x10002;thread.stack=overflow; + CHECK(mcc_hs_script_evaluate(0,0,TRUE));CHECK(errors==1);mcc_hs_release_thread(0);errors=0;thread.stack=frame; + } + frame->size=0;CHECK(mcc_hs_script_evaluate(0,0,FALSE));CHECK(returned==1&&last_result==42&&!errors&&!refs); +done: + mcc_parameters_dispose();CHECK(!mcc_parameters_signature(0));free(runtime.tags);return 0; +} diff --git a/tools/harness/tests/mcc_scripts.c b/tools/harness/tests/mcc_scripts.c new file mode 100644 index 0000000000..a4d98c9232 --- /dev/null +++ b/tools/harness/tests/mcc_scripts.c @@ -0,0 +1,259 @@ +/* Native MCC linker tests with generated syntax, no game assets. */ +#include "cseries.h" +#include "math/real_math.h" +#include "errors.h" +#include "hs/hs.h" +#include "hs/hs_scenario_definitions.h" +#include "memory/data.h" +#include "objects/objects.h" +#include "scenario/scenario_definitions.h" +#include "mcc_runtime.h" +#include "mcc_scripts.h" +#include +#include +#include +#include +#include +#undef malloc +#undef free +#undef memset +#undef memcpy +#undef strcmp +#undef strcpy +#undef strlen + +static boolean loaded; +static struct data_array list_data; +struct data_array *object_list_header_data = &list_data; +static long arguments[2], returned; +static int segment_events; +static char last_event[256]; +static struct object_datum mock_object; +static int deleted_lists; +word const hs_object_type_masks[6] = {0xFFFF, 3, 2, 4, 0x380, 0x40}; +struct scenario *global_scenario_get(void) { return NULL; } +boolean ai_index_from_string(struct scenario *scenario, char const *name, long *result) { + (void)scenario; + if (strcmp(name, "falcon/pilot")) return FALSE; + *result = (long)0x80000018; return TRUE; +} +long object_list_from_ai_reference(long reference) { return reference == NONE ? NONE : 0; } +void object_list_delete(long list) { (void)list; ++deleted_lists; } + +void *csmemcpy(void *to, void const *from, unsigned long bytes) { return memcpy(to, from, bytes); } +void *csmemset(void *to, long value, unsigned long bytes) { return memset(to, value, bytes); } +long csstrcasecmp(char const *a, char const *b) { return _stricmp(a, b); } +void error(short priority, char const *format, ...) { + va_list args; + if (priority != _error_log) return; + ++segment_events; + va_start(args, format); + _vsnprintf(last_event, sizeof(last_event) - 1, format, args); + va_end(args); + last_event[sizeof(last_event) - 1] = 0; +} +boolean mcc_cache_tags_loaded(void) { return loaded; } +boolean mcc_cache_contains(void const *data, unsigned long bytes) { (void)data; (void)bytes; return loaded; } +int mcc_parameters_prepare(struct mcc_runtime *runtime, struct scenario *scenario, struct data_array *syntax) { + (void)runtime; (void)scenario; (void)syntax; return 1; +} +boolean hs_macro_function_parse(short index, long expression) { (void)index; (void)expression; return TRUE; } +long *hs_macro_function_evaluate(short function, long thread, boolean initialize) { + (void)function; (void)thread; return initialize ? arguments : NULL; +} +void hs_return(long thread, long value) { (void)thread; returned = value; } +short hs_find_function_by_name(char const *name) { + if (!strcmp(name, "sleep")) return 321; + if (!strcmp(name, "player_effect_set_max_rumble")) return 322; + return NONE; +} +short hs_find_global_by_name(char const *name) { return !strcmp(name, "known_global") ? (short)0x8009 : NONE; } +void *object_try_and_get_and_verify_type(long index, unsigned long mask) { + (void)mask; return index >= 0 && index <= 2 ? &mock_object : NULL; +} +real_point3d *object_get_origin(long index, real_point3d *point) { + point->x = index == 0 ? 1.f : index == 1 ? 8.f : 10.f; + point->y = point->z = 0; return point; +} +long object_list_get_first(long list, long *reference) { (void)list; *reference = 0; return 0; } +long object_list_get_next(long list, long *reference) { (void)list; return ++*reference == 1 ? 1 : NONE; } +void *datum_try_and_get(struct data_array *data, long handle) { + unsigned index = (unsigned)handle & 0xFFFFu; + struct hs_syntax_node *node; + if (data == &list_data) return handle == 0 ? arguments : NULL; + if (handle == NONE || index >= (unsigned)data->count) return NULL; + node = (struct hs_syntax_node *)data->data + index; + return node->datum_header == (short)((unsigned)handle >> 16) ? node : NULL; +} +void *mcc_runtime_pointer(struct mcc_runtime *runtime, uint32_t address, uint32_t bytes) { + uint32_t offset = address - runtime->report.tag_base; + return address >= runtime->report.tag_base && offset <= runtime->used && bytes <= runtime->used - offset ? + runtime->tags + offset : NULL; +} + +#define CHECK(value) do { if (!(value)) { fprintf(stderr, "check failed at line %d\n", __LINE__); return 1; } } while (0) +int main(int argc, char **argv) { + struct mcc_runtime runtime = {0}; + struct scenario *scenario; + struct data_array *syntax; + struct hs_syntax_node *nodes; + struct hs_function_definition *extension; + char *strings; + uint32_t pointer; + int expected = 1; + CHECK(argc == 2); + runtime.used = 0x100000; + runtime.tags = malloc(runtime.used); + CHECK(runtime.tags); + memset(runtime.tags, 0, runtime.used); + runtime.tag_index = runtime.tags + 40; + runtime.report.tag_base = (uint32_t)(uintptr_t)runtime.tags; + scenario = (struct scenario *)(runtime.tags + 0x100); + syntax = (struct data_array *)(runtime.tags + 0x1000); + nodes = (struct hs_syntax_node *)(syntax + 1); + strings = (char *)(runtime.tags + 0xF0000); + pointer = (uint32_t)(uintptr_t)scenario; + memcpy(runtime.tags + 60, &pointer, 4); + scenario->hs_syntax_data.address = syntax; + scenario->hs_syntax_data.size = sizeof(*syntax) + 32767 * sizeof(*nodes); + scenario->hs_string_constants.address = strings; + scenario->hs_string_constants.size = 128; + syntax->size = sizeof(*nodes); + syntax->maximum_count = 32767; + syntax->count = 4; + syntax->actual_count = 4; + syntax->first_free_absolute_index = 4; + syntax->valid = TRUE; + syntax->signature = 'd@t@'; + syntax->data = nodes; + nodes[0].datum_header = nodes[1].datum_header = nodes[2].datum_header = nodes[3].datum_header = 1; + nodes[0].flags = 8; + nodes[0].next_node_index = NONE; + nodes[0].data = 0x10001; + nodes[0].function_index = 777; + nodes[1].type = _hs_function_name; + nodes[1].flags = 1; + nodes[1].next_node_index = 0x10002; + nodes[2].type = _hs_type_object_list; + nodes[2].flags = 1; + nodes[2].next_node_index = 0x10003; + nodes[3].type = _hs_type_object; + nodes[3].flags = 1; + nodes[3].next_node_index = NONE; + strcpy(strings, "sleep"); + if (!strcmp(argv[1], "unknown")) { strcpy(strings, "not_implemented"); expected = 0; } + if (!strcmp(argv[1], "bad_child")) { nodes[0].data = 0x10008; expected = 0; } + if (!strcmp(argv[1], "child_salt")) { nodes[0].data = 0x20001; expected = 0; } + if (!strcmp(argv[1], "short_data")) { scenario->hs_syntax_data.size = sizeof(*syntax) + 4 * sizeof(*nodes); expected = 0; } + if (!strcmp(argv[1], "capacity")) syntax->count = 19002; + if (!strcmp(argv[1], "negative_count")) { syntax->count = -1; expected = 0; } + if (!strcmp(argv[1], "full_capacity")) { + syntax->count = 32767; + syntax->actual_count = 6; + nodes[32765] = nodes[0]; nodes[32765].data = 0x17FFE; + nodes[32766] = nodes[1]; nodes[32766].next_node_index = NONE; + } + if (!strcmp(argv[1], "unterminated")) { memset(strings, 'x', 128); expected = 0; } + if (!strcmp(argv[1], "alias")) strcpy(strings, "player_effect_set_max_vibrate"); + if (!strcmp(argv[1], "extension")) strcpy(strings, "objects_distance_to_object"); + if (!strcmp(argv[1], "segment")) { + strcpy(strings, "mcc_mission_segment"); + nodes[0].type = _hs_type_boolean; + nodes[2].type = _hs_type_string; + nodes[2].next_node_index = NONE; + } + if (!strcmp(argv[1], "global")) { + nodes[0].flags = 5; nodes[0].data = 0x8001; strcpy(strings, "known_global"); + } + CHECK(mcc_scripts_prepare(&runtime) == expected); + if (!expected) { free(runtime.tags); return 0; } + CHECK(syntax->maximum_count == 32767); + CHECK(syntax->data == nodes); + if (!strcmp(argv[1], "full_capacity")) { + CHECK(nodes[32765].function_index == 321 && nodes[32766].function_index == 321); + } + CHECK(!mcc_script_function(MCC_HS_DISTANCE_TO_OBJECT)); + CHECK(!mcc_script_function(MCC_HS_MISSION_SEGMENT)); + CHECK(mcc_script_find("objects_distance_to_object") == NONE); + CHECK(mcc_script_find("mcc_mission_segment") == NONE); + loaded = TRUE; + { + struct hs_syntax_node ai = {0}; + CHECK(mcc_script_ai_cast(_hs_type_ai, _hs_type_unit)); + ai.flags = 9; ai.constant_type = _hs_type_ai; ai.type = _hs_type_unit; + CHECK(mcc_script_parse_ai(&ai, "falcon/pilot") == 1 && ai.data == (long)0x80000018); + CHECK(mcc_script_parse_ai(&ai, "missing/pilot") == 0); + ai.flags |= 4; + CHECK(mcc_script_parse_ai(&ai, "falcon/pilot") == -1); + CHECK(mcc_script_cast_ai(_hs_type_unit, 24) == 0 && deleted_lists == 1); + CHECK(mcc_script_cast_ai(_hs_type_vehicle, 24) == NONE && deleted_lists == 2); + CHECK(mcc_script_cast_ai(_hs_type_object_list, 24) == 0 && deleted_lists == 2); + CHECK(mcc_script_cast_ai(_hs_type_unit, NONE) == NONE); + loaded = FALSE; + CHECK(!mcc_script_ai_cast(_hs_type_ai, _hs_type_unit)); + loaded = TRUE; + } + CHECK(mcc_script_find("player_effect_set_max_vibrate") == 322); + CHECK(mcc_script_find("MCC_MISSION_SEGMENT") == MCC_HS_MISSION_SEGMENT); + if (!strcmp(argv[1], "global")) CHECK((unsigned short)nodes[0].short_value == 0x8009); + else if (!strcmp(argv[1], "alias")) CHECK(nodes[0].function_index == 322); + else if (!strcmp(argv[1], "extension")) { + union { long bits; float value; } result; + CHECK(nodes[0].function_index == MCC_HS_DISTANCE_TO_OBJECT); + CHECK(mcc_script_call_valid(&nodes[0], syntax)); + nodes[3].type = _hs_type_real; + CHECK(!mcc_script_call_valid(&nodes[0], syntax)); + extension = mcc_script_function(MCC_HS_DISTANCE_TO_OBJECT); + CHECK(extension && extension->parameter_types[1] == _hs_type_object); + arguments[0] = 0; arguments[1] = 2; + extension->evaluate(MCC_HS_DISTANCE_TO_OBJECT, 0, TRUE); + result.bits = returned; + CHECK(result.value == 2.f); + arguments[0] = NONE; + extension->evaluate(MCC_HS_DISTANCE_TO_OBJECT, 0, TRUE); + result.bits = returned; + CHECK(result.value == -1.f); + } else if (!strcmp(argv[1], "segment")) { + char long_segment[200]; + CHECK(nodes[0].function_index == MCC_HS_MISSION_SEGMENT); + CHECK(nodes[1].function_index == MCC_HS_MISSION_SEGMENT); + CHECK(mcc_script_call_valid(&nodes[0], syntax)); + extension = mcc_script_function(MCC_HS_MISSION_SEGMENT); + CHECK(extension && extension->return_type == _hs_type_boolean); + CHECK(extension->parameter_count == 1 && extension->parameter_types[0] == _hs_type_string); + nodes[2].type = _hs_type_real; + CHECK(!mcc_script_call_valid(&nodes[0], syntax)); + nodes[2].type = _hs_type_string; + nodes[1].next_node_index = NONE; + CHECK(!mcc_script_call_valid(&nodes[0], syntax)); + nodes[1].next_node_index = 0x20002; + CHECK(!mcc_script_call_valid(&nodes[0], syntax)); + nodes[1].next_node_index = 0x10002; + nodes[2].next_node_index = 0x10003; + CHECK(!mcc_script_call_valid(&nodes[0], syntax)); + nodes[2].next_node_index = NONE; + arguments[0] = (long)(uintptr_t)"cine1_intro"; + returned = -123; + extension->evaluate(MCC_HS_MISSION_SEGMENT, 0, FALSE); + CHECK(returned == -123 && !segment_events); /* argument expression still yielding */ + extension->evaluate(MCC_HS_MISSION_SEGMENT, 0, TRUE); + CHECK(returned == TRUE && segment_events == 1); + CHECK(!strcmp(last_event, "mcc: mission segment 'cine1_intro'")); + arguments[0] = (long)(uintptr_t)"03_escape"; /* use the evaluated argument each time */ + extension->evaluate(MCC_HS_MISSION_SEGMENT, 0, TRUE); + CHECK(returned == TRUE && segment_events == 2); + CHECK(!strcmp(last_event, "mcc: mission segment '03_escape'")); + memset(long_segment, 'x', sizeof(long_segment)); long_segment[199] = 0; + arguments[0] = (long)(uintptr_t)long_segment; + extension->evaluate(MCC_HS_MISSION_SEGMENT, 0, TRUE); + CHECK(returned == TRUE && segment_events == 3 && strlen(last_event) == 151); + arguments[0] = 0; + extension->evaluate(MCC_HS_MISSION_SEGMENT, 0, TRUE); + CHECK(returned == FALSE && segment_events == 3); + loaded = FALSE; + CHECK(!mcc_script_function(MCC_HS_MISSION_SEGMENT)); + CHECK(mcc_script_find("mcc_mission_segment") == NONE); + } else CHECK(nodes[0].function_index == 321 && nodes[1].function_index == 321); + free(runtime.tags); + return 0; +} diff --git a/tools/harness/tests/mcc_syntax.c b/tools/harness/tests/mcc_syntax.c new file mode 100644 index 0000000000..7b3b099504 --- /dev/null +++ b/tools/harness/tests/mcc_syntax.c @@ -0,0 +1,203 @@ +/* Generated MCC syntax blobs: no shipped game data or external resources. */ +#include "cseries.h" +#include "math/real_math.h" +#include "errors.h" +#include "hs/hs.h" +#include "hs/hs_scenario_definitions.h" +#include "memory/data.h" +#include "scenario/scenario_definitions.h" +#include "mcc_syntax.h" +#include +#include +#include +#include +#undef memset +#undef memcpy +#undef memcmp +#undef malloc +#undef free +#undef strcmp +#undef strcpy + +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "check failed at line %d\n", __LINE__); return 1; } } while (0) + +static boolean loaded; +static unsigned char *allocation; +static long allocation_bytes; +static char strings[1024]; +static unsigned errors, visited; + +void *csmemset(void *buffer, long value, unsigned long bytes) { return memset(buffer, (int)value, bytes); } +boolean mcc_cache_tags_loaded(void) { return loaded; } +boolean mcc_cache_contains(void const *pointer, long bytes) +{ + uintptr_t address = (uintptr_t)pointer, base = (uintptr_t)allocation; + if (bytes < 0) return FALSE; + if (address >= base && address - base <= (unsigned long)allocation_bytes && + (unsigned long)bytes <= (unsigned long)allocation_bytes - (address - base)) return TRUE; + return pointer == strings && bytes <= sizeof(strings); +} +void error(short severity, char const *format, ...) { (void)severity; (void)format; ++errors; } + +static long handle(unsigned index) { return (long)(0x81230000u | index); } +static short refusal(struct hs_syntax_node const *node, char const **name) +{ + ++visited; + *name = node->source_offset == 99 ? "blocked_function" : NULL; + return node->source_offset == 99 ? 1 : 0; +} +static void live_node(struct hs_syntax_node *node) +{ + node->datum_header = (short)0x8123; + node->flags = 9; + node->type = node->constant_type = _hs_type_boolean; + node->next_node_index = NONE; +} + +int main(int argc, char **argv) +{ + struct data_array *syntax; + struct hs_syntax_node *nodes; + struct scenario scenario = {0}; + struct hs_script scripts[2] = {0}; + struct hs_global globals[1] = {0}; + unsigned long disabled_scripts[16] = {0}, disabled_globals[32] = {0}; + unsigned char *before; + long bytes; + unsigned i; + int expected = 1; + char const *test; + CHECK(argc == 2); + test = argv[1]; + allocation_bytes = sizeof(*syntax) + MCC_SYNTAX_CAPACITY * sizeof(*nodes); + allocation = malloc(allocation_bytes); + before = malloc(allocation_bytes); + CHECK(allocation && before); + memset(allocation, 0, allocation_bytes); + syntax = (struct data_array *)allocation; + nodes = (struct hs_syntax_node *)(syntax + 1); + syntax->signature = 'd@t@'; + syntax->size = sizeof(*nodes); + syntax->maximum_count = MCC_SYNTAX_CAPACITY; + syntax->valid = TRUE; + syntax->identifier_zero_invalid = TRUE; + syntax->count = syntax->actual_count = 2; + syntax->data = (void *)1; /* File pointer is stale; validation must ignore it. */ + for (i = 0; i < 2; ++i) live_node(nodes + i); + bytes = allocation_bytes; + + if (!strcmp(test, "empty")) { + syntax->count = syntax->actual_count = 0; + memset(nodes, 0, 2 * sizeof(*nodes)); + } else if (!strcmp(test, "high_slot") || !strcmp(test, "callbacks")) { + syntax->count = MCC_SYNTAX_CAPACITY; + memset(nodes + 1, 0, sizeof(*nodes)); + live_node(nodes + MCC_SYNTAX_CAPACITY - 1); + nodes[0].flags = 8; + nodes[0].data = handle(MCC_SYNTAX_CAPACITY - 1); + } else if (!strcmp(test, "sparse")) { + syntax->count = 19723; + syntax->actual_count = 18480; + memset(nodes, 0, 2 * sizeof(*nodes)); + for (i = 1243; i < 19723; ++i) live_node(nodes + i); + } else if (!strcmp(test, "deep") || !strcmp(test, "wide") || !strcmp(test, "deep_cycle")) { + syntax->count = syntax->actual_count = MCC_SYNTAX_CAPACITY; + for (i = 0; i < MCC_SYNTAX_CAPACITY; ++i) { + live_node(nodes + i); + if (i + 1 < MCC_SYNTAX_CAPACITY) { + if (!strcmp(test, "wide") && i) nodes[i].next_node_index = handle(i + 1); + else { nodes[i].flags = 8; nodes[i].data = handle(i + 1); } + } + } + if (!strcmp(test, "deep_cycle")) { + nodes[MCC_SYNTAX_CAPACITY - 1].next_node_index = handle(0); + expected = 0; + } + } else if (!strcmp(test, "salt")) { nodes[0].next_node_index = handle(1) ^ 0x10000; expected = 0; + } else if (!strcmp(test, "zero_salt")) { nodes[0].next_node_index = 1; expected = 0; + } else if (!strcmp(test, "out_of_range")) { nodes[0].next_node_index = handle(32767); expected = 0; + } else if (!strcmp(test, "unused")) { nodes[1].datum_header = 0; syntax->actual_count = 1; nodes[0].next_node_index = handle(1); expected = 0; + } else if (!strcmp(test, "sibling_cycle")) { nodes[0].next_node_index = handle(1); nodes[1].next_node_index = handle(0); expected = 0; + } else if (!strcmp(test, "call_cycle")) { nodes[0].flags = 8; nodes[0].data = handle(0); expected = 0; + } else if (!strcmp(test, "short_blob")) { --bytes; expected = 0; + } else if (!strcmp(test, "long_blob")) { ++bytes; expected = 0; + } else if (!strcmp(test, "negative_count")) { syntax->count = -1; expected = 0; + } else if (!strcmp(test, "negative_capacity")) { syntax->maximum_count = (short)0x8000; expected = 0; + } else if (!strcmp(test, "count_over_capacity")) { syntax->maximum_count = 1; expected = 0; + } else if (!strcmp(test, "actual_count")) { syntax->actual_count = 1; expected = 0; + } else if (!strcmp(test, "negative_actual")) { syntax->actual_count = -1; expected = 0; + } else if (!strcmp(test, "free_hint")) { syntax->first_free_absolute_index = -1; expected = 0; + } else if (!strcmp(test, "signature")) { syntax->signature = 0; expected = 0; + } else if (!strcmp(test, "node_size")) { ++syntax->size; expected = 0; + } else if (!strcmp(test, "invalid_array")) { syntax->valid = FALSE; expected = 0; + } + memcpy(before, allocation, allocation_bytes); + CHECK(mcc_syntax_graph_valid(syntax, bytes) == expected); + CHECK(!memcmp(before, allocation, allocation_bytes)); + CHECK(!mcc_syntax_graph_valid(NULL, bytes)); + CHECK(!mcc_syntax_graph_valid(syntax, 1)); + CHECK(!mcc_syntax_graph_valid((struct data_array *)(allocation + 1), bytes)); + if (!expected) { free(before); free(allocation); return 0; } + + scenario.hs_syntax_data.address = syntax; + scenario.hs_syntax_data.size = bytes; + scenario.hs_string_constants.address = strings; + scenario.hs_string_constants.size = sizeof(strings); + CHECK(mcc_syntax_scenario_valid((struct scenario *)1) == -1); + CHECK(!mcc_syntax_functions_check((struct scenario *)1, (struct data_array *)1, NULL, NULL, NULL)); + loaded = TRUE; + CHECK(mcc_syntax_scenario_valid(&scenario) == 1); + scenario.hs_string_constants.size = 1023; + CHECK(mcc_syntax_scenario_valid(&scenario) == 0); + scenario.hs_string_constants.size = 1024; + scenario.hs_syntax_data.size = bytes + 1; + CHECK(mcc_syntax_scenario_valid(&scenario) == 0); + scenario.hs_syntax_data.size = bytes; + scenario.hs_syntax_data.address = (void *)1; + CHECK(mcc_syntax_scenario_valid(&scenario) == 0); + scenario.hs_syntax_data.address = syntax; + CHECK(mcc_syntax_scenario_valid(NULL) == 0); + + if (!strcmp(test, "callbacks") || !strcmp(test, "wide") || !strcmp(test, "deep") || + !strcmp(test, "root_type") || !strcmp(test, "damaged_root") || !strcmp(test, "shared_child")) { + scenario.hs_scripts.count = 1; + scenario.hs_scripts.address = scripts; + scenario.hs_globals.count = 1; + scenario.hs_globals.address = globals; + strcpy(scripts[0].name, "script"); + scripts[0].root_expression_index = handle(0); + scripts[0].return_type = _hs_type_boolean; + globals[0].initialization_expression_index = NONE; + globals[0].type = _hs_type_boolean; + if (!strcmp(test, "root_type")) scripts[0].return_type = _hs_type_void; + if (!strcmp(test, "damaged_root")) scripts[0].root_expression_index = handle(32767); + if (!strcmp(test, "shared_child")) { + syntax->count = syntax->actual_count = 3; + live_node(nodes + 2); + nodes[0].flags = nodes[1].flags = 8; + nodes[0].data = handle(1); + nodes[1].data = handle(2); + nodes[1].next_node_index = handle(2); /* Acyclic, but two references within one expression. */ + CHECK(mcc_syntax_graph_valid(syntax, bytes)); + } + CHECK(mcc_syntax_functions_check(&scenario, syntax, refusal, disabled_scripts, disabled_globals)); + if (!strcmp(test, "root_type") || !strcmp(test, "damaged_root") || !strcmp(test, "shared_child")) { + CHECK(disabled_scripts[0] == 1 && !disabled_globals[0] && errors == 1); + } else { + CHECK(!disabled_scripts[0] && !disabled_globals[0] && !errors); + CHECK(visited == (unsigned)syntax->actual_count); + } + if (!strcmp(test, "callbacks")) { + nodes[MCC_SYNTAX_CAPACITY - 1].source_offset = 99; + globals[0].initialization_expression_index = handle(MCC_SYNTAX_CAPACITY - 1); + CHECK(mcc_syntax_functions_check(&scenario, syntax, refusal, disabled_scripts, disabled_globals)); + CHECK(disabled_scripts[0] == 1 && disabled_globals[0] == 1 && errors == 1); + loaded = FALSE; + CHECK(!mcc_syntax_functions_check(&scenario, syntax, refusal, disabled_scripts, disabled_globals)); + CHECK(disabled_scripts[0] == 1 && disabled_globals[0] == 1 && errors == 1); + } + } + free(before); + free(allocation); + return 0; +} diff --git a/tools/harness/tests/mcc_tag_validation.c b/tools/harness/tests/mcc_tag_validation.c new file mode 100644 index 0000000000..7eec716cf8 --- /dev/null +++ b/tools/harness/tests/mcc_tag_validation.c @@ -0,0 +1,295 @@ +/* Real MCC walker and legacy helper dispatch, with a tiny schema and no assets. */ +#include "cseries.h" +#include "tag_schema.h" +#include "mcc_runtime.h" +#include "mcc_tag_validate.h" +#include "mcc_syntax.h" +#include +#include +#include + +#define CHECK(c) do { if (!(c)) { fprintf(stderr, "line %d: %s\n", __LINE__, #c); return 1; } } while (0) + +struct test_instance { unsigned long group, parent[2]; long handle; char *name; void *root; unsigned long unused[2]; }; +struct test_header { + struct test_instance *instances; long scenario; unsigned long checksum; long count; + long vertex_count; void *vertices; long index_count; void *indices; unsigned long signature; +}; +struct test_child { short enumeration; short pad; struct tag_data data; struct tag_reference reference; }; +struct test_root { + struct tag_block children; short index; short enumeration; struct tag_data file; + struct tag_data hs_syntax_data, other_data; +}; +struct test_bsp { void *root; long vcount; void *vertices; long icount; void *indices; unsigned long signature; }; +struct test_device { short kind; unsigned short flags; struct tag_data identifier, profile; }; +struct test_padded_inner { short kind; byte unused[14]; }; +struct test_padded_root { struct test_padded_inner inner; }; +struct test_graph { struct tag_block nodes, animations; }; +struct test_graph_node { short next_sibling_node_index, first_child_node_index, parent_node_index; }; +struct test_animation { short next_animation_index; }; +struct test_first_person { struct tag_block animations; }; +struct test_grenades {struct tag_block grenades;}; +struct test_equipment {short grenade_type;}; +static int callback_calls, callback_failures; +static void *valid_bitmap; +static unsigned char external_vertices[32], external_indices[8]; +static int hardware; + +static boolean check_root(struct tag_validation *validation, void *base) +{ + int mcc = mcc_validation_owns(validation); + void *scenario = tag_validate_tag_get(validation, (long)0xe0000000UL, 'scnr'); + ++callback_calls; + if (tag_validate_root(validation) != base || !tag_validate_contains(validation, base, sizeof(struct test_root)) || + tag_validate_custom_edition(validation) || !tag_validate_tag_get(validation, (long)0xe0000000UL, 'scnr') || + !tag_validate_file_contains(validation, 100, 10) || tag_validate_file_contains(validation, 1020, 20)) ++callback_failures; + if (mcc && (!mcc_validation_callback_active() || !tag_validate_any_claimed(base, sizeof(struct test_root)) || + tag_validate_vertex_buffer_data(validation, &hardware) != external_vertices || + tag_validate_index_buffer_data(validation, &hardware) != (scenario == base ? external_indices : external_vertices) || + !tag_validate_contains(validation, external_vertices, sizeof(external_vertices)) || + tag_validate_any_claimed(external_indices, sizeof(external_indices)))) ++callback_failures; + return TRUE; +} +static struct tag_schema_field const child_fields[] = { + TAG_SCHEMA_ENUM(struct test_child, enumeration, 3, 0), + TAG_SCHEMA_DATA(struct test_child, data, 128), + TAG_SCHEMA_REFERENCE(struct test_child, reference, TAG_SCHEMA_GROUPS('scnr')), + TAG_SCHEMA_END +}; +static struct tag_schema_definition const child_schema = TAG_SCHEMA_DEFINITION(child, struct test_child, child_fields); +static struct tag_schema_field const root_fields[] = { + TAG_SCHEMA_BLOCK(struct test_root, children, child_schema, 4), + TAG_SCHEMA_BLOCK_INDEX(struct test_root, index, TAG_SCHEMA_ROOT, offsetof(struct test_root, children), 0), + TAG_SCHEMA_ENUM(struct test_root, enumeration, 4, 0), + TAG_SCHEMA_FILE_DATA(struct test_root, file, 256), + TAG_SCHEMA_DATA(struct test_root, hs_syntax_data, 56L + 20L * 19001), + TAG_SCHEMA_DATA(struct test_root, other_data, 128), + TAG_SCHEMA_CHECK(check_root), + TAG_SCHEMA_END +}; +static struct tag_schema_definition const root_schema = TAG_SCHEMA_DEFINITION(scenario, struct test_root, root_fields); +static struct tag_schema_field const padded_inner_fields[] = { + TAG_SCHEMA_ENUM(struct test_padded_inner, kind, 3, 0), TAG_SCHEMA_END +}; +static struct tag_schema_definition const padded_inner_schema = {"padded_inner", 2, padded_inner_fields}; +static struct tag_schema_field const padded_root_fields[] = { + TAG_SCHEMA_STRUCT(struct test_padded_root, inner, padded_inner_schema), TAG_SCHEMA_END +}; +static struct tag_schema_definition const padded_root_schema = {"padded_root", 2, padded_root_fields}; +static boolean animation_graph_check(struct tag_validation *validation, void *base) +{ (void)validation; (void)base; ++callback_failures; return FALSE; } +static boolean bitmap_data_check(struct tag_validation *validation, void *base) +{ (void)validation; (void)base; ++callback_failures; return FALSE; } +static struct tag_schema_field const graph_node_fields[] = { + TAG_SCHEMA_BLOCK_INDEX(struct test_graph_node, next_sibling_node_index, TAG_SCHEMA_ROOT, offsetof(struct test_graph,nodes), FLAG(_tag_schema_none_bit)), + TAG_SCHEMA_BLOCK_INDEX(struct test_graph_node, first_child_node_index, TAG_SCHEMA_ROOT, offsetof(struct test_graph,nodes), FLAG(_tag_schema_none_bit)), + TAG_SCHEMA_BLOCK_INDEX(struct test_graph_node, parent_node_index, TAG_SCHEMA_ROOT, offsetof(struct test_graph,nodes), FLAG(_tag_schema_none_bit)), + TAG_SCHEMA_END +}; +static struct tag_schema_definition const graph_node_schema = TAG_SCHEMA_DEFINITION(node, struct test_graph_node, graph_node_fields); +static struct tag_schema_field const animation_fields[] = { + TAG_SCHEMA_BLOCK_INDEX(struct test_animation,next_animation_index,TAG_SCHEMA_ROOT,offsetof(struct test_graph,animations),FLAG(_tag_schema_none_bit)), TAG_SCHEMA_END +}; +static struct tag_schema_definition const animation_schema = TAG_SCHEMA_DEFINITION(animation,struct test_animation,animation_fields); +static struct tag_schema_field const graph_fields[] = { + TAG_SCHEMA_BLOCK(struct test_graph,nodes,graph_node_schema,64), + TAG_SCHEMA_TOOL_BLOCK(struct test_graph,animations,animation_schema,256), + TAG_SCHEMA_CHECK(animation_graph_check), TAG_SCHEMA_END +}; +static struct tag_schema_definition const graph_schema = TAG_SCHEMA_DEFINITION(graph,struct test_graph,graph_fields); +static struct tag_schema_field const bitmap_fields[] = {TAG_SCHEMA_CHECK(bitmap_data_check),TAG_SCHEMA_END}; +static struct tag_schema_definition const bitmap_schema = {"bitmap",4,bitmap_fields}; +static struct tag_schema_field const fp_element_fields[] = {TAG_SCHEMA_END}; +static struct tag_schema_definition const fp_element_schema = {"index",2,fp_element_fields}; +static struct tag_schema_field const fp_fields[] = {TAG_SCHEMA_BLOCK(struct test_first_person,animations,fp_element_schema,28),TAG_SCHEMA_END}; +static struct tag_schema_definition const fp_schema = {"animation_graph_first_person_weapon_animations",sizeof(struct test_first_person),fp_fields}; +static struct tag_schema_field const grenade_fields[] = {TAG_SCHEMA_BLOCK(struct test_grenades,grenades,fp_element_schema,2),TAG_SCHEMA_END}; +static struct tag_schema_definition const grenade_schema = {"game_globals",sizeof(struct test_grenades),grenade_fields}; +static struct tag_schema_field const equipment_fields[] = {TAG_SCHEMA_ENUM(struct test_equipment,grenade_type,2,0),TAG_SCHEMA_END}; +static struct tag_schema_definition const equipment_schema = {"equipment",sizeof(struct test_equipment),equipment_fields}; +static struct tag_schema_group const groups[] = { + {'scnr', {NONE, NONE}, &root_schema}, {'sbsp', {NONE, NONE}, &root_schema}, + {'antr', {NONE, NONE}, &graph_schema}, {'bitm', {NONE, NONE}, &bitmap_schema}, {'afp!', {NONE,NONE}, &fp_schema}, + {'matg', {NONE,NONE}, &grenade_schema}, {'eqip',{NONE,NONE}, &equipment_schema}, + {'trim', {NONE, NONE}, &padded_root_schema}, {0, {0, 0}, NULL} +}; +struct tag_schema_group const *const tag_schema_group_lists[] = {groups, NULL}; +struct tag_schema_group const tag_schema_custom_edition_groups[] = {{0, {0, 0}, NULL}}; + +void tag_validate_report(char const *message) { (void)message; } +uint32_t mcc_audio_stream_end(struct mcc_runtime *runtime) { (void)runtime; return 1024; } +uint32_t mcc_bitmaps_stream_end(struct mcc_runtime *runtime) { (void)runtime; return 1024; } +int mcc_audio_contains(struct mcc_runtime *runtime, uint32_t offset, uint32_t size) +{ (void)runtime; (void)offset; (void)size; return 0; } +int mcc_bitmaps_contains(struct mcc_runtime *runtime, uint32_t offset, uint32_t size) +{ (void)runtime; (void)offset; (void)size; return 0; } +int mcc_bitmaps_valid(struct mcc_runtime *runtime, struct bitmap_data *bitmap) +{ (void)runtime; return bitmap == valid_bitmap; } +void *mcc_geometry_buffer_data(struct mcc_runtime *runtime, void const *buffer, int indices, unsigned long *bytes) +{ + (void)runtime; *bytes = indices ? sizeof(external_indices) : sizeof(external_vertices); + return buffer == &hardware ? indices ? (void *)external_indices : external_vertices : NULL; +} +int mcc_geometry_contains(struct mcc_runtime *runtime, void const *address, unsigned long bytes) +{ + uintptr_t a = (uintptr_t)address, v = (uintptr_t)external_vertices, i = (uintptr_t)external_indices; + (void)runtime; + return (a >= v && a - v <= sizeof(external_vertices) && bytes <= sizeof(external_vertices) - (a - v)) || + (a >= i && a - i <= sizeof(external_indices) && bytes <= sizeof(external_indices) - (a - i)); +} + +int main(int argc, char **argv) +{ + struct mcc_runtime runtime; + struct test_header *header; + struct test_instance *instance; + struct test_root *root; + struct test_child *child; + int expected = 1; + CHECK(argc == 2); + memset(&runtime, 0, sizeof(runtime)); + runtime.capacity = 65536; runtime.used = 4096; runtime.source.size = 1024; + if (!strncmp(argv[1], "syntax_", 7)) runtime.capacity = runtime.used = 1048576; + runtime.tags = calloc(runtime.capacity, 1); CHECK(runtime.tags != NULL); + header = (struct test_header *)runtime.tags; + instance = (struct test_instance *)(runtime.tags + 0x28); + root = (struct test_root *)(runtime.tags + 0x100); + child = (struct test_child *)(runtime.tags + 0x200); + header->instances = instance; header->count = 1; header->scenario = (long)0xe0000000UL; header->signature = 'tags'; + instance->group = 'scnr'; instance->parent[0] = instance->parent[1] = NONE; + instance->handle = header->scenario; instance->root = root; instance->name = (char *)runtime.tags + 0x180; + strcpy(instance->name, "scenario"); + root->children.count = 1; root->children.address = child; root->index = 7; root->enumeration = 9; + root->file.file_offset = 100; root->file.size = 8; + child->data.address = runtime.tags + 0x300; child->data.size = 16; + child->reference.group_tag = 'scnr'; child->reference.index = header->scenario; child->reference.name = NULL; + if (!strcmp(argv[1], "overlap")) { child->data.address = root; expected = 0; } + if (!strcmp(argv[1], "outside")) { root->children.address = runtime.tags + runtime.used - 1; expected = 0; } + if (!strcmp(argv[1], "misaligned")) { root->children.address = (byte *)child + 1; expected = 0; } + if (!strcmp(argv[1], "negative")) { root->children.count = -1; expected = 0; } + if (!strcmp(argv[1], "cycle")) { root->children.address = root; expected = 0; } + if (!strcmp(argv[1], "file_range")) { root->file.file_offset = 1023; expected = 0; } + if (!strcmp(argv[1], "stream_hole")) { root->file.file_offset = 0x4ffffff0; expected = 0; } + if (!strcmp(argv[1], "handle")) { instance->handle++; expected = 0; } + if (!strcmp(argv[1], "unknown_group")) { instance->group = 'nope'; expected = 0; } + if (!strcmp(argv[1], "bsp")) { + header->count = 2; instance[1] = instance[0]; instance[1].group = 'sbsp'; instance[1].handle++; + } + if (!strncmp(argv[1], "collection", 10)) { + struct tag_block *collection = (struct tag_block *)(runtime.tags + 0x500); + struct tag_reference *reference = (struct tag_reference *)(runtime.tags + 0x600); + header->count = 2; instance[1] = instance[0]; instance[1].group = 'tagc'; instance[1].handle++; + instance[1].root = collection; collection->count = 1; collection->address = reference; + *reference = child->reference; + if (!strcmp(argv[1], "collection_outside")) { collection->address = runtime.tags + runtime.used - 1; expected = 0; } + } + if (!strncmp(argv[1], "device", 6)) { + struct test_device *device = (struct test_device *)(runtime.tags + 0x500); + header->count = 2; instance[1] = instance[0]; instance[1].group = 'devc'; instance[1].handle++; + instance[1].root = device; device->kind = 1; + device->identifier.address = runtime.tags + 0x600; device->identifier.size = 16; + device->profile.address = runtime.tags + 0x700; device->profile.size = 128; + if (!strcmp(argv[1], "device_outside")) { device->profile.size = 4096; expected = 0; } + } + if (!strcmp(argv[1], "trimmed_structure")) { + header->count = 2; instance[1] = instance[0]; instance[1].group = 'trim'; instance[1].handle++; + instance[1].root = runtime.tags + runtime.used - 2; + } + if (!strncmp(argv[1],"graph",5)) { + struct test_graph *graph=(struct test_graph *)(runtime.tags+0x700); + struct test_graph_node *nodes=(struct test_graph_node *)(runtime.tags+0x800); + struct test_animation *animations=(struct test_animation *)(runtime.tags+0x900); + int i; + header->count=2; instance[1]=instance[0]; instance[1].group='antr'; instance[1].handle++; instance[1].root=graph; + graph->nodes.count=2; graph->nodes.address=nodes; graph->animations.count=257; graph->animations.address=animations; + nodes[0].next_sibling_node_index=NONE; nodes[0].first_child_node_index=1; nodes[0].parent_node_index=0; + nodes[1].next_sibling_node_index=NONE; nodes[1].first_child_node_index=NONE; nodes[1].parent_node_index=0; + for(i=0;i<257;i++) animations[i].next_animation_index=i==256?NONE:(short)(i+1); + if (!strcmp(argv[1],"graph_cycle")) {animations[256].next_animation_index=0; expected=0;} + if (!strcmp(argv[1],"graph_node_cycle")) {nodes[1].first_child_node_index=0; expected=0;} + if (!strcmp(argv[1],"graph_orphan")) {nodes[0].first_child_node_index=NONE; expected=0;} + } + if (!strncmp(argv[1],"bitmap",6)) { + header->count=2; instance[1]=instance[0]; instance[1].group='bitm'; instance[1].handle++; + instance[1].root=runtime.tags+0x700; valid_bitmap=instance[1].root; + if(!strcmp(argv[1],"bitmap_unowned")){valid_bitmap=NULL;expected=0;} + } + if (!strcmp(argv[1],"first_person_slots")) { + struct test_first_person *fp=(struct test_first_person *)(runtime.tags+0x700); + header->count=2; instance[1]=instance[0]; instance[1].group='afp!'; instance[1].handle++; instance[1].root=fp; + fp->animations.count=30; fp->animations.address=runtime.tags+0x800; + } + if (!strncmp(argv[1],"grenades",8)) { + struct test_grenades *globals=(struct test_grenades *)(runtime.tags+0x700); + struct test_equipment *equipment=(struct test_equipment *)(runtime.tags+0x900); + header->count=3; instance[1]=instance[0]; instance[1].group='matg'; instance[1].handle++; instance[1].root=globals; + instance[2]=instance[0];instance[2].group='eqip';instance[2].handle+=2;instance[2].root=equipment;equipment->grenade_type=3; + globals->grenades.count=4;globals->grenades.address=runtime.tags+0x800; + if(!strcmp(argv[1],"grenades_two")){globals->grenades.count=2;equipment->grenade_type=1;} + if(!strcmp(argv[1],"grenades_one")){globals->grenades.count=1;expected=0;} + if(!strcmp(argv[1],"grenades_zero")){globals->grenades.count=0;expected=0;} + if(!strcmp(argv[1],"grenades_excess")){globals->grenades.count=5;expected=0;} + } + if (!strcmp(argv[1], "legacy_isolation")) { + CHECK(tag_validate_tags(header, runtime.used, runtime.source.size, "legacy")); + CHECK(tag_validate_corrections() == 3); + } + if (!strncmp(argv[1], "syntax_", 7)) { + root->hs_syntax_data.address = runtime.tags + 0x1000; + root->hs_syntax_data.size = MCC_SYNTAX_MAXIMUM_DATA_BYTES; + ((byte *)root->hs_syntax_data.address)[MCC_SYNTAX_MAXIMUM_DATA_BYTES - 1] = 0xA5; + root->other_data.address = runtime.tags + 0xB0000; + root->other_data.size = 256; + if (!strcmp(argv[1], "syntax_overflow")) root->hs_syntax_data.size += 20; + if (!strcmp(argv[1], "syntax_outside")) { + root->hs_syntax_data.address = runtime.tags + runtime.used - 1; expected = 0; + } + if (!strcmp(argv[1], "syntax_overlap")) { + root->other_data.address = (byte *)root->hs_syntax_data.address + 32; expected = 0; + } + if (!strcmp(argv[1], "syntax_legacy")) { + CHECK(tag_validate_tags(header, runtime.used, runtime.source.size, "legacy syntax")); + CHECK(root->hs_syntax_data.size == 56L + 20L * 19001); + root->hs_syntax_data.size = MCC_SYNTAX_MAXIMUM_DATA_BYTES; + } + } + CHECK(mcc_tags_validate(&runtime) == expected); + CHECK(!mcc_validation_callback_active()); + if (!strncmp(argv[1], "syntax_", 7) && expected) { + CHECK(root->hs_syntax_data.size == MCC_SYNTAX_MAXIMUM_DATA_BYTES); + CHECK(root->hs_syntax_data.address == runtime.tags + 0x1000); + CHECK(((byte *)root->hs_syntax_data.address)[MCC_SYNTAX_MAXIMUM_DATA_BYTES - 1] == 0xA5); + CHECK(root->other_data.size == 128); + if (!strcmp(argv[1], "syntax_legacy")) { + CHECK(tag_validate_tags(header, runtime.used, runtime.source.size, "legacy syntax again")); + CHECK(root->hs_syntax_data.size == 56L + 20L * 19001); + } + } + if (!strcmp(argv[1],"first_person_slots")) CHECK(((struct test_first_person *)instance[1].root)->animations.count==30); + if (!strcmp(argv[1],"grenades_four")) { + CHECK(((struct test_grenades *)instance[1].root)->grenades.count==4); + CHECK(((struct test_equipment *)instance[2].root)->grenade_type==3); + } + if (expected) { + CHECK(root->index == 0 && root->enumeration == 0); + CHECK(child->reference.name == instance->name && child->reference.name_length == 8); + CHECK(callback_calls > 0 && callback_failures == 0); + } + if (!strcmp(argv[1], "legacy_isolation")) { + CHECK(tag_validate_corrections() == 3); + CHECK(tag_validate_claimed(root)); + CHECK(tag_validate_tags(header, runtime.used, runtime.source.size, "legacy again")); + CHECK(tag_validate_corrections() == 0 && callback_failures == 0); + } + if (!strcmp(argv[1], "bsp")) { + struct test_bsp *bsp = (struct test_bsp *)(runtime.tags + 8192); + struct test_root *broot = (struct test_root *)((byte *)bsp + 64); + bsp->root = broot; bsp->signature = 'sbsp'; broot->file.file_offset = 100; + CHECK(mcc_bsp_validate(&runtime, instance[1].handle, bsp, 512)); + CHECK(callback_failures == 0); + broot->children.count = 1; broot->children.address = root; + CHECK(!mcc_bsp_validate(&runtime, instance[1].handle, bsp, 512)); + } + mcc_validation_dispose(&runtime); free(runtime.tags); + return 0; +} diff --git a/tools/harness/tests/server_teardown.c b/tools/harness/tests/server_teardown.c index 20f26d085a..2c7c00580e 100644 --- a/tools/harness/tests/server_teardown.c +++ b/tools/harness/tests/server_teardown.c @@ -36,6 +36,10 @@ static int new_servers, clients_closed, joins_cleared, variants_cleared; static short expected_message; static boolean message_fails, send_fails, machines_fail; +/* These legacy teardown cases have no pending MCC restart. Its independent + snapshot lifetime is exercised by test_mcc_ui_network.py. */ +static void mcc_ui_network_server_dispose(struct network_game_server *server) { (void)server; } + static void note(char event) { size_t length = strlen(order); diff --git a/tools/port_settings.py b/tools/port_settings.py index 944783f630..13151b3aa4 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -1342,7 +1342,7 @@ def _map_kind() -> list: ("header_bounds", "7 -6 19 0"), ("footer_bounds", "7 238 19 244")], ['']) lines += _strings("main_menu/new_select/var_map_kinds", - ["SINGLEPLAYER", "MULTIPLAYER", "CUSTOM SINGLEPLAYER", "CUSTOM MULTIPLAYER"]) + ["SINGLEPLAYER", "MULTIPLAYER", "CUSTOM SINGLEPLAYER", "CUSTOM MULTIPLAYER", "MCC SINGLEPLAYER", "MCC MULTIPLAYER"]) return lines diff --git a/tools/test_mcc_bitmap_flags.py b/tools/test_mcc_bitmap_flags.py new file mode 100644 index 0000000000..104366198d --- /dev/null +++ b/tools/test_mcc_bitmap_flags.py @@ -0,0 +1,183 @@ +"""Exercise MCC descriptor conversion with generated pixels, without map assets.""" +from pathlib import Path +import shutil +import subprocess +import sys + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from harness import function, structure + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="module") +def bitmap_converter(tmp_path_factory): + compiler = shutil.which("clang") + if not compiler: + pytest.skip("clang is needed for MCC bitmap conversion tests") + bitmap = (ROOT / "port/linux/game/mcc_bitmaps.c").read_text() + native = (ROOT / "source/bitmaps/bitmaps.c").read_text() + swizzle = (ROOT / "source/rasterizer/rasterizer_swizzle.c").read_text() + source = r''' +#include +#include +#include +#include +#define MIN(a,b) ((a)<(b)?(a):(b)) +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define TRUE 1 +#define FALSE 0 +#define NONE -1 +#define FLAG(bit) (1u<<(bit)) +#define TEST_FLAG(value,bit) ((value)&FLAG(bit)) +#define VALID_INDEX(index,count) ((index)>=0 && (index)<(count)) +#define NUMBER_OF_BITMAP_FLAGS 9 +#define NUMBER_OF_BITMAP_TYPES 3 +#define NUMBER_OF_BITMAP_FORMATS 18 +#define BITMAP_GROUP_TAG 0x6269746du +#define _bitmap_format_a8r8g8b8 11 +#define _bitmap_has_power_of_two_dimensions_bit 0 +#define _bitmap_compressed_bit 1 +#define _bitmap_palettized_bit 2 +#define _bitmap_swizzled_bit 3 +#define _bitmap_linear_bit 4 +#define _error_silent 0 +#define error(...) ((void)0) +#define match_assert(file,line,condition) do {if(!(condition))abort();} while(0) +#define MCC_BITMAP_BASE 0x60000000u +#define MCC_BITMAP_LIMIT 0x1FFFFFFFu +#define MCC_BITMAP_ALLOCATION_LIMIT 0x08000000u +typedef unsigned char boolean; +struct bitmap_data { + uint32_t signature; + short width,height,depth,type,format; + unsigned short flags; + short mipmap_count; + int32_t pixels_offset,pixels_size,tag_index,cache_block_index; + void *hardware_format,*base_address; +}; +struct bitmap_group {short usage;}; +struct mcc_runtime {struct bitmap_group group;unsigned char source[350000];uint32_t size,reads;}; +static short floor_log2(unsigned value) {short n=0;while(value>1){value>>=1;n++;}return n;} +static int bitmap_format_type_valid_width(short f,short t,short v) {(void)f;(void)t;return v>0 && v<=4096;} +static int bitmap_format_type_valid_height(short f,short t,short v) {return bitmap_format_type_valid_width(f,t,v);} +static int bitmap_format_type_valid_depth(short f,short t,short v) {(void)f;(void)t;return v==1;} +static void *mcc_bitmap_tag(struct mcc_runtime *r,uint32_t h,uint32_t g,uint32_t s) { + (void)s;return h==7 && g==BITMAP_GROUP_TAG ? &r->group : NULL; +} +static int mcc_runtime_read(struct mcc_runtime *r,uint32_t offset,uint32_t size,void *out) { + r->reads++;if(offset || size>r->size)return 0;memcpy(out,r->source,size);return 1; +} +static long rasterizer_xbox_bitmap_get_pixel_data_size(struct bitmap_data *b) { + uint32_t bytes=0;unsigned level; + for(level=0;level<=(unsigned)b->mipmap_count;level++) { + unsigned w=MAX(b->width>>level,1),h=MAX(b->height>>level,1); + bytes+=b->format==14 ? ((w+3)/4)*((h+3)/4)*8 : w*h*2; + } + return (bytes+127u)&~127u; +} +static uint32_t global_vector_palette[256]; +#define BCDEC_STATIC +#define BCDEC_IMPLEMENTATION +#include "bcdec.h" +''' + source += "\n" + structure(bitmap, "mcc_texture") + source += "\n" + structure(bitmap, "mcc_bitmap_clone") + source += "\n" + structure(bitmap, "mcc_textures") + source += "\n" + function(native, "bitmap_verify") + source += "\n" + function(swizzle, "rasterizer_xbox_bitmap_get_max_mipmap_count") + source += "\n" + "\n".join(function(bitmap, name) for name in [ + "mcc_bitmap_bytes", "mcc_pixel", "mcc_channels", "mcc_bitmap_decode", + "mcc_morton_axis", "mcc_bitmap_pack", "mcc_bitmap_lightmap_flags", "mcc_bitmap_complete_mips", + "mcc_texture_prepare"]) + source += r''' +int main(int argc,char **argv) { + struct bitmap_data bitmap={0},original; + struct mcc_textures textures={0}; + struct mcc_texture texture={0}; + struct mcc_runtime runtime={0}; + unsigned i;int expect=1,converted,tail=0; + static unsigned const order[8]={0,1,4,5,2,3,6,7}; + if(argc!=2)return 2; + runtime.group.usage=4;runtime.size=16; + bitmap.signature=BITMAP_GROUP_TAG;bitmap.width=4;bitmap.height=2;bitmap.depth=1; + bitmap.format=6;bitmap.flags=0x1281;bitmap.pixels_size=16; + texture.bitmap=&bitmap;texture.handle=7; + for(i=0;i<16;i++)runtime.source[i]=(unsigned char)i; + if(!strncmp(argv[1],"tail",4)) { + tail=1;runtime.group.usage=0;runtime.size=349525; + bitmap.width=1024;bitmap.height=512;bitmap.format=14;bitmap.flags=0x81; + bitmap.mipmap_count=10;bitmap.pixels_size=349525; + for(i=0;i 0 + assert int(report["bsps"]) > 0 diff --git a/tools/test_mcc_checkpoint.py b/tools/test_mcc_checkpoint.py new file mode 100644 index 0000000000..4f01f000d5 --- /dev/null +++ b/tools/test_mcc_checkpoint.py @@ -0,0 +1,191 @@ +"""Actual MCC snapshot module and native save dispatch behavior, without assets.""" +from pathlib import Path +import shutil +import subprocess +import sys + +import pytest +from harness import function + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="session") +def checkpoint_tool(tmp_path_factory): + compiler = shutil.which("clang") or shutil.which("cc") + if not compiler: + pytest.skip("a C compiler is needed for MCC checkpoint tests") + work = tmp_path_factory.mktemp("mcc-checkpoint") + (work / "saved games").mkdir() + (work / "memory").mkdir() + (work / "objects").mkdir() + (work / "cseries.h").write_text("#pragma once\ntypedef unsigned char boolean,byte;\n") + (work / "errors.h").write_text("#define _error_silent 0\nvoid error(int level,char const *format,...);\n") + (work / "saved games/game_state.h").write_text( + "#pragma once\n#include \nstruct game_state_header {uint32_t allocation_checksum;" + "char map_name[256],build_number[32];short player_count,difficulty;" + "uint32_t cache_file_checksum,unused[7],checksum;};\n") + (work / "memory/data.h").write_text( + "struct data_array {short maximum_count,size;unsigned char valid;short count;void *data;};\n") + (work / "objects/objects.h").write_text( + "#define _object_mask_unit 3\nstruct object_datum {unsigned char state[64];};\n" + "struct object_header_datum {short identifier;unsigned char flags,type;" + "short cluster_index,data_size;struct object_datum *datum;};\n" + "extern struct data_array *object_header_data;\n") + native = (ROOT / "source/saved games/game_state.c").read_text() + funcs = "\n".join(function(native, name) for name in + ("game_state_save", "game_state_save_core", "game_state_image_accept")) + source = r''' +#include +#include +#include +#include +#define TRUE 1 +#define FALSE 0 +#define GAME_STATE_SIZE 262144 +#define GAME_STATE_CPU_SIZE 196608 +#define csmemcpy memcpy +static int loaded=1, malformed=0, reject_handles=0, captures=0, restores=0, writes=0; +static uint32_t payload_size=16; +static unsigned char live[GAME_STATE_SIZE], saved[GAME_STATE_SIZE]; +static struct {void *base_address;long cpu_allocation_size;int saved_game_valid;} game_state_globals; +enum {_game_state_allocation_data,_game_state_allocation_memory_pool,_game_state_allocation_lruv_cache}; +struct game_state_allocation {int kind;void *address;long maximum_count,element_size,size;}; +static struct game_state_allocation game_state_allocations[1]; +static long game_state_allocation_count; +static int native_valid=1; +''' + '#include "' + (ROOT / "port/linux/game/mcc_checkpoint.c").as_posix() + '"\n' + r''' +boolean mcc_cache_tags_loaded(void) {return loaded;} +struct data_array *object_header_data; +short mcc_grenades_type_count(void) {return 4;} +boolean mcc_level_name(char const *name) {return !strncmp(name,"mcc_maps\\",9);} +void error(int level,char const *format,...) {(void)level;(void)format;} +uint32_t mcc_grenades_snapshot(void *out,uint32_t capacity) { + unsigned char *p=out; + if(!p)return payload_size; + if(capacity=8 && !memcmp(in,"GMC4\1\0",6); +} +int mcc_grenades_restore(void const *in,uint32_t bytes) { + if(!mcc_grenades_snapshot_validate(in,bytes)||reject_handles)return 0; + restores++;return 1; +} +void game_state_call_before_save_procs(void) {} +void main_stop_time(void) {} +void main_start_time(void) {} +void game_state_note_event(char const *event) {(void)event;} +int game_state_write_to_file(void) {memcpy(saved,live,sizeof(saved));writes++;return 1;} +int game_state_write_core(char const *name,void const *data,long size) { + (void)name;memcpy(saved,data,size);writes++;return 1; +} +void console_printf(int flag,char const *format,...) {(void)flag;(void)format;} +int game_state_image_refuse(char const *name,char const *reason) {(void)name;(void)reason;return 0;} +int game_state_image_data_valid(void *image,void *address,long count,long size) { + (void)image;(void)address;(void)count;(void)size;return native_valid; +} +int game_state_image_memory_pool_valid(void *image,void *address,long size) { + (void)image;(void)address;(void)size;return native_valid; +} +int game_state_image_lruv_cache_valid(void *image,void *address,struct game_state_allocation const *a) { + (void)image;(void)address;(void)a;return native_valid; +} +''' + funcs + r''' +int main(int argc,char **argv) { + unsigned char *tail,*footer; + unsigned long used=4096; + int result=0; + struct game_state_header *header=(void *)live,*incoming=(void *)saved; + if(argc!=2)return 2; + memset(live,0xA5,sizeof(live)); + memset(header,0,sizeof(*header));strcpy(header->map_name,"mcc_maps\\a10"); + header->cache_file_checksum=0x12345678; + object_header_data=(void *)(live+512); + memset(object_header_data,0,sizeof(*object_header_data)); + object_header_data->maximum_count=object_header_data->count=1; + object_header_data->valid=1;object_header_data->size=sizeof(struct object_header_datum); + object_header_data->data=live+640; + {struct object_header_datum *unit=object_header_data->data; + memset(unit,0,sizeof(*unit));unit->identifier=1;unit->datum=(void *)(live+768);} + game_state_globals.base_address=live;game_state_globals.cpu_allocation_size=used; + tail=live+GAME_STATE_CPU_SIZE-MCC_CHECKPOINT_SLOT_BYTES; + footer=tail+MCC_CHECKPOINT_PAYLOAD_LIMIT; + if(!strcmp(argv[1],"stock")) { + loaded=0;strcpy(header->map_name,"levels\\a10\\a10"); + game_state_save();result=game_state_image_accept(saved,sizeof(saved)); + if(captures||restores||memcmp(tail,saved+(tail-live),MCC_CHECKPOINT_SLOT_BYTES))return 3; + } else if(!strcmp(argv[1],"capacity")) { + game_state_globals.cpu_allocation_size=GAME_STATE_CPU_SIZE-MCC_CHECKPOINT_SLOT_BYTES+1; + game_state_save();result=!writes&&!game_state_globals.saved_game_valid; + } else if(!strcmp(argv[1],"capture_fail")) { + malformed=1;game_state_save();result=!writes&&!game_state_globals.saved_game_valid; + } else if(!strcmp(argv[1],"maximum")) { + payload_size=MCC_CHECKPOINT_PAYLOAD_LIMIT; + result=mcc_checkpoint_capture(live,sizeof(live),used,GAME_STATE_CPU_SIZE); + result &= mcc_checkpoint_restore(live,sizeof(live),used,GAME_STATE_CPU_SIZE); + } else if(!strcmp(argv[1],"oversize")) { + payload_size=MCC_CHECKPOINT_PAYLOAD_LIMIT+1; + result=!mcc_checkpoint_capture(live,sizeof(live),used,GAME_STATE_CPU_SIZE); + } else { + if(!strcmp(argv[1],"core"))game_state_save_core("test");else game_state_save(); + if(writes!=1||captures!=1)return 4; + if(!strcmp(argv[1],"roundtrip")||!strcmp(argv[1],"core")) { + live[4095]=33;result=game_state_image_accept(saved,sizeof(saved)); + if(restores!=1||live[4095]!=0xA5)return 5; + } else if(!strcmp(argv[1],"cleanup")) { + mcc_checkpoint_dispose(); + for(unsigned long i=0;imap_name,"mcc_maps\\other"); + else if(!strcmp(argv[1],"checksum"))incoming->cache_file_checksum++; + else if(!strcmp(argv[1],"highwater"))game_state_globals.cpu_allocation_size=GAME_STATE_CPU_SIZE-1; + else if(!strcmp(argv[1],"native")){game_state_allocation_count=1;native_valid=0;} + else if(!strcmp(argv[1],"handles"))reject_handles=1; + else if(!strncmp(argv[1],"unit_",5)) { + struct data_array *a=(void *)(saved+512); + struct object_header_datum *unit=(void *)(saved+640); + p[6]=1;p[8]=0;p[9]=0;p[10]=1;p[11]=0;p[12]=2; + mcc_checkpoint_put(f+12,mcc_checkpoint_checksum(p,16)); + if(!strcmp(argv[1],"unit_salt"))unit->identifier=2; + else if(!strcmp(argv[1],"unit_type"))unit->type=2; + else if(!strcmp(argv[1],"unit_pointer"))unit->datum=(void *)(live+sizeof(live)); + else if(!strcmp(argv[1],"unit_array"))a->data=(void *)(live+sizeof(live)); + else return 11; + } + else return 7; + result=!game_state_image_accept(saved,sizeof(saved)); + if(restores)return 8; + } + } + mcc_checkpoint_dispose(); + if(!result)return 9; + puts("ok");return 0; +} +''' + path = work / "checkpoint.c" + path.write_text(source) + output = work / ("checkpoint.exe" if sys.platform == "win32" else "checkpoint") + command = [compiler, "-std=c99", "-Wall", "-Wextra", "-Werror", "-I", str(work), str(path), "-o", str(output)] + if sys.platform == "win32": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return output + + +@pytest.mark.parametrize("case", ["stock", "capacity", "capture_fail", "maximum", "oversize", + "roundtrip", "core", "cleanup", "payload", "footer", "version", + "reserved", "name", "checksum", "highwater", "native", "handles", + "unit_salt", "unit_type", "unit_pointer", "unit_array"]) +def test_mcc_checkpoint_and_native_hooks(checkpoint_tool, case): + result = subprocess.run([str(checkpoint_tool), case], capture_output=True, text=True) + assert result.returncode == 0, result.stdout + result.stderr + f" ({case}: {result.returncode})" diff --git a/tools/test_mcc_geometry.py b/tools/test_mcc_geometry.py new file mode 100644 index 0000000000..24351e77ca --- /dev/null +++ b/tools/test_mcc_geometry.py @@ -0,0 +1,150 @@ +"""Exercise MCC model/BSP conversion, the real vertex compressor, and cleanup. + +GPU allocation is mocked. These checks establish data/ownership behavior; +they do not substitute for an in-game rendering test. +""" +import os +from pathlib import Path +import shutil +import struct +import subprocess +import sys + +import pytest + +ROOT = Path(__file__).resolve().parents[1] + + +def geometry_map(): + base, tag_offset = 0x50000000, 0xC00 + data = bytearray(0x1700) + def u(o, v): + struct.pack_into(" 0 diff --git a/tools/test_mcc_grenades.py b/tools/test_mcc_grenades.py new file mode 100644 index 0000000000..fff80d9ca9 --- /dev/null +++ b/tools/test_mcc_grenades.py @@ -0,0 +1,123 @@ +"""Execute the MCC-only inventory and snapshot routines with salted fake units. + +The native two-byte inventory has guard bytes, so additional slots cannot +silently extend its layout. No game data or CE code participates. +""" +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from harness import function, structure + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="session") +def grenade_tool(tmp_path_factory): + clang = shutil.which("clang") + if not clang: + pytest.skip("clang is required for MCC inventory tests") + work = tmp_path_factory.mktemp("mcc-grenades") + implementation = (ROOT / "port/linux/game/mcc_grenades.c").read_text() + names = ["mcc_grenades_active", "mcc_grenades_type_count", "mcc_grenade_definition", + "mcc_grenade_inventory", "mcc_grenades_reset", "mcc_grenades_clear", "mcc_grenades_get", + "mcc_grenades_set", "mcc_grenades_total", "mcc_grenades_next", "mcc_grenades_add", + "mcc_grenade_u32", "mcc_grenades_snapshot", "mcc_grenades_snapshot_validate", "mcc_grenades_restore"] + source = r''' +#include +#include +#include +typedef int boolean; +#define NONE -1 +#define MAXIMUM_OBJECTS_PER_MAP 8 +#define MAXIMUM_NUMBER_OF_LOCAL_PLAYERS 4 +#define NUMBEROF(a) (sizeof(a)/sizeof(*(a))) +#define MIN(a,b) ((a)<(b)?(a):(b)) +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define PIN(x,a,b) ((x)<(a)?(a):((x)>(b)?(b):(x))) +struct unit_datum {struct {signed char grenade_counts[2];unsigned char guards[2];char current_grenade_index,desired_grenade_index;} unit;}; +struct game_globals_grenade {short maximum_count;}; +struct game_globals {struct {long count;void *address;} grenades;}; +static struct unit_datum units[8]; +static long handles[8]; +static int active=1; +static struct game_globals_grenade definitions[4]; +static struct game_globals globals={ {4,definitions} }; +static boolean mcc_cache_tags_loaded(void){return active;} +static struct game_globals *scenario_get_game_globals(void){return &globals;} +static struct unit_datum *unit_try_and_get(long handle){ + unsigned i=(unsigned long)handle&65535; + return handle!=NONE && i<8 && handles[i]==handle ? &units[i] : NULL; +} +''' + structure(implementation, "mcc_grenade_inventory") + r''' +static struct mcc_grenade_inventory mcc_grenade_inventories[MAXIMUM_OBJECTS_PER_MAP]; +static long mcc_grenade_flash_time[MAXIMUM_NUMBER_OF_LOCAL_PLAYERS]; +''' + '\n'.join(function(implementation, n) for n in names) + r''' +int main(int argc,char **argv){ + long unit=0x12340001;unsigned char snapshot[72],bad[72];uint32_t bytes;unsigned i; + if(argc!=2)return 99; + for(i=0;i<8;i++)handles[i]=NONE; + handles[1]=unit;units[1].unit.guards[0]=0xAA;units[1].unit.guards[1]=0x55; + mcc_grenades_reset(); + mcc_grenades_set(unit,0,2);mcc_grenades_set(unit,1,3);mcc_grenades_set(unit,2,4);mcc_grenades_set(unit,3,5); + if(!strcmp(argv[1],"inventory")){ + if(mcc_grenades_total(unit)!=14 || mcc_grenades_get(unit,2)!=4 || mcc_grenades_get(unit,3)!=5)return 1; + if(units[1].unit.guards[0]!=0xAA || units[1].unit.guards[1]!=0x55)return 2; + if(mcc_grenades_next(unit,1,1)!=2 || mcc_grenades_next(unit,0,-1)!=3 || mcc_grenades_next(unit,2,0)!=2)return 3; + mcc_grenades_set(unit,2,0); + if(mcc_grenades_next(unit,1,1)!=3)return 4; + if(mcc_grenades_add(unit,2,7)!=7 || units[1].unit.current_grenade_index!=2)return 5; + mcc_grenades_set(unit,2,300);mcc_grenades_set(unit,3,-3); + if(mcc_grenades_get(unit,2)!=127 || mcc_grenades_get(unit,3)!=0)return 6; + mcc_grenades_clear(unit); + if(mcc_grenades_get(unit,2) || mcc_grenades_total(unit)!=5)return 7; + active=0;mcc_grenades_set(unit,0,99); + if(mcc_grenades_get(unit,0) || units[1].unit.grenade_counts[0]!=2)return 8; + return 0; + } + bytes=mcc_grenades_snapshot(snapshot,sizeof(snapshot)); + if(bytes!=16 || mcc_grenades_snapshot(NULL,0)!=bytes || mcc_grenades_snapshot(bad,15))return 9; + if(memcmp(snapshot,"GMC4\1\0\1\0\1\0\x34\x12\4\5\0\0",16))return 10; + if(!strcmp(argv[1],"roundtrip")){ + mcc_grenades_clear(unit); + if(!mcc_grenades_restore(snapshot,bytes) || mcc_grenades_get(unit,2)!=4 || mcc_grenades_get(unit,3)!=5)return 11; + if(units[1].unit.grenade_counts[0]!=2 || units[1].unit.guards[0]!=0xAA)return 12; + handles[1]=0x23450001; + if(mcc_grenades_get(handles[1],2) || mcc_grenades_restore(snapshot,bytes))return 13; + mcc_grenades_set(handles[1],2,9); + if(mcc_grenades_get(handles[1],2)!=9 || mcc_grenades_get(unit,2))return 14; + return 0; + } + if(!strcmp(argv[1],"invalid")){ + for(i=0;i<16;i++)if(mcc_grenades_snapshot_validate(snapshot,i))return 15; + memcpy(bad,snapshot,16);bad[4]=2;if(mcc_grenades_snapshot_validate(bad,16))return 16; + memcpy(bad,snapshot,16);bad[12]=128;if(mcc_grenades_snapshot_validate(bad,16))return 17; + memcpy(bad,snapshot,16);bad[14]=1;if(mcc_grenades_snapshot_validate(bad,16))return 18; + memcpy(bad,snapshot,16);bad[8]=8;if(mcc_grenades_snapshot_validate(bad,16))return 19; + memcpy(bad,snapshot,16);bad[6]=2;memcpy(bad+16,snapshot+8,8); + if(mcc_grenades_snapshot_validate(bad,24) || mcc_grenades_restore(bad,24))return 20; + if(mcc_grenades_get(unit,2)!=4 || mcc_grenades_get(unit,3)!=5)return 21; + return 0; + } + return 98; +} +''' + path = work / "grenades.c" + path.write_text(source) + output = work / ("grenades.exe" if sys.platform == "win32" else "grenades") + command = [clang, "-std=gnu99", "-O2", "-Wall", "-Wextra", "-Werror", str(path), "-o", str(output)] + if sys.platform == "win32": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return output + + +@pytest.mark.parametrize("case", ["inventory", "roundtrip", "invalid"]) +def test_mcc_inventory_and_snapshots(grenade_tool, case): + result = subprocess.run([str(grenade_tool), case], capture_output=True, text=True) + assert result.returncode == 0, (case, result.returncode, result.stdout, result.stderr) diff --git a/tools/test_mcc_hud.py b/tools/test_mcc_hud.py new file mode 100644 index 0000000000..1e70eee44b --- /dev/null +++ b/tools/test_mcc_hud.py @@ -0,0 +1,143 @@ +"""MCC canvas and full-resolution icon geometry, using the real C routines. + +The observed Ruby fixtures use 30x38 unflagged pause glyphs and 93x93 +half-HUD-scale pickup glyphs. Cover their independent sizing and text advances, +including split-screen and the neutral Xbox/CE dispatch. +""" +from pathlib import Path +import shutil +import subprocess +import sys + +import pytest + +from harness import function + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="module") +def hud_tool(tmp_path_factory): + compiler = shutil.which("clang") + if not compiler: + pytest.skip("clang is needed for MCC HUD tests") + work = tmp_path_factory.mktemp("mcc-hud") + source = (ROOT / "port/linux/game/mcc_hud_draw.c").read_text() + functions = "\n".join(function(source, name) for name in [ + "mcc_hud_canvas_scale", "mcc_hud_bitmap_scale", "mcc_hud_icon_draw"]) + code = r''' +#include +#include +#include +#include +typedef float real; +typedef unsigned char boolean; +typedef unsigned long pixel32; +typedef struct {short x,y;} point2d; +typedef struct {short x0,y0,x1,y1;} rectangle2d; +typedef struct {float x0,x1,y0,y1;} real_rectangle2d; +struct bitmap_data {short width,height;}; +struct bitmap_group {unsigned flags;}; +struct icon_hud_element_definition { + short sequence_index,width_offset;point2d offset; + unsigned long color;char frame_rate;unsigned char flags;short text_index; +}; +#define NONE (-1) +#define TRUE 1 +#define FALSE 0 +#define _hud_anchor_bottom_left 2 +static int mcc,players,lookups,draws; +static float drawn_scale; +static point2d drawn_point; +static pixel32 drawn_color; +static struct bitmap_group group; +static int mcc_cache_tags_loaded(void) {return mcc;} +static int local_player_count(void) {return players;} +static struct bitmap_group *bitmap_group_get(long id) { + if(id!=17)abort();lookups++;return &group; +} +static short draw_unicode_string_capital_middle(rectangle2d const *bounds,wchar_t const *text) { + if(text[0])abort();return (short)(bounds->y0+8); +} +static void hud_draw_bitmap_direct(struct bitmap_data const *bitmap,short corner, + point2d const *point,real_rectangle2d const *clip,real scale,real theta, + pixel32 color,boolean linear) { + (void)bitmap;(void)clip; + if(corner!=2||theta||linear)abort(); + draws++;drawn_scale=scale;drawn_point=*point;drawn_color=color; +} +''' + functions + r''' +int main(int argc,char **argv) { + struct bitmap_data bitmap={256,256}; + real_rectangle2d clip={0.0f,30.0f/256,0.0f,38.0f/256}; + rectangle2d cursor={100,120,200,140},before=cursor; + struct icon_hud_element_definition icon={0,-6,{-2,15},0xFF123456,0,2,0},saved=icon; + int menu,handled; + if(argc!=6 && argc!=7)return 2; + mcc=atoi(argv[1]);players=atoi(argv[2]);menu=atoi(argv[3]); + group.flags=(unsigned)atoi(argv[4]);icon.flags=(unsigned char)atoi(argv[5]); + if(argc==7)icon.offset.y=(short)atoi(argv[6]);saved=icon; + if(!menu) {clip.x1=clip.y1=93.0f/256;} + handled=mcc_hud_icon_draw(17,&bitmap,&clip,&cursor,0xFFABCDEF,&icon,menu); + if(memcmp(&icon,&saved,sizeof(icon)))return 3; + if(!mcc && (memcmp(&cursor,&before,sizeof(cursor))||draws||lookups||handled))return 4; + if(mcc_hud_bitmap_scale(NONE)!=1.0f)return 5; + printf("%d %d %.3f %d %d %d %lu %.3f\n",handled,draws,drawn_scale, + drawn_point.x,drawn_point.y,cursor.x0,drawn_color,mcc_hud_canvas_scale()); + return 0; +} +''' + path = work / "hud.c" + binary = work / ("hud.exe" if sys.platform == "win32" else "hud") + path.write_text(code) + command = [compiler, "-std=gnu99", "-Wall", "-Wextra", "-Werror", str(path), "-o", str(binary)] + if sys.platform == "win32": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return binary + + +@pytest.mark.parametrize("players", [1, 2, 4]) +@pytest.mark.parametrize("menu", [False, True]) +@pytest.mark.parametrize("flags", [0, 0x10, 0x80, 0x90]) +@pytest.mark.parametrize("icon_flags", [0, 2, 4, 6]) +def test_icon_canvas_size_and_cursor_advance(hud_tool, players, menu, flags, icon_flags): + result = subprocess.run([str(hud_tool), "1", str(players), str(int(menu)), str(flags), str(icon_flags)], + capture_output=True, text=True) + assert result.returncode == 0, result.stderr + handled, draws, scale, x, y, cursor, color, canvas = result.stdout.split() + viewport = 0.75 if players > 1 and not menu else 1.0 + expected_scale = viewport * (0.25 if flags else 0.5) + offset_scale = viewport + expected_x = int(100 - 2 * offset_scale + int(menu)) + expected_y = int(128 + 38 * expected_scale * 0.5) if menu else int(140 - 15 * offset_scale) + advance = -6 * offset_scale + if not icon_flags & 4: + advance += (30 if menu else 93) * expected_scale + assert (handled, draws) == ("1", "1") + assert float(scale) == pytest.approx(expected_scale, abs=0.0006) + assert (int(x), int(y), int(cursor)) == (expected_x, expected_y, int(expected_x + advance)) + assert int(color) == (0xFF123456 if icon_flags & 2 else 0xFFABCDEF) + assert float(canvas) == 0.5 + + +@pytest.mark.parametrize("menu", [False, True]) +def test_xbox_and_ce_dispatch_are_neutral(hud_tool, menu): + result = subprocess.run([str(hud_tool), "0", "2", str(int(menu)), "16", "2"], + capture_output=True, text=True) + assert result.returncode == 0, result.stderr + assert result.stdout.split()[:2] == ["0", "0"] + assert float(result.stdout.split()[-1]) == 1.0 + + +@pytest.mark.parametrize("players", [1, 2]) +@pytest.mark.parametrize("offset_y", [-5, 15]) +def test_messaging_offsets_preserve_stock_and_map_authored_values(hud_tool, players, offset_y): + # Xbox/Mercury/Nitra use -5; Ruby changes only Y to +15. These text + # offsets must survive independently of the MCC bitmap canvas factor. + result = subprocess.run([str(hud_tool), "1", str(players), "0", "16", "2", str(offset_y)], + capture_output=True, text=True) + assert result.returncode == 0, result.stderr + viewport = 0.75 if players > 1 else 1.0 + assert int(result.stdout.split()[4]) == int(140 - offset_y * viewport) diff --git a/tools/test_mcc_lifecycle.py b/tools/test_mcc_lifecycle.py new file mode 100644 index 0000000000..92052608fd --- /dev/null +++ b/tools/test_mcc_lifecycle.py @@ -0,0 +1,255 @@ +"""Exercise real scenario-load dispatch against a failed initial MCC BSP.""" +from pathlib import Path +import shutil +import subprocess +import sys + +import pytest +from harness import function + +ROOT = Path(__file__).resolve().parents[1] + + +def test_initial_mcc_bsp_failure_balances_state_and_preserves_legacy_rollback(tmp_path): + """Exercise real BSP switching with and without the MCC dispatch active.""" + compiler = shutil.which("clang") or shutil.which("cc") + if not compiler: + pytest.skip("a C compiler is required") + current = function((ROOT / "source/scenario/scenario.c").read_text(), "scenario_switch_structure_bsp") + prefix = r''' +#include +#include +#include +typedef int boolean; +#define FALSE 0 +#define TRUE 1 +#define NONE (-1) +#define _error_silent 0 +#define _error_immediate 1 +#define MAXIMUM_STRUCTURE_BSPS_PER_SCENARIO 32 +#define match_assert(file,line,condition) do {if(!(condition))exit(20);}while(0) +#define TAG_BLOCK_GET_ELEMENT(block,index,type) ((type *)(block)->address+(index)) +struct block {int count;void *address;}; +struct scenario_structure_bsp_reference {struct {int index;} structure_bsp;}; +struct collision_bsp {int unused;}; +struct bsp3d {int unused;}; +struct structure_bsp {struct block collision_bsp;}; +static struct {struct block structure_bsp_references;} scenario,*global_scenario=&scenario; +static struct {int structure_bsp_index;} globals,*scenario_globals=&globals; +static struct scenario_structure_bsp_reference references[2]={{{0}},{{1}}}; +static struct collision_bsp collision; +static struct structure_bsp bsp={{1,&collision}}; +static struct structure_bsp *global_structure_bsp; +static struct collision_bsp *global_collision_bsp; +static struct bsp3d *global_bsp3d; +static short global_structure_bsp_index; +static int mcc,mode; +static struct results { + int stopped,started,collision_disabled,collision_enabled,disconnects,reconnects, + unloads,loads,verifies,modal,silent,menu,returned,index,scenario_index,pointers; +} observed; +static int mcc_cache_tags_loaded(void) {return mcc;} +static void main_stop_time(void) {observed.stopped++;} +static void main_start_time(void) {observed.started++;} +static void collision_log_enable(int enabled) { + if(enabled)observed.collision_enabled++;else observed.collision_disabled++; +} +static void scenario_call_disconnect_from_structure_bsp_procs(void) {observed.disconnects++;} +static void scenario_call_reconnect_to_structure_bsp_procs(void) {observed.reconnects++;} +static void scenario_structure_bsp_unload(struct scenario_structure_bsp_reference *reference) { + (void)reference;observed.unloads++; +} +static int scenario_structure_bsp_load(struct scenario_structure_bsp_reference *reference) { + (void)reference;observed.loads++; + return mode==0 || mode==4 || (mode==2&&observed.loads==2); +} +static struct structure_bsp *structure_bsp_definition_get(int index) {(void)index;return &bsp;} +static int structure_bsp_port_verify(struct structure_bsp *value) { + (void)value;observed.verifies++;return mode!=4 || observed.verifies>1; +} +static void main_goto_main_menu(void) {observed.menu++;} +static void error(int priority,char const *format,...) { + (void)format;if(priority==_error_immediate)observed.modal++;else observed.silent++; +} +static void reset(int old) { + memset(&observed,0,sizeof(observed)); + scenario.structure_bsp_references.count=2;scenario.structure_bsp_references.address=references; + global_structure_bsp_index=globals.structure_bsp_index=(short)old; + global_structure_bsp=old==NONE?NULL:&bsp; + global_collision_bsp=old==NONE?NULL:&collision; + global_bsp3d=old==NONE?NULL:(void *)&collision; +} +static void finish(int result) { + observed.returned=result;observed.index=global_structure_bsp_index; + observed.scenario_index=globals.structure_bsp_index; + observed.pointers=!!global_structure_bsp+!!global_collision_bsp+!!global_bsp3d; +} +''' + current + r''' +int main(void) { + int old,requested; + for(old=NONE;old<=0;old++) + for(requested=NONE;requested<=2;requested++)for(mode=0;mode<=4;mode++) { + struct results expected; + /* Xbox and CE share the inactive MCC gate. Their preserved flow + * remains the reference for all non-admission cases. */ + mcc=0;reset(old);finish(scenario_switch_structure_bsp((short)requested));expected=observed; + if(old==NONE&&requested>=0&&requested<2&&mode!=0) { + if(expected.modal!=1||expected.returned||expected.pointers||expected.loads!=1|| + expected.stopped!=1||expected.started!=1||expected.collision_disabled!=1|| + expected.collision_enabled!=1||expected.menu||expected.index!=NONE)return 3; + } + if(old==0&&requested==1&&mode==2) { + if(expected.modal!=1||expected.loads!=2||expected.returned||expected.pointers!=3|| + expected.disconnects!=1||expected.reconnects!=1||expected.menu|| + expected.index!=0||expected.scenario_index!=0)return 4; + } + mcc=1;reset(old);finish(scenario_switch_structure_bsp((short)requested)); + if(old==NONE&&requested>=0&&requested<2&&mode!=0) { + if(observed.modal||observed.returned||observed.pointers||observed.loads!=1|| + observed.stopped!=1||observed.started!=1||observed.collision_disabled!=1|| + observed.collision_enabled!=1||observed.disconnects||observed.reconnects|| + observed.menu||observed.index!=NONE||observed.scenario_index!=NONE)return 1; + /* The sole behavioral difference is avoiding the modal error. */ + observed.modal=expected.modal;observed.silent=expected.silent; + } + if(memcmp(&observed,&expected,sizeof(expected))) { + fprintf(stderr,"dispatch changed: old=%d new=%d mode=%d\n",old,requested,mode); + return 2; + } + } + return 0; +} +''' + path = tmp_path / "bsp_dispatch.c" + path.write_text(prefix) + binary = tmp_path / ("bsp_flow.exe" if sys.platform == "win32" else "bsp_flow") + flags = (["--target=i686-pc-windows-msvc", "-fuse-ld=lld"] if sys.platform == "win32" else []) + built = subprocess.run([compiler, *flags, "-std=c99", "-Wall", "-Wextra", "-Werror", + str(path), "-o", str(binary)], capture_output=True, text=True) + assert built.returncode == 0, built.stderr + result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_failed_mcc_bsp_releases_io_owner(tmp_path): + compiler = shutil.which("clang") or shutil.which("cc") + if not compiler: + pytest.skip("a C compiler is required") + actual = function((ROOT / "source/scenario/scenario.c").read_text(), "scenario_load") + prefix = r''' +#include +#include +typedef int boolean; +#define FALSE 0 +#define TRUE 1 +#define NONE (-1) +#define GAME_GLOBALS_TAG 0 +#define _error_delayed 0 +static int scenario_memory_status_attributed, global_scenario_index; +static int mcc_owner, bsp_result, unloads, tag_result = 1; +static void *global_game_globals; +struct scenario { struct { int count; } structure_bsp_references; }; +static struct scenario scenario = {{1}}, *global_scenario; +static void check_memory_status(int a, char const *b) {(void)a;(void)b;} +static int scenario_tags_load(char const *name) {(void)name;return tag_result;} +static struct scenario *scenario_definition_get(int index) {(void)index;return &scenario;} +static int tag_loaded(int group,char const *name) {(void)group;(void)name;return 0;} +static void *game_globals_definition_get(int index) {(void)index;return &scenario;} +static int scenario_switch_structure_bsp(int index) {(void)index;return bsp_result;} +static void error(int priority,char const *format,...) {(void)priority;(void)format;} +static int mcc_cache_tags_loaded(void) {return mcc_owner;} +static void scenario_unload(void) {unloads++;mcc_owner=0;global_scenario=NULL;} +''' + checks = r''' +int main(void) { + mcc_owner=1; + if(scenario_load("mcc_maps\\a10") || unloads!=1 || mcc_owner || global_scenario) return 1; + /* A subsequent ordinary map must retain its existing lifetime behavior. */ + unloads=0; + if(scenario_load("a10") || unloads || mcc_owner) return 2; + bsp_result=1; + if(!scenario_load("custom_maps\\hugeass") || unloads) return 3; + mcc_owner=1; + if(!scenario_load("mcc_maps\\dangercanyon") || unloads || !mcc_owner) return 4; + /* Failed conversion already disposed its private state; no double unload. */ + mcc_owner=0;tag_result=NONE; + if(scenario_load("mcc_maps\\bad") || unloads) return 5; + return 0; +} +''' + source = tmp_path / "lifecycle.c" + source.write_text(prefix + actual + checks) + binary = tmp_path / ("lifecycle.exe" if sys.platform == "win32" else "lifecycle") + flags = (["--target=i686-pc-windows-msvc", "-fuse-ld=lld"] if sys.platform == "win32" else []) + built = subprocess.run([compiler, *flags, "-std=c99", "-Wall", "-Wextra", "-Werror", + str(source), "-o", str(binary)], capture_output=True, text=True) + assert built.returncode == 0, built.stderr + result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_failed_mcc_map_returns_to_ui_before_game_initialization(tmp_path): + compiler = shutil.which("clang") or shutil.which("cc") + if not compiler: + pytest.skip("a C compiler is required") + main = function((ROOT / "source/main/main.c").read_text(), "main_new_map") + recovery = function((ROOT / "port/linux/game/mcc_main.c").read_text(), "mcc_main_load_failed") + source = r''' +#include +#include +typedef int boolean; +#define TRUE 1 +#define FALSE 0 +#define NONE -1 +#define _error_immediate 1 +#define _error_silent 0 +struct game_options {char const *map_name;}; +static struct {int reset_map,defer_map_change,revert_map,skip_cinematic,saving_map, + won_map,lost_map,respawn,save_core,switch_to_structure_bsp_index,load_core, + load_core_at_startup,allow_persistent_storage;} main_globals; +static int loads,initialized,players,pulses,menus,popups,clears,errors,gotos,refuse=1; +static void main_new_map(struct game_options *options); +static void input_flush(void) {} +static int game_load(struct game_options *options) { + loads++; return !strcmp(options->map_name,"levels\\ui\\ui") || !refuse; +} +static void game_initialize_for_new_map(void) {initialized++;} +static void error(int priority,char const *format,...) {(void)format;if(priority)errors++;} +static int errors_handle(void) {return errors!=0;} +static void create_local_players(void) {players++;} +static void game_time_start(void) {} +static void game_initial_pulse(void) {pulses++;} +static void game_state_try_and_load_from_persistent_storage(void) {} +static void ui_widgets_disable_pause_game(int ticks) {(void)ticks;} +static int mcc_level_name(char const *name) {return !strncmp(name,"mcc_maps\\",9);} +static void display_error_text_when_main_menu_loaded(wchar_t const *text) {(void)text;popups++;} +static void errors_clear(void) {errors=0;clears++;} +static void main_goto_main_menu(void) {gotos++;} +static void main_menu_load(void) { + struct game_options ui={"levels\\ui\\ui"};menus++;main_new_map(&ui); +} +''' + recovery + main + r''' +int main(void) { + struct game_options options={"mcc_maps\\bad_bsp"}; + errors=1; /* A failed initial BSP already reported a native error. */ + main_new_map(&options); + if(loads!=2 || initialized!=1 || players!=1 || pulses!=1 || menus!=1 || + popups!=1 || clears!=1 || gotos!=1 || errors) return 1; + options.map_name="a10";main_new_map(&options); + if(menus!=1 || clears!=1 || initialized!=1 || loads!=3) return 2; + options.map_name="custom_maps\\bad";main_new_map(&options); + if(menus!=1 || clears!=1 || initialized!=1 || loads!=4) return 3; + options.map_name="mcc_maps\\good";refuse=0;errors=0;main_new_map(&options); + if(menus!=1 || clears!=1 || initialized!=2 || loads!=5) return 4; + return 0; +} +''' + path = tmp_path / "recovery.c" + path.write_text(source) + binary = tmp_path / ("recovery.exe" if sys.platform == "win32" else "recovery") + flags = (["--target=i686-pc-windows-msvc", "-fuse-ld=lld"] if sys.platform == "win32" else []) + built = subprocess.run([compiler, *flags, "-std=c99", "-Wall", "-Wextra", "-Werror", + str(path), "-o", str(binary)], capture_output=True, text=True) + assert built.returncode == 0, built.stderr + result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/tools/test_mcc_maps.py b/tools/test_mcc_maps.py new file mode 100644 index 0000000000..b808e48cc0 --- /dev/null +++ b/tools/test_mcc_maps.py @@ -0,0 +1,353 @@ +"""Compile the real MCC catalog with narrow host filesystem stubs. + +Generated maps contain only a header; the catalog must not read tag payloads. +The stubs require every discovery/open operation to name d:\\mcc_maps\\, so +an accidental fallback to Xbox or CE directories fails the executable. +""" +import os +from pathlib import Path +import shutil +import struct +import subprocess +import sys + +import pytest +from harness import function + +ROOT = Path(__file__).resolve().parents[1] + + +def header(version=13, scenario=0, internal="different_internal_name"): + data = bytearray(0x828) + for offset, value in {0: 0x68656164, 4: version, 8: len(data), + 16: 0x800, 20: 40, 0x7FC: 0x666F6F74}.items(): + struct.pack_into(" +#include +#include +#include +typedef unsigned char boolean; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +#define FLAG(x) (1u << (x)) +#ifdef _WIN32 +#define csstrcasecmp _stricmp +#else +#include +#define csstrcasecmp strcasecmp +#define _strnicmp strncasecmp +#endif +FILE *mcc_test_fopen(char const *,char const *); +size_t mcc_test_fread(void *,size_t,size_t,FILE *); +#define fopen mcc_test_fopen +#define fread mcc_test_fread +#endif +''') + (work / "errors.h").write_text('#define _error_silent 0\nvoid error(short,const char *,...);\n') + (work / "tag_files/tag_files.h").write_text("/* No tag access in the menu catalog. */\n") + (work / "tag_files/files.h").write_text(r''' +struct file_reference {char name[256];}; +struct file_last_modification_date; +#define MAXIMUM_FILENAME_LENGTH 255 +enum {_name_directory_bit,_name_parent_directory_bit,_name_filename_bit,_name_extension_bit}; +struct file_reference *file_reference_create_from_path(struct file_reference *,char const *,boolean); +void find_files_start(unsigned long,struct file_reference const *); +boolean find_files_next(struct file_reference *,struct file_last_modification_date *); +char *file_reference_get_name(struct file_reference const *,unsigned long,char *); +''') + source = work / "catalog.c" + source_code = r''' +#include "cseries.h" +#include "tag_files/files.h" +#include "mcc_maps.h" +#include +#include +#undef fopen +#undef fread +static char const *root; +static char const *listing="list.txt"; +static FILE *inventory; +static unsigned reads, largest, scans; +struct mcc_runtime { + unsigned char *tag_index; + struct {uint32_t scenario_handle,tag_count;} report; +}; +static char saved_identity[64]; +static int allocation_fails; +void *mcc_runtime_allocate(struct mcc_runtime *r,uint32_t bytes) { + (void)r; + return bytes<=sizeof(saved_identity) && !allocation_fails ? saved_identity : NULL; +} +FILE *mcc_test_fopen(char const *path,char const *mode) { + char translated[1024]; + if (strncmp(path,"d:\\mcc_maps\\",12) || strcmp(mode,"rb") || + strpbrk(path+12,"\\/:*?\"<>|")) exit(20); + snprintf(translated,sizeof(translated),"%s/mcc_maps/%s",root,path+12); + return fopen(translated,mode); +} +size_t mcc_test_fread(void *out,size_t size,size_t count,FILE *file) { + size_t bytes=size*count; + if (bytes>2048) exit(21); + if (bytes>largest) largest=(unsigned)bytes; + reads++; + return fread(out,size,count,file); +} +void error(short priority,char const *format,...) {(void)priority;(void)format;} +struct file_reference *file_reference_create_from_path(struct file_reference *r,char const *path,boolean directory) { + if (!directory || strcmp(path,"d:\\mcc_maps\\")) exit(22); + strcpy(r->name,path);return r; +} +void find_files_start(unsigned long flags,struct file_reference const *directory) { + char path[1024]; + if (flags || strcmp(directory->name,"d:\\mcc_maps\\")) exit(23); + if (inventory) fclose(inventory); + snprintf(path,sizeof(path),"%s/mcc_maps/%s",root,listing); + inventory=fopen(path,"r");scans++; +} +boolean find_files_next(struct file_reference *file,struct file_last_modification_date *unused) { + (void)unused; + if (!inventory || !fgets(file->name,sizeof(file->name),inventory)) return FALSE; + file->name[strcspn(file->name,"\r\n")]=0;return TRUE; +} +char *file_reference_get_name(struct file_reference const *file,unsigned long flags,char *out) { + char const *dot=strrchr(file->name,'.'); + if (flags==FLAG(_name_extension_bit)) strcpy(out,dot ? dot+1 : ""); + else if (flags==FLAG(_name_filename_bit)) { + size_t n=dot ? (size_t)(dot-file->name) : strlen(file->name); + memcpy(out,file->name,n);out[n]=0; + } else exit(24); + return out; +} +int main(int argc,char **argv) { + short count,i; + if(argc<2)return 2; + if(argc==4 && (!strcmp(argv[2],"identity") || !strcmp(argv[2],"identity_fail"))) { + unsigned char entries[64],before[64]; + uint32_t address=0;int accepted; + struct mcc_runtime r; + memset(entries,0xAB,sizeof(entries));memcpy(before,entries,sizeof(entries)); + r.tag_index=entries;r.report.scenario_handle=0xE1750001;r.report.tag_count=2; + allocation_fails=!strcmp(argv[2],"identity_fail"); + accepted=mcc_scenario_identity(&r,argv[3]); + printf("accepted=%d\n",accepted); + if(accepted) { + memcpy(&address,entries+48,4); + if(address!=(uint32_t)(uintptr_t)saved_identity)return 28; + memcpy(before+48,entries+48,4); + printf("identity=%s\n",saved_identity); + } + if(memcmp(entries,before,sizeof(entries)))return 29; + return 0; + } + if(argc==4 && !strcmp(argv[2],"path")) { + char path[128];int accepted=mcc_path(argv[3],path); + printf("accepted=%d\n",accepted); + if(accepted)printf("path=%s\n",path); + return 0; + } + root=argv[1];count=mcc_maps_count(FALSE); + printf("count=%d\nmultiplayer=%d\nsingleplayer=%d\n",count,mcc_maps_count(TRUE),mcc_maps_type_count(TRUE)); + if(mcc_maps_type_count(FALSE)!=mcc_maps_count(TRUE) || + mcc_maps_type_count(TRUE)+mcc_maps_type_count(FALSE)!=count)return 30; + { + int campaign; + for(campaign=0;campaign<=1;campaign++) { + short total=mcc_maps_type_count((boolean)campaign),previous=NONE; + if(mcc_maps_type_index(-1,(boolean)campaign)!=NONE || + mcc_maps_type_index(total,(boolean)campaign)!=NONE)return 31; + for(i=0;i=count || mcc_maps_campaign(index)!=campaign)return 32; + if(argc==3 && !strcmp(argv[2],"types")) + printf("%s_%d=%s\n",campaign ? "singleplayer" : "multiplayer",i,mcc_maps_level_name(index)); + previous=index; + } + } + } + for(i=0;i63)return 25; + printf("map=%s|%d\n",level,mcc_maps_campaign(i)); + } + for(i=0;i2 && !strcmp(argv[2],"rescan")) { + listing="list2.txt"; + printf("cached=%d\n",mcc_maps_count(FALSE)); + mcc_maps_rescan();printf("rescanned=%d\n",mcc_maps_count(FALSE)); + } else if(argc>2) printf("found=%d\n",mcc_maps_find(argv[2])); + printf("scans=%u\nreads=%u\nlargest_read=%u\n",scans,reads,largest); + if(inventory)fclose(inventory); + return 0; +} +''' + # Exercise the actual side-effect-free runtime admission functions too. + cache_source = (ROOT / "port/linux/game/mcc_cache.c").read_text() + admission = "\n".join(function(cache_source, name) for name in + ("mcc_prefix", "mcc_path", "mcc_scenario_identity")) + source.write_text(source_code.replace("int main(int argc,char **argv)", admission + "\nint main(int argc,char **argv)")) + output = work / ("catalog.exe" if sys.platform == "win32" else "catalog") + command = [compiler, "-std=c99", "-Wall", "-Wextra", "-Werror", + "-I", str(work), "-I", str(ROOT / "port/linux/game"), str(source), + str(ROOT / "port/linux/game/mcc_maps.c"), + str(ROOT / "port/linux/game/mcc_cache_format.c"), "-o", str(output)] + if sys.platform == "win32": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return output + + +def install(root, entries): + directory = root / "mcc_maps" + directory.mkdir(exist_ok=True) + for name, contents in entries.items(): + (directory / name).write_bytes(contents) + (directory / "list.txt").write_text("\n".join(entries)) + + +def run(tool, root, *args): + result = subprocess.run([str(tool), str(root), *args], capture_output=True, text=True, timeout=10) + assert result.returncode == 0, result.stdout + result.stderr + fields = dict(line.split("=", 1) for line in result.stdout.splitlines() if not line.startswith("map=")) + maps = [line[4:] for line in result.stdout.splitlines() if line.startswith("map=")] + return fields, maps + + +def test_versions_and_folders_are_isolated(catalog_tool, tmp_path): + install(tmp_path, {"a10.map": header(), "bloodgulch.map": header(scenario=1), + "xbox.map": header(version=5), "custom.map": header(version=609), + "ui.map": header(scenario=2), "truncated.map": header()[:300], + "ignored.yelo": header()}) + for folder in ("maps", "custom_maps"): + (tmp_path / folder).mkdir() + for name in ("a10", "bloodgulch", "outside_only"): + (tmp_path / folder / (name + ".map")).write_bytes(header()) + fields, maps = run(catalog_tool, tmp_path) + assert fields["count"] == "2" + assert fields["multiplayer"] == "1" + assert maps == ["mcc_maps\\a10|1", "mcc_maps\\bloodgulch|0"] + assert fields["largest_read"] == "2048" + assert fields["scans"] == "1" + + +@pytest.mark.parametrize("entries,singleplayer,multiplayer", [ + ({}, [], []), + ({"only_sp": 0}, ["only_sp"], []), + ({"only_mp": 1}, [], ["only_mp"]), + ({"a10": 1, "bloodgulch": 0}, ["bloodgulch"], ["a10"]), + ({"a_mp": 1, "b_sp": 0, "c_mp": 1, "d_sp": 0}, ["b_sp", "d_sp"], ["a_mp", "c_mp"]), +]) +def test_scenario_type_lists_keep_catalog_identity(catalog_tool, tmp_path, entries, singleplayer, multiplayer): + install(tmp_path, {name + ".map": header(scenario=kind) for name, kind in entries.items()}) + fields, _ = run(catalog_tool, tmp_path, "types") + for label, expected in [("singleplayer", singleplayer), ("multiplayer", multiplayer)]: + assert int(fields[label]) == len(expected) + assert [fields[f"{label}_{row}"] for row in range(len(expected))] == [ + "mcc_maps\\" + name for name in expected] + + +def test_filename_identity_case_and_duplicate_filter(catalog_tool, tmp_path): + install(tmp_path, {"UPPER.map": header(internal="a10"), "lower.map": header()}) + (tmp_path / "mcc_maps/list.txt").write_text("UPPER.MAP\nUPPER.map\nlower.map\n") + fields, maps = run(catalog_tool, tmp_path, "MCC_MAPS\\upper") + assert fields["count"] == "2" + assert fields["found"] == "1" + assert maps == ["mcc_maps\\lower|1", "mcc_maps\\UPPER|1"] + + +def test_filename_length_limit(catalog_tool, tmp_path): + install(tmp_path, {"a" * 54 + ".map": header(), "b" * 55 + ".map": header()}) + fields, maps = run(catalog_tool, tmp_path) + assert fields["count"] == "1" + assert maps[0] == "mcc_maps\\" + "a" * 54 + "|1" + + +def test_rescan_rebuilds_only_mcc_catalog(catalog_tool, tmp_path): + install(tmp_path, {"first.map": header(), "second.map": header(scenario=1)}) + (tmp_path / "mcc_maps/list.txt").write_text("first.map\n") + (tmp_path / "mcc_maps/list2.txt").write_text("second.map\nfirst.map\n") + fields, _ = run(catalog_tool, tmp_path, "rescan") + assert fields["count"] == fields["cached"] == "1" + assert fields["rescanned"] == "2" + assert fields["scans"] == "2" + + +def test_catalog_limit(catalog_tool, tmp_path): + install(tmp_path, {f"map{i:04}.map": header(scenario=i % 2) for i in range(1025)}) + fields, maps = run(catalog_tool, tmp_path) + assert fields["count"] == "1024" + assert fields["multiplayer"] == "512" + assert len(maps) == 1024 + + +def test_empty_or_missing_mcc_directory(catalog_tool, tmp_path): + fields, maps = run(catalog_tool, tmp_path) + assert fields["count"] == fields["multiplayer"] == "0" + assert maps == [] + + +def test_unsafe_or_unlaunchable_names_are_not_opened(catalog_tool, tmp_path): + install(tmp_path, {"valid.map": header()}) + unsafe = ["../a10.map", "..\\a10.map", "bad?.map", "bad*.map", "bad|.map", + 'bad".map', "bad<.map", "bad>.map", "d:a10.map", ".map"] + (tmp_path / "mcc_maps/list.txt").write_text("\n".join(unsafe + ["valid.map"])) + fields, maps = run(catalog_tool, tmp_path) + assert fields["count"] == "1" + assert maps == ["mcc_maps\\valid|1"] + + +@pytest.mark.parametrize("name,accepted", [ + ("mcc_maps\\a10", True), ("MCC_MAPS/bloodgulch", True), + ("mcc_maps\\" + "a" * 54, True), ("mcc_maps\\" + "a" * 55, False), + ("a10", False), ("custom_maps\\a10", False), ("maps\\a10", False), + ("mcc_maps\\..\\a10", False), ("mcc_maps/../a10", False), + ("mcc_maps\\", False), ("mcc_maps\\a10.", False), + ("mcc_maps\\a10 ", False), ("mcc_maps\\a?0", False), +]) +def test_runtime_path_admission(catalog_tool, tmp_path, name, accepted): + fields, _ = run(catalog_tool, tmp_path, "path", name) + assert fields["accepted"] == str(int(accepted)) + if accepted: + assert fields["path"] == "d:\\mcc_maps\\" + name[9:] + ".map" + + +@pytest.mark.parametrize("name,expected", [ + ("mcc_maps\\a10", "mcc_maps\\a10"), + ("MCC_MAPS/BloodGulch", "mcc_maps\\BloodGulch"), + ("mcc_maps/" + "a" * 54, "mcc_maps\\" + "a" * 54), + ("custom_maps\\a10", None), +]) +def test_saved_identity_keeps_mcc_namespace(catalog_tool, tmp_path, name, expected): + fields, _ = run(catalog_tool, tmp_path, "identity", name) + assert fields["accepted"] == str(int(expected is not None)) + if expected: + assert fields["identity"] == expected + + +def test_saved_identity_allocation_failure_does_not_modify_tags(catalog_tool, tmp_path): + fields, _ = run(catalog_tool, tmp_path, "identity_fail", "mcc_maps\\a10") + assert fields["accepted"] == "0" diff --git a/tools/test_mcc_media.py b/tools/test_mcc_media.py new file mode 100644 index 0000000000..4aa6bb447f --- /dev/null +++ b/tools/test_mcc_media.py @@ -0,0 +1,506 @@ +"""Asset-free codec vectors and stream ownership tests for MCC-only media. + +Compile the actual private routines, not copies. Pixel vectors check channel +ordering, partial blocks and BC7 mode 6. PCM tests independently decode the +serialized Xbox ADPCM blocks in Python, including stereo interleave. +""" +import math +import os +from pathlib import Path +import re +import shutil +import struct +import subprocess +import sys + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from harness import function, structure + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="session") +def media_tool(tmp_path_factory): + clang = shutil.which("clang") + if not clang: + pytest.skip("clang is needed for the MCC media tests") + work = tmp_path_factory.mktemp("mcc-media") + audio = (ROOT / "port/linux/game/mcc_audio.c").read_text() + bitmap = (ROOT / "port/linux/game/mcc_bitmaps.c").read_text() + native_bitmap = (ROOT / "source/bitmaps/bitmaps.c").read_text() + native_swizzle = (ROOT / "source/rasterizer/rasterizer_swizzle.c").read_text() + hud = (ROOT / "port/linux/game/mcc_hud.c").read_text() + declarations = "\n".join(structure(audio, name) for name in ["mcc_audio_storage", "mcc_pcm", "mcc_ima"]) + tables = "\n".join(re.search(r"static int const " + name + r"\[.*?\};", audio, re.S).group() + for name in ["mcc_ima_steps", "mcc_ima_changes"]) + functions = "\n".join(function(audio, name) for name in ["mcc_ima_advance", "mcc_ima_quantize", + "mcc_pcm_sample", "mcc_resampled", "mcc_audio_store", "mcc_audio_contains"]) + functions += "\n" + function(native_swizzle, "rasterizer_xbox_bitmap_get_max_mipmap_count") + functions += "\n" + function(native_bitmap, "bitmap_2d_address") + functions += "\n" + "\n".join(function(bitmap, name) for name in ["mcc_bitmap_bytes", "mcc_pixel", + "mcc_channels", "mcc_bitmap_decode", "mcc_morton_axis", "mcc_bitmap_pack"]) + functions += "\n" + function(hud, "mcc_hud_offset") + functions += "\n" + function(hud, "mcc_hud_scale") + functions += "\n" + function(hud, "mcc_hud_score_text") + source = r''' +#include +#include +#include +#include +#include +#define MIN(a,b) ((a)<(b)?(a):(b)) +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define PIN(x,a,b) ((x)<(a)?(a):((x)>(b)?(b):(x))) +#define MCC_AUDIO_BASE 0x50000000u +#define MCC_AUDIO_LIMIT 0x10000000u +#define MCC_AUDIO_FRAME_LIMIT 0x1000000u +struct sound_permutation {struct {uint32_t file_offset,size;} samples; uint32_t sample_buffer_size; int compression;}; +struct mcc_runtime {void *audio;}; +struct hud_placement_definition {struct {short x,y;} offset;struct {float i,j;} scale; short multiplayer_scaling_flags;}; +struct bitmap_data {short type,width,height,depth,format,mipmap_count;unsigned short flags;void *base_address;}; +typedef unsigned char byte; +#define FALSE 0 +#define _bitmap_type_2d 0 +#define _bitmap_has_power_of_two_dimensions_bit 0 +#define _bitmap_compressed_bit 1 +#define _bitmap_swizzled_bit 3 +#define _bitmap_linear_bit 4 +#define TEST_FLAG(value,bit) ((value)&(1u<<(bit))) +#define match_assert(file,line,condition) do {if(!(condition))abort();} while(0) +#define match_vassert(file,line,condition,description) match_assert(file,line,condition) +static int bitmap_verify(struct bitmap_data *bitmap,int pixels) {(void)bitmap;(void)pixels;return 1;} +static short floor_log2(unsigned value) {short result=0;while(value>1){value>>=1;result++;}return result;} +static long bitmap_format_get_bits_per_pixel(short format) {return format==14 ? 4 : 8;} +uint32_t global_vector_palette[256]; +#define BCDEC_STATIC +#define BCDEC_IMPLEMENTATION +#include "bcdec.h" +''' + declarations + "\n" + tables + "\n" + functions + r''' +int main(int argc,char **argv) { + if (argc<2) return 2; + if (!strcmp(argv[1],"audio")) { + int channels=atoi(argv[2]), input_rate=atoi(argv[3]), output_rate=atoi(argv[4]); + struct mcc_audio_storage output={0}; + struct sound_permutation permutation={0}; + struct mcc_pcm pcm; + uint32_t i; + pcm.frames=4096;pcm.channels=channels;pcm.rate=input_rate; + pcm.samples=malloc(pcm.frames*channels*sizeof(short)); + for (i=0;i>level)/4,1)*block; + unsigned char *address=bitmap_2d_address(&bitmap,0,0,(short)level); + if(address!=packed+1+offset || memcmp(address,input+offset,size))return 34; + offset+=size; + } + if(offset!=source_bytes || packed[0]!=0xDD || packed[allocation+1]!=0xDD)return 35; + for(i=source_bytes;i> (frame % 2 * 4) & 15 + value = steps[step] + delta = value // 8 + (value // 4 if code & 1 else 0) + (value // 2 if code & 2 else 0) + (value if code & 4 else 0) + predictor = max(-32768, min(32767, predictor + (-delta if code & 8 else delta))) + step = max(0, min(88, step + adjustments[code & 7])) + samples[channel].append(predictor) + return samples + + +@pytest.mark.parametrize("channels,source_rate,target_rate", [(1,22050,22050),(2,44100,44100),(1,44100,22050)]) +def test_xbox_adpcm_waveform(media_tool, tmp_path, channels, source_rate, target_rate): + target = tmp_path / "encoded.bin" + run(media_tool, "audio", channels, source_rate, target_rate, target) + data = target.read_bytes() + frames = 4096 * target_rate // source_rate + assert len(data) == ((frames + 63) // 64) * 36 * channels + decoded = decode_adpcm(data, channels) + for channel in range(channels): + signal = [(math.sin(i * source_rate / target_rate * 0.035) * 14000 if channel == 0 + else math.cos(i * source_rate / target_rate * 0.051) * 9000) for i in range(frames)] + # Skip the initial codec predictor warmup; wrong stereo block order + # or sample-rate conversion produces errors thousands of times larger. + rms = math.sqrt(sum((decoded[channel][i] - signal[i]) ** 2 for i in range(64, frames)) / (frames - 64)) + assert rms < 160 + + +def test_virtual_audio_ranges_do_not_alias_raw_file(media_tool): + run(media_tool, "contains") + + +def test_hud_canvas_and_bitmap_scale_are_independent(media_tool): + run(media_tool, "hud") + + +def test_mcc_score_hint_resolves_binding_without_mutating_source(media_tool): + run(media_tool, "score-hint") + + +def test_hardware_packing_swizzle_rows_faces_and_mips(media_tool): + run(media_tool, "pack") + + +def test_cpu_dxt_tiny_mips_preserve_source_at_native_addresses(media_tool): + run(media_tool, "compressed-tail") + + +def test_shared_bitmap_consumers_get_independent_metadata(tmp_path): + clang = shutil.which("clang") + if not clang: + pytest.skip("clang is needed for the MCC bitmap ownership test") + bitmap = (ROOT / "port/linux/game/mcc_bitmaps.c").read_text() + header = (ROOT / "source/bitmaps/bitmap_group.h").read_text() + declarations = "\n".join(structure(header, n) for n in ["bitmap_data", "bitmap_group_sprite", + "bitmap_group_sequence", "bitmap_group"]) + declarations += "\n" + "\n".join(structure(bitmap, n) for n in ["mcc_texture", "mcc_bitmap_clone", "mcc_textures"]) + routines = "\n".join(function(bitmap, n) for n in ["mcc_bitmap_tag", "mcc_bitmap_block", + "mcc_bitmap_clone_block", "mcc_bitmap_specialize", "mcc_bitmaps_contains", "mcc_bitmaps_read"]) + source = r''' +#include +#include +#include +#include +typedef float real; +union point2d {struct {short x,y;};uint32_t n;}; +typedef struct {float x0,x1,y0,y1;} real_rectangle2d; +typedef struct {float x,y;} real_point2d; +struct tag_block {long count;void *address,*definition;}; +struct tag_data {long size,flags,file_offset;void *address,*definition;}; +#define BITMAP_GROUP_TAG 'bitm' +#define MCC_BITMAP_BASE 0x60000000u +struct mcc_runtime {unsigned char *tags,*tag_index;uint32_t used;struct {uint32_t tag_count;} report;void *bitmaps;}; +static void *mcc_runtime_pointer(struct mcc_runtime *r,uint32_t p,uint32_t size) { + uintptr_t start=(uintptr_t)r->tags; + return p>=start && p-start<=r->used && size<=r->used-(p-start) ? (void *)(uintptr_t)p : NULL; +} +static void *mcc_runtime_allocate(struct mcc_runtime *r,uint32_t size) { + uint32_t at=(r->used+15)&~15u; + if(size>65536-at)return NULL; + r->used=at+size;return r->tags+at; +} +''' + declarations + '\n' + routines + r''' +int main(void) { + struct mcc_runtime r={0};struct mcc_textures textures={0}; + struct bitmap_group *original,*copy,*meter;struct bitmap_data *pixels; + struct bitmap_group_sequence *sequence;struct bitmap_group_sprite *sprite; + uint32_t *entry,source=0xE1740000,material=source,meter_handle=source,repeated=source; + r.tags=calloc(1,65536);r.tag_index=r.tags;r.used=512;r.report.tag_count=1; + original=mcc_runtime_allocate(&r,sizeof(*original));pixels=mcc_runtime_allocate(&r,sizeof(*pixels)); + sequence=mcc_runtime_allocate(&r,sizeof(*sequence));sprite=mcc_runtime_allocate(&r,sizeof(*sprite)); + original->bitmaps.count=1;original->bitmaps.address=pixels; + original->sequences.count=1;original->sequences.address=sequence; + sequence->sprites.count=1;sequence->sprites.address=sprite; + pixels->width=64;pixels->pixels_offset=2048;sprite->bitmap_index=0; + entry=(uint32_t *)r.tag_index;entry[0]='bitm';entry[3]=source;entry[5]=(uint32_t)original; + textures.capacity=8;textures.tag_capacity=8; + textures.items=calloc(8,sizeof(*textures.items));textures.clones=calloc(8,sizeof(*textures.clones)); + if(!mcc_bitmap_specialize(&r,&textures,&material,2) || material==source || r.report.tag_count!=2)return 1; + copy=mcc_bitmap_tag(&r,material,'bitm',sizeof(*copy)); + if(!copy || copy==original || copy->bitmaps.address==pixels || copy->sequences.address==sequence)return 2; + if(((struct bitmap_group_sequence *)copy->sequences.address)->sprites.address==sprite)return 3; + ((struct bitmap_data *)copy->bitmaps.address)->width=32; + if(pixels->width!=64 || pixels->pixels_offset!=2048)return 4; + if(!mcc_bitmap_specialize(&r,&textures,&repeated,2) || repeated!=material || r.report.tag_count!=2)return 5; + if(!mcc_bitmap_specialize(&r,&textures,&meter_handle,4) || meter_handle==material || r.report.tag_count!=3)return 6; + meter=mcc_bitmap_tag(&r,meter_handle,'bitm',sizeof(*meter)); + if(!meter || ((struct bitmap_data *)meter->bitmaps.address)->width!=64)return 7; + if(textures.items[0].channels!=2 || textures.items[1].channels!=4)return 8; + r.bitmaps=&textures;textures.items[0].offset=MCC_BITMAP_BASE;textures.items[0].size=128; + textures.items[0].pixels=calloc(1,128);textures.items[0].pixels[127]=0xAB; + { + unsigned char out[3]={0xCC,0,0xDD}; + if(!mcc_bitmaps_contains(&r,MCC_BITMAP_BASE,128) || mcc_bitmaps_contains(&r,MCC_BITMAP_BASE,129))return 9; + if(mcc_bitmaps_read(&r,material,MCC_BITMAP_BASE+127,1,out+1)!=1 || out[1]!=0xAB || out[0]!=0xCC || out[2]!=0xDD)return 10; + if(mcc_bitmaps_read(&r,source,MCC_BITMAP_BASE,1,out+1)!=0 || mcc_bitmaps_read(&r,material,MCC_BITMAP_BASE+127,2,out+1)!=0)return 11; + if(mcc_bitmaps_read(&r,material,2048,1,out+1)!=-1)return 12; + } + free(textures.items[0].pixels); + free(textures.clones);free(textures.items);free(r.tags);return 0; +} +''' + path = tmp_path / "ownership.c" + path.write_text(source) + output = tmp_path / ("ownership.exe" if sys.platform == "win32" else "ownership") + command = [clang, "-std=gnu99", "-O2", "-Wall", "-Wextra", "-Werror", "-Wno-multichar", str(path), "-o", str(output)] + if sys.platform == "win32": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + else: + command[1:1] = ["-m32"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + run(output) + + +def test_mcc_owned_pixels_survive_pressure_without_legacy_cache(tmp_path): + """Direct bindings retain original pixels above the legacy staging budget.""" + clang = shutil.which("clang") + if not clang: + pytest.skip("clang is needed for MCC bitmap ownership tests") + bitmap = (ROOT / "port/linux/game/mcc_bitmaps.c").read_text() + declarations = "\n".join(structure(bitmap, n) for n in ["mcc_texture", "mcc_bitmap_clone", "mcc_textures"]) + routines = "\n".join(function(bitmap, n) for n in ["mcc_bitmaps_valid", "mcc_bitmaps_pixels", "mcc_bitmaps_dispose"]) + source = r''' +#include +#include +#include +#include "halo_port_capacity.h" +#define FALSE 0 +struct bitmap_data { + uint32_t tag_index,pixels_offset,pixels_size; + short width,height,depth,format,mipmap_count; + void *base_address; +}; +struct mcc_runtime {void *bitmaps;}; +static int bitmap_verify(struct bitmap_data *bitmap,int import) { + (void)import; + return bitmap->width==2048 && bitmap->height==2048 && bitmap->depth==1 && + bitmap->format==11 && bitmap->mipmap_count==11; +} +static long rasterizer_xbox_bitmap_get_pixel_data_size(struct bitmap_data *bitmap) { + (void)bitmap;return 22369664; +} +''' + declarations + '\n' + routines + r''' +int main(void) { + enum {count=13,chain_bytes=22369664}; + struct bitmap_data bitmaps[count],copy; + struct mcc_texture items[count]; + struct mcc_textures textures={0}; + struct mcc_runtime runtime; + uint32_t bytes,i,frame,offset=0x60000000; + memset(bitmaps,0,sizeof(bitmaps));memset(items,0,sizeof(items)); + textures.items=items;textures.count=count;runtime.bitmaps=&textures; + /* Thirteen real 2K ARGB mip chains exceed the desktop cache's 256MiB. + * Only boundary pages need touching: the API must return, never copy, + * the owned immutable allocation, so no legacy allocator is linked. */ + if((uint64_t)count*chain_bytes<=HALO_PORT_TEXTURE_CACHE_SIZE)return 1; + for(i=0;itag_index=0xE1000000+i;bitmap->pixels_offset=offset;bitmap->pixels_size=chain_bytes; + bitmap->width=bitmap->height=2048;bitmap->depth=1;bitmap->format=11;bitmap->mipmap_count=11; + item->bitmap=bitmap;item->handle=bitmap->tag_index;item->offset=offset;item->size=chain_bytes; + item->pixels=malloc(chain_bytes);if(!item->pixels)return 2; + item->pixels[0]=(unsigned char)i;item->pixels[chain_bytes-1]=(unsigned char)(255-i); + offset+=chain_bytes; + } + for(frame=0;frame<3;frame++)for(i=0;iitems=calloc(2,sizeof(*owned->items));owned->count=2; + owned->items[0].pixels=malloc(128);owned->items[0].bitmap=&bitmaps[0]; + owned->items[1].pixels=malloc(128);owned->items[1].bitmap=&bitmaps[1]; + bitmaps[0].base_address=owned->items[0].pixels;bitmaps[1].base_address=&foreign; + runtime.bitmaps=owned;mcc_bitmaps_dispose(&runtime); + if(runtime.bitmaps || bitmaps[0].base_address || bitmaps[1].base_address!=&foreign)return 14; + mcc_bitmaps_dispose(&runtime); + } + return 0; +} +''' + path = tmp_path / "residency.c" + path.write_text(source) + output = tmp_path / ("residency.exe" if sys.platform == "win32" else "residency") + command = [clang, "-std=gnu99", "-O2", "-Wall", "-Wextra", "-Werror", + "-I", str(ROOT / "port/linux/include"), str(path), "-o", str(output)] + if sys.platform == "win32": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + run(output) + + +def pixel_vector(media_tool, tmp_path, data, format, usage=0, side=4): + path = tmp_path / "block.bin" + path.write_bytes(data) + return [int(line, 16) for line in run(media_tool, "pixels", format, usage, side, path).splitlines()] + + +def test_bc1_red_and_partial_block(media_tool, tmp_path): + block = struct.pack(" +#include +#include +#include +typedef int boolean; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +#define MAP_KIND_ROWS 10 +#define NUMBER_OF_GAME_DIFFICULTY_LEVELS 4 +#define LEVEL_UNAVAILABLE 99 +#define SOUND_FORWARD 1 +#define SOUND_BACK 2 +#define SERVER_SETUP_NAME "server_setup" +#define MIN(a,b) ((a)<(b)?(a):(b)) +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define PIN(x,a,b) ((x)<(a)?(a):((x)>(b)?(b):(x))) +#define csmemset memset +enum {MAP_STEP_MAPS, MAP_STEP_DIFFICULTIES}; +struct player_profile {int unused;}; +struct widget_instance {struct {struct {short selected_index;struct widget_instance *extended_description;} list;} parameters;}; +static struct widget_instance spinner, description, list; +static struct {short shown_level,saved_level;struct {boolean available;void *finished;} levels[10];} campaign; +static int no_spinner, empty_catalog, failed, launched, deferred, cooperated, multiplayer; +static int opened, backed, last_controller, last_difficulty, last_sound, focus_first, focus_chosen; +static int solo_updated, host_updated, legacy_updated; +static char last_level[40]; +''' + kinds + '\n' + states + r''' +static struct widget_instance *named(struct widget_instance *widget,char const *name,short marker) { + (void)widget;(void)marker; + return !strcmp(name,"list_item_0_map_kind_spinner") ? (no_spinner ? NULL : &spinner) : &description; +} +/* A sorted mixed catalog: MP, SP, MP, SP, MP. */ +static short mcc_maps_type_count(boolean singleplayer) {return empty_catalog ? 0 : singleplayer ? 2 : 3;} +static short mcc_maps_type_index(short row,boolean singleplayer) { + if (row<0 || row>=mcc_maps_type_count(singleplayer))return NONE; + return singleplayer ? row*2+1 : row*2; +} +static char const *mcc_maps_level_name(short index) { + static char const *names[]={"mcc_maps\\mp0","mcc_maps\\sp0","mcc_maps\\mp1","mcc_maps\\sp1","mcc_maps\\mp2"}; + return index<0 || index>=5 ? NULL : names[index]; +} +static boolean mcc_maps_campaign(short index) {return index==1 || index==3;} +static boolean campaign_profile(short controller,struct player_profile *profile) {(void)profile;last_controller=controller;return TRUE;} +static boolean campaign_fail(void) {failed++;return FALSE;} +static void main_set_map_name(char const *name) {strcpy(last_level,name);} +static void main_defer_map_map_change(void) {deferred++;} +static short main_get_difficulty(void) {return 2;} +static void campaign_start(char const *name,short difficulty,short controller) { + launched++;strcpy(last_level,name);last_difficulty=difficulty;last_controller=controller; +} +static boolean ui_widget_port_mcc_cooperative_level_choose(char const *name,short difficulty) { + cooperated++;strcpy(last_level,name);last_difficulty=difficulty;return TRUE; +} +static boolean ui_widget_port_mcc_multiplayer_map_choose(char const *name) { + multiplayer++;strcpy(last_level,name);return TRUE; +} +static boolean ui_widget_port_open(struct widget_instance *widget,char const *name,boolean *deleted) { + (void)widget; if(strcmp(name,SERVER_SETUP_NAME))exit(70);opened++;*deleted=TRUE;return TRUE; +} +static void map_kind_focus(struct widget_instance *widget,short first,short chosen) {(void)widget;focus_first=first;focus_chosen=chosen;} +static void ui_play_audio_feedback_sound(short sound) {last_sound=sound;} +static void ui_widget_port_go_back(struct widget_instance *widget) {(void)widget;backed++;} +static void map_step_open(struct widget_instance *widget,short step,short entry) { + (void)widget;map_list.step=step;map_list.chosen=entry; +} +static void mcc_solo_list_update(struct widget_instance *widget) {(void)widget;solo_updated++;} +static void mcc_host_list_update(struct widget_instance *widget) {(void)widget;host_updated++;} +static short map_kind_count(short kind,short count) {(void)kind;(void)count;return 3;} +static short map_step_count(void) {return 3;} +static void map_difficulty_text(short entry,wchar_t *text) {(void)entry;(void)text;} +static void map_level_text(short entry,wchar_t *text) {(void)entry;(void)text;} +static void level_row_text(short entry,wchar_t *text) {(void)entry;(void)text;} +static void multiplayer_map_text(short entry,wchar_t *text) {(void)entry;(void)text;} +static void custom_campaign_map_text(short entry,wchar_t *text) {(void)entry;(void)text;} +static void custom_multiplayer_map_text(short entry,wchar_t *text) {(void)entry;(void)text;} +static short map_kind_rows_update(struct widget_instance *widget,short *first,short count,void(*text)(short,wchar_t *)) { + (void)widget;(void)first;(void)count;(void)text;legacy_updated++;return NONE; +} +static short map_kind_display_index(short kind,short entry) {(void)kind;return entry;} +static void map_description_show(struct widget_instance *widget,short level,short map) {(void)widget;(void)level;(void)map;} +static void visible_set(struct widget_instance *widget,boolean visible) {(void)widget;(void)visible;} +static void profile_name_show(struct widget_instance *widget) {(void)widget;} +static void level_description(struct widget_instance *widget,char const *prefix,short level,boolean saved,void *finished,short controller) { + (void)widget;(void)prefix;(void)level;(void)saved;(void)finished;(void)controller; +} +''' + '\n'.join(function(implementation, name) for name in names) + r''' +#define CHECK(c,n) do {if(!(c)){fprintf(stderr,"check %d failed\n",n);return n;}} while(0) +int main(int argc,char **argv) { + boolean deleted=FALSE; + if(argc<2)return 99; + list.parameters.list.extended_description=&description; + if(!strcmp(argv[1],"kinds")) { + int i;printf("%d\n",NUMBER_OF_MAP_KINDS); + for(i=0;i +#include +#include +#include +#include +#include +#include +typedef unsigned char boolean,byte; +typedef unsigned short word; +typedef struct {short y0,x0,y1,x1;} rectangle2d; +typedef struct {float alpha,red,green,blue;} real_argb_color; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +#define NUMBEROF(a) (sizeof(a)/sizeof(*(a))) +#define FLAG(bit) (1u<<(bit)) +#define MIN(a,b) ((a)<(b)?(a):(b)) +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define csstrnzcpy(d,s,n) do {strncpy(d,s,n);(d)[(n)-1]=0;} while(0) +#endif +''') + source = r''' +#include "cseries.h" +#include "mcc_pause.h" +#include "mcc_ui.h" +''' + native_types + r''' +typedef char widget_size_must_match_native[sizeof(struct ui_widget_definition)==0x3ec?1:-1]; +typedef char child_size_must_match_native[sizeof(struct ui_widget_child_reference)==0x50?1:-1]; +typedef char event_size_must_match_native[sizeof(struct ui_widget_event_handler_reference)==0x48?1:-1]; +#define CHECK(c) do {if(!(c)){fprintf(stderr,"line %d: %s\n",__LINE__,#c);return 1;}}while(0) +static unsigned allocation_calls,live_allocations,fail_allocation; +static void *test_malloc(size_t bytes) { + void *p; + if(++allocation_calls==fail_allocation)return NULL; + p=malloc(bytes);if(p)live_allocations++;return p; +} +static void *test_calloc(size_t count,size_t bytes) { + void *p=test_malloc(count*bytes);if(p)memset(p,0,count*bytes);return p; +} +static void test_free(void *p) {if(p){assert(live_allocations);live_allocations--;free(p);}} +#define malloc test_malloc +#define calloc test_calloc +#define free test_free +#include "mcc_pause.c" +#undef malloc +#undef calloc +#undef free +static struct {short width,height;enum mcc_pause_art_kind kind;long handle;} art_records[64]; +static unsigned art_count,art_calls,fail_art; +static long art(short width,short height,enum mcc_pause_art_kind kind) { + unsigned i; + if(++art_calls==fail_art)return NONE; + assert(width>0&&height>0&&width<=640&&height<=480); + assert(kind>=MCC_PAUSE_ART_PANEL&&kind<=MCC_PAUSE_ART_DIM); + for(i=0;ix0=(short)(bounds->x0+width); +} +''' + function(native, "ui_widget_port_text_wrap") + r''' +static void apply_wrap_hook(long handle,wchar_t *value,rectangle2d bounds,rectangle2d *clipped) { + struct {long definition_tag_index;} instance={handle},*widget=&instance; + wchar_t **text=&value; + rectangle2d clip=*clipped; +''' + wrap_hook + r''' + *clipped=clip; +} +static int wide_equal(wchar_t const *a,wchar_t const *b) { + while(*a&&*a==*b){a++;b++;}return *a==*b; +} +static struct mcc_pause_tag const *find(long handle) { + long i; + for(i=0;ihandle==handle)return mcc_pause_tag_at(i); + return NULL; +} +static struct ui_widget_definition const *widget(long handle) { + struct mcc_pause_tag const *tag=find(handle); + assert(tag&&tag->group=='DeLa'&&tag->data);return tag->data; +} +static unsigned actions(long handle,unsigned depth) { + struct ui_widget_definition const *w=widget(handle); + struct ui_widget_event_handler_reference const *events=w->event_handlers.address; + struct ui_widget_child_reference const *children=w->child_widgets.address; + unsigned result=0;long i; + assert(depth<10); + for(i=0;ievent_handlers.count;i++) { + if((events[i].flags&128)&&events[i].function>=MCC_PAUSE_ACTION_RESUME&& + events[i].function<=MCC_PAUSE_ACTION_SETTINGS) + result|=1u<<(events[i].function-MCC_PAUSE_ACTION_RESUME); + if(events[i].flags&8)result|=actions(events[i].widget_tag.index,depth+1); + } + for(i=0;ichild_widgets.count;i++)result|=actions(children[i].widget_tag.index,depth+1); + return result; +} +static unsigned objectives; +static int walk_bounds(long handle,int x,int y,int width,int height,int layout,unsigned depth) { + struct ui_widget_definition const *w=widget(handle); + struct ui_widget_child_reference const *children=w->child_widgets.address; + struct ui_widget_game_data_input_reference const *inputs=w->game_data_inputs.address; + long i; + CHECK(depth<10); + CHECK(w->bounds.x0+x>=0&&w->bounds.y0+y>=0&&w->bounds.x1+x<=width&&w->bounds.y1+y<=height); + CHECK(w->bounds.x1>w->bounds.x0&&w->bounds.y1>w->bounds.y0); + if(w->type==1)CHECK(w->text_font.index==assets.font[layout]||w->text_font.index==assets.small_font[layout]); + for(i=0;igame_data_inputs.count;i++){CHECK(inputs[i].function==18);objectives++;} + if(w->type==3) { + CHECK((w->flags&0x61)==0x21||(w->flags&0x61)==0x41); + for(i=1;ichild_widgets.count;i++) { + struct ui_widget_definition const *previous=widget(children[i-1].widget_tag.index); + if(w->flags&0x20)CHECK(children[i].vertical_offset>=children[i-1].vertical_offset+previous->bounds.y1); + else CHECK(children[i].horizontal_offset>=children[i-1].horizontal_offset+previous->bounds.x1); + } + } + for(i=0;ichild_widgets.count;i++)CHECK(!walk_bounds(children[i].widget_tag.index, + x+children[i].horizontal_offset,y+children[i].vertical_offset,width,height,layout,depth+1)); + return 0; +} +static int context_check(boolean do_bounds) { + int campaign,network,host,teams,count,first; + for(campaign=0;campaign<2;campaign++)for(network=0;network<2;network++) + for(host=0;host<2;host++)for(teams=0;teams<2;teams++)for(count=1;count<=4;count++)for(first=0;first<2;first++) { + int layout=count==1?0:count==2||(count==3&&first)?1:2; + int width=layout==2?320:640,height=layout==0?480:240; + long root=mcc_pause_select(campaign,network,host,teams,(short)count,first); + struct ui_widget_definition const *w; + unsigned bits; + CHECK(root!=NONE&&mcc_pause_owns(root));w=widget(root); + CHECK(w->bounds.x0==0&&w->bounds.y0==0&&w->bounds.x1==width&&w->bounds.y1==height); + if(do_bounds) { + struct ui_widget_child_reference const *children=w->child_widgets.address; + long child,list=NONE,footer=NONE; + objectives=0;CHECK(!walk_bounds(root,0,0,width,height,layout,0)); + CHECK(objectives==(unsigned)campaign); + for(child=0;childchild_widgets.count;child++) { + struct ui_widget_definition const *definition=widget(children[child].widget_tag.index); + if(definition->type==3)list=child; + if(!strcmp(definition->name,"button_key"))footer=child; + } + CHECK(list!=NONE&&footer!=NONE); + CHECK(children[footer].vertical_offset>=children[list].vertical_offset+ + widget(children[list].widget_tag.index)->bounds.y1); + CHECK(widget(children[footer].widget_tag.index)->text_font.index==assets.small_font[layout]); + CHECK(widget(children[footer].widget_tag.index)->event_handlers.count==0); + if(campaign&&layout!=2) { + /* The stock campaign panels have a five-pixel gap. */ + CHECK(widget(children[0].widget_tag.index)->bounds.x1==218); + CHECK(children[0].horizontal_offset+218+5==children[1].horizontal_offset); + } + } + bits=actions(root,0); + CHECK(bits&1u); /* Resume is always reachable. */ + CHECK(bits&(1u<<(MCC_PAUSE_ACTION_QUIT-MCC_PAUSE_ACTION_RESUME))); + if(network&&!host)CHECK(!(bits&((1u<<1)|(1u<<2)|(1u<<3)|(1u<<5)))); + if(campaign)CHECK(!(bits&((1u<<5)|(1u<<6)|(1u<<7)|(1u<<8)))); + /* The native Settings screen requires a full viewport, including + * when the first player owns the half-screen in a three-player game. */ + CHECK(!!(bits&(1u<<8))==(!campaign&&assets.settings&&count==1)); + if(!campaign)CHECK(!(bits&((1u<<1)|(1u<<3)))); + if(!network||campaign||!teams)CHECK(!(bits&((1u<<6)|(1u<<7)))); + if(!campaign&&network&&teams)CHECK((bits&((1u<<6)|(1u<<7)))==((1u<<6)|(1u<<7))); + if(!campaign&&network&&host)CHECK(bits&(1u<<5)); + } + return 0; +} +int main(int argc,char **argv) { + struct mcc_pause_assets original=assets; + long i,j;unsigned calls,art_total; + CHECK(argc==2); + CHECK(mcc_pause_tag_count()==0&&mcc_pause_tag_at(0)==NULL&&!mcc_pause_owns(0)); + CHECK(mcc_pause_select(TRUE,FALSE,FALSE,FALSE,1,TRUE)==NONE); + CHECK(mcc_pause_build(1024,0x4321,&assets)); + CHECK(!memcmp(&assets,&original,sizeof(assets))); + calls=allocation_calls;art_total=art_calls; + if(!strcmp(argv[1],"identity")) { + CHECK(mcc_pause_tag_count()>24&&mcc_pause_tag_count()<4096); + CHECK(mcc_pause_tag_at(-1)==NULL&&mcc_pause_tag_at(mcc_pause_tag_count())==NULL); + for(i=0;idata&&t->name&&strlen(t->name)>0); + CHECK(!strncmp(t->name,"mcc_pause\\",10)); + CHECK((t->handle&0xffff)==1024+i); + CHECK(t->group=='DeLa'||t->group=='ustr'); + CHECK(mcc_pause_owns(t->handle)==(t->group=='DeLa')); + CHECK(!mcc_pause_owns(t->handle^0x01000000)); + for(j=0;jname,mcc_pause_tag_at(j)->name)!=0&&t->handle!=mcc_pause_tag_at(j)->handle); + } + } else if(!strcmp(argv[1],"contexts")) { + CHECK(!context_check(FALSE)); + } else if(!strcmp(argv[1],"layout")) { + CHECK(!context_check(TRUE)); + } else if(!strcmp(argv[1],"actions")) { + for(i=0;igroup!='DeLa')continue; + w=t->data;e=w->event_handlers.address; + for(j=0;jevent_handlers.count;j++) { + CHECK(!(e[j].flags&1024)&&!e[j].script[0]); + if(e[j].flags&128)CHECK(e[j].function>=MCC_PAUSE_ACTION_RESUME&&e[j].function<=MCC_PAUSE_ACTION_SETTINGS); + if(e[j].flags&8)CHECK(find(e[j].widget_tag.index)&&mcc_pause_owns(e[j].widget_tag.index)); + if(e[j].event_type==0) { + long k; + for(k=0;kevent_handlers.count;k++)if(e[k].event_type==28)break; + CHECK(kevent_handlers.count&&e[k].flags==e[j].flags&&e[k].function==e[j].function&& + e[k].widget_tag.index==e[j].widget_tag.index); + } + } + } + } else if(!strcmp(argv[1],"controller-close")) { + unsigned resume_accept=0,resume_back=0,red=0,blue=0; + for(i=0;igroup!='DeLa')continue; + w=t->data;e=w->event_handlers.address; + for(j=0;jevent_handlers.count;j++) { + if(e[j].function!=MCC_PAUSE_ACTION_RESUME&& + e[j].function!=MCC_PAUSE_ACTION_RED_TEAM&& + e[j].function!=MCC_PAUSE_ACTION_BLUE_TEAM)continue; + /* These callbacks own controller-local tree/history cleanup. + * Generic close-all would dismiss another player's settings. */ + CHECK(e[j].flags==128&&e[j].widget_tag.index==NONE); + if(e[j].function==MCC_PAUSE_ACTION_RESUME) { + if(e[j].event_type==0||e[j].event_type==28)resume_accept++; + else {CHECK(e[j].event_type==1||e[j].event_type==13);resume_back++;} + } else { + CHECK(e[j].event_type==0||e[j].event_type==28); + if(e[j].function==MCC_PAUSE_ACTION_RED_TEAM)red++;else blue++; + } + } + } + CHECK(resume_accept&&resume_back&&red&&blue); + } else if(!strcmp(argv[1],"dialogs")) { + unsigned dialogs=0; + for(i=0;igroup!='DeLa')continue; + w=t->data; + if(strcmp(w->name,"confirmation"))continue; + dialogs++;children=w->child_widgets.address; + for(j=0;jchild_widgets.count;j++)if(widget(children[j].widget_tag.index)->type==3)list=children[j].widget_tag.index; + CHECK(list!=NONE); + children=widget(list)->child_widgets.address; + e=widget(children[0].widget_tag.index)->event_handlers.address; + CHECK(e[0].event_type==0&&e[0].flags==512); /* Native default focus starts on Cancel. */ + e=widget(children[1].widget_tag.index)->event_handlers.address; + CHECK(e[0].event_type==0&&(e[0].flags&128)&&e[0].function!=MCC_PAUSE_ACTION_RESUME); + e=w->event_handlers.address; + CHECK(w->event_handlers.count==2&&e[0].event_type==1&&e[1].event_type==13&&e[0].flags==512&&e[1].flags==512); + objectives=0; + CHECK(!walk_bounds(t->handle,0,0,w->bounds.x1,w->bounds.y1, + w->bounds.x1==320?2:w->bounds.y1==240?1:0,0)); + } + CHECK(dialogs>=15); + } else if(!strcmp(argv[1],"paragraphs")) { + unsigned objective_count=0,confirm_count=0; + for(i=0;idata; + boolean paragraph=t->group=='DeLa'&&(!strcmp(w->name,"objective")||!strcmp(w->name,"confirmation_text")); + CHECK(mcc_pause_text_wrap_needed(t->handle)==paragraph); + CHECK(!mcc_pause_text_wrap_needed(t->handle^0x01000000)); + if(!paragraph)continue; + if(!strcmp(w->name,"objective"))objective_count++;else confirm_count++; + } + CHECK(objective_count&&confirm_count); + CHECK(!mcc_pause_text_wrap_needed(NONE)); + } else if(!strncmp(argv[1],"wrap-",5)) { + long paragraph=NONE,plain=mcc_pause_select(TRUE,FALSE,FALSE,FALSE,1,TRUE); + rectangle2d bounds={20,100,90,130},clip={0,0,480,640},before=clip; + struct {wchar_t text[128];unsigned canary;} value={L"aa aa aa",0x1234abcd}; + for(i=0;ihandle)) { + paragraph=mcc_pause_tag_at(i)->handle;break; + } + CHECK(paragraph!=NONE); + if(!strcmp(argv[1],"wrap-boundaries")) { + wchar_t exact[]=L"WW WW",over[]=L"WW WW",unbroken[]=L"WWWWWW"; + CHECK(ui_widget_port_text_wrap(exact,&bounds,39)&&wide_equal(exact,L"WW WW")); + CHECK(ui_widget_port_text_wrap(over,&bounds,38)&&wide_equal(over,L"WW\rWW")); + CHECK(!ui_widget_port_text_wrap(unbroken,&bounds,30)&&wide_equal(unbroken,L"WWWWWW")); + apply_wrap_hook(paragraph,value.text,bounds,&clip); + CHECK(wide_equal(value.text,L"aa aa\raa")&&value.canary==0x1234abcd); + CHECK(!memcmp(&clip,&bounds,sizeof(clip))); + } else if(!strcmp(argv[1],"wrap-lines")) { + wchar_t lines[]=L"aa aa\r\nbb bb cc",empty[]=L""; + apply_wrap_hook(paragraph,lines,bounds,&clip); + CHECK(wide_equal(lines,L"aa aa\r\nbb bb\rcc")); + apply_wrap_hook(paragraph,lines,bounds,&clip); + CHECK(wide_equal(lines,L"aa aa\r\nbb bb\rcc")); + CHECK(ui_widget_port_text_wrap(empty,&bounds,30)&&!empty[0]); + } else { + CHECK(!strcmp(argv[1],"wrap-isolation")); + apply_wrap_hook(plain,value.text,bounds,&clip); + CHECK(wide_equal(value.text,L"aa aa aa")&&!memcmp(&clip,&before,sizeof(clip))); + apply_wrap_hook(paragraph^0x01000000,value.text,bounds,&clip); + CHECK(wide_equal(value.text,L"aa aa aa")&&!memcmp(&clip,&before,sizeof(clip))); + clip=(rectangle2d){25,105,80,125};before=clip; + apply_wrap_hook(paragraph,value.text,bounds,&clip); + CHECK(wide_equal(value.text,L"aa aa\raa")&&!memcmp(&clip,&before,sizeof(clip))); + } + } else if(!strcmp(argv[1],"strings")) { + unsigned lists=0; + for(i=0;idata; + struct tag_data const *entries=block->address; + if(t->group!='ustr')continue; + lists++;CHECK(block->count>=20&&block->count<128); + for(j=0;jcount;j++) { + unsigned short const *text=entries[j].address;long k; + CHECK(text&&entries[j].size>=2&&entries[j].size<=256&&!(entries[j].size&1)); + CHECK(!text[entries[j].size/2-1]); + for(k=0;k=32&&text[k]<127); + } + } + CHECK(lists==1); + } else if(!strcmp(argv[1],"failure")) { + mcc_pause_dispose();CHECK(!live_allocations); + for(fail_allocation=1;fail_allocation<=calls;fail_allocation++) { + allocation_calls=0;CHECK(!mcc_pause_build(1024,0x4321,&assets)); + CHECK(!live_allocations&&!mcc_pause_tag_count()); + CHECK(mcc_pause_select(TRUE,FALSE,FALSE,FALSE,1,TRUE)==NONE); + } + fail_allocation=0; + CHECK(mcc_pause_build(1024,0x4321,&assets)); + } else if(!strcmp(argv[1],"art-failure")) { + mcc_pause_dispose();CHECK(!live_allocations); + for(fail_art=1;fail_art<=art_total;fail_art++) { + art_calls=0;CHECK(!mcc_pause_build(1024,0x4321,&assets)); + CHECK(!live_allocations&&!mcc_pause_tag_count()); + } + fail_art=0;CHECK(mcc_pause_build(1024,0x4321,&assets)); + } else if(!strcmp(argv[1],"invalid")) { + CHECK(mcc_pause_select(TRUE,FALSE,FALSE,FALSE,0,TRUE)==NONE); + CHECK(mcc_pause_select(TRUE,FALSE,FALSE,FALSE,5,TRUE)==NONE); + CHECK(!mcc_pause_build(-1,1,&assets));CHECK(!live_allocations&&!mcc_pause_tag_count()); + CHECK(!mcc_pause_build(65535,1,&assets));CHECK(!live_allocations&&!mcc_pause_tag_count()); + CHECK(!mcc_pause_build(0,1,NULL));CHECK(!live_allocations&&!mcc_pause_tag_count()); + assets.art=NULL;CHECK(!mcc_pause_build(1024,1,&assets));assets=original; + for(i=0;i<3;i++) { + assets.font[i]=NONE;CHECK(!mcc_pause_build(1024,1,&assets));assets=original; + assets.small_font[i]=NONE;CHECK(!mcc_pause_build(1024,1,&assets));assets=original; + } + CHECK(!live_allocations&&!mcc_pause_tag_count()); + } else if(!strcmp(argv[1],"lifetime")) { + long old=mcc_pause_select(TRUE,FALSE,FALSE,FALSE,1,TRUE); + mcc_pause_dispose();mcc_pause_dispose();CHECK(!live_allocations&&!mcc_pause_owns(old)); + assets.settings=FALSE;CHECK(mcc_pause_build(2048,0x4322,&assets));CHECK(!mcc_pause_owns(old)); + for(i=0;i<2;i++)for(j=0;j<2;j++) { + long root=mcc_pause_select(FALSE,TRUE,(boolean)i,(boolean)j,1,TRUE); + CHECK(!(actions(root,0)&(1u<<8))); + } + } else return 99; + mcc_pause_dispose();CHECK(!live_allocations&&!mcc_pause_tag_count());return 0; +} +''' + path = work / "pause_check.c" + path.write_text(source) + binary = work / ("pause_check.exe" if sys.platform == "win32" else "pause_check") + command = [clang, "-std=gnu99", "-O2", "-Wall", "-Wextra", "-Werror", + "-Wno-unused-function", "-I", str(work), "-I", str(ROOT / "port/linux/game"), + str(path), "-o", str(binary)] + if sys.platform == "win32": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + else: + command[1:1] = ["-m32", "-fshort-wchar"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return binary + + +@pytest.mark.parametrize("case", ["identity", "contexts", "layout", "actions", "controller-close", "dialogs", "paragraphs", "strings", + "wrap-boundaries", "wrap-lines", "wrap-isolation", "failure", "art-failure", "invalid", "lifetime"]) +def test_mcc_pause_builder(pause_tool, case): + result = subprocess.run([str(pause_tool), case], capture_output=True, text=True, timeout=30) + assert result.returncode == 0, f"{case}: exit {result.returncode}\n{result.stdout}{result.stderr}" diff --git a/tools/test_mcc_pause_runtime.py b/tools/test_mcc_pause_runtime.py new file mode 100644 index 0000000000..32a15d0e70 --- /dev/null +++ b/tools/test_mcc_pause_runtime.py @@ -0,0 +1,313 @@ +"""Compile the real pause builder and resource importer with a fake renderer. + +Guarded allocations cover generated pixels and the copied live tag table. +Failure injection checks every allocation and texture-creation boundary, so +partial imports cannot publish dangling tags or leak native texture headers. +""" +from pathlib import Path +import re +import shutil +import subprocess +import sys + +import pytest + +from harness import function, structure + +ROOT = Path(__file__).resolve().parents[1] + + +def _compile(source, binary): + clang = shutil.which("clang") + if not clang: + pytest.skip("clang is needed for the MCC pause runtime tests") + command = [clang, "-std=gnu99", "-O2", "-Wall", "-Wextra", "-Werror", "-Wno-unused-function", + "-I", str(ROOT / "port/linux/game"), str(source), "-o", str(binary)] + if sys.platform == "win32": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + else: + command[1:1] = ["-m32"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + + +@pytest.fixture(scope="module") +def runtime_tool(tmp_path_factory): + work = tmp_path_factory.mktemp("mcc-pause-runtime") + tags = (ROOT / "source/tag_files/tag_groups.h").read_text() + bitmaps = (ROOT / "source/bitmaps/bitmap_group.h").read_text() + strings = (ROOT / "source/text/text_group.h").read_text() + types = "\n".join(structure(tags, name) for name in ["tag_block", "tag_reference", "tag_data"]) + types += "\n" + "\n".join(structure(bitmaps, name) for name in [ + "bitmap_data", "bitmap_group_sequence", "bitmap_group"]) + types += "\n" + "\n".join(structure(strings, name) for name in ["string_list", "string_list_entry"]) + # Keep both implementations intact; replace only their project headers + # with the native structures and narrow host services declared below. + implementations = "\n".join(re.sub(r'^#include "[^"\n]+"\s*$', "", (ROOT / name).read_text(), flags=re.M) + for name in ["port/linux/game/mcc_pause.c", "port/linux/game/mcc_pause_runtime.c"]) + unbind = function((ROOT / "port/linux/src/d3d8_gl.c").read_text(), "xgpu_mcc_texture_unbind") + source = r''' +#include +#include +#include +#include +#include +#include +typedef unsigned char boolean; +typedef unsigned short word; +typedef unsigned long DWORD; +typedef float real; +union point2d {struct {short x,y;} point;long value;}; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +#define NUMBEROF(a) (sizeof(a)/sizeof(*(a))) +#include "mcc_pause.h" +#include "mcc_ui.h" +#include "mcc_pause_runtime.h" +''' + types + r''' +enum {D3DTSS_MAXSTAGES=4}; +static struct {void *textures[D3DTSS_MAXSTAGES];} device; +''' + unbind + r''' +#define CHECK(c) do {if(!(c)){fprintf(stderr,"line %d: %s\n",__LINE__,#c);return 1;}}while(0) +struct allocation {struct allocation *next;size_t bytes;unsigned long pad[2];}; +static struct allocation *allocations; +static unsigned malloc_calls,live_allocations,fail_malloc; +static unsigned gpu_calls,gpu_live,gpu_changed,gpu_deleted,fail_gpu; +static unsigned published,restored,logs; +static boolean active=TRUE,null_renderer; +static int connection; +static long large_font=0x11110000,small_font=0x11120001,live_count; +static void *live_table,*original_table; +static struct test_scenario {short type;} scenario,*global_scenario; +static struct {struct {boolean teams;} universal_variant;} variant; +enum {_game_connection_local,_game_connection_network_client,_game_connection_network_server}; +static void *test_malloc(size_t bytes) { + struct allocation *a;unsigned char *end; + if(++malloc_calls==fail_malloc)return NULL; + a=malloc(sizeof(*a)+bytes+16);if(!a)return NULL; + a->next=allocations;a->bytes=bytes;a->pad[0]=a->pad[1]=0xcafebabe;allocations=a; + end=(unsigned char *)(a+1)+bytes;memset(end,0xD7,16);live_allocations++;return a+1; +} +static void check_guards(void) { + struct allocation *a;unsigned i; + for(a=allocations;a;a=a->next) { + assert(a->pad[0]==0xcafebabe&&a->pad[1]==0xcafebabe); + for(i=0;i<16;i++)assert(((unsigned char *)(a+1))[a->bytes+i]==0xD7); + } +} +static void test_free(void *p) { + struct allocation **slot=&allocations,*a; + assert(p);check_guards(); + while(*slot&&(*slot)+1!=p)slot=&(*slot)->next; + assert(*slot);a=*slot;*slot=a->next;memset(p,0xEA,a->bytes);free(a);live_allocations--; +} +static boolean mcc_cache_tags_loaded(void){return active;} +void mcc_ui_settings_close(short controller){assert(controller==NONE);} +static short game_connection(void){return (short)connection;} +static void *global_network_game_server_get(void){return connection==_game_connection_network_server?&variant:NULL;} +#define game_engine_get_variant() (&variant) +static long tag_loaded(long group,char const *name) { + assert(group=='font'); + if(!strcmp(name,"ui\\large_ui"))return large_font; + if(!strcmp(name,"ui\\small_ui"))return small_font; + assert(0);return NONE; +} +char const *config_string(char const *name){assert(!strcmp(name,"display.menus"));return "pc";} +void platform_log(char const *format,...){assert(format);logs++;} +void *cache_files_tag_instances(long *count){*count=live_count;return live_table;} +void cache_files_set_tag_instances(void *instances,long count) { + assert(instances&&count>=2); + if(instances==original_table)restored++; + else {assert(live_table==original_table&&mcc_pause_tag_count()>24);published++;} + live_table=instances;live_count=count; +} +static boolean rasterizer_bitmap_new(struct bitmap_data *bitmap) { + assert(bitmap->width>0&&!(bitmap->width&(bitmap->width-1))); + assert(bitmap->height>0&&!(bitmap->height&(bitmap->height-1))); + assert(bitmap->format==11&&bitmap->depth==1&&bitmap->type==0&&bitmap->flags==1); + assert(bitmap->pixels_size==bitmap->width*bitmap->height*4&&bitmap->base_address); + if(++gpu_calls==fail_gpu)return FALSE; + if(null_renderer)return TRUE; + bitmap->hardware_format=test_malloc(16); + if(!bitmap->hardware_format)return FALSE; + device.textures[gpu_calls%3]=bitmap->hardware_format; + gpu_live++;return TRUE; +} +static void rasterizer_bitmap_changed(struct bitmap_data *bitmap) { + assert(bitmap->base_address&&(bitmap->hardware_format||null_renderer));check_guards();gpu_changed++; +} +static void rasterizer_bitmap_delete(struct bitmap_data *bitmap) { + unsigned stage; + assert(bitmap->hardware_format&&gpu_live); + for(stage=0;stagehardware_format) { + fputs("texture header released while still bound\n",stderr);exit(88); + } + test_free(bitmap->hardware_format);bitmap->hardware_format=NULL;gpu_live--;gpu_deleted++; +} +#define malloc test_malloc +#define free test_free +''' + implementations + r''' +#undef malloc +#undef free +static struct mcc_pause_instance original[2]; +static unsigned char original_copy[sizeof(original)]; +static void setup(void) { + memset(original,0,sizeof(original)); + original[0].group=original[1].group='font';original[0].handle=0x11110000;original[1].handle=0x11120001; + original[0].name="ui\\large_ui";original[1].name="ui\\small_ui"; + original[0].data=&original[0];original[1].data=&original[1]; + memcpy(original_copy,original,sizeof(original)); + live_table=original;original_table=original;live_count=2; + global_scenario=&scenario;scenario.type=0; + device.textures[3]=&scenario; /* An unrelated texture identity must survive. */ +} +static int clean(void) { + mcc_pause_runtime_unload(); + CHECK(live_table==original_table&&live_count==2&&!live_allocations&&!gpu_live&&!mcc_pause_tag_count()); + CHECK(!device.textures[0]&&!device.textures[1]&&!device.textures[2]&&device.textures[3]==&scenario); + CHECK(!memcmp(original_copy,original,sizeof(original)));return 0; +} +static int inventory(void) { + long i,j; + struct mcc_pause_instance *table=live_table; + CHECK(live_table!=original_table&&live_count>2&&published==1); + CHECK(!memcmp(original,table,sizeof(original))); + for(i=2;isequences.address; + struct bitmap_data const *frames=group->bitmaps.address; + CHECK(group->sequences.count==1&&sequence->first_bitmap_index==0&& + sequence->bitmap_count==group->bitmaps.count&&group->bitmaps.count>=1&&group->bitmaps.count<=2); + for(j=0;jbitmaps.count;j++) { + CHECK(frames[j].tag_index==table[i].handle&&frames[j].base_address); + CHECK(frames[j].hardware_format||null_renderer); + } + } else CHECK(table[i].group=='DeLa'||table[i].group=='ustr'); + } + return 0; +} +static int pixel_check(void) { + long i,j;int x,y; + for(i=0;igroup.bitmaps.count;j++) { + struct bitmap_data const *bitmap=&art->frames[j]; + unsigned long const *pixels=bitmap->base_address; + for(y=0;yheight;y++)for(x=0;xwidth;x++) { + unsigned long color=pixels[y*bitmap->width+x]; + if(art->kind==MCC_PAUSE_ART_DIM)CHECK(color==0x80000000UL); + else if(x>=art->width||y>=art->height)CHECK(!color); + else if(art->kind==MCC_PAUSE_ART_HIGHLIGHT&&!j)CHECK(!color); + else CHECK(!color||color==0xFF2896FFUL||color==0xC00A2649UL||color==0x60204A75UL); + } + } + } + check_guards();return 0; +} +int main(int argc,char **argv) { + unsigned calls,textures,k;long old; + CHECK(argc==2);setup(); + if(!strcmp(argv[1],"inactive")) { + active=FALSE;CHECK(!mcc_pause_runtime_load()); + CHECK(!malloc_calls&&!gpu_calls&&!published&&!mcc_pause_runtime_active()); + CHECK(mcc_pause_runtime_screen(1,TRUE)==NONE);CHECK(!clean());return 0; + } + if(!strcmp(argv[1],"fonts")) { + large_font=NONE;CHECK(mcc_pause_runtime_load());CHECK(mcc_pause.assets.font[0]==small_font);CHECK(!clean()); + large_font=original[0].handle;small_font=NONE; + CHECK(mcc_pause_runtime_load());CHECK(mcc_pause.assets.small_font[0]==large_font);CHECK(!clean()); + large_font=small_font=NONE; + CHECK(mcc_pause_runtime_load());CHECK(mcc_pause.assets.font[0]==original[0].handle);CHECK(!clean()); + original[0].group=original[1].group='bitm';memcpy(original_copy,original,sizeof(original)); + published=0;CHECK(!mcc_pause_runtime_load());CHECK(!published&&!live_allocations&&!gpu_live);CHECK(!clean());return 0; + } + if(!strcmp(argv[1],"table-bounds")) { + live_count=-1;CHECK(!mcc_pause_runtime_load()&&!published&&!live_allocations);setup(); + live_count=63001;CHECK(!mcc_pause_runtime_load()&&!published&&!live_allocations);setup(); + live_table=NULL;CHECK(!mcc_pause_runtime_load()&&!published&&!live_allocations);setup(); + CHECK(!clean());return 0; + } + if(!strcmp(argv[1],"art-bounds")) { + static short const invalid[][2]={{0,1},{1,0},{-1,1},{1,-1},{1025,1},{1,513}}; + long handle; + for(k=0;k +#include +#include +#include +typedef unsigned char boolean; +typedef unsigned short word; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +#define NUMBER_OF_GAME_DIFFICULTY_LEVELS 4 +#define MAXIMUM_ROWS 11 +#define MAXIMUM_PROFILES 32 +#define MAXIMUM_PLAYER_PROFILE_NAME_LENGTH 11 +#define PROFILE_VALID_BIT 0x80000000UL +#define PIN(v,a,b) ((v)<(a)?(a):((v)>(b)?(b):(v))) +struct player_profile {wchar_t player_name[12];}; +''' + structure(menu, "campaign_saved_game") + r''' +static struct { + short saved_game_count,shown_saved_game; + struct campaign_saved_game saved_games[MAXIMUM_ROWS]; +} campaign; +static char mcc_saved_game_names[MAXIMUM_ROWS][256]; +static long active=PROFILE_VALID_BIT; +static char saves[3][256]; +static short difficulty=2; +static int storage_valid=1,required=0,available=1,stock_calls=0,starts=0; +static char started[256]; +long player_ui_get_active_player_profile_index(short player) {(void)player;return active;} +boolean game_state_test_persistent_storage(char *name,short *out,boolean *corrupt) { + strcpy(name,saves[active&255]);*out=difficulty;*corrupt=0;return storage_valid; +} +boolean mcc_level_name(char const *name) {return !strncmp(name,"mcc_maps\\",9);} +boolean mcc_cache_require(char const *name,unsigned long checksum) { + (void)checksum;if(!mcc_level_name(name))exit(10);required++;return available; +} +boolean campaign_profile(short controller,struct player_profile *profile) { + (void)controller;wcscpy(profile->player_name,L"player");return active!=NONE; +} +boolean campaign_fail(void) {return FALSE;} +void campaign_start(char const *name,short level,short controller) { + (void)level;(void)controller;strcpy(started,name);starts++; +} +boolean ui_widget_port_saved_game(char const **name,short *level,short *out) { + stock_calls++;*name=saves[active&255];*level=0;*out=difficulty; + return storage_valid && !strncmp(*name,"levels\\",7); +} +void player_profiles_enumerate_available_to_local_player_index(long player,word *count,long *profiles,boolean unused) { + int i;(void)player;(void)unused;*count=3; + for(i=0;i<3;i++)profiles[i]=PROFILE_VALID_BIT+i; +} +boolean player_profile_get(long index,struct player_profile *profile) { + if((index&255)>2)return FALSE;wcscpy(profile->player_name,L"player");return TRUE; +} +void player_ui_set_active_player_profile(short player,long index,struct player_profile const *profile) { + (void)player;(void)profile;active=index; +} +void ustrncpy(wchar_t *out,wchar_t const *in,long count) {wcsncpy(out,in,(size_t)count);} +void campaign_levels_read(struct player_profile const *profile) {(void)profile;} +char const *main_get_solo_level_name(short level) {if(level!=0)exit(11);return "levels\\a10\\a10";} +''' + functions + r''' +int main(int argc,char **argv) { + int result=0; + if(argc<2)return 2; + strcpy(saves[0],"levels\\a10\\a10"); + strcpy(saves[1],"mcc_maps\\a10"); + strcpy(saves[2],"custom_maps\\a10"); + if(!strcmp(argv[1],"continue")) { + if(argc<3)return 2; + strcpy(saves[0],argv[2]); + if(argc>3)available=atoi(argv[3]); + result=campaign_continue(0); + } else if(!strcmp(argv[1],"list")) { + saved_games_read(0); + if(campaign.saved_game_count!=2 || mcc_saved_game_names[0][0] || + strcmp(mcc_saved_game_names[1],"mcc_maps\\a10") || (unsigned long)active!=PROFILE_VALID_BIT)return 12; + if(argc>2) { + campaign.shown_saved_game=(short)atoi(argv[2]); + result=saved_game_continue(0); + } else result=1; + } else if(!strcmp(argv[1],"read")) { + char name[256];short out=0; + if(argc<3)return 2; + strcpy(saves[0],argv[2]); + if(argc>3)difficulty=(short)atoi(argv[3]); + result=mcc_saved_game_read(name,&out); + if(!result && name[0])return 13; + printf("difficulty=%d\n",out); + } else return 2; + printf("result=%d\nstarts=%d\nstarted=%s\nrequired=%d\nstock_calls=%d\ncount=%d\n", + result,starts,started,required,stock_calls,campaign.saved_game_count); + return 0; +} +''' + path = work / "saved_games.c" + path.write_text(source) + output = work / ("saved_games.exe" if sys.platform == "win32" else "saved_games") + command = [compiler, "-std=c99", "-Wall", "-Wextra", "-Werror", str(path), "-o", str(output)] + if sys.platform == "win32": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return output + + +def run(tool, *args): + result = subprocess.run([str(tool), *map(str, args)], capture_output=True, text=True) + assert result.returncode == 0, result.stdout + result.stderr + return dict(line.split("=", 1) for line in result.stdout.splitlines()) + + +@pytest.mark.parametrize("name,starts,required,stock_calls", [ + ("mcc_maps\\a10", 1, 1, 0), + ("levels\\a10\\a10", 1, 0, 1), + ("custom_maps\\a10", 0, 0, 1), +]) +def test_continue_routes_only_mcc_to_new_loader(saved_game_tool, name, starts, required, stock_calls): + fields = run(saved_game_tool, "continue", name) + assert fields["starts"] == str(starts) + assert fields["required"] == str(required) + assert fields["stock_calls"] == str(stock_calls) + if starts: + assert fields["started"] == name + + +def test_missing_mcc_checkpoint_map_does_not_start(saved_game_tool): + fields = run(saved_game_tool, "continue", "mcc_maps\\missing", 0) + assert fields["result"] == fields["starts"] == "0" + + +def test_save_list_keeps_stock_and_mcc_identities_separate(saved_game_tool): + fields = run(saved_game_tool, "list") + assert fields["count"] == "2" + + +@pytest.mark.parametrize("row,expected,required", [(0, "levels\\a10\\a10", 0), (1, "mcc_maps\\a10", 1)]) +def test_saved_game_selection_uses_its_own_namespace(saved_game_tool, row, expected, required): + fields = run(saved_game_tool, "list", row) + assert fields["result"] == "1" + assert fields["started"] == expected + assert fields["required"] == str(required) + + +@pytest.mark.parametrize("difficulty,expected", [(-8, 0), (8, 3)]) +def test_mcc_saved_difficulty_is_bounded(saved_game_tool, difficulty, expected): + fields = run(saved_game_tool, "read", "mcc_maps\\a10", difficulty) + assert fields["result"] == "1" + assert fields["difficulty"] == str(expected) diff --git a/tools/test_mcc_scripts.py b/tools/test_mcc_scripts.py new file mode 100644 index 0000000000..059a8fd993 --- /dev/null +++ b/tools/test_mcc_scripts.py @@ -0,0 +1,60 @@ +"""Check independent MCC script relocation and native extension behavior.""" +from pathlib import Path +import shutil +import subprocess +import sys + +import pytest + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="module") +def native_mcc_harness(tmp_path_factory): + compiler = shutil.which("clang") + if not compiler or sys.platform != "win32": + pytest.skip("the script harness currently uses native x86 Windows game headers") + directory = tmp_path_factory.mktemp("mcc-script-behavior") + includes = ["port/windows/include/crt", "port/windows/include", "source", "source/cseries", + "source/math", "source/tag_files", "source/rasterizer", "source/structures", + "source/cache", "source/render", "port/include/xdk", "port/linux/game"] + command = [compiler, "--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-fms-extensions", + "-std=gnu89", "-O1", "-DDEBUG", "-Dxbox", "-D_CRT_SECURE_NO_WARNINGS", + "-Wno-nonportable-include-path", "-Wno-c99-compat", "-Wno-visibility", + "-include", str(ROOT / "port/windows/include/halo_windows_prefix.h"), + "-iquote", str(ROOT / "port/linux/include")] + command += ["-I" + str(ROOT / path) for path in includes] + def build(name): + binary = directory / (name + ".exe") + extra = [str(ROOT / "port/linux/game/mcc_syntax.c")] if name == "mcc_scripts" else [] + result = subprocess.run(command + [str(ROOT / "tools/harness/tests" / (name + ".c")), + str(ROOT / "port/linux/game" / (name + ".c"))] + extra + ["-o", str(binary)], + capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return binary + return build + + +@pytest.fixture(scope="module") +def scripts_tool(native_mcc_harness): + return native_mcc_harness("mcc_scripts") + + +@pytest.mark.parametrize("case", ["link", "alias", "extension", "segment", "global", "unknown", + "bad_child", "child_salt", "short_data", "capacity", "full_capacity", + "negative_count", "unterminated"]) +def test_mcc_script_linking(scripts_tool, case): + result = subprocess.run([str(scripts_tool), case], capture_output=True, text=True, timeout=10) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_mcc_vehicle_masks_and_legacy_isolation(native_mcc_harness): + result = subprocess.run([str(native_mcc_harness("mcc_objects"))], capture_output=True, + text=True, timeout=10) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_mcc_player_input_and_profile_slots(native_mcc_harness): + result = subprocess.run([str(native_mcc_harness("mcc_player"))], capture_output=True, + text=True, timeout=10) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/tools/test_mcc_shader_channels.py b/tools/test_mcc_shader_channels.py new file mode 100644 index 0000000000..8fef2deb26 --- /dev/null +++ b/tools/test_mcc_shader_channels.py @@ -0,0 +1,156 @@ +"""MCC model and meter flags select channels per consumer of shared bitmaps.""" +from pathlib import Path +import shutil +import subprocess +import sys + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from harness import function, structure + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="module") +def model_masks(tmp_path_factory): + clang = shutil.which("clang") + if not clang: + pytest.skip("clang is needed for MCC shader channel tests") + bitmap = (ROOT / "port/linux/game/mcc_bitmaps.c").read_text() + header = (ROOT / "source/bitmaps/bitmap_group.h").read_text() + declarations = "\n".join(structure(header, name) for name in [ + "bitmap_data", "bitmap_group_sprite", "bitmap_group_sequence", "bitmap_group"]) + declarations += "\n" + "\n".join(structure(bitmap, name) for name in [ + "mcc_texture", "mcc_bitmap_clone", "mcc_textures"]) + routines = "\n".join(function(bitmap, name) for name in [ + "mcc_bitmap_tag", "mcc_bitmap_block", "mcc_bitmap_mark_one", "mcc_bitmap_clone_block", + "mcc_bitmap_specialize", "mcc_bitmap_mark", "mcc_meter_usage", "mcc_bitmap_model_usage", + "mcc_bitmap_bytes", "mcc_pixel", "mcc_channels", "mcc_bitmap_decode"]) + source = r''' +#include +#include +#include +#include +typedef float real; +union point2d {struct {short x,y;};uint32_t n;}; +typedef struct {float x0,x1,y0,y1;} real_rectangle2d; +typedef struct {float x,y;} real_point2d; +struct tag_block {long count;void *address,*definition;}; +struct tag_data {long size,flags,file_offset;void *address,*definition;}; +struct meter_hud_element_definition {struct {uint32_t index;} meter_bitmap;short sequence_index;unsigned char meter_flags;}; +#define BITMAP_GROUP_TAG 'bitm' +#define NONE -1 +static uint32_t global_vector_palette[256]; +struct mcc_runtime {unsigned char *tags,*tag_index;uint32_t used;struct {uint32_t tag_count;} report;}; +static void *mcc_runtime_pointer(struct mcc_runtime *r,uint32_t p,uint32_t size) { + uintptr_t start=(uintptr_t)r->tags; + return p>=start && p-start<=r->used && size<=r->used-(p-start) ? (void *)(uintptr_t)p : NULL; +} +static void *mcc_runtime_allocate(struct mcc_runtime *r,uint32_t size) { + uint32_t at=(r->used+15)&~15u; + if(size>65536-at)return NULL; + r->used=at+size;return r->tags+at; +} +#define BCDEC_STATIC +#define BCDEC_IMPLEMENTATION +#include "bcdec.h" +''' + declarations + "\n" + routines + r''' +static void shader_init(unsigned char *shader,uint16_t flags,uint32_t handle) { + static unsigned const offsets[]={0xB0,0xC8,0xE8,0x170};unsigned i; + memset(shader,0,0x174);memcpy(shader+0x28,&flags,sizeof(flags)); + for(i=0;i<4;i++)memcpy(shader+offsets[i],&handle,sizeof(handle)); +} +int main(int argc,char **argv) { + struct mcc_runtime runtime={0};struct mcc_textures textures={0}; + struct bitmap_group *original;struct bitmap_data *descriptor,original_descriptor; + unsigned char xbox[0x174],gearbox[0x174],repeated[0x174],missing[0x174]; + uint32_t *entry,source=0xE1740000,cloned,decoded[16];unsigned i; + /* A red-only opaque BC1 block: specular reflection, with no color + * change. Misreading alpha as change color instead tints all texels. */ + unsigned char dxt1[8]={0,0xF8,0,0,0,0,0,0}; + unsigned char rgba[4]={10,20,30,40}; + struct meter_hud_element_definition xbox_meter={{source},NONE,0x3F},gearbox_meter={{source},NONE,0x1F}; + int reverse; + if(argc!=2)return 1;reverse=atoi(argv[1]); + runtime.tags=calloc(1,65536);runtime.tag_index=runtime.tags;runtime.used=512; + runtime.report.tag_count=1; + original=mcc_runtime_allocate(&runtime,sizeof(*original)); + descriptor=mcc_runtime_allocate(&runtime,sizeof(*descriptor)); + original->bitmaps.count=1;original->bitmaps.address=descriptor; + descriptor->width=descriptor->height=4;descriptor->depth=1; + descriptor->format=14;descriptor->pixels_offset=2048;descriptor->pixels_size=8; + original_descriptor=*descriptor; + entry=(uint32_t *)runtime.tag_index;entry[0]='bitm';entry[3]=source;entry[5]=(uint32_t)original; + textures.capacity=8;textures.tag_capacity=8;textures.count=1; + textures.items=calloc(8,sizeof(*textures.items));textures.clones=calloc(8,sizeof(*textures.clones)); + textures.items[0].bitmap=descriptor;textures.items[0].handle=source; + shader_init(xbox,0x7F,source);shader_init(gearbox,0x3F,source);shader_init(repeated,0,source); + if(!mcc_bitmap_model_usage(&runtime,&textures,reverse ? gearbox : xbox) || + !mcc_bitmap_model_usage(&runtime,&textures,reverse ? xbox : gearbox))return 2; + cloned=*(uint32_t *)(gearbox+0xC8); + if(cloned==source || runtime.report.tag_count!=2 || textures.count!=2)return 3; + if(*(uint32_t *)(xbox+0xC8)!=source || textures.items[0].channels!=1 || textures.items[1].channels!=2)return 4; + if(memcmp(&original_descriptor,descriptor,sizeof(*descriptor)) || + textures.items[1].bitmap==descriptor || memcmp(textures.items[1].bitmap,descriptor,sizeof(*descriptor)))return 5; + if(*(uint16_t *)(xbox+0x28)!=0x7F || *(uint16_t *)(gearbox+0x28)!=0x3F)return 6; + if(!mcc_bitmap_model_usage(&runtime,&textures,repeated) || *(uint32_t *)(repeated+0xC8)!=cloned || + runtime.report.tag_count!=2 || textures.count!=2)return 7; + /* Base, detail and cube textures remain ordinary in both shaders. */ + if(*(uint32_t *)(gearbox+0xB0)!=source || *(uint32_t *)(gearbox+0xE8)!=source || + *(uint32_t *)(gearbox+0x170)!=source)return 8; + mcc_bitmap_decode(dxt1,decoded,4,4,14,textures.items[0].channels); + for(i=0;i<16;i++)if(decoded[i]!=0xFFFF0000u)return 9; + mcc_bitmap_decode(rgba,decoded,1,1,11,textures.items[1].channels); + if(decoded[0]!=0x1E0A1428u)return 10; + shader_init(missing,0x40,0xFFFFFFFFu); + if(!mcc_bitmap_model_usage(&runtime,&textures,missing) || runtime.report.tag_count!=2)return 11; + shader_init(missing,0,0xFFFFFFFFu); + if(!mcc_bitmap_model_usage(&runtime,&textures,missing) || runtime.report.tag_count!=2)return 12; + /* Neither channel path can silently accept a stale/non-bitmap handle. */ + shader_init(missing,0x40,source+3); + if(mcc_bitmap_model_usage(&runtime,&textures,missing))return 13; + shader_init(missing,0,source^0x10000u); + if(mcc_bitmap_model_usage(&runtime,&textures,missing))return 14; + /* A bitmap can simultaneously serve two meter conventions and a + * material mask without any of these consumers overwriting another. */ + if(!mcc_meter_usage(&runtime,&textures,reverse ? &gearbox_meter : &xbox_meter) || + !mcc_meter_usage(&runtime,&textures,reverse ? &xbox_meter : &gearbox_meter))return 15; + if(xbox_meter.meter_bitmap.index!=source || gearbox_meter.meter_bitmap.index==source || + gearbox_meter.meter_bitmap.index==cloned || textures.count!=3 || runtime.report.tag_count!=3 || + textures.items[0].channels!=1 || textures.items[1].channels!=2 || textures.items[2].channels!=4)return 16; + if(xbox_meter.meter_flags!=0x3F || gearbox_meter.meter_flags!=0x1F || + memcmp(&original_descriptor,descriptor,sizeof(*descriptor)))return 17; + /* Transparent outside-shape pixel has nonzero fill order. Preserve + * alpha zero in Xbox order, while an unflagged meter must swap it. */ + rgba[0]=rgba[1]=rgba[2]=128;rgba[3]=0; + mcc_bitmap_decode(rgba,decoded,1,1,11,textures.items[0].channels); + if(decoded[0]!=0x00808080u)return 18; + mcc_bitmap_decode(rgba,decoded,1,1,11,textures.items[2].channels); + if(decoded[0]!=0x80000000u)return 19; + xbox_meter.meter_bitmap.index=gearbox_meter.meter_bitmap.index=0xFFFFFFFFu; + if(!mcc_meter_usage(&runtime,&textures,&xbox_meter) || !mcc_meter_usage(&runtime,&textures,&gearbox_meter) || + runtime.report.tag_count!=3)return 20; + free(textures.clones);free(textures.items);free(runtime.tags);return 0; +} +''' + work = tmp_path_factory.mktemp("mcc-model-masks") + path = work / "masks.c" + path.write_text(source) + output = work / ("masks.exe" if sys.platform == "win32" else "masks") + command = [clang, "-std=gnu99", "-O2", "-Wall", "-Wextra", "-Werror", + "-Wno-multichar", "-Wno-unused-function", "-I", str(ROOT / "port/third_party/bcdec"), + str(path), "-o", str(output)] + if sys.platform == "win32": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + else: + command[1:1] = ["-m32", "-fsanitize=undefined", "-fsanitize-trap=undefined"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return output + + +@pytest.mark.parametrize("reversed_consumers", [0, 1]) +def test_mcc_model_channel_order_is_per_shader(model_masks, reversed_consumers): + result = subprocess.run([str(model_masks), str(reversed_consumers)], capture_output=True, text=True, timeout=10) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/tools/test_mcc_syntax.py b/tools/test_mcc_syntax.py new file mode 100644 index 0000000000..af92aa575f --- /dev/null +++ b/tools/test_mcc_syntax.py @@ -0,0 +1,23 @@ +"""Exercise MCC capacity, graph validation and isolated script safety traversal.""" +import subprocess + +import pytest + +from test_mcc_scripts import native_mcc_harness + + +@pytest.fixture(scope="module") +def syntax_tool(native_mcc_harness): + return native_mcc_harness("mcc_syntax") + + +@pytest.mark.parametrize("case", [ + "empty", "high_slot", "sparse", "deep", "wide", "deep_cycle", "salt", "zero_salt", + "out_of_range", "unused", "sibling_cycle", "call_cycle", "short_blob", "long_blob", + "negative_count", "negative_capacity", "count_over_capacity", "actual_count", + "negative_actual", "free_hint", "signature", "node_size", "invalid_array", "callbacks", + "root_type", "damaged_root", "shared_child", +]) +def test_mcc_syntax_behavior(syntax_tool, case): + result = subprocess.run([str(syntax_tool), case], capture_output=True, text=True, timeout=10) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/tools/test_mcc_tag_validation.py b/tools/test_mcc_tag_validation.py new file mode 100644 index 0000000000..b9fd038c39 --- /dev/null +++ b/tools/test_mcc_tag_validation.py @@ -0,0 +1,88 @@ +"""Exercise MCC traversal and the real shared callback dispatch without assets.""" +import os +from pathlib import Path +import shutil +import subprocess +import sys + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +GAME = ROOT / "port/linux/game" + + +@pytest.fixture(scope="module") +def validation_tool(tmp_path_factory): + compiler = shutil.which("clang") + if compiler is None: + pytest.skip("clang is required for native MCC validation tests") + directory = tmp_path_factory.mktemp("mcc-validation") + (directory / "cache").mkdir() + (directory / "tag_files").mkdir() + (directory / "cseries.h").write_text(""" +#ifndef TEST_CSERIES_H +#define TEST_CSERIES_H +#include +typedef unsigned char byte; +typedef unsigned char boolean; +typedef float real; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +#define UNSIGNED_SHORT_MAX 65535 +#define FLAG(n) (1UL << (n)) +#define TEST_FLAG(v,n) (((v) & FLAG(n)) != 0) +#define MIN(a,b) ((a) < (b) ? (a) : (b)) +#endif +""") + (directory / "cache/physical_memory_map.h").write_text("#define TAG_CACHE_SIZE 0x01600000\n") + (directory / "tag_files/tag_groups.h").write_text(""" +#ifndef TEST_TAG_GROUPS_H +#define TEST_TAG_GROUPS_H +#include "cseries.h" +struct tag_block { long count; void *address; void *definition; }; +struct tag_data { long size; unsigned long pad; long file_offset; void *address; void *definition; }; +struct tag_reference { unsigned long group_tag; char *name; long name_length; long index; }; +#endif +""") + binary = directory / ("validation.exe" if os.name == "nt" else "validation") + target = (["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + if sys.platform == "win32" else ["-m32"]) + command = [compiler, *target, "-std=gnu99", "-O1", "-g", "-Wall", "-Wextra", "-Werror", + "-Wno-multichar", "-fsanitize=undefined", "-fsanitize-trap=undefined", + f"-I{directory}", f"-I{GAME}", str(GAME / "mcc_tag_validate.c"), + str(GAME / "tag_validate.c"), str(ROOT / "tools/harness/tests/mcc_tag_validation.c"), + "-o", str(binary)] + built = subprocess.run(command, capture_output=True, text=True) + assert built.returncode == 0, built.stdout + built.stderr + # The existing map_validate executable links tag_validate.c without any + # game runtime adapters. MCC callback routing must retain that property. + legacy = directory / "legacy.c" + legacy.write_text(''' +#include "cseries.h" +#include "tag_schema.h" +struct tag_schema_group const *const tag_schema_group_lists[] = {NULL}; +struct tag_schema_group const tag_schema_custom_edition_groups[] = {{0, {0, 0}, NULL}}; +void tag_validate_report(char const *message) { (void)message; } +int main(void) { return tag_validate_corrections() != 0; } +''') + legacy_binary = directory / ("legacy.exe" if os.name == "nt" else "legacy") + standalone = command[:command.index(str(GAME / "mcc_tag_validate.c"))] + [ + str(GAME / "tag_validate.c"), str(legacy), "-o", str(legacy_binary)] + linked = subprocess.run(standalone, capture_output=True, text=True) + assert linked.returncode == 0, linked.stdout + linked.stderr + assert subprocess.run([str(legacy_binary)], timeout=30).returncode == 0 + return binary + + +@pytest.mark.parametrize("case", ["valid", "overlap", "outside", "negative", "cycle", + "file_range", "handle", "unknown_group", "bsp", + "legacy_isolation", "callbacks", "misaligned", "stream_hole", + "collection", "collection_outside", "device", "device_outside", + "trimmed_structure", "graph_large", "graph_cycle", "graph_node_cycle", + "graph_orphan", "bitmap_owned", "bitmap_unowned", "first_person_slots", + "grenades_zero", "grenades_one", "grenades_two", "grenades_four", "grenades_excess", + "syntax_full", "syntax_overflow", "syntax_outside", "syntax_overlap", "syntax_legacy"]) +def test_mcc_validation(validation_tool, case): + result = subprocess.run([str(validation_tool), case], capture_output=True, text=True, timeout=30) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/tools/test_mcc_texture_bridge.py b/tools/test_mcc_texture_bridge.py new file mode 100644 index 0000000000..8631cde937 --- /dev/null +++ b/tools/test_mcc_texture_bridge.py @@ -0,0 +1,221 @@ +"""MCC texture lifetime/pressure tests with real descriptors and a fake GPU. + +Compile the actual private bridge and native descriptor/size routines. GPU +stubs record ownership and failures; no legacy staging allocator is linked. +""" +from pathlib import Path +import re +import shutil +import subprocess +import sys + +import pytest + +from harness import function, structure + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="module") +def bridge_tool(tmp_path_factory): + clang = shutil.which("clang") + if not clang: + pytest.skip("clang is needed for the MCC texture bridge tests") + work = tmp_path_factory.mktemp("mcc-texture-bridge") + native = (ROOT / "port/linux/src/xbox_textures.c").read_text() + xgpu = (ROOT / "port/linux/src/xgpu.h").read_text() + xdk = (ROOT / "port/include/xdk/xdk_d3d8.h").read_text() + pdb = (ROOT / "port/include/xdk/xdk_pdb.h").read_text() + macros = "\n".join(line for line in xdk.splitlines() if re.match( + r"#define (D3DFORMAT_|D3DSIZE_|D3DCOMMON_TYPE_TEXTURE|D3DTEXTURE_(?:PITCH|CUBEFACE)_ALIGNMENT)", line)) + formats = "\n".join("#define " + name + " " + value for name, value in re.findall( + r"\b(D3DFMT_\w+)\s*=\s*(0x[0-9a-fA-F]+|[0-9]+)\b", pdb)) + header = r''' +#ifndef TEST_XGPU_H +#define TEST_XGPU_H +#include +#include +#include +typedef unsigned long DWORD,D3DCOLOR; +typedef unsigned GLuint,GLenum; +typedef int BOOL,boolean; +#define TRUE 1 +#define FALSE 0 +#define GL_TEXTURE_2D 0x0DE1 +#define GL_TEXTURE_3D 0x806F +#define GL_TEXTURE_CUBE_MAP 0x8513 +''' + macros + '\n' + formats + '\n' + structure(xgpu, "xgpu_texture_description") + r''' +void xgpu_texture_describe(DWORD,DWORD,struct xgpu_texture_description *); +unsigned long xgpu_texture_face_size(struct xgpu_texture_description const *); +void glGenTextures(int,GLuint *); +void glDeleteTextures(int,GLuint const *); +void xgpu_gl_state_invalidate(void); +void xgpu_mcc_texture_unbind(DWORD const *); +BOOL xgpu_mcc_texture_upload(GLuint,GLenum,struct xgpu_texture_description const *,void const *,unsigned long,D3DCOLOR const *); +#endif +''' + (work / "xgpu.h").write_text(header) + helpers = re.search(r"enum texel_kind\s*\{.*?\};", native, re.S).group() + helpers += '\n' + structure(native, "format_information") + helpers += '\n' + '\n'.join(function(native, name) for name in ["format_information", + "kind_compressed", "floor_log2", "level_dimension", "xgpu_texture_describe", "level_bytes", + "xgpu_texture_level_offset", "xgpu_texture_face_size"]) + bridge = (ROOT / "port/linux/src/mcc_texture_bridge.c").read_text() + game = (ROOT / "port/linux/game/mcc_texture_cache.c").read_text() + source = r''' +#include "xgpu.h" +#include +static unsigned allocations,live_allocations,largest_allocation,fail_allocate; +static unsigned generated,deleted,uploads,unbound,invalidated,fail_upload,fail_generate; +static void const *last_pixels; +static unsigned long last_bytes; +static void *tracked_calloc(size_t count,size_t bytes) { + void *p; + if(fail_allocate)return NULL; + if(count*bytes>largest_allocation)largest_allocation=(unsigned)(count*bytes); + p=calloc(count,bytes);if(p){allocations++;live_allocations++;}return p; +} +static void tracked_free(void *p) {if(p)live_allocations--;free(p);} +void glGenTextures(int count,GLuint *p) {(void)count;*p=fail_generate?0:++generated;} +void glDeleteTextures(int count,GLuint const *p) {(void)count;if(!*p)exit(80);deleted++;} +void xgpu_gl_state_invalidate(void) {invalidated++;} +void xgpu_mcc_texture_unbind(DWORD const *p) {if(!p)exit(81);unbound++;} +BOOL xgpu_mcc_texture_upload(GLuint texture,GLenum target,struct xgpu_texture_description const *d, + void const *pixels,unsigned long bytes,D3DCOLOR const *palette) { + (void)texture;(void)target;(void)d;(void)palette; + uploads++;last_pixels=pixels;last_bytes=bytes;return !fail_upload; +} +''' + helpers + r''' +#define calloc tracked_calloc +#define free tracked_free +''' + bridge + r''' +#undef calloc +#undef free +struct bitmap_data {short width,height,depth,type,format,mipmap_count;unsigned short flags;void *base_address;}; +static int active=1; +static void const *owned; +static unsigned long owned_bytes; +static int mcc_cache_tags_loaded(void) {return active;} +static void const *mcc_cache_bitmap_pixels(struct bitmap_data const *b,unsigned long *bytes) { + (void)b;*bytes=owned_bytes;return owned; +} +static unsigned rasterizer_xbox_bitmap_get_max_mipmap_count(struct bitmap_data *b) {return b->mipmap_count;} +static unsigned bitmap_mipmap_get_row_pitch(struct bitmap_data *b,int level) {(void)level;return b->width*4;} +''' + function(game, "mcc_texture_cache_get") + r''' +static DWORD format(unsigned logw,unsigned logh,unsigned logd,unsigned levels,unsigned type) { + return (D3DFMT_A8R8G8B8<2048 || allocations!=count || live_allocations!=count)return 20; + for(frame=0;frame<3;frame++)for(i=0;i +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define csstrcasecmp _stricmp +#else +#include +#define csstrcasecmp strcasecmp +#endif +typedef unsigned char boolean; +typedef unsigned short word; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +#define NUMBEROF(a) (sizeof(a)/sizeof(*(a))) +#define csmemcpy memcpy +#define MAXIMUM_NUMBER_OF_LOCAL_PLAYERS 4 +#define PC_MENU_FUNCTION_BASE 256 +#define TEST_FLAG(value,bit) ((value)&(1u<<(bit))) +#define _error_silent 0 +#define match_assert(file,line,condition) assert(condition) +#define match_vassert(file,line,condition,...) assert(condition) +enum {_game_connection_local,_game_connection_network_client,_game_connection_network_server}; +enum {_event_handler_close_current_widget_bit,_event_handler_close_other_widget_bit, +_event_handler_close_all_widgets_bit,_event_handler_open_widget_bit,_event_handler_reload_self_bit, +_event_handler_reload_widget_bit,_event_handler_give_focus_to_widget_bit,_event_handler_run_function_bit, +_event_handler_replace_with_other_widget_bit,_event_handler_go_back_to_previous_widget_bit, +_event_handler_run_scenario_script_bit,_event_handler_look_for_conditional_widget_on_failure_bit}; +enum {_conditional_widget_load_if_event_handler_function_fails_bit}; +enum {_ui_audio_feedback_none,_ui_audio_feedback_cursor,_ui_audio_feedback_forward,_ui_audio_feedback_back}; +struct widget_instance {long definition_tag_index;char const *name;short local_player_index; +short horizontal_offset,vertical_offset;struct widget_instance *parent,*next,*previous,*child,*focused_child;}; +struct event_record {short type,controller_index;}; +struct tag_reference {long index;}; +struct tag_block {long count;void *address;}; +struct ui_widget_definition {struct tag_block conditional_widgets;}; +struct ui_widget_event_handler_reference {uint32_t flags;short event_type,function; +struct tag_reference widget_tag,sound_effect;char script[32];}; +struct ui_widget_conditional_reference {struct tag_reference widget_tag;long flags;}; +struct widget_stack_data {int unused;}; +static struct {struct widget_instance *active_widgets[4];void *widget_stack[4];} widget_globals; +struct network_player {int valid;short machine_index,controller_index;char team_index;}; +struct network_game {struct {struct {boolean teams;} universal_variant;} variant;struct network_player players[4];}; +struct scenario {short type;}; +static struct scenario scenario; +static struct scenario *global_scenario = &scenario; +static struct network_game game; +static struct network_player sent_player; +static int mcc=1,connection,coop,saved=1,profile=1,have_settings=1,profile_begin,settings_open; +static int campaign=1,pc_menus=1,pause_owned=1; +static int settings_load=1,settings_controller=-1,last_profile=1004,available_profile=1005,assigned_controller=-1; +static long settings_history_tag; +static short mcc_settings_controller=NONE; +struct player_profile {int value;}; +struct game_variant {int value;}; +static struct { + long edit_profile_index; + struct {long active_profile_index;} local_players[4]; + struct {union {struct player_profile player;struct game_variant variant;} original,current;} edit_profile; +} player_ui_globals; +static struct {int original,current;} player_ui_edit_options; +enum {_saved_game_file_type_player_profile=1,_saved_game_file_type_game_variant=2}; +static struct widget_instance settings_widgets[4]; +static int revert_count,reset_count,restart_count,save_count,persist_count,menu_count,quit_count,end_count,settings_collision; +static int posted_button=-1,posted_controller=-1,deleted_count,opened_count,legacy_count,port_count; +static int message_count,script_count,team_sent,denied_count,top_dispatch,quit_controller=-1,balance_allowed=1; +static int local_players=1,left_count,left_controller=-1,stack_disposed; +static boolean quit_closes_widgets; +static boolean free_widget_on_delete,round_active=TRUE; +static struct widget_instance *released_widget; +static long opened_tag; +static wchar_t const *message; +static char const *widget_tag_name="ui\\shell\\multiplayer_game\\pause_game\\blue_team_button"; +static char owned[0x60],unowned[0x60]; +static boolean mcc_cache_tags_loaded(void){return mcc;} +static boolean mcc_level_name(char const *name){return name&&!strncmp(name,"mcc_maps\\",9);} +static boolean mcc_pause_owns(long tag){return pause_owned&&tag==300;} +static void *tag_get(long group,long index){(void)group;return index==100?owned:unowned;} +static char *tag_get_name(long index){(void)index;return (char *)widget_tag_name;} +static boolean mcc_cache_contains(void const *p,long size){return p==owned && size<=sizeof(owned);} +static char const *config_string(char const *name){(void)name;return pc_menus==2?"PC":pc_menus?"pc":"xbox";} +static short game_connection(void){return connection;} +static void display_error_text_deferred(wchar_t const *text,short controller){assert(controller>=0&&controller<4);message_count++;message=text;} +static void *global_network_game_server_get(void){return connection==_game_connection_network_server?&game:NULL;} +static void *global_network_game_client_get(void){return connection?&game:NULL;} +static boolean mcc_ui_restart_network_game(void){restart_count++;return TRUE;} +static void main_reset_map(void){reset_count++;} +static void main_revert_map(void){revert_count++;} +static void main_goto_main_menu(void){menu_count++;} +static boolean game_engine_running(void){return TRUE;} +static boolean game_engine_can_score(void){return round_active;} +static void ui_widget_delete(struct widget_instance *widget); +static void ui_widgets_close_all(void); +static void game_engine_end_game(void){end_count++;ui_widgets_close_all();} +static boolean network_coop_active(void){return coop;} +static boolean mcc_maps_level_campaign(char const *name){(void)name;return campaign;} +static char *main_get_map_name(void){return "mcc_maps\\a10";} +static boolean game_state_port_saved_game_valid(void){return saved;} +static void game_state_save_to_persistent_storage(void){persist_count++;} +static void game_state_save(void){save_count++;saved=TRUE;} +static void ui_widgets_close_all_for_local_player(short controller); +static void network_game_client_local_player_quit(short controller){quit_count++;quit_controller=controller;if(quit_closes_widgets){ui_widgets_close_all_for_local_player(controller);local_players--;}} +static short local_player_count(void){return (short)local_players;} +static void player_ui_local_player_left_multiplayer_game(short controller){left_count++;left_controller=controller;} +static struct network_game *network_game_get_game(void){return connection?&game:NULL;} +static short network_game_client_get_local_machine_index(void){return 2;} +static boolean network_player_is_valid(struct network_player const *player){return player->valid;} +static boolean network_game_client_update_local_player_data(void *client,struct network_player *player){assert(client);sent_player=*player;team_sent++;return TRUE;} +static boolean mcc_ui_team_balance_allows(short machine,short controller,short team){(void)machine;(void)controller;(void)team;return balance_allowed;} +static void event_manager_post_button(short controller,short button){posted_button=button;posted_controller=controller;} +static long tag_loaded(long group,char const *name){(void)group;(void)name;return have_settings?(settings_collision?100:200):NONE;} +static int saved_game_file_get_type(long index){return index>=1000&&index<=1005?1:index==2000?2:0;} +static boolean player_profile_get(long index,struct player_profile *out){profile_begin++;if(!profile)return FALSE;out->value=(int)index;return TRUE;} +static boolean playlist_profile_get(long index,struct game_variant *out){out->value=(int)index;return TRUE;} +static void playlist_profile_get_options(long index,int *out){*out=(int)index;} +static long player_ui_get_player1_last_used_profile_index(void){return last_profile;} +static void player_profiles_enumerate_available_to_local_player_index(short controller,word *count,long *index,boolean defaults){assert(controller==0&&!defaults&&*count==1);*count=available_profile==NONE?0:1;*index=available_profile;} +static void player_ui_set_active_player_profile(short controller,long index,struct player_profile *p){assert(p->value==index);assigned_controller=controller;player_ui_globals.local_players[controller].active_profile_index=index;} +void mcc_ui_settings_profile_released(void); +void mcc_ui_settings_close(short controller); +static void player_ui_end_editing_profile(void); +static struct widget_instance *widget_instance_get_topmost_parent(struct widget_instance *widget){assert(widget!=released_widget);while(widget->parent)widget=widget->parent;return widget;} +static boolean ui_widget_port_dispatch_event(struct widget_instance *widget,short type,short controller,boolean *deleted){(void)widget;(void)deleted;assert(type==32&&controller==2);top_dispatch++;return TRUE;} +static void error(int priority,char const *text,...){(void)priority;(void)text;} +static void console_warning(char const *text,...){(void)text;} +static boolean pc_menu_tag(long index){return index==200;} +static boolean main_menu_is_active(void){return FALSE;} +static boolean pc_menu_event_function_invoke(struct widget_instance *widget,struct event_record *event,long function,boolean *deleted){(void)widget;(void)event;(void)function;(void)deleted;port_count++;return TRUE;} +static boolean legacy_function(struct widget_instance *widget,struct event_record *event,boolean *deleted){(void)widget;(void)event;(void)deleted;legacy_count++;return TRUE;} +static struct {boolean (*functions[102])(struct widget_instance *,struct event_record *,boolean *);char const *names[102];} event_handler_function_list; +static boolean hs_evaluate_by_name(char const *name){assert(!strcmp(name,"my_action"));script_count++;return TRUE;} +static void widget_instance_give_focus_by_tag(struct widget_instance *widget,long tag,short controller){(void)widget;(void)tag;(void)controller;} +static void widget_instance_reload_recursive(struct widget_instance *widget){(void)widget;} +static void ui_widget_reload_by_tag(long tag){(void)tag;} +static struct widget_instance *widget_instance_find_by_tag_index(long tag){(void)tag;return NULL;} +static void ui_widget_delete(struct widget_instance *widget){int i;assert(widget!=released_widget);deleted_count++;for(i=0;i<4;i++)if(widget_globals.active_widgets[i]==widget)widget_globals.active_widgets[i]=NULL;if(free_widget_on_delete){released_widget=widget;memset(widget,0xDD,sizeof(*widget));free(widget);}} +static boolean ui_widget_launch_widget(struct widget_instance *widget,long tag){assert(widget!=released_widget);opened_count++;opened_tag=tag;return TRUE;} +static struct widget_instance *ui_widget_load_by_name_or_tag(char const *name,long tag,struct widget_instance *widget,short controller,long a,long b,long c){ + (void)tag;(void)widget;(void)b;(void)c; + if(!name||!strstr(name,"pc\\main_menu\\settings_select"))return NULL; + assert(controller>=0&&controller<4); + if(!settings_load)return NULL; + if(widget_globals.active_widgets[controller])ui_widget_delete(widget_globals.active_widgets[controller]); + settings_widgets[controller]=(struct widget_instance){200,"native settings",controller}; + widget_globals.active_widgets[controller]=&settings_widgets[controller]; + settings_controller=controller;settings_history_tag=a;settings_open++;return &settings_widgets[controller]; +} +static void widget_instance_go_back_to_previous(struct widget_instance *widget){assert(widget!=released_widget);} +static void unspatialized_impulse_sound_new(long tag,float volume){(void)tag;(void)volume;} +static void pop_widget(void **stack,struct widget_stack_data *data){(void)data;*stack=NULL;} +static void dispose_widget_stack(void **stack){assert(*stack);stack_disposed++;*stack=NULL;} +static boolean virtual_keyboard_active(void){return FALSE;} +static void virtual_keyboard_close(void){} +static void ui_play_audio_feedback_sound(long sound){(void)sound;} +''' + actions + '\n' + '\n'.join(function(profiles, name) for name in [ + "player_ui_get_active_player_profile_index", "player_ui_get_edit_player_profile", "player_ui_get_edit_playlist_profile", + "player_ui_begin_editing_profile", "clear_profile_edit_data", "player_ui_end_editing_profile" + ]) + '\n' + function(native, "ui_widgets_close_all_for_local_player") + '\n' + \ + function(native, "ui_widgets_close_all") + '\n' + \ + structure(ui, "mcc_ui_widget_prefix") + '\n' + '\n'.join(function(ui, name) for name in names) + '\n' + \ + '\n'.join(function(events, name) for name in ["network_game_remove_local_player", "ui_widget_function_denied", + "ui_widget_event_handler_function_invoke"]) + '\n' + \ + function(native, "event_handler_dispatch") + r''' +struct mcc_runtime {unsigned char *tag_index;uint32_t used;struct {uint32_t tag_count;}report;}; +static unsigned char metadata[0x400]; +static void *mcc_runtime_pointer(struct mcc_runtime *r,uint32_t address,uint32_t bytes){(void)r;return address<=sizeof(metadata)&&bytes<=sizeof(metadata)-address?metadata+address:NULL;} +static int mcc_hud(struct mcc_runtime *r,uint32_t group,unsigned char *p){(void)r;(void)group;(void)p;return TRUE;} +''' + '\n'.join(function(tags, name) for name in ["mcc_word", "mcc_block", "mcc_tags_prepare"]) + r''' +#define CHECK(c,n) do{if(!(c)){fprintf(stderr,"check %d failed\n",n);return n;}}while(0) +static boolean fire(struct widget_instance *widget,short button,struct ui_widget_event_handler_reference *handler){ + boolean deleted=FALSE;struct event_record event={1,widget->local_player_index};struct ui_widget_definition definition={0}; + if(button==handler->event_type)event_handler_dispatch(widget,&definition,&event,handler,&deleted); + return deleted; +} +int main(int argc,char **argv){ + struct widget_instance widget={100,"button",2};int i; + struct ui_widget_event_handler_reference mouse={128,28,108,{NONE},{NONE},{0}}; + struct ui_widget_event_handler_reference open={8,0,0,{101},{NONE},{0}}; + struct ui_widget_event_handler_reference action={132,0,11,{NONE},{NONE},{0}}; + for(i=0;i<102;i++)event_handler_function_list.functions[i]=legacy_function; + for(i=0;i<4;i++)player_ui_globals.local_players[i].active_profile_index=1000+i; + player_ui_globals.edit_profile_index=NONE; + event_handler_function_list.functions[72]=network_game_remove_local_player; + widget_globals.active_widgets[2]=&widget; + if(argc!=2)return 99; + if(!strcmp(argv[1],"metadata")){ + unsigned char entries[32]={0},before[0x400];struct mcc_runtime r={entries,sizeof(metadata),{1}}; + uint32_t group='DeLa',address=0,count=2,events=0x100; + memcpy(entries,&group,4);memcpy(entries+20,&address,4); + memcpy(metadata+0x54,&count,4);memcpy(metadata+0x58,&events,4); + memset(metadata+0x100,0xA5,0x90);memcpy(before,metadata,sizeof(before)); + CHECK(mcc_tags_prepare(&r),1);CHECK(!memcmp(before,metadata,sizeof(before)),2); + events=0x3FF;memcpy(metadata+0x58,&events,4);CHECK(!mcc_tags_prepare(&r),3);return 0; + } + if(!strcmp(argv[1],"confirmation")){ + CHECK(!fire(&widget,28,&mouse)&&posted_button==0&&posted_controller==2,4); + CHECK(!revert_count&&!opened_count&&!deleted_count,5); + CHECK(fire(&widget,posted_button,&open)&&opened_count==1&&opened_tag==101,6); + CHECK(!revert_count,7); + CHECK(fire(&widget,0,&action)&&revert_count==1&&deleted_count==1,8);return 0; + } + if(!strcmp(argv[1],"client-denial")||!strcmp(argv[1],"no-checkpoint")){ + if(!strcmp(argv[1],"client-denial")){connection=1;coop=TRUE;}else saved=FALSE; + CHECK(!fire(&widget,0,&action)&&message_count==1,9); + CHECK(!revert_count&&!deleted_count&&!opened_count,10); + action.function=12;connection=1; + CHECK(!fire(&widget,0,&action)&&message_count==2&&!reset_count&&!restart_count,11);return 0; + } + if(!strcmp(argv[1],"restart")){ + action.function=12;CHECK(fire(&widget,0,&action)&&reset_count==1,12); + connection=2;coop=TRUE;widget_globals.active_widgets[2]=&widget; + CHECK(fire(&widget,0,&action)&&restart_count==1&&reset_count==1,13); + action.function=163;coop=FALSE;widget_globals.active_widgets[2]=&widget; + CHECK(fire(&widget,0,&action)&&restart_count==2,14);return 0; + } + if(!strcmp(argv[1],"quit")){ + action.function=13;action.flags=128;CHECK(!fire(&widget,0,&action)&&menu_count==1&&persist_count==1,15); + coop=TRUE;connection=1;fire(&widget,0,&action);CHECK(quit_count==4&&persist_count==1,16); + coop=FALSE;action.function=72;fire(&widget,0,&action);CHECK(quit_count==5&&quit_controller==2,17);return 0; + } + if(!strcmp(argv[1],"team")){ + connection=1;game.variant.universal_variant.teams=TRUE;game.players[0]=(struct network_player){1,1,2,0}; + game.players[1]=(struct network_player){1,2,2,1};action.function=157; + CHECK(fire(&widget,0,&action)&&team_sent==1&&sent_player.team_index==1&&sent_player.machine_index==2,18); + widget_tag_name="ui\\shell\\multiplayer_game\\pause_game\\red_team_button"; + fire(&widget,0,&action);CHECK(team_sent==2&&sent_player.team_index==0,19); + game.variant.universal_variant.teams=FALSE;CHECK(!fire(&widget,0,&action)&&message_count==1,20);return 0; + } + if(!strcmp(argv[1],"settings")||!strcmp(argv[1],"settings-uppercase")){ + if(!strcmp(argv[1],"settings-uppercase"))pc_menus=2; + campaign=FALSE;scenario.type=1; + action.function=137;action.flags=136;action.widget_tag.index=300; + CHECK(fire(&widget,0,&action)&&settings_open==1&&profile_begin==1,21); + CHECK(!opened_count,22);have_settings=FALSE; + CHECK(!fire(&widget,0,&action)&&message_count==1&&settings_open==1,23); + have_settings=TRUE;settings_collision=TRUE; + CHECK(!fire(&widget,0,&action)&&profile_begin==1&&settings_open==1&&message_count==2,36); + CHECK(mcc_ui_settings_needed("mcc_maps\\dangercanyon"),24);mcc=FALSE;CHECK(!mcc_ui_settings_needed("mcc_maps\\dangercanyon"),25);return 0; + } + if(!strcmp(argv[1],"save")){ + action.function=179;fire(&widget,0,&action);CHECK(save_count==1&&persist_count==1,26); + coop=TRUE;connection=2;fire(&widget,0,&action);CHECK(save_count==2&&persist_count==1,27); + connection=1;CHECK(!fire(&widget,0,&action)&&save_count==2&&message_count==1,28);return 0; + } + if(!strcmp(argv[1],"team-balance")){ + connection=1;game.variant.universal_variant.teams=TRUE;balance_allowed=FALSE;action.function=157; + CHECK(!fire(&widget,0,&action)&&message_count==1&&!team_sent&&!deleted_count,41);return 0; + } + if(!strcmp(argv[1],"new-game")){ + struct widget_instance *heap_widget=malloc(sizeof(*heap_widget)); + CHECK(heap_widget!=NULL,42);*heap_widget=widget; + widget_tag_name="ui\\shell\\multiplayer_game\\pause_game\\new_game_button"; + fire(&widget,28,&mouse);CHECK(posted_button==0,37); + /* The actual engine synchronously frees the whole tree. Pending + * close/go-back flags must not touch it again after MCC handles it. */ + open.flags|=(1u<<_event_handler_close_current_widget_bit)|(1u<<_event_handler_go_back_to_previous_widget_bit); + connection=2;widget_globals.active_widgets[2]=heap_widget;free_widget_on_delete=TRUE; + CHECK(fire(heap_widget,posted_button,&open)&&end_count==1&&!opened_count&&deleted_count==1,38); + CHECK(!widget_globals.active_widgets[2]&&released_widget==heap_widget,43); + free_widget_on_delete=FALSE;widget_globals.active_widgets[2]=&widget; + connection=1;CHECK(!fire(&widget,0,&open)&&end_count==1&&message_count==1&&!opened_count&&deleted_count==1,39); + connection=2;round_active=FALSE; + CHECK(!fire(&widget,0,&open)&&end_count==1&&message_count==2&&deleted_count==1,44); + open.flags=8; + mcc=FALSE;fire(&widget,0,&open);CHECK(opened_count==1&&end_count==1,40);return 0; + } + if(!strcmp(argv[1],"trusted-pause")){ + widget.definition_tag_index=300; + CHECK(tag_get('DeLa',300)==unowned&&mcc_ui_trusted_action(&widget,MCC_PAUSE_ACTION_RESUME),45); + action.function=MCC_PAUSE_ACTION_RESUME; + CHECK(fire(&widget,0,&action)&&deleted_count==1&&!legacy_count,46); + widget_globals.active_widgets[2]=&widget;action.function=MCC_PAUSE_ACTION_REVERT; + CHECK(fire(&widget,0,&action)&&revert_count==1,47); + widget_globals.active_widgets[2]=&widget;action.function=MCC_PAUSE_ACTION_RESTART; + CHECK(fire(&widget,0,&action)&&reset_count==1,48); + action.flags=128;action.function=MCC_PAUSE_ACTION_SAVE; + CHECK(!fire(&widget,0,&action)&&save_count==1&&persist_count==1,49); + action.function=MCC_PAUSE_ACTION_QUIT; + CHECK(!fire(&widget,0,&action)&&menu_count==1&&persist_count==2,50); + connection=1;coop=TRUE; + CHECK(!fire(&widget,0,&action)&&quit_count==4&&persist_count==2,51); + campaign=FALSE;scenario.type=1;coop=FALSE; + CHECK(fire(&widget,0,&action)&&quit_count==5&&quit_controller==2,52);return 0; + } + if(!strcmp(argv[1],"trusted-teams")){ + widget.definition_tag_index=300;widget_tag_name="generated\\arbitrary_button_name"; + connection=1;campaign=FALSE;scenario.type=1;game.variant.universal_variant.teams=TRUE; + game.players[1]=(struct network_player){1,2,2,1}; + action.function=MCC_PAUSE_ACTION_RED_TEAM; + CHECK(fire(&widget,0,&action)&&team_sent==1&&sent_player.team_index==0,53); + widget_globals.active_widgets[2]=&widget;action.function=MCC_PAUSE_ACTION_BLUE_TEAM; + CHECK(fire(&widget,0,&action)&&team_sent==2&&sent_player.team_index==1,54); + balance_allowed=FALSE;CHECK(!fire(&widget,0,&action)&&team_sent==2&&message_count==1,55);return 0; + } + if(!strcmp(argv[1],"split-resume")||!strcmp(argv[1],"split-team")|| + !strcmp(argv[1],"split-team-denied")||!strcmp(argv[1],"split-quit")|| + !strcmp(argv[1],"split-quit-sync")||!strcmp(argv[1],"local-quit")){ + struct widget_instance *heap_widget=malloc(sizeof(*heap_widget)); + struct widget_instance other={300,"other player's pause",1}; + struct widget_stack_data history={0},other_history={0}; + boolean team=!strncmp(argv[1],"split-team",10),resume=!strcmp(argv[1],"split-resume"); + boolean denied=!strcmp(argv[1],"split-team-denied"),local=!strcmp(argv[1],"local-quit"); + CHECK(heap_widget!=NULL,73);*heap_widget=widget;heap_widget->definition_tag_index=300; + campaign=FALSE;scenario.type=1;connection=local?0:1;local_players=local?1:2; + widget_globals.active_widgets[1]=&other;widget_globals.widget_stack[1]=&other_history; + widget_globals.active_widgets[2]=heap_widget;widget_globals.widget_stack[2]=&history; + free_widget_on_delete=TRUE;quit_closes_widgets=!strcmp(argv[1],"split-quit-sync"); + action.flags=128;action.function=resume?MCC_PAUSE_ACTION_RESUME: + team?MCC_PAUSE_ACTION_RED_TEAM:MCC_PAUSE_ACTION_QUIT; + if(team){game.variant.universal_variant.teams=TRUE;game.players[1]=(struct network_player){1,2,2,1};} + if(denied){ + balance_allowed=FALSE; + CHECK(!fire(heap_widget,0,&action)&&!deleted_count&&!stack_disposed&&!team_sent&&message_count==1,74); + CHECK(widget_globals.active_widgets[2]==heap_widget&&widget_globals.widget_stack[2]==&history,75); + free(heap_widget); + }else{ + /* The real close-for-controller helper frees the caller and its + * history. Native quit may do so first; dispatch must not reuse it. */ + CHECK(fire(heap_widget,0,&action)&&deleted_count==1&&stack_disposed==1,76); + CHECK(released_widget==heap_widget&&!widget_globals.active_widgets[2]&&!widget_globals.widget_stack[2],77); + CHECK(!opened_count&&!legacy_count&&!persist_count,78); + if(team)CHECK(team_sent==1&&sent_player.team_index==0&&!quit_count,79); + else if(resume)CHECK(!team_sent&&!quit_count&&!menu_count,80); + else if(local)CHECK(menu_count==1&&!quit_count&&!left_count,81); + else CHECK(quit_count==1&&quit_controller==2&&left_count==1&&left_controller==2&&!menu_count,82); + } + CHECK(widget_globals.active_widgets[1]==&other&&widget_globals.widget_stack[1]==&other_history,83); + return 0; + } + if(!strcmp(argv[1],"trusted-end")){ + struct widget_instance *heap_widget=malloc(sizeof(*heap_widget)); + CHECK(heap_widget!=NULL,56);*heap_widget=widget;heap_widget->definition_tag_index=300; + widget_globals.active_widgets[2]=heap_widget;free_widget_on_delete=TRUE; + connection=2;campaign=FALSE;scenario.type=1;action.function=MCC_PAUSE_ACTION_END_GAME; + /* Builder confirmations use run-function only for END_GAME. */ + action.flags=128; + CHECK(fire(heap_widget,0,&action)&&end_count==1&&deleted_count==1&&!opened_count,57); + CHECK(released_widget==heap_widget&&!widget_globals.active_widgets[2],58); + widget.definition_tag_index=300;connection=1;free_widget_on_delete=FALSE; + CHECK(!fire(&widget,0,&action)&&end_count==1&&message_count==1,59);return 0; + } + if(!strcmp(argv[1],"trusted-settings")){ + widget.definition_tag_index=300;action.function=MCC_PAUSE_ACTION_SETTINGS;action.flags=128; + CHECK(!fire(&widget,0,&action)&&!profile_begin&&!settings_open&&message_count==1,60); + campaign=FALSE;scenario.type=1;pc_menus=FALSE; + CHECK(!fire(&widget,0,&action)&&!profile_begin&&!settings_open&&message_count==2,61); + pc_menus=TRUE; + CHECK(fire(&widget,0,&action)&&profile_begin==1&&settings_open==1,62); + settings_collision=TRUE; + CHECK(!fire(&widget,0,&action)&&profile_begin==1&&settings_open==1&&message_count==3,63);return 0; + } + if(!strcmp(argv[1],"settings-namespace")){ + CHECK(!mcc_ui_settings_needed("mcc_maps\\a10"),69); + CHECK(!mcc_ui_settings_needed("mercury_falling")&&!mcc_ui_settings_needed("a10"),70); + campaign=FALSE;CHECK(mcc_ui_settings_needed("mcc_maps\\dangercanyon"),71); + CHECK(!mcc_ui_settings_needed("ui")&&!mcc_ui_settings_needed("custom_maps\\dangercanyon")&& + !mcc_ui_settings_needed("dangercanyon")&&!mcc_ui_settings_needed(NULL),72);return 0; + } + if(!strcmp(argv[1],"network-stale-solo")){ + /* Network startup changes its own map, leaving main_get_map_name() + * and the solo catalog classification at the previous campaign. */ + int role;widget.definition_tag_index=300;scenario.type=1;campaign=TRUE;action.flags=128; + CHECK(!strcmp(main_get_map_name(),"mcc_maps\\a10"),84); + for(role=1;role<=2;role++){ + connection=role;action.function=MCC_PAUSE_ACTION_SETTINGS; + CHECK(fire(&widget,0,&action)&&settings_open==role&&profile_begin==role,85); + widget_globals.active_widgets[2]=&widget;action.function=MCC_PAUSE_ACTION_QUIT; + CHECK(fire(&widget,0,&action)&&quit_count==role&&quit_controller==2,86); + CHECK(!persist_count&&!menu_count,87); + } + return 0; + } + if(!strcmp(argv[1],"campaign-stale-catalog")){ + /* Saving/reloading a live scenario must not depend on later catalog + * changes or the last multiplayer selection. */ + widget.definition_tag_index=300;scenario.type=0;campaign=FALSE;action.flags=128; + action.function=MCC_PAUSE_ACTION_SETTINGS; + CHECK(!fire(&widget,0,&action)&&!profile_begin&&!settings_open,88); + action.function=MCC_PAUSE_ACTION_SAVE; + CHECK(!fire(&widget,0,&action)&&save_count==1&&persist_count==1,89); + action.function=MCC_PAUSE_ACTION_QUIT; + CHECK(!fire(&widget,0,&action)&&persist_count==2&&menu_count==1&&!quit_count,90); + coop=TRUE;connection=1; + CHECK(!fire(&widget,0,&action)&&persist_count==2&&quit_count==4,91);return 0; + } + if(!strcmp(argv[1],"settings-no-scenario")){ + widget.definition_tag_index=300;global_scenario=NULL;campaign=FALSE;action.flags=128; + action.function=MCC_PAUSE_ACTION_SETTINGS; + CHECK(!fire(&widget,0,&action)&&!profile_begin&&!settings_open&&message_count==1,92); + global_scenario=&scenario;scenario.type=2; + CHECK(!fire(&widget,0,&action)&&!profile_begin&&!settings_open&&message_count==2,93);return 0; + } + if(!strcmp(argv[1],"settings-controller")){ + struct widget_instance *heap_widget=malloc(sizeof(*heap_widget)); + struct widget_instance other={300,"other notification",0}; + CHECK(heap_widget!=NULL,94);*heap_widget=widget;heap_widget->definition_tag_index=300; + scenario.type=1;action.flags=128;action.function=MCC_PAUSE_ACTION_SETTINGS; + widget_globals.active_widgets[0]=&other;widget_globals.active_widgets[2]=heap_widget;free_widget_on_delete=TRUE; + CHECK(fire(heap_widget,0,&action)&&released_widget==heap_widget&&settings_controller==2,95); + CHECK(player_ui_globals.edit_profile_index==1002&&player_ui_get_edit_player_profile()->value==1002,96); + CHECK(mcc_settings_controller==2&&settings_history_tag==300&&widget_globals.active_widgets[0]==&other,97); + CHECK(player_ui_globals.local_players[0].active_profile_index==1000&&assigned_controller==NONE,98); + free_widget_on_delete=FALSE;ui_widgets_close_all_for_local_player(2); + CHECK(!player_ui_get_edit_player_profile()&&mcc_settings_controller==NONE,99);return 0; + } + if(!strcmp(argv[1],"settings-split-denied")){ + int count;widget.definition_tag_index=300;scenario.type=1;action.flags=128; + for(count=2;count<=4;count++){ + local_players=count;action.function=MCC_PAUSE_ACTION_SETTINGS; + CHECK(!fire(&widget,0,&action)&&!profile_begin&&!settings_open&&!deleted_count,100); + widget.definition_tag_index=100;action.function=137; + CHECK(!fire(&widget,0,&action)&&!profile_begin&&!settings_open&&!deleted_count,101); + widget.definition_tag_index=300; + } + CHECK(message_count==6&&widget_globals.active_widgets[2]==&widget,102);return 0; + } + if(!strcmp(argv[1],"settings-busy")){ + widget.definition_tag_index=300;scenario.type=1;action.flags=128;action.function=MCC_PAUSE_ACTION_SETTINGS; + player_ui_begin_editing_profile(1000);player_ui_get_edit_player_profile()->value=765; + CHECK(!fire(&widget,0,&action)&&profile_begin==1&&!settings_open,103); + CHECK(player_ui_globals.edit_profile_index==1000&&player_ui_get_edit_player_profile()->value==765,104); + mcc_ui_settings_close(NONE);CHECK(player_ui_globals.edit_profile_index==1000,105); + player_ui_end_editing_profile();player_ui_begin_editing_profile(2000); + CHECK(!fire(&widget,0,&action)&&player_ui_get_edit_playlist_profile()!=NULL&&!settings_open,106); + player_ui_end_editing_profile();CHECK(fire(&widget,0,&action)&&mcc_settings_controller==2,107); + CHECK(!fire(&widget,0,&action)&&settings_open==1&&profile_begin==2,108);return 0; + } + if(!strcmp(argv[1],"settings-lifecycle")){ + struct widget_stack_data history={0}; + widget.definition_tag_index=300;scenario.type=1;action.flags=128;action.function=MCC_PAUSE_ACTION_SETTINGS; + CHECK(fire(&widget,0,&action)&&mcc_settings_controller==2,109); + /* Moving to a nested subpage deletes the root widget but must keep + * its native edit buffer; only closing the whole owner clears it. */ + ui_widget_delete(widget_globals.active_widgets[2]); + settings_widgets[2].definition_tag_index=201;widget_globals.active_widgets[2]=&settings_widgets[2]; + widget_globals.widget_stack[2]=&history;mcc_ui_settings_close(1); + CHECK(player_ui_get_edit_player_profile()!=NULL&&mcc_settings_controller==2,110); + ui_widgets_close_all_for_local_player(2); + CHECK(!player_ui_get_edit_player_profile()&&mcc_settings_controller==NONE&&!widget_globals.widget_stack[2],111); + widget.local_player_index=1;widget_globals.active_widgets[1]=&widget; + CHECK(fire(&widget,0,&action)&&settings_controller==1&&player_ui_globals.edit_profile_index==1001,112); + /* Cancel/save's native profile-clear releases ownership before any + * later unrelated editor begins; closing old UI cannot cancel it. */ + player_ui_end_editing_profile();CHECK(mcc_settings_controller==NONE,113); + player_ui_begin_editing_profile(1000);ui_widgets_close_all_for_local_player(1); + CHECK(player_ui_globals.edit_profile_index==1000,114); + player_ui_end_editing_profile();widget_globals.active_widgets[1]=&widget; + CHECK(fire(&widget,0,&action)&&mcc_settings_controller==1,115); + ui_widgets_close_all();CHECK(!player_ui_get_edit_player_profile()&&mcc_settings_controller==NONE,116); + widget_globals.active_widgets[1]=&widget;CHECK(fire(&widget,0,&action),117); + mcc_ui_settings_close(NONE);CHECK(!player_ui_get_edit_player_profile()&&mcc_settings_controller==NONE,118);return 0; + } + if(!strcmp(argv[1],"settings-open-failure")){ + widget.definition_tag_index=300;scenario.type=1;action.flags=128;action.function=MCC_PAUSE_ACTION_SETTINGS; + settings_load=FALSE; + CHECK(!fire(&widget,0,&action)&&!deleted_count&&!settings_open&&mcc_settings_controller==NONE,119); + CHECK(!player_ui_get_edit_player_profile()&&widget_globals.active_widgets[2]==&widget,120); + settings_load=TRUE;profile=FALSE; + CHECK(!fire(&widget,0,&action)&&!settings_open&&mcc_settings_controller==NONE,121); + profile=TRUE;player_ui_globals.local_players[2].active_profile_index=2000; + CHECK(!fire(&widget,0,&action)&&!player_ui_get_edit_playlist_profile()&&mcc_settings_controller==NONE,127); + player_ui_globals.local_players[2].active_profile_index=1002; + profile=TRUE;CHECK(fire(&widget,0,&action)&&settings_open==1&&player_ui_globals.edit_profile_index==1002,122);return 0; + } + if(!strcmp(argv[1],"settings-replaced-editor")){ + widget.definition_tag_index=300;scenario.type=1;action.flags=128;action.function=MCC_PAUSE_ACTION_SETTINGS; + CHECK(fire(&widget,0,&action)&&mcc_settings_controller==2,128); + /* Any later native editor entry invalidates MCC's ownership even + * when it does not call the normal end-edit helper first. */ + player_ui_begin_editing_profile(1000); + CHECK(mcc_settings_controller==NONE&&player_ui_globals.edit_profile_index==1000,129); + ui_widgets_close_all();CHECK(player_ui_globals.edit_profile_index==1000,130);return 0; + } + if(!strcmp(argv[1],"settings-profile-fallback")){ + widget.definition_tag_index=300;scenario.type=1;action.flags=128;action.function=MCC_PAUSE_ACTION_SETTINGS; + player_ui_globals.local_players[2].active_profile_index=NONE; + CHECK(!fire(&widget,0,&action)&&!profile_begin&&assigned_controller==NONE,123); + widget.local_player_index=0;widget_globals.active_widgets[0]=&widget; + player_ui_globals.local_players[0].active_profile_index=NONE; + CHECK(fire(&widget,0,&action)&&player_ui_globals.edit_profile_index==1004&&assigned_controller==0,124); + ui_widgets_close_all_for_local_player(0);player_ui_globals.local_players[0].active_profile_index=NONE;last_profile=NONE; + widget_globals.active_widgets[0]=&widget; + CHECK(fire(&widget,0,&action)&&player_ui_globals.edit_profile_index==1005&&assigned_controller==0,125); + CHECK(player_ui_globals.local_players[2].active_profile_index==NONE,126);return 0; + } + if(!strcmp(argv[1],"trusted-isolation")){ + action.function=MCC_PAUSE_ACTION_RESUME; + CHECK(!mcc_ui_trusted_action(&widget,action.function)&&!fire(&widget,0,&action)&&!deleted_count,64); + widget.definition_tag_index=301; + CHECK(!mcc_ui_trusted_action(&widget,action.function)&&!fire(&widget,0,&action)&&!deleted_count,65); + widget.definition_tag_index=300;pause_owned=FALSE; + CHECK(!fire(&widget,0,&action)&&!deleted_count,66); + pause_owned=TRUE;mcc=FALSE; + CHECK(!fire(&widget,0,&action)&&!deleted_count,67); + mcc=TRUE;action.function=MCC_PAUSE_ACTION_SETTINGS+1; + CHECK(!mcc_ui_trusted_action(&widget,action.function)&&!fire(&widget,0,&action)&&!deleted_count,68);return 0; + } + if(!strcmp(argv[1],"isolation")){ + mcc=FALSE;action.flags=128;fire(&widget,0,&action);CHECK(legacy_count==1&&!revert_count,29); + mcc=TRUE;widget.definition_tag_index=101;fire(&widget,0,&action);CHECK(legacy_count==2&&!revert_count,30); + widget.definition_tag_index=200;action.function=256;fire(&widget,0,&action);CHECK(port_count==1,31); + widget.definition_tag_index=100;fire(&widget,0,&action);CHECK(port_count==1,32); + action.function=67;fire(&widget,0,&action);CHECK(legacy_count==2,33);return 0; + } + if(!strcmp(argv[1],"unsupported")){ + action.function=190;action.flags=140;action.widget_tag.index=300; + CHECK(!fire(&widget,0,&action)&&message_count==1&&!opened_count&&!deleted_count,34);return 0; + } + if(!strcmp(argv[1],"script")){ + action.function=0;action.flags=1024;strcpy(action.script,"my_action");fire(&widget,0,&action); + CHECK(script_count==1&&!revert_count&&!message_count,35);return 0; + } + return 98; +} +''' + work = tmp_path_factory.mktemp("mcc-ui") + path = work / "ui.c" + path.write_text(source) + executable = work / ("ui.exe" if os.name == "nt" else "ui") + command = [compiler, "-std=c99", str(path), "-o", str(executable)] + if os.name == "nt": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return executable + + +@pytest.mark.parametrize("case", ["metadata", "confirmation", "client-denial", "no-checkpoint", "restart", + "quit", "team", "team-balance", "settings", "settings-uppercase", "save", "new-game", "isolation", "unsupported", "script", + "trusted-pause", "trusted-teams", "trusted-end", "trusted-settings", "trusted-isolation", "settings-namespace", + "split-resume", "split-team", "split-team-denied", "split-quit", "split-quit-sync", "local-quit", + "network-stale-solo", "campaign-stale-catalog", "settings-no-scenario", + "settings-controller", "settings-split-denied", "settings-busy", "settings-lifecycle", + "settings-open-failure", "settings-profile-fallback", "settings-replaced-editor"]) +def test_mcc_widget_events(ui_tool, case): + result = subprocess.run([str(ui_tool), case], capture_output=True, text=True) + assert result.returncode == 0, (case, result.returncode, result.stdout, result.stderr) + + +def test_native_settings_admission_preserves_mcc_pause_ownership(tmp_path): + """Run the real pre-allocation route with stock, CE and MCC tag layouts. + + Stop at the allocation boundary: this checks which maps reach the native + builder and whether its campaign patch is enabled, without loading assets. + """ + compiler = shutil.which("clang") + if not compiler: + pytest.skip("clang is needed for native menu admission tests") + menu = (ROOT / "port/linux/game/menu_tags.c").read_text() + admission = function(menu, "menu_tags_loaded").split("menus = halo_menus_load();", 1)[0] + source = r''' +#include +#include +typedef int boolean; +#define FALSE 0 +#define NONE (-1) +#define MULTIPLAYER_COLLECTION "collection" +static int mcc, wants_settings, pc=1, campaign_tag, multiplayer_tag; +static int built, built_campaign; +static int mcc_cache_tags_loaded(void){return mcc;} +static int mcc_ui_settings_needed(char const *name){(void)name;return wants_settings;} +static int single_player_campaign_map(void){return campaign_tag;} +static long tag_loaded(long group,char const *name){(void)group;(void)name;return multiplayer_tag?1:NONE;} +static int menus_pc_chosen(void){return pc;} +''' + admission + r''' + built++;built_campaign=campaign; +} +static void check(char const *name,int expected,int expected_campaign) +{ + built=0;built_campaign=0;menu_tags_loaded(name); + assert(built==expected);assert(built_campaign==expected_campaign); +} +int main(void) +{ + /* Upstream's stock/CE campaign Settings and normal menu route remain. */ + check("ui",1,0); + campaign_tag=1;check("a10",1,1);check("custom_maps\\a10",1,1); + campaign_tag=0;multiplayer_tag=1;check("bloodgulch",1,0); + pc=0;check("bloodgulch",0,0);check("ui",0,0);pc=1; + /* Even an MCC map carrying the native campaign widget must not get + that widget patched, nor get an extra set of campaign Settings tags. */ + mcc=1;campaign_tag=1;multiplayer_tag=0; + check("mcc_maps\\a10",0,0); + /* MCC multiplayer retains native Settings without the campaign patch. */ + wants_settings=1;check("mcc_maps\\dangercanyon",1,0); + wants_settings=0;pc=0;check("mcc_maps\\dangercanyon",0,0); + return 0; +} +''' + path = tmp_path / "admission.c" + path.write_text(source) + exe = tmp_path / ("admission.exe" if os.name == "nt" else "admission") + flags = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld"] if os.name == "nt" else [] + result = subprocess.run([compiler, *flags, str(path), "-o", str(exe)], capture_output=True, text=True) + assert result.returncode == 0, result.stderr + result = subprocess.run([str(exe)], capture_output=True, text=True) + assert result.returncode == 0, result.stderr diff --git a/tools/test_mcc_ui_network.py b/tools/test_mcc_ui_network.py new file mode 100644 index 0000000000..827a6aa927 --- /dev/null +++ b/tools/test_mcc_ui_network.py @@ -0,0 +1,208 @@ +"""Exercise the MCC restart transaction across the round-lifecycle boundary.""" +import os +from pathlib import Path +import shutil +import subprocess + +import pytest +from harness import function + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="module") +def restart_tool(tmp_path_factory): + compiler = os.environ.get("CC") or shutil.which("clang") or shutil.which("cc") + if not compiler: + pytest.skip("a C compiler is needed") + directory = tmp_path_factory.mktemp("mcc_ui_network") + (directory / "mcc_cache.h").write_text("") + implementation = (ROOT / "port/linux/game/mcc_ui_network.inl").read_text() + server_source = (ROOT / "source/networking/network_server_manager.c").read_text() + widget_source = (ROOT / "source/interface/ui_widget.c").read_text() + source = r''' +#include +#include +#include +#include +typedef int boolean; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +#define _error_silent 0 +#define NETWORK_SERVER_MANAGER_FILE "network_server_manager.c" +#define match_assert(file,line,condition) assert(condition) +#define MAXIMUM_NETWORK_PLAYER_COUNT 3 +#define csmemcpy memcpy +#define csmemset memset +enum { _network_game_server_state_pregame, _network_game_server_state_ingame, _network_game_server_state_postgame }; +struct network_player { int machine_index, controller_index, player_list_index, team_index; }; +struct network_game { + struct { int version; char name[128]; } map; + int variant, variant_options, cooperative_flags, minimum_players, maximum_players, maximum_teams, difficulty; + char name[32]; + struct network_player players[3]; + int number_of_games_played, local_data, machine_count; +}; +struct network_game_server { int state; struct network_game game; + struct {int timer;boolean active,paused,adjusted_time_this_tick;} countdown_state; +} server,other_server; +static int exists=1,loaded=1,fail_reset,countdowns,opened,switched,reset,leaver; +static int splitscreen,quickstart,closed_ui,loaded_ui,errors; +static struct network_game_server *selected_server=&server; +static struct network_game original; +static char map_name[128],screen_name[256]; +static struct network_game_server *global_network_game_server_get(void) {return exists ? selected_server : NULL;} +static int mcc_cache_tags_loaded(void) {return loaded;} +static int mcc_level_name(char const *name) {return !strncmp(name,"mcc_maps\\",9);} +static int network_player_is_valid(struct network_player const *p) {return p->machine_index>=0;} +static void main_set_multiplayer_map_name(char const *name) {strcpy(map_name,name);} +static void network_game_server_open_game(struct network_game_server *s) {(void)s;opened++;} +static void network_game_server_switch_to_postgame(struct network_game_server *s) {s->state=_network_game_server_state_postgame;switched++;} +static void network_event(char const *s) {(void)s;} +static void countdown_timer_set_time_remaining(int *timer,long ms) {*timer=(int)ms;} +static void network_game_server_countdown_started(struct network_game_server *s) { + assert(s->state==_network_game_server_state_pregame);countdowns++; +} +static void ui_widgets_close_all(void) {closed_ui++;} +static boolean network_game_is_splitscreen_local(void) {return splitscreen;} +static boolean network_game_is_quickstart_local(void) {return quickstart;} +static char const *pc_menus_screen(char const *name) {return name;} +static boolean ui_widget_load_by_name_or_tag(char const *name,long tag,void *parent,long controller,long a,long b,long c) { + (void)tag;(void)parent;(void)controller;(void)a;(void)b;(void)c; + strcpy(screen_name,name);loaded_ui++;return TRUE; +} +static void error(int priority,char const *text,...) {(void)priority;(void)text;errors++;} +boolean mcc_ui_network_restart_settings(struct network_game_server *s); +static int network_game_server_reset_to_pregame(struct network_game_server *s) { + int i;reset++; + memset(&s->countdown_state,0,sizeof(s->countdown_state)); + s->game.number_of_games_played++; + if(fail_reset)return 0; + s->game.local_data=0; + for(i=0;i<3;i++)s->game.players[i].team_index=1-s->game.players[i].team_index; + if(leaver)s->game.players[2].machine_index=-1; + if(!mcc_ui_network_restart_settings(s))exit(81); + /* Existing reset sends this exact state to every machine. */ + if(strcmp(s->game.map.name,original.map.name) || s->game.variant!=original.variant || + s->game.variant_options!=original.variant_options || s->game.difficulty!=original.difficulty || + s->game.cooperative_flags!=original.cooperative_flags || strcmp(s->game.name,original.name) || + s->game.map.version!=original.map.version || s->game.machine_count!=original.machine_count || + s->game.minimum_players!=original.minimum_players || s->game.maximum_players!=original.maximum_players || + s->game.maximum_teams!=original.maximum_teams)exit(82); + s->state=_network_game_server_state_pregame;return 1; +} +''' + '\n'.join(function(server_source, name) for name in [ + "network_game_server_pause_countdown", "network_game_server_begin_game_start_countdown" + ]) + '\n' + implementation + '\n' + function(widget_source, "network_game_reset_to_pregame_ui") + r''' +#define CHECK(c,n) do {if(!(c))return n;} while(0) +int main(int argc,char **argv) { + int i,result;struct network_game_server untouched; + if(argc!=2)return 99; + server.state=_network_game_server_state_ingame; + strcpy(server.game.map.name,"mcc_maps\\a10");server.game.map.version=123; + strcpy(server.game.name,"MCC game"); + server.game.variant=atoi(argv[1])==1 ? 0 : 7; + server.game.variant_options=13;server.game.cooperative_flags=1; + server.game.minimum_players=2;server.game.maximum_players=16; + server.game.maximum_teams=2;server.game.difficulty=3; + server.game.number_of_games_played=4;server.game.local_data=1;server.game.machine_count=3; + for(i=0;i<3;i++) {server.game.players[i].machine_index=i;server.game.players[i].controller_index=0; + server.game.players[i].player_list_index=i;server.game.players[i].team_index=i%2;} + original=server.game; + if(!strcmp(argv[1],"no-server"))exists=0; + if(!strcmp(argv[1],"inactive"))loaded=0; + if(!strcmp(argv[1],"xbox"))strcpy(server.game.map.name,"levels\\a10\\a10"); + if(!strcmp(argv[1],"ce"))strcpy(server.game.map.name,"custom_maps\\a10"); + if(!strcmp(argv[1],"pregame"))server.state=_network_game_server_state_pregame; + if(!strcmp(argv[1],"leaver"))leaver=1; + if(!strcmp(argv[1],"failure"))fail_reset=1; + if(!strcmp(argv[1],"dispose"))fail_reset=1; + if(!strcmp(argv[1],"splitscreen"))splitscreen=1; + untouched=server; + CHECK(!mcc_ui_network_restart_settings(NULL),1); + CHECK(!mcc_ui_network_restart_settings(&server),2); + result=mcc_ui_restart_network_game(); + if(!exists || !loaded || !mcc_level_name(untouched.game.map.name) || untouched.state==_network_game_server_state_pregame) { + CHECK(!result && !switched && !reset && !countdowns && !opened,3); + CHECK(!memcmp(&server,&untouched,sizeof(server)),4); + CHECK(!mcc_ui_network_restart_pregame(),15); + network_game_reset_to_pregame_ui(); + CHECK(closed_ui==1&&loaded_ui==1&&!countdowns,16); + if(exists)CHECK(server.countdown_state.paused&&strstr(screen_name,"map_select_postgame"),17); + else CHECK(strstr(screen_name,"connected_pregame_screen"),18); + return 0; + } + if(fail_reset) { + CHECK(!result && !countdowns && !opened,5); + CHECK(server.state==_network_game_server_state_postgame,6); + if(!strcmp(argv[1],"dispose")) { + mcc_ui_network_server_dispose(&server); + CHECK(!mcc_ui_network_restart_settings(&server),14); + server=untouched; + } + fail_reset=0;result=mcc_ui_restart_network_game(); + } + CHECK(result && !countdowns && opened==1,7); + CHECK(server.game.number_of_games_played==5 && !server.game.local_data,8); + CHECK(!strcmp(map_name,original.map.name),9); + CHECK(server.game.players[0].team_index==0 && server.game.players[1].team_index==1,10); + if(leaver)CHECK(server.game.players[2].machine_index==-1,11); + CHECK(!mcc_ui_network_restart_settings(&server),12); + CHECK(!mcc_ui_restart_network_game(),13); + /* Reliable messages are delivered later. The local client has unloaded + * the MCC map before this actual native UI transition executes. */ + loaded=0; + if(!strncmp(argv[1],"ui-",3)) { + struct network_game_server pending=server; + if(!strcmp(argv[1],"ui-dispose"))mcc_ui_network_server_dispose(&server); + if(!strcmp(argv[1],"ui-map"))strcpy(server.game.map.name,"mcc_maps\\different"); + if(!strcmp(argv[1],"ui-version"))server.game.map.version++; + if(!strcmp(argv[1],"ui-round"))server.game.number_of_games_played++; + if(!strcmp(argv[1],"ui-state"))server.state=_network_game_server_state_ingame; + if(!strcmp(argv[1],"ui-server"))selected_server=&other_server; + if(!strcmp(argv[1],"ui-exit"))exists=0; + CHECK(!mcc_ui_network_restart_pregame()&&!countdowns,19); + selected_server=&server;exists=1;server=pending; + CHECK(!mcc_ui_network_restart_pregame()&&!countdowns,20); + network_game_reset_to_pregame_ui(); + CHECK(server.countdown_state.paused&&strstr(screen_name,"map_select_postgame"),21); + return 0; + } + network_game_reset_to_pregame_ui(); + CHECK(closed_ui==1&&loaded_ui==1&&!errors&&countdowns==1,22); + CHECK(server.countdown_state.active&&!server.countdown_state.paused&&server.countdown_state.timer==3000,23); + CHECK(strstr(screen_name,splitscreen?"splitscreen_pregame_wrapper_normal":"connected_pregame_screen"),24); + CHECK(!strstr(screen_name,"map_select_postgame"),25); + CHECK(!mcc_ui_network_restart_pregame()&&countdowns==1,26); + if(!strcmp(argv[1],"second-round")) { + server.state=_network_game_server_state_ingame;loaded=1;original=server.game; + CHECK(mcc_ui_restart_network_game()&&countdowns==1&&server.game.number_of_games_played==6,27); + loaded=0;network_game_reset_to_pregame_ui(); + CHECK(countdowns==2&&closed_ui==2&&loaded_ui==2&&!server.countdown_state.paused,28); + } else if(!splitscreen) { + /* A subsequent ordinary End Game must return to its original map + * picker and pause the timer, not reuse an old restart token. */ + network_game_reset_to_pregame_ui(); + CHECK(server.countdown_state.paused&&!server.countdown_state.active&&countdowns==1,29); + CHECK(strstr(screen_name,"map_select_postgame"),30); + } + return 0; +} +''' + path = directory / "restart.c" + path.write_text(source) + executable = directory / ("restart.exe" if os.name == "nt" else "restart") + command = [compiler, "-std=c99", "-I", str(directory), str(path), "-o", str(executable)] + if os.name == "nt": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return executable + + +@pytest.mark.parametrize("case", ["1", "2", "leaver", "failure", "dispose", "no-server", "inactive", "xbox", "ce", "pregame", + "splitscreen", "second-round", "ui-dispose", "ui-map", "ui-version", "ui-round", + "ui-state", "ui-server", "ui-exit"]) +def test_restart_transaction(restart_tool, case): + subprocess.run([str(restart_tool), case], check=True, capture_output=True) diff --git a/tools/test_mcc_ui_teams.py b/tools/test_mcc_ui_teams.py new file mode 100644 index 0000000000..cc125a1b82 --- /dev/null +++ b/tools/test_mcc_ui_teams.py @@ -0,0 +1,249 @@ +"""Exercise the real network settings handler and MCC's team-only ingress.""" +import os +from pathlib import Path +import shutil +import subprocess + +import pytest +from harness import function, structure + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="module") +def teams_tool(tmp_path_factory): + compiler = os.environ.get("CC") or shutil.which("clang") or shutil.which("cc") + if not compiler: + pytest.skip("a C compiler is needed") + own = (ROOT / "port/linux/game/mcc_ui_teams.c").read_text() + native = (ROOT / "source/networking/network_server_message_handler.c").read_text() + engine = (ROOT / "source/game/game_engine.c").read_text() + slayer = (ROOT / "source/game/game_engine_slayer.c").read_text() + damage = function((ROOT / "source/objects/damage.c").read_text(), "object_damage_aftermath") + start = damage.index("{", damage.index("else if (game_engine_can_score())")) + end, depth = start + 1, 1 + while depth: + depth += (damage[end] == "{") - (damage[end] == "}") + end += 1 + no_statistics_branch = damage[start:end] + cache = (ROOT / "port/linux/game/mcc_cache.c").read_text() + source = r''' +#include +#include +#include +#include +typedef int boolean; +typedef uint16_t word; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +#define NUMBEROF(a) (sizeof(a)/sizeof(*(a))) +#define VALID_INDEX(i,n) ((unsigned long)(i)<(unsigned long)(n)) +#define MAXIMUM_NUMBER_OF_PLAYERS 4 +#define NUMBER_OF_MULTIPLAYER_TEAMS 2 +#define HALO_PORT_NETWORK_GAME_MESSAGE_VERSION 12 +#define NETWORK_GAME_MESSAGE_VERSION HALO_PORT_NETWORK_GAME_MESSAGE_VERSION +#define _message_client_player_settings_request 16 +#define _network_game_packet_class_client_pregame 3 +#define HALO_PORT_MAXIMUM_NETWORK_PLAYERS 4 +#define DATUM_INDEX_TO_ABSOLUTE_INDEX(i) ((unsigned long)(i)&0xFFFF) +#define TEST_FLAG(v,b) ((v)&(1u<<(b))) +#define _object_die_act_of_god_no_statistics_bit 2 +#define _damage_no_statistics_bit 3 +#define _game_connection_network_server 2 +#define _game_connection_network_client 1 +#define _object_type_vehicle 1 +#define TICKS_PER_SECOND 30 +#define MIN(a,b) ((a)<(b)?(a):(b)) +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define match_assert(file,line,condition) do{if(!(condition))exit(91);}while(0) +struct network_player {uint16_t name[12];short color,icon;char machine_index,controller_index,team_index,player_list_index;}; +struct network_game {struct {char name[128];} map;struct {struct {int teams;}universal_variant;}variant;struct network_player players[4];int machines[4];}; +struct network_game_server {int state;struct network_game game;}server; +struct network_game_server_client_machine {int index;}machine; +struct player_datum {int quit_out_of_game,team_index,unit_index;struct network_player network_player_data; + long quit_out_of_game_time,death_time,respawn_timer,respawn_penalty,multiplayer_special; + struct {int multiple_kills,kills_in_a_row;}statistics;}players[4]; +struct unit_datum {struct {unsigned damage_flags;}object;}units[4]; +struct object_datum {struct {int type;}object;}object; +static long player_handles[4]={0x10000,0x10001,0x10002,0x10003}; +static long unit_handles[4]={0x20000,0x20001,0x20002,0x20003}; +struct data_iterator {int i;long datum_index;}; +struct {int auto_team_balance,friendly_fire_penalty;}options; +static int player_data,mcc=1,bad_decode,legacy,send_count,kill_count,killed,decoded; +static int connection=2,score,death_broadcast,death_friendly,engine_active=1; +static long death_killer,death_object; +static struct network_player request; +static struct {int type;}scenario={1}; +#define global_scenario (&scenario) +static boolean mcc_cache_tags_loaded(void){return mcc;} +static short game_connection(void){return (short)connection;} +static struct player_datum *player_try_and_get(long index){unsigned long i=DATUM_INDEX_TO_ABSOLUTE_INDEX(index);return i<4&&player_handles[i]==index?&players[i]:NULL;} +static struct player_datum *player_get(long index){struct player_datum *p=player_try_and_get(index);if(!p)exit(92);return p;} +static struct unit_datum *unit_try_and_get(long index){unsigned long i=DATUM_INDEX_TO_ABSOLUTE_INDEX(index);return i<4&&unit_handles[i]==index?&units[i]:NULL;} +static int network_game_server_get_state(struct network_game_server *s,void *unused){(void)unused;return s->state;} +static struct network_game *network_game_server_get_game(struct network_game_server *s){return &s->game;} +static boolean mcc_level_name(char const *s){return !strncmp(s,"mcc_maps\\",9);} +static boolean decode_network_game_message(void *out,void const *wire,short *size,short *type,short *version,int cls){ + (void)wire;(void)size;decoded++;if(*type!=16||*version!=12||cls!=3)exit(80);memcpy(out,&request,sizeof(request));return !bad_decode; +} +static void *network_game_server_get_client_machine(struct network_game_server *s,struct network_game_server_client_machine *m,long *index){(void)s;*index=m->index;return m->index==-1?NULL:m;} +static boolean network_player_is_valid(struct network_player *p){return p->machine_index>=0;} +static void data_iterator_new(struct data_iterator *it,int data){(void)data;it->i=0;} +static void *data_iterator_next(struct data_iterator *it){if(it->i>=4)return NULL;it->datum_index=player_handles[it->i];return &players[it->i++];} +static void *game_variant_options_get(void); +#define game_variant_options_get() (&options) +static void unit_kill_no_statistics(long unit){struct unit_datum *p=unit_try_and_get(unit);if(!p)exit(93);p->object.damage_flags|=1u<<_object_die_act_of_god_no_statistics_bit;kill_count++;killed=(int)unit;} +static void network_event(char const *fmt,...){(void)fmt;} +static boolean network_game_server_lobby_is_open(struct network_game_server *s){legacy++;return s->state==0;} +static void network_game_server_clean_player_name(struct network_game_server *s,struct network_player *p,long slot){(void)s;(void)p;(void)slot;} +static boolean network_game_update_player(struct network_game *g,struct network_player *p){g->players[p->player_list_index]=*p;return TRUE;} +static boolean network_game_server_send_game_data_pregame(struct network_game_server *s){(void)s;send_count++;return TRUE;} +/* Native game-engine and Slayer callbacks below run unchanged. */ +static struct {struct {int respawn_time,respawn_time_growth,suicide_penalty,teams;}universal_variant;}global_variant; +static short game_engine_betrayal_penalty[4]; +static struct {struct {struct {int kill_in_order;}slayer;}game_engine_variant;}variant; +static struct {void (*player_killed_player)(long,long,long,boolean);}engine_callbacks; +#define game_engine (engine_active?&engine_callbacks:NULL) +static int network_game_distributed_client(void){return connection==1;} +static void network_distributed_player_killed(long *k,long *o,long d,boolean *f){(void)d;death_broadcast++;death_killer=*k;death_object=*o;death_friendly=*f;} +static long game_time_get(void){return 1000;} +static struct network_game_server *global_network_game_server_get(void){return connection==2?&server:NULL;} +static struct object_datum *object_get(long index){(void)index;return &object;} +static long player_index_from_unit_index(long unit){int i;for(i=0;i<4;i++)if(players[i].unit_index==unit)return player_handles[i];return NONE;} +struct damage_data {unsigned flags;}; +static void game_show_score_extended(long p,long m,long d){(void)p;(void)m;(void)d;} +static void multiplayer_message(long p,long m,long d){(void)p;(void)m;(void)d;} +static void update_speed_for_score(long d,long k){(void)d;(void)k;} +#define game_engine_get_variant() (&variant) +static void find_next_target(long p){(void)p;} +static void slayer_engine_adjust_score(long p,int amount){(void)p;score+=amount;} +enum {_game_engine_message_killed_by_unknown,_game_engine_message_killed_by_biped,_game_engine_message_killed_by_vehicle, +_game_engine_message_killed_by_self,_game_engine_message_killed_by_player,_game_engine_message_killed_by_friendly_fire, +_game_engine_message_quit,_game_engine_message_killed_friendly,_game_engine_message_multi_kill,_game_engine_message_triple_kill, +_game_engine_message_double_kill,_game_engine_message_five_kills_in_row,_game_engine_message_killed_enemy,_game_engine_message_ten_kills_in_a_row}; +#define _object_type_biped 0 +''' + structure(own, "mcc_team_respawn") + r''' +static struct mcc_team_respawn mcc_team_respawns[HALO_PORT_MAXIMUM_NETWORK_PLAYERS]; +''' + '\n'.join(function(own, n) for n in ["mcc_ui_teams_reset", "mcc_ui_team_respawn_track", "mcc_ui_team_respawn_damage", "mcc_ui_team_respawn_death", + "mcc_ui_team_balance_allows", "mcc_ui_team_request"]) + '\n' + function( + native, "network_game_server_handle_message_client_player_settings_request") + '\n' + function( + slayer, "slayer_engine_player_killed_player") + '\n' + function(engine, "game_engine_player_killed") + '\n' + \ + 'static void no_statistics_death(long object_index, struct damage_data *damage)\n' + no_statistics_branch + r''' +/* Exercise the actual MCC teardown, with resource releases reduced to fakes. */ +static struct {void *tags;unsigned capacity;}mcc_loaded_cache; +static int mcc_loaded; +static FILE *mcc_stream; +static void cleanup(void *unused){(void)unused;} +#define mcc_checkpoint_dispose() cleanup(NULL) +#define mcc_grenades_reset() cleanup(NULL) +#define mcc_parameters_dispose() cleanup(NULL) +#define mcc_texture_cache_dispose() cleanup(NULL) +#define mcc_geometry_dispose(p) cleanup(p) +#define mcc_audio_dispose(p) cleanup(p) +#define mcc_bitmaps_dispose(p) cleanup(p) +#define mcc_validation_dispose(p) cleanup(p) +#define mcc_memory_release(p,n) cleanup(p) +''' + function(cache, "mcc_cache_tags_unload") + r''' +#define CHECK(c,n) do{if(!(c)){fprintf(stderr,"check%d failed\n",n);return n;}}while(0) +int main(int argc,char **argv){ + word wire[18]={16};int i;short size=sizeof(wire);struct network_game before;struct player_datum prior[4]; + char const *test=argc>1?argv[1]:""; + ((unsigned char *)wire)[size-1]=16; + strcpy(server.game.map.name,"mcc_maps\\dangercanyon");server.game.variant.universal_variant.teams=TRUE;server.state=1;machine.index=2; + for(i=0;i<4;i++){server.game.players[i].machine_index=-1;players[i].quit_out_of_game=TRUE;players[i].unit_index=NONE;} + request=(struct network_player){{'A',0},2,3,2,1,0,1};server.game.players[1]=request; + players[1].quit_out_of_game=FALSE;players[1].team_index=0;players[1].unit_index=0x20001;players[1].network_player_data=request; + players[1].quit_out_of_game_time=NONE;engine_callbacks.player_killed_player=slayer_engine_player_killed_player; + global_variant.universal_variant.respawn_time=120; + request.team_index=1; + if(!strcmp(test,"non-mcc"))mcc=0; + if(!strcmp(test,"non-namespace"))strcpy(server.game.map.name,"custom_maps\\x"); + if(!strcmp(test,"pregame"))server.state=0; + if(!strcmp(test,"postgame"))server.state=2; + if(!strcmp(test,"no-teams"))server.game.variant.universal_variant.teams=FALSE; + if(!strcmp(test,"campaign"))scenario.type=0; + if(!strcmp(test,"unjoined"))machine.index=-1; + if(!strcmp(test,"malformed"))bad_decode=1; + if(!strcmp(test,"short"))size=1; + if(!strcmp(test,"negative"))size=-1; + if(!strcmp(test,"minimum"))size=-32768; + if(!strcmp(test,"oversized"))size=128; + if(!strcmp(test,"wrong-packet"))((unsigned char *)wire)[size-1]=15; + if(!strcmp(test,"machine"))machine.index=3; + if(!strcmp(test,"controller"))request.controller_index=2; + if(!strcmp(test,"bad-team"))request.team_index=2; + if(!strcmp(test,"bad-slot"))request.player_list_index=-1; + if(!strcmp(test,"unknown-slot"))request.player_list_index=2; + if(!strcmp(test,"name"))request.name[0]='B'; + if(!strcmp(test,"color"))request.color=4; + if(!strcmp(test,"icon"))request.icon=4; + if(!strcmp(test,"unknown-live"))players[1].network_player_data.controller_index=2; + if(!strcmp(test,"already-team"))request.team_index=0; + if(!strcmp(test,"dead"))players[1].unit_index=NONE; + if(!strcmp(test,"balance")){options.auto_team_balance=TRUE;players[0].quit_out_of_game=FALSE;players[0].team_index=1;} + before=server.game;memcpy(prior,players,sizeof(prior)); + CHECK(network_game_server_handle_message_client_player_settings_request(&server,&machine,wire,size),1); + if(!strcmp(test,"valid")||!strcmp(test,"dead")){ + CHECK(!legacy&&server.game.players[1].team_index==1&&players[1].team_index==1&&players[1].network_player_data.team_index==1,2); + CHECK(kill_count==(!strcmp(test,"valid")?1:0),3);if(kill_count)CHECK(killed==0x20001,4);return 0; + } + if(!strncmp(test,"death-",6)){ + long victim=player_handles[1],killer=victim,unit=unit_handles[1]; + struct damage_data cause={1u<<_damage_no_statistics_bit}; + CHECK(kill_count==1,9); + if(strcmp(test,"death-real-suicide"))mcc_ui_team_respawn_damage(victim,unit,TRUE); + if(!strcmp(test,"death-unload"))mcc_cache_tags_unload(); + if(!strcmp(test,"death-non-mcc"))mcc=FALSE; + if(!strcmp(test,"death-client"))connection=1; + if(!strcmp(test,"death-new-player")){player_handles[1]=0x30001;victim=killer=player_handles[1];} + if(!strcmp(test,"death-new-unit")){unit_handles[1]=0x40001;unit=players[1].unit_index=unit_handles[1];} + if(!strcmp(test,"death-deleted-unit"))unit_handles[1]=NONE; + if(!strcmp(test,"death-no-flag"))units[1].object.damage_flags=0; + if(!strcmp(test,"death-other-killer"))killer=player_handles[0]; + if(!strcmp(test,"death-no-engine")){ + engine_active=FALSE;no_statistics_death(unit,&cause);CHECK(!death_broadcast,16);engine_active=TRUE; + } + if(!strcmp(test,"death-neutral"))no_statistics_death(unit,&cause); + else game_engine_player_killed(killer,unit,victim,TRUE); + CHECK(death_broadcast==1&&players[1].death_time==1000&&players[1].respawn_timer==120,10); + if(!strcmp(test,"death-neutral")){ + CHECK(!score&&death_killer==NONE&&death_object==NONE&&!death_friendly,11); + game_engine_player_killed(killer,unit,victim,TRUE); + CHECK(score==-1&&death_killer==killer&&death_object==unit&&death_friendly,12); + }else if(!strcmp(test,"death-client"))CHECK(!score&&death_killer==killer,13); + else { + CHECK(score==-1&&death_killer==killer&&death_object==unit&&death_friendly,14); + if(!strcmp(test,"death-other-killer")){ + game_engine_player_killed(victim,unit,victim,TRUE); + CHECK(score==-2&&death_killer==victim,15); + } + } + return 0; + } + if(!strcmp(test,"pregame")){CHECK(legacy==1&&send_count==1&&!kill_count,5);return 0;} + CHECK(!memcmp(&before,&server.game,sizeof(before))&&!memcmp(prior,players,sizeof(prior))&&!kill_count,6); + if(!strcmp(test,"non-mcc")||!strcmp(test,"non-namespace")||!strcmp(test,"postgame"))CHECK(legacy==1,7); + else CHECK(!legacy,8); + return 0; +} +''' + work = tmp_path_factory.mktemp("mcc-ui-teams") + path = work / "teams.c" + path.write_text(source) + executable = work / ("teams.exe" if os.name == "nt" else "teams") + command = [compiler, "-std=c99", str(path), "-o", str(executable)] + if os.name == "nt": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return executable + + +@pytest.mark.parametrize("case", ["valid", "dead", "non-mcc", "non-namespace", "pregame", "postgame", "no-teams", + "campaign", "unjoined", "malformed", "short", "negative", "minimum", "oversized", "wrong-packet", "machine", "controller", "bad-team", "bad-slot", "unknown-slot", "name", "color", "icon", + "unknown-live", "already-team", "balance", "death-neutral", "death-unload", "death-non-mcc", "death-client", + "death-new-player", "death-new-unit", "death-deleted-unit", "death-no-flag", "death-other-killer", "death-real-suicide", "death-no-engine"]) +def test_mcc_ingame_team_request(teams_tool, case): + result = subprocess.run([str(teams_tool), case], capture_output=True, text=True) + assert result.returncode == 0, (case, result.returncode, result.stdout, result.stderr) diff --git a/tools/test_mcc_vorbis.py b/tools/test_mcc_vorbis.py new file mode 100644 index 0000000000..0101105730 --- /dev/null +++ b/tools/test_mcc_vorbis.py @@ -0,0 +1,339 @@ +"""Asset-free Vorbis streams exercise MCC's isolated empty-residue adapter. + +Generated streams have two residue passes: an empty vector book followed by a +nonzero vector book. This catches the original decoder's premature residue exit +as well as error suppression that would return the wrong PCM. No game audio is +included. The fixture also links the unchanged legacy decoder alongside MCC. +""" +from pathlib import Path +import os +import shutil +import struct +import subprocess +import re +import sys + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "tools")) +from harness import function, structure + + +class Bits: + def __init__(self): + self.bits = [] + + def put(self, value, width): + self.bits.extend((value >> i) & 1 for i in range(width)) + + def bytes(self): + out = bytearray((len(self.bits) + 7) // 8) + for i, value in enumerate(self.bits): + out[i // 8] |= value << (i % 8) + return bytes(out) + + +def crc32(data): + crc = 0 + for value in data: + crc ^= value << 24 + for _ in range(8): + crc = ((crc << 1) ^ (0x04C11DB7 if crc & 0x80000000 else 0)) & 0xFFFFFFFF + return crc + + +def page(packet, sequence, flags, granule): + lacing = [255] * (len(packet) // 255) + [len(packet) % 255] + result = bytearray(b"OggS" + bytes([0, flags]) + struct.pack(" +#include +#include +#include "mcc_vorbis.h" +#define STB_VORBIS_HEADER_ONLY +#include "stb_vorbis.c" +int main(int argc,char **argv) { + FILE *file;unsigned char *bytes;long size;short *pcm=NULL; + uint32_t frames=0,rate=0;int channels=0,ok; + if(argc!=4)return 2; + file=fopen(argv[1],"rb");if(!file)return 3; + fseek(file,0,SEEK_END);size=ftell(file);rewind(file); + bytes=malloc(size);if(!bytes || fread(bytes,1,size,file)!=(size_t)size)return 4;fclose(file); + if(atoi(argv[3])) { + int error=0;stb_vorbis *v=stb_vorbis_open_memory(bytes,size,&error,NULL); + if(!v){free(bytes);return 1;} + channels=stb_vorbis_get_info(v).channels;rate=stb_vorbis_get_info(v).sample_rate; + pcm=calloc(512*channels,sizeof(short)); + frames=stb_vorbis_get_samples_short_interleaved(v,channels,pcm,512*channels); + ok=frames && stb_vorbis_get_error(v)==0;stb_vorbis_close(v); + } else ok=mcc_vorbis_decode(bytes,(uint32_t)size,1024,&pcm,&frames,&rate,&channels); + if(ok) {file=fopen(argv[2],"wb");if(!file)return 5; + fwrite(&frames,4,1,file);fwrite(&rate,4,1,file);fwrite(&channels,4,1,file); + fwrite(pcm,2,frames*channels,file);fclose(file);} + if(atoi(argv[3]))free(pcm);else mcc_vorbis_free(pcm); + free(bytes);return ok?0:1; +} +''') + exe = work / ("decoder.exe" if os.name == "nt" else "decoder") + flags = ["-target", "i686-pc-windows-msvc"] if os.name == "nt" else [] + subprocess.run([compiler, *flags, "-O2", "-Wno-deprecated-declarations", "-Wno-tautological-compare", + "-I", str(ROOT / "port/linux/game"), str(source), + str(ROOT / "port/linux/game/mcc_vorbis.c"), + str(ROOT / "port/linux/game/stb_vorbis.c"), + *([] if os.name == "nt" else ["-lm"]), "-o", str(exe)], check=True, capture_output=True) + return exe + + +def decode(decoder, tmp_path, data, legacy=False): + source, target = tmp_path / "input.ogg", tmp_path / "out.pcm" + source.write_bytes(data) + result = subprocess.run([str(decoder), str(source), str(target), str(int(legacy))], capture_output=True) + assert result.returncode in (0, 1), result.stderr + return target.read_bytes() if result.returncode == 0 else None + + +@pytest.mark.parametrize("channels", [1, 2]) +@pytest.mark.parametrize("residue_type", [0, 1, 2]) +@pytest.mark.parametrize("empty_entries", [1, 625]) +def test_empty_residue_preserves_later_nonzero_pass(decoder, tmp_path, channels, residue_type, empty_entries): + expected = decode(decoder, tmp_path, stream(channels, residue_type, empty=False)) + assert expected is not None + assert struct.unpack_from(" +#include +#include +#include +#include "mcc_vorbis.h" +#define PIN(x,a,b) ((x)<(a)?(a):((x)>(b)?(b):(x))) +#define MCC_AUDIO_FRAME_LIMIT 0x1000000u +static void *tracked; +static int allocations,releases,fail_allocation; +static void *tracked_malloc(size_t size) { + unsigned char *allocation; + if(fail_allocation)return NULL; + if(tracked)exit(70); + allocation=malloc(size+16);if(!allocation)exit(71); + tracked=allocation+16;allocations++;return tracked; +} +static void tracked_free(void *pointer) { + /* A plain decoder pointer is rejected before any header is dereferenced. */ + if(pointer!=tracked || !tracked)exit(72); + free((unsigned char *)pointer-16);tracked=NULL;releases++; +} +#define malloc tracked_malloc +#define free tracked_free +''' + declarations + "\n" + tables + "\n" + routines + r''' +#undef malloc +#undef free +int main(int argc,char **argv) { + unsigned char raw[8]={0,0,1,0,2,0,3,0},bad_adpcm[36]={0}; + FILE *file;long size;unsigned char *bytes;struct mcc_pcm pcm; + if(argc!=2)return 2; + file=fopen(argv[1],"rb");if(!file)return 3; + fseek(file,0,SEEK_END);size=ftell(file);rewind(file);bytes=malloc(size); + if(!bytes || fread(bytes,1,size,file)!=(size_t)size)return 4;fclose(file); + if(!mcc_pcm_open(bytes,size,3,2,22050,&pcm) || !pcm.samples || !pcm.vorbis_owned)return 5; + /* This close must use the decoder's owner, with no tracked allocation. */ + mcc_pcm_close(&pcm);mcc_pcm_close(&pcm); + if(pcm.samples || allocations || releases)return 6; + if(!mcc_pcm_open(raw,8,0,1,22050,&pcm) || pcm.vorbis_owned || pcm.frames!=4)return 7; + mcc_pcm_close(&pcm);mcc_pcm_close(&pcm); + if(pcm.samples || allocations!=1 || releases!=1)return 8; + /* A post-allocation ADPCM failure must also return to the tracked owner. */ + bad_adpcm[2]=89; + if(mcc_pcm_open(bad_adpcm,36,1,1,22050,&pcm))return 9; + mcc_pcm_close(&pcm); + if(allocations!=2 || releases!=2 || tracked)return 10; + if(mcc_pcm_open(raw,8,3,2,22050,&pcm))return 11; + mcc_pcm_close(&pcm); + fail_allocation=1; + if(mcc_pcm_open(raw,8,0,1,22050,&pcm))return 12; + mcc_pcm_close(&pcm); + if(allocations!=2 || releases!=2 || tracked)return 13; + free(bytes);return 0; +} +''') + exe = work / ("allocator.exe" if os.name == "nt" else "allocator") + flags = ["-target", "i686-pc-windows-msvc"] if os.name == "nt" else [] + subprocess.run([compiler, *flags, "-O2", "-Wno-deprecated-declarations", "-Wno-tautological-compare", + "-I", str(ROOT / "port/linux/game"), str(source), + str(ROOT / "port/linux/game/mcc_vorbis.c"), + *([] if os.name == "nt" else ["-lm"]), "-o", str(exe)], check=True, capture_output=True) + return exe + + +def test_pcm_returns_to_its_own_allocator(allocator_decoder, tmp_path): + source = tmp_path / "input.ogg" + source.write_bytes(stream()) + subprocess.run([str(allocator_decoder), str(source)], check=True, capture_output=True) From 957d649164db9eade0c3aa35d46b6103bd39b8a1 Mon Sep 17 00:00:00 2001 From: chestahh Date: Sat, 10 Oct 2026 16:38:23 +0100 Subject: [PATCH 2/8] Add MCC resource and campaign compatibility fixes (#241) Bring the tested MCC-only fixes from the fork into the contribution: Vorbis window transitions, separate bitmap/sound resource readers, campaign script operations, larger textures and materialless model parts. Keep campaign state in MCC checkpoints and host-controlled snapshots. Advance the contribution's network version from 26 to 27 for campaign message 84. All peers require matching builds. Xbox and Custom Edition loaders and function/global tables remain unchanged; shared source hooks are additive and MCC-scoped. Resolve the protocol-version conflict, correct the upstream installation documentation, and add resource/campaign tests to the MCC validation jobs. Keep the existing updater and release workflow unchanged. The combined contribution passes 738 tests (6 optional-fixture skips) and the Windows x86 release build. Targeted rendered loads of Solar Flare m0, Combat Revolved b40 and Spasm pass, as do native checkpoint/core restore, MCC-to-Xbox-to-CE transitions and two-instance loopback co-op state checks. Development-build validation covered all 16 supplied maps and 247 sound permutations against a reference decoder. Full campaign playthroughs and Linux/Android builds of this update remain unverified. (cherry picked from commit b14d17226a0e56a41fb07a7a031c19859c6be92f) --- .github/workflows/mcc.yml | 3 +- README.md | 5 +- docs/mcc_maps.md | 142 ++++++++- port/linux/game/mcc_audio.c | 57 +++- port/linux/game/mcc_bitmaps.c | 46 ++- port/linux/game/mcc_cache.c | 5 + port/linux/game/mcc_campaign.c | 326 ++++++++++++++++++++ port/linux/game/mcc_campaign.h | 18 ++ port/linux/game/mcc_checkpoint.c | 44 ++- port/linux/game/mcc_geometry.c | 2 +- port/linux/game/mcc_network.c | 23 ++ port/linux/game/mcc_network.h | 2 + port/linux/game/mcc_resources.c | 105 +++++++ port/linux/game/mcc_resources.h | 9 + port/linux/game/mcc_runtime.h | 1 + port/linux/game/mcc_script_runtime.inl | 32 ++ port/linux/game/mcc_scripts.c | 39 ++- port/linux/game/mcc_vorbis.c | 49 ++- port/linux/game/network_distributed.c | 5 + port/linux/game/network_distributed.h | 2 + port/linux/include/halo_port_limits.h | 4 +- port/linux/src/mcc_texture_bridge.c | 4 +- port/linux/src/xbox_textures.c | 8 +- source/hs/hs_globals_external.c | 6 + source/interface/hud_nav_points.c | 5 + tools/harness/tests/mcc_campaign.c | 155 ++++++++++ tools/harness/tests/mcc_network.c | 20 +- tools/harness/tests/mcc_script_parameters.c | 34 +- tools/harness/tests/mcc_scripts.c | 15 + tools/test_mcc_bitmap_flags.py | 29 +- tools/test_mcc_campaign.py | 15 + tools/test_mcc_checkpoint.py | 33 +- tools/test_mcc_geometry.py | 10 + tools/test_mcc_network.py | 2 +- tools/test_mcc_parameters.py | 2 +- tools/test_mcc_resources.py | 78 +++++ tools/test_mcc_scripts.py | 2 +- tools/test_mcc_texture_bridge.py | 13 +- tools/test_mcc_ui_teams.py | 2 + tools/test_mcc_vorbis.py | 60 +++- 40 files changed, 1323 insertions(+), 89 deletions(-) create mode 100644 port/linux/game/mcc_campaign.c create mode 100644 port/linux/game/mcc_campaign.h create mode 100644 port/linux/game/mcc_resources.c create mode 100644 port/linux/game/mcc_resources.h create mode 100644 tools/harness/tests/mcc_campaign.c create mode 100644 tools/test_mcc_campaign.py create mode 100644 tools/test_mcc_resources.py diff --git a/.github/workflows/mcc.yml b/.github/workflows/mcc.yml index e9123e9016..a337eac755 100644 --- a/.github/workflows/mcc.yml +++ b/.github/workflows/mcc.yml @@ -40,6 +40,7 @@ jobs: tools/test_mcc_cache_format.py tools/test_mcc_tag_validation.py tools/test_mcc_media.py + tools/test_mcc_resources.py tools/test_mcc_bitmap_flags.py tools/test_mcc_shader_channels.py tools/test_mcc_hud.py @@ -74,4 +75,4 @@ jobs: call "%MCC_VS_INSTALL%\Common7\Tools\VsDevCmd.bat" -host_arch=x64 -arch=x86 if errorlevel 1 exit /b 1 clang --version - python -m pytest -q tools/test_mcc_cache_format.py tools/test_mcc_tag_validation.py tools/test_mcc_media.py tools/test_mcc_bitmap_flags.py tools/test_mcc_shader_channels.py tools/test_mcc_hud.py tools/test_mcc_vorbis.py tools/test_mcc_texture_bridge.py tools/test_mcc_geometry.py tools/test_mcc_scripts.py tools/test_mcc_parameters.py tools/test_mcc_syntax.py tools/test_mcc_maps.py tools/test_mcc_menu.py tools/test_mcc_pause.py tools/test_mcc_pause_runtime.py tools/test_mcc_ui.py tools/test_mcc_ui_network.py tools/test_mcc_ui_teams.py tools/test_mcc_lifecycle.py tools/test_mcc_saved_games.py tools/test_mcc_network.py tools/test_mcc_checkpoint.py tools/test_mcc_grenades.py tools/test_cache_file_formats.py + python -m pytest -q tools/test_mcc_cache_format.py tools/test_mcc_tag_validation.py tools/test_mcc_media.py tools/test_mcc_resources.py tools/test_mcc_bitmap_flags.py tools/test_mcc_shader_channels.py tools/test_mcc_hud.py tools/test_mcc_vorbis.py tools/test_mcc_texture_bridge.py tools/test_mcc_geometry.py tools/test_mcc_scripts.py tools/test_mcc_campaign.py tools/test_mcc_parameters.py tools/test_mcc_syntax.py tools/test_mcc_maps.py tools/test_mcc_menu.py tools/test_mcc_pause.py tools/test_mcc_pause_runtime.py tools/test_mcc_ui.py tools/test_mcc_ui_network.py tools/test_mcc_ui_teams.py tools/test_mcc_lifecycle.py tools/test_mcc_saved_games.py tools/test_mcc_network.py tools/test_mcc_checkpoint.py tools/test_mcc_grenades.py tools/test_cache_file_formats.py diff --git a/README.md b/README.md index 6c0b964332..e5b9624dd0 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,9 @@ so players of the two versions can play together. Halo 1 MCC custom maps use a separate `mcc_maps/` directory and the **MCC SINGLEPLAYER** and **MCC MULTIPLAYER** map categories. The experimental -adapter supports modern, self-contained version-13 maps; it does not need -Custom Edition's shared resource files. See [MCC map support](docs/mcc_maps.md) +adapter supports modern version-13 maps with embedded resources or matching +MCC `sounds.map` and `bitmaps.map` files in `mcc_maps`. It does not use +Custom Edition's resource files. See [MCC map support](docs/mcc_maps.md) for supported features, installation and compatibility limits. On Linux and Windows, the game puts `maps/` next to the executable. On diff --git a/docs/mcc_maps.md b/docs/mcc_maps.md index 82ec3b40d4..a66aed4a31 100644 --- a/docs/mcc_maps.md +++ b/docs/mcc_maps.md @@ -16,6 +16,8 @@ maps/ Xbox game data custom_maps/ Halo Custom Edition data mcc_maps/a10.map Halo 1 MCC custom map mcc_maps/a10.txt Optional plain-text description +mcc_maps/sounds.map Matching MCC resources, when required by the package +mcc_maps/bitmaps.map Matching MCC resources, when required by the package ``` The level identity is `mcc_maps\a10`. The filename is retained even when @@ -24,6 +26,13 @@ an example: its filename is `a10.map`, while its header names `mercury_falling`. The namespaces prevent it replacing either Xbox `a10` or Custom Edition `custom_maps\a10`. +Some campaigns, including the supplied Combat Revolved package, keep their +audio and textures in separate resource files. Install the matching files +in `mcc_maps` alongside the levels. A filename alone does not establish a +match: the loader verifies each resource's indexed name and byte range. +It never searches `maps` or `custom_maps` for replacements. Packages that +require different resource files must use separate game-data installations. + Open the map-kind chooser and select **MCC SINGLEPLAYER** for campaign maps or **MCC MULTIPLAYER** for multiplayer maps. The cache header's scenario type determines the category, independently of the filename; both lists @@ -47,9 +56,9 @@ The catalog scans only `mcc_maps`, reads each candidate's 2,048-byte header, and refreshes when a map menu opens. It accepts at most 1,024 entries, with filenames of at most 54 characters before `.map`. A description is optional; the default identifies the scenario type. MCC thumbnail sidecars, Workshop -downloads and MCC package manifests are not implemented. Supported maps -contain their own resources; do not copy `bitmaps.map`, -`sounds.map` or `loc.map` from Custom Edition into `mcc_maps`. +downloads and MCC package manifests are not implemented. Maps may embed +their resources or reference matching MCC bitmap/sound payload files as +described above. Custom Edition resource files are not substitutes. There is no dependency on the Custom Edition setting or its resource directories. @@ -58,8 +67,8 @@ stage in `debug.txt` and returns a menu error. Matching the header checksum can detect that a network host selected a different map version; it is not a recomputation or authentication of the map's contents. Network support uses OpenCE's game protocol, not the MCC game client or its matchmaking. -This contribution uses network protocol 26 for the additional MCC inventory -message and grenade action values. Every participant needs a matching build +This contribution uses network protocol 27 for the additional MCC inventory +and campaign-state messages and grenade action values. Every participant needs a matching build and map data; older builds are rejected even when playing non-MCC maps. ### In-game pause menus @@ -144,6 +153,7 @@ are not an MCC implementation dependency. The independent components are: | `port/linux/src/mcc_memory.c` | Separate 64 MiB linked tag window; never replaces an existing allocation. | | `port/linux/game/mcc_geometry.c` | New model descriptors, model vertex/index conversion, external BSP vertex streams and node palettes. | | `port/linux/game/mcc_audio.c` | MCC sound decoding, resampling and Xbox ADPCM encoding into an MCC virtual stream. | +| `port/linux/game/mcc_resources.c` | MCC-only indexed sound/bitmap resource files, names, ranges and file lifetime. | | `port/linux/game/mcc_vorbis.c` | Private namespaced Vorbis decoder, bounded Ogg validation and empty residue vector compatibility. | | `port/linux/game/mcc_bitmaps.c` | MCC pixel layout, BC7 decoding and shader/HUD channel normalization into an MCC virtual stream. | | `port/linux/game/mcc_texture_cache.c` and `port/linux/src/mcc_texture_bridge.c` | Direct bindings of checked MCC pixel allocations and lazy GPU textures, independent of the Xbox/CE staging cache. | @@ -151,13 +161,14 @@ are not an MCC implementation dependency. The independent components are: | `port/linux/game/mcc_hud.c` | MCC bitmap/placement scaling, including nested weapon and grenade HUD items. | | `port/linux/game/mcc_hud_draw.c` | MCC canvas, glyph geometry and text advances; neutral dispatch for Xbox/CE. | | `port/linux/game/mcc_scripts.c` | Name-based MCC function/global linking and supported MCC native functions. | +| `port/linux/game/mcc_campaign.c` | Local-player/authority queries, gravity, campaign markers and scoped global permissions. | | `port/linux/game/mcc_syntax.c` | MCC's 32,767-slot syntax validation and traversal workspace. | | `port/linux/game/mcc_script_parameters.c` and `mcc_script_runtime.inl` | MCC parameter metadata/scopes and interpreter frames inside the existing HS stack. | | `port/linux/game/mcc_objects.c` | MCC multiplayer vehicle placement masks. | | `port/linux/game/mcc_grenades.c` | MCC slots 2/3, salted unit inventories, pickup/throw/drop behavior and HUD. | | `port/linux/game/mcc_player.c` | MCC grenade selection/action checks and starting-profile counts. | -| `port/linux/game/mcc_network.c` | MCC-only host inventory messages and client reconciliation. | -| `port/linux/game/mcc_checkpoint.c` | MCC inventory snapshot inside proven-unused CPU save memory. | +| `port/linux/game/mcc_network.c` | MCC host inventory and campaign-state messages over the existing distributed transport. | +| `port/linux/game/mcc_checkpoint.c` | MCC inventory/campaign snapshot inside proven-unused CPU save memory. | | `port/linux/game/mcc_tag_validate.c` | Independent range/ownership state while applying stock tag schemas and their validators. | Shared engine entry points have additive MCC dispatch for an MCC-namespaced @@ -183,7 +194,7 @@ live in a separately owned table keyed by full salted unit handles. Selection remains in the native unit fields. MCC profile, input, HUD, pickup, throwing and inventory-network routes handle these slots. -MCC saves put the additional counts into a 65,640-byte slot at the CPU +MCC saves put the additional counts and campaign state into a 66,392-byte slot at the CPU arena's end only if the allocator's current high-water mark leaves that entire slot unused. No `game_state_malloc` call, pool capacity, native allocation checksum or save-image size changes. Native checkpoint, core @@ -193,6 +204,20 @@ footer adds version, canonical map identity, cache checksum and payload CRC32. Before accepting the image, the adapter checks the payload and remaps incoming object-header pointers to verify every salted unit record. Unload restores the original unused bytes, including nonzero contents. +The version-2 payload includes gravity, navigation markers, dialogue gain +and the campaign's infinite-ammunition flag. Version-1 inventory snapshots +remain readable, with the new campaign state initialized to its baseline. +The footer remains at its original address. Images are validated before +restoring either state. + +Campaign snapshots use the existing OpenCE host/client transport, authority +checks and stale-tick filtering. They are sent only in MCC games; the native +inventory and co-op message layouts are unchanged. Protocol version 27 is +required on every peer because this adds message type 84 to the earlier +version-26 contribution's inventory and grenade-action extensions. +This is an additional message in OpenCE's protocol, not an MCC networking +stack or compatibility with the retail MCC client. Client cheat restrictions +remain in force; only the host decides ammunition consumption. This is an independently written implementation of documented wire formats, informed by reading the existing engine's public interfaces and schemas. @@ -268,12 +293,18 @@ their complete 2-by-2 and 1-by-1 source mip blocks for those samples, while the GPU descriptor retains its native mip limit. Unload clears only CPU base pointers that still name the MCC-owned allocation. -The audio adapter accepts embedded PCM16, Xbox ADPCM and Ogg Vorbis +The audio adapter accepts embedded or externally indexed PCM16, Xbox ADPCM and Ogg Vorbis permutations. It uses independent bounds checks and codec state. Mono output uses the game's 22,050 Hz format; stereo output follows the admitted 22,050/44,100 Hz tag rate. Necessary conversions produce an MCC-only ADPCM stream. Unsupported or damaged audio rejects the map instead of silently removing sound. +External ADPCM that already matches the native sample rate is validated +and retained byte-for-byte. The independent resource reader checks the +16-byte resource header, bounded names/index tables, resource type and +payload ranges. Sound names identify the tag, pitch range and permutation; +bitmap names identify the tag and image. Indexed whole-tag replacements +are still unsupported. Compiled script function and engine-global indices are linked by the names retained in the script strings. The adapter provides @@ -286,6 +317,22 @@ it does not implement the MCC telemetry service. The boolean result retains the conditional `sleep` calls used in shipped and Ruby campaign scripts. It does not call `core_save_name`: that Xbox function writes a raw debug save, and remains unchanged and unavailable to map scripts. + +Additional MCC campaign operations include `local_players`, +`game_is_authoritative`, `objects_distance_to_position`, `sleep_forever` +(current or named script), gravity setting/reset and breadcrumb navigation +at positions, flags or objects. Local players excludes remote and dead +units. Sleep uses the existing interpreter stack and native wake semantics. +Navigation has eight independently owned team markers and uses the native +HUD drawing services; the host replicates the complete marker state and +gravity periodically, including after changes and late joins. + +The two recognized `debug_ice_cream_flavor_status_*` globals for IWHBYD and +Grunt Birthday Party are read-only false values. This does not implement +skull gameplay, and scripts trying to set them are refused. MCC maps may +set `sound_gain_under_dialog` and `cheat_infinite_ammo`; the adapter captures +their previous values and restores them on unload. The native global table +and Xbox/CE write permissions remain unchanged. Compiled MCC static/stub scripts support up to 16 typed parameters. Signatures, scoped local references and call arguments are checked before loading. The MCC interpreter extension keeps arguments, mutable local @@ -339,11 +386,25 @@ references and 8,325 distinct streams: all decoded successfully and every sample differed from libsndfile/libvorbis by at most one signed-16-bit unit. These codec checks do not establish a complete campaign playthrough. +Solar Flare and Spasm Playground also use valid final Vorbis granule trims +across long/short window transitions. MCC decoding retains the requested +PCM frames while validating the remaining packets and a bounded overlap +tail. Page checksums, sequence, monotonic granules and decoder errors are +still checked. A comparison of 247 permutations from the two reported +sound tags matched libsndfile/libvorbis within one signed-16-bit unit. + +Combat Revolved's material-less model part retains the native renderer's +skip behavior. Its 8192-pixel credits strip uses the MCC texture route; +the legacy texture cache's size policy is unchanged. Each bitmap tag has +its own virtual stream with exact tag-handle checks, allowing the total +decoded texture storage to exceed the former single 512 MiB offset range. + ## Current boundaries - Modern uncompressed version-13 maps are the target. Earlier Anniversary - chunk-compressed caches, Halo 2 or later MCC maps, external indexed tags, - and shared external bitmap/sound resource files are not supported. + chunk-compressed caches, Halo 2 or later MCC maps and external indexed tags + are not supported. Indexed MCC bitmap/sound payload resources are supported + when matching files are installed as described above. - The portable inspector accepts files up to `INT32_MAX`; runtime raw files must be smaller than `0x50000000` bytes because higher offsets identify MCC virtual resources. Runtime tag windows must be free, 64 KiB-aligned, @@ -354,10 +415,12 @@ These codec checks do not establish a complete campaign playthrough. currently admits at most 64 nodes per model, 22 palette nodes per part, 256 geometries per model and 128 parts per geometry. Each part's vertex and triangle counts must be nonzero and at most 65,535. -- Non-volume bitmap dimensions are limited to 4,096 by 4,096 with depth one; - volumes are limited to 512 by 512 by 256. There are at most 12 +- Non-volume bitmap dimensions are limited to 8,192 by 8,192 with depth one; + volumes are limited to 512 by 512 by 256. There are at most 13 mip levels after the base level. Individual raw/decoded allocations are - capped at 128 MiB; the normalized bitmap stream is smaller than 512 MiB. + capped at 128 MiB; aggregate normalized bitmap storage is capped at 1 GiB, + with each tag's virtual stream smaller than 512 MiB. Actual allocation and + GPU texture-size support remain required, especially on mobile devices. Linear textures must be two-dimensional, unmipped and uncompressed. Audio permutations are capped at 64 MiB of input and 16,777,216 decoded frames, with at most 256 MiB of converted audio. @@ -411,6 +474,56 @@ C runtime (`gcc-multilib`). The existing `tools/harness` suite also checks legacy engine behavior. A passing parser or build alone does not establish map playability. +### Solar Flare, Spasm and Combat Revolved compatibility checks + +The October 10 compatibility changes were checked in the development build +against all 16 supplied +maps: Solar Flare `m0` through `m3`, Spasm Playground, and Combat Revolved +`a10`, `a30`, `a50`, `b30`, `b40`, `c10`, `c20`, `c40`, `d20`, `d40` and +`credits`. Each passed a bounded native Windows x86 load with rendering +and opening scripts enabled. Combat Revolved used its matching external +`sounds.map` and `bitmaps.map`; source assets remained read-only and were +not added to the repository. These are initial-load checks, not complete +campaign playthroughs or automatic level-progression tests. + +The production Vorbis decoder was compared with libvorbis through +libsndfile for all 158 permutations in Solar Flare's reported sound tag +and all 89 in Spasm's. All decoded, with a maximum difference of one +signed 16-bit PCM unit. Generated tests also cover legitimate granule +trimming, long/short block transitions, damaged granules, CRC errors and +truncated input; accepting these valid streams does not disable integrity +checks. + +A single native process loaded Solar Flare, saved changed gravity, +dialogue gain, infinite-ammunition state and a breadcrumb marker, changed +them again, and restored the saved state through both checkpoint revert +and core load. Read-only inspection of that test process confirmed the +restored MCC snapshot and live gravity/gain values. The same process then +loaded Xbox `a10`, CE `hugeass`, and Solar Flare again. MCC state was cleared +on the legacy maps, their original gravity/gain restored, and the MCC-only +functions were unavailable there. The process exited normally. + +A bounded two-process cooperative run used the existing host/join path +and confirmed equal gravity, dialogue gain and breadcrumb state on host +and client after host changes, followed by a synchronized gravity reset. +Both exited normally. This tests the new state message on loopback; it +does not establish Internet reliability, every campaign's co-op behavior, +or a full campaign playthrough. + +After applying the fixes to the OpenCE contribution, the combined MCC, +existing cache-format and BMP suite passed 738 tests, with six +optional-fixture skips, and the Windows x86 release build passed. +That contribution build also passed targeted rendered loads of Solar Flare +`m0`, Combat Revolved `b40` and Spasm Playground. Checkpoint/core restoration, +the MCC-to-Xbox-to-CE-to-MCC transition, and the two-process cooperative +gravity/gain/marker checks were repeated on that build successfully. + +Linux and Android builds of these particular changes have not been verified +locally; the Windows build configuration did not provide an Android target. +Earlier CI results above predate these changes. The MCC validation workflow +includes the new portable resource-reader cases and native Windows campaign +cases so they run with the existing checks when the branch is published. + Run the isolated tests with a C compiler and Python/pytest available: ```text @@ -426,6 +539,7 @@ python -m pytest -q tools/test_mcc_menu.py python -m pytest -q tools/test_mcc_saved_games.py python -m pytest -q tools/test_mcc_checkpoint.py tools/test_mcc_grenades.py tools/test_mcc_network.py python -m pytest -q tools/test_mcc_parameters.py tools/test_mcc_scripts.py tools/test_mcc_syntax.py +python -m pytest -q tools/test_mcc_campaign.py tools/test_mcc_resources.py python -m pytest -q tools/test_mcc_lifecycle.py ``` diff --git a/port/linux/game/mcc_audio.c b/port/linux/game/mcc_audio.c index 4ccdd5fe6c..aacb0e35b4 100644 --- a/port/linux/game/mcc_audio.c +++ b/port/linux/game/mcc_audio.c @@ -5,6 +5,7 @@ #include "errors.h" #include "sound/sound_definitions.h" #include "mcc_runtime.h" +#include "mcc_resources.h" #include "mcc_vorbis.h" #include #include @@ -169,10 +170,31 @@ static int mcc_audio_store(struct mcc_audio_storage *storage, struct mcc_pcm con return 1; } +/* Keep validated external ADPCM byte-for-byte, avoiding a lossy round trip. */ +static int mcc_audio_store_raw(struct mcc_audio_storage *storage, + unsigned char const *bytes,struct sound_permutation *permutation) +{ + uint32_t size=(uint32_t)permutation->samples.size; + if (size>MCC_AUDIO_LIMIT-storage->used) return 0; + if (storage->used+size>storage->capacity) { + uint32_t capacity=(storage->used+size+0xFFFFF)&~0xFFFFFu; + unsigned char *larger=realloc(storage->bytes,capacity); + if (!larger) return 0; + storage->bytes=larger;storage->capacity=capacity; + } + memcpy(storage->bytes+storage->used,bytes,size); + permutation->samples.file_offset=MCC_AUDIO_BASE+storage->used; + permutation->samples.pad&=~1; + storage->used+=size; + return 1; +} + int mcc_audio_prepare(struct mcc_runtime *runtime) { struct mcc_audio_storage *storage; uint32_t index,converted=0; + long failed_range=NONE,failed_permutation=NONE,failed_codec=NONE; + char const *stage="sound header", *tag_name="(unavailable)"; if (runtime->source.size>=MCC_AUDIO_BASE) return 0; storage=malloc(sizeof(*storage)); if (!storage) return 0; @@ -184,6 +206,10 @@ int mcc_audio_prepare(struct mcc_runtime *runtime) uint32_t range_index,rate,output_rate; int channels; if (entry[0]!=(uint32_t)SOUND_DEFINITION_TAG) continue; + failed_range=failed_permutation=failed_codec=NONE; + stage="sound header"; + tag_name=mcc_runtime_pointer(runtime,entry[4],256); + if (!tag_name || !memchr(tag_name,0,256)) tag_name="(unavailable)"; sound=mcc_runtime_pointer(runtime,entry[5],sizeof(*sound)); if (!sound || sound->encoding<0 || sound->encoding>1 || sound->sample_rate<0 || sound->sample_rate>1) goto failed; channels=sound->encoding+1; rate=sound->sample_rate ? 44100 : 22050; @@ -192,15 +218,19 @@ int mcc_audio_prepare(struct mcc_runtime *runtime) struct sound_pitch_range *range=mcc_runtime_pointer(runtime, (uint32_t)sound->pitch_ranges.address+range_index*sizeof(*range),sizeof(*range)); uint32_t permutation_index; + failed_range=range_index;stage="pitch range"; if (!range) goto failed; for (permutation_index=0;permutation_index<(uint32_t)range->permutations.count;permutation_index++) { struct sound_permutation *permutation=mcc_runtime_pointer(runtime, (uint32_t)range->permutations.address+permutation_index*sizeof(*permutation),sizeof(*permutation)); unsigned char *data; struct mcc_pcm pcm; - int ok; - if (!permutation || permutation->samples.size<0 || permutation->samples.size>0x4000000 || - (permutation->samples.pad&1)) goto failed; + int ok,external; + char resource_name[288]; + failed_permutation=permutation_index;stage="permutation header"; + failed_codec=permutation ? permutation->compression : NONE; + if (!permutation || permutation->samples.size<0 || permutation->samples.size>0x4000000) goto failed; + external=(permutation->samples.pad&1)!=0; permutation->unknown0=NONE; permutation->unknown1=0; permutation->unknown2=entry[3]; @@ -209,16 +239,30 @@ int mcc_audio_prepare(struct mcc_runtime *runtime) permutation->sample_buffer_size=0; if (!permutation->samples.size) continue; data=malloc(permutation->samples.size); + stage="input allocation"; if (!data) goto failed; - if (!mcc_runtime_read(runtime,permutation->samples.file_offset,permutation->samples.size,data)) { + stage="sample read"; + if (external) { + stage="external samples (matching mcc_maps/sounds.map required)"; + snprintf(resource_name,sizeof(resource_name),"%s__%lu__%lu",tag_name,range_index,permutation_index); + ok=mcc_resources_read(runtime,2,resource_name,permutation->samples.file_offset,permutation->samples.size,data); + } else ok=mcc_runtime_read(runtime,permutation->samples.file_offset,permutation->samples.size,data); + if (!ok) { free(data); goto failed; } + stage="codec decode or sample format"; ok=mcc_pcm_open(data,permutation->samples.size,permutation->compression,channels,rate,&pcm); - free(data); if (ok && (permutation->compression!=1 || rate!=output_rate)) { + stage="ADPCM output allocation or encoding"; ok=mcc_audio_store(storage,&pcm,channels,output_rate,permutation); if (ok) converted++; + } else if (ok && external) { + stage="external ADPCM allocation"; + ok=mcc_audio_store_raw(storage,data,permutation); + if (ok) converted++; } + free(data); + if (ok) permutation->samples.pad&=~1; mcc_pcm_close(&pcm); if (!ok) goto failed; } @@ -229,7 +273,8 @@ int mcc_audio_prepare(struct mcc_runtime *runtime) error(_error_silent,"mcc audio: %lu permutations converted, %lu ADPCM bytes",converted,storage->used); return 1; failed: - error(_error_silent,"mcc audio: unsupported or damaged sound tag #%lu; map refused",index); + error(_error_silent,"mcc audio: sound tag #%lu '%s', range %ld, permutation %ld, codec %ld: %s; map refused", + index,tag_name,failed_range,failed_permutation,failed_codec,stage); return 0; } diff --git a/port/linux/game/mcc_bitmaps.c b/port/linux/game/mcc_bitmaps.c index f88757100d..7dbab437c3 100644 --- a/port/linux/game/mcc_bitmaps.c +++ b/port/linux/game/mcc_bitmaps.c @@ -8,6 +8,7 @@ #include "interface/unit_hud_interface_definition.h" #include "rasterizer/rasterizer_swizzle.h" #include "mcc_runtime.h" +#include "mcc_resources.h" #include #include #define BCDEC_STATIC @@ -17,18 +18,20 @@ #define MCC_BITMAP_BASE 0x60000000u #define MCC_BITMAP_LIMIT 0x1FFFFFFFu #define MCC_BITMAP_ALLOCATION_LIMIT 0x08000000u +#define MCC_BITMAP_STORAGE_LIMIT 0x40000000u struct mcc_texture { struct bitmap_data *bitmap; uint32_t handle, offset, size; unsigned char *pixels; unsigned channels; + uint32_t resource_handle, resource_index; }; struct mcc_bitmap_clone { uint32_t source, target; unsigned channels; }; struct mcc_textures { struct mcc_texture *items; struct mcc_bitmap_clone *clones; - uint32_t count, end, capacity, clone_count, tag_capacity; + uint32_t count, end, capacity, clone_count, tag_capacity, stream_end; }; static void *mcc_bitmap_tag(struct mcc_runtime *runtime,uint32_t handle,uint32_t group,uint32_t size) @@ -114,6 +117,7 @@ static int mcc_bitmap_specialize(struct mcc_runtime *runtime,struct mcc_textures if (textures->count>=textures->capacity) return 0; texture=&textures->items[textures->count++]; texture->bitmap=&bitmaps[i];texture->handle=new_handle;texture->channels=channels; + texture->resource_handle=*handle;texture->resource_index=i; } textures->clones[textures->clone_count].source=*handle; textures->clones[textures->clone_count].target=new_handle; @@ -417,10 +421,10 @@ static int mcc_texture_prepare(struct mcc_runtime *runtime,struct mcc_textures * int convert=bitmap->format==18 || texture->channels==2 || texture->channels==4 || ((bitmap->flags&16) && bitmap->format>=14 && bitmap->format<=17); int bytes_per_pixel=mcc_bitmap_bytes(bitmap->format); - uint32_t input_offset=0,output_offset=0,hardware_size,allocation; + uint32_t input_offset=0,output_offset=0,hardware_size,allocation,stream_offset=0,i; if (bitmap->width<=0 || bitmap->height<=0 || bitmap->depth<=0 || bitmap->type<0 || bitmap->type>2 || - bitmap->width>4096 || bitmap->height>4096 || bitmap->depth>512 || bitmap->mipmap_count<0 || bitmap->mipmap_count>12 || - bitmap->pixels_size<=0 || (bitmap->flags&0x100) || (!bytes_per_pixel && (bitmap->format<14 || bitmap->format>18))) return 0; + bitmap->width>8192 || bitmap->height>8192 || bitmap->depth>512 || bitmap->mipmap_count<0 || bitmap->mipmap_count>13 || + bitmap->pixels_size<=0 || (!bytes_per_pixel && (bitmap->format<14 || bitmap->format>18))) return 0; if ((bitmap->type==1 && (bitmap->width>512 || bitmap->height>512 || bitmap->depth>256)) || (bitmap->type!=1 && bitmap->depth!=1)) return 0; if (texture->channels && (texture->channels&(texture->channels-1))) { @@ -464,11 +468,35 @@ static int mcc_texture_prepare(struct mcc_runtime *runtime,struct mcc_textures * * native hardware stops at a 4-texel dimension. Own enough storage for * both contracts, including the 2D tail sampled by CPU object lighting. */ allocation=(MAX(hardware_size,(uint32_t)(convert ? decoded_size : raw_size))+127u)&~127u; - if (!hardware_size || allocation>MCC_BITMAP_ALLOCATION_LIMIT || allocation>MCC_BITMAP_LIMIT-textures->end) return 0; + /* The cache read contract includes the bitmap tag handle. Give each tag + * its own bounded virtual stream; the aggregate decoded storage can then + * exceed the old 512 MiB offset window without signed offsets or aliases + * between descriptors within one tag. Reads still require exact handles. */ + for (i=0;icount;i++) { + struct mcc_texture const *previous=&textures->items[i]; + if (previous->pixels && previous->handle==texture->handle) + stream_offset=MAX(stream_offset,previous->offset-MCC_BITMAP_BASE+previous->size); + } + if (!hardware_size || allocation>MCC_BITMAP_ALLOCATION_LIMIT || + allocation>MCC_BITMAP_LIMIT-stream_offset || allocation>MCC_BITMAP_STORAGE_LIMIT-textures->end) { + error(_error_silent,"mcc bitmap: storage limit (used %lu, requested %lu)",textures->end,allocation); + return 0; + } raw=malloc((size_t)raw_size); pixels=malloc(allocation); if (pixels) memset(pixels,0,allocation); - if (!raw || !pixels || !mcc_runtime_read(runtime,bitmap->pixels_offset,(uint32_t)raw_size,raw)) goto failed; + if (!raw || !pixels) goto failed; + if (bitmap->flags&0x100) { + uint32_t *entry=(uint32_t *)(runtime->tag_index+(texture->resource_handle&0xFFFF)*32); + char const *name=mcc_runtime_pointer(runtime,entry[4],256); + char resource_name[272]; + if (!name || !memchr(name,0,256)) goto failed; + snprintf(resource_name,sizeof(resource_name),"%s_%lu",name,(unsigned long)texture->resource_index); + if (!mcc_resources_read(runtime,1,resource_name,bitmap->pixels_offset,(uint32_t)raw_size,raw)) { + error(_error_silent,"mcc bitmap: external '%s' requires matching mcc_maps/bitmaps.map",resource_name); + goto failed; + } + } else if (!mcc_runtime_read(runtime,bitmap->pixels_offset,(uint32_t)raw_size,raw)) goto failed; if (convert) { for (level=0;level<=(unsigned)normalized.mipmap_count;level++) { unsigned w=MAX(bitmap->width>>level,1),h=MAX(bitmap->height>>level,1),d=MAX(bitmap->depth>>level,1),slice; @@ -491,7 +519,7 @@ static int mcc_texture_prepare(struct mcc_runtime *runtime,struct mcc_textures * bitmap->format=normalized.format; bitmap->flags=normalized.flags; bitmap->mipmap_count=normalized.mipmap_count; - bitmap->pixels_offset=MCC_BITMAP_BASE+textures->end; + bitmap->pixels_offset=MCC_BITMAP_BASE+stream_offset; bitmap->pixels_size=allocation; bitmap->tag_index=texture->handle; bitmap->cache_block_index=NONE; @@ -499,6 +527,7 @@ static int mcc_texture_prepare(struct mcc_runtime *runtime,struct mcc_textures * bitmap->base_address=NULL; texture->offset=bitmap->pixels_offset;texture->size=allocation;texture->pixels=pixels; textures->end+=allocation; + textures->stream_end=MAX(textures->stream_end,stream_offset+allocation); return 1; failed: if (raw) free(raw); @@ -541,6 +570,7 @@ int mcc_bitmaps_prepare(struct mcc_runtime *runtime) if (textures->count>=runtime->report.bitmap_count) return 0; texture=&textures->items[textures->count++]; texture->bitmap=&bitmaps[n];texture->handle=entry[3]; + texture->resource_handle=entry[3];texture->resource_index=n; } } if (!mcc_bitmap_usages(runtime,textures)) { @@ -574,7 +604,7 @@ int mcc_bitmaps_read(struct mcc_runtime *runtime,long tag,uint32_t offset,uint32 uint32_t mcc_bitmaps_stream_end(struct mcc_runtime *runtime) { struct mcc_textures *textures=runtime->bitmaps; - return textures && textures->end ? MCC_BITMAP_BASE+textures->end : (uint32_t)runtime->source.size; + return textures && textures->stream_end ? MCC_BITMAP_BASE+textures->stream_end : (uint32_t)runtime->source.size; } int mcc_bitmaps_contains(struct mcc_runtime *runtime,uint32_t offset,uint32_t bytes) diff --git a/port/linux/game/mcc_cache.c b/port/linux/game/mcc_cache.c index 0da5717b65..aa7d18c477 100644 --- a/port/linux/game/mcc_cache.c +++ b/port/linux/game/mcc_cache.c @@ -8,10 +8,12 @@ #include "mcc_cache.h" #include "mcc_maps.h" #include "mcc_runtime.h" +#include "mcc_resources.h" #include "mcc_tag_validate.h" #include "mcc_checkpoint.h" #include "mcc_grenades.h" #include "mcc_script_parameters.h" +#include "mcc_campaign.h" #include "mcc_texture_cache.h" #include "mcc_ui_teams.h" #include @@ -210,6 +212,7 @@ boolean mcc_cache_contains(void const *address, long bytes) void mcc_cache_tags_unload(void) { + mcc_campaign_dispose(); mcc_ui_teams_reset(); mcc_checkpoint_dispose(); mcc_grenades_reset(); @@ -219,6 +222,7 @@ void mcc_cache_tags_unload(void) mcc_geometry_dispose(&mcc_loaded_cache); mcc_audio_dispose(&mcc_loaded_cache); mcc_bitmaps_dispose(&mcc_loaded_cache); + mcc_resources_dispose(&mcc_loaded_cache); mcc_validation_dispose(&mcc_loaded_cache); mcc_memory_release(mcc_loaded_cache.tags, mcc_loaded_cache.capacity); if (mcc_stream) fclose(mcc_stream); @@ -273,6 +277,7 @@ struct cache_file_tag_header *mcc_cache_tags_load(char const *name, void *header goto failed; } mcc_loaded = TRUE; + mcc_campaign_begin(); error(_error_silent, "mcc: loaded %s (%lu tags, %lu bytes of runtime tags)", name, (unsigned long)runtime->report.tag_count, (unsigned long)runtime->used); return (struct cache_file_tag_header *)runtime->tags; diff --git a/port/linux/game/mcc_campaign.c b/port/linux/game/mcc_campaign.c new file mode 100644 index 0000000000..203d7d6c1c --- /dev/null +++ b/port/linux/game/mcc_campaign.c @@ -0,0 +1,326 @@ +/* MCC campaign operations. No native function/global table is extended or + * replaced. State belongs to MCC snapshots, never the Xbox/CE tag data. */ +#include "mcc_campaign.h" +#include "mcc_cache.h" +#include "errors.h" +#include "hs/hs.h" +#include "hs/hs_scenario_definitions.h" +#include "hs/object_lists.h" +#include "game/game.h" +#include "game/cheats.h" +#include "game/players.h" +#include "interface/hud.h" +#include "interface/hud_definitions.h" +#include "memory/data.h" +#include "objects/objects.h" +#include "physics/physics.h" +#include "scenario/scenario.h" +#include "scenario/scenario_definitions.h" +#include "units/units.h" +#include +#include + +enum { MCC_NAV_POSITION=1, MCC_NAV_FLAG, MCC_NAV_OBJECT, MCC_NAV_LIMIT=8 }; +struct mcc_campaign_nav { + short kind, team, arrow, reserved; + long reference; + char name[64]; + real_point3d position; + real offset; +}; +struct mcc_campaign_state { + uint32_t version; + real gravity; + real dialog_gain; + uint32_t flags; + struct mcc_campaign_nav nav[MCC_NAV_LIMIT]; +}; +typedef char mcc_campaign_state_size[sizeof(struct mcc_campaign_state)==MCC_CAMPAIGN_SNAPSHOT_BYTES ? 1 : -1]; +static struct mcc_campaign_state mcc_campaign; +static real mcc_original_gravity; +static real mcc_original_dialog_gain; +static boolean mcc_original_infinite_ammo; +static boolean mcc_campaign_owned; +extern real sound_gain_under_dialog; +extern short const hs_external_global_count; + +boolean mcc_campaign_global_settable(short index) +{ + char const *name; + if (!mcc_cache_tags_loaded() || index<0 || index>=hs_external_global_count) return FALSE; + name=hs_global_external_get(index)->name; + return !csstrcasecmp(name,"sound_gain_under_dialog") || !csstrcasecmp(name,"cheat_infinite_ammo"); +} + +static int mcc_campaign_real(real value) { return value>=-1000000.0f && value<=1000000.0f; } +static real mcc_campaign_argument(long value) +{ + union { long bits; real number; } result; + result.bits=value; return result.number; +} + +void mcc_campaign_begin(void) +{ + if (!mcc_campaign_owned) { + mcc_original_gravity=global_gravity;mcc_original_dialog_gain=sound_gain_under_dialog;mcc_campaign_owned=TRUE; + mcc_original_infinite_ammo=cheat.infinite_ammo; + } + memset(&mcc_campaign,0,sizeof(mcc_campaign)); + mcc_campaign.version=1; mcc_campaign.gravity=1.0f; + mcc_campaign.dialog_gain=mcc_original_dialog_gain; + global_gravity=mcc_original_gravity; + sound_gain_under_dialog=mcc_original_dialog_gain; + cheat.infinite_ammo=mcc_original_infinite_ammo; +} + +void mcc_campaign_dispose(void) +{ + if (mcc_campaign_owned) { + global_gravity=mcc_original_gravity;sound_gain_under_dialog=mcc_original_dialog_gain; + cheat.infinite_ammo=mcc_original_infinite_ammo; + } + mcc_campaign_owned=FALSE; + memset(&mcc_campaign,0,sizeof(mcc_campaign)); +} + +void mcc_campaign_snapshot(void *out) +{ + mcc_campaign.dialog_gain=sound_gain_under_dialog; + mcc_campaign.flags=cheat.infinite_ammo ? 1u : 0u; + memcpy(out,&mcc_campaign,sizeof(mcc_campaign)); +} + +int mcc_campaign_validate(void const *in, unsigned long bytes) +{ + struct mcc_campaign_state state; + int i,j; + if (!in || bytes!=sizeof(state)) return 0; + memcpy(&state,in,sizeof(state)); + if (state.version!=1 || (state.flags&~1u) || !mcc_campaign_real(state.dialog_gain) || + !(state.gravity>=-1000.0f && state.gravity<=1000.0f)) return 0; + for (i=0;ikind<0 || nav->kind>MCC_NAV_OBJECT || nav->reserved || + !mcc_campaign_real(nav->position.x) || !mcc_campaign_real(nav->position.y) || + !mcc_campaign_real(nav->position.z) || !mcc_campaign_real(nav->offset) || + !memchr(nav->name,0,sizeof(nav->name))) return 0; + if (!nav->kind) continue; + if (nav->team<0 || nav->team>=10 || nav->arrow<0) return 0; + if (hud_globals && nav->arrow>=hud_globals->waypoint.arrows.count) return 0; + if (nav->kind==MCC_NAV_FLAG && (nav->reference<0 || + nav->reference>=global_scenario_get()->cutscene_flags.count)) return 0; + if (nav->kind==MCC_NAV_OBJECT && nav->reference==NONE) return 0; + for (j=0;jkind==other->kind && nav->team==other->team && + (nav->kind==MCC_NAV_POSITION ? !strcmp(nav->name,other->name) : nav->reference==other->reference)) return 0; + } + } + return 1; +} + +int mcc_campaign_restore(void const *in, unsigned long bytes) +{ + if (!mcc_cache_tags_loaded() || !mcc_campaign_owned || !mcc_campaign_validate(in,bytes)) return 0; + memcpy(&mcc_campaign,in,sizeof(mcc_campaign)); + global_gravity=mcc_original_gravity*mcc_campaign.gravity; + sound_gain_under_dialog=mcc_campaign.dialog_gain; + /* The native client cheat policy remains authoritative. Only the host + * runs campaign scripts and decides ammunition consumption. */ + if (game_connection()!=_game_connection_network_client) cheat.infinite_ammo=(mcc_campaign.flags&1u)!=0; + return 1; +} + +static void mcc_campaign_nav_change(short kind, short team, short arrow, long reference, + char const *name, real_point3d const *position, real offset) +{ + int i,slot=-1; + if (team<0 || team>=10 || !mcc_campaign_real(offset) || + (kind==MCC_NAV_POSITION && (!name || strlen(name)>=64 || !position || + !mcc_campaign_real(position->x) || !mcc_campaign_real(position->y) || !mcc_campaign_real(position->z)))) return; + for (i=0;ikind) { if (slot<0) slot=i; continue; } + if (nav->kind==kind && nav->team==team && + (kind==MCC_NAV_POSITION ? !strcmp(nav->name,name) : nav->reference==reference)) { slot=i; break; } + } + if (slot<0) { error(_error_silent,"mcc: campaign navigation point capacity reached"); return; } + if (arrow!=NONE && (!hud_globals || arrow<0 || arrow>=hud_globals->waypoint.arrows.count || + (kind==MCC_NAV_FLAG && (reference<0 || reference>=global_scenario_get()->cutscene_flags.count)) || + (kind==MCC_NAV_OBJECT && !object_try_and_get(reference)))) return; + memset(&mcc_campaign.nav[slot],0,sizeof(mcc_campaign.nav[slot])); + if (arrow!=NONE) { + struct mcc_campaign_nav *nav=&mcc_campaign.nav[slot]; + nav->kind=kind;nav->team=team;nav->arrow=arrow;nav->reference=reference;nav->offset=offset; + if (position) nav->position=*position; + if (name) memcpy(nav->name,name,strlen(name)+1); + } +} + +void mcc_campaign_render(short local_player) +{ + struct player_datum *player; + long player_index; + real_point3d head; + int i; + if (!mcc_cache_tags_loaded() || !mcc_campaign_owned || local_player<0 || + local_player>=MAXIMUM_NUMBER_OF_LOCAL_PLAYERS || !hud_globals || + hud_globals->waypoint.arrow_bitmap.index==NONE) return; + player_index=local_player_get_player_index(local_player); + player=player_index==NONE ? NULL : player_try_and_get(player_index); + if (!player || !unit_try_and_get(player->unit_index)) return; + unit_get_head_position(player->unit_index,&head); + for (i=0;iposition; + long reference=NONE; + short screen; + if (!nav->kind || nav->team!=player->team_index || nav->arrow>=hud_globals->waypoint.arrows.count) continue; + if (nav->kind==MCC_NAV_FLAG) { + struct scenario *scenario=global_scenario_get(); + if (nav->reference<0 || nav->reference>=scenario->cutscene_flags.count) continue; + position=((struct scenario_cutscene_flag *)scenario->cutscene_flags.address)[nav->reference].position; + } else if (nav->kind==MCC_NAV_OBJECT) { + real radius; + struct object_datum *object=object_try_and_get(nav->reference); + if (!object || TEST_FLAG(object->object.damage_flags,_object_dead_bit)) continue; + reference=nav->reference; + object_get_bounding_sphere(reference,&position,&radius); + } + position.z+=nav->offset; + screen=hud_get_nav_point_render_type(local_player,&head,&position,reference); + custom_render_nav_point(local_player,&position,nav->arrow,screen); + } +} + +static void mcc_local_players_evaluate(short function, long thread, boolean initialize) +{ + long list=object_list_new(); + short local; + (void)function;(void)initialize; + if (list!=NONE) for (local=0;localunit_index) : NULL; + if (unit && !TEST_FLAG(unit->object.damage_flags,_object_dead_bit)) object_list_add(list,player->unit_index); + } + hs_return(thread,list); +} + +static void mcc_authority_evaluate(short function, long thread, boolean initialize) +{ + short connection=game_connection(); + (void)function;(void)initialize; + hs_return(thread,connection==_game_connection_local || connection==_game_connection_network_server); +} + +static void mcc_position_distance_evaluate(short function, long thread, boolean initialize) +{ + long *args=hs_macro_function_evaluate(function,thread,initialize),object,cursor; + real_point3d target,origin; + union { real number; long bits; } result; + if (!args) return; + result.number=-1.0f; + target.x=mcc_campaign_argument(args[1]);target.y=mcc_campaign_argument(args[2]);target.z=mcc_campaign_argument(args[3]); + if (args[0]!=NONE && datum_try_and_get(object_list_header_data,args[0]) && + mcc_campaign_real(target.x) && mcc_campaign_real(target.y) && mcc_campaign_real(target.z)) { + for (object=object_list_get_first(args[0],&cursor);object!=NONE;object=object_list_get_next(args[0],&cursor)) { + if (object_try_and_get(object)) { + real distance; + object_get_origin(object,&origin);distance=distance3d(&origin,&target); + if (result.number<0 || distance=-1000.0f && scale<=1000.0f && mcc_campaign_owned && game_connection()!=_game_connection_network_client) { + mcc_campaign.gravity=scale;global_gravity=mcc_original_gravity*scale; + } + hs_return(thread,0); + } +} + +static void mcc_physics_reset_evaluate(short function, long thread, boolean initialize) +{ + (void)function;(void)initialize; + if (mcc_campaign_owned && game_connection()!=_game_connection_network_client) { + mcc_campaign.gravity=1.0f;global_gravity=mcc_original_gravity; + } + hs_return(thread,0); +} + +static void mcc_breadcrumbs_active_evaluate(short function, long thread, boolean initialize) +{ + (void)function;(void)initialize; + /* This is feature availability, not whether a marker currently exists. */ + hs_return(thread,TRUE); +} + +static void mcc_nav_evaluate(short function, long thread, boolean initialize); +static boolean mcc_sleep_parse(short function, long expression) +{ + extern struct data_array *hs_syntax_data; + struct hs_syntax_node *call=datum_try_and_get(hs_syntax_data,expression),*predicate,*argument; + (void)function; + if (!call || !(predicate=datum_try_and_get(hs_syntax_data,call->data))) return FALSE; + if (predicate->next_node_index==NONE) return TRUE; + argument=datum_try_and_get(hs_syntax_data,predicate->next_node_index); + return argument && argument->next_node_index==NONE && hs_parse(predicate->next_node_index,_hs_type_script); +} + +boolean hs_macro_function_parse(short function,long expression); +struct mcc_campaign_definition { struct hs_function_definition function;short rest[6]; }; +static struct mcc_campaign_definition mcc_campaign_functions[]={ + {{_hs_type_object_list,0,"local_players",hs_macro_function_parse,mcc_local_players_evaluate,NULL,NULL,0,{0}},{0}}, + {{_hs_type_boolean,0,"game_is_authoritative",hs_macro_function_parse,mcc_authority_evaluate,NULL,NULL,0,{0}},{0}}, + {{_hs_type_void,0,"sleep_forever",mcc_sleep_parse,mcc_sleep_forever_evaluate,NULL,NULL,0,{0}},{0}}, + {{_hs_type_real,0,"objects_distance_to_position",hs_macro_function_parse,mcc_position_distance_evaluate,NULL,NULL,4,{_hs_type_object_list}},{_hs_type_real,_hs_type_real,_hs_type_real}}, + {{_hs_type_void,0,"physics_set_gravity",hs_macro_function_parse,mcc_gravity_evaluate,NULL,NULL,1,{_hs_type_real}},{0}}, + {{_hs_type_void,0,"physics_constants_reset",hs_macro_function_parse,mcc_physics_reset_evaluate,NULL,NULL,0,{0}},{0}}, + {{_hs_type_boolean,0,"breadcrumbs_nav_points_active",hs_macro_function_parse,mcc_breadcrumbs_active_evaluate,NULL,NULL,0,{0}},{0}}, + {{_hs_type_void,0,"breadcrumbs_activate_team_nav_point_position",hs_macro_function_parse,mcc_nav_evaluate,NULL,NULL,7,{_hs_type_navpoint}},{_hs_type_enum_team,_hs_type_real,_hs_type_real,_hs_type_real,_hs_type_string,_hs_type_real}}, + {{_hs_type_void,0,"breadcrumbs_deactivate_team_nav_point",hs_macro_function_parse,mcc_nav_evaluate,NULL,NULL,2,{_hs_type_enum_team}},{_hs_type_string}}, + {{_hs_type_void,0,"breadcrumbs_activate_team_nav_point_flag",hs_macro_function_parse,mcc_nav_evaluate,NULL,NULL,4,{_hs_type_navpoint}},{_hs_type_enum_team,_hs_type_cutscene_flag,_hs_type_real}}, + {{_hs_type_void,0,"breadcrumbs_deactivate_team_nav_point_flag",hs_macro_function_parse,mcc_nav_evaluate,NULL,NULL,2,{_hs_type_enum_team}},{_hs_type_cutscene_flag}}, + {{_hs_type_void,0,"breadcrumbs_activate_team_nav_point_object",hs_macro_function_parse,mcc_nav_evaluate,NULL,NULL,4,{_hs_type_navpoint}},{_hs_type_enum_team,_hs_type_object,_hs_type_real}}, + {{_hs_type_void,0,"breadcrumbs_deactivate_team_nav_point_object",hs_macro_function_parse,mcc_nav_evaluate,NULL,NULL,2,{_hs_type_enum_team}},{_hs_type_object}} +}; + +struct hs_function_definition *mcc_campaign_function(short index) +{ + index=(short)(index-MCC_HS_CAMPAIGN_FIRST); + return index>=0 && index #include #include #define MCC_CHECKPOINT_PAYLOAD_LIMIT (8u + 8192u * 8u) +#define MCC_CHECKPOINT_TOTAL_PAYLOAD (MCC_CHECKPOINT_PAYLOAD_LIMIT + MCC_CAMPAIGN_SNAPSHOT_BYTES) #define MCC_CHECKPOINT_FOOTER_BYTES 96u -#define MCC_CHECKPOINT_SLOT_BYTES (MCC_CHECKPOINT_PAYLOAD_LIMIT + MCC_CHECKPOINT_FOOTER_BYTES) +#define MCC_CHECKPOINT_SLOT_BYTES (MCC_CHECKPOINT_TOTAL_PAYLOAD + MCC_CHECKPOINT_FOOTER_BYTES) static struct { unsigned char *original; @@ -120,15 +122,19 @@ int mcc_checkpoint_capture(void *image, unsigned long total, if (!tail || !mcc_checkpoint_identity(header) || !mcc_checkpoint_claim(tail)) goto failed; bytes = mcc_grenades_snapshot(NULL, 0); if (bytes < 8 || bytes > MCC_CHECKPOINT_PAYLOAD_LIMIT) goto failed; - footer = tail + MCC_CHECKPOINT_PAYLOAD_LIMIT; + footer = tail + MCC_CHECKPOINT_TOTAL_PAYLOAD; /* Invalidate the previous footer before any partial payload write. */ memset(footer, 0, MCC_CHECKPOINT_FOOTER_BYTES); if (mcc_grenades_snapshot(tail, bytes) != bytes || !mcc_grenades_snapshot_validate(tail, bytes)) goto failed; + mcc_campaign_snapshot(tail + bytes); + if (!mcc_campaign_validate(tail + bytes, MCC_CAMPAIGN_SNAPSHOT_BYTES)) goto failed; + bytes += MCC_CAMPAIGN_SNAPSHOT_BYTES; memcpy(footer, "MCCS", 4); - mcc_checkpoint_put(footer + 4, 1); + mcc_checkpoint_put(footer + 4, 2); mcc_checkpoint_put(footer + 8, bytes); mcc_checkpoint_put(footer + 12, mcc_checkpoint_checksum(tail, bytes)); mcc_checkpoint_put(footer + 16, (uint32_t)header->cache_file_checksum); + mcc_checkpoint_put(footer + 20, MCC_CAMPAIGN_SNAPSHOT_BYTES); memcpy(footer + 24, header->map_name, strlen(header->map_name) + 1); return 1; failed: @@ -141,23 +147,30 @@ int mcc_checkpoint_validate(void const *image, unsigned long total, { struct game_state_header const *header = image, *current = current_image; unsigned char *tail, *current_tail, *footer; - uint32_t bytes; + uint32_t bytes, campaign_bytes, version; if (!mcc_cache_tags_loaded()) return 1; tail = mcc_checkpoint_tail(image, total, used, capacity); current_tail = mcc_checkpoint_tail(current_image, total, used, capacity); if (!tail || !current_tail || !mcc_checkpoint_identity(header) || !mcc_checkpoint_identity(current) || strcmp(header->map_name, current->map_name) || header->cache_file_checksum != current->cache_file_checksum) goto failed; - footer = tail + MCC_CHECKPOINT_PAYLOAD_LIMIT; + footer = tail + MCC_CHECKPOINT_TOTAL_PAYLOAD; bytes = mcc_checkpoint_word(footer + 8); - if (memcmp(footer, "MCCS", 4) || mcc_checkpoint_word(footer + 4) != 1 || - bytes < 8 || bytes > MCC_CHECKPOINT_PAYLOAD_LIMIT || + version = mcc_checkpoint_word(footer + 4); + campaign_bytes = mcc_checkpoint_word(footer + 20); + /* The footer stays at the same CPU address. Version 1 had only inventory; + * those maps could not use these formerly unsupported campaign functions. */ + if (version == 1) tail += MCC_CAMPAIGN_SNAPSHOT_BYTES; + if (memcmp(footer, "MCCS", 4) || (version != 1 && version != 2) || + (version == 1 ? campaign_bytes != 0 : campaign_bytes != MCC_CAMPAIGN_SNAPSHOT_BYTES) || + bytes < 8 + campaign_bytes || bytes - campaign_bytes > MCC_CHECKPOINT_PAYLOAD_LIMIT || mcc_checkpoint_word(footer + 12) != mcc_checkpoint_checksum(tail, bytes) || mcc_checkpoint_word(footer + 16) != (uint32_t)header->cache_file_checksum || - mcc_checkpoint_word(footer + 20) || mcc_checkpoint_word(footer + 88) || + mcc_checkpoint_word(footer + 88) || mcc_checkpoint_word(footer + 92) || !memchr(footer + 24, 0, 64) || strcmp((char const *)footer + 24, header->map_name) || - !mcc_grenades_snapshot_validate(tail, bytes) || + !mcc_grenades_snapshot_validate(tail, bytes - campaign_bytes) || + (campaign_bytes && !mcc_campaign_validate(tail + bytes - campaign_bytes, campaign_bytes)) || !mcc_checkpoint_units(image, current_image, used, tail) || !mcc_checkpoint_claim(current_tail)) goto failed; return 1; @@ -170,12 +183,19 @@ int mcc_checkpoint_restore(void const *image, unsigned long total, unsigned long used, unsigned long capacity) { unsigned char *tail; - uint32_t bytes; + uint32_t bytes, campaign_bytes, version; if (!mcc_cache_tags_loaded()) return 1; if (!mcc_checkpoint_validate(image, total, used, capacity, image)) return 0; tail = mcc_checkpoint_tail(image, total, used, capacity); - bytes = mcc_checkpoint_word(tail + MCC_CHECKPOINT_PAYLOAD_LIMIT + 8); - if (mcc_grenades_restore(tail, bytes)) return 1; + bytes = mcc_checkpoint_word(tail + MCC_CHECKPOINT_TOTAL_PAYLOAD + 8); + campaign_bytes = mcc_checkpoint_word(tail + MCC_CHECKPOINT_TOTAL_PAYLOAD + 20); + version = mcc_checkpoint_word(tail + MCC_CHECKPOINT_TOTAL_PAYLOAD + 4); + if (version == 1) tail += MCC_CAMPAIGN_SNAPSHOT_BYTES; + if (mcc_grenades_restore(tail, bytes - campaign_bytes)) { + if (campaign_bytes) return mcc_campaign_restore(tail + bytes - campaign_bytes, campaign_bytes); + mcc_campaign_begin(); + return 1; + } error(_error_silent, "mcc checkpoint: grenade records do not match restored units"); return 0; } diff --git a/port/linux/game/mcc_geometry.c b/port/linux/game/mcc_geometry.c index b380fd1a98..4a798b2609 100644 --- a/port/linux/game/mcc_geometry.c +++ b/port/linux/game/mcc_geometry.c @@ -140,7 +140,7 @@ static int mg_part(struct mcc_runtime *r,struct model const *model,unsigned char memset(part,0,sizeof(*part)); if (!count || count>65535 || !triangles || triangles>65535 || mg_u16(wire+0x44)!=1 || mg_u16(wire+0x54)!=4 || model->nodes.count<1 || model->nodes.count>64 || - mg_u16(wire+4)>=(uint32_t)model->shaders.count || + (mg_u16(wire+4)!=0xFFFFu && mg_u16(wire+4)>=(uint32_t)model->shaders.count) || ((signed char)wire[6]!=-1 && (unsigned char)wire[6]>=part_count) || ((signed char)wire[7]!=-1 && (unsigned char)wire[7]>=part_count)) return mg_error("invalid model part descriptor"); diff --git a/port/linux/game/mcc_network.c b/port/linux/game/mcc_network.c index bdb8267134..ff0f5bb6d0 100644 --- a/port/linux/game/mcc_network.c +++ b/port/linux/game/mcc_network.c @@ -1,6 +1,8 @@ #include "cseries.h" #include "mcc_network.h" #include "mcc_grenades.h" +#include "mcc_campaign.h" +#include "mcc_cache.h" #include "game/game.h" #include "game/players.h" #include "objects/objects.h" @@ -46,6 +48,26 @@ static void mcc_network_observe_clock(long now) } word mcc_network_inventory_entry_size(void) { return sizeof(struct mcc_network_inventory); } +word mcc_network_campaign_entry_size(void) { return MCC_CAMPAIGN_SNAPSHOT_BYTES; } + +static void mcc_network_campaign_tick(long now) +{ + struct { + struct distributed_message_header header; + byte state[MCC_CAMPAIGN_SNAPSHOT_BYTES]; + } message; + if (!mcc_cache_tags_loaded() || game_connection()!=_game_connection_network_server || now%3) return; + /* Complete replacement snapshots also repair lost packets and late joins. + * OpenCE's normal host-only transport and stale-tick checks apply. */ + mcc_campaign_snapshot(message.state); + distributed_send(&message,_distributed_message_mcc_campaign,1,sizeof(message),_distributed_to_clients); +} + +void mcc_network_handle_campaign(void const *entries, short count) +{ + if (mcc_cache_tags_loaded() && game_connection()==_game_connection_network_client && count==1) + mcc_campaign_restore(entries,MCC_CAMPAIGN_SNAPSHOT_BYTES); +} void mcc_network_host_tick(void) { @@ -57,6 +79,7 @@ void mcc_network_host_tick(void) short count = 0; long now = game_time_get(); short limit = MIN(64, DATAGRAM_ENTRIES(struct mcc_network_inventory)); + mcc_network_campaign_tick(now); if (!mcc_grenades_active() || mcc_grenades_type_count() <= 2 || game_connection() != _game_connection_network_server || now % 3) return; mcc_network_observe_clock(now); diff --git a/port/linux/game/mcc_network.h b/port/linux/game/mcc_network.h index 1380012140..cfbe99e4ac 100644 --- a/port/linux/game/mcc_network.h +++ b/port/linux/game/mcc_network.h @@ -8,4 +8,6 @@ void mcc_network_host_tick(void); void mcc_network_client_tick(void); word mcc_network_inventory_entry_size(void); void mcc_network_handle_inventories(void const *entries, short count); +word mcc_network_campaign_entry_size(void); +void mcc_network_handle_campaign(void const *entries, short count); #endif diff --git a/port/linux/game/mcc_resources.c b/port/linux/game/mcc_resources.c new file mode 100644 index 0000000000..ad49859b3a --- /dev/null +++ b/port/linux/game/mcc_resources.c @@ -0,0 +1,105 @@ +/* MCC resource cache reader. The on-disk header and twelve-byte entries are + * decoded explicitly; no CE resource state or readers are shared. */ +#include "mcc_runtime.h" +#include "mcc_resources.h" +#include +#include +#include + +#define MCC_RESOURCE_METADATA_LIMIT 0x1000000u +struct mcc_resource_item { uint32_t name, size, offset; }; +struct mcc_resource_file { + FILE *stream; + char *names; + struct mcc_resource_item *items; + uint32_t count; +}; +struct mcc_resources { struct mcc_resource_file files[2]; }; + +static uint32_t mcc_resource_word(unsigned char const *p) +{ + return (uint32_t)p[0]|(uint32_t)p[1]<<8|(uint32_t)p[2]<<16|(uint32_t)p[3]<<24; +} + +static void mcc_resource_close(struct mcc_resource_file *file) +{ + if (file->stream) fclose(file->stream); + free(file->names);free(file->items);memset(file,0,sizeof(*file)); +} + +static int mcc_resource_compare(void const *a,void const *b) +{ + uint32_t x=((struct mcc_resource_item const *)a)->offset; + uint32_t y=((struct mcc_resource_item const *)b)->offset; + return xy; +} + +static int mcc_resource_open(struct mcc_resource_file *file,unsigned type) +{ + unsigned char header[16],entry[12]; + uint32_t names,index,count,names_bytes,i; + long length; + file->stream=fopen(type==1 ? "mcc_maps/bitmaps.map" : "mcc_maps/sounds.map","rb"); + if (!file->stream) return 0; + if (fseek(file->stream,0,SEEK_END) || (length=ftell(file->stream))<16 || + fseek(file->stream,0,SEEK_SET) || fread(header,1,16,file->stream)!=16) goto failed; + names=mcc_resource_word(header+4);index=mcc_resource_word(header+8);count=mcc_resource_word(header+12); + if (mcc_resource_word(header)!=type || names<16 || index(uint32_t)length || + !count || count>MCC_RESOURCE_METADATA_LIMIT/sizeof(*file->items) || + count>((uint32_t)length-index)/12) goto failed; + names_bytes=index-names; + if (!names_bytes || names_bytes>MCC_RESOURCE_METADATA_LIMIT) goto failed; + file->names=malloc(names_bytes);file->items=malloc(count*sizeof(*file->items)); + if (!file->names || !file->items || fseek(file->stream,(long)names,SEEK_SET) || + fread(file->names,1,names_bytes,file->stream)!=names_bytes) goto failed; + for (i=0;iitems[i]; + if (fread(entry,1,12,file->stream)!=12) goto failed; + item->name=mcc_resource_word(entry);item->size=mcc_resource_word(entry+4);item->offset=mcc_resource_word(entry+8); + if (item->name>=names_bytes || !memchr(file->names+item->name,0, + names_bytes-item->name<320 ? names_bytes-item->name : 320) || + item->offset<16 || item->offset>names || item->size>names-item->offset) goto failed; + } + file->count=count; + qsort(file->items,count,sizeof(*file->items),mcc_resource_compare); + return 1; +failed: + mcc_resource_close(file);return 0; +} + +int mcc_resources_read(struct mcc_runtime *runtime,unsigned type,char const *name, + uint32_t offset,uint32_t bytes,void *out) +{ + struct mcc_resources *resources; + struct mcc_resource_file *file; + uint32_t lo=0,hi; + if (!runtime || type<1 || type>2 || !name || !out || !bytes) return 0; + resources=runtime->resources; + if (!resources) { + resources=calloc(1,sizeof(*resources)); + if (!resources) return 0; + runtime->resources=resources; + } + file=&resources->files[type-1]; + if (!file->stream && !mcc_resource_open(file,type)) return 0; + hi=file->count; + while (loitems[middle].offsetcount && file->items[lo].offset==offset;lo++) { + struct mcc_resource_item const *item=&file->items[lo]; + if (bytes<=item->size && !strcmp(file->names+item->name,name)) + return !fseek(file->stream,(long)offset,SEEK_SET) && fread(out,1,bytes,file->stream)==bytes; + } + return 0; +} + +void mcc_resources_dispose(struct mcc_runtime *runtime) +{ + struct mcc_resources *resources=runtime->resources; + if (!resources) return; + mcc_resource_close(&resources->files[0]);mcc_resource_close(&resources->files[1]); + free(resources);runtime->resources=NULL; +} diff --git a/port/linux/game/mcc_resources.h b/port/linux/game/mcc_resources.h new file mode 100644 index 0000000000..c9fd47a290 --- /dev/null +++ b/port/linux/game/mcc_resources.h @@ -0,0 +1,9 @@ +/* Separate resource files belonging to an MCC map installation. */ +#ifndef MCC_RESOURCES_H +#define MCC_RESOURCES_H +#include +struct mcc_runtime; +int mcc_resources_read(struct mcc_runtime *runtime, unsigned type, + char const *name, uint32_t offset, uint32_t bytes, void *out); +void mcc_resources_dispose(struct mcc_runtime *runtime); +#endif diff --git a/port/linux/game/mcc_runtime.h b/port/linux/game/mcc_runtime.h index 6210b315f2..40c69cfb67 100644 --- a/port/linux/game/mcc_runtime.h +++ b/port/linux/game/mcc_runtime.h @@ -17,6 +17,7 @@ struct mcc_runtime { void *geometry; void *audio; void *bitmaps; + void *resources; }; /* Only ranges actually loaded into tags are accessible here. BSP data is diff --git a/port/linux/game/mcc_script_runtime.inl b/port/linux/game/mcc_script_runtime.inl index 398aa27a8d..077d8c55a6 100644 --- a/port/linux/game/mcc_script_runtime.inl +++ b/port/linux/game/mcc_script_runtime.inl @@ -2,8 +2,40 @@ * adapter uses its existing stack service, so arguments, recursion, sleep * and checkpoint state remain inside each native 512-byte thread stack. */ #include "mcc_script_parameters.h" +#include "mcc_campaign.h" long hs_cast(long thread_index, short actual_type, short desired_type, long value); +void mcc_sleep_forever_evaluate(short function, long thread_index, boolean initialize) +{ + struct hs_thread_datum *thread=hs_thread_get(thread_index); + long *script=hs_stack_allocate(thread_index,sizeof(long)); + long target=thread_index; + (void)function; + if (!script) return; + if (initialize) { + struct hs_syntax_node *call=hs_syntax_get(thread->stack->expression_index); + struct hs_syntax_node *predicate=call ? hs_syntax_get(call->data) : NULL; + if (!predicate) { hs_syntax_error(thread_index); return; } + *script=NONE; + if (predicate->next_node_index!=NONE) { + hs_evaluate(thread_index,predicate->next_node_index,script); + return; + } + } + if ((short)*script!=NONE) target=hs_find_thread_by_script((short)*script); + if (target!=NONE) { + struct hs_thread_datum *sleeper=hs_thread_get(target); + if (sleeper->sleep_until!=NONE) { + if (target!=thread_index && !TEST_FLAG(sleeper->flags,_hs_thread_sleeping_bit)) { + sleeper->previous_sleep_until=sleeper->sleep_until; + SET_FLAG(sleeper->flags,_hs_thread_sleeping_bit,TRUE); + } + sleeper->sleep_until=NONE-1; + } + } + hs_return(thread_index,0); +} + struct mcc_hs_arguments { unsigned long magic; short script, count, next, phase; diff --git a/port/linux/game/mcc_scripts.c b/port/linux/game/mcc_scripts.c index cfbfb44d35..5909f10894 100644 --- a/port/linux/game/mcc_scripts.c +++ b/port/linux/game/mcc_scripts.c @@ -15,6 +15,7 @@ #include "mcc_scripts.h" #include "mcc_script_parameters.h" #include "mcc_syntax.h" +#include "mcc_campaign.h" #include boolean hs_macro_function_parse(short index, long expression); @@ -108,7 +109,7 @@ struct hs_function_definition *mcc_script_function(short index) if (!mcc_cache_tags_loaded()) return NULL; if (index == MCC_HS_DISTANCE_TO_OBJECT) return &mcc_distance_definition.definition; if (index == MCC_HS_MISSION_SEGMENT) return &mcc_mission_segment_definition; - return NULL; + return mcc_campaign_function(index); } short mcc_script_find(char const *name) @@ -118,7 +119,7 @@ short mcc_script_find(char const *name) if (!csstrcasecmp(name, "mcc_mission_segment")) return MCC_HS_MISSION_SEGMENT; if (!csstrcasecmp(name, "player_effect_set_max_vibrate")) return hs_find_function_by_name("player_effect_set_max_rumble"); - return NONE; + return mcc_campaign_find(name); } boolean mcc_script_call_valid(struct hs_syntax_node const *call, struct data_array *syntax) @@ -130,6 +131,11 @@ boolean mcc_script_call_valid(struct hs_syntax_node const *call, struct data_arr if (!function || !(call->flags & 8) || (call->flags & 3) || !(node = datum_try_and_get(syntax, call->data))) return FALSE; next = node->next_node_index; + if (call->function_index == MCC_HS_CAMPAIGN_FIRST + 2) { + if (next == NONE) return TRUE; + node = datum_try_and_get(syntax, next); + return node && node->type == _hs_type_script && node->next_node_index == NONE; + } for (argument = 0; argument < function->parameter_count; ++argument) { node = datum_try_and_get(syntax, next); if (!node || node->type != function->parameter_types[argument]) return FALSE; @@ -144,6 +150,12 @@ static char const *mcc_script_string(char const *strings, uint32_t bytes, long o strings + offset : NULL; } +static boolean mcc_disabled_skull(char const *name) +{ + return name && (!csstrcasecmp(name,"debug_ice_cream_flavor_status_i_would_have_been_your_daddy") || + !csstrcasecmp(name,"debug_ice_cream_flavor_status_grunt_birthday_party")); +} + int mcc_scripts_prepare(struct mcc_runtime *runtime) { uint32_t address; @@ -165,6 +177,7 @@ int mcc_scripts_prepare(struct mcc_runtime *runtime) return FALSE; } nodes = (struct hs_syntax_node *)(syntax + 1); + syntax->data = nodes; for (i = 0; i < syntax->count; ++i) { struct hs_syntax_node *node = &nodes[i]; char const *name = NULL; @@ -178,15 +191,35 @@ int mcc_scripts_prepare(struct mcc_runtime *runtime) return FALSE; name = mcc_script_string(strings, scenario->hs_string_constants.size, nodes[child].string_offset); if (!name) return FALSE; + /* These status queries describe disabled, unimplemented skulls. + * They are read-only constants, not writable legacy globals. */ + if (!csstrcasecmp(name,"set")) { + struct hs_syntax_node *target = datum_try_and_get(syntax,nodes[child].next_node_index); + if (target && (target->flags & 1) && mcc_disabled_skull( + mcc_script_string(strings,scenario->hs_string_constants.size,target->string_offset))) { + error(_error_silent,"mcc: scripts cannot enable unsupported skull gameplay"); + return FALSE; + } + } if (!csstrcasecmp(name, "objects_distance_to_object")) index = MCC_HS_DISTANCE_TO_OBJECT; else if (!csstrcasecmp(name, "mcc_mission_segment")) index = MCC_HS_MISSION_SEGMENT; else if (!csstrcasecmp(name, "player_effect_set_max_vibrate")) index = hs_find_function_by_name("player_effect_set_max_rumble"); - else index = hs_find_function_by_name(name); + else { + index = mcc_campaign_find(name); + if (index == NONE) index = hs_find_function_by_name(name); + } if (index != NONE) node->function_index = nodes[child].function_index = index; } else if ((node->flags & 5) == 5 && ((uint16_t)node->data & 0x8000)) { name = mcc_script_string(strings, scenario->hs_string_constants.size, node->string_offset); if (!name) return FALSE; + if (mcc_disabled_skull(name)) { + if (node->type != _hs_type_boolean) return FALSE; + node->flags &= ~4; + node->constant_type = _hs_type_boolean; + node->data = FALSE; + continue; + } index = hs_find_global_by_name(name); if (index != NONE && ((uint16_t)index & 0x8000)) node->short_value = index; else index = NONE; diff --git a/port/linux/game/mcc_vorbis.c b/port/linux/game/mcc_vorbis.c index b7dd63a0c9..19678bcab3 100644 --- a/port/linux/game/mcc_vorbis.c +++ b/port/linux/game/mcc_vorbis.c @@ -47,7 +47,7 @@ static int mcc_ogg_validate(unsigned char const *bytes, uint32_t size, uint32_t uint32_t *payload_end, uint32_t *final_page) { uint32_t table[256], offset=0, serial=0, sequence=0, i, j; - uint32_t last_page=0, last_end=0; + uint32_t last_page=0, last_end=0, previous_granule=0; int continued=0, ended=0; *payload_end=size;*final_page=0; for (i=0;i<256;i++) { @@ -73,6 +73,12 @@ static int mcc_ogg_validate(unsigned char const *bytes, uint32_t size, uint32_t crc=(crc<<8)^table[(crc>>24)^value]; } if (crc!=mcc_ogg_u32(page+22)) return 0; + /* A page without a completed packet has no granule. All other known + * positions must progress; EOS cannot trim an earlier page's audio. */ + if (mcc_ogg_u32(page+6)!=0xFFFFFFFFu || mcc_ogg_u32(page+10)!=0xFFFFFFFFu) { + if (mcc_ogg_u32(page+10) || mcc_ogg_u32(page+6)INT_MAX || !mcc_ogg_validate(bytes,size,&expected,&payload_end,&final_page) || !expected || expected>frame_limit || expected>INT_MAX/(2*sizeof(short))) return 0; @@ -191,16 +199,35 @@ int mcc_vorbis_decode(unsigned char const *bytes, uint32_t size, uint32_t frame_ /* Normalization precedes the discarded overlap packet, which can itself use * an empty residue vector book. No Xbox/CE decoder state is accessible here. */ if (!vorbis_pump_first_frame(&stream) || stream.error) goto done; - while (countexpected-count) { + /* Vorbis I, appendix A.2. A final-packet trim plus the validated + * window-boundary offset is bounded by one maximum block. Never + * drain an unbounded remainder or suppress packet errors. */ + if (decoded-expected>(uint32_t)stream.blocksize_1 || + (!(stream.page_flag&PAGEFLAG_last_page) && + (!boundary_known || decoded-expected>(uint32_t)boundary_bias))) goto done; + retain=expected-count; + } + memcpy(pcm+count*stream.channels,packet,(size_t)retain*stream.channels*sizeof(short)); + count+=retain; } + if (count!=expected) goto done; *samples=pcm;*frames=count;*rate=stream.sample_rate;*channels=stream.channels; pcm=NULL;ok=1; done: diff --git a/port/linux/game/network_distributed.c b/port/linux/game/network_distributed.c index 05e88b9e67..fa2c347966 100644 --- a/port/linux/game/network_distributed.c +++ b/port/linux/game/network_distributed.c @@ -3445,6 +3445,7 @@ static boolean distributed_message_stale( case _distributed_message_coop_device_states: case _distributed_message_votekick_status: case _distributed_message_mcc_grenades: + case _distributed_message_mcc_campaign: break; default: return FALSE; @@ -3992,6 +3993,7 @@ void network_distributed_handle_message( case _distributed_message_player_statistics: entry_size = sizeof(struct distributed_player_statistics); break; case _distributed_message_pings: entry_size = sizeof(struct distributed_player_ping); break; case _distributed_message_mcc_grenades: entry_size = mcc_network_inventory_entry_size(); break; + case _distributed_message_mcc_campaign: entry_size = mcc_network_campaign_entry_size(); break; case _distributed_message_actor_states: entry_size = network_actors_entry_size(); break; case _distributed_message_structure_bsp: entry_size = sizeof(struct distributed_structure_bsp); break; case _distributed_message_coop_presentation: entry_size = network_coop_presentation_entry_size(); break; @@ -4169,6 +4171,9 @@ void network_distributed_handle_message( case _distributed_message_mcc_grenades: mcc_network_handle_inventories(entries, header.count); break; + case _distributed_message_mcc_campaign: + mcc_network_handle_campaign(entries, header.count); + break; case _distributed_message_object_changes: network_objects_handle_changes(entries, header.count); break; diff --git a/port/linux/game/network_distributed.h b/port/linux/game/network_distributed.h index 463083c3af..b403721a2c 100644 --- a/port/linux/game/network_distributed.h +++ b/port/linux/game/network_distributed.h @@ -120,6 +120,8 @@ enum _distributed_message_voice_config = 82, /* MCC-only extra grenade slots; ordinary inventory packets stay exact. */ _distributed_message_mcc_grenades = 83, + /* MCC-only campaign gravity and breadcrumb state. */ + _distributed_message_mcc_campaign = 84, NUMBER_OF_DISTRIBUTED_MESSAGES }; diff --git a/port/linux/include/halo_port_limits.h b/port/linux/include/halo_port_limits.h index 333a173c0d..a93f2a10a4 100644 --- a/port/linux/include/halo_port_limits.h +++ b/port/linux/include/halo_port_limits.h @@ -66,8 +66,8 @@ is newer (network_client_manager.c). A host advertises it, with its netcode, in its game's advertisement's reserved bytes (network_server_message_handler.c), which hosts built before there was a version send as zeros: version 0. Raise it with any change to what the machines send each other. */ -/* MCC adds an inventory message and additional grenade action values. */ -#define HALO_PORT_NETWORK_VERSION 26 +/* MCC inventory/grenade actions and campaign snapshots require matching peers. */ +#define HALO_PORT_NETWORK_VERSION 27 /* ... the advertisement's reserved bytes: the version (a little-endian word), then flags */ #define HALO_PORT_ADVERTISED_VERSION_OFFSET 0 diff --git a/port/linux/src/mcc_texture_bridge.c b/port/linux/src/mcc_texture_bridge.c index cf1e213acf..c107c18ada 100644 --- a/port/linux/src/mcc_texture_bridge.c +++ b/port/linux/src/mcc_texture_bridge.c @@ -47,9 +47,9 @@ void *mcc_texture_bridge_register(void const *bitmap,unsigned long format, if (!bitmap || !pixels || !bytes) return NULL; xgpu_texture_describe(format,size,&description); if (!description.width || !description.height || !description.depth || - description.width>4096 || description.height>4096 || description.depth>512 || + description.width>8192 || description.height>8192 || description.depth>512 || (description.depth>1 && (description.width>512 || description.height>512)) || - !description.levels || description.levels>13) return NULL; + !description.levels || description.levels>14) return NULL; face_bytes=xgpu_texture_face_size(&description); faces=description.cube_map ? 6 : 1; if (!face_bytes || face_bytes>bytes/faces) return NULL; diff --git a/port/linux/src/xbox_textures.c b/port/linux/src/xbox_textures.c index 1f84e74ef9..94be0287a8 100644 --- a/port/linux/src/xbox_textures.c +++ b/port/linux/src/xbox_textures.c @@ -808,8 +808,12 @@ BOOL xgpu_mcc_texture_upload(GLuint texture, GLenum target, const struct xgpu_te unsigned long *converted; unsigned long face, level; GLenum gl_error; - if (!base || !texture_size_supported(description) || !description->levels || - description->levels > 13 || !face_size || face_size > bytes / face_count || + /* MCC credits can use an 8192-pixel strip. Keep the legacy texture cache's + * size policy unchanged; this entry point owns only checked MCC pixels. */ + if (!base || !description->width || !description->height || !description->depth || + description->width > 8192 || description->height > 8192 || description->depth > 512 || + (description->depth > 1 && (description->width > 512 || description->height > 512)) || + !description->levels || description->levels > 14 || !face_size || face_size > bytes / face_count || information.kind == _texel_unknown) return FALSE; /* Account for this upload's errors independently of earlier rendering. */ gl_error = glGetError(); diff --git a/source/hs/hs_globals_external.c b/source/hs/hs_globals_external.c index a4f1b1cbdf..f187f8b2b0 100644 --- a/source/hs/hs_globals_external.c +++ b/source/hs/hs_globals_external.c @@ -2884,6 +2884,12 @@ typedef char verify_hs_external_global_settable_in_maps_size[ boolean hs_external_global_settable_by_maps( short global_index) { + /* port: MCC's separately scoped campaign controls (mcc_campaign.c). */ + { + extern boolean mcc_campaign_global_settable(short index); + if (mcc_campaign_global_settable(global_index)) + return TRUE; + } return global_index>=0 && global_index<(short)NUMBEROF(hs_external_global_settable_in_maps) && hs_external_global_settable_in_maps[global_index]; diff --git a/source/interface/hud_nav_points.c b/source/interface/hud_nav_points.c index fbd54ba945..7be58a9348 100644 --- a/source/interface/hud_nav_points.c +++ b/source/interface/hud_nav_points.c @@ -970,6 +970,11 @@ void hud_render_nav_points( } game_engine_render_nav_points(local_player_index); + /* port: MCC campaign markers have separate state (mcc_campaign.c). */ + { + extern void mcc_campaign_render(short local_player); + mcc_campaign_render(local_player_index); + } return; } diff --git a/tools/harness/tests/mcc_campaign.c b/tools/harness/tests/mcc_campaign.c new file mode 100644 index 0000000000..d47a7dbbd8 --- /dev/null +++ b/tools/harness/tests/mcc_campaign.c @@ -0,0 +1,155 @@ +/* Real MCC handlers with a deterministic local/remote player world. */ +#include "cseries.h" +#include "hs/hs.h" +#include "hs/hs_scenario_definitions.h" +#include "memory/data.h" +#include "game/game.h" +#include "game/cheats.h" +#include "game/players.h" +#include "objects/objects.h" +#include "units/units.h" +#include "interface/hud.h" +#include "interface/hud_definitions.h" +#include "scenario/scenario_definitions.h" +#include "mcc_campaign.h" +#include +#include +#include +#undef memset +#undef memcpy +#undef strcmp +#undef strlen + +#define CHECK(x) do {if(!(x)){fprintf(stderr,"line %d: %s\n",__LINE__,#x);return 1;}}while(0) +static boolean loaded=TRUE; +static short connection; +static long args[7],result,added[4]; +static int count,draws; +static real_point3d drawn; +static struct player_datum players[3]; +static struct unit_datum units[3]; +static struct scenario scenario; +static struct scenario_cutscene_flag flags[2]; +static struct hud_globals_definition hud; +static struct data_array player_array,list_array,syntax_array; +struct data_array *player_data=&player_array,*object_list_header_data=&list_array,*hs_syntax_data=&syntax_array; +struct hud_globals_definition *hud_globals=&hud; +real global_gravity=0.0035651792f; +real sound_gain_under_dialog=.7f; +struct cheat_globals cheat; +short const hs_external_global_count=3; +static struct hs_external_global_definition globals[3]={{"sound_gain_under_dialog",_hs_type_real,0,&sound_gain_under_dialog}, + {"cheat_infinite_ammo",_hs_type_boolean,0,&cheat.infinite_ammo},{"unsafe",_hs_type_real,0,NULL}}; +struct hs_external_global_definition *hs_global_external_get(short index) {return &globals[index];} +void *csmemset(void *p,long c,unsigned long n) {return memset(p,c,n);} +void *csmemcpy(void *p,void const *q,unsigned long n) {return memcpy(p,q,n);} +long csstrcasecmp(char const *a,char const *b) {return _stricmp(a,b);} +long csstrcmp(char const *a,char const *b) {return strcmp(a,b);} +unsigned long csstrlen(char const *s) {return (unsigned long)strlen(s);} +void error(short p,char const *f,...) {(void)p;(void)f;} +boolean mcc_cache_tags_loaded(void) {return loaded;} +short game_connection(void) {return connection;} +struct scenario *global_scenario_get(void) {return &scenario;} +long local_player_get_player_index(short local) {return local==0 ? 0 : local==1 ? 2 : NONE;} +void *datum_try_and_get(struct data_array *data,long index) { + if(data==&player_array)return index>=0&&index<3 ? players+index : NULL; + if(data==&list_array)return index==8 ? &list_array : NULL; + return NULL; +} +void *object_try_and_get_and_verify_type(long index,unsigned long mask) {(void)mask;return index>=0&&index<3 ? units+index : NULL;} +void *object_get_and_verify_type(long index,unsigned long mask) {return object_try_and_get_and_verify_type(index,mask);} +real_point3d *object_get_origin(long index,real_point3d *p) {p->x=(real)index*4;p->y=p->z=0;return p;} +void unit_get_head_position(long index,real_point3d *p) {object_get_origin(index,p);} +short hud_get_nav_point_render_type(short local,real_point3d const *h,real_point3d const *p,long ref) { + (void)local;(void)h;(void)p;(void)ref;return 0; +} +void custom_render_nav_point(short local,real_point3d const *p,short arrow,short type) { + (void)local;(void)arrow;(void)type;draws++;drawn=*p; +} +long object_list_new(void) {count=0;return 8;} +void object_list_add(long list,long object) {(void)list;added[count++]=object;} +long object_list_get_first(long list,long *cursor) {(void)list;*cursor=0;return 0;} +long object_list_get_next(long list,long *cursor) {(void)list;return ++*cursor==1 ? 2 : NONE;} +boolean hs_macro_function_parse(short f,long x) {(void)f;(void)x;return TRUE;} +boolean hs_parse(long x,short type) {(void)x;(void)type;return TRUE;} +long *hs_macro_function_evaluate(short f,long t,boolean initialize) {(void)f;(void)t;return initialize ? args : NULL;} +void hs_return(long thread,long value) {(void)thread;result=value;} +void mcc_sleep_forever_evaluate(short f,long t,boolean i) {(void)f;(void)t;(void)i;} +static long bits(real n) {union {real n;long b;} u;u.n=n;return u.b;} +static real number(long b) {union {real n;long b;} u;u.b=b;return u.n;} +static void call(char const *name) { + short id=mcc_campaign_find(name); + struct hs_function_definition *definition=mcc_campaign_function(id); + if(!definition)exit(3); + definition->evaluate(id,0,TRUE); +} +int main(int argc,char **argv) { + unsigned char saved[MCC_CAMPAIGN_SNAPSHOT_BYTES],bad[MCC_CAMPAIGN_SNAPSHOT_BYTES]; + real original=global_gravity; + int i; + CHECK(argc==2); + for(i=0;i<3;i++){players[i].unit_index=i;players[i].team_index=1;units[i].object.bounding_sphere_center.x=(real)i*4;} + hud.waypoint.arrows.count=2;hud.waypoint.arrow_bitmap.index=1; + scenario.cutscene_flags.address=flags;scenario.cutscene_flags.count=2; + flags[1].position.x=10; + mcc_campaign_begin(); + if(!strcmp(argv[1],"players")) { + call("local_players");CHECK(result==8&&count==2&&added[0]==0&&added[1]==2); + units[2].object.damage_flags=1u<<_object_dead_bit; + call("local_players");CHECK(count==1&&added[0]==0); + players[0].unit_index=NONE;call("local_players");CHECK(count==0); + connection=_game_connection_local;call("game_is_authoritative");CHECK(result); + connection=_game_connection_network_server;call("game_is_authoritative");CHECK(result); + connection=_game_connection_network_client;call("game_is_authoritative");CHECK(!result); + } else if(!strcmp(argv[1],"distance")) { + args[0]=8;args[1]=bits(5);args[2]=args[3]=bits(0);call("objects_distance_to_position");CHECK(number(result)==3); + args[0]=NONE;call("objects_distance_to_position");CHECK(number(result)==-1); + args[0]=8;args[1]=0x7fc00000;call("objects_distance_to_position");CHECK(number(result)==-1); + } else if(!strcmp(argv[1],"gravity")) { + args[0]=bits(.25f);call("physics_set_gravity");CHECK(global_gravity==original*.25f); + mcc_campaign_snapshot(saved); + call("physics_constants_reset");CHECK(global_gravity==original); + CHECK(mcc_campaign_restore(saved,sizeof(saved)));CHECK(global_gravity==original*.25f); + args[0]=0x7fc00000;call("physics_set_gravity");CHECK(global_gravity==original*.25f); + connection=_game_connection_network_client;args[0]=bits(2);call("physics_set_gravity");CHECK(global_gravity==original*.25f); + mcc_campaign_begin();CHECK(global_gravity==original); + connection=_game_connection_local;args[0]=bits(2);call("physics_set_gravity");mcc_campaign_dispose();CHECK(global_gravity==original); + loaded=FALSE;CHECK(!mcc_campaign_restore(saved,sizeof(saved))); + mcc_campaign_dispose();CHECK(global_gravity==original); + } else if(!strcmp(argv[1],"controls")) { + sound_gain_under_dialog=.35f;cheat.infinite_ammo=TRUE;mcc_campaign_snapshot(saved); + sound_gain_under_dialog=.88f;cheat.infinite_ammo=FALSE; + CHECK(mcc_campaign_restore(saved,sizeof(saved)) && sound_gain_under_dialog==.35f && cheat.infinite_ammo); + connection=_game_connection_network_client;cheat.infinite_ammo=FALSE; + CHECK(mcc_campaign_restore(saved,sizeof(saved)) && !cheat.infinite_ammo); + mcc_campaign_dispose();CHECK(sound_gain_under_dialog==.7f && !cheat.infinite_ammo); + } else if(!strcmp(argv[1],"navigation")) { + call("breadcrumbs_nav_points_active");CHECK(result==TRUE); + args[0]=0;args[1]=1;args[2]=bits(1);args[3]=bits(2);args[4]=bits(3);args[5]=(long)"test";args[6]=bits(.5f); + call("breadcrumbs_activate_team_nav_point_position");mcc_campaign_render(0);CHECK(draws==1&&drawn.x==1&&drawn.z==3.5f); + mcc_campaign_snapshot(saved); + args[0]=1;args[1]=(long)"test";call("breadcrumbs_deactivate_team_nav_point");draws=0;mcc_campaign_render(0);CHECK(!draws); + CHECK(mcc_campaign_restore(saved,sizeof(saved)));mcc_campaign_render(1);CHECK(draws==1); + players[2].team_index=2;draws=0;mcc_campaign_render(1);CHECK(!draws); + mcc_campaign_begin();args[0]=0;args[1]=1;args[2]=1;args[3]=bits(1); + call("breadcrumbs_activate_team_nav_point_flag");mcc_campaign_render(0);CHECK(draws==1&&drawn.x==10&&drawn.z==1); + args[0]=1;args[1]=1;call("breadcrumbs_deactivate_team_nav_point_flag");draws=0;mcc_campaign_render(0);CHECK(!draws); + args[0]=0;args[1]=1;args[2]=2;args[3]=bits(0);call("breadcrumbs_activate_team_nav_point_object"); + mcc_campaign_render(0);CHECK(draws==1&&drawn.x==8); + units[2].object.damage_flags=1u<<_object_dead_bit;draws=0;mcc_campaign_render(0);CHECK(!draws); + units[2].object.damage_flags=0;args[0]=1;args[1]=2;call("breadcrumbs_deactivate_team_nav_point_object");mcc_campaign_render(0);CHECK(!draws); + loaded=FALSE;mcc_campaign_render(0);CHECK(!draws); + } else if(!strcmp(argv[1],"validation")) { + mcc_campaign_snapshot(saved);CHECK(mcc_campaign_validate(saved,sizeof(saved))); + CHECK(!mcc_campaign_validate(saved,sizeof(saved)-1)); + memcpy(bad,saved,sizeof(saved));bad[0]=2;CHECK(!mcc_campaign_restore(bad,sizeof(bad))); + memcpy(bad,saved,sizeof(saved));bad[12]=2;CHECK(!mcc_campaign_restore(bad,sizeof(bad))); + memcpy(bad,saved,sizeof(saved));{long nan=0x7fc00000;memcpy(bad+4,&nan,4);}CHECK(!mcc_campaign_restore(bad,sizeof(bad))); + memcpy(bad,saved,sizeof(saved));bad[16]=4;CHECK(!mcc_campaign_restore(bad,sizeof(bad))); + CHECK(global_gravity==original); + CHECK(mcc_campaign_global_settable(0));CHECK(mcc_campaign_global_settable(1)); + CHECK(!mcc_campaign_global_settable(2));CHECK(!mcc_campaign_global_settable(3));CHECK(!mcc_campaign_global_settable(-1)); + loaded=FALSE;CHECK(!mcc_campaign_global_settable(0)); + } else return 2; + mcc_campaign_dispose();return 0; +} diff --git a/tools/harness/tests/mcc_network.c b/tools/harness/tests/mcc_network.c index c4f5e79b82..7e443dd2c0 100644 --- a/tools/harness/tests/mcc_network.c +++ b/tools/harness/tests/mcc_network.c @@ -23,6 +23,8 @@ typedef float real; #define _game_connection_network_server 1 #define _game_connection_network_client 2 #define _distributed_message_mcc_grenades 83 +#define _distributed_message_mcc_campaign 84 +#define MCC_CAMPAIGN_SNAPSHOT_BYTES 752 #define _distributed_to_clients 0 struct distributed_message_header {word header;byte type,count;long time;}; struct object_iterator {long index;int done;}; @@ -40,6 +42,14 @@ static word sent_size; static int known=1; static boolean local=0; static real round_trip=6; +static int campaign_sends,campaign_restores; +static byte campaign_state[MCC_CAMPAIGN_SNAPSHOT_BYTES]; +boolean mcc_cache_tags_loaded(void) { return active; } +void mcc_campaign_snapshot(void *out) { memcpy(out,campaign_state,sizeof(campaign_state)); } +int mcc_campaign_restore(void const *in,unsigned long bytes) { + if(bytes!=sizeof(campaign_state))return 0; + memcpy(campaign_state,in,bytes);campaign_restores++;return 1; +} short mcc_grenades_get(long index,short type) {return index==live_unit&&type>=0&&type=0&&typeunit:NONE;} void distributed_send(void *message,byte type,short count,word size,short destination){ + if(type==84) { if(count==1&&size==sizeof(struct distributed_message_header)+MCC_CAMPAIGN_SNAPSHOT_BYTES)campaign_sends++;return; } (void)destination;if(type!=83||size>sizeof(sent))return; memcpy(sent,message,size);sent_count=count;sent_size=size;sends++; } @@ -129,9 +140,16 @@ int main(int argc,char **argv){ rate=0;type=NONE;kinds=0;mcc_network_extra_grenade_damage(0,102,&kinds,&reach,&type,&rate);CHECK(rate==0&&type==NONE); paid=1;mcc_network_extra_grenade_damage(0,102,&kinds,&reach,&type,&rate);CHECK(rate==4&&type==2); connection=2;mcc_network_note_extra_grenade(live_unit,3);CHECK(distributed_grenade_throw(0,3)==NONE); + }else if(!strcmp(argv[1],"campaign")){ + types=2;mcc_network_host_tick();CHECK(campaign_sends==1&&sends==0); + mcc_network_handle_campaign(campaign_state,1);CHECK(campaign_restores==0); + connection=2;mcc_network_handle_campaign(campaign_state,0);mcc_network_handle_campaign(campaign_state,2); + CHECK(campaign_restores==0);mcc_network_handle_campaign(campaign_state,1);CHECK(campaign_restores==1); + active=0;mcc_network_handle_campaign(campaign_state,1);CHECK(campaign_restores==1); + connection=1;mcc_network_host_tick();CHECK(campaign_sends==1); }else if(!strcmp(argv[1],"legacy")){ active=0;mcc_network_host_tick();connection=2;mcc_network_client_tick();mcc_network_handle_inventories(&packet,1); - CHECK(sends==0&&counts[2]==3&&counts[3]==4&&counts[0]==1&&counts[1]==2); + CHECK(sends==0&&campaign_sends==0&&counts[2]==3&&counts[3]==4&&counts[0]==1&&counts[1]==2); }else return 2; return 0; } diff --git a/tools/harness/tests/mcc_script_parameters.c b/tools/harness/tests/mcc_script_parameters.c index 86203f46c3..566e4aa998 100644 --- a/tools/harness/tests/mcc_script_parameters.c +++ b/tools/harness/tests/mcc_script_parameters.c @@ -15,19 +15,22 @@ #undef memset #undef memcpy #undef strcmp +#undef strncmp #undef strcpy #define CHECK(value) do {if(!(value)){fprintf(stderr,"check failed at %d: %s\n",__LINE__,#value);return 1;}}while(0) struct hs_stack_frame {struct hs_stack_frame *previous;long expression_index;void *result;short size;byte data[2];}; struct hs_thread_datum {short identifier;byte type,flags;long script_index,sleep_until,previous_sleep_until; struct hs_stack_frame *stack;long result;byte stack_data[512];}; -static struct hs_thread_datum thread; +static struct hs_thread_datum thread,other_thread; static struct scenario *scenario; struct data_array *hs_syntax_data; static int loaded=1,errors,refs,returned,disabled; short const hs_external_global_count=443; static long last_result; -static struct hs_thread_datum *hs_thread_get(long index) {(void)index;return &thread;} +static struct hs_thread_datum *hs_thread_get(long index) {return index==1 ? &other_thread : &thread;} +#define _hs_thread_sleeping_bit 1 +static long hs_find_thread_by_script(short script) { return script==0 ? 0 : script==1 ? 1 : NONE; } boolean mcc_cache_tags_loaded(void) {return loaded;} void error(short priority,char const *message,...) {(void)priority;(void)message;} void *csmemset(void *p,long value,unsigned long size) {return memset(p,(int)value,size);} @@ -99,6 +102,33 @@ int main(int argc,char **argv) { nodes[2].flags=10;nodes[2].script_index=0;nodes[2].data=0x10003; nodes[3].type=_hs_function_name;nodes[3].next_node_index=0x10004; nodes[4].data=42; + if(!strncmp(argv[1],"sleep_",6)) { + int named=strcmp(argv[1],"sleep_self")!=0; + struct hs_thread_datum saved; + memset(&thread,0,sizeof(thread));memset(&other_thread,0,sizeof(other_thread)); + frame=(void *)(thread.stack_data+32);thread.stack=frame;frame->expression_index=0x10002; + other_thread.sleep_until=123;nodes[4].data=1; + if(!named)nodes[3].next_node_index=NONE; + if(!strcmp(argv[1],"sleep_missing"))nodes[4].data=9; + if(!strcmp(argv[1],"sleep_finished"))other_thread.sleep_until=NONE; + mcc_sleep_forever_evaluate(0,0,TRUE); + if(named) { + CHECK(!returned && other_thread.sleep_until!=NONE-1); + saved=thread;memset(&thread,0,sizeof(thread));thread=saved; + frame->size=0;mcc_sleep_forever_evaluate(0,0,FALSE); + if(!strcmp(argv[1],"sleep_missing"))CHECK(other_thread.sleep_until==123); + else if(!strcmp(argv[1],"sleep_finished"))CHECK(other_thread.sleep_until==NONE); + else { + CHECK(other_thread.sleep_until==NONE-1 && other_thread.previous_sleep_until==123); + CHECK(TEST_FLAG(other_thread.flags,_hs_thread_sleeping_bit)); + frame->size=0;mcc_sleep_forever_evaluate(0,0,TRUE); + frame->size=0;mcc_sleep_forever_evaluate(0,0,FALSE); + CHECK(other_thread.previous_sleep_until==123); + } + CHECK(thread.sleep_until==0); + } else CHECK(thread.sleep_until==NONE-1 && !thread.flags); + CHECK(returned && !errors);free(runtime.tags);return 0; + } if(!strcmp(argv[1],"count")){block[0]=17;valid=0;} else if(!strcmp(argv[1],"type")){type=_hs_type_void;memcpy(parameter+32,&type,2);valid=0;} else if(!strcmp(argv[1],"pointer")){block[1]=1;valid=0;} diff --git a/tools/harness/tests/mcc_scripts.c b/tools/harness/tests/mcc_scripts.c index a4d98c9232..b8b03c8528 100644 --- a/tools/harness/tests/mcc_scripts.c +++ b/tools/harness/tests/mcc_scripts.c @@ -31,6 +31,8 @@ static char last_event[256]; static struct object_datum mock_object; static int deleted_lists; word const hs_object_type_masks[6] = {0xFFFF, 3, 2, 4, 0x380, 0x40}; +struct hs_function_definition *mcc_campaign_function(short index) { (void)index;return NULL; } +short mcc_campaign_find(char const *name) { (void)name;return NONE; } struct scenario *global_scenario_get(void) { return NULL; } boolean ai_index_from_string(struct scenario *scenario, char const *name, long *result) { (void)scenario; @@ -165,8 +167,21 @@ int main(int argc, char **argv) { if (!strcmp(argv[1], "global")) { nodes[0].flags = 5; nodes[0].data = 0x8001; strcpy(strings, "known_global"); } + if (!strcmp(argv[1], "skull_read") || !strcmp(argv[1], "skull_wrong_type")) { + nodes[0].flags=5;nodes[0].data=0x8001;nodes[0].type=_hs_type_boolean; + strcpy(strings,"debug_ice_cream_flavor_status_grunt_birthday_party"); + if (!strcmp(argv[1], "skull_wrong_type")) {nodes[0].type=_hs_type_real;expected=0;} + } + if (!strcmp(argv[1], "skull_write")) { + strcpy(strings,"set");strcpy(strings+4,"debug_ice_cream_flavor_status_i_would_have_been_your_daddy"); + nodes[2].flags=5;nodes[2].data=0x8001;nodes[2].string_offset=4;nodes[2].type=_hs_type_boolean;expected=0; + } CHECK(mcc_scripts_prepare(&runtime) == expected); if (!expected) { free(runtime.tags); return 0; } + if (!strcmp(argv[1], "skull_read")) { + CHECK(!(nodes[0].flags&4) && nodes[0].data==FALSE && nodes[0].constant_type==_hs_type_boolean); + free(runtime.tags);return 0; + } CHECK(syntax->maximum_count == 32767); CHECK(syntax->data == nodes); if (!strcmp(argv[1], "full_capacity")) { diff --git a/tools/test_mcc_bitmap_flags.py b/tools/test_mcc_bitmap_flags.py index 104366198d..66e1c2ab49 100644 --- a/tools/test_mcc_bitmap_flags.py +++ b/tools/test_mcc_bitmap_flags.py @@ -49,6 +49,7 @@ def bitmap_converter(tmp_path_factory): #define MCC_BITMAP_BASE 0x60000000u #define MCC_BITMAP_LIMIT 0x1FFFFFFFu #define MCC_BITMAP_ALLOCATION_LIMIT 0x08000000u + #define MCC_BITMAP_STORAGE_LIMIT 0x40000000u typedef unsigned char boolean; struct bitmap_data { uint32_t signature; @@ -59,7 +60,11 @@ def bitmap_converter(tmp_path_factory): void *hardware_format,*base_address; }; struct bitmap_group {short usage;}; -struct mcc_runtime {struct bitmap_group group;unsigned char source[350000];uint32_t size,reads;}; + struct mcc_runtime {struct bitmap_group group;unsigned char source[350000];uint32_t size,reads;unsigned char *tag_index;void *bitmaps;}; + static void *mcc_runtime_pointer(struct mcc_runtime *r,uint32_t address,uint32_t size) {(void)r;(void)address;(void)size;return NULL;} + static int mcc_resources_read(struct mcc_runtime *r,unsigned type,char const *name,uint32_t offset,uint32_t bytes,void *out) { + (void)r;(void)type;(void)name;(void)offset;(void)bytes;(void)out;return 0; + } static short floor_log2(unsigned value) {short n=0;while(value>1){value>>=1;n++;}return n;} static int bitmap_format_type_valid_width(short f,short t,short v) {(void)f;(void)t;return v>0 && v<=4096;} static int bitmap_format_type_valid_height(short f,short t,short v) {return bitmap_format_type_valid_width(f,t,v);} @@ -91,7 +96,7 @@ def bitmap_converter(tmp_path_factory): source += "\n" + "\n".join(function(bitmap, name) for name in [ "mcc_bitmap_bytes", "mcc_pixel", "mcc_channels", "mcc_bitmap_decode", "mcc_morton_axis", "mcc_bitmap_pack", "mcc_bitmap_lightmap_flags", "mcc_bitmap_complete_mips", - "mcc_texture_prepare"]) + "mcc_texture_prepare", "mcc_bitmaps_read"]) source += r''' int main(int argc,char **argv) { struct bitmap_data bitmap={0},original; @@ -106,6 +111,24 @@ def bitmap_converter(tmp_path_factory): bitmap.format=6;bitmap.flags=0x1281;bitmap.pixels_size=16; texture.bitmap=&bitmap;texture.handle=7; for(i=0;i<16;i++)runtime.source[i]=(unsigned char)i; + if(!strcmp(argv[1],"tag-streams")) { + struct mcc_texture items[3]={{0}}; + struct bitmap_data bitmaps[3];unsigned char out[16]; + bitmap.flags=0x81;textures.end=0x20000000u; + textures.items=items;textures.count=3;runtime.bitmaps=&textures; + for(i=0;i<3;i++) { + bitmaps[i]=bitmap;items[i].bitmap=&bitmaps[i];items[i].handle=i==2 ? 8 : 7; + if(!mcc_texture_prepare(&runtime,&textures,&items[i]))return 12; + } + if(items[0].offset!=MCC_BITMAP_BASE || items[1].offset!=MCC_BITMAP_BASE+128 || + items[2].offset!=MCC_BITMAP_BASE || textures.end!=0x20000180u)return 13; + if(mcc_bitmaps_read(&runtime,7,MCC_BITMAP_BASE+128,16,out)!=1 || + mcc_bitmaps_read(&runtime,8,MCC_BITMAP_BASE,16,out)!=1 || + mcc_bitmaps_read(&runtime,8,MCC_BITMAP_BASE+128,1,out)!=0 || + mcc_bitmaps_read(&runtime,9,MCC_BITMAP_BASE,1,out)!=0)return 14; + for(i=0;i<3;i++)free(items[i].pixels); + return 0; + } if(!strncmp(argv[1],"tail",4)) { tail=1;runtime.group.usage=0;runtime.size=349525; bitmap.width=1024;bitmap.height=512;bitmap.format=14;bitmap.flags=0x81; @@ -170,7 +193,7 @@ def bitmap_converter(tmp_path_factory): @pytest.mark.parametrize("case", ["known", "ordinary", "wrong-group", "missing-group", "missing-environment", "unknown-flag", "extra-pixels", "short-pixels", "bad-signature", - "wrong-format", "non-power-two", "mipmaps"]) + "wrong-format", "non-power-two", "mipmaps", "tag-streams"]) def test_mcc_lightmap_conversion(bitmap_converter, case): result = subprocess.run([str(bitmap_converter), case], capture_output=True, text=True, timeout=10) assert result.returncode == 0, result.stdout + result.stderr diff --git a/tools/test_mcc_campaign.py b/tools/test_mcc_campaign.py new file mode 100644 index 0000000000..97f8e4ddc2 --- /dev/null +++ b/tools/test_mcc_campaign.py @@ -0,0 +1,15 @@ +"""Real MCC campaign handlers against local/remote players and state snapshots.""" +import subprocess +import pytest +from test_mcc_scripts import native_mcc_harness + + +@pytest.fixture(scope="module") +def campaign_tool(native_mcc_harness): + return native_mcc_harness("mcc_campaign") + + +@pytest.mark.parametrize("case", ["players", "distance", "gravity", "controls", "navigation", "validation"]) +def test_campaign_handlers(campaign_tool, case): + result = subprocess.run([str(campaign_tool), case], capture_output=True, text=True, timeout=10) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/tools/test_mcc_checkpoint.py b/tools/test_mcc_checkpoint.py index 4f01f000d5..120e80aaa6 100644 --- a/tools/test_mcc_checkpoint.py +++ b/tools/test_mcc_checkpoint.py @@ -56,6 +56,15 @@ def checkpoint_tool(tmp_path_factory): static int native_valid=1; ''' + '#include "' + (ROOT / "port/linux/game/mcc_checkpoint.c").as_posix() + '"\n' + r''' boolean mcc_cache_tags_loaded(void) {return loaded;} +static int campaign_restores; +void mcc_campaign_snapshot(void *out) {memset(out,0,MCC_CAMPAIGN_SNAPSHOT_BYTES);((unsigned char *)out)[0]=1;} +int mcc_campaign_validate(void const *in,unsigned long bytes) { + return bytes==MCC_CAMPAIGN_SNAPSHOT_BYTES && ((unsigned char const *)in)[0]==1; +} +int mcc_campaign_restore(void const *in,unsigned long bytes) { + if(!mcc_campaign_validate(in,bytes))return 0;campaign_restores++;return 1; +} +void mcc_campaign_begin(void) {campaign_restores++;} struct data_array *object_header_data; short mcc_grenades_type_count(void) {return 4;} boolean mcc_level_name(char const *name) {return !strncmp(name,"mcc_maps\\",9);} @@ -112,7 +121,7 @@ def checkpoint_tool(tmp_path_factory): memset(unit,0,sizeof(*unit));unit->identifier=1;unit->datum=(void *)(live+768);} game_state_globals.base_address=live;game_state_globals.cpu_allocation_size=used; tail=live+GAME_STATE_CPU_SIZE-MCC_CHECKPOINT_SLOT_BYTES; - footer=tail+MCC_CHECKPOINT_PAYLOAD_LIMIT; + footer=tail+MCC_CHECKPOINT_TOTAL_PAYLOAD; if(!strcmp(argv[1],"stock")) { loaded=0;strcpy(header->map_name,"levels\\a10\\a10"); game_state_save();result=game_state_image_accept(saved,sizeof(saved)); @@ -132,9 +141,17 @@ def checkpoint_tool(tmp_path_factory): } else { if(!strcmp(argv[1],"core"))game_state_save_core("test");else game_state_save(); if(writes!=1||captures!=1)return 4; - if(!strcmp(argv[1],"roundtrip")||!strcmp(argv[1],"core")) { + if(!strcmp(argv[1],"v1")) { + unsigned char *f=saved+(footer-live),*p=saved+(tail-live); + memmove(p+MCC_CAMPAIGN_SNAPSHOT_BYTES,p,payload_size); + mcc_checkpoint_put(f+4,1);mcc_checkpoint_put(f+8,payload_size); + mcc_checkpoint_put(f+12,mcc_checkpoint_checksum(p+MCC_CAMPAIGN_SNAPSHOT_BYTES,payload_size)); + mcc_checkpoint_put(f+20,0); + result=game_state_image_accept(saved,sizeof(saved)); + if(restores!=1 || campaign_restores!=1)return 12; + } else if(!strcmp(argv[1],"roundtrip")||!strcmp(argv[1],"core")) { live[4095]=33;result=game_state_image_accept(saved,sizeof(saved)); - if(restores!=1||live[4095]!=0xA5)return 5; + if(restores!=1||campaign_restores!=1||live[4095]!=0xA5)return 5; } else if(!strcmp(argv[1],"cleanup")) { mcc_checkpoint_dispose(); for(unsigned long i=0;imap_name,"mcc_maps\\other"); else if(!strcmp(argv[1],"checksum"))incoming->cache_file_checksum++; @@ -154,7 +175,7 @@ def checkpoint_tool(tmp_path_factory): struct data_array *a=(void *)(saved+512); struct object_header_datum *unit=(void *)(saved+640); p[6]=1;p[8]=0;p[9]=0;p[10]=1;p[11]=0;p[12]=2; - mcc_checkpoint_put(f+12,mcc_checkpoint_checksum(p,16)); + mcc_checkpoint_put(f+12,mcc_checkpoint_checksum(p,16+MCC_CAMPAIGN_SNAPSHOT_BYTES)); if(!strcmp(argv[1],"unit_salt"))unit->identifier=2; else if(!strcmp(argv[1],"unit_type"))unit->type=2; else if(!strcmp(argv[1],"unit_pointer"))unit->datum=(void *)(live+sizeof(live)); @@ -185,7 +206,7 @@ def checkpoint_tool(tmp_path_factory): @pytest.mark.parametrize("case", ["stock", "capacity", "capture_fail", "maximum", "oversize", "roundtrip", "core", "cleanup", "payload", "footer", "version", "reserved", "name", "checksum", "highwater", "native", "handles", - "unit_salt", "unit_type", "unit_pointer", "unit_array"]) + "unit_salt", "unit_type", "unit_pointer", "unit_array", "v1", "campaign"]) def test_mcc_checkpoint_and_native_hooks(checkpoint_tool, case): result = subprocess.run([str(checkpoint_tool), case], capture_output=True, text=True) assert result.returncode == 0, result.stdout + result.stderr + f" ({case}: {result.returncode})" diff --git a/tools/test_mcc_geometry.py b/tools/test_mcc_geometry.py index 24351e77ca..1767705e0a 100644 --- a/tools/test_mcc_geometry.py +++ b/tools/test_mcc_geometry.py @@ -102,6 +102,16 @@ def test_partial_gpu_failure_cleanup(geometry_tool, tmp_path, failure_at): assert code == 1 +@pytest.mark.parametrize("shader,expected", [(0xffff, 0), (1, 1), (0x8000, 1)]) +def test_absent_model_material_keeps_native_skip_semantics(geometry_tool, tmp_path, shader, expected): + data = geometry_map() + struct.pack_into(" +#include +#include +int main(int argc,char **argv) { + struct mcc_runtime runtime={0};unsigned char bytes[32]={0};int ok; + if(argc!=6)return 2; + ok=mcc_resources_read(&runtime,(unsigned)atoi(argv[1]),argv[2],(uint32_t)strtoul(argv[3],NULL,0), + (uint32_t)atoi(argv[4]),bytes); + if(ok)fwrite(bytes,1,(size_t)atoi(argv[4]),stdout); + mcc_resources_dispose(&runtime);mcc_resources_dispose(&runtime); + if(runtime.resources)return 3; + return ok==atoi(argv[5]) ? 0 : 4; +} +''') + exe = work / "resources.exe" + target = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld"] if sys.platform == "win32" else [] + result = subprocess.run([clang] + target + ["-std=c89", "-Wall", "-Wextra", "-Werror", "-D_CRT_SECURE_NO_WARNINGS", "-I"+str(ROOT / "port/linux/game"), + str(source), str(ROOT / "port/linux/game/mcc_resources.c"), "-o", str(exe)], capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return exe + + +def fixture_blob(kind=2): + names = b"sound\\test__0__0\0second\0" + # Deliberately reverse the directory's offset order. + return bytearray(struct.pack("<4I", kind, 24, 24+len(names), 2) + b"abcdefgh" + names + + struct.pack("<6I", 17, 4, 20, 0, 4, 16)) + + +@pytest.mark.parametrize("case", ["valid", "bitmap", "prefix", "wrong_name", "wrong_type", "truncated", + "metadata_overlap", "past_end", "names_outside", "unterminated", "large_count", "inner_offset", + "too_long", "missing", "ce_only", "wrap_offset"]) +def test_resource_reader(resource_tool, tmp_path, case): + folder = tmp_path / ("custom_maps" if case == "ce_only" else "mcc_maps") + folder.mkdir() + kind = 1 if case == "bitmap" else 2 + blob = fixture_blob(kind) + expected = case in {"valid", "bitmap", "prefix"} + offset, length, name = 16, 4, r"sound\test__0__0" + index = struct.unpack_from(" Date: Sat, 10 Oct 2026 17:54:48 +0100 Subject: [PATCH 3/8] Fix MCC campaign segment linking and startup validation (#241) Route H1A core_save_name to the MCC segment handler without changing Xbox or Custom Edition code. Add regression coverage and correct the earlier false-positive startup reports. On this contribution branch, the suite passed 746 tests with 6 skips; the Windows x86 release build and a 45-second B40 startup with delayed script output and opening dialogue passed. (cherry picked from commit 47b263f90a47d0631ddb0e754a50aa8a7119250e) --- .github/workflows/mcc.yml | 2 + docs/mcc_maps.md | 80 +++++++++++----- port/linux/game/mcc_scripts.c | 17 +++- port/linux/game/mcc_scripts.h | 1 + tools/harness/tests/mcc_scripts.c | 29 +++--- tools/mcc_native_smoke.py | 148 ++++++++++++++++++++++++++++++ tools/test_mcc_native_smoke.py | 24 +++++ tools/test_mcc_scripts.py | 2 +- 8 files changed, 266 insertions(+), 37 deletions(-) create mode 100644 tools/mcc_native_smoke.py create mode 100644 tools/test_mcc_native_smoke.py diff --git a/.github/workflows/mcc.yml b/.github/workflows/mcc.yml index a337eac755..f869ba28b6 100644 --- a/.github/workflows/mcc.yml +++ b/.github/workflows/mcc.yml @@ -11,6 +11,7 @@ on: - "port/third_party/bcdec/**" - "source/**" - "tools/test_mcc_*.py" + - "tools/mcc_native_smoke.py" - "tools/test_cache_file_formats.py" - "tools/harness/**" - ".github/workflows/mcc.yml" @@ -54,6 +55,7 @@ jobs: tools/test_mcc_ui_teams.py tools/test_mcc_ui_network.py tools/test_mcc_lifecycle.py + tools/test_mcc_native_smoke.py tools/test_mcc_saved_games.py tools/test_mcc_network.py tools/test_mcc_checkpoint.py diff --git a/docs/mcc_maps.md b/docs/mcc_maps.md index a66aed4a31..9063170cf4 100644 --- a/docs/mcc_maps.md +++ b/docs/mcc_maps.md @@ -476,15 +476,50 @@ map playability. ### Solar Flare, Spasm and Combat Revolved compatibility checks -The October 10 compatibility changes were checked in the development build -against all 16 supplied -maps: Solar Flare `m0` through `m3`, Spasm Playground, and Combat Revolved -`a10`, `a30`, `a50`, `b30`, `b40`, `c10`, `c20`, `c40`, `d20`, `d40` and -`credits`. Each passed a bounded native Windows x86 load with rendering -and opening scripts enabled. Combat Revolved used its matching external -`sounds.map` and `bitmaps.map`; source assets remained read-only and were -not added to the repository. These are initial-load checks, not complete -campaign playthroughs or automatic level-progression tests. +The initial October 10 load checks incorrectly classified all 16 supplied +maps as passing. They accepted the tag-loader's `mcc: loaded` message and +exit code zero, but missed `type is inconsistent with usage` during the +subsequent script initialization. The retained logs contained this fatal +error for Combat Revolved `a10`, `a50`, `b30`, `b40`, `c10` and `d20`. +Those results did not establish successful startup and are withdrawn. + +B40's first failing expression called `core_save_name` as a boolean. +The legacy Xbox function with that name returns void and writes a debug +save. H1A repurposed it for campaign segments, as documented in the +[public scripting reference](https://c20.reclaimers.net/h1/scripting/#functions). +The MCC linker now gives this spelling its own boolean function definition +and the existing MCC segment evaluator. It logs the evaluated segment and +acknowledges it without writing a core file or contacting MCC telemetry. +The native Xbox/CE function table, compiler, validator and save functions +are unchanged. MCC console lookups use the same segment meaning; use the +existing `core_save`/`core_load` commands for an explicit debug core instead. + +`tools/mcc_native_smoke.py` replaces the old private load-only checker. +It runs one hidden Windows instance at a time with a disposable data root, +rejects script failures in the logs, and requires a unique console marker +after a 150-tick sleep, followed by normal exit. An echoed command, a loader +message or a zero exit code alone cannot pass. Asset-free tests include +the previously missed fatal log and verify rejection of echoed markers. +For example, with privately supplied maps and any matching resource files: + +```powershell +python tools/mcc_native_smoke.py --executable build/windows/halo.exe ` + --data-root build/private-mcc-data --output build/mcc-startup-check b40 +``` + +The checker needs the Xbox UI maps and each selected MCC map in that test +data root. Its logs and map assets must remain outside version control. +These are bounded startup checks, not complete campaign playthroughs or +automatic level-progression tests. + +After the segment fix, all 16 supplied maps passed 45-second native Windows +x86 runs in the development build with rendering enabled: Combat Revolved +`a10`, `a30`, `a50`, `b30`, +`b40`, `c10`, `c20`, `c40`, `d20`, `d40` and `credits`; Solar Flare `m0` +through `m3`; and Spasm Playground. Each produced its delayed marker and +exited normally without fatal script diagnostics. B40 also reached the +opening pilot/Cortana dialogue. Combat Revolved used its matching external +resources; no map assets are included in this contribution. The production Vorbis decoder was compared with libvorbis through libsndfile for all 158 permutations in Solar Flare's reported sound tag @@ -510,19 +545,20 @@ Both exited normally. This tests the new state message on loopback; it does not establish Internet reliability, every campaign's co-op behavior, or a full campaign playthrough. -After applying the fixes to the OpenCE contribution, the combined MCC, -existing cache-format and BMP suite passed 738 tests, with six -optional-fixture skips, and the Windows x86 release build passed. -That contribution build also passed targeted rendered loads of Solar Flare -`m0`, Combat Revolved `b40` and Spasm Playground. Checkpoint/core restoration, -the MCC-to-Xbox-to-CE-to-MCC transition, and the two-process cooperative -gravity/gain/marker checks were repeated on that build successfully. - -Linux and Android builds of these particular changes have not been verified -locally; the Windows build configuration did not provide an Android target. -Earlier CI results above predate these changes. The MCC validation workflow -includes the new portable resource-reader cases and native Windows campaign -cases so they run with the existing checks when the branch is published. +After transferring the segment fix to the OpenCE contribution, the combined +MCC, existing cache-format and BMP suite passed 746 tests, with six +optional-fixture skips. The Windows x86 release build passed. B40 was then +rechecked on that contribution build: its 45-second hidden rendered run +produced the delayed script marker, reached the opening dialogue and exited +normally without fatal script diagnostics. +The earlier contribution build's B40 load claim was also a false positive +and is withdrawn. Its separately checked checkpoint/core restoration, +MCC-to-Xbox-to-CE-to-MCC transition and cooperative state checks remain +historical results; they have not been repeated for this segment fix. +Linux and Android builds of these +particular changes have not been verified locally; the Windows build +configuration did not provide an Android target. Earlier CI results +above apply to their named commits, not these changes. Run the isolated tests with a C compiler and Python/pytest available: diff --git a/port/linux/game/mcc_scripts.c b/port/linux/game/mcc_scripts.c index 5909f10894..dcc93be894 100644 --- a/port/linux/game/mcc_scripts.c +++ b/port/linux/game/mcc_scripts.c @@ -91,8 +91,9 @@ static void mcc_mission_segment_evaluate(short function, long thread, boolean in segment = (char const *)(uintptr_t)arguments[0]; /* This port acknowledges a local segment event, without MCC's external * telemetry service. Keep the boolean result: shipped scripts use it in - * conditional expressions before cinematics. core_save_name is unrelated - * in the Xbox runtime and would create a debug save, not a segment event. + * conditional expressions before cinematics. MCC also uses the older + * core_save_name spelling for segments; the Xbox function with that name + * instead writes a debug save and must not be called by MCC map scripts. * Argument expressions still run normally, including any sleep/yield. */ if (segment) error(_error_log, "mcc: mission segment '%.128s'", segment); hs_return(thread, segment != NULL); @@ -104,11 +105,21 @@ static struct hs_function_definition mcc_mission_segment_definition = { NULL, 1, { _hs_type_string } }; +/* H1A repurposed this name for campaign segments with a boolean result. + * Keep it outside the legacy table, including its original debug-save API. + * Format reference: https://c20.reclaimers.net/h1/scripting/#functions */ +static struct hs_function_definition mcc_core_segment_definition = { + _hs_type_boolean, 0, "core_save_name", hs_macro_function_parse, + mcc_mission_segment_evaluate, "Records a local mission segment and acknowledges it.", + NULL, 1, { _hs_type_string } +}; + struct hs_function_definition *mcc_script_function(short index) { if (!mcc_cache_tags_loaded()) return NULL; if (index == MCC_HS_DISTANCE_TO_OBJECT) return &mcc_distance_definition.definition; if (index == MCC_HS_MISSION_SEGMENT) return &mcc_mission_segment_definition; + if (index == MCC_HS_CORE_SEGMENT) return &mcc_core_segment_definition; return mcc_campaign_function(index); } @@ -117,6 +128,7 @@ short mcc_script_find(char const *name) if (!mcc_cache_tags_loaded() || !name) return NONE; if (!csstrcasecmp(name, "objects_distance_to_object")) return MCC_HS_DISTANCE_TO_OBJECT; if (!csstrcasecmp(name, "mcc_mission_segment")) return MCC_HS_MISSION_SEGMENT; + if (!csstrcasecmp(name, "core_save_name")) return MCC_HS_CORE_SEGMENT; if (!csstrcasecmp(name, "player_effect_set_max_vibrate")) return hs_find_function_by_name("player_effect_set_max_rumble"); return mcc_campaign_find(name); @@ -203,6 +215,7 @@ int mcc_scripts_prepare(struct mcc_runtime *runtime) } if (!csstrcasecmp(name, "objects_distance_to_object")) index = MCC_HS_DISTANCE_TO_OBJECT; else if (!csstrcasecmp(name, "mcc_mission_segment")) index = MCC_HS_MISSION_SEGMENT; + else if (!csstrcasecmp(name, "core_save_name")) index = MCC_HS_CORE_SEGMENT; else if (!csstrcasecmp(name, "player_effect_set_max_vibrate")) index = hs_find_function_by_name("player_effect_set_max_rumble"); else { diff --git a/port/linux/game/mcc_scripts.h b/port/linux/game/mcc_scripts.h index d24e1015d4..035e3ebfd8 100644 --- a/port/linux/game/mcc_scripts.h +++ b/port/linux/game/mcc_scripts.h @@ -7,6 +7,7 @@ struct mcc_runtime; /* These indices are outside the unchanged Xbox/CE function table. */ #define MCC_HS_DISTANCE_TO_OBJECT 0x7000 #define MCC_HS_MISSION_SEGMENT 0x7001 +#define MCC_HS_CORE_SEGMENT 0x7002 struct hs_function_definition *mcc_script_function(short index); short mcc_script_find(char const *name); boolean mcc_script_call_valid(struct hs_syntax_node const *call, struct data_array *syntax); diff --git a/tools/harness/tests/mcc_scripts.c b/tools/harness/tests/mcc_scripts.c index b8b03c8528..4536c56968 100644 --- a/tools/harness/tests/mcc_scripts.c +++ b/tools/harness/tests/mcc_scripts.c @@ -103,7 +103,9 @@ int main(int argc, char **argv) { char *strings; uint32_t pointer; int expected = 1; + short segment_index; CHECK(argc == 2); + segment_index = !strcmp(argv[1], "core_segment") ? MCC_HS_CORE_SEGMENT : MCC_HS_MISSION_SEGMENT; runtime.used = 0x100000; runtime.tags = malloc(runtime.used); CHECK(runtime.tags); @@ -158,8 +160,8 @@ int main(int argc, char **argv) { if (!strcmp(argv[1], "unterminated")) { memset(strings, 'x', 128); expected = 0; } if (!strcmp(argv[1], "alias")) strcpy(strings, "player_effect_set_max_vibrate"); if (!strcmp(argv[1], "extension")) strcpy(strings, "objects_distance_to_object"); - if (!strcmp(argv[1], "segment")) { - strcpy(strings, "mcc_mission_segment"); + if (!strcmp(argv[1], "segment") || !strcmp(argv[1], "core_segment")) { + strcpy(strings, segment_index == MCC_HS_CORE_SEGMENT ? "core_save_name" : "mcc_mission_segment"); nodes[0].type = _hs_type_boolean; nodes[2].type = _hs_type_string; nodes[2].next_node_index = NONE; @@ -189,6 +191,8 @@ int main(int argc, char **argv) { } CHECK(!mcc_script_function(MCC_HS_DISTANCE_TO_OBJECT)); CHECK(!mcc_script_function(MCC_HS_MISSION_SEGMENT)); + CHECK(!mcc_script_function(MCC_HS_CORE_SEGMENT)); + CHECK(mcc_script_find("core_save_name") == NONE); CHECK(mcc_script_find("objects_distance_to_object") == NONE); CHECK(mcc_script_find("mcc_mission_segment") == NONE); loaded = TRUE; @@ -210,6 +214,7 @@ int main(int argc, char **argv) { } CHECK(mcc_script_find("player_effect_set_max_vibrate") == 322); CHECK(mcc_script_find("MCC_MISSION_SEGMENT") == MCC_HS_MISSION_SEGMENT); + CHECK(mcc_script_find("CORE_SAVE_NAME") == MCC_HS_CORE_SEGMENT); if (!strcmp(argv[1], "global")) CHECK((unsigned short)nodes[0].short_value == 0x8009); else if (!strcmp(argv[1], "alias")) CHECK(nodes[0].function_index == 322); else if (!strcmp(argv[1], "extension")) { @@ -228,12 +233,12 @@ int main(int argc, char **argv) { extension->evaluate(MCC_HS_DISTANCE_TO_OBJECT, 0, TRUE); result.bits = returned; CHECK(result.value == -1.f); - } else if (!strcmp(argv[1], "segment")) { + } else if (!strcmp(argv[1], "segment") || !strcmp(argv[1], "core_segment")) { char long_segment[200]; - CHECK(nodes[0].function_index == MCC_HS_MISSION_SEGMENT); - CHECK(nodes[1].function_index == MCC_HS_MISSION_SEGMENT); + CHECK(nodes[0].function_index == segment_index); + CHECK(nodes[1].function_index == segment_index); CHECK(mcc_script_call_valid(&nodes[0], syntax)); - extension = mcc_script_function(MCC_HS_MISSION_SEGMENT); + extension = mcc_script_function(segment_index); CHECK(extension && extension->return_type == _hs_type_boolean); CHECK(extension->parameter_count == 1 && extension->parameter_types[0] == _hs_type_string); nodes[2].type = _hs_type_real; @@ -249,24 +254,24 @@ int main(int argc, char **argv) { nodes[2].next_node_index = NONE; arguments[0] = (long)(uintptr_t)"cine1_intro"; returned = -123; - extension->evaluate(MCC_HS_MISSION_SEGMENT, 0, FALSE); + extension->evaluate(segment_index, 0, FALSE); CHECK(returned == -123 && !segment_events); /* argument expression still yielding */ - extension->evaluate(MCC_HS_MISSION_SEGMENT, 0, TRUE); + extension->evaluate(segment_index, 0, TRUE); CHECK(returned == TRUE && segment_events == 1); CHECK(!strcmp(last_event, "mcc: mission segment 'cine1_intro'")); arguments[0] = (long)(uintptr_t)"03_escape"; /* use the evaluated argument each time */ - extension->evaluate(MCC_HS_MISSION_SEGMENT, 0, TRUE); + extension->evaluate(segment_index, 0, TRUE); CHECK(returned == TRUE && segment_events == 2); CHECK(!strcmp(last_event, "mcc: mission segment '03_escape'")); memset(long_segment, 'x', sizeof(long_segment)); long_segment[199] = 0; arguments[0] = (long)(uintptr_t)long_segment; - extension->evaluate(MCC_HS_MISSION_SEGMENT, 0, TRUE); + extension->evaluate(segment_index, 0, TRUE); CHECK(returned == TRUE && segment_events == 3 && strlen(last_event) == 151); arguments[0] = 0; - extension->evaluate(MCC_HS_MISSION_SEGMENT, 0, TRUE); + extension->evaluate(segment_index, 0, TRUE); CHECK(returned == FALSE && segment_events == 3); loaded = FALSE; - CHECK(!mcc_script_function(MCC_HS_MISSION_SEGMENT)); + CHECK(!mcc_script_function(segment_index)); CHECK(mcc_script_find("mcc_mission_segment") == NONE); } else CHECK(nodes[0].function_index == 321 && nodes[1].function_index == 321); free(runtime.tags); diff --git a/tools/mcc_native_smoke.py b/tools/mcc_native_smoke.py new file mode 100644 index 0000000000..06b5219a76 --- /dev/null +++ b/tools/mcc_native_smoke.py @@ -0,0 +1,148 @@ +"""Bounded Windows MCC startup checks using locally supplied, private test assets. + +Use a disposable data root containing maps/ui.map and mcc_maps/*.map (plus +matching resources where required). Never commit the assets or generated logs. +Only one hidden game runs at a time. Success requires a console expression to +finish after 150 simulation ticks, not just the tag-loader's 'loaded' message. +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import socket +import subprocess +import time +import uuid + + +FAILURES = ("recompile scripts", "recompile.)", "error loading scripts", + "scripts won't run", "unsupported script", "invalid script", + "mcc: refused", "exception halt", "assertion failed", + "failed to allocate", "could not load", "invalid mcc script parameter") + + +def check_result(trace, console, marker, map_name, exit_code): + combined = (trace + "\n" + console).lower() + failures = [message for message in FAILURES if message in combined] + loaded = f"mcc: loaded mcc_maps\\{map_name.lower()} (" in trace.lower() + # The console echoes commands; an echoed print expression is not evidence + # it executed. Require the entire output line after the sleep has finished. + advanced = marker.lower() in {line.strip().lower() for line in console.splitlines()} + return dict(map=map_name, loaded=loaded, simulation_advanced=advanced, + failures=failures, exit_code=exit_code, + passed=loaded and advanced and exit_code == 0 and not failures) + + +def run_case(args, name, binary): + started = time.monotonic() + data, output = args.data_root, args.output / name + output.mkdir(parents=True, exist_ok=True) + init = data / "init.txt" + original = init.read_bytes() if init.exists() else None + debug = data / "debug.txt" + offset = debug.stat().st_size if debug.exists() else 0 + marker = "mcc_smoke_" + uuid.uuid4().hex + env = {key: value for key, value in os.environ.items() if not key.startswith("HALO_")} + env.update(HALO_DATA_ROOT=str(data), HALO_SAVE_ROOT=str(output / "saves"), + HALO_HIDDEN_WINDOW="1", HALO_EXIT_AFTER=str(args.seconds), + HALO_NULL_RENDERER="0", HALO_MAX_FPS="30", HALO_NO_VSYNC="1", HALO_NO_AUDIO="1", + HALO_DISPLAY_MODE="windowed", HALO_WINDOW_SIZE="640x480", HALO_FULLSCREEN="0", + HALO_NET_ONLINE="0", HALO_NET_PUBLIC_LOBBY="0", HALO_NET_ALLOW_UPNP="0", + HALO_NET_ADDRESS="127.0.0.236", HALO_NET_BROADCAST="127.0.0.237", + HALO_NET_JOIN_FROM_CLIPBOARD="0", HALO_UPDATE_ANSWER="never", HALO_CRASH_REPORTS="no", + HALO_TELNET_CONSOLE="1", HALO_TELNET_CONSOLE_PORT=str(args.port)) + transcript = "" + process = connection = None + def trace(): + if not debug.exists(): + return "" + with debug.open("rb") as stream: + stream.seek(offset) + return stream.read().decode("latin1") + try: + init.write_text(f"map_name mcc_maps\\{name}\n", encoding="ascii") + with (output / "stdout.log").open("wb") as stdout, (output / "stderr.log").open("wb") as stderr: + process = subprocess.Popen([str(binary)], cwd=data, env=env, stdout=stdout, stderr=stderr, + creationflags=subprocess.CREATE_NO_WINDOW) + deadline = time.monotonic() + args.seconds + 60 + sent = False + while process.poll() is None and time.monotonic() < deadline: + current = trace() + if any(message in current.lower() for message in FAILURES): + break + if connection is None and f"mcc: loaded mcc_maps\\{name.lower()} (" in current.lower(): + try: + connection = socket.create_connection(("127.0.0.1", args.port), .2) + connection.settimeout(.1) + except OSError: + connection = None + if connection is not None: + try: + if not sent: + connection.sendall(f'(begin (sleep 150) (print "{marker}"))\r\n'.encode("ascii")) + sent = True + received = connection.recv(65536) + if received: + transcript += received.decode("latin1") + except socket.timeout: + pass + except OSError: + connection.close() + connection = None + time.sleep(.1) + finally: + if connection is not None: + connection.close() + if process is not None and process.poll() is None: + process.kill() + process.wait(timeout=10) + if original is None: + init.unlink(missing_ok=True) + else: + init.write_bytes(original) + (output / "debug.log").write_text(trace(), encoding="utf-8") + (output / "console.log").write_text(transcript, encoding="utf-8") + diagnostics = trace() + (output / "stderr.log").read_text(errors="replace") + result = check_result(diagnostics, transcript, marker, name, process.returncode) + result["elapsed_seconds"] = round(time.monotonic() - started, 2) + result["executable_sha256"] = hashlib.sha256(binary.read_bytes()).hexdigest() + (output / "result.json").write_text(json.dumps(result, indent=2) + "\n") + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--executable", type=Path, required=True) + parser.add_argument("--data-root", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--seconds", type=int, default=45) + parser.add_argument("--port", type=int, default=23251) + parser.add_argument("maps", nargs="+") + args = parser.parse_args() + if os.name != "nt": + parser.error("this launcher currently supports Windows only") + if args.seconds < 20 or args.seconds > 300: + parser.error("--seconds must be between 20 and 300") + for name in args.maps: + if not re.fullmatch(r"[A-Za-z0-9_-]+", name) or not (args.data_root / "mcc_maps" / (name + ".map")).is_file(): + parser.error("supply existing map basenames from the disposable data root") + args.data_root, args.output = args.data_root.resolve(), args.output.resolve() + binary = args.output / "bin" / "halo.exe" + binary.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(args.executable, binary) + shutil.copy2(args.executable.parent / "SDL3.dll", binary.parent / "SDL3.dll") + results = [] + for name in args.maps: + print(f"{name}: hidden startup and simulation check", flush=True) + result = run_case(args, name, binary) + results.append(result) + print(json.dumps(result), flush=True) + (args.output / "results.json").write_text(json.dumps(results, indent=2) + "\n") + return 0 if all(result["passed"] for result in results) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/test_mcc_native_smoke.py b/tools/test_mcc_native_smoke.py new file mode 100644 index 0000000000..4e680e6a3b --- /dev/null +++ b/tools/test_mcc_native_smoke.py @@ -0,0 +1,24 @@ +"""The startup checker must not repeat the old false-positive B40 result.""" +import pytest +from mcc_native_smoke import check_result + +LOADED = "mcc: loaded mcc_maps\\b40 (5172 tags, 20224832 bytes of runtime tags)" +MARKER = "mcc_smoke_test" + + +@pytest.mark.parametrize("fatal", ["type is inconsistent with usage (you need to recompile scripts.)", + "the scenario's scripts won't run", "EXCEPTION halt"]) +def test_loader_success_cannot_hide_fatal_script_error(fatal): + assert not check_result(LOADED + "\n" + fatal, MARKER, MARKER, "b40", 0)["passed"] + + +@pytest.mark.parametrize("console", ["", '(begin (sleep 150) (print "mcc_smoke_test"))', + "previous_mcc_smoke_test"]) +def test_requires_actual_delayed_output(console): + assert not check_result(LOADED, console, MARKER, "b40", 0)["passed"] + + +def test_completed_simulation_and_normal_exit(): + assert check_result(LOADED, "\r\nmcc_smoke_test\r\n", MARKER, "b40", 0)["passed"] + assert not check_result(LOADED, MARKER, MARKER, "b40", 1)["passed"] + assert not check_result(LOADED, MARKER, MARKER, "a10", 0)["passed"] diff --git a/tools/test_mcc_scripts.py b/tools/test_mcc_scripts.py index 6ec2f42d44..a2708b440d 100644 --- a/tools/test_mcc_scripts.py +++ b/tools/test_mcc_scripts.py @@ -40,7 +40,7 @@ def scripts_tool(native_mcc_harness): return native_mcc_harness("mcc_scripts") -@pytest.mark.parametrize("case", ["link", "alias", "extension", "segment", "global", "unknown", +@pytest.mark.parametrize("case", ["link", "alias", "extension", "segment", "core_segment", "global", "unknown", "bad_child", "child_salt", "short_data", "capacity", "full_capacity", "negative_count", "unterminated", "skull_read", "skull_wrong_type", "skull_write"]) def test_mcc_script_linking(scripts_tool, case): From 5a88b1be9e43460274ddf643058dd4ebb3649c51 Mon Sep 17 00:00:00 2001 From: chester Date: Sat, 10 Oct 2026 18:20:07 +0100 Subject: [PATCH 4/8] Update NETCODE.md to remove erroneous MCC custom map netcode support details --- port/linux/NETCODE.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/port/linux/NETCODE.md b/port/linux/NETCODE.md index 4defe212d7..890a1e4fce 100644 --- a/port/linux/NETCODE.md +++ b/port/linux/NETCODE.md @@ -180,14 +180,6 @@ sends each killing blow again reliably and an object come to rest three times (a client waits for a player's blow before its body dies without one), and switches co-op's BSP on the host's crossing alone. -Version 26 adds MCC custom-map support, including a separate message for -the extra grenade inventories and the corresponding grenade action values. -All participants need the same protocol version, including on Xbox or -Custom Edition maps; their existing inventory packet layouts are unchanged. -MCC sessions use this netcode and the existing host-controlled co-op systems, -not the official MCC client or matchmaking. See [MCC map support](../../docs/mcc_maps.md) -for map requirements and the bounds of gameplay testing. - A client plays by its host's rules: in another's game (searching for it, in its lobby, or playing it) the developer console, the telnet console and the cheat buttons run only commands that change nothing of the game From 53eb67aa1971329b061dd9b0382a951ec0714b31 Mon Sep 17 00:00:00 2001 From: chestahh Date: Sat, 10 Oct 2026 18:33:16 +0100 Subject: [PATCH 5/8] Fix invalid weapon selections in MCC recorded animations (#241) Normalize unsupported recorded weapon slots to NONE in MCC-owned tag data before playback. Validate both stream codecs without changing Xbox or Custom Edition loaders, unit control, inventory limits or the network protocol. Reproduced the M1 assertions using build 32's pilot recording. The fixed Windows x86 build passed the same trigger and a 270-second run invoking all five recordings. Audited 377 supplied recordings; 821 tests passed with six optional skips. Android target unavailable locally. No full campaign playthrough claimed. (cherry picked from commit 4062c3e58181b4cd6b68546d2ed4307c020f016a) --- .github/workflows/mcc.yml | 3 +- docs/mcc_maps.md | 42 +++++++++ port/linux/game/mcc_recordings.c | 70 ++++++++++++++ port/linux/game/mcc_recordings.h | 11 +++ port/linux/game/mcc_tags.c | 27 ++++++ tools/mcc_native_smoke.py | 2 +- tools/test_mcc_native_smoke.py | 3 +- tools/test_mcc_recordings.py | 156 +++++++++++++++++++++++++++++++ tools/test_mcc_ui.py | 1 + 9 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 port/linux/game/mcc_recordings.c create mode 100644 port/linux/game/mcc_recordings.h create mode 100644 tools/test_mcc_recordings.py diff --git a/.github/workflows/mcc.yml b/.github/workflows/mcc.yml index f869ba28b6..7e2ecadd96 100644 --- a/.github/workflows/mcc.yml +++ b/.github/workflows/mcc.yml @@ -56,6 +56,7 @@ jobs: tools/test_mcc_ui_network.py tools/test_mcc_lifecycle.py tools/test_mcc_native_smoke.py + tools/test_mcc_recordings.py tools/test_mcc_saved_games.py tools/test_mcc_network.py tools/test_mcc_checkpoint.py @@ -77,4 +78,4 @@ jobs: call "%MCC_VS_INSTALL%\Common7\Tools\VsDevCmd.bat" -host_arch=x64 -arch=x86 if errorlevel 1 exit /b 1 clang --version - python -m pytest -q tools/test_mcc_cache_format.py tools/test_mcc_tag_validation.py tools/test_mcc_media.py tools/test_mcc_resources.py tools/test_mcc_bitmap_flags.py tools/test_mcc_shader_channels.py tools/test_mcc_hud.py tools/test_mcc_vorbis.py tools/test_mcc_texture_bridge.py tools/test_mcc_geometry.py tools/test_mcc_scripts.py tools/test_mcc_campaign.py tools/test_mcc_parameters.py tools/test_mcc_syntax.py tools/test_mcc_maps.py tools/test_mcc_menu.py tools/test_mcc_pause.py tools/test_mcc_pause_runtime.py tools/test_mcc_ui.py tools/test_mcc_ui_network.py tools/test_mcc_ui_teams.py tools/test_mcc_lifecycle.py tools/test_mcc_saved_games.py tools/test_mcc_network.py tools/test_mcc_checkpoint.py tools/test_mcc_grenades.py tools/test_cache_file_formats.py + python -m pytest -q tools/test_mcc_recordings.py tools/test_mcc_native_smoke.py tools/test_mcc_cache_format.py tools/test_mcc_tag_validation.py tools/test_mcc_media.py tools/test_mcc_resources.py tools/test_mcc_bitmap_flags.py tools/test_mcc_shader_channels.py tools/test_mcc_hud.py tools/test_mcc_vorbis.py tools/test_mcc_texture_bridge.py tools/test_mcc_geometry.py tools/test_mcc_scripts.py tools/test_mcc_campaign.py tools/test_mcc_parameters.py tools/test_mcc_syntax.py tools/test_mcc_maps.py tools/test_mcc_menu.py tools/test_mcc_pause.py tools/test_mcc_pause_runtime.py tools/test_mcc_ui.py tools/test_mcc_ui_network.py tools/test_mcc_ui_teams.py tools/test_mcc_lifecycle.py tools/test_mcc_saved_games.py tools/test_mcc_network.py tools/test_mcc_checkpoint.py tools/test_mcc_grenades.py tools/test_cache_file_formats.py diff --git a/docs/mcc_maps.md b/docs/mcc_maps.md index 9063170cf4..7d7df50061 100644 --- a/docs/mcc_maps.md +++ b/docs/mcc_maps.md @@ -512,6 +512,47 @@ data root. Its logs and map assets must remain outside version control. These are bounded startup checks, not complete campaign playthroughs or automatic level-progression tests. +Solar Flare M1 exposed a later runtime failure that a startup check cannot +cover. Its five pilot/dropship recordings contain `0xFF00` in their initial +weapon selection field. The Xbox playback code reads this as the signed +short `-256`, outside the four-slot unit inventory. Triggering `r_a_ds1` on +`a_pilot` in the published Windows release build 32 reproduced the reported +`units.c` assertions at original lines 1512 and 8371. The release's matching +symbols place the subsequent invalid object lookup in `unit_can_use_weapon`. +For a targeted reproduction in M1, create the pilot with +`(object_create_anew a_pilot)` and start `(recording_play a_pilot r_a_ds1)` +through the console. This exercises the control stream independently of the +mission trigger volumes. + +`mcc_recordings.c` validates each recording stream before normalizing invalid +initial or event weapon selections to `NONE` (retain the unit's current +weapon). Valid slots 0 through 3 and the existing `NONE` sentinel are unchanged, +as are event timing, movement, flags and the remaining control fields. The +conversion writes only to the MCC loader's private tag buffer through +`mcc_tags.c`; it changes neither the Xbox playback interpreter nor Xbox/CE +loaders, unit code, inventory limits or network messages. Unsupported or +truncated streams are refused with a recording-specific diagnostic; +explicitly disabled empty recordings remain disabled. + +Generated regression fixtures cover both playback codecs, all supported +control versions, selection events, truncation at every byte, unknown formats, +idempotence and checked scenario pointers/counts. A private audit of 377 +recordings across the 17 supplied MCC maps found no parsing failures. It +normalized 23 invalid selections: five in each Solar Flare map, one in Combat +Revolved's credits and two in its D40. No map contents are included in tests. +The native startup checker now also treats `EXCEPTION assert` as a failure, +even if the process subsequently exits normally. + +The recording fix passed a 65-second native M1 reproduction, followed by a +270-second Windows x86 run invoking all five recordings sequentially. Each +invocation was followed by a 1,290-tick wait (longer than the longest recording) +and a fresh completion marker. The run exited normally without assertions. +Only one hidden game instance ran at a time. These targeted checks do not +establish a full M1 playthrough. The final combined MCC/cache/BMP suite passed +821 tests with six optional-fixture skips, and the Windows release build +passed. Android could not be verified because this local configuration has +no Android build target; Linux was not built locally. + After the segment fix, all 16 supplied maps passed 45-second native Windows x86 runs in the development build with rendering enabled: Combat Revolved `a10`, `a30`, `a50`, `b30`, @@ -577,6 +618,7 @@ python -m pytest -q tools/test_mcc_checkpoint.py tools/test_mcc_grenades.py tool python -m pytest -q tools/test_mcc_parameters.py tools/test_mcc_scripts.py tools/test_mcc_syntax.py python -m pytest -q tools/test_mcc_campaign.py tools/test_mcc_resources.py python -m pytest -q tools/test_mcc_lifecycle.py +python -m pytest -q tools/test_mcc_recordings.py tools/test_mcc_native_smoke.py ``` Set `MCC_TEST_MAP` to an existing map path to opt into the real-fixture diff --git a/port/linux/game/mcc_recordings.c b/port/linux/game/mcc_recordings.c new file mode 100644 index 0000000000..e5885f4f50 --- /dev/null +++ b/port/linux/game/mcc_recordings.c @@ -0,0 +1,70 @@ +/* Independently written MCC cache normalization. Solar Flare M1's pilot + * recordings contain 0xFF00 in the initial weapon field. Passing that signed + * short (-256) to the Xbox interpreter indexes outside the unit's inventory. + * There is no valid slot to translate it to: retain the current weapon instead. + * Apply the same rule to later selection events, without changing their timing, + * movement or flags. This runs only on the MCC loader's private tag buffer. */ +#include "mcc_recordings.h" + +static unsigned mcc_recording_word(unsigned char const *p) +{ + return (unsigned)p[0] | ((unsigned)p[1] << 8); +} + +static uint32_t mcc_recording_weapon(unsigned char *p, int write) +{ + unsigned value = mcc_recording_word(p); + /* Xbox units have four slots; 0xFFFF requests no selection change. */ + if (value < 4 || value == 0xFFFF) return 0; + if (write) p[0] = p[1] = 0xFF; + return 1; +} + +static int mcc_recording_walk(unsigned char *stream, uint32_t size, + unsigned version, unsigned control_version, int write, uint32_t *corrected) +{ + uint32_t offset = 52, changes; + unsigned i; + if (!stream || version < 1 || version > 4 || control_version > 4) return 0; + for (i = 1; i < control_version; ++i) offset += i == 1 ? 4 : 2; + if (version == 4) offset += 12; /* facing/aiming/looking angle state */ + if (offset > size) return 0; + changes = mcc_recording_weapon(stream + 4, write); + while (offset < size) { + uint32_t header_size, payload_size; + unsigned type; + if (version == 4) { + unsigned time = stream[offset] & 3; + type = stream[offset] >> 2; + header_size = time < 2 ? 1 : time; + payload_size = type < 2 ? 0 : type < 4 ? 1 : type < 6 ? 2 : + type == 6 ? 8 : type < 15 ? 2 : 4; + } else { + if (size - offset < 4) return 0; + type = mcc_recording_word(stream + offset); + header_size = 4; + payload_size = type < 2 || type == 7 || type == 8 ? 0 : + type < 6 ? 2 : type == 6 || type >= 16 ? 8 : 12; + } + if (type >= 23 || header_size > size - offset) return 0; + offset += header_size; + if (payload_size > size - offset) return 0; + if (type == 1) { + *corrected = changes; + return 1; + } + if (type == 5) changes += mcc_recording_weapon(stream + offset, write); + offset += payload_size; + } + return 0; /* missing end event */ +} + +int mcc_recording_prepare(unsigned char *stream, uint32_t size, + unsigned version, unsigned control_version, uint32_t *corrected) +{ + uint32_t changes; + *corrected = 0; + if (!mcc_recording_walk(stream, size, version, control_version, 0, &changes)) return 0; + if (changes) return mcc_recording_walk(stream, size, version, control_version, 1, corrected); + return 1; +} diff --git a/port/linux/game/mcc_recordings.h b/port/linux/game/mcc_recordings.h new file mode 100644 index 0000000000..0816f2534f --- /dev/null +++ b/port/linux/game/mcc_recordings.h @@ -0,0 +1,11 @@ +/* MCC recording conversion; the established playback codecs stay unchanged. */ +#ifndef MCC_RECORDINGS_H +#define MCC_RECORDINGS_H +#include + +/* Validate the complete stream before changing any bytes. Invalid weapon + * selections become NONE (keep the unit's current weapon). */ +int mcc_recording_prepare(unsigned char *stream, uint32_t size, + unsigned version, unsigned control_version, uint32_t *corrected); + +#endif diff --git a/port/linux/game/mcc_tags.c b/port/linux/game/mcc_tags.c index 199ee8e071..63212cc9f6 100644 --- a/port/linux/game/mcc_tags.c +++ b/port/linux/game/mcc_tags.c @@ -3,6 +3,7 @@ #include "cseries.h" #include "errors.h" #include "mcc_runtime.h" +#include "mcc_recordings.h" #include static uint32_t mcc_word(void const *p) @@ -35,6 +36,30 @@ static void mcc_hud_placement(unsigned char *p) memcpy(p + 12, &flags, 2); } +static int mcc_scenario_recordings(struct mcc_runtime *r, unsigned char *scenario) +{ + uint32_t count, i; + unsigned char *recordings = mcc_block(r, scenario + 0x36C, 0x40, &count); + if (count && !recordings) return FALSE; + for (i = 0; i < count; ++i) { + unsigned char *recording = recordings + i * 0x40; + uint32_t size = mcc_word(recording + 0x2C), corrected; + unsigned char *stream = mcc_runtime_pointer(r, mcc_word(recording + 0x38), size); + /* Preserve an explicitly disabled, empty recording. */ + if (!recording[0x20] && !recording[0x24] && !recording[0x25] && + !size && !mcc_word(recording + 0x38)) continue; + if (!mcc_recording_prepare(stream, size, recording[0x20], recording[0x22], &corrected)) { + error(_error_silent, "mcc: recorded animation #%lu has an unsupported or damaged control stream", + (unsigned long)i); + return FALSE; + } + if (corrected) error(_error_silent, + "mcc: recorded animation #%lu: %lu invalid weapon selections changed to NONE", + (unsigned long)i, (unsigned long)corrected); + } + return TRUE; +} + static int mcc_hud(struct mcc_runtime *r, uint32_t group, unsigned char *p) { unsigned char *block; @@ -96,6 +121,7 @@ int mcc_tags_prepare(struct mcc_runtime *r) case 'hudg': size = 0x450; break; case 'weap': size = 0x508; break; case 'DeLa': size = 0x60; break; + case 'scnr': size = 0x5B0; break; default: break; } if (shader >= 0) size = group == 'scex' ? 0x78 : 0x28; @@ -106,6 +132,7 @@ int mcc_tags_prepare(struct mcc_runtime *r) return FALSE; } if (shader >= 0) memcpy(p + 0x24, &shader, 2); + if (group == 'scnr' && !mcc_scenario_recordings(r, p)) return FALSE; if (group == 'scex') { uint32_t flags = mcc_word(p + 0x6C), new_group = 'schi'; if (!mcc_word(p + 0x54)) memcpy(p + 0x54, p + 0x60, 12); diff --git a/tools/mcc_native_smoke.py b/tools/mcc_native_smoke.py index 06b5219a76..410affd2e0 100644 --- a/tools/mcc_native_smoke.py +++ b/tools/mcc_native_smoke.py @@ -20,7 +20,7 @@ FAILURES = ("recompile scripts", "recompile.)", "error loading scripts", "scripts won't run", "unsupported script", "invalid script", - "mcc: refused", "exception halt", "assertion failed", + "mcc: refused", "exception halt", "exception assert", "assertion failed", "failed to allocate", "could not load", "invalid mcc script parameter") diff --git a/tools/test_mcc_native_smoke.py b/tools/test_mcc_native_smoke.py index 4e680e6a3b..7f40f2faf5 100644 --- a/tools/test_mcc_native_smoke.py +++ b/tools/test_mcc_native_smoke.py @@ -7,7 +7,8 @@ @pytest.mark.parametrize("fatal", ["type is inconsistent with usage (you need to recompile scripts.)", - "the scenario's scripts won't run", "EXCEPTION halt"]) + "the scenario's scripts won't run", "EXCEPTION halt", + "EXCEPTION assert in units.c,#1512: control_data->weapon_index"]) def test_loader_success_cannot_hide_fatal_script_error(fatal): assert not check_result(LOADED + "\n" + fatal, MARKER, MARKER, "b40", 0)["passed"] diff --git a/tools/test_mcc_recordings.py b/tools/test_mcc_recordings.py new file mode 100644 index 0000000000..b80ae726ea --- /dev/null +++ b/tools/test_mcc_recordings.py @@ -0,0 +1,156 @@ +"""Generated recording streams exercise the production MCC-only conversion. + +No map assets are included. Cover both playback codecs, all control versions, +invalid selections after variable-sized events, truncation and tag bounds. +""" +from pathlib import Path +import shutil +import struct +import subprocess +import sys + +import pytest + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="module") +def recording_tool(tmp_path_factory): + clang = shutil.which("clang") + if not clang: + pytest.skip("clang is needed for MCC recording tests") + work = tmp_path_factory.mktemp("mcc-recordings") + (work / "cseries.h").write_text("#define TRUE 1\n#define FALSE 0\n#define NUMBEROF(a) (sizeof(a)/sizeof((a)[0]))\n") + (work / "errors.h").write_text("#define _error_silent 0\nvoid error(int level, char const *format, ...);\n") + source = work / "runner.c" + source.write_text(r''' +#include +#include +#include +#include "mcc_recordings.h" +#include "mcc_runtime.h" +int mcc_tags_prepare(struct mcc_runtime *r); +void error(int level,char const *format,...) {(void)level;(void)format;} +void *mcc_runtime_pointer(struct mcc_runtime *r,uint32_t address,uint32_t size) { + uint32_t offset=address-r->report.tag_base; + return address>=r->report.tag_base && offset<=r->used && size<=r->used-offset ? r->tags+offset : NULL; +} +static void put(unsigned char *p,uint32_t value) {memcpy(p,&value,4);} +int main(int argc,char **argv) { + unsigned char data[65536];uint32_t size,corrected=0;int ok; + FILE *f;struct mcc_runtime r={0};unsigned char *sc,*rec; + if(argc!=6)return 2; + memset(data,0,sizeof(data));f=fopen(argv[3],"rb");if(!f)return 3; + size=(uint32_t)fread(data+0x700,1,sizeof(data)-0x700,f);fclose(f); + if(!strcmp(argv[5],"stream")) + ok=mcc_recording_prepare(data+0x700,size,atoi(argv[1]),atoi(argv[2]),&corrected); + else { + r.tags=r.tag_index=data;r.used=0x700+size;r.report.tag_base=0x50000000;r.report.tag_count=1; + put(data,'scnr');put(data+20,0x50000020);sc=data+0x20;rec=data+0x600; + put(sc+0x36C,1);put(sc+0x370,0x50000600); + rec[0x20]=(unsigned char)atoi(argv[1]);rec[0x22]=(unsigned char)atoi(argv[2]); + put(rec+0x2C,size);put(rec+0x38,0x50000700); + if(!strcmp(argv[5],"bad-block"))put(sc+0x36C,0xFFFFFFFF); + if(!strcmp(argv[5],"bad-pointer"))put(rec+0x38,0xFFFFFFFF); + if(!strcmp(argv[5],"bad-size"))put(rec+0x2C,0xFFFFFFFF); + if(!strcmp(argv[5],"disabled")) {rec[0x20]=0;put(rec+0x2C,0);put(rec+0x38,0);} + ok=mcc_tags_prepare(&r); + } + f=fopen(argv[4],"wb");if(!f)return 4;fwrite(data+0x700,1,size,f);fclose(f); + printf("%d %u\n",ok,(unsigned)corrected);return 0; +} +''') + binary = work / ("recordings.exe" if sys.platform == "win32" else "recordings") + game = ROOT / "port/linux/game" + command = [clang, "-std=gnu89", "-Wall", "-Wextra", "-Werror", "-Wno-multichar", + "-I" + str(work), "-I" + str(game), str(source), str(game / "mcc_recordings.c"), + str(game / "mcc_tags.c"), "-o", str(binary)] + if sys.platform == "win32": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return binary + + +def stream(version=4, control=4, weapon=-256): + data = bytearray(52 + sum(4 if i == 1 else 2 for i in range(1, control)) + (12 if version == 4 else 0)) + struct.pack_into("= 16 else 12 + data += event(version, kind, b"\x7f" * size, time) + data += event(version, 1, time=time) + result, converted = convert(recording_tool, tmp_path, data, version) + assert result == (1, 1) + assert len(converted) == len(data) + assert sum(a != b for a, b in zip(converted, data)) == 2 + assert convert(recording_tool, tmp_path, converted, version) == ((1, 0), converted) + + +@pytest.mark.parametrize("version", [1, 2, 3, 4]) +def test_every_truncation_fails_without_mutation(recording_tool, tmp_path, version): + data = stream(version) + event(version, 5, b"\x00\xff", 3) + event(version, 6, b"\0" * 8) + event(version, 1) + for size in range(len(data)): + assert convert(recording_tool, tmp_path, data[:size], version) == ((0, 0), data[:size]) + + +@pytest.mark.parametrize("version,control,tail", [(0, 4, b"\x04"), (5, 4, b"\x04"), (4, 5, b"\x04"), + (4, 4, b"\xfc"), (1, 4, b"\xff\xff\0\0")]) +def test_unknown_formats_fail_without_mutation(recording_tool, tmp_path, version, control, tail): + data = stream() + tail + assert convert(recording_tool, tmp_path, data, version, control) == ((0, 0), data) + + +@pytest.mark.parametrize("mode", ["tags", "bad-block", "bad-pointer", "bad-size", "disabled"]) +def test_checked_mcc_scenario_integration(recording_tool, tmp_path, mode): + data = stream() + event(4, 1) + result, converted = convert(recording_tool, tmp_path, data, mode=mode) + assert result[0] == (mode in ("tags", "disabled")) + expected = bytearray(data) + if mode == "tags":expected[4:6] = b"\xff\xff" + assert converted == expected diff --git a/tools/test_mcc_ui.py b/tools/test_mcc_ui.py index 5f8e61855c..81ec55c1c6 100644 --- a/tools/test_mcc_ui.py +++ b/tools/test_mcc_ui.py @@ -205,6 +205,7 @@ def ui_tool(tmp_path_factory): static unsigned char metadata[0x400]; static void *mcc_runtime_pointer(struct mcc_runtime *r,uint32_t address,uint32_t bytes){(void)r;return address<=sizeof(metadata)&&bytes<=sizeof(metadata)-address?metadata+address:NULL;} static int mcc_hud(struct mcc_runtime *r,uint32_t group,unsigned char *p){(void)r;(void)group;(void)p;return TRUE;} +static int mcc_scenario_recordings(struct mcc_runtime *r,unsigned char *p){(void)r;(void)p;abort();return FALSE;} ''' + '\n'.join(function(tags, name) for name in ["mcc_word", "mcc_block", "mcc_tags_prepare"]) + r''' #define CHECK(c,n) do{if(!(c)){fprintf(stderr,"check %d failed\n",n);return n;}}while(0) static boolean fire(struct widget_instance *widget,short button,struct ui_widget_event_handler_reference *handler){ From f47ccfd9d12ed6314c363ab552b1c561ea69420b Mon Sep 17 00:00:00 2001 From: chestahh Date: Sat, 10 Oct 2026 19:22:09 +0100 Subject: [PATCH 6/8] Add MCC script adapters and correct custom HUD placement (#241) Support the reported MCC integer, conditional-print and sound-prediction functions, scenario-local developer_mode, and objects_distance_to_flag. Preserve MCC edge-center HUD anchors and normalize only terminal drawing. Refuse every MCC object-name list above the native 512-name capacity instead of truncating script targets. B30 Evolved still needs an MCC-owned name-table/checkpoint/co-op extension and is documented as not playable. Existing Xbox/CE loader code and constants are unchanged. Shared source changes are additive MCC-only drawing dispatches. Validation: Windows x86 release; 876 tests passed, six optional skips; Precipice and Combat Revolved B30 70-second simulation checks; rendered HUD/console inspection; MCC/Xbox/CE/MCC transition simulation. Android could not build because this local configuration has no Android target. (cherry picked from commit dc2c4982ed10c88abae470d727b2934916439c25) --- docs/mcc_maps.md | 82 +++++++++++++- port/linux/game/mcc_campaign.c | 75 ++++++++++++- port/linux/game/mcc_hud_draw.c | 41 +++++++ port/linux/game/mcc_hud_draw.h | 9 ++ port/linux/game/mcc_scripts.c | 63 ++++++++++- port/linux/game/mcc_tag_validate.c | 11 ++ port/linux/game/mcc_terminal.c | 42 +++++++ source/interface/hud_draw.c | 13 +++ source/interface/terminal.c | 7 ++ tools/harness/tests/mcc_campaign.c | 41 ++++++- tools/harness/tests/mcc_scripts.c | 55 ++++++++- tools/harness/tests/mcc_tag_validation.c | 34 ++++++ tools/mcc_native_smoke.py | 3 +- tools/test_mcc_campaign.py | 2 +- tools/test_mcc_hud.py | 137 +++++++++++++++++++++++ tools/test_mcc_native_smoke.py | 3 +- tools/test_mcc_scripts.py | 2 +- tools/test_mcc_tag_validation.py | 2 + 18 files changed, 609 insertions(+), 13 deletions(-) create mode 100644 port/linux/game/mcc_terminal.c diff --git a/docs/mcc_maps.md b/docs/mcc_maps.md index 7d7df50061..86dd06d492 100644 --- a/docs/mcc_maps.md +++ b/docs/mcc_maps.md @@ -160,6 +160,7 @@ are not an MCC implementation dependency. The independent components are: | `port/linux/game/mcc_tags.c` | MCC metadata, shader-type, HUD placement and widget normalization. | | `port/linux/game/mcc_hud.c` | MCC bitmap/placement scaling, including nested weapon and grenade HUD items. | | `port/linux/game/mcc_hud_draw.c` | MCC canvas, glyph geometry and text advances; neutral dispatch for Xbox/CE. | +| `port/linux/game/mcc_terminal.c` | MCC terminal-only font scaling; preserves the map's HUD fonts and all legacy rendering. | | `port/linux/game/mcc_scripts.c` | Name-based MCC function/global linking and supported MCC native functions. | | `port/linux/game/mcc_campaign.c` | Local-player/authority queries, gravity, campaign markers and scoped global permissions. | | `port/linux/game/mcc_syntax.c` | MCC's 32,767-slot syntax validation and traversal workspace. | @@ -327,9 +328,11 @@ Navigation has eight independently owned team markers and uses the native HUD drawing services; the host replicates the complete marker state and gravity periodically, including after changes and late joins. -The two recognized `debug_ice_cream_flavor_status_*` globals for IWHBYD and -Grunt Birthday Party are read-only false values. This does not implement -skull gameplay, and scripts trying to set them are refused. MCC maps may +The recognized `debug_ice_cream_flavor_status_*` globals for IWHBYD, +Grunt Birthday Party and Bandanna report false. This does not implement +skull gameplay. A literal assignment of false is accepted because the +skull is already disabled; attempts to enable one or assign a computed +value are refused. MCC maps may set `sound_gain_under_dialog` and `cheat_infinite_ammo`; the adapter captures their previous values and restores them on unload. The native global table and Xbox/CE write permissions remain unchanged. @@ -628,6 +631,79 @@ requires the x86 Windows game headers/toolchain and skips elsewhere. the C standard library, so files can also be audited outside the game. None of these commands downloads or bundles a game map. +## Additional script and HUD compatibility + +The MCC function registry also supports `print_if`, `bitwise_and`, +`bitwise_or`, `bitwise_left_shift`, `bitwise_right_shift`, `bit_test`, +`bit_toggle`, `objects_distance_to_flag` and `sound_impulse_predict`. +The integer operators use defined unsigned 32-bit operations; right shift +fills with zero. Negative shift counts produce zero, while nonnegative +counts use the low five bits. Bit-test/toggle use a bit position, and +toggle's third argument selects set or clear. The prediction function +requests all sound permutations from the existing sound cache without +playing them or consuming random numbers. Its boolean selects a blocking +request. Public signatures and the supplied compiled scripts were checked; +this is not an exhaustive comparison with every MCC runtime edge case. + +When referenced, `developer_mode` is linked to an MCC scenario-owned short +global, initially zero. Script reads, writes and native checkpoint storage +therefore work without adding a global to the Xbox interpreter or enabling +engine developer privileges. Loading a different scenario discards it. + +MCC HUD anchors 5 through 8 (top, bottom, left and right center) retain +their authored values. MCC-only drawing handles their position, bitmap +bounds and number alignment using the current player's window. The native +anchor enum and its five existing cases are unchanged. Combat Revolved's +unit HUD uses anchor 5; previously the native schema clamped it to top left. + +Oversized MCC terminal fonts are scaled at draw time to a 15-pixel line at +the engine's 480p reference resolution. The supplied Combat Revolved font +has a 33-pixel line. This affects only the console input/output, including +wrapping and line spacing; it does not resize HUD text or modify font tags. +Xbox/CE paths and MCC fonts at or below the nominal size remain unchanged. + +### B30 Evolved capacity limitation + +The supplied `b30_evolved.map` gets past its reported missing functions +with these adapters, but it is **not yet playable**. It contains 640 named +objects and a 101-entry scenery palette. The native object-name table and +co-op presence message support 512 names; the scenario palette schema +supports 100 entries. Earlier conversion truncated the names, followed by +`backpack_1_argl: this is not a valid object name` during script startup. +The MCC validator now refuses the oversized name list explicitly instead +of reporting a loaded map after discarding required script targets. +This check applies to every MCC map: 512 names are allowed, while 513 or +more are refused. It does not change Xbox or Custom Edition validation. + +Supporting this map requires an MCC-owned name table with lifecycle, +checkpoint and co-op synchronization support, plus an audit of palette +consumers. Raising the shared Xbox/CE constants or dropping excess names +is not a safe fix. The changes here do not claim that this map runs. + +### Checks for these changes (10 October 2026) + +The Windows x86 release build passed. The MCC, cache-format and BMP suite +passed 876 tests with six optional-fixture skips. New tests cover typed +integer operations, conditional printing, sound-cache prediction, writable +scenario-local developer state, disabled-skull assignments, all four new +HUD anchors, terminal layout bounds and unchanged legacy dispatch. Name +capacity tests accept 512 and reject 513/640 without truncating the list; +the same fixture still follows the original legacy validator behavior. + +The supplied Precipice and Combat Revolved B30 each passed a 70-second +hidden native run, including delayed output after 150 simulation ticks, +normal exit and no fatal script/assert diagnostics. Rendered Combat +Revolved frames confirm centered shields/health and smaller console text. +An initial transition test sent its command before map startup and was +inconclusive; rerunning after waiting for loading verified simulation in +MCC B30, Xbox A10, CE Hugeass and MCC B30 again in one process. + +B30 Evolved was tested separately and is refused at its object-name limit; +it is not counted as a successful load. No full campaign playthrough or +network co-op test was performed for these changes. Android remains +unverified locally: `ninja android` reports an unknown target in this +Windows-only configuration. + ## Public format evidence These are wire-format references, not copied implementation code. Current diff --git a/port/linux/game/mcc_campaign.c b/port/linux/game/mcc_campaign.c index 203d7d6c1c..5a7a9c3dd3 100644 --- a/port/linux/game/mcc_campaign.c +++ b/port/linux/game/mcc_campaign.c @@ -3,6 +3,8 @@ #include "mcc_campaign.h" #include "mcc_cache.h" #include "errors.h" +#include "cache/sound_cache.h" +#include "sound/sound_definitions.h" #include "hs/hs.h" #include "hs/hs_scenario_definitions.h" #include "hs/object_lists.h" @@ -221,7 +223,14 @@ static void mcc_position_distance_evaluate(short function, long thread, boolean union { real number; long bits; } result; if (!args) return; result.number=-1.0f; - target.x=mcc_campaign_argument(args[1]);target.y=mcc_campaign_argument(args[2]);target.z=mcc_campaign_argument(args[3]); + if (function==MCC_HS_CAMPAIGN_FIRST+21) { + struct scenario *scenario=global_scenario_get(); + short flag=(short)args[1]; + if (flag<0 || flag>=scenario->cutscene_flags.count) {hs_return(thread,result.bits);return;} + target=((struct scenario_cutscene_flag *)scenario->cutscene_flags.address)[flag].position; + } else { + target.x=mcc_campaign_argument(args[1]);target.y=mcc_campaign_argument(args[2]);target.z=mcc_campaign_argument(args[3]); + } if (args[0]!=NONE && datum_try_and_get(object_list_header_data,args[0]) && mcc_campaign_real(target.x) && mcc_campaign_real(target.y) && mcc_campaign_real(target.z)) { for (object=object_list_get_first(args[0],&cursor);object!=NONE;object=object_list_get_next(args[0],&cursor)) { @@ -263,6 +272,59 @@ static void mcc_breadcrumbs_active_evaluate(short function, long thread, boolean hs_return(thread,TRUE); } +/* MCC integer operators use 32-bit values, including a zero-filling right + * shift. Use unsigned operations so high bits and overflow are defined on + * both native x86 and the Android guest. Nonnegative counts wrap at 32; + * negative counts produce an empty mask/result. */ +static void mcc_bits_evaluate(short function, long thread, boolean initialize) +{ + long *args=hs_macro_function_evaluate(function,thread,initialize); + uint32_t value,mask,result; + short shift; + if (!args) return; + value=(uint32_t)args[0];shift=(short)args[1]; + mask=shift<0 ? 0u : (UINT32_C(1) << ((unsigned)shift&31u)); + switch (function-MCC_HS_CAMPAIGN_FIRST) { + case 13: result=value&(uint32_t)args[1];break; + case 14: result=value|(uint32_t)args[1];break; + case 15: result=shift<0 ? 0u : value << ((unsigned)shift&31u);break; + case 16: result=shift<0 ? 0u : value >> ((unsigned)shift&31u);break; + case 17: result=(value&mask)!=0;break; + case 18: result=(boolean)args[2] ? value|mask : value&~mask;break; + default: result=0;break; + } + hs_return(thread,(long)result); +} + +static void mcc_print_if_evaluate(short function, long thread, boolean initialize) +{ + long *args=hs_macro_function_evaluate(function,thread,initialize); + void hs_print(char const *message); + if (!args) return; + if ((boolean)args[0] && args[1]) hs_print((char const *)args[1]); + hs_return(thread,0); +} + +static void mcc_impulse_predict_evaluate(short function, long thread, boolean initialize) +{ + long *args=hs_macro_function_evaluate(function,thread,initialize); + struct sound_definition *sound; + long range,permutation; + if (!args) return; + /* Preload without creating a playing sound, acquiring references or + * choosing a random permutation. The boolean requests a blocking load. */ + if (args[0]!=NONE) { + sound=sound_definition_get(args[0]); + for (range=0;rangepitch_ranges.count;range++) { + struct sound_pitch_range *pitch=(struct sound_pitch_range *)sound->pitch_ranges.address+range; + for (permutation=0;permutationpermutations.count;permutation++) + _sound_cache_sound_request((struct sound_permutation *)pitch->permutations.address+permutation, + (boolean)args[1],TRUE,FALSE); + } + } + hs_return(thread,0); +} + static void mcc_nav_evaluate(short function, long thread, boolean initialize); static boolean mcc_sleep_parse(short function, long expression) { @@ -290,7 +352,16 @@ static struct mcc_campaign_definition mcc_campaign_functions[]={ {{_hs_type_void,0,"breadcrumbs_activate_team_nav_point_flag",hs_macro_function_parse,mcc_nav_evaluate,NULL,NULL,4,{_hs_type_navpoint}},{_hs_type_enum_team,_hs_type_cutscene_flag,_hs_type_real}}, {{_hs_type_void,0,"breadcrumbs_deactivate_team_nav_point_flag",hs_macro_function_parse,mcc_nav_evaluate,NULL,NULL,2,{_hs_type_enum_team}},{_hs_type_cutscene_flag}}, {{_hs_type_void,0,"breadcrumbs_activate_team_nav_point_object",hs_macro_function_parse,mcc_nav_evaluate,NULL,NULL,4,{_hs_type_navpoint}},{_hs_type_enum_team,_hs_type_object,_hs_type_real}}, - {{_hs_type_void,0,"breadcrumbs_deactivate_team_nav_point_object",hs_macro_function_parse,mcc_nav_evaluate,NULL,NULL,2,{_hs_type_enum_team}},{_hs_type_object}} + {{_hs_type_void,0,"breadcrumbs_deactivate_team_nav_point_object",hs_macro_function_parse,mcc_nav_evaluate,NULL,NULL,2,{_hs_type_enum_team}},{_hs_type_object}}, + {{_hs_type_long_integer,0,"bitwise_and",hs_macro_function_parse,mcc_bits_evaluate,NULL,NULL,2,{_hs_type_long_integer}},{_hs_type_long_integer}}, + {{_hs_type_long_integer,0,"bitwise_or",hs_macro_function_parse,mcc_bits_evaluate,NULL,NULL,2,{_hs_type_long_integer}},{_hs_type_long_integer}}, + {{_hs_type_long_integer,0,"bitwise_left_shift",hs_macro_function_parse,mcc_bits_evaluate,NULL,NULL,2,{_hs_type_long_integer}},{_hs_type_short_integer}}, + {{_hs_type_long_integer,0,"bitwise_right_shift",hs_macro_function_parse,mcc_bits_evaluate,NULL,NULL,2,{_hs_type_long_integer}},{_hs_type_short_integer}}, + {{_hs_type_long_integer,0,"bit_test",hs_macro_function_parse,mcc_bits_evaluate,NULL,NULL,2,{_hs_type_long_integer}},{_hs_type_short_integer}}, + {{_hs_type_long_integer,0,"bit_toggle",hs_macro_function_parse,mcc_bits_evaluate,NULL,NULL,3,{_hs_type_long_integer}},{_hs_type_short_integer,_hs_type_boolean}}, + {{_hs_type_void,0,"print_if",hs_macro_function_parse,mcc_print_if_evaluate,NULL,NULL,2,{_hs_type_boolean}},{_hs_type_string}}, + {{_hs_type_void,0,"sound_impulse_predict",hs_macro_function_parse,mcc_impulse_predict_evaluate,NULL,NULL,2,{_hs_type_sound}},{_hs_type_boolean}}, + {{_hs_type_real,0,"objects_distance_to_flag",hs_macro_function_parse,mcc_position_distance_evaluate,NULL,NULL,2,{_hs_type_object_list}},{_hs_type_cutscene_flag}} }; struct hs_function_definition *mcc_campaign_function(short index) diff --git a/port/linux/game/mcc_hud_draw.c b/port/linux/game/mcc_hud_draw.c index 58fb31ac68..72422a0a7a 100644 --- a/port/linux/game/mcc_hud_draw.c +++ b/port/linux/game/mcc_hud_draw.c @@ -51,3 +51,44 @@ boolean mcc_hud_icon_draw(long bitmap_group_index, struct bitmap_data const *bit cursor->x0=(short)(point.x+advance); return TRUE; } + +/* The four edge-center anchors are H1A additions (5..8). Keep them in + * MCC tags and handle them here instead of changing the native enum. */ +static boolean mcc_hud_edge(short anchor) +{ + return mcc_cache_tags_loaded() && anchor>=5 && anchor<=8; +} + +boolean mcc_hud_anchor_point(short anchor, struct hud_placement_definition const *placement, + struct bitmap_data const *bitmap, real scale, rectangle2d const *window, + rectangle2d const *viewport, point2d *point) +{ + real x,y; + if (!mcc_hud_edge(anchor)) return FALSE; + x=(anchor==7 ? window->x0 : anchor==8 ? window->x1 : (window->x0+window->x1)*0.5f); + y=(anchor==5 ? window->y0 : anchor==6 ? window->y1 : (window->y0+window->y1)*0.5f); + x+=placement->offset.x*scale*(anchor==8 ? -1.0f : 1.0f)-viewport->x0; + y+=placement->offset.y*scale*(anchor==6 ? -1.0f : 1.0f)-viewport->y0; + if (bitmap) { + x+=(bitmap->registration_point.x-(anchor==8 ? bitmap->width : anchor<=6 ? bitmap->width*0.5f : 0))*scale; + y+=(bitmap->registration_point.y-(anchor==6 ? bitmap->height : anchor>=7 ? bitmap->height*0.5f : 0))*scale; + } + point->x=(short)x;point->y=(short)y; + return TRUE; +} + +boolean mcc_hud_anchor_bounds(short anchor, real width, real height, real_rectangle2d *bounds) +{ + if (!mcc_hud_edge(anchor)) return FALSE; + bounds->x0=anchor==8 ? -width : anchor<=6 ? -width*0.5f : 0; + bounds->y0=anchor==6 ? -height : anchor>=7 ? -height*0.5f : 0; + bounds->x1=bounds->x0+width;bounds->y1=bounds->y0+height; + return TRUE; +} + +boolean mcc_hud_anchor_number(short anchor, real width, real origin, short *cursor) +{ + if (!mcc_hud_edge(anchor)) return FALSE; + *cursor=(short)(origin+(anchor<=6 ? width*0.5f : anchor==7 ? width : 0)); + return TRUE; +} diff --git a/port/linux/game/mcc_hud_draw.h b/port/linux/game/mcc_hud_draw.h index bbedce3556..e460c78957 100644 --- a/port/linux/game/mcc_hud_draw.h +++ b/port/linux/game/mcc_hud_draw.h @@ -5,6 +5,7 @@ #include "math/real_math.h" struct bitmap_data; +struct hud_placement_definition; struct icon_hud_element_definition; /* Neutral for every non-MCC cache. No native or CE tag is rewritten. */ @@ -14,4 +15,12 @@ boolean mcc_hud_icon_draw(long bitmap_group_index, struct bitmap_data const *bit real_rectangle2d const *clip, rectangle2d *cursor, pixel32 color, struct icon_hud_element_definition const *icon, boolean menu); +boolean mcc_hud_anchor_point(short anchor, struct hud_placement_definition const *placement, + struct bitmap_data const *bitmap, real scale, rectangle2d const *window, + rectangle2d const *viewport, point2d *point); +boolean mcc_hud_anchor_bounds(short anchor, real width, real height, real_rectangle2d *bounds); +boolean mcc_hud_anchor_number(short anchor, real width, real origin, short *cursor); +short mcc_terminal_line_height(long font, short height); +boolean mcc_terminal_draw(long font, rectangle2d const *bounds, char const *text); + #endif diff --git a/port/linux/game/mcc_scripts.c b/port/linux/game/mcc_scripts.c index dcc93be894..f66b6bfbc1 100644 --- a/port/linux/game/mcc_scripts.c +++ b/port/linux/game/mcc_scripts.c @@ -162,10 +162,50 @@ static char const *mcc_script_string(char const *strings, uint32_t bytes, long o strings + offset : NULL; } +/* Give MCC's developer flag ordinary scenario storage. This preserves + * script reads/writes and checkpoint lifetime without adding an external + * engine global or enabling native developer privileges. */ +static short mcc_developer_global(struct mcc_runtime *runtime, struct scenario *scenario, + struct data_array *syntax) +{ + extern short const hs_external_global_count; + struct hs_global *old,*globals; + struct hs_syntax_node *nodes=(struct hs_syntax_node *)(syntax+1),*initializer; + long count=scenario->hs_globals.count,i,slot; + if (count<0 || count>=1024-hs_external_global_count) return NONE; + old=mcc_runtime_pointer(runtime,(uint32_t)(uintptr_t)scenario->hs_globals.address, + (uint32_t)count*sizeof(*old)); + if (count && !old) return NONE; + for (i=0;icount;slot++) if (!nodes[slot].datum_header) break; + if (slot==syntax->maximum_count) return NONE; + globals=mcc_runtime_allocate(runtime,(uint32_t)(count+1)*sizeof(*globals)); + if (!globals) return NONE; + if (count) memcpy(globals,old,count*sizeof(*globals)); + memset(globals+count,0,sizeof(*globals)); + memcpy(globals[count].name,"developer_mode",15); + globals[count].type=_hs_type_short_integer; + globals[count].initialization_expression_index=0x10000|slot; + initializer=nodes+slot; + memset(initializer,0,sizeof(*initializer)); + initializer->datum_header=1;initializer->flags=9; + initializer->type=initializer->constant_type=_hs_type_short_integer; + initializer->next_node_index=NONE;initializer->source_offset=NONE; + if (slot>=syntax->count) syntax->count=(short)(slot+1); + syntax->actual_count++; + syntax->first_free_absolute_index=(short)(slot+1); + scenario->hs_globals.address=globals;scenario->hs_globals.count=count+1; + return (short)count; +} + static boolean mcc_disabled_skull(char const *name) { return name && (!csstrcasecmp(name,"debug_ice_cream_flavor_status_i_would_have_been_your_daddy") || - !csstrcasecmp(name,"debug_ice_cream_flavor_status_grunt_birthday_party")); + !csstrcasecmp(name,"debug_ice_cream_flavor_status_grunt_birthday_party") || + !csstrcasecmp(name,"debug_ice_cream_flavor_status_bandanna")); } int mcc_scripts_prepare(struct mcc_runtime *runtime) @@ -176,6 +216,7 @@ int mcc_scripts_prepare(struct mcc_runtime *runtime) struct hs_syntax_node *nodes; char const *strings; long i, missing = 0; + short developer=NONE; memcpy(&address, runtime->tag_index + (runtime->report.scenario_handle & 0xFFFFu) * 32 + 20, 4); scenario = mcc_runtime_pointer(runtime, address, sizeof(*scenario)); if (!scenario) return FALSE; @@ -209,6 +250,17 @@ int mcc_scripts_prepare(struct mcc_runtime *runtime) struct hs_syntax_node *target = datum_try_and_get(syntax,nodes[child].next_node_index); if (target && (target->flags & 1) && mcc_disabled_skull( mcc_script_string(strings,scenario->hs_string_constants.size,target->string_offset))) { + struct hs_syntax_node *value=datum_try_and_get(syntax,target->next_node_index); + /* A literal disable is already satisfied. Retain the + * expression's requested result type and sibling link; + * never discard computed values or an enable request. */ + if (value && (value->flags&7)==1 && value->type==_hs_type_boolean && + value->constant_type==_hs_type_boolean && !value->boolean_value && + value->next_node_index==NONE) { + node->flags=9;node->constant_type=_hs_type_boolean; + node->data=FALSE;node->source_offset=NONE; + continue; + } error(_error_silent,"mcc: scripts cannot enable unsupported skull gameplay"); return FALSE; } @@ -233,6 +285,15 @@ int mcc_scripts_prepare(struct mcc_runtime *runtime) node->data = FALSE; continue; } + if (!csstrcasecmp(name,"developer_mode")) { + if (developer==NONE) developer=mcc_developer_global(runtime,scenario,syntax); + if (developer==NONE) { + error(_error_silent,"mcc: no scenario storage for developer_mode"); + return FALSE; + } + node->data=developer; + continue; + } index = hs_find_global_by_name(name); if (index != NONE && ((uint16_t)index & 0x8000)) node->short_value = index; else index = NONE; diff --git a/port/linux/game/mcc_tag_validate.c b/port/linux/game/mcc_tag_validate.c index 5b6a8b0870..11b45f9ee1 100644 --- a/port/linux/game/mcc_tag_validate.c +++ b/port/linux/game/mcc_tag_validate.c @@ -310,6 +310,14 @@ static void mcc_fields(struct mcc_validation_context *context, byte *address, unsigned long bytes; long maximum = field->maximum; block->definition = NULL; + /* Dropping object names breaks compiled script references + * and the native/co-op name tables still have fixed size. + * Refuse explicitly until MCC owns an extended runtime. */ + if (schema->name && !strcmp(schema->name, "scenario") && + !strcmp(field->name, "object_names") && block->count > maximum) { + mcc_problem(context, 1, "object-name count exceeds the supported runtime capacity; names cannot be truncated"); + break; + } if (!element || element->size <= 0 || block->count < 0 || (unsigned long)block->count > context->length / (unsigned long)element->size) { mcc_problem(context, 1, "invalid block count"); break; @@ -412,6 +420,9 @@ static void mcc_fields(struct mcc_validation_context *context, byte *address, if (field->type == _tag_schema_enum && schema->name && (!strcmp(schema->name, "unit") || !strcmp(schema->name, "equipment")) && !strcmp(field->name, "grenade_type")) maximum = 4; + if (field->type == _tag_schema_enum && schema->name && + !strcmp(schema->name, "hud_absolute_placement") && + !strcmp(field->name, "corner")) maximum = 9; if (field->type == _tag_schema_block_index) { byte *target = NULL; if (field->target_level == TAG_SCHEMA_ROOT) target = context->stack[0].address; diff --git a/port/linux/game/mcc_terminal.c b/port/linux/game/mcc_terminal.c new file mode 100644 index 0000000000..97d4927721 --- /dev/null +++ b/port/linux/game/mcc_terminal.c @@ -0,0 +1,42 @@ +/* Normalize only terminal drawing, leaving map fonts and HUD text intact. */ +#include "cseries.h" +#include "text/font_group.h" +#include "rasterizer/rasterizer.h" +#include "mcc_cache.h" +#include "mcc_hud_draw.h" + +static real mcc_terminal_scale(long index) +{ + struct font_header const *font; + real height; + if (!mcc_cache_tags_loaded() || index==NONE) return 1.0f; + font=font_definition_get(index); + height=font->ascending_height+font->descending_height+font->leading_height; + /* The native terminal's nominal line is 15 pixels at 480p. MCC maps + * sometimes package a 33-pixel terminal font. Do not upscale small fonts. */ + return height>15.0f ? 15.0f/height : 1.0f; +} + +short mcc_terminal_line_height(long font, short height) +{ + real scale=mcc_terminal_scale(font); + return scale==1.0f ? height : (short)(height*scale+0.5f); +} + +boolean mcc_terminal_draw(long font, rectangle2d const *bounds, char const *text) +{ + real scale=mcc_terminal_scale(font); + real right,bottom; + rectangle2d layout; + if (scale==1.0f) return FALSE; + layout=*bounds; + right=layout.x0+(bounds->x1-bounds->x0)/scale; + bottom=layout.y0+(bounds->y1-bounds->y0)/scale+1.0f; + /* Map font metrics must not overflow the engine's short rectangles. */ + layout.x1=(short)(right>32767.0f ? 32767.0f : right< -32768.0f ? -32768.0f : right); + layout.y1=(short)(bottom>32767.0f ? 32767.0f : bottom< -32768.0f ? -32768.0f : bottom); + rasterizer_text_set_scale(scale,(real)bounds->x0,(real)bounds->y0); + rasterizer_draw_string(&layout,NULL,NULL,0,text); + rasterizer_text_set_scale(1.0f,0.0f,0.0f); + return TRUE; +} diff --git a/source/interface/hud_draw.c b/source/interface/hud_draw.c index 4f33f8c0c3..9c0b728962 100644 --- a/source/interface/hud_draw.c +++ b/source/interface/hud_draw.c @@ -1050,6 +1050,11 @@ void hud_calculate_point( 127, placement); + /* port: MCC edge-center anchors use the current player's viewport. */ + if (mcc_hud_anchor_point(absolute_placement->corner, placement, bitmap_data, + scale, &render.camera.window_bounds, &render.camera.viewport_bounds, result)) + return; + corner = absolute_placement->corner; if (corner < _hud_anchor_center) { @@ -1764,6 +1769,10 @@ static void hud_calculate_bitmap_bounds( width = (clip->x1-clip->x0)*(is_interface_bitmap ? 1 : bitmap->width); height = (clip->y1-clip->y0)*(is_interface_bitmap ? 1 : bitmap->height); + /* port: MCC-only bounds for its additional HUD anchors. */ + if (mcc_hud_anchor_bounds(placement_type, width, height, bounds)) + return; + switch (placement_type) { case _hud_anchor_top_left: @@ -2080,6 +2089,10 @@ void hud_draw_numbers( 0.0f, &origin); + /* port: MCC edge-center number alignment; native anchors keep their cases. */ + if (!mcc_hud_anchor_number(absolute_placement->corner, + ((digit_count-1.0f)*hud_number->screen_width+decimal_point_width)*scale, + origin.x, &cursor.x)) switch (absolute_placement->corner) { case _hud_anchor_top_left: diff --git a/source/interface/terminal.c b/source/interface/terminal.c index a055d5b08b..38d44fecad 100644 --- a/source/interface/terminal.c +++ b/source/interface/terminal.c @@ -49,6 +49,7 @@ symbols in this file: /* ---------- headers */ #include "cseries.h" +#include "mcc_hud_draw.h" /* port: MCC-only terminal sizing. */ #include "terminal.h" #include "cache_files.h" @@ -218,6 +219,8 @@ void terminal_draw( { struct font_header *font = font_definition_get(font_tag_index); short line_height = font->ascending_height + font->descending_height + font->leading_height; + /* port: preserve native line spacing outside MCC maps. */ + line_height = mcc_terminal_line_height(font_tag_index, line_height); if (terminal_globals.input_state) { short buff_len; @@ -247,6 +250,8 @@ void terminal_draw( } buffer[cursor_index] = '_'; } + /* port: MCC terminal font scaling does not modify the font tag. */ + if (!mcc_terminal_draw(font_tag_index, &terminal_gets_bounds, buffer)) rasterizer_draw_string(&terminal_gets_bounds, NULL, NULL, 0, buffer); } @@ -289,6 +294,8 @@ void terminal_draw( } draw_string_set_draw_mode(font_tag_index, NONE, 0, 0, &color); + /* port: MCC-only scaling, also applied to console output. */ + if (!mcc_terminal_draw(font_tag_index, &terminal_gets_bounds, line->buffer)) rasterizer_draw_string(&terminal_gets_bounds, NULL, NULL, 0, line->buffer); draw_string_set_tab_stops(terminal_tab_stops, 0); } diff --git a/tools/harness/tests/mcc_campaign.c b/tools/harness/tests/mcc_campaign.c index d47a7dbbd8..735d21ee05 100644 --- a/tools/harness/tests/mcc_campaign.c +++ b/tools/harness/tests/mcc_campaign.c @@ -12,6 +12,7 @@ #include "interface/hud_definitions.h" #include "scenario/scenario_definitions.h" #include "mcc_campaign.h" +#include "sound/sound_definitions.h" #include #include #include @@ -21,6 +22,15 @@ #undef strlen #define CHECK(x) do {if(!(x)){fprintf(stderr,"line %d: %s\n",__LINE__,#x);return 1;}}while(0) +static int printed,requests,blocks; +static struct sound_definition sound; +static struct sound_pitch_range ranges[2]; +static struct sound_permutation permutations[3]; +void hs_print(char const *message) {if(strcmp(message,"100% literal"))abort();printed++;} +void *tag_get(long group,long index) {if(group!='snd!'||index!=17)abort();return &sound;} +boolean _sound_cache_sound_request(struct sound_permutation *p,boolean block,boolean load,boolean ref) { + if(p=permutations+3||!load||ref)abort();requests++;blocks+=block;return TRUE; +} static boolean loaded=TRUE; static short connection; static long args[7],result,added[4]; @@ -93,7 +103,36 @@ int main(int argc,char **argv) { scenario.cutscene_flags.address=flags;scenario.cutscene_flags.count=2; flags[1].position.x=10; mcc_campaign_begin(); - if(!strcmp(argv[1],"players")) { + if(!strcmp(argv[1],"bits")) { + struct hs_function_definition *f; + args[0]=6;args[1]=3;call("bitwise_and");CHECK(result==2); + call("bitwise_or");CHECK(result==7); + args[0]=-1;args[1]=1;call("bitwise_right_shift");CHECK(result==0x7fffffff); + args[0]=1;args[1]=31;call("bitwise_left_shift");CHECK((unsigned long)result==0x80000000UL); + args[1]=-1;call("bitwise_left_shift");CHECK(result==0); + call("bitwise_right_shift");CHECK(result==0); + args[1]=32;call("bitwise_left_shift");CHECK(result==1); + args[0]=0;args[1]=31;args[2]=(long)0xffffff01;call("bit_toggle");CHECK((unsigned long)result==0x80000000UL); + args[0]=result;call("bit_test");CHECK(result==1); + args[2]=(long)0xffffff00;call("bit_toggle");CHECK(result==0); + args[0]=7;args[1]=-1;args[2]=1;call("bit_toggle");CHECK(result==7); + call("bit_test");CHECK(result==0); + f=mcc_campaign_function(mcc_campaign_find("bit_toggle")); + CHECK(f->return_type==_hs_type_long_integer && f->parameter_count==3 && + f->parameter_types[1]==_hs_type_short_integer && f->parameter_types[2]==_hs_type_boolean); + result=123;f->evaluate(mcc_campaign_find("bit_toggle"),0,FALSE);CHECK(result==123); + args[0]=8;args[1]=1;call("objects_distance_to_flag");CHECK(number(result)==2.0f); + args[1]=NONE;call("objects_distance_to_flag");CHECK(number(result)==-1.0f); + } else if(!strcmp(argv[1],"print_predict")) { + args[1]=(long)"100% literal";args[0]=(long)0xffffff00;call("print_if");CHECK(!printed); + args[0]=1;call("print_if");CHECK(printed==1); + sound.pitch_ranges.count=2;sound.pitch_ranges.address=ranges; + ranges[0].permutations.count=1;ranges[0].permutations.address=permutations; + ranges[1].permutations.count=2;ranges[1].permutations.address=permutations+1; + args[0]=17;args[1]=0;call("sound_impulse_predict");CHECK(requests==3 && !blocks); + args[1]=(long)0xffffff01;call("sound_impulse_predict");CHECK(requests==6 && blocks==3); + args[0]=NONE;call("sound_impulse_predict");CHECK(requests==6); + } else if(!strcmp(argv[1],"players")) { call("local_players");CHECK(result==8&&count==2&&added[0]==0&&added[1]==2); units[2].object.damage_flags=1u<<_object_dead_bit; call("local_players");CHECK(count==1&&added[0]==0); diff --git a/tools/harness/tests/mcc_scripts.c b/tools/harness/tests/mcc_scripts.c index 4536c56968..e867490911 100644 --- a/tools/harness/tests/mcc_scripts.c +++ b/tools/harness/tests/mcc_scripts.c @@ -22,6 +22,7 @@ #undef strcpy #undef strlen +short const hs_external_global_count=443; static boolean loaded; static struct data_array list_data; struct data_array *object_list_header_data = &list_data; @@ -65,7 +66,7 @@ long *hs_macro_function_evaluate(short function, long thread, boolean initialize } void hs_return(long thread, long value) { (void)thread; returned = value; } short hs_find_function_by_name(char const *name) { - if (!strcmp(name, "sleep")) return 321; + if (!strcmp(name, "sleep") || !strcmp(name,"set")) return 321; if (!strcmp(name, "player_effect_set_max_rumble")) return 322; return NONE; } @@ -93,6 +94,10 @@ void *mcc_runtime_pointer(struct mcc_runtime *runtime, uint32_t address, uint32_ runtime->tags + offset : NULL; } +void *mcc_runtime_allocate(struct mcc_runtime *runtime,uint32_t bytes) { + if(runtime->used+bytes>runtime->capacity)return NULL; + {void *p=runtime->tags+runtime->used;runtime->used+=bytes;return p;} +} #define CHECK(value) do { if (!(value)) { fprintf(stderr, "check failed at line %d\n", __LINE__); return 1; } } while (0) int main(int argc, char **argv) { struct mcc_runtime runtime = {0}; @@ -107,7 +112,8 @@ int main(int argc, char **argv) { CHECK(argc == 2); segment_index = !strcmp(argv[1], "core_segment") ? MCC_HS_CORE_SEGMENT : MCC_HS_MISSION_SEGMENT; runtime.used = 0x100000; - runtime.tags = malloc(runtime.used); + runtime.capacity=runtime.used+65536; + runtime.tags = malloc(runtime.capacity); CHECK(runtime.tags); memset(runtime.tags, 0, runtime.used); runtime.tag_index = runtime.tags + 40; @@ -178,7 +184,52 @@ int main(int argc, char **argv) { strcpy(strings,"set");strcpy(strings+4,"debug_ice_cream_flavor_status_i_would_have_been_your_daddy"); nodes[2].flags=5;nodes[2].data=0x8001;nodes[2].string_offset=4;nodes[2].type=_hs_type_boolean;expected=0; } + if (!strcmp(argv[1], "developer_read") || !strcmp(argv[1], "developer_cast")) { + nodes[0].flags=13;nodes[0].data=0x8001; + nodes[0].type=!strcmp(argv[1],"developer_cast") ? _hs_type_real : _hs_type_short_integer; + strcpy(strings,"developer_mode"); + } + if (!strcmp(argv[1], "developer_write")) { + strcpy(strings,"set");strcpy(strings+4,"developer_mode"); + nodes[2].flags=5;nodes[2].data=0x8001;nodes[2].string_offset=4; + nodes[2].type=_hs_type_short_integer; + } + if (!strcmp(argv[1],"skull_disable") || !strcmp(argv[1],"skull_enable") || !strcmp(argv[1],"skull_computed")) { + strcpy(strings,"set");strcpy(strings+4,"debug_ice_cream_flavor_status_bandanna"); + nodes[0].type=_hs_type_void; + nodes[2].flags=5;nodes[2].data=0x8001;nodes[2].string_offset=4;nodes[2].type=_hs_type_boolean; + nodes[3].flags=9;nodes[3].type=nodes[3].constant_type=_hs_type_boolean;nodes[3].data=(long)0xffffff00; + if (!strcmp(argv[1],"skull_enable")) {nodes[3].data=1;expected=0;} + if (!strcmp(argv[1],"skull_computed")) {nodes[3].flags=13;expected=0;} + } + if (!strcmp(argv[1],"developer_capacity") || !strcmp(argv[1],"developer_allocation")) { + nodes[0].flags=13;nodes[0].data=0x8001;nodes[0].type=_hs_type_short_integer; + strcpy(strings,"developer_mode");expected=0; + if (!strcmp(argv[1],"developer_capacity")) scenario->hs_globals.count=1024-hs_external_global_count; + else runtime.capacity=runtime.used; + } CHECK(mcc_scripts_prepare(&runtime) == expected); + if (!strcmp(argv[1],"skull_disable")) { + CHECK(nodes[0].flags==9 && nodes[0].data==FALSE && nodes[0].constant_type==_hs_type_boolean); + CHECK(nodes[0].type==_hs_type_void && nodes[0].next_node_index==NONE); + free(runtime.tags);return 0; + } + if (!strcmp(argv[1],"developer_write")) { + struct hs_global *global=scenario->hs_globals.address; + struct hs_syntax_node *initial; + CHECK(scenario->hs_globals.count==1 && nodes[2].data==0 && (nodes[2].flags&4)); + CHECK(!strcmp(global->name,"developer_mode") && global->type==_hs_type_short_integer); + initial=datum_try_and_get(syntax,global->initialization_expression_index); + CHECK(initial && initial->flags==9 && initial->short_value==0 && initial->type==_hs_type_short_integer); + CHECK(initial->next_node_index==NONE && initial->source_offset==NONE); + } + if (!strcmp(argv[1], "developer_read") || !strcmp(argv[1], "developer_cast")) { + CHECK((nodes[0].flags&4) && nodes[0].data==0 && scenario->hs_globals.count==1); + CHECK(((struct hs_global *)scenario->hs_globals.address)->type==_hs_type_short_integer); + CHECK(syntax->actual_count==5); + CHECK(nodes[0].type==(!strcmp(argv[1],"developer_cast") ? _hs_type_real : _hs_type_short_integer)); + free(runtime.tags);return 0; + } if (!expected) { free(runtime.tags); return 0; } if (!strcmp(argv[1], "skull_read")) { CHECK(!(nodes[0].flags&4) && nodes[0].data==FALSE && nodes[0].constant_type==_hs_type_boolean); diff --git a/tools/harness/tests/mcc_tag_validation.c b/tools/harness/tests/mcc_tag_validation.c index 7eec716cf8..f5f2b75019 100644 --- a/tools/harness/tests/mcc_tag_validation.c +++ b/tools/harness/tests/mcc_tag_validation.c @@ -19,6 +19,7 @@ struct test_child { short enumeration; short pad; struct tag_data data; struct t struct test_root { struct tag_block children; short index; short enumeration; struct tag_data file; struct tag_data hs_syntax_data, other_data; + struct tag_block object_names; }; struct test_bsp { void *root; long vcount; void *vertices; long icount; void *indices; unsigned long signature; }; struct test_device { short kind; unsigned short flags; struct tag_data identifier, profile; }; @@ -57,6 +58,8 @@ static struct tag_schema_field const child_fields[] = { TAG_SCHEMA_END }; static struct tag_schema_definition const child_schema = TAG_SCHEMA_DEFINITION(child, struct test_child, child_fields); +static struct tag_schema_field const name_fields[] = {TAG_SCHEMA_END}; +static struct tag_schema_definition const name_schema = {"object_name",36,name_fields}; static struct tag_schema_field const root_fields[] = { TAG_SCHEMA_BLOCK(struct test_root, children, child_schema, 4), TAG_SCHEMA_BLOCK_INDEX(struct test_root, index, TAG_SCHEMA_ROOT, offsetof(struct test_root, children), 0), @@ -64,6 +67,7 @@ static struct tag_schema_field const root_fields[] = { TAG_SCHEMA_FILE_DATA(struct test_root, file, 256), TAG_SCHEMA_DATA(struct test_root, hs_syntax_data, 56L + 20L * 19001), TAG_SCHEMA_DATA(struct test_root, other_data, 128), + TAG_SCHEMA_BLOCK(struct test_root, object_names, name_schema, 512), TAG_SCHEMA_CHECK(check_root), TAG_SCHEMA_END }; @@ -107,10 +111,14 @@ static struct tag_schema_field const grenade_fields[] = {TAG_SCHEMA_BLOCK(struct static struct tag_schema_definition const grenade_schema = {"game_globals",sizeof(struct test_grenades),grenade_fields}; static struct tag_schema_field const equipment_fields[] = {TAG_SCHEMA_ENUM(struct test_equipment,grenade_type,2,0),TAG_SCHEMA_END}; static struct tag_schema_definition const equipment_schema = {"equipment",sizeof(struct test_equipment),equipment_fields}; +struct test_anchor {short corner;}; +static struct tag_schema_field const anchor_fields[]={TAG_SCHEMA_ENUM(struct test_anchor,corner,5,0),TAG_SCHEMA_END}; +static struct tag_schema_definition const anchor_schema=TAG_SCHEMA_DEFINITION(hud_absolute_placement,struct test_anchor,anchor_fields); static struct tag_schema_group const groups[] = { {'scnr', {NONE, NONE}, &root_schema}, {'sbsp', {NONE, NONE}, &root_schema}, {'antr', {NONE, NONE}, &graph_schema}, {'bitm', {NONE, NONE}, &bitmap_schema}, {'afp!', {NONE,NONE}, &fp_schema}, {'matg', {NONE,NONE}, &grenade_schema}, {'eqip',{NONE,NONE}, &equipment_schema}, + {'anch', {NONE, NONE}, &anchor_schema}, {'trim', {NONE, NONE}, &padded_root_schema}, {0, {0, 0}, NULL} }; struct tag_schema_group const *const tag_schema_group_lists[] = {groups, NULL}; @@ -150,6 +158,7 @@ int main(int argc, char **argv) memset(&runtime, 0, sizeof(runtime)); runtime.capacity = 65536; runtime.used = 4096; runtime.source.size = 1024; if (!strncmp(argv[1], "syntax_", 7)) runtime.capacity = runtime.used = 1048576; + if (!strncmp(argv[1], "names_", 6)) runtime.used = runtime.capacity; runtime.tags = calloc(runtime.capacity, 1); CHECK(runtime.tags != NULL); header = (struct test_header *)runtime.tags; instance = (struct test_instance *)(runtime.tags + 0x28); @@ -253,7 +262,32 @@ int main(int argc, char **argv) root->hs_syntax_data.size = MCC_SYNTAX_MAXIMUM_DATA_BYTES; } } + if (!strncmp(argv[1],"anchor_",7)) { + struct test_anchor *anchor=(void *)(runtime.tags+0x500); + header->count=2;instance[1].group='anch';instance[1].handle=(long)0xe0010001UL; + instance[1].root=anchor;instance[1].name=instance->name; + anchor->corner=(short)atoi(argv[1]+7); + } + if (!strncmp(argv[1],"names_",6)) { + root->object_names.count=atoi(argv[1]+6); + root->object_names.address=runtime.tags+0x1000; + expected=root->object_names.count<=512; + } CHECK(mcc_tags_validate(&runtime) == expected); + if (!strncmp(argv[1],"names_",6)) { + CHECK(root->object_names.count==atoi(argv[1]+6)); + CHECK(tag_validate_tags(header,runtime.used,runtime.source.size,"legacy names")); + CHECK(root->object_names.count==512); + mcc_validation_dispose(&runtime);free(runtime.tags);return 0; + } + if (!strncmp(argv[1],"anchor_",7)) { + short input=(short)atoi(argv[1]+7); + struct test_anchor *anchor=instance[1].root; + CHECK(anchor->corner==(input>=0 && input<9 ? input : 0)); + CHECK(tag_validate_tags(header,runtime.used,runtime.source.size,"legacy anchors")); + CHECK(anchor->corner==(input>=0 && input<5 ? input : 0)); + mcc_validation_dispose(&runtime);free(runtime.tags);return 0; + } CHECK(!mcc_validation_callback_active()); if (!strncmp(argv[1], "syntax_", 7) && expected) { CHECK(root->hs_syntax_data.size == MCC_SYNTAX_MAXIMUM_DATA_BYTES); diff --git a/tools/mcc_native_smoke.py b/tools/mcc_native_smoke.py index 410affd2e0..4bdefadc26 100644 --- a/tools/mcc_native_smoke.py +++ b/tools/mcc_native_smoke.py @@ -21,7 +21,8 @@ FAILURES = ("recompile scripts", "recompile.)", "error loading scripts", "scripts won't run", "unsupported script", "invalid script", "mcc: refused", "exception halt", "exception assert", "assertion failed", - "failed to allocate", "could not load", "invalid mcc script parameter") + "failed to allocate", "could not load", "invalid mcc script parameter", + "this is not a valid object name") def check_result(trace, console, marker, map_name, exit_code): diff --git a/tools/test_mcc_campaign.py b/tools/test_mcc_campaign.py index 97f8e4ddc2..421a1c2a22 100644 --- a/tools/test_mcc_campaign.py +++ b/tools/test_mcc_campaign.py @@ -9,7 +9,7 @@ def campaign_tool(native_mcc_harness): return native_mcc_harness("mcc_campaign") -@pytest.mark.parametrize("case", ["players", "distance", "gravity", "controls", "navigation", "validation"]) +@pytest.mark.parametrize("case", ["players", "distance", "gravity", "controls", "navigation", "validation", "bits", "print_predict"]) def test_campaign_handlers(campaign_tool, case): result = subprocess.run([str(campaign_tool), case], capture_output=True, text=True, timeout=10) assert result.returncode == 0, result.stdout + result.stderr diff --git a/tools/test_mcc_hud.py b/tools/test_mcc_hud.py index 1e70eee44b..a36c85580f 100644 --- a/tools/test_mcc_hud.py +++ b/tools/test_mcc_hud.py @@ -141,3 +141,140 @@ def test_messaging_offsets_preserve_stock_and_map_authored_values(hud_tool, play assert result.returncode == 0, result.stderr viewport = 0.75 if players > 1 else 1.0 assert int(result.stdout.split()[4]) == int(140 - offset_y * viewport) + + +@pytest.fixture(scope="module") +def anchor_tool(tmp_path_factory): + compiler = shutil.which("clang") + if not compiler: + pytest.skip("clang is needed") + work = tmp_path_factory.mktemp("mcc-anchors") + source = (ROOT / "port/linux/game/mcc_hud_draw.c").read_text() + routines = "\n".join(function(source, n) for n in ["mcc_hud_edge", "mcc_hud_anchor_point", + "mcc_hud_anchor_bounds", "mcc_hud_anchor_number"]) + code = r''' +#include +#include +typedef float real; +typedef int boolean; +typedef struct {short x,y;} point2d; +typedef struct {short x0,y0,x1,y1;} rectangle2d; +typedef struct {float x0,x1,y0,y1;} real_rectangle2d; +struct bitmap_data {short width,height;point2d registration_point;}; +struct hud_placement_definition {point2d offset;}; +#define TRUE 1 +#define FALSE 0 +static int loaded; +static int mcc_cache_tags_loaded(void) {return loaded;} +''' + routines + r''' +int main(int argc,char **argv) { + struct hud_placement_definition placement={{10,20}}; + rectangle2d window={40,30,840,430},viewport={20,10,860,450}; + point2d point={-99,-99}; + real_rectangle2d bounds={-99,-99,-99,-99}; + short cursor=-99,anchor;int a,b,c;float scale; + if(argc!=4)return 2; + loaded=atoi(argv[1]);anchor=(short)atoi(argv[2]);scale=(float)atof(argv[3]); + a=mcc_hud_anchor_point(anchor,&placement,NULL,scale,&window,&viewport,&point); + b=mcc_hud_anchor_bounds(anchor,200,80,&bounds); + c=mcc_hud_anchor_number(anchor,100,300,&cursor); + printf("%d %d %d %d %d %.1f %.1f %.1f %.1f %d",a,b,c,point.x,point.y, + bounds.x0,bounds.x1,bounds.y0,bounds.y1,cursor); + return 0; +} +''' + path=work / "anchors.c" + path.write_text(code) + binary=work / ("anchors.exe" if sys.platform=="win32" else "anchors") + command=[compiler,"-std=gnu89","-fsanitize=undefined","-fsanitize-trap=undefined",str(path),"-o",str(binary)] + if sys.platform=="win32": + command[1:1]=["--target=i686-pc-windows-msvc","-fuse-ld=lld"] + result=subprocess.run(command,capture_output=True,text=True) + assert result.returncode==0,result.stderr + return binary + + +@pytest.mark.parametrize("scale", [1.0, 0.75]) +@pytest.mark.parametrize("anchor,origin,direction,bounds,cursor", [ + (5, (420,20), (1,1), (-100,100,0,80), 350), + (6, (420,420), (1,-1), (-100,100,-80,0), 350), + (7, (20,220), (1,1), (0,200,-40,40), 400), + (8, (820,220), (-1,1), (-200,0,-40,40), 300)]) +def test_mcc_edge_anchors_follow_player_window(anchor_tool,scale,anchor,origin,direction,bounds,cursor): + result=subprocess.run([str(anchor_tool),"1",str(anchor),str(scale)],capture_output=True,text=True) + assert result.returncode==0,result.stderr + values=list(map(float,result.stdout.split())) + assert values[:3]==[1,1,1] + assert values[3:5]==[int(origin[0]+10*scale*direction[0]),int(origin[1]+20*scale*direction[1])] + assert values[5:9]==list(bounds) + assert values[9]==cursor + + +@pytest.mark.parametrize("loaded,anchor", [(0,n) for n in range(10)]+[(1,n) for n in [-1,0,1,2,3,4,9]]) +def test_edge_dispatch_preserves_legacy_and_unknown_anchors(anchor_tool,loaded,anchor): + result=subprocess.run([str(anchor_tool),str(loaded),str(anchor),"1"],capture_output=True,text=True) + assert result.returncode==0,result.stderr + assert list(map(float,result.stdout.split()))==[0,0,0]+[-99]*7 + + +@pytest.fixture(scope="module") +def terminal_tool(tmp_path_factory): + compiler=shutil.which("clang") + if not compiler: + pytest.skip("clang is needed") + work=tmp_path_factory.mktemp("mcc-terminal") + source=(ROOT / "port/linux/game/mcc_terminal.c").read_text() + routines="\n".join(function(source,n) for n in ["mcc_terminal_scale","mcc_terminal_line_height","mcc_terminal_draw"]) + code=r''' +#include +#include +#include +typedef float real;typedef int boolean; +typedef struct {short x0,y0,x1,y1;} rectangle2d; +struct font_header {short ascending_height,descending_height,leading_height;}; +#define TRUE 1 +#define FALSE 0 +#define NONE (-1) +static int loaded,calls,draws,lookups; +static real current=1,used,origin_x,origin_y; +static rectangle2d drawn; +static struct font_header font; +static int mcc_cache_tags_loaded(void) {return loaded;} +static struct font_header *font_definition_get(long index) {if(index!=7)abort();lookups++;return &font;} +static void rasterizer_text_set_scale(real scale,real x,real y) {current=scale;calls++;if(scale!=1){origin_x=x;origin_y=y;}} +static void rasterizer_draw_string(rectangle2d const *b,void *clip,void *cursor,short height,char const *text) { + if(clip||cursor||height||strcmp(text,"100% unchanged"))abort();draws++;drawn=*b;used=current; +} +'''+routines+r''' +int main(int argc,char **argv) { + rectangle2d bounds={100,400,740,415};struct font_header saved;short h;int handled; + if(argc!=3)return 2;loaded=atoi(argv[1]);font.ascending_height=(short)atoi(argv[2]);saved=font; + h=mcc_terminal_line_height(7,font.ascending_height); + handled=mcc_terminal_draw(7,&bounds,"100% unchanged"); + if(memcmp(&saved,&font,sizeof(font))||current!=1)return 3; + if(!loaded && lookups)return 4; + printf("%d %d %d %d %.6f %.1f %.1f %d %d",h,handled,calls,draws,used,origin_x,origin_y,drawn.x1,drawn.y1); + return 0; +} +''' + path=work / "terminal.c";path.write_text(code) + binary=work / ("terminal.exe" if sys.platform=="win32" else "terminal") + command=[compiler,"-std=gnu89",str(path),"-o",str(binary)] + if sys.platform=="win32":command[1:1]=["--target=i686-pc-windows-msvc","-fuse-ld=lld"] + result=subprocess.run(command,capture_output=True,text=True) + assert result.returncode==0,result.stderr + return binary + + +@pytest.mark.parametrize("loaded,height",[(0,33),(0,15),(0,-1),(1,15),(1,12),(1,33),(1,30),(1,32767)]) +def test_terminal_scales_only_oversized_mcc_text(terminal_tool,loaded,height): + result=subprocess.run([str(terminal_tool),str(loaded),str(height)],capture_output=True,text=True) + assert result.returncode==0,result.stderr + values=list(map(float,result.stdout.split())) + if loaded and height>15: + assert values[:4]==[15,1,2,1] + assert values[4]==pytest.approx(15/height,abs=0.000001) + assert values[5:7]==[100,400] + assert values[7:]==[min(32767,int(100+640/(15/height))),min(32767,401+height)] + else: + assert values[:4]==[height,0,0,0] diff --git a/tools/test_mcc_native_smoke.py b/tools/test_mcc_native_smoke.py index 7f40f2faf5..1faf0299e9 100644 --- a/tools/test_mcc_native_smoke.py +++ b/tools/test_mcc_native_smoke.py @@ -8,7 +8,8 @@ @pytest.mark.parametrize("fatal", ["type is inconsistent with usage (you need to recompile scripts.)", "the scenario's scripts won't run", "EXCEPTION halt", - "EXCEPTION assert in units.c,#1512: control_data->weapon_index"]) + "EXCEPTION assert in units.c,#1512: control_data->weapon_index", + "backpack_1_argl: this is not a valid object name."]) def test_loader_success_cannot_hide_fatal_script_error(fatal): assert not check_result(LOADED + "\n" + fatal, MARKER, MARKER, "b40", 0)["passed"] diff --git a/tools/test_mcc_scripts.py b/tools/test_mcc_scripts.py index a2708b440d..a699e9325f 100644 --- a/tools/test_mcc_scripts.py +++ b/tools/test_mcc_scripts.py @@ -42,7 +42,7 @@ def scripts_tool(native_mcc_harness): @pytest.mark.parametrize("case", ["link", "alias", "extension", "segment", "core_segment", "global", "unknown", "bad_child", "child_salt", "short_data", "capacity", "full_capacity", - "negative_count", "unterminated", "skull_read", "skull_wrong_type", "skull_write"]) + "negative_count", "unterminated", "skull_read", "skull_wrong_type", "skull_write", "developer_read", "developer_cast", "developer_write", "skull_disable", "skull_enable", "skull_computed", "developer_capacity", "developer_allocation"]) def test_mcc_script_linking(scripts_tool, case): result = subprocess.run([str(scripts_tool), case], capture_output=True, text=True, timeout=10) assert result.returncode == 0, result.stdout + result.stderr diff --git a/tools/test_mcc_tag_validation.py b/tools/test_mcc_tag_validation.py index b9fd038c39..c71997a41e 100644 --- a/tools/test_mcc_tag_validation.py +++ b/tools/test_mcc_tag_validation.py @@ -82,6 +82,8 @@ def validation_tool(tmp_path_factory): "trimmed_structure", "graph_large", "graph_cycle", "graph_node_cycle", "graph_orphan", "bitmap_owned", "bitmap_unowned", "first_person_slots", "grenades_zero", "grenades_one", "grenades_two", "grenades_four", "grenades_excess", + "anchor_0", "anchor_4", "anchor_5", "anchor_6", "anchor_7", "anchor_8", "anchor_9", "anchor_-1", + "names_512", "names_513", "names_640", "syntax_full", "syntax_overflow", "syntax_outside", "syntax_overlap", "syntax_legacy"]) def test_mcc_validation(validation_tool, case): result = subprocess.run([str(validation_tool), case], capture_output=True, text=True, timeout=30) From e391e585427a6a22b983cf5f422b54936b53f16b Mon Sep 17 00:00:00 2001 From: chestahh Date: Sat, 10 Oct 2026 20:11:09 +0100 Subject: [PATCH 7/8] Fix MCC cinematic cameras and scenario completion (#241) (cherry picked from commit c6bb2366795c21e767ddb05f475c92e07c9eb54a) --- docs/mcc_maps.md | 48 ++++++++++++++++ port/linux/game/mcc_main.c | 16 +++++- port/linux/game/mcc_main.h | 1 + port/linux/game/mcc_tags.c | 31 ++++++++++- source/main/main.c | 3 + tools/test_mcc_cameras.py | 106 ++++++++++++++++++++++++++++++++++++ tools/test_mcc_lifecycle.py | 46 ++++++++++++++++ tools/test_mcc_ui.py | 1 + 8 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 tools/test_mcc_cameras.py diff --git a/docs/mcc_maps.md b/docs/mcc_maps.md index 86dd06d492..f9f581764f 100644 --- a/docs/mcc_maps.md +++ b/docs/mcc_maps.md @@ -704,6 +704,54 @@ network co-op test was performed for these changes. Android remains unverified locally: `ninja android` reports an unknown target in this Windows-only configuration. +## Solar Flare M0 cinematic and completion fixes + +The supplied M0 has a 100-degree field of view in camera `cin_x4` (index 3). +The native scripted camera and observer accept at most 90 degrees; the +observer also clamps its output to that maximum. The reported assertions +were reproduced as that camera became active. MCC metadata conversion now +limits wider, valid cinematic lenses to 90 degrees and logs the camera +index. This narrows those shots; it does not implement wider cinematic +lenses. Zero still selects the native default, and already supported +angles remain unchanged. Invalid angles (including NaN and infinity) and +out-of-bounds camera blocks are refused. Only the in-memory MCC copy is +changed; the map file and Xbox/CE camera code are untouched. + +M0's `cin_a` script calls `game_won` at the end of its cinematics. In local +play, an unknown campaign level previously fell through to the Xbox credits +movie immediately after loading and precaching the menu. A separate access +violation was reproduced through menu -> M0 -> menu, even after fixing the +camera: the cache worker was copying a pending read into protected cache +memory during that movie transition. The matching Windows symbols identify +`read_at` / `ReadFileEx` / `cached_map_issue_async_request` on the failing +thread. Direct startup into M0 did not reproduce this second failure. + +MCC scenario completion now queues the ordinary return to the menu, without +starting the Xbox campaign outro. Its helper lives in `mcc_main.c`, selected +by one additive dispatch in `main_roll_credits`. When MCC is inactive the +original statements execute unchanged. The existing network co-op win +branch still returns to its lobby before this path. No network message or +compatibility-version change is involved. This is an MCC-specific avoidance +of the credits/cache interaction, not a general change to the movie player +or background I/O. Xbox and CE completion remain unchanged. + +Automatic MCC campaign-package progression remains unsupported: after M0 +finishes, select M1 manually. An unrelated saved-map identity rejection is +normal, and the map's equipment/grenade warnings are not suppressed here. + +Regression tests cover camera bounds, default and supported lenses, wide +lenses, malformed floats, repeat conversion, and MCC versus legacy +completion dispatch. The Windows x86 release build and 889 automated tests +passed (six optional-fixture skips). Android could not be built locally: +this Windows configuration has no `android` target. + +The final Windows native run started at the menu, loaded the supplied M0, +played its entire cinematic sequence, returned to the menu and remained +there for approximately another minute before a normal timed exit. This +220-second test used one hidden instance at 60 FPS with audio decoding +enabled and output muted. It produced neither camera assertions nor an +access violation. No new network co-op playtest was performed for this fix. + ## Public format evidence These are wire-format references, not copied implementation code. Current diff --git a/port/linux/game/mcc_main.c b/port/linux/game/mcc_main.c index 4b9a4c4d3b..7e82ff361f 100644 --- a/port/linux/game/mcc_main.c +++ b/port/linux/game/mcc_main.c @@ -1,5 +1,5 @@ -/* Local MCC load failures recover through the native UI lifecycle. The - * failed map has already released its tags; no game is initialized for it. */ +/* MCC transitions use the native menu lifecycle, without selecting Xbox + * campaign progression or its credits movie for a standalone MCC scenario. */ #include "cseries.h" #include "errors.h" #include "main/main.h" @@ -7,6 +7,18 @@ #include "mcc_cache.h" #include "mcc_main.h" +boolean mcc_main_map_completed(void) +{ + if (!mcc_cache_tags_loaded()) return FALSE; + /* Unknown campaign levels reach main_roll_credits. MCC packages have + * no native next-level mapping: return normally instead of starting the + * Xbox outro immediately after menu precaching. That movie borrows and + * protects texture-cache memory while menu reads may still be pending. */ + error(_error_silent, "mcc: scenario completed; returning to the menu (select the next campaign map manually)"); + main_goto_main_menu(); + return TRUE; +} + boolean mcc_main_load_failed(char const *name) { if (!mcc_level_name(name)) return FALSE; diff --git a/port/linux/game/mcc_main.h b/port/linux/game/mcc_main.h index 491c0d7d57..781b550ae2 100644 --- a/port/linux/game/mcc_main.h +++ b/port/linux/game/mcc_main.h @@ -1,4 +1,5 @@ #ifndef MCC_MAIN_H #define MCC_MAIN_H boolean mcc_main_load_failed(char const *name); +boolean mcc_main_map_completed(void); #endif diff --git a/port/linux/game/mcc_tags.c b/port/linux/game/mcc_tags.c index 63212cc9f6..a5e11e220a 100644 --- a/port/linux/game/mcc_tags.c +++ b/port/linux/game/mcc_tags.c @@ -60,6 +60,35 @@ static int mcc_scenario_recordings(struct mcc_runtime *r, unsigned char *scenari return TRUE; } +static int mcc_scenario_cameras(struct mcc_runtime *r, unsigned char *scenario) +{ + uint32_t count, i; + unsigned char *points = mcc_block(r, scenario + 0x4F0, 0x68, &count); + float const native_maximum = 1.5707963267948966f; + if (count && !points) return FALSE; + for (i = 0; i < count; ++i) { + unsigned char *field = points + i * 0x68 + 0x40; + float angle; + memcpy(&angle, field, 4); + /* Zero selects the native default. Wide MCC cinematic lenses are + * valid authored data, but the native observer both validates and + * clamps to 90 degrees. Normalize the MCC copy before commands reach + * it; the Xbox/CE camera and its validation remain unchanged. */ + if (angle == 0.0f) continue; + if (!(angle >= 0.001f && angle < 3.1415926535897932f)) { + error(_error_silent, "mcc: cutscene camera #%lu has an invalid field of view", + (unsigned long)i); + return FALSE; + } + if (angle > native_maximum) { + memcpy(field, &native_maximum, 4); + error(_error_silent, "mcc: cutscene camera #%lu: field of view limited to 90 degrees", + (unsigned long)i); + } + } + return TRUE; +} + static int mcc_hud(struct mcc_runtime *r, uint32_t group, unsigned char *p) { unsigned char *block; @@ -132,7 +161,7 @@ int mcc_tags_prepare(struct mcc_runtime *r) return FALSE; } if (shader >= 0) memcpy(p + 0x24, &shader, 2); - if (group == 'scnr' && !mcc_scenario_recordings(r, p)) return FALSE; + if (group == 'scnr' && (!mcc_scenario_recordings(r, p) || !mcc_scenario_cameras(r, p))) return FALSE; if (group == 'scex') { uint32_t flags = mcc_word(p + 0x6C), new_group = 'schi'; if (!mcc_word(p + 0x54)) memcpy(p + 0x54, p + 0x60, 12); diff --git a/source/main/main.c b/source/main/main.c index 1bcccb40e7..538d3494d6 100644 --- a/source/main/main.c +++ b/source/main/main.c @@ -1926,6 +1926,9 @@ void main_load_ui_scenario( void main_roll_credits( void) { + /* port: standalone MCC scenarios finish through their own menu transition. */ + if (mcc_main_map_completed()) + return; error(_error_silent, "congratulations, you won the game!"); main_menu_load(); game_end_credits_start(); diff --git a/tools/test_mcc_cameras.py b/tools/test_mcc_cameras.py new file mode 100644 index 0000000000..0198fc800f --- /dev/null +++ b/tools/test_mcc_cameras.py @@ -0,0 +1,106 @@ +"""Exercise MCC camera admission using the real metadata normalizer.""" +from pathlib import Path +import math +import shutil +import struct +import subprocess +import sys + +import pytest + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="module") +def camera_tool(tmp_path_factory): + compiler = shutil.which("clang") + if not compiler: + pytest.skip("clang is required") + work = tmp_path_factory.mktemp("mcc-cameras") + (work / "cseries.h").write_text("#define TRUE 1\n#define FALSE 0\n#define NUMBEROF(a) (sizeof(a)/sizeof((a)[0]))\n") + (work / "errors.h").write_text("#define _error_silent 0\nvoid error(int,const char *,...);\n") + source = work / "test.c" + source.write_text(r''' +#include "mcc_runtime.h" +#include +#include +#include +int mcc_tags_prepare(struct mcc_runtime *); +void error(int level,char const *format,...) {(void)level;(void)format;} +int mcc_recording_prepare(unsigned char *p,uint32_t s,unsigned char v,unsigned char c,uint32_t *n) { + (void)p;(void)s;(void)v;(void)c;(void)n;abort();return 0; +} +void *mcc_runtime_pointer(struct mcc_runtime *r,uint32_t address,uint32_t size) { + uint32_t offset=address-r->report.tag_base; + return address>=r->report.tag_base && offset<=r->used && size<=r->used-offset ? r->tags+offset : NULL; +} +int main(int argc,char **argv) { + unsigned char data[4096];struct mcc_runtime r={0};FILE *f;int ok; + if(argc!=3)return 2; + f=fopen(argv[1],"rb");if(!f)return 3; + r.used=(uint32_t)fread(data,1,sizeof(data),f);fclose(f); + r.tags=r.tag_index=data;r.report.tag_base=0x50000000;r.report.tag_count=1; + ok=mcc_tags_prepare(&r); + f=fopen(argv[2],"wb");if(!f)return 4;fwrite(data,1,r.used,f);fclose(f); + printf("%d\n",ok);return 0; +} +''') + binary = work / ("camera.exe" if sys.platform == "win32" else "camera") + game = ROOT / "port/linux/game" + command = [compiler, "-std=gnu89", "-Wall", "-Wextra", "-Werror", "-Wno-multichar", + "-I" + str(work), "-I" + str(game), str(source), str(game / "mcc_tags.c"), "-o", str(binary)] + if sys.platform == "win32": + command[1:1] = ["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + result = subprocess.run(command, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return binary + + +def fixture(angles): + data = bytearray(0x600 + len(angles) * 0x68) + struct.pack_into(" Date: Sat, 10 Oct 2026 23:05:19 +0100 Subject: [PATCH 8/8] Isolate MCC map discovery from startup directory cleanup (#241) Use an owned platform directory handle for the MCC catalog so it cannot replace the shared iterator used by startup cleanup. Preserve map filtering, sorting and namespace isolation; add directory and handle-lifetime regression coverage. (cherry picked from commit 1d446580) --- docs/mcc_maps.md | 11 ++++++ port/linux/game/mcc_maps.c | 32 ++++++++++------ tools/test_mcc_maps.py | 78 ++++++++++++++++++++++---------------- 3 files changed, 77 insertions(+), 44 deletions(-) diff --git a/docs/mcc_maps.md b/docs/mcc_maps.md index f9f581764f..2ff5c7c89c 100644 --- a/docs/mcc_maps.md +++ b/docs/mcc_maps.md @@ -752,6 +752,17 @@ there for approximately another minute before a normal timed exit. This enabled and output muted. It produced neither camera assertions nor an access violation. No new network co-op playtest was performed for this fix. +## Independent MCC directory listing + +Upstream's directory fix revealed the same shared-iterator hazard in MCC's +catalog: startup profile cleanup can enumerate directories concurrently. +MCC discovery now owns and closes a separate platform directory handle, +without accessing the Xbox or CE catalogs. Catalog tests exercise filtering, +missing/empty directories, handle cleanup, sorting and rescan behavior. + +This change is confined to MCC discovery. It does not modify the shared +iterator or Xbox/Custom Edition catalogs, loaders, or networking. + ## Public format evidence These are wire-format references, not copied implementation code. Current diff --git a/port/linux/game/mcc_maps.c b/port/linux/game/mcc_maps.c index 2a116c3082..f72113165d 100644 --- a/port/linux/game/mcc_maps.c +++ b/port/linux/game/mcc_maps.c @@ -9,14 +9,13 @@ list, loader, resource directory or display index is used here. #include "cseries.h" #include "errors.h" -#include "tag_files/files.h" -#include "tag_files/tag_files.h" #include "mcc_cache_format.h" #include "mcc_maps.h" #include #include #include +#include /* main_set_map_name retains 63 characters including the namespace. */ #define MCC_MAP_FILENAME_MAXIMUM (63 - (sizeof(MCC_MAPS_LEVEL_PREFIX) - 1)) @@ -155,23 +154,34 @@ static int mcc_map_order(void const *left, void const *right) static void mcc_maps_scan(void) { - struct file_reference directory, file; - char name[MAXIMUM_FILENAME_LENGTH + 1], extension[MAXIMUM_FILENAME_LENGTH + 1]; + WIN32_FIND_DATAA data; + HANDLE find; if (mcc_menu_catalog.scanned) return; mcc_menu_catalog.scanned = TRUE; mcc_menu_catalog.count = 0; - file_reference_create_from_path(&directory, MCC_MAPS_DIRECTORY, TRUE); - find_files_start(0, &directory); - while (find_files_next(&file, NULL)) + /* Keep this listing independent of the game's shared file iterator: + startup profile cleanup can use that iterator on another thread. */ + find = FindFirstFileA(MCC_MAPS_DIRECTORY "*.*", &data); + if (find == INVALID_HANDLE_VALUE) + return; + do { - file_reference_get_name(&file, FLAG(_name_extension_bit), extension); - if (csstrcasecmp(extension, "map")) + char name[sizeof(data.cFileName) + 1]; + char *extension; + + if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue; - file_reference_get_name(&file, FLAG(_name_filename_bit), name); + memcpy(name, data.cFileName, sizeof(data.cFileName)); + name[sizeof(data.cFileName)] = 0; + extension = strrchr(name, '.'); + if (!extension || csstrcasecmp(extension + 1, "map")) + continue; + *extension = 0; mcc_map_register(name); - } + } while (FindNextFileA(find, &data)); + CloseHandle(find); qsort(mcc_menu_catalog.maps, (size_t)mcc_menu_catalog.count, sizeof(mcc_menu_catalog.maps[0]), mcc_map_order); error(_error_silent, "mcc maps: %d maps found in mcc_maps", mcc_menu_catalog.count); } diff --git a/tools/test_mcc_maps.py b/tools/test_mcc_maps.py index b808e48cc0..d6dfc28dd9 100644 --- a/tools/test_mcc_maps.py +++ b/tools/test_mcc_maps.py @@ -34,7 +34,6 @@ def catalog_tool(tmp_path_factory): if not compiler: pytest.skip("a C compiler is needed for MCC catalog tests") work = tmp_path_factory.mktemp("mcc-catalog") - (work / "tag_files").mkdir() (work / "cseries.h").write_text(r''' #ifndef MCC_TEST_CSERIES_H #define MCC_TEST_CSERIES_H @@ -61,21 +60,19 @@ def catalog_tool(tmp_path_factory): #endif ''') (work / "errors.h").write_text('#define _error_silent 0\nvoid error(short,const char *,...);\n') - (work / "tag_files/tag_files.h").write_text("/* No tag access in the menu catalog. */\n") - (work / "tag_files/files.h").write_text(r''' -struct file_reference {char name[256];}; -struct file_last_modification_date; -#define MAXIMUM_FILENAME_LENGTH 255 -enum {_name_directory_bit,_name_parent_directory_bit,_name_filename_bit,_name_extension_bit}; -struct file_reference *file_reference_create_from_path(struct file_reference *,char const *,boolean); -void find_files_start(unsigned long,struct file_reference const *); -boolean find_files_next(struct file_reference *,struct file_last_modification_date *); -char *file_reference_get_name(struct file_reference const *,unsigned long,char *); + (work / "xtl.h").write_text(r''' +typedef void *HANDLE; +typedef struct {unsigned long dwFileAttributes;char cFileName[256];} WIN32_FIND_DATAA; +#define FILE_ATTRIBUTE_DIRECTORY 16 +#define INVALID_HANDLE_VALUE ((HANDLE)-1) +HANDLE FindFirstFileA(char const *,WIN32_FIND_DATAA *); +int FindNextFileA(HANDLE,WIN32_FIND_DATAA *); +int CloseHandle(HANDLE); ''') source = work / "catalog.c" source_code = r''' #include "cseries.h" -#include "tag_files/files.h" +#include "xtl.h" #include "mcc_maps.h" #include #include @@ -84,7 +81,7 @@ def catalog_tool(tmp_path_factory): static char const *root; static char const *listing="list.txt"; static FILE *inventory; -static unsigned reads, largest, scans; +static unsigned reads, largest, scans, closed; struct mcc_runtime { unsigned char *tag_index; struct {uint32_t scenario_handle,tag_count;} report; @@ -110,30 +107,29 @@ def catalog_tool(tmp_path_factory): return fread(out,size,count,file); } void error(short priority,char const *format,...) {(void)priority;(void)format;} -struct file_reference *file_reference_create_from_path(struct file_reference *r,char const *path,boolean directory) { - if (!directory || strcmp(path,"d:\\mcc_maps\\")) exit(22); - strcpy(r->name,path);return r; -} -void find_files_start(unsigned long flags,struct file_reference const *directory) { +HANDLE FindFirstFileA(char const *pattern,WIN32_FIND_DATAA *data) { char path[1024]; - if (flags || strcmp(directory->name,"d:\\mcc_maps\\")) exit(23); - if (inventory) fclose(inventory); + if (strcmp(pattern,"d:\\mcc_maps\\*.*") || inventory) exit(23); snprintf(path,sizeof(path),"%s/mcc_maps/%s",root,listing); inventory=fopen(path,"r");scans++; + if (!inventory)return INVALID_HANDLE_VALUE; + if (FindNextFileA(inventory,data))return inventory; + fclose(inventory);inventory=NULL;return INVALID_HANDLE_VALUE; } -boolean find_files_next(struct file_reference *file,struct file_last_modification_date *unused) { - (void)unused; - if (!inventory || !fgets(file->name,sizeof(file->name),inventory)) return FALSE; - file->name[strcspn(file->name,"\r\n")]=0;return TRUE; +int FindNextFileA(HANDLE handle,WIN32_FIND_DATAA *data) { + if (!inventory || handle!=inventory)exit(24); + if (!fgets(data->cFileName,sizeof(data->cFileName),inventory))return FALSE; + data->cFileName[strcspn(data->cFileName,"\r\n")]=0; + data->dwFileAttributes=0; + if (data->cFileName[0]=='/') { + data->dwFileAttributes=FILE_ATTRIBUTE_DIRECTORY; + memmove(data->cFileName,data->cFileName+1,strlen(data->cFileName)); + } + return TRUE; } -char *file_reference_get_name(struct file_reference const *file,unsigned long flags,char *out) { - char const *dot=strrchr(file->name,'.'); - if (flags==FLAG(_name_extension_bit)) strcpy(out,dot ? dot+1 : ""); - else if (flags==FLAG(_name_filename_bit)) { - size_t n=dot ? (size_t)(dot-file->name) : strlen(file->name); - memcpy(out,file->name,n);out[n]=0; - } else exit(24); - return out; +int CloseHandle(HANDLE handle) { + if (!inventory || handle!=inventory)exit(24); + fclose(inventory);inventory=NULL;closed++;return TRUE; } int main(int argc,char **argv) { short count,i; @@ -200,7 +196,8 @@ def catalog_tool(tmp_path_factory): mcc_maps_rescan();printf("rescanned=%d\n",mcc_maps_count(FALSE)); } else if(argc>2) printf("found=%d\n",mcc_maps_find(argv[2])); printf("scans=%u\nreads=%u\nlargest_read=%u\n",scans,reads,largest); - if(inventory)fclose(inventory); + printf("closed=%u\n",closed); + if(inventory)return 33; return 0; } ''' @@ -279,6 +276,21 @@ def test_filename_identity_case_and_duplicate_filter(catalog_tool, tmp_path): assert maps == ["mcc_maps\\lower|1", "mcc_maps\\UPPER|1"] +def test_listing_skips_directories_and_closes_its_handle(catalog_tool, tmp_path): + install(tmp_path, {"folder.map": header(), "level.map": header()}) + (tmp_path / "mcc_maps/list.txt").write_text("/folder.map\nlevel.map\nno_extension\n") + fields, maps = run(catalog_tool, tmp_path) + assert maps == ["mcc_maps\\level|1"] + assert fields["closed"] == "1" + + +def test_missing_directory_has_no_handle_to_close(catalog_tool, tmp_path): + fields, maps = run(catalog_tool, tmp_path) + assert maps == [] + assert fields["scans"] == "1" + assert fields["closed"] == "0" + + def test_filename_length_limit(catalog_tool, tmp_path): install(tmp_path, {"a" * 54 + ".map": header(), "b" * 55 + ".map": header()}) fields, maps = run(catalog_tool, tmp_path)