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..7e2ecadd96 --- /dev/null +++ b/.github/workflows/mcc.yml @@ -0,0 +1,81 @@ +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/mcc_native_smoke.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_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_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_native_smoke.py + tools/test_mcc_recordings.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_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/.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..e5b9624dd0 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,13 @@ 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 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 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..2ff5c7c89c --- /dev/null +++ b/docs/mcc_maps.md @@ -0,0 +1,787 @@ +# 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 +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 +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`. + +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 +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. 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. + +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 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 + +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_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. | +| `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. | +| `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 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 +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 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 +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. +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. +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 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 +`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. + +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 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. +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. + +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 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, + 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 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; 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. +- 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. + +### Solar Flare, Spasm and Combat Revolved compatibility checks + +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. + +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`, +`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 +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 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: + +```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_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 +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. + +## 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. + +## 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. + +## 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 +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/game/mcc_audio.c b/port/linux/game/mcc_audio.c new file mode 100644 index 0000000000..aacb0e35b4 --- /dev/null +++ b/port/linux/game/mcc_audio.c @@ -0,0 +1,311 @@ +/* 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_resources.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; +} + +/* 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; + 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; + 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; + 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; + 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,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]; + permutation->unknown3=entry[3]; + permutation->samples.address=NULL; + permutation->sample_buffer_size=0; + if (!permutation->samples.size) continue; + data=malloc(permutation->samples.size); + stage="input allocation"; + if (!data) goto failed; + 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); + 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; + } + } + 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: 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; +} + +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..7dbab437c3 --- /dev/null +++ b/port/linux/game/mcc_bitmaps.c @@ -0,0 +1,668 @@ +/* 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 "mcc_resources.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 +#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, stream_end; +}; + +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; + texture->resource_handle=*handle;texture->resource_index=i; + } + 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,stream_offset=0,i; + if (bitmap->width<=0 || bitmap->height<=0 || bitmap->depth<=0 || bitmap->type<0 || bitmap->type>2 || + 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))) { + 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; + /* 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) 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; + 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+stream_offset; + 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; + textures->stream_end=MAX(textures->stream_end,stream_offset+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]; + texture->resource_handle=entry[3];texture->resource_index=n; + } + } + 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->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) +{ + 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..aa7d18c477 --- /dev/null +++ b/port/linux/game/mcc_cache.c @@ -0,0 +1,355 @@ +/* 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_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 +#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_campaign_dispose(); + 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_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); + 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; + 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; +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_campaign.c b/port/linux/game/mcc_campaign.c new file mode 100644 index 0000000000..5a7a9c3dd3 --- /dev/null +++ b/port/linux/game/mcc_campaign.c @@ -0,0 +1,397 @@ +/* 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 "cache/sound_cache.h" +#include "sound/sound_definitions.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; + 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)) { + 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); +} + +/* 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) +{ + 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}}, + {{_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) +{ + 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_TOTAL_PAYLOAD + 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_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, 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: + 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, 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_TOTAL_PAYLOAD; + bytes = mcc_checkpoint_word(footer + 8); + 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 + 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 - 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; +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, 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_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; +} + +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..4a798b2609 --- /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)!=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"); + 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..72422a0a7a --- /dev/null +++ b/port/linux/game/mcc_hud_draw.c @@ -0,0 +1,94 @@ +/* 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; +} + +/* 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 new file mode 100644 index 0000000000..e460c78957 --- /dev/null +++ b/port/linux/game/mcc_hud_draw.h @@ -0,0 +1,26 @@ +#ifndef MCC_HUD_DRAW_H +#define MCC_HUD_DRAW_H + +#include "cseries.h" +#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. */ +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); + +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_main.c b/port/linux/game/mcc_main.c new file mode 100644 index 0000000000..7e82ff361f --- /dev/null +++ b/port/linux/game/mcc_main.c @@ -0,0 +1,32 @@ +/* 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" +#include "interface/ui_widget.h" +#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; + 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..781b550ae2 --- /dev/null +++ b/port/linux/game/mcc_main.h @@ -0,0 +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_maps.c b/port/linux/game/mcc_maps.c new file mode 100644 index 0000000000..f72113165d --- /dev/null +++ b/port/linux/game/mcc_maps.c @@ -0,0 +1,292 @@ +/* +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 "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)) +#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) +{ + WIN32_FIND_DATAA data; + HANDLE find; + + if (mcc_menu_catalog.scanned) + return; + mcc_menu_catalog.scanned = TRUE; + mcc_menu_catalog.count = 0; + /* 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 + { + char name[sizeof(data.cFileName) + 1]; + char *extension; + + if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + continue; + 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); +} + +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..ff0f5bb6d0 --- /dev/null +++ b/port/linux/game/mcc_network.c @@ -0,0 +1,172 @@ +#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" +#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); } +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) +{ + 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)); + 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); + 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..cfbe99e4ac --- /dev/null +++ b/port/linux/game/mcc_network.h @@ -0,0 +1,13 @@ +/* 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); +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_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_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_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 new file mode 100644 index 0000000000..40c69cfb67 --- /dev/null +++ b/port/linux/game/mcc_runtime.h @@ -0,0 +1,61 @@ +/* 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; + void *resources; +}; + +/* 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..077d8c55a6 --- /dev/null +++ b/port/linux/game/mcc_script_runtime.inl @@ -0,0 +1,191 @@ +/* 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" +#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; + 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..f66b6bfbc1 --- /dev/null +++ b/port/linux/game/mcc_scripts.c @@ -0,0 +1,310 @@ +/* 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 "mcc_campaign.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. 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); +} + +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 } +}; + +/* 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); +} + +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); +} + +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; + 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; + 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; +} + +/* 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_bandanna")); +} + +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; + 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; + 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); + syntax->data = nodes; + 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; + /* 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))) { + 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; + } + } + 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 { + 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; + } + 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; + } + 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..035e3ebfd8 --- /dev/null +++ b/port/linux/game/mcc_scripts.h @@ -0,0 +1,19 @@ +#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 +#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); +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..11b45f9ee1 --- /dev/null +++ b/port/linux/game/mcc_tag_validate.c @@ -0,0 +1,615 @@ +/* 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; + /* 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; + } + 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_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; + 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..a5e11e220a --- /dev/null +++ b/port/linux/game/mcc_tags.c @@ -0,0 +1,195 @@ +/* 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 "mcc_recordings.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_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_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; + 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; + case 'scnr': size = 0x5B0; 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 == '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); + 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_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/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..19678bcab3 --- /dev/null +++ b/port/linux/game/mcc_vorbis.c @@ -0,0 +1,243 @@ +/* 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, previous_granule=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; + /* 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)>(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; + short packet[8192 * 2]; + uint32_t decoded=0; + int ok=0, boundary_known=0, boundary_bias=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; + for (;;) { + int got=stb_vorbis_get_frame_short_interleaved(&stream,stream.channels,packet,8192*2); + uint32_t retain; + if (stream.error) goto done; + if (!got) break; + decoded+=(uint32_t)got; + if (!(stream.page_flag&PAGEFLAG_last_page) && stream.last_seg_which==stream.end_seg_with_known_loc) { + int difference=(int)decoded-(int)stream.known_loc_for_packet; + /* A long block followed by a short block emits up to its right + * window's start, beyond the packet's centre/granule. This offset + * disappears again after the short packet. Accept only that exact + * window transition, not arbitrary drifting page positions. */ + if (difference!=0 && difference!=(stream.blocksize_1-stream.blocksize_0)/4) goto done; + boundary_bias=difference;boundary_known=1; + } + retain=(uint32_t)got; + if (retain>expected-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: + free(pcm); + vorbis_deinit(&stream); + free(normalized); + return ok; +} + +void mcc_vorbis_free(short *samples) +{ + free(samples); +} diff --git a/port/linux/game/mcc_vorbis.h b/port/linux/game/mcc_vorbis.h new file mode 100644 index 0000000000..631d4065b9 --- /dev/null +++ b/port/linux/game/mcc_vorbis.h @@ -0,0 +1,11 @@ +#ifndef MCC_VORBIS_H +#define MCC_VORBIS_H +#include + +/* 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..fa2c347966 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,8 @@ 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: + case _distributed_message_mcc_campaign: break; default: return FALSE; @@ -3989,6 +3992,8 @@ 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_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; @@ -4163,6 +4168,12 @@ 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_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 0ba551911c..b403721a2c 100644 --- a/port/linux/game/network_distributed.h +++ b/port/linux/game/network_distributed.h @@ -118,6 +118,10 @@ 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, + /* MCC-only campaign gravity and breadcrumb state. */ + _distributed_message_mcc_campaign = 84, 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..a93f2a10a4 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 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/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..c107c18ada --- /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>8192 || description.height>8192 || description.depth>512 || + (description.depth>1 && (description.width>512 || description.height>512)) || + !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; + 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..94be0287a8 100644 --- a/port/linux/src/xbox_textures.c +++ b/port/linux/src/xbox_textures.c @@ -794,6 +794,119 @@ 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; + /* 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(); + 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 +1031,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_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/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..9c0b728962 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; @@ -1040,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) { @@ -1754,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: @@ -2047,6 +2066,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; @@ -2067,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/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..7be58a9348 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); @@ -968,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/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/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/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..538d3494d6 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."); } @@ -1919,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/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_campaign.c b/tools/harness/tests/mcc_campaign.c new file mode 100644 index 0000000000..735d21ee05 --- /dev/null +++ b/tools/harness/tests/mcc_campaign.c @@ -0,0 +1,194 @@ +/* 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 "sound/sound_definitions.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 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]; +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],"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); + 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 new file mode 100644 index 0000000000..7e443dd2c0 --- /dev/null +++ b/tools/harness/tests/mcc_network.c @@ -0,0 +1,155 @@ +#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_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;}; +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; +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&&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){ + 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++; +} +/* 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],"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&&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_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..566e4aa998 --- /dev/null +++ b/tools/harness/tests/mcc_script_parameters.c @@ -0,0 +1,225 @@ +#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 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,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) {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);} +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(!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;} + 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..e867490911 --- /dev/null +++ b/tools/harness/tests/mcc_scripts.c @@ -0,0 +1,330 @@ +/* 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 + +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; +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 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; + 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") || !strcmp(name,"set")) 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; +} + +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}; + 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; + 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.capacity=runtime.used+65536; + runtime.tags = malloc(runtime.capacity); + 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") || !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; + } + 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; + } + 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); + 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_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; + { + 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); + 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")) { + 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") || !strcmp(argv[1], "core_segment")) { + char long_segment[200]; + 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(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; + 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(segment_index, 0, FALSE); + CHECK(returned == -123 && !segment_events); /* argument expression still yielding */ + 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(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(segment_index, 0, TRUE); + CHECK(returned == TRUE && segment_events == 3 && strlen(last_event) == 151); + arguments[0] = 0; + extension->evaluate(segment_index, 0, TRUE); + CHECK(returned == FALSE && segment_events == 3); + loaded = FALSE; + 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); + 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..f5f2b75019 --- /dev/null +++ b/tools/harness/tests/mcc_tag_validation.c @@ -0,0 +1,329 @@ +/* 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 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; }; +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 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), + 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_BLOCK(struct test_root, object_names, name_schema, 512), + 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}; +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}; +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; + 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); + 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; + } + } + 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); + 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/mcc_native_smoke.py b/tools/mcc_native_smoke.py new file mode 100644 index 0000000000..4bdefadc26 --- /dev/null +++ b/tools/mcc_native_smoke.py @@ -0,0 +1,149 @@ +"""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", "exception assert", "assertion failed", + "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): + 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/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..66e1c2ab49 --- /dev/null +++ b/tools/test_mcc_bitmap_flags.py @@ -0,0 +1,206 @@ +"""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 + #define MCC_BITMAP_STORAGE_LIMIT 0x40000000u +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;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);} +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", "mcc_bitmaps_read"]) + 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(!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; + bitmap.mipmap_count=10;bitmap.pixels_size=349525; + for(i=0;i 0 + assert int(report["bsps"]) > 0 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("\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;} +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);} +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_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)); + 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],"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||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++; + 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+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)); + 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", "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 new file mode 100644 index 0000000000..1767705e0a --- /dev/null +++ b/tools/test_mcc_geometry.py @@ -0,0 +1,160 @@ +"""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..a36c85580f --- /dev/null +++ b/tools/test_mcc_hud.py @@ -0,0 +1,280 @@ +"""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) + + +@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_lifecycle.py b/tools/test_mcc_lifecycle.py new file mode 100644 index 0000000000..638705893b --- /dev/null +++ b/tools/test_mcc_lifecycle.py @@ -0,0 +1,301 @@ +"""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_mcc_completion_defers_menu_load_without_starting_xbox_credits(tmp_path): + compiler = shutil.which("clang") or shutil.which("cc") + if not compiler: + pytest.skip("a C compiler is required") + native = (ROOT / "source/main/main.c").read_text() + adapter = (ROOT / "port/linux/game/mcc_main.c").read_text() + source = r''' +typedef int boolean; +#define TRUE 1 +#define FALSE 0 +#define _error_silent 0 +static int active,queued,loaded,movies,logged; +static boolean mcc_cache_tags_loaded(void) {return active;} +static void error(int level,char const *text,...) {(void)level;(void)text;logged++;} +static void main_goto_main_menu(void) {queued++;} +static void main_menu_load(void) {loaded++;active=0;} +static void game_end_credits_start(void) {movies++;} +''' + function(adapter, "mcc_main_map_completed") + function(native, "main_roll_credits") + r''' +int main(void) { + int i; + /* Repeated MCC loads and completion do not retain transition state. */ + for(i=0;i<2;i++) { + active=1;main_roll_credits(); + if(queued!=i+1 || loaded!=i || movies || logged!=i+1 || !active)return 1; + /* The main loop performs the queued transition, after this returns. */ + main_menu_load(); + } + /* With MCC inactive, both Xbox and CE retain the exact native sequence. */ + for(i=0;i<2;i++) { + main_roll_credits(); + if(queued!=2 || loaded!=3+i || movies!=1+i || logged!=3+i)return 2; + } + return 0; +} +''' + path = tmp_path / "completion.c" + path.write_text(source) + binary = tmp_path / ("completion.exe" if sys.platform == "win32" else "completion") + flags = (["--target=i686-pc-windows-msvc", "-fuse-ld=lld"] if sys.platform == "win32" else []) + built = subprocess.run([compiler, *flags, "-std=gnu89", "-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_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..d6dfc28dd9 --- /dev/null +++ b/tools/test_mcc_maps.py @@ -0,0 +1,365 @@ +"""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 / "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 "xtl.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, closed; +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;} +HANDLE FindFirstFileA(char const *pattern,WIN32_FIND_DATAA *data) { + char path[1024]; + 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; +} +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; +} +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; + 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); + printf("closed=%u\n",closed); + if(inventory)return 33; + 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_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) + 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;iweapon_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"] + + +@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_network.py b/tools/test_mcc_network.py new file mode 100644 index 0000000000..ddef894f86 --- /dev/null +++ b/tools/test_mcc_network.py @@ -0,0 +1,50 @@ +"""Exercise MCC's independent inventory packets, prediction and throw ledger.""" +import os +from pathlib import Path +import shutil +import subprocess + +import pytest + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="module") +def mcc_network_tool(tmp_path_factory): + clang = shutil.which("clang") + if not clang: + pytest.skip("clang is required for MCC network checks") + work = tmp_path_factory.mktemp("mcc-network") + # Compile all real new implementation bodies against deterministic + # transport/clock/object adapters; no game data or sockets are needed. + implementation = "\n".join(line for line in (ROOT / "port/linux/game/mcc_network.c").read_text().splitlines() + if not line.startswith("#include")) + damage = (ROOT / "port/linux/game/mcc_network_damage.inc").read_text() + prefix = (ROOT / "tools/harness/tests/mcc_network.c").read_text() + source = work / "network.c" + source.write_text(prefix.replace("/* MCC_INVENTORY_IMPLEMENTATION */", implementation) + .replace("/* MCC_DAMAGE_IMPLEMENTATION */", damage)) + binary = work / ("network.exe" if os.name == "nt" else "network") + target = (["--target=i686-pc-windows-msvc", "-fuse-ld=lld", "-D_CRT_SECURE_NO_WARNINGS"] + if os.name == "nt" else ["-m32"]) + command = [clang, *target, "-std=gnu99", "-O1", "-Wall", "-Wextra", "-Werror", + "-fsanitize=undefined", "-fsanitize-trap=undefined", str(source), "-o", str(binary)] + if os.name != "nt": + command.append("-lm") + build = subprocess.run(command, capture_output=True, text=True) + assert build.returncode == 0, build.stdout + build.stderr + return binary + + +@pytest.mark.parametrize("case", ["host", "receive", "prediction", "rewind_host", "rewind_client", "damage", "legacy", "campaign"]) +def test_mcc_network(mcc_network_tool, case): + run = subprocess.run([str(mcc_network_tool), case], capture_output=True, text=True) + assert run.returncode == 0, run.stdout + run.stderr + + +def test_legacy_inventory_wire_unchanged(): + from harness import structure + source = (ROOT / "port/linux/game/network_objects.c").read_text() + inventory = structure(source, "distributed_inventory") + assert "grenade_counts[NUMBER_OF_UNIT_GRENADE_TYPES]" in inventory + assert "mcc" not in inventory diff --git a/tools/test_mcc_parameters.py b/tools/test_mcc_parameters.py new file mode 100644 index 0000000000..29c8500cef --- /dev/null +++ b/tools/test_mcc_parameters.py @@ -0,0 +1,20 @@ +"""MCC generated parameter metadata and the real MCC interpreter adapter.""" +import subprocess + +import pytest +from test_mcc_scripts import native_mcc_harness + + +@pytest.fixture(scope="module") +def parameters_tool(native_mcc_harness): + return native_mcc_harness("mcc_script_parameters") + + +@pytest.mark.parametrize("case", ["metadata", "count", "type", "pointer", "slot", "orphan", "flags", + "call_arity", "call_type", "global_scope", "legacy", "return", "snapshot", + "nested_scope", "recursive_arguments", "lists", "set", "overflow", "ai_cast", + "global_capacity", "global_overflow", "maximum", "disabled", "predicate", "script_kind", + "high_node", "negative_count", "sleep_self", "sleep_named", "sleep_missing", "sleep_finished"]) +def test_mcc_parameter_metadata_and_runtime(parameters_tool, case): + result = subprocess.run([str(parameters_tool), case], capture_output=True, text=True, timeout=10) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/tools/test_mcc_pause.py b/tools/test_mcc_pause.py new file mode 100644 index 0000000000..f54cc09be7 --- /dev/null +++ b/tools/test_mcc_pause.py @@ -0,0 +1,436 @@ +"""Read generated MCC pause tags through the actual native widget ABI. + +The builder runs intact with synthetic font/art handles. These tests require +no proprietary map assets and never enter the Xbox or Custom Edition loaders. +""" +from pathlib import Path +import shutil +import subprocess +import sys + +import pytest + +from harness import function, structure + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="module") +def pause_tool(tmp_path_factory): + clang = shutil.which("clang") + if not clang: + pytest.skip("clang is needed for the MCC pause builder tests") + work = tmp_path_factory.mktemp("mcc-pause") + native = (ROOT / "source/interface/ui_widget.c").read_text() + wrap_hook_start = native.index("\n\t{\n\t\textern boolean mcc_pause_text_wrap_needed(long);") + wrap_hook_end = native.index("\n\tif (string_has_icons_to_draw(*text))", wrap_hook_start) + wrap_hook = native[wrap_hook_start:wrap_hook_end] + tag_groups = (ROOT / "source/tag_files/tag_groups.h").read_text() + native_types = "\n".join(structure(tag_groups, name) for name in [ + "tag_block", "tag_reference", "tag_data"]) + native_types += "\n" + "\n".join(structure(native, name) for name in [ + "ui_widget_event_handler_reference", "ui_widget_child_reference", + "ui_widget_conditional_reference", "ui_widget_game_data_input_reference", + "ui_widget_search_and_replace_reference", "ui_widget_definition"]) + (work / "tag_files").mkdir() + (work / "text").mkdir() + (work / "tag_files/tag_groups.h").write_text("/* Native tag structures already included by harness. */\n") + text_groups = (ROOT / "source/text/text_group.h").read_text() + (work / "text/text_group.h").write_text("\n".join(structure(text_groups, name) for name in [ + "string_list", "string_list_entry"])) + (work / "cseries.h").write_text(r''' +#ifndef MCC_PAUSE_TEST_CSERIES +#define MCC_PAUSE_TEST_CSERIES +#include +#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 "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_resources.py b/tools/test_mcc_resources.py new file mode 100644 index 0000000000..4cd390fd84 --- /dev/null +++ b/tools/test_mcc_resources.py @@ -0,0 +1,78 @@ +"""Generated MCC resource files: identity, bounds, missing files and lifetime.""" +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 resource_tool(tmp_path_factory): + clang = shutil.which("clang") + if not clang: + pytest.skip("clang required") + work = tmp_path_factory.mktemp("mcc-resources") + source = work / "main.c" + source.write_text(r''' +#include "mcc_runtime.h" +#include "mcc_resources.h" +#include +#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(" +#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..a699e9325f --- /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", "core_segment", "global", "unknown", + "bad_child", "child_salt", "short_data", "capacity", "full_capacity", + "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 + + +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..c71997a41e --- /dev/null +++ b/tools/test_mcc_tag_validation.py @@ -0,0 +1,90 @@ +"""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", + "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) + 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..32a0b57572 --- /dev/null +++ b/tools/test_mcc_texture_bridge.py @@ -0,0 +1,228 @@ +"""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;} +static int mcc_scenario_recordings(struct mcc_runtime *r,unsigned char *p){(void)r;(void)p;abort();return FALSE;} +static int mcc_scenario_cameras(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){ + 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..0476a1eb9c --- /dev/null +++ b/tools/test_mcc_ui_teams.py @@ -0,0 +1,251 @@ +"""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_campaign_dispose() cleanup(NULL) +#define mcc_resources_dispose(p) cleanup(p) +#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..b6aedd6354 --- /dev/null +++ b/tools/test_mcc_vorbis.py @@ -0,0 +1,385 @@ +"""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) + + +@pytest.mark.parametrize("channels", [1, 2]) +@pytest.mark.parametrize("frames", [256, 280, 304, 320]) +def test_short_final_block_trims_preceding_overlap(decoder, tmp_path, channels, frames): + complete = decode(decoder, tmp_path, stream(channels=channels, transition=True, final_frames=320)) + assert complete is not None + actual = decode(decoder, tmp_path, stream(channels=channels, transition=True, final_frames=frames)) + assert actual is not None + assert struct.unpack_from("