233 Commits

Author SHA1 Message Date
renovate[bot] 16027e7124 fix(deps): update rust crate toml to v1
Test / Lint (push) Failing after 1m54s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
Test / Lint (pull_request) Failing after 1m49s
Test / Test (pull_request) Has been skipped
Test / Render parity (pull_request) Has been skipped
2026-06-25 00:05:23 +00:00
renovate[bot] 27af3806b3 fix(deps): update all digest updates
Test / Lint (pull_request) Failing after 1m53s
Test / Test (pull_request) Has been skipped
Test / Render parity (pull_request) Has been skipped
Docs Deploy / Build and Deploy MkDocs (push) Successful in 34s
Test / Lint (push) Failing after 1m50s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
2026-06-24 00:02:21 +00:00
Valentin Popov 021b1c8dac feat(vulkan-smoke): run native swapchain acquire/present
Docs Deploy / Build and Deploy MkDocs (push) Successful in 34s
Test / Lint (push) Failing after 2m24s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
2026-06-24 01:53:39 +04:00
Valentin Popov 278567d6de revert: "ci: fix unreadable_directory_produces_error error"
Docs Deploy / Build and Deploy MkDocs (push) Successful in 34s
Test / Lint (push) Successful in 2m35s
Test / Test (push) Failing after 2m53s
Test / Render parity (push) Has been skipped
This reverts commit 7eced77483.
2026-06-24 01:45:15 +04:00
Valentin Popov 7eced77483 ci: fix unreadable_directory_produces_error error
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s
Test / Lint (push) Failing after 2m32s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
2026-06-24 01:33:57 +04:00
Valentin Popov d41add32c4 feat: create Vulkan swapchain probe
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
Test / Lint (push) Successful in 2m36s
Test / Test (push) Failing after 2m52s
Test / Render parity (push) Has been skipped
2026-06-24 01:05:31 +04:00
Valentin Popov 159731664f feat: probe Vulkan logical device creation 2026-06-24 00:14:26 +04:00
Valentin Popov e6b7fa1896 feat: probe live Vulkan runtime capabilities 2026-06-24 00:05:46 +04:00
Valentin Popov 0e127117e9 feat: require created Vulkan smoke surface 2026-06-23 23:59:07 +04:00
Valentin Popov 4d19728c39 feat: create native smoke window handles 2026-06-23 23:56:40 +04:00
Valentin Popov 54f07ee3be feat: audit native smoke artifacts 2026-06-23 23:51:38 +04:00
Valentin Popov ed2b540abf feat: add target triple to native smoke report 2026-06-23 23:48:31 +04:00
Valentin Popov 00ae9067d8 feat: require swapchain recreation evidence in smoke reports 2026-06-23 23:45:08 +04:00
Valentin Popov c71e706d69 feat: add native smoke window preflight 2026-06-23 23:42:20 +04:00
Valentin Popov aa2133d82b feat: add Vulkan surface preflight to smoke runner 2026-06-23 23:38:09 +04:00
Valentin Popov 71ead678c0 feat: add Vulkan instance probe to smoke runner 2026-06-23 23:35:41 +04:00
Valentin Popov f15ea95bf2 feat: add Vulkan loader probe to smoke runner 2026-06-23 23:33:42 +04:00
Valentin Popov 99bcbf388f refactor: remove xtask native smoke generator 2026-06-23 23:30:26 +04:00
Valentin Popov 227d95fc49 feat: add Vulkan smoke runner entrypoint 2026-06-23 23:27:47 +04:00
Valentin Popov dceea70122 ci: add native smoke artifact schema 2026-06-23 23:22:29 +04:00
Valentin Popov fd452f6016 ci: add acceptance artifact metadata 2026-06-23 23:18:36 +04:00
Valentin Popov 1d0244c3e4 ci: enforce reproducible Rust toolchain 2026-06-23 23:16:50 +04:00
Valentin Popov 5d9e1cbe38 feat: add Vulkan frame submission plan 2026-06-23 23:13:52 +04:00
Valentin Popov 0e76c2ed7c ci: add built-in supply chain policy fallback 2026-06-23 23:10:16 +04:00
Valentin Popov 4c1edef21b test: verify headless dependency closure 2026-06-23 23:05:01 +04:00
Valentin Popov e6778d43af feat: add Vulkan shader manifest validation 2026-06-23 23:01:34 +04:00
Valentin Popov ec8f6599fc feat: add Vulkan swapchain planning policy 2026-06-23 22:57:03 +04:00
Valentin Popov f5fae8e84a feat: add Vulkan surface bootstrap boundary 2026-06-23 22:53:54 +04:00
Valentin Popov a0a4089e4b feat: expose native window handles 2026-06-23 22:50:32 +04:00
Valentin Popov dc7e72961a feat: add Vulkan instance bootstrap plan 2026-06-23 22:47:20 +04:00
Valentin Popov 8ea1fd5c18 feat: probe Vulkan loader boundary 2026-06-23 22:43:54 +04:00
Valentin Popov 69c032acca feat: add Vulkan capability selection boundary 2026-06-23 22:40:01 +04:00
Valentin Popov 9cc24e715d fix: close stage 0-2 synthetic gates 2026-06-23 22:32:50 +04:00
Valentin Popov f8e447ffee feat: close stage 0-2 audit groundwork
Remove legacy SDL/OpenGL adapters from the workspace and introduce winit/Vulkan adapter boundaries for the rendered composition root.

Add reproducible toolchain and xtask CI coverage for formatting, tests, clippy, docs, policy, deny, acceptance auditing, and hosted OS matrix evidence.

Strengthen Stage 1 data contracts with byte-first paths, VFS hardening, structured diagnostics, RsLi writer/edit scaffolding, corpus reporting, and resource error classification.

Advance Stage 2 asset preparation by moving mission loading through assets/runtime boundaries, materializing prototype graph data, preserving provenance, and adding inspection/viewer integration.

Record the Stage 0-2 audit input, acceptance roadmap, coverage updates, and documentation notes for follow-up evidence.
2026-06-23 22:05:16 +04:00
Valentin Popov 83d763dd70 fix: trace runtime scheduler phases
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
Test / Lint (push) Failing after 1m15s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
2026-06-22 17:32:56 +04:00
Valentin Popov 162de8ccab fix: require manifests for licensed gates
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s
Test / Lint (push) Failing after 1m16s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
2026-06-22 17:29:33 +04:00
Valentin Popov 0b23cf48e7 fix: use canonical sha256 world hashes 2026-06-22 17:11:21 +04:00
Valentin Popov 7356238ffb fix: harden render command validation 2026-06-22 17:05:45 +04:00
Valentin Popov 42441082f0 fix: cap fixed-step catch-up 2026-06-22 17:02:00 +04:00
Valentin Popov ccd61c05b0 fix: expose configurable rsli read profiles 2026-06-22 16:58:59 +04:00
Valentin Popov 813beec7be fix: preserve nres gaps during edits 2026-06-22 16:55:10 +04:00
Valentin Popov 91c7a8a14e fix: make corpus reports explicit and fallible 2026-06-22 16:49:32 +04:00
Valentin Popov 8b91a0bfbf fix: make core error displays actionable 2026-06-22 16:41:21 +04:00
Valentin Popov fb97405e0c fix: decode payloads outside resource lock 2026-06-22 16:36:50 +04:00
Valentin Popov d579b696e6 fix: cap decoded payload cache bytes 2026-06-22 16:34:14 +04:00
Valentin Popov aa1b809bd8 fix: strengthen resource fingerprints 2026-06-22 16:31:57 +04:00
Valentin Popov f69c893a40 fix: harden path lookup and mark gl backend gap 2026-06-22 16:12:57 +04:00
Valentin Popov 5436727961 docs: mark stage 4 runtime gaps explicit 2026-06-22 16:04:35 +04:00
Valentin Popov be41fa839f fix: harden resource and world state correctness 2026-06-22 16:02:16 +04:00
Valentin Popov 8e5e46b7b3 fix: make ci locked and isolate licensed tests 2026-06-22 15:55:37 +04:00
Valentin Popov d0bdbaa1ed feat: implement FParkan architecture foundation
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s
Test / Lint (push) Failing after 1m14s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
Add the modular fparkan workspace, domain crates, adapters, apps, xtask policy/CI, acceptance evidence, and licensed corpus gates for the macOS-focused roadmap foundation.
2026-06-22 13:13:32 +04:00
Valentin Popov 7416fdc7e9 Merge pull request 'chore(deps): update actions/checkout action to v7' (#16) from renovate/actions-checkout-7.x into devel
Docs Deploy / Build and Deploy MkDocs (push) Successful in 34s
Test / Lint (push) Failing after 1m8s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
Reviewed-on: #16
2026-06-22 03:09:59 +04:00
Valentin Popov 78fc5f1deb docs: rewrite MkDocs documentation
Docs Deploy / Build and Deploy MkDocs (push) Successful in 34s
Test / Lint (push) Failing after 1m7s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
2026-06-22 01:58:51 +04:00
Valentin Popov 50c2cf4686 chore: remove Python tooling and resource viewer
Docs Deploy / Build and Deploy MkDocs (push) Successful in 2m6s
Test / Lint (push) Failing after 1m10s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
2026-06-22 00:35:19 +04:00
renovate[bot] a63290fbc8 chore(deps): update actions/checkout action to v7
Test / Lint (push) Failing after 1m2s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
Test / Lint (pull_request) Failing after 1m1s
Test / Test (pull_request) Has been skipped
Test / Render parity (pull_request) Has been skipped
2026-06-19 00:05:00 +00:00
renovate[bot] 96a25b6c0e fix(deps): update rust crate glow to 0.17
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
Test / Lint (pull_request) Failing after 1m1s
Test / Test (pull_request) Has been skipped
Test / Render parity (pull_request) Has been skipped
Docs Deploy / Build and Deploy MkDocs (push) Successful in 46s
Test / Lint (push) Failing after 1m5s
RenovateBot / renovate (push) Successful in 25s
2026-03-08 00:02:26 +00:00
Valentin Popov f4262cf369 Merge pull request 'fix(deps): update rust crate toml to v1' (#14) from renovate/toml-1.x into devel
Docs Deploy / Build and Deploy MkDocs (push) Successful in 33s
Test / Lint (push) Failing after 1m3s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
RenovateBot / renovate (push) Successful in 28s
Reviewed-on: #14
2026-03-02 18:42:03 +04:00
renovate[bot] 9b100b8fc3 chore(deps): update actions/upload-artifact action to v7
Test / Lint (pull_request) Failing after 1m6s
Test / Test (pull_request) Has been skipped
Test / Render parity (pull_request) Has been skipped
Test / Lint (push) Has been cancelled
Test / Test (push) Has been cancelled
Test / Render parity (push) Has been cancelled
Docs Deploy / Build and Deploy MkDocs (push) Has been cancelled
2026-02-27 00:01:47 +00:00
renovate[bot] 9fceeb9a0a fix(deps): update rust crate toml to v1
Test / Lint (push) Failing after 1m2s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
Test / Lint (pull_request) Failing after 1m4s
Test / Test (pull_request) Has been skipped
Test / Render parity (pull_request) Has been skipped
2026-02-26 00:01:14 +00:00
renovate[bot] 4b7f1a16b9 fix(deps): update all digest updates
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
Test / Lint (pull_request) Failing after 59s
Test / Test (pull_request) Has been skipped
Test / Render parity (pull_request) Has been skipped
Docs Deploy / Build and Deploy MkDocs (push) Successful in 32s
Test / Lint (push) Failing after 1m2s
RenovateBot / renovate (push) Successful in 28s
2026-02-25 00:01:21 +00:00
Valentin Popov ada3b903ad chore: update docs deployment branch from master to devel
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m9s
Test / Lint (push) Failing after 2m11s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
RenovateBot / renovate (push) Successful in 1m7s
2026-02-24 22:42:18 +00:00
Valentin Popov 31d849ddbf updated docs
Test / Lint (push) Failing after 1m57s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
2026-02-19 16:10:57 +04:00
Valentin Popov 4ef08d0bf6 feat: add terrain-core, tma, and unitdat crates with parsing functionality
- Introduced `terrain-core` crate for loading and processing terrain mesh data.
- Added `tma` crate for parsing mission files, including footer and object records.
- Created `unitdat` crate for reading unit data files with validation of structure.
- Implemented error handling and tests for all new crates.
- Documented object registry format and rendering pipeline in specifications.
2026-02-19 16:07:01 +04:00
Valentin Popov 598137ed13 feat(resource-viewer): добавить новый ресурсный просмотрщик с базовой функциональностью
Test / Lint (push) Failing after 2m30s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
feat(nres): улучшить структуру архива с добавлением заголовка и информации о записях
feat(rsli): добавить поддержку заголовка библиотеки и улучшить обработку записей
2026-02-19 10:51:54 +00:00
Valentin Popov cb0ca2f2f0 feat(render-demo): добавить отображение FPS в заголовок окна и stdout в интерактивном режиме
Test / Lint (push) Failing after 1m16s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
2026-02-19 10:27:10 +00:00
Valentin Popov 7346e695c4 feat(render-demo): обновить поддержку OpenGL с добавлением выбора между GLES2 и Core 3.3
Test / Lint (push) Failing after 1m17s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
2026-02-19 10:17:14 +00:00
Valentin Popov bb827c3928 feat: Refactor code structure and enhance functionality across multiple crates 2026-02-19 10:09:18 +00:00
Valentin Popov efab61a45c feat(render-core): add default UV scale and refactor UV mapping logic
Test / Lint (push) Failing after 1m12s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
- Introduced a constant `DEFAULT_UV_SCALE` for UV scaling.
- Refactored UV mapping in `build_render_mesh` to use the new constant.
- Simplified `compute_bounds` functions by extracting common logic into `compute_bounds_impl`.

test(render-core): add tests for rendering with empty and multi-node models

- Added tests to verify behavior when building render meshes from models with no slots and multiple nodes.
- Ensured UV scaling is correctly applied in tests.

feat(render-demo): add FOV argument and improve error handling

- Added a `--fov` command-line argument to set the field of view.
- Enhanced error messages for texture resolution failures.
- Updated MVP computation to use the new FOV parameter.

fix(rsli): improve error handling in LZH decompression

- Added checks to prevent out-of-bounds access in LZH decoding logic.

refactor(texm): streamline texture parsing and decoding tests

- Created a helper function `build_texm_payload` for constructing test payloads.
- Added tests for various texture formats including RGB565, RGB556, ARGB4444, and Luminance Alpha.
- Improved error handling for invalid TEXM headers and mip bounds.
2026-02-19 09:46:23 +00:00
Valentin Popov 0d7ae6a017 Документирование и обновление спецификаций
Test / Lint (push) Failing after 1m10s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
- Обновлены спецификации `runtime-pipeline`, `sound`, `terrain-map-loading`, `texture`, `ui` и `wear`.
- Добавлены разделы о статусе покрытия и оставшихся задачах для достижения 100% завершенности.
- Внесены уточнения по архитектурным ролям, минимальным контрактам и требованиям к toolchain для каждой подсистемы.
- Уточнены форматы данных и правила взаимодействия между компонентами системы.
2026-02-19 11:07:04 +04:00
Valentin Popov a281ffa32e feat: Enhance model and texture loading with improved error handling and new features
Test / Lint (push) Failing after 1m10s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
- Introduced `LoadedModel` and `LoadedTexture` structs for better encapsulation of model and texture data.
- Added functions to load models and textures from archives, including support for resolving textures based on materials and wear entries.
- Implemented error handling for missing textures, materials, and wear entries.
- Updated the rendering pipeline to support texture loading and binding, including command-line arguments for texture customization.
- Enhanced the `texm` crate with new decoding capabilities for various pixel formats, including indexed textures.
- Added tests for texture decoding and loading to ensure reliability and correctness.
- Updated documentation to reflect changes in the material and texture resolution process.
2026-02-19 05:19:18 +04:00
Valentin Popov 18d4c6cf9f feat(render-parity): add deterministic frame comparison tool
- Introduced `render-parity` crate for comparing rendered frames against reference images.
- Added command-line options for specifying manifest and output directory.
- Implemented image comparison metrics: mean absolute difference, maximum absolute difference, and changed pixel ratio.
- Created a configuration file `cases.toml` for defining test cases with global defaults and specific parameters.
- Added functionality to capture frames from `render-demo` and save diff images on discrepancies.
- Updated documentation to include usage instructions and CI model for automated testing.
2026-02-19 05:02:26 +04:00
Valentin Popov 0e19660eb5 Refactor documentation structure and add new specifications
- Updated MSH documentation to reflect changes in material, wear, and texture specifications.
- Introduced new `render.md` file detailing the render pipeline process.
- Removed outdated sections from `runtime-pipeline.md` and redirected to `render.md`.
- Added detailed specifications for `Texm` texture format and `WEAR` wear table.
- Updated navigation in `mkdocs.yml` to align with new documentation structure.
2026-02-19 04:46:23 +04:00
Valentin Popov 8a69872576 Refactor code structure for improved readability and maintainability
Test / Lint (push) Successful in 1m8s
Test / Test (push) Successful in 1m15s
2026-02-12 11:07:25 +00:00
Valentin Popov aa68906a3d feat: добавить экспорт в формат OBJ для рендеринга террейна и опциональных полигонов 2026-02-12 10:36:15 +00:00
Valentin Popov 8bf3b7b209 feat: добавить 3D рендерер для террейна с поддержкой Land.msh и Land.map 2026-02-12 10:24:42 +00:00
Valentin Popov 669fb40a70 Add terrain map documentation validator
This commit introduces a new Python script, `terrain_map_doc_validator.py`, which validates terrain and map documentation assumptions against actual game data. The validator checks for the presence and correctness of various data chunks in the `Land.msh` and `Land.map` files, reporting any issues found during the validation process. It also generates a summary report of the validation results, including counts of errors and warnings, and statistics related to the map and mesh data.
2026-02-12 10:17:41 +00:00
Valentin Popov 9c0df3d299 feat: добавить скрипт для детерминированного аудита FXID "absolute parity" 2026-02-12 08:22:51 +00:00
Valentin Popov 4c4f542fc2 Merge branch 'master' into renovate/actions-setup-python-6.x
Test / Lint (pull_request) Successful in 1m2s
Test / Test (pull_request) Successful in 59s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m4s
Test / Lint (push) Successful in 1m4s
Test / Test (push) Successful in 58s
RenovateBot / renovate (push) Successful in 22s
2026-02-12 04:52:10 +04:00
renovate[bot] 4c9d772b03 chore(deps): update actions/setup-python action to v6
Test / Lint (push) Successful in 51s
Test / Lint (pull_request) Successful in 53s
Test / Test (push) Successful in 52s
Test / Test (pull_request) Successful in 51s
2026-02-12 00:01:33 +00:00
renovate[bot] 097a915f35 fix(deps): update all digest updates
Test / Lint (pull_request) Successful in 1m3s
Test / Test (pull_request) Successful in 50s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m10s
Test / Lint (push) Successful in 1m0s
Test / Test (push) Successful in 58s
2026-02-12 00:01:28 +00:00
Valentin Popov c691de0dd0 fix: обновить срок действия авторских прав в документации и улучшить параметры rsync для развертывания
Test / Lint (pull_request) Successful in 56s
Test / Test (pull_request) Successful in 59s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 26s
Test / Lint (push) Successful in 57s
Test / Test (push) Successful in 52s
RenovateBot / renovate (push) Successful in 1m32s
2026-02-11 23:16:15 +00:00
Valentin Popov 92818ce0c4 fix: обновить путь развертывания в конфигурации rsync для корректной работы
Test / Lint (pull_request) Successful in 1m8s
Test / Test (pull_request) Successful in 49s
Docs Deploy / Build and Deploy MkDocs (push) Failing after 25s
Test / Lint (push) Successful in 54s
Test / Test (push) Successful in 55s
2026-02-11 23:08:37 +00:00
Valentin Popov 6676cfdd8d feat: обновить параметры SSH для развертывания документации с использованием rsync
Test / Lint (pull_request) Successful in 58s
Test / Test (pull_request) Successful in 53s
Docs Deploy / Build and Deploy MkDocs (push) Failing after 25s
Test / Lint (push) Successful in 52s
Test / Test (push) Successful in 51s
2026-02-11 23:02:03 +00:00
Valentin Popov 8b639ee6c9 feat: добавить установку rsync и openssh-client для развертывания документации
Test / Lint (pull_request) Successful in 52s
Test / Test (pull_request) Successful in 1m13s
Docs Deploy / Build and Deploy MkDocs (push) Failing after 26s
Test / Lint (push) Successful in 1m0s
Test / Test (push) Successful in 53s
2026-02-11 22:49:30 +00:00
Valentin Popov a58dea5499 feat: добавить рабочий процесс для развертывания документации MkDocs при пуше в ветку master
Test / Lint (pull_request) Successful in 47s
Test / Test (pull_request) Successful in 49s
Docs Deploy / Build and Deploy MkDocs (push) Failing after 1m29s
Test / Lint (push) Successful in 53s
Test / Test (push) Successful in 52s
2026-02-11 22:41:22 +00:00
Valentin Popov 615891d550 feat: обновить заголовки разделов в документации по FXID и NRes для улучшения структуры
Test / Lint (pull_request) Successful in 46s
Test / Test (pull_request) Successful in 48s
Test / Lint (push) Successful in 48s
Test / Test (push) Successful in 49s
2026-02-11 22:10:43 +00:00
Valentin Popov 481ff1c06d Implement feature X to enhance user experience and fix bug Y in module Z 2026-02-11 22:06:56 +00:00
Valentin Popov 7702d800a0 feat: улучшить документацию по материалам и текстурам, добавить детали о сборке и парсинге 2026-02-11 22:04:43 +00:00
Valentin Popov 3c06e768d6 feat: добавить поддержку атомарной замены файлов для Windows и тесты на максимальную длину имени 2026-02-11 22:00:46 +00:00
Valentin Popov 70ed6480c2 Refactor materials and Texm documentation for clarity and completeness
- Updated the structure and content of the materials and Texm documentation to provide a comprehensive overview of the material subsystem in the engine.
- Enhanced sections on identifiers, architecture, material layout, and runtime storage.
- Improved explanations of material attributes, animation modes, and parsing behavior.
- Added detailed specifications for toolchain interactions, including lossless write rules and validation recommendations.
- Included pseudocode examples for parsing MAT0 and Texm formats to aid in understanding.
2026-02-11 21:50:33 +00:00
Valentin Popov 662b292b5b feat: обновить методы обработки данных и улучшить обработку ошибок в библиотеке 2026-02-11 21:43:40 +00:00
Valentin Popov 3410b54793 feat: добавить тесты для проверки структурных инвариантов и корректности сортировки в RsLi 2026-02-11 21:21:32 +00:00
Valentin Popov 041b1a6cb3 Добавлены спецификации для сетевой подсистемы, системы звука, загрузки ландшафта, интерфейса пользователя и пайплайна выполнения. Обновлен файл навигации mkdocs.yml для включения новых документов. 2026-02-11 21:12:05 +00:00
Valentin Popov 5035d02220 Add MSH geometry export and preview rendering tools
Test / Lint (push) Successful in 46s
Test / Test (push) Successful in 41s
- Implemented msh_export_obj.py for exporting NGI MSH geometry to Wavefront OBJ format, including model selection and geometry extraction.
- Added msh_preview_renderer.py for rendering NGI MSH models to binary PPM images, featuring a primitive software renderer with customizable parameters.
- Both tools utilize the same NRes parsing logic and provide command-line interfaces for listing models and exporting or rendering geometry.
2026-02-10 23:27:43 +00:00
Valentin Popov ba1789f106 fix: обработка выхода за пределы индекса сортировки в архиве и улучшение декодирования LZSS с поддержкой XOR
Test / Lint (push) Successful in 47s
Test / Test (push) Successful in 41s
2026-02-10 08:57:00 +00:00
Valentin Popov 842f4a8569 Implement LZSS decompression with optional XOR decryption
- Added `lzss_decompress_simple` function for LZSS decompression in `lzss.rs`.
- Introduced `XorState` struct and `xor_stream` function for XOR decryption in `xor.rs`.
- Updated `mod.rs` to include new LZSS and XOR modules.
- Refactored `parse_library` function in `parse.rs` to utilize the new XOR decryption functionality.
- Cleaned up and organized code in `lib.rs` by removing redundant functions and structures.
- Added tests for new functionality in `tests.rs`.
2026-02-10 08:38:58 +00:00
Valentin Popov ce6e30f727 feat: добавить библиотеку common с ресурсами и буферами вывода; обновить зависимости в nres и rsli 2026-02-10 08:26:49 +00:00
Valentin Popov 4af183ad74 feat: добавить новые тесты для обработки не-NRes байтов и минимальной структуры архива
Test / Lint (pull_request) Successful in 59s
Test / Test (pull_request) Successful in 58s
Test / Lint (push) Successful in 53s
Test / Test (push) Successful in 48s
RenovateBot / renovate (push) Successful in 1m3s
2026-02-09 23:56:30 +00:00
Valentin Popov ab413bd751 fix: добавить проверку на наличие архивов в тестах для nres и rsli 2026-02-09 23:54:30 +00:00
Valentin Popov b5e6fad3c3 fix: исправить ссылки на репозитории в разделе Contributing & Support
Test / Lint (push) Successful in 51s
Test / Test (push) Failing after 1m12s
2026-02-09 23:40:21 +00:00
Valentin Popov c69cad6a26 feat: добавить начальный README с описанием проекта и инструкциями
Test / Lint (push) Successful in 53s
Test / Test (push) Failing after 54s
2026-02-09 23:36:32 +00:00
Valentin Popov a24910791e feat: добавить README для библиотеки nres с описанием функционала и тестирования 2026-02-09 23:15:43 +00:00
Valentin Popov 371a060eb6 Refactor tests and move them to a dedicated module
- Moved the test suite from `lib.rs` to a new `tests.rs` file for better organization.
- Added a `SyntheticRsliEntry` struct to facilitate synthetic test cases.
- Introduced `RsliBuildOptions` struct to manage options for building RsLi byte arrays.
- Implemented various utility functions for file handling, data compression, and bit manipulation.
- Enhanced the `rsli_read_unpack_and_repack_all_files` test to validate all RsLi archives.
- Added new tests for synthetic entries covering all packing methods, overlay handling, and validation error cases.
2026-02-09 23:11:11 +00:00
Valentin Popov e08b5f3853 feat: add initial implementation of rsli crate
Test / Lint (push) Failing after 1m30s
Test / Test (push) Has been skipped
- Created Cargo.toml for the rsli crate with flate2 dependency.
- Implemented ResourceData enum for handling borrowed and owned byte slices.
- Added OutputBuffer trait and its Vec<u8> implementation for writing data.
- Defined a comprehensive Error enum for error handling in the library.
- Developed the Library struct to manage resource entries and provide methods for loading and unpacking resources.
- Implemented various packing methods and decompression algorithms, including LZSS and Deflate.
- Added tests for validating the functionality of the rsli library against sample data.
2026-02-09 22:58:16 +00:00
Valentin Popov 5a97f2e429 feat: удалить файл конфигурации dependabot 2026-02-09 22:51:08 +00:00
Valentin Popov 9e2dcb44a6 feat: обновить конфигурацию CI для тестирования и линтинга кода
Test / Lint (push) Failing after 1m8s
Test / Test (push) Has been skipped
2026-02-09 22:47:25 +00:00
Valentin Popov 828106ba81 feat: добавить скрипты для инициализации тестовых данных и настройки окружения
Test / cargo test (push) Failing after 58s
2026-02-09 22:39:12 +00:00
Valentin Popov a7dd18fa1d feat: удалить устаревшие файлы и директории из проекта 2026-02-10 02:07:52 +04:00
Valentin Popov f8cca32968 feat: изменить язык документации на русский 2026-02-10 02:05:27 +04:00
Valentin Popov ef93237724 Add .gitignore for Python and project-specific files; implement archive roundtrip validator
Test / cargo test (push) Failing after 50s
- Updated .gitignore to include common Python artifacts and project-specific files.
- Added `archive_roundtrip_validator.py` script for validating NRes and RsLi formats against real game data.
- Created README.md for the tools directory, detailing usage and supported signatures.
- Enhanced nres.md with practical nuances and empirical checks for game data.
2026-02-10 01:58:16 +04:00
Valentin Popov 58a896221f feat: обновление навигации в документации, добавление разделов для 3D моделей, текстур и эффектов 2026-02-10 01:49:09 +04:00
Valentin Popov 3f48f53bd5 feat: добавление документации по эффектам и частицам 2026-02-10 01:48:59 +04:00
Valentin Popov 2953f0c8c9 feat: добавление документации по модели ресурсов MSH/AniMesh 2026-02-10 01:47:19 +04:00
Valentin Popov 022ec608f5 feat: добавление документации по текстурам и материалам 2026-02-10 01:44:01 +04:00
Valentin Popov 54c94fddb5 Add detailed documentation for NRes and RsLi resource formats
Test / cargo test (push) Failing after 41s
- Introduced a comprehensive markdown file `nres.md` detailing the structure, header, and operations of the NRes and RsLi formats.
- Updated `mkdocs.yml` to reflect the new documentation structure, consolidating NRes and RsLi under a single entry.
2026-02-10 00:30:25 +04:00
Valentin Popov 0def311fd1 feat: обновление документации по алгоритмам декомпрессии и добавление файлов .gitkeep в директории libs и tools 2026-02-05 03:28:03 +04:00
Valentin Popov 2f157d0972 feat: добавление файлов .gitkeep в директории libs и tools 2026-02-05 01:44:31 +04:00
Valentin Popov 8f57a8f0f9 feat: добавление файлов конфигурации Zig и обновление .gitignore
Test / cargo test (push) Failing after 47s
2026-02-05 01:40:47 +04:00
Valentin Popov 40e7d88fd0 Add NRes format documentation and decompression algorithms
Test / cargo test (push) Failing after 40s
- Created `huffman_decompression.md` detailing the Huffman decompression algorithm used in NRes, including context structure, block modes, and decoding methods.
- Created `overview.md` for the NRes format, outlining file structure, header details, file entries, and packing algorithms.
- Updated `mkdocs.yml` to include new documentation files in the navigation structure.
2026-02-05 01:32:24 +04:00
Valentin Popov afe6b9a29b feat: remove Rust project 2026-02-05 00:37:59 +04:00
renovate[bot] 6a46fe9825 chore(deps): update actions/checkout action to v6
Test / cargo test (pull_request) Successful in 1m36s
Test / cargo test (push) Successful in 1m45s
RenovateBot / renovate (push) Successful in 1m50s
2026-01-30 14:16:24 +00:00
Valentin Popov 7818a7ef3f chore: update renovate workflow to include GITHUB_COM_TOKEN
Test / cargo test (push) Successful in 1m43s
RenovateBot / renovate (push) Successful in 25s
2026-01-30 18:15:52 +04:00
Valentin Popov 15f2a73e95 chore: wire RENOVATE_LOG_LEVEL
RenovateBot / renovate (push) Successful in 21s
Test / cargo test (push) Successful in 1m35s
2026-01-30 04:35:32 +04:00
Valentin Popov 2890b69678 migrate renovate config to gitea
RenovateBot / renovate (push) Successful in 1m51s
Test / cargo test (push) Successful in 1m34s
2026-01-30 04:27:02 +04:00
Valentin Popov 27e9d2b39c Move CI to Gitea Actions
Test / cargo test (push) Successful in 1m37s
2026-01-30 04:00:58 +04:00
Valentin Popov b283e2a8df Update dependencies and fix clippy warnings
Mirror / mirror (push) Failing after 7s
Test / cargo test (push) Successful in 1m39s
2026-01-30 03:29:08 +04:00
Valentin Popov 9dcce90201 chore: update dependencies and fix clippy warnings
Mirror / mirror (push) Failing after 1m45s
Test / cargo test (push) Successful in 1m33s
- refresh Cargo.lock to latest compatible crates
- simplify u32->u64 conversion in libnres
- use is_multiple_of in unpacker list validation
2026-01-19 20:52:54 +04:00
renovate[bot] 7c876faf12 Update Rust crate console to v0.16.1 (#48)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 13:25:23 +00:00
renovate[bot] 39c66e698e Update Rust crate log to v0.4.28 (#47)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 05:03:11 +00:00
renovate[bot] abac84a008 Update Rust crate image to v0.25.8 (#46)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-03 20:21:26 +00:00
renovate[bot] b44217d4af Update Rust crate clap to v4.5.47 (#45)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-03 05:25:25 +00:00
renovate[bot] c268e4c205 Update all digest updates (#41)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 12:23:15 +04:00
renovate[bot] 8aabe74eb2 Update Rust crate thiserror to v2.0.15 (#39)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-17 10:13:52 +00:00
Valentin Popov 84f2175fd2 Merge pull request #33 from valentineus/renovate/all-digest
Update all digest updates
2025-08-13 18:16:56 +04:00
renovate[bot] 307b9c6d90 Update all digest updates 2025-08-13 13:45:03 +00:00
renovate[bot] 7de26b16d4 Update Rust crate clap to v4.5.41 (#32)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-10 19:47:12 +00:00
Valentin Popov 52f2ad43e6 Merge pull request #29 from valentineus/renovate/all-digest
Update all digest updates
2025-07-09 03:23:23 +04:00
renovate[bot] c4dec3fe4c Update all digest updates 2025-07-08 20:30:48 +00:00
Valentin Popov e51edcb561 Update dependencies in Cargo.lock 2025-06-14 23:02:49 +00:00
Valentin Popov 2273fd4263 Merge pull request #7 from valentineus/nres
Обновление структуры проекта
2025-06-15 02:42:55 +04:00
renovate[bot] d4f104cf5e Update Rust crate clap to v4.5.40 (#28)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 13:27:38 +00:00
renovate[bot] 7f41a51f2a Update all digest updates (#27)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 03:58:39 +00:00
renovate[bot] e97610a8ac Update Rust crate clap to v4.5.38 (#26)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 06:45:14 +00:00
renovate[bot] ee02d922ae Update Rust crate miette to v7.6.0 (#25)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 14:41:46 +00:00
renovate[bot] dbd7b6bf33 Update all digest updates (#24)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 18:14:47 +00:00
renovate[bot] 949c0aa087 Update all digest updates (#14)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 09:37:22 +00:00
renovate[bot] 4f29af53b6 Update Rust crate console to v0.15.11 (#13)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-02 05:23:27 +00:00
renovate[bot] 1d62740d59 Update Rust crate clap to v4.5.31 (#12)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 22:22:28 +00:00
Valentin Popov d274602104 Merge branch 'master' into nres 2025-02-23 17:23:33 +04:00
Valentin Popov 8bc39d10b1 Updated dependencies 2025-02-23 17:22:30 +04:00
Valentin Popov 88faa6e3ea Merge branch 'master' into nres 2025-02-22 14:19:02 +04:00
renovate[bot] 66705ba4f0 Update Rust crate log to v0.4.26 (#11)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 10:51:06 +00:00
renovate[bot] bb4c217ee2 Update all digest updates (#10)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 12:35:42 +00:00
renovate[bot] c83822e353 Update Rust crate clap to v4.5.30 (#9)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 03:03:44 +00:00
renovate[bot] 130ee8df5b Update Rust crate clap to v4.5.29 (#8)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-12 02:55:33 +00:00
Valentin Popov 8d8653133b Обновление структуры проекта 2025-02-08 01:11:02 +00:00
Valentin Popov 94d2f8a512 Обновление зависимостей 2025-02-08 00:44:59 +00:00
Valentin Popov 215a093344 Updated Renovate config 2025-02-05 03:43:58 +04:00
Valentin Popov 3de1575082 Merge pull request #5 from valentineus/renovate/all-digest 2025-02-04 05:49:24 +04:00
renovate[bot] aa8e1184bf Update Rust crate clap to v4.5.28 2025-02-04 01:47:42 +00:00
Valentin Popov feb7ebe722 Merge pull request #4 from valentineus/renovate/all-digest
Update Rust crate miette to v7.5.0
2025-02-01 17:58:58 +04:00
renovate[bot] becadef5ee Update Rust crate miette to v7.5.0 2025-02-01 04:26:56 +00:00
Valentin Popov a4b36e1aea Merge pull request #3 from valentineus/renovate/all-digest
Update all digest updates
2025-01-30 04:34:02 +04:00
renovate[bot] c7b099b596 Update all digest updates 2025-01-30 00:26:30 +00:00
Valentin Popov 48a08445e7 Added mirror 2025-01-30 04:25:11 +04:00
Valentin Popov 694de5edfa Moved Renocate config 2025-01-30 01:59:31 +04:00
Valentin Popov 0dc37e9604 Outdated CI and Renovate configurations have been removed, and a new Dependabot configuration file for dependency management has been added. 2025-01-24 20:50:13 +04:00
renovate[bot] 3d2e970225 Update Rust crate clap to v4.5.27 2025-01-21 00:01:51 +00:00
Valentin Popov d90b9830bc Updated all dependencies 2025-01-20 20:18:27 +00:00
renovate[bot] f91e1bda22 Update Rust crate serde_json to v1.0.137 2025-01-20 00:02:56 +00:00
renovate[bot] e9a0fd718f Update Rust crate log to v0.4.25 2025-01-15 00:03:21 +00:00
renovate[bot] 509ce2d83d Update all digest updates 2025-01-10 23:44:03 +00:00
renovate[bot] 391756b77d Update all digest updates 2025-01-10 21:04:54 +00:00
renovate[bot] 035153c7c0 Update all digest updates 2025-01-07 21:04:58 +00:00
renovate[bot] 885a593829 Update Rust crate serde to v1.0.217 2024-12-27 21:02:46 +00:00
renovate[bot] 7c3c8cc969 Update all digest updates 2024-12-21 21:03:04 +00:00
renovate[bot] 00c62a9909 Update Rust crate thiserror to v2.0.8 2024-12-18 21:03:04 +00:00
renovate[bot] c2899d27af Update Rust crate console to v0.15.10 2024-12-16 15:42:52 +00:00
renovate[bot] e60fdd1958 Update Rust crate thiserror to v2.0.7 2024-12-14 21:02:34 +00:00
renovate[bot] dd6d440ba5 Update Rust crate serde to v1.0.216 2024-12-11 21:04:41 +00:00
renovate[bot] 36a082ba18 Update all digest updates 2024-12-08 21:03:54 +00:00
renovate[bot] 09689a937c Update all digest updates 2024-12-03 21:01:39 +00:00
renovate[bot] 39f6479415 Update Rust crate miette to v7.4.0 2024-11-27 21:02:44 +00:00
renovate[bot] 01a2a47370 Update Rust crate miette to v7.3.0 2024-11-26 21:05:22 +00:00
renovate[bot] 4cd42afa37 Update Rust crate serde_json to v1.0.133 2024-11-17 21:05:34 +00:00
renovate[bot] 298aa954b9 Update Rust crate clap to v4.5.21 2024-11-13 21:01:52 +00:00
renovate[bot] 910deb6c17 Update all digest updates 2024-11-12 21:01:58 +00:00
Valentin Popov 4a22e2177e Merge pull request 'Update Rust crate thiserror to v2' (!36) from renovate/thiserror-2.x into master
Reviewed-on: #36
2024-11-11 15:10:34 +03:00
renovate[bot] 729c972573 Update Rust crate thiserror to v2 2024-11-10 21:05:05 +00:00
renovate[bot] 250d78a955 Update Rust crate thiserror to v1.0.69 2024-11-10 21:04:56 +00:00
Valentin Popov 03f2d762bb Merge pull request 'Update ghcr.io/renovatebot/renovate Docker tag to v39' (!34) from renovate/ghcr.io-renovatebot-renovate-39.x into master
Reviewed-on: #34
2024-11-06 09:43:20 +03:00
renovate[bot] fcaa729544 Update all digest updates 2024-11-05 21:02:13 +00:00
renovate[bot] 8c2a6e2c19 Update ghcr.io/renovatebot/renovate Docker tag to v39 2024-11-04 21:02:13 +00:00
renovate[bot] daa2efba89 Update Rust crate thiserror to v1.0.66 2024-11-01 21:03:36 +00:00
renovate[bot] b5748505ef Update Rust crate serde to v1.0.214 2024-10-28 21:02:55 +00:00
renovate[bot] d305b1f005 Update all digest updates 2024-10-22 21:01:55 +00:00
renovate[bot] 2cfba4891c Update Rust crate serde_json to v1.0.132 2024-10-19 21:01:55 +00:00
renovate[bot] 777d3814d3 Update Rust crate serde_json to v1.0.131 2024-10-18 23:23:57 +00:00
renovate[bot] 784ceeebdf Update Rust crate serde_json to v1.0.130 2024-10-18 21:02:00 +00:00
renovate[bot] e3675555ea Update all digest updates 2024-10-17 21:02:30 +00:00
renovate[bot] 91104e214f Update Rust crate image to v0.25.3 2024-10-16 21:04:18 +00:00
renovate[bot] 9198b18652 Update Rust crate clap to v4.5.20 2024-10-08 21:04:51 +00:00
renovate[bot] 1ad7949828 Update Rust crate clap to v4.5.19 2024-10-01 21:03:54 +00:00
renovate[bot] b98f01a810 Update Rust crate thiserror to v1.0.64 2024-09-24 09:34:04 +00:00
renovate[bot] fa88050a52 Update all digest updates 2024-09-23 21:04:33 +00:00
renovate[bot] 1123c8a56e Update all digest updates 2024-09-15 21:07:25 +00:00
renovate[bot] 2eb6333552 Update Rust crate serde to v1.0.209 2024-08-24 12:51:04 +00:00
renovate[bot] c5224e006f Update Rust crate serde_json to v1.0.127 2024-08-23 21:04:54 +00:00
renovate[bot] 79599f3cf4 Update Rust crate clap to v4.5.16 2024-08-15 23:00:19 +00:00
renovate[bot] 7acf99b9d6 Update all digest updates 2024-08-15 21:03:43 +00:00
renovate[bot] ec542703b4 Update Rust crate serde to v1.0.207 2024-08-12 21:02:14 +00:00
renovate[bot] ee1cdda38b Update Rust crate serde_json to v1.0.124 2024-08-11 21:42:34 +00:00
renovate[bot] 293a1de413 Update all digest updates 2024-08-11 21:04:17 +00:00
renovate[bot] 6635d4da9a Update Rust crate clap to v4.5.15 2024-08-10 21:04:17 +00:00
renovate[bot] f549769fcf Update all digest updates 2024-08-08 21:05:42 +00:00
renovate[bot] c0a56acc0c Update Rust crate serde_json to v1.0.122 2024-08-02 21:05:34 +00:00
renovate[bot] a136dc5fa4 Update Rust crate clap to v4.5.13 2024-07-31 22:13:23 +00:00
renovate[bot] 1b13f2acfc Update Rust crate clap to v4.5.12 2024-07-31 21:03:54 +00:00
renovate[bot] 6c127ce028 Update Rust crate serde_json to v1.0.121 2024-07-29 21:03:12 +00:00
Valentin Popov bc2e051741 Merge branch 'master' into renovate/ghcr.io-renovatebot-renovate-38.x 2024-07-26 17:12:49 +03:00
renovate[bot] 9abd2a4558 Update ghcr.io/renovatebot/renovate Docker tag to v38 2024-07-25 21:03:45 +00:00
renovate[bot] f267a56fd0 Update Rust crate clap to v4.5.11 2024-07-25 21:03:42 +00:00
renovate[bot] 1d592418af Update Rust crate clap to v4.5.10 2024-07-23 21:02:15 +00:00
renovate[bot] 3448f0f930 Update Rust crate image to v0.25.2 2024-07-21 21:04:37 +00:00
Valentin Popov 039ed238a6 Added Gitea CI testing 2024-07-19 18:23:35 +04:00
Valentin Popov b7349f9df9 Added CI check 2024-07-19 13:08:47 +00:00
Valentin Popov 12c7f0284e Added DevContainer 2024-07-19 13:08:46 +00:00
renovate[bot] 5c9a691495 Update Rust crate miette to v7 2024-07-19 12:43:23 +00:00
renovate[bot] bf8be5c045 Update all digest updates 2024-07-19 12:41:15 +00:00
Valentin Popov ee8a5fc02b Added Gitea 2024-07-19 16:39:08 +04:00
Valentin Popov a990de90fe Deleted vendor folder 2024-07-19 16:37:58 +04:00
Valentin Popov 3d48cd3f81 Initial MkDocs 2024-02-06 02:26:50 +04:00
Valentin Popov 78d6eca336 Initial GitHub Actions 2024-02-06 02:20:26 +04:00
7442 changed files with 47677 additions and 2161977 deletions
+2 -5
View File
@@ -1,5 +1,2 @@
[source.crates-io]
replace-with = "vendored-sources"
[source.vendored-sources]
directory = "vendor"
[alias]
xtask = "run -p xtask --"
+9
View File
@@ -0,0 +1,9 @@
{
"image": "mcr.microsoft.com/devcontainers/rust:latest",
"customizations": {
"vscode": {
"extensions": ["rust-lang.rust-analyzer"]
}
},
"runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"]
}
+48
View File
@@ -0,0 +1,48 @@
name: Docs Deploy
on:
push:
branches:
- devel
jobs:
deploy-docs:
name: Build and Deploy MkDocs
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.14"
- name: Install docs dependencies
run: pip install -r requirements.txt
- name: Build MkDocs site
run: mkdocs build
- name: Install rsync
run: |
sudo apt-get update
sudo apt-get install -y rsync openssh-client
- name: Prepare SSH key
env:
SSH_KEY_B64: ${{ secrets.ROOT_CI_KEY_B64 }}
run: |
umask 077
mkdir -p ~/.ssh
printf '%s' "$SSH_KEY_B64" | base64 -d > ~/.ssh/id_root_ci
chmod 600 ~/.ssh/id_root_ci
- name: Deploy via rsync
env:
DEPLOY_HOST: ${{ secrets.FPARKAN_DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.FPARKAN_DEPLOY_PORT }}
run: |
rsync -rlz --delete \
-e "ssh -p ${DEPLOY_PORT} -i ~/.ssh/id_root_ci -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \
site/ "gitea-runner@${DEPLOY_HOST}:./"
+25
View File
@@ -0,0 +1,25 @@
name: RenovateBot
on:
schedule:
- cron: "@daily"
jobs:
renovate:
container: ghcr.io/renovatebot/renovate:43
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Run renovate
run: |
renovate
env:
GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_TOKEN }}
LOG_LEVEL: ${{ vars.RENOVATE_LOG_LEVEL }}
RENOVATE_CONFIG_FILE: renovate.config.cjs
RENOVATE_LOG_LEVEL: ${{ vars.RENOVATE_LOG_LEVEL }}
RENOVATE_REPOSITORIES: ${{ gitea.repository }}
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
+55
View File
@@ -0,0 +1,55 @@
name: Test
on: [push, pull_request]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cargo check
run: cargo check --workspace --all-targets --all-features
- name: Clippy (deny warnings)
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
test:
name: Test
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- name: Cargo test
run: cargo test --workspace --all-features -- --nocapture
render-parity:
name: Render parity
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- name: Install headless GL runtime
run: |
sudo apt-get update
sudo apt-get install -y xvfb libgl1-mesa-dri libgles2-mesa-dev mesa-utils
- name: Build render-demo binary
run: cargo build -p render-demo --features demo
- name: Run frame parity suite
run: |
xvfb-run -s "-screen 0 1280x720x24" cargo run -p render-parity -- \
--manifest parity/cases.toml \
--output-dir target/render-parity/current \
--demo-bin target/debug/parkan-render-demo \
--keep-going
- name: Upload parity artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: render-parity-artifacts
path: target/render-parity/current
if-no-files-found: ignore
+90
View File
@@ -0,0 +1,90 @@
name: fparkan-ci
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
msrv-backend-neutral:
name: MSRV backend-neutral crates
runs-on: ubuntu-latest
env:
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.87.0
- name: Test backend-neutral crates
run: >
cargo test
-p fparkan-animation
-p fparkan-binary
-p fparkan-corpus
-p fparkan-diagnostics
-p fparkan-fx
-p fparkan-inspection
-p fparkan-material
-p fparkan-mission-format
-p fparkan-msh
-p fparkan-nres
-p fparkan-path
-p fparkan-platform
-p fparkan-prototype
-p fparkan-render
-p fparkan-resource
-p fparkan-rsli
-p fparkan-runtime
-p fparkan-terrain
-p fparkan-terrain-format
-p fparkan-texm
-p fparkan-vfs
-p fparkan-world
--all-targets
--locked
stage0-matrix:
name: Stage 0-2 CI (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
smoke_platform: linux
- os: windows-latest
smoke_platform: windows
- os: macos-latest
smoke_platform: macos
env:
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain-file: rust-toolchain.toml
- name: Install cargo-deny
run: cargo install cargo-deny --locked
- name: Run canonical CI gate
run: cargo xtask ci
- name: Record native Vulkan smoke status
if: always()
shell: bash
run: >
cargo run -p fparkan-vulkan-smoke --locked --
--platform "${{ matrix.smoke_platform }}"
--out "target/fparkan/native-smoke/${{ runner.os }}.json"
--status blocked
--probe-surface
--reason "native Vulkan smoke runner is not enabled on this CI lane yet"
- name: Upload acceptance evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: stage-0-2-acceptance-${{ matrix.os }}
path: |
target/fparkan/acceptance/stage-0-2-audit.json
target/fparkan/native-smoke/*.json
if-no-files-found: ignore
+214 -1
View File
@@ -1 +1,214 @@
/target
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
tmp/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pdm
.pdm.toml
# PEP 582
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Poetry local configuration file
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":disableDependencyDashboard"
],
"assignees": [
"valentineus"
],
"labels": [
"dependencies",
"automated"
],
"packageRules": [
{
"groupName": "all digest updates",
"groupSlug": "all-digest",
"matchUpdateTypes": [
"minor",
"patch",
"pin",
"digest"
],
"matchPackageNames": [
"*"
],
"automerge": true
}
]
}
Generated
+1918 -867
View File
File diff suppressed because it is too large Load Diff
+59 -6
View File
@@ -1,13 +1,66 @@
[workspace]
resolver = "2"
resolver = "3"
members = [
"libnres",
"nres-cli",
"packer",
"texture-decoder",
"unpacker",
"crates/fparkan-animation",
"crates/fparkan-assets",
"crates/fparkan-binary",
"crates/fparkan-corpus",
"crates/fparkan-diagnostics",
"crates/fparkan-fx",
"crates/fparkan-inspection",
"crates/fparkan-material",
"crates/fparkan-mission-format",
"crates/fparkan-msh",
"crates/fparkan-nres",
"crates/fparkan-path",
"crates/fparkan-platform",
"crates/fparkan-prototype",
"crates/fparkan-render",
"crates/fparkan-resource",
"crates/fparkan-rsli",
"crates/fparkan-runtime",
"crates/fparkan-terrain",
"crates/fparkan-terrain-format",
"crates/fparkan-test-support",
"crates/fparkan-texm",
"crates/fparkan-vfs",
"crates/fparkan-world",
"adapters/fparkan-platform-winit",
"adapters/fparkan-render-vulkan",
"apps/fparkan-cli",
"apps/fparkan-game",
"apps/fparkan-headless",
"apps/fparkan-vulkan-smoke",
"apps/fparkan-viewer",
"xtask",
]
[workspace.package]
version = "0.1.0"
edition = "2021"
rust-version = "1.87"
license = "GPL-2.0-only"
repository = "https://github.com/valentineus/fparkan"
[workspace.lints.rust]
unsafe_code = "forbid"
missing_docs = "warn"
unreachable_pub = "warn"
unused_must_use = "deny"
[workspace.lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = { level = "warn", priority = -1 }
unwrap_used = "deny"
expect_used = "deny"
panic = "deny"
todo = "deny"
unimplemented = "deny"
dbg_macro = "deny"
print_stdout = "warn"
print_stderr = "warn"
lossy_float_literal = "deny"
[profile.release]
codegen-units = 1
lto = true
+74 -8
View File
@@ -1,11 +1,77 @@
# Utilities for the game "Parkan: Iron Strategy"
# FParkan
This repository contains utilities, tools, and libraries for the game "Parkan: Iron Strategy."
Open source проект с реализацией компонентов игрового движка игры **«Паркан: Железная Стратегия»**.
## List of projects
## Описание
- [unpacker](unpacker): Text-based utility for unpacking game resources in the NRres format. Allows unpacking 100% of game resources.
- [packer](packer): Text-based utility for packing game resources in the NRres format. Allows packing 100% of game resources.
- [texture-decoder](texture-decoder): (WIP) Decoder for game textures. Decodes approximately 20% of game textures.
- [libnres](libnres): _(Deprecation)_ Library for NRes files.
- [nres-cli](nres-cli): _(Deprecation)_ Console tool for NRes files.
Проект находится в активной разработке и включает:
- библиотеки для работы с форматами игровых архивов;
- спецификации форматов и сопутствующую документацию.
## Установка
Проект находится в начальной стадии, подробная инструкция по установке пока отсутствует.
## Документация
- локально: каталог [`docs/`](docs)
- сайт: <https://fparkan.popov.link>
## Библиотеки
- [crates/fparkan-nres](crates/fparkan-nres) — strict/lossless модель архивов NRes.
- [crates/fparkan-rsli](crates/fparkan-rsli) — чтение, lookup и lossless roundtrip архивов RsLi.
- [crates/fparkan-msh](crates/fparkan-msh) — validated static MSH geometry.
- [crates/fparkan-runtime](crates/fparkan-runtime) — transactional mission loading и headless runtime foundation.
- [apps/fparkan-cli](apps/fparkan-cli), [apps/fparkan-viewer](apps/fparkan-viewer), [apps/fparkan-headless](apps/fparkan-headless), [apps/fparkan-game](apps/fparkan-game) — composition roots.
## Тестирование
Базовое тестирование проходит на синтетических тестах из репозитория:
```bash
cargo xtask ci
```
Для дополнительного тестирования на реальных игровых ресурсах:
- используйте оригинальную копию игры (диск или [GOG-версия](https://www.gog.com/en/game/parkan_iron_strategy));
- разместите игровые каталоги в [`testdata/`](testdata);
- игровые ресурсы в репозиторий не включаются, так как защищены авторским правом.
Локальный licensed gate использует некоммитимый manifest:
```bash
cat > /private/tmp/fparkan-corpora.toml <<'EOF'
schema = 1
[[corpus]]
id = "part1-local"
kind = "part1"
root = "/absolute/path/to/IS"
expected_profile = "parkan-is-part1"
[[corpus]]
id = "part2-local"
kind = "part2"
root = "/absolute/path/to/IS2"
expected_profile = "parkan-is-part2"
EOF
FPARKAN_CORPORA_MANIFEST=/private/tmp/fparkan-corpora.toml \
cargo xtask acceptance report --suite licensed --stage 5
```
## Contributing & Support
Проект активно поддерживается и открыт для contribution. Issues и pull requests можно создавать в обоих репозиториях:
- **Primary development**: [valentineus/fparkan](https://code.popov.link/valentineus/fparkan)
- **GitHub mirror**: [valentineus/fparkan](https://github.com/valentineus/fparkan)
Основная разработка ведётся в self-hosted репозитории.
## Лицензия
Проект распространяется под лицензией **[GNU GPL v2](LICENSE.txt)**.
@@ -0,0 +1,14 @@
[package]
name = "fparkan-platform-winit"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-platform = { path = "../../crates/fparkan-platform" }
raw-window-handle = "0.6"
winit = "0.30"
[lints]
workspace = true
+509
View File
@@ -0,0 +1,509 @@
#![forbid(unsafe_code)]
#![cfg_attr(
test,
allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::expect_used,
clippy::float_cmp,
clippy::identity_op,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::map_unwrap_or,
clippy::needless_raw_string_hashes,
clippy::semicolon_if_nothing_returned,
clippy::type_complexity,
clippy::panic,
clippy::unwrap_used
)
)]
//! Minimal `winit`-backed platform adapter shim.
use fparkan_platform::{
EventSource, MonotonicClock, MonotonicInstant, NativeWindowHandles, PhysicalSize,
PlatformError, PlatformEvent, RenderRequest, WindowHandle, WindowPort,
};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalSize as WinitPhysicalSize;
use winit::event::{Event, MouseButton, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::platform::scancode::PhysicalKeyExtScancode;
use winit::window::{Window, WindowId};
static NEXT_WINDOW_HANDLE_ID: AtomicU64 = AtomicU64::new(1);
const DEFAULT_SMOKE_WIDTH: u32 = 1280;
const DEFAULT_SMOKE_HEIGHT: u32 = 720;
fn next_window_id() -> u64 {
NEXT_WINDOW_HANDLE_ID.fetch_add(1, Ordering::Relaxed)
}
/// Simple monotonic clock for windowing abstractions.
#[derive(Clone, Copy, Debug)]
pub struct WinitClock;
impl MonotonicClock for WinitClock {
fn now(&self) -> MonotonicInstant {
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
MonotonicInstant(duration.as_millis().try_into().unwrap_or(u64::MAX))
}
}
/// Event source backed by pre-buffered platform events.
#[derive(Clone, Debug, Default)]
pub struct WinitEventSource {
queue: VecDeque<PlatformEvent>,
}
impl WinitEventSource {
/// Creates an empty source.
#[must_use]
pub const fn new() -> Self {
Self {
queue: VecDeque::new(),
}
}
/// Pushes a synthetic event (used by tests and smoke stubs).
pub fn push(&mut self, event: PlatformEvent) {
self.queue.push_back(event);
}
/// Pushes a mapped native window event.
pub fn push_window_event(&mut self, event: &WindowEvent) {
match event {
WindowEvent::KeyboardInput { event, .. } => {
self.queue.push_back(PlatformEvent::KeyboardInput {
scancode: event.physical_key.to_scancode().unwrap_or(0),
pressed: event.state.is_pressed(),
});
}
WindowEvent::MouseInput { state, button, .. } => {
self.queue.push_back(PlatformEvent::MouseInput {
button: mouse_button_code(*button),
pressed: state.is_pressed(),
x: 0.0,
y: 0.0,
});
}
WindowEvent::CursorMoved { position, .. } => {
self.queue.push_back(PlatformEvent::CursorMoved {
x: position.x,
y: position.y,
});
}
WindowEvent::Resized(size) => {
self.queue.push_back(PlatformEvent::Resize {
width: size.width,
height: size.height,
});
}
WindowEvent::Focused(focused) => {
self.queue
.push_back(PlatformEvent::FocusChanged { focused: *focused });
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.queue.push_back(PlatformEvent::DpiChanged {
scale: *scale_factor,
});
}
WindowEvent::CloseRequested => {
self.queue.push_back(PlatformEvent::QuitRequested);
}
_ => {}
}
}
/// Pushes events from an event loop event.
pub fn push_event<T>(&mut self, event: &Event<T>) {
if let Event::WindowEvent { event, .. } = event {
self.push_window_event(event);
}
}
}
fn mouse_button_code(button: MouseButton) -> u16 {
match button {
MouseButton::Left => 0,
MouseButton::Right => 1,
MouseButton::Middle => 2,
MouseButton::Back => 3,
MouseButton::Forward => 4,
MouseButton::Other(index) => 100 + index,
}
}
impl EventSource for WinitEventSource {
fn poll(&mut self, out: &mut Vec<PlatformEvent>) -> Result<(), PlatformError> {
while let Some(event) = self.queue.pop_front() {
out.push(event);
}
Ok(())
}
}
/// Window creation plan for native smoke entrypoints.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WinitWindowPlan {
/// Requested drawable width in physical pixels.
pub width: u32,
/// Requested drawable height in physical pixels.
pub height: u32,
/// Whether native window/display handles are required by the caller.
pub requires_native_handles: bool,
}
impl WinitWindowPlan {
/// Returns the Stage 0 native smoke window plan.
#[must_use]
pub const fn smoke() -> Self {
Self {
width: DEFAULT_SMOKE_WIDTH,
height: DEFAULT_SMOKE_HEIGHT,
requires_native_handles: true,
}
}
/// Validates the window plan before a native event loop is entered.
///
/// # Errors
///
/// Returns [`PlatformError`] when the drawable extent is zero.
pub fn validate(self) -> Result<Self, PlatformError> {
if self.width == 0 || self.height == 0 {
return Err(PlatformError::Backend {
context: "winit window plan",
message: "drawable extent must be non-zero".to_string(),
});
}
Ok(self)
}
}
/// Native smoke window creation result.
#[derive(Clone, Copy, Debug)]
pub struct WinitSmokeWindowProbe {
/// Validated creation plan.
pub plan: WinitWindowPlan,
/// Captured window descriptor.
pub window: WinitWindow,
}
impl WinitSmokeWindowProbe {
/// Returns raw native handles captured from the native window.
#[must_use]
pub fn native_handles(&self) -> Option<NativeWindowHandles> {
self.window.native_handles()
}
}
/// Creates a native smoke window, captures raw handles, then exits the event loop.
///
/// # Errors
///
/// Returns [`PlatformError`] when the plan is invalid, the event loop/window
/// cannot be created, or raw native handles are unavailable.
pub fn probe_smoke_window() -> Result<WinitSmokeWindowProbe, PlatformError> {
let plan = WinitWindowPlan::smoke().validate()?;
let event_loop = EventLoop::new().map_err(|err| PlatformError::Backend {
context: "winit event loop",
message: err.to_string(),
})?;
let mut app = SmokeWindowApp::new(plan);
event_loop
.run_app(&mut app)
.map_err(|err| PlatformError::Backend {
context: "winit event loop",
message: err.to_string(),
})?;
app.into_probe()
}
struct SmokeWindowApp {
plan: WinitWindowPlan,
window: Option<WinitWindow>,
error: Option<String>,
}
impl SmokeWindowApp {
const fn new(plan: WinitWindowPlan) -> Self {
Self {
plan,
window: None,
error: None,
}
}
fn into_probe(self) -> Result<WinitSmokeWindowProbe, PlatformError> {
if let Some(message) = self.error {
return Err(PlatformError::Backend {
context: "winit smoke window",
message,
});
}
let window = self.window.ok_or_else(|| PlatformError::Backend {
context: "winit smoke window",
message: "event loop exited before creating a window".to_string(),
})?;
if self.plan.requires_native_handles && window.native_handles().is_none() {
return Err(PlatformError::Backend {
context: "winit smoke window",
message: "native window/display handles are unavailable".to_string(),
});
}
Ok(WinitSmokeWindowProbe {
plan: self.plan,
window,
})
}
}
impl ApplicationHandler for SmokeWindowApp {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_some() || self.error.is_some() {
event_loop.exit();
return;
}
let attributes = Window::default_attributes()
.with_title("FParkan Vulkan smoke")
.with_inner_size(WinitPhysicalSize::new(self.plan.width, self.plan.height));
match event_loop.create_window(attributes) {
Ok(window) => {
self.window = Some(WinitWindow::from_window(&window));
}
Err(err) => {
self.error = Some(err.to_string());
}
}
event_loop.exit();
}
fn window_event(
&mut self,
_event_loop: &ActiveEventLoop,
_window_id: WindowId,
_event: WindowEvent,
) {
}
}
/// Minimal window view over a `winit` window.
#[derive(Clone, Copy, Debug)]
pub struct WinitWindow {
handle: WindowHandle,
width: u32,
height: u32,
scale: f64,
focused: bool,
minimized: bool,
occluded: bool,
native_handles: Option<NativeWindowHandles>,
}
impl WinitWindow {
/// Builds a stable descriptor from a `winit` window.
#[must_use]
pub fn from_window(window: &Window) -> Self {
let scale = window.scale_factor();
let size = window.inner_size();
Self {
handle: WindowHandle {
id: next_window_id(),
},
width: size.width,
height: size.height,
scale,
focused: true,
minimized: false,
occluded: false,
native_handles: native_handles(window),
}
}
/// Returns conservative defaults if a native window is not available yet.
#[must_use]
pub fn synthetic(width: u32, height: u32) -> Self {
Self {
handle: WindowHandle {
id: next_window_id(),
},
width,
height,
scale: 1.0,
focused: true,
minimized: false,
occluded: false,
native_handles: None,
}
}
/// Returns requested default render profile for integration points.
#[must_use]
pub const fn default_render_request() -> RenderRequest {
RenderRequest::conservative()
}
}
impl WindowPort for WinitWindow {
fn drawable_size(&self) -> PhysicalSize {
PhysicalSize {
width: self.width,
height: self.height,
}
}
fn dpi_scale(&self) -> f64 {
self.scale
}
fn has_focus(&self) -> bool {
self.focused
}
fn is_minimized(&self) -> bool {
self.minimized
}
fn is_occluded(&self) -> bool {
self.occluded
}
fn handle(&self) -> WindowHandle {
self.handle
}
fn native_handles(&self) -> Option<NativeWindowHandles> {
self.native_handles
}
}
fn native_handles(window: &Window) -> Option<NativeWindowHandles> {
let display = window.display_handle().ok()?.as_raw();
let window = window.window_handle().ok()?.as_raw();
Some(NativeWindowHandles { display, window })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_source_buffers_synthetic_events() -> Result<(), PlatformError> {
let mut source = WinitEventSource::new();
source.push(PlatformEvent::Resumed);
source.push(PlatformEvent::QuitRequested);
let mut events = Vec::new();
source.poll(&mut events)?;
assert_eq!(
events,
vec![PlatformEvent::Resumed, PlatformEvent::QuitRequested]
);
Ok(())
}
#[test]
fn window_port_reports_default_request_profile() {
let window = WinitWindow::synthetic(640, 360);
let request = WinitWindow::default_render_request();
assert_eq!(
request.presentation,
fparkan_platform::PresentationMode::Fifo
);
assert_eq!(
window.drawable_size(),
PhysicalSize {
width: 640,
height: 360
}
);
assert!(window.native_handles().is_none());
}
#[test]
fn smoke_window_plan_requires_native_handles_and_nonzero_extent() -> Result<(), PlatformError> {
let plan = WinitWindowPlan::smoke().validate()?;
assert_eq!(plan.width, DEFAULT_SMOKE_WIDTH);
assert_eq!(plan.height, DEFAULT_SMOKE_HEIGHT);
assert!(plan.requires_native_handles);
Ok(())
}
#[test]
fn smoke_window_plan_rejects_zero_extent() {
let plan = WinitWindowPlan {
width: 0,
height: DEFAULT_SMOKE_HEIGHT,
requires_native_handles: true,
};
assert!(matches!(
plan.validate(),
Err(PlatformError::Backend {
context: "winit window plan",
..
})
));
}
#[test]
fn smoke_window_app_requires_created_native_window() {
let app = SmokeWindowApp::new(WinitWindowPlan::smoke());
assert!(matches!(
app.into_probe(),
Err(PlatformError::Backend {
context: "winit smoke window",
..
})
));
}
#[test]
fn smoke_window_app_rejects_synthetic_window_without_native_handles() {
let mut app = SmokeWindowApp::new(WinitWindowPlan::smoke());
app.window = Some(WinitWindow::synthetic(
DEFAULT_SMOKE_WIDTH,
DEFAULT_SMOKE_HEIGHT,
));
assert!(matches!(
app.into_probe(),
Err(PlatformError::Backend {
context: "winit smoke window",
..
})
));
}
#[test]
fn window_events_push_expected_platform_events() {
let mut source = WinitEventSource::new();
let size = winit::dpi::PhysicalSize::new(1024u32, 768u32);
source.push_window_event(&WindowEvent::Resized(size));
source.push_window_event(&WindowEvent::Focused(false));
source.push_window_event(&WindowEvent::CloseRequested);
let mut events = Vec::new();
source
.poll(&mut events)
.expect("platform event pump should never fail");
assert!(events.contains(&PlatformEvent::Resize {
width: 1024,
height: 768,
}));
assert!(events.contains(&PlatformEvent::FocusChanged { focused: false }));
assert!(events.contains(&PlatformEvent::QuitRequested));
}
}
// SAFETY: no unsafe usage in this crate.
+32
View File
@@ -0,0 +1,32 @@
[package]
name = "fparkan-render-vulkan"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
ash = "0.38"
ash-window = "0.13"
fparkan-binary = { path = "../../crates/fparkan-binary" }
fparkan-platform = { path = "../../crates/fparkan-platform" }
fparkan-render = { path = "../../crates/fparkan-render" }
[lints.rust]
unsafe_code = "allow"
missing_docs = "warn"
unreachable_pub = "warn"
unused_must_use = "deny"
[lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = { level = "warn", priority = -1 }
unwrap_used = "deny"
expect_used = "deny"
panic = "deny"
todo = "deny"
unimplemented = "deny"
dbg_macro = "deny"
print_stdout = "warn"
print_stderr = "warn"
lossy_float_literal = "deny"
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
# ADR-0001: Modular Monolith
Status: accepted
FParkan is implemented as one Cargo workspace with local crates grouped by domain. Binaries and adapters compose domain crates; domain crates do not import platform, windowing, OpenGL, GUI, or application packages.
+5
View File
@@ -0,0 +1,5 @@
# ADR-0002: Behavior Compatibility, Not ABI Compatibility
Status: accepted
The project targets clean-room behavior compatibility for formats, resource lookup, loading order, deterministic runtime behavior, and presentation command semantics. It does not reproduce original DLL boundaries, exports, calling conventions, object layouts, RVAs, or native singleton access patterns.
+5
View File
@@ -0,0 +1,5 @@
# ADR-0003: Raw And Interpreted Data
Status: accepted
Legacy data models keep raw bytes distinct from validated structure and interpreted domain views. Writers preserve raw data unless an explicit editing profile requests canonical rebuilding.
@@ -0,0 +1,5 @@
# ADR-0004: Synthetic And Licensed Tests
Status: accepted
Synthetic tests run everywhere and contain no proprietary data. Licensed corpus tests require an explicit local manifest and fail when requested without configuration. Reports contain metrics and fingerprints, not payload dumps or absolute game roots.
@@ -0,0 +1,5 @@
# ADR-0005: Deterministic Reference Runtime
Status: accepted
Stages 0-5 use a single-threaded deterministic reference profile. Stable ordering, explicit ticks, named random streams, and canonical captures are part of the contract. Wall-clock time, pointer addresses, hash iteration order, and GPU handles are not semantic inputs.
+5
View File
@@ -0,0 +1,5 @@
# ADR-0006: Error Policy
Status: accepted
Missing required resources, malformed bytes, unsupported documented branches, capability mismatches, and budget failures are structured errors. Runtime code must not silently skip mandatory objects or convert corrupted data into empty success values.
+26
View File
@@ -0,0 +1,26 @@
# ADR-0007: Safe SDL/OpenGL Boundary
Status: provisional
Workspace-owned code forbids `unsafe`. SDL/OpenGL adapters must use maintained
external crates behind a safe project API; local Objective-C/CGL/SDL/OpenGL FFI
inside FParkan is not an acceptable implementation strategy.
The current adapter crates are safe boundary stubs. They compile the intended
ports and deterministic command contracts, but they do not create SDL windows,
GL contexts, GPU resources, shaders, draw calls, swapchains, or presents. They
must not be treated as backend readiness evidence.
To close the macOS backend requirement, choose and vendor/lock a maintained
safe facade stack, then implement:
- SDL event source, window creation, GL context lifecycle, drawable size and
present;
- GL shader compile/link, buffer/texture upload, render state, draw calls and
diagnostics;
- game/viewer composition roots using those adapters;
- hidden-window/offscreen macOS smoke tests and licensed local model/terrain
frame captures.
Until those are implemented, Desktop GL evidence may document external probes
only; it does not satisfy the permanent adapter requirement.
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "fparkan-cli"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-assets = { path = "../../crates/fparkan-assets" }
fparkan-corpus = { path = "../../crates/fparkan-corpus" }
fparkan-prototype = { path = "../../crates/fparkan-prototype" }
fparkan-inspection = { path = "../../crates/fparkan-inspection" }
fparkan-resource = { path = "../../crates/fparkan-resource" }
fparkan-runtime = { path = "../../crates/fparkan-runtime" }
fparkan-vfs = { path = "../../crates/fparkan-vfs" }
[lints]
workspace = true
+363
View File
@@ -0,0 +1,363 @@
#![forbid(unsafe_code)]
#![cfg_attr(
test,
allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::expect_used,
clippy::float_cmp,
clippy::identity_op,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::map_unwrap_or,
clippy::needless_raw_string_hashes,
clippy::semicolon_if_nothing_returned,
clippy::type_complexity,
clippy::panic,
clippy::unwrap_used
)
)]
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! `FParkan` command-line tools.
use fparkan_assets::extend_graph_report_with_visual_dependencies;
use fparkan_corpus::{discover, render_report_json, report, DiscoverOptions};
use fparkan_inspection::inspect_archive_file;
use fparkan_inspection::ArchiveInspection;
use fparkan_prototype::build_prototype_graph_report;
use fparkan_resource::{resource_name, CachedResourceRepository};
use fparkan_runtime::{
create, load_mission, EngineConfig, EngineMode, EngineServices, MissionRequest,
};
use fparkan_vfs::DirectoryVfs;
use std::fmt::Write;
use std::path::PathBuf;
use std::sync::Arc;
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let result = run(&args);
let code = exit_code(&result);
if let Err(err) = result {
eprintln!("{err}");
}
std::process::exit(code);
}
fn run(args: &[String]) -> Result<(), String> {
match args {
[domain, command, rest @ ..] if domain == "corpus" && command == "discover" => {
let rest = strip_format_json(rest)?;
let root = parse_root(&rest)?;
let manifest =
discover(&root, DiscoverOptions::default()).map_err(|e| e.to_string())?;
let report = report(&root, &manifest).map_err(|e| e.to_string())?;
println!("{}", render_report_json(&report));
Ok(())
}
[domain, command, rest @ ..] if domain == "corpus" && command == "validate" => {
let rest = strip_format_json(rest)?;
let root = parse_root(&rest)?;
let manifest =
discover(&root, DiscoverOptions::default()).map_err(|e| e.to_string())?;
let report = report(&root, &manifest).map_err(|e| e.to_string())?;
if report.casefold_collisions > 0 {
return Err("casefold collisions found".to_string());
}
if report.failures > 0 {
return Err(format!("corpus report found {} failures", report.failures));
}
println!("{}", render_report_json(&report));
Ok(())
}
[domain, command, rest @ ..] if domain == "archive" && command == "inspect" => {
let rest = strip_format_json(rest)?;
inspect_archive(&rest)
}
[domain, command, rest @ ..] if domain == "prototype" && command == "inspect" => {
let rest = strip_format_json(rest)?;
inspect_prototype(&rest)
}
[domain, command, rest @ ..] if domain == "mission" && command == "graph" => {
let rest = strip_format_json(rest)?;
graph_mission(&rest)
}
_ => Err(usage()),
}
}
fn exit_code(result: &Result<(), String>) -> i32 {
if result.is_ok() {
0
} else {
2
}
}
fn strip_format_json(args: &[String]) -> Result<Vec<String>, String> {
let mut stripped = Vec::with_capacity(args.len());
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--format" {
let value = iter
.next()
.ok_or_else(|| "--format requires a value".to_string())?;
if value != "json" {
return Err(format!("unsupported output format: {value}"));
}
continue;
}
stripped.push(arg.clone());
}
Ok(stripped)
}
fn parse_root(args: &[String]) -> Result<PathBuf, String> {
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--root" {
return iter
.next()
.map(PathBuf::from)
.ok_or_else(|| "--root requires a path".to_string());
}
}
Err("missing --root".to_string())
}
fn parse_root_alias(args: &[String]) -> Result<PathBuf, String> {
parse_option(args, &["--root", "--game-root"])
.map(PathBuf::from)
.ok_or_else(|| "missing --root".to_string())
}
fn parse_required(args: &[String], names: &[&str], label: &str) -> Result<String, String> {
parse_option(args, names).ok_or_else(|| format!("missing {label}"))
}
fn parse_option(args: &[String], names: &[&str]) -> Option<String> {
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if names.iter().any(|name| arg == name) {
return iter.next().cloned();
}
}
None
}
fn inspect_prototype(args: &[String]) -> Result<(), String> {
let root = parse_root_alias(args)?;
let key = parse_required(args, &["--key"], "--key")?;
let vfs = Arc::new(DirectoryVfs::new(root));
let repository = CachedResourceRepository::new(vfs.clone());
let roots = [resource_name(key.as_bytes())];
let (graph, resolved, mut report) =
build_prototype_graph_report(&repository, vfs.as_ref(), &roots);
extend_graph_report_with_visual_dependencies(&repository, &mut report, &graph, &resolved);
println!("{}", prototype_inspect_json(&key, &graph, &report));
Ok(())
}
fn prototype_inspect_json(
key: &str,
graph: &fparkan_prototype::PrototypeGraph,
report: &fparkan_prototype::PrototypeGraphReport,
) -> String {
format!(
"{{\"schema_version\":\"fparkan-prototype-inspect-v1\",\"key\":{},\"roots\":{},\"prototype_requests\":{},\"resolved\":{},\"unit_references\":{},\"unit_components\":{},\"direct_references\":{},\"wear\":{},\"materials\":{},\"textures\":{},\"lightmaps\":{},\"failures\":{}}}",
json_string(key),
report.root_count,
graph.prototype_requests.len(),
report.resolved_count,
report.unit_reference_count,
report.unit_component_count,
report.direct_reference_count,
report.wear_resolved_count,
report.material_resolved_count,
report.texture_resolved_count,
report.lightmap_resolved_count,
report.failures.len()
)
}
fn graph_mission(args: &[String]) -> Result<(), String> {
let root = parse_root_alias(args)?;
let mission = parse_required(args, &["--mission"], "--mission")?;
let services = EngineServices::new(Arc::new(DirectoryVfs::new(root)));
let mut engine = create(
EngineConfig {
mode: EngineMode::Headless,
},
services,
)
.map_err(|err| err.to_string())?;
let loaded = load_mission(
&mut engine,
MissionRequest {
key: mission.clone(),
},
)
.map_err(|err| err.to_string())?;
println!(
"{{\"schema_version\":\"fparkan-mission-graph-v1\",\"mission\":{},\"objects\":{},\"paths\":{},\"clans\":{},\"extras\":{},\"roots\":{},\"direct_references\":{},\"unit_references\":{},\"unit_components\":{},\"prototype_requests\":{},\"wear\":{},\"materials\":{},\"textures\":{},\"lightmaps\":{},\"failures\":{}}}",
json_string(&mission),
loaded.object_count,
loaded.path_count,
loaded.clan_count,
loaded.extra_count,
loaded.graph_root_count,
loaded.graph_direct_reference_count,
loaded.graph_unit_reference_count,
loaded.graph_unit_component_count,
loaded.graph_resolved_count,
loaded.graph_wear_resolved_count,
loaded.graph_material_resolved_count,
loaded.graph_texture_resolved_count,
loaded.graph_lightmap_resolved_count,
loaded.graph_failure_count
);
Ok(())
}
fn inspect_archive(args: &[String]) -> Result<(), String> {
let path = parse_archive_path(args)?;
let inspection = inspect_archive_file(&path, 0).map_err(|err| err.to_string())?;
match inspection {
ArchiveInspection::Nres {
entries,
lookup_order_valid,
..
} => {
println!(
"{}",
archive_inspect_json(
&path.display().to_string(),
"NRes",
entries,
Some(lookup_order_valid),
)
);
Ok(())
}
ArchiveInspection::Rsli { entries } => {
println!(
"{}",
archive_inspect_json(&path.display().to_string(), "RsLi", entries, None)
);
Ok(())
}
ArchiveInspection::Unsupported => {
Err(format!("{}: unsupported archive magic", path.display()))
}
}
}
fn archive_inspect_json(
path: &str,
kind: &str,
entries: usize,
lookup_order_valid: Option<bool>,
) -> String {
let mut out = format!(
"{{\"schema_version\":\"fparkan-archive-inspect-v1\",\"path\":{},\"kind\":{},\"entries\":{}",
json_string(path),
json_string(kind),
entries
);
if let Some(valid) = lookup_order_valid {
let _ = write!(out, ",\"lookup_order_valid\":{valid}");
}
out.push('}');
out
}
fn parse_archive_path(args: &[String]) -> Result<PathBuf, String> {
match args {
[path] => Ok(PathBuf::from(path)),
[flag, path] if flag == "--file" => Ok(PathBuf::from(path)),
_ => Err("archive inspect requires <file> or --file <file>".to_string()),
}
}
fn json_string(value: &str) -> String {
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c.is_control() => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
fn usage() -> String {
"usage: fparkan corpus discover|validate --root <path> [--format json] | archive inspect <file> [--format json] | prototype inspect --root <path> --key <key> [--format json] | mission graph --root <path> --mission <path> [--format json]".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
fn strings(values: &[&str]) -> Vec<String> {
values.iter().map(|value| (*value).to_string()).collect()
}
#[test]
fn stable_exit_codes_are_mapped() {
assert_eq!(exit_code(&Ok(())), 0);
assert_eq!(exit_code(&Err("failure".to_string())), 2);
}
#[test]
fn accepts_json_format_option() {
assert_eq!(
strip_format_json(&strings(&["--root", "testdata", "--format", "json"])),
Ok(strings(&["--root", "testdata"]))
);
assert_eq!(
strip_format_json(&strings(&["--format", "text"])),
Err("unsupported output format: text".to_string())
);
}
#[test]
fn archive_json_has_schema_version() {
let json = archive_inspect_json("archive.lib", "NRes", 3, Some(true));
assert!(json.contains("\"schema_version\":\"fparkan-archive-inspect-v1\""));
assert!(json.contains("\"kind\":\"NRes\""));
assert!(json.contains("\"lookup_order_valid\":true"));
}
#[test]
fn prototype_graph_json_has_canonical_field_order() {
let mut graph = fparkan_prototype::PrototypeGraph::default();
graph
.prototype_requests
.push(fparkan_prototype::PrototypeKey(resource_name(b"root")));
let report = fparkan_prototype::PrototypeGraphReport {
root_count: 1,
direct_reference_count: 1,
resolved_count: 1,
..fparkan_prototype::PrototypeGraphReport::default()
};
let json = prototype_inspect_json("root", &graph, &report);
assert_eq!(
json,
"{\"schema_version\":\"fparkan-prototype-inspect-v1\",\"key\":\"root\",\"roots\":1,\"prototype_requests\":1,\"resolved\":1,\"unit_references\":0,\"unit_components\":0,\"direct_references\":1,\"wear\":0,\"materials\":0,\"textures\":0,\"lightmaps\":0,\"failures\":0}"
);
}
}
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "fparkan-game"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-assets = { path = "../../crates/fparkan-assets" }
fparkan-platform = { path = "../../crates/fparkan-platform" }
fparkan-render = { path = "../../crates/fparkan-render" }
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" }
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
fparkan-runtime = { path = "../../crates/fparkan-runtime" }
fparkan-vfs = { path = "../../crates/fparkan-vfs" }
fparkan-world = { path = "../../crates/fparkan-world" }
[lints]
workspace = true
+389
View File
@@ -0,0 +1,389 @@
#![forbid(unsafe_code)]
#![cfg_attr(
test,
allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::expect_used,
clippy::float_cmp,
clippy::identity_op,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::map_unwrap_or,
clippy::needless_raw_string_hashes,
clippy::semicolon_if_nothing_returned,
clippy::type_complexity,
clippy::panic,
clippy::unwrap_used
)
)]
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! `FParkan` rendered game composition root.
use fparkan_assets::PreparedVisual;
use fparkan_platform::WindowPort;
use fparkan_platform_winit::WinitWindow;
use fparkan_render::{
DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderBackend, RenderCommand,
RenderCommandList, RenderPhase,
};
use fparkan_render_vulkan::VulkanBackend;
use fparkan_runtime::{
create, frame, load_mission, loaded_mission_assets, EngineConfig, EngineMode, EngineServices,
MissionAssets, MissionRequest,
};
use fparkan_vfs::DirectoryVfs;
use fparkan_world::WorldSnapshot;
use std::path::PathBuf;
use std::sync::Arc;
fn main() {
let raw_args = std::env::args().skip(1).collect::<Vec<_>>();
let code = match run(&raw_args) {
Ok(output) => {
println!("{output}");
0
}
Err(err) => {
eprintln!("{err}");
2
}
};
std::process::exit(code);
}
fn run(args: &[String]) -> Result<String, String> {
let args = Args::parse(args)?;
let services = EngineServices::new(Arc::new(DirectoryVfs::new(&args.root)));
let mut engine = create(
EngineConfig {
mode: EngineMode::Rendered,
},
services,
)
.map_err(|err| err.to_string())?;
let loaded = load_mission(
&mut engine,
MissionRequest {
key: args.mission.clone(),
},
)
.map_err(|err| err.to_string())?;
let mut backend = VulkanBackend::new();
let _request = WinitWindow::default_render_request();
let window = WinitWindow::synthetic(1280, 720);
let _ = window.drawable_size();
let _ = window.handle();
let mut last_draw_count = 0usize;
let mut last_tick = 0u64;
let mut last_hash = [0u8; 32];
for _ in 0..args.frames {
let result = frame(&mut engine).map_err(|err| err.to_string())?;
last_tick = result.snapshot.tick.0;
last_hash = result.snapshot.hash.0;
let mission_assets = loaded_mission_assets(&engine);
let commands = render_snapshot_commands_with_assets(&result.snapshot, mission_assets);
last_draw_count = commands
.commands
.iter()
.filter(|command| matches!(command, RenderCommand::Draw(_)))
.count();
backend
.execute(&commands)
.map_err(|err| format!("render backend: {err}"))?;
}
let capture_report = backend.report();
Ok(format!(
"{{\"mission\":{},\"objects\":{},\"frames\":{},\"tick\":{},\"draws\":{},\"captures\":{},\"last_capture_bytes\":{},\"hash\":{}}}",
json_string(&args.mission),
loaded.object_count,
args.frames,
last_tick,
last_draw_count,
capture_report.submissions,
capture_report.last_capture_size,
json_hash(&last_hash)
))
}
#[cfg(test)]
fn render_snapshot_commands(snapshot: &WorldSnapshot) -> RenderCommandList {
render_snapshot_commands_with_assets(snapshot, None)
}
fn render_snapshot_commands_with_assets(
snapshot: &WorldSnapshot,
mission_assets: Option<&MissionAssets>,
) -> RenderCommandList {
let mut commands = Vec::with_capacity(snapshot.objects.len() + 2);
commands.push(RenderCommand::BeginFrame);
for (index, handle) in snapshot.objects.iter().enumerate() {
let stable_order = u64::from(handle.slot);
let prepared = mission_assets.and_then(|assets| {
assets
.visual_for_object(index)
.and_then(|visual_id| assets.visual_by_id(visual_id))
});
let mesh = if let Some(visual) = prepared {
visual.mesh.as_ref().map_or_else(
|| GpuMeshId(u64::from(handle.slot) + 1),
|_| GpuMeshId(visual.id.raw()),
)
} else {
GpuMeshId(u64::from(handle.slot) + 1)
};
let material = prepared
.and_then(PreparedVisual::primary_material_id)
.map_or(GpuMaterialId(1), |material_id| {
GpuMaterialId(material_id.raw())
});
let draw_id = snapshot
.tick
.0
.wrapping_mul(1_000_003)
.wrapping_add(stable_order);
commands.push(RenderCommand::Draw(DrawCommand {
id: DrawId(draw_id),
phase: RenderPhase::Opaque,
object_id: None,
mesh,
material,
transform: identity_transform(index_to_f32(index)),
range: IndexRange { start: 0, count: 3 },
stable_order,
}));
}
commands.push(RenderCommand::EndFrame);
RenderCommandList { commands }
}
fn identity_transform(x: f32) -> [f32; 16] {
[
1.0, 0.0, 0.0, x, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
]
}
fn index_to_f32(index: usize) -> f32 {
u16::try_from(index).map_or(f32::from(u16::MAX), f32::from)
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct Args {
root: PathBuf,
mission: String,
frames: u64,
}
impl Args {
fn parse(args: &[String]) -> Result<Self, String> {
let mut root = None;
let mut mission = None;
let mut frames = 1;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--root" => {
root = Some(
iter.next()
.map(PathBuf::from)
.ok_or_else(|| "--root requires a path".to_string())?,
);
}
"--mission" => {
mission = Some(
iter.next()
.cloned()
.ok_or_else(|| "--mission requires a path".to_string())?,
);
}
"--frames" => {
frames = iter
.next()
.ok_or_else(|| "--frames requires a value".to_string())?
.parse()
.map_err(|_| "--frames must be an integer".to_string())?;
}
_ => return Err(usage()),
}
}
let root = root.ok_or_else(|| "missing --root".to_string())?;
let mission = mission.ok_or_else(|| "missing --mission".to_string())?;
if frames == 0 {
return Err("--frames must be greater than zero".to_string());
}
Ok(Self {
root,
mission,
frames,
})
}
}
fn json_string(value: &str) -> String {
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c.is_control() => {
use std::fmt::Write as _;
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
fn json_hash(hash: &[u8; 32]) -> String {
let mut out = String::from("\"");
for byte in hash {
use std::fmt::Write as _;
let _ = write!(out, "{byte:02x}");
}
out.push('"');
out
}
fn usage() -> String {
"usage: fparkan-game --root <path> --mission <path> [--frames <n>]".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use fparkan_world::{ObjectHandle, StateHash, Tick};
use std::path::Path;
fn strings(values: &[&str]) -> Vec<String> {
values.iter().map(|value| (*value).to_string()).collect()
}
#[test]
fn parses_required_args() {
assert_eq!(
Args::parse(&strings(&[
"--root",
"testdata/IS",
"--mission",
"MISSIONS/Autodemo.00/data.tma",
"--frames",
"3",
])),
Ok(Args {
root: PathBuf::from("testdata/IS"),
mission: "MISSIONS/Autodemo.00/data.tma".to_string(),
frames: 3,
})
);
}
#[test]
fn render_commands_follow_snapshot_order() -> Result<(), String> {
let snapshot = WorldSnapshot {
tick: Tick(7),
objects: vec![
ObjectHandle {
generation: 1,
slot: 2,
},
ObjectHandle {
generation: 1,
slot: 5,
},
],
events: Vec::new(),
hash: StateHash([0; 32]),
};
let commands = render_snapshot_commands(&snapshot);
assert_eq!(commands.commands.len(), 4);
assert!(matches!(commands.commands[0], RenderCommand::BeginFrame));
assert!(matches!(commands.commands[3], RenderCommand::EndFrame));
let RenderCommand::Draw(first) = &commands.commands[1] else {
return Err("expected draw".to_string());
};
assert_eq!(first.mesh, GpuMeshId(3));
assert_eq!(first.stable_order, 2);
Ok(())
}
#[test]
#[ignore = "requires licensed corpus"]
fn selected_is_and_is2_missions_produce_approved_render_captures() {
for case in [
RenderCase {
root: "IS",
mission: "MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma",
expected: "{\"mission\":\"MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma\",\"objects\":33,\"frames\":1,\"tick\":1,\"draws\":33,\"captures\":1,\"last_capture_bytes\":810,\"hash\":\"ca17cc76e55c45e83c1c9c1c088e84bf1a698be91a7730943210fe27596af841\"}",
},
RenderCase {
root: "IS2",
mission: "MISSIONS/Campaign/CAMPAIGN.00/Mission.02/data.tma",
expected: "{\"mission\":\"MISSIONS/Campaign/CAMPAIGN.00/Mission.02/data.tma\",\"objects\":10,\"frames\":1,\"tick\":1,\"draws\":10,\"captures\":1,\"last_capture_bytes\":235,\"hash\":\"5d720b3ab690076a398a79a404850bbeaee2e33811b5bb570ec8a96d4a7a2fc4\"}",
},
] {
assert_eq!(
run(&render_args(&licensed_root(case.root), case.mission)),
Ok(case.expected.to_string())
);
}
}
#[test]
fn json_hash_is_hex() {
let mut hash = [0; 32];
hash[0] = 0xab;
hash[31] = 0xcd;
assert_eq!(
json_hash(&hash),
"\"ab000000000000000000000000000000000000000000000000000000000000cd\""
);
}
#[derive(Clone, Copy)]
struct RenderCase {
root: &'static str,
mission: &'static str,
expected: &'static str,
}
fn render_args(root: &Path, mission: &str) -> Vec<String> {
vec![
"--root".to_string(),
root.to_str().expect("utf8 root").to_string(),
"--mission".to_string(),
mission.to_string(),
"--frames".to_string(),
"1".to_string(),
]
}
fn licensed_root(name: &str) -> PathBuf {
let variable = match name {
"IS" => "FPARKAN_CORPUS_PART1_ROOT",
"IS2" => "FPARKAN_CORPUS_PART2_ROOT",
_ => panic!("unknown licensed corpus part: {name}"),
};
let root = std::env::var_os(variable)
.map(PathBuf::from)
.unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests"));
assert!(
root.is_dir(),
"licensed corpus root is missing: {}",
root.display()
);
root
}
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "fparkan-headless"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-runtime = { path = "../../crates/fparkan-runtime" }
fparkan-vfs = { path = "../../crates/fparkan-vfs" }
fparkan-world = { path = "../../crates/fparkan-world" }
[lints]
workspace = true
+133
View File
@@ -0,0 +1,133 @@
#![forbid(unsafe_code)]
#![cfg_attr(
test,
allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::expect_used,
clippy::float_cmp,
clippy::identity_op,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::map_unwrap_or,
clippy::needless_raw_string_hashes,
clippy::semicolon_if_nothing_returned,
clippy::type_complexity,
clippy::panic,
clippy::unwrap_used
)
)]
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! `FParkan` headless runtime entrypoint.
use fparkan_runtime::{
create, load_mission, step_headless, EngineConfig, EngineMode, EngineServices, MissionRequest,
};
use fparkan_vfs::DirectoryVfs;
use fparkan_world::InputSnapshot;
use std::path::PathBuf;
use std::sync::Arc;
fn main() {
if let Err(err) = run() {
eprintln!("{err}");
std::process::exit(2);
}
}
fn run() -> Result<(), String> {
let raw_args: Vec<String> = std::env::args().skip(1).collect();
let args = Args::parse(&raw_args)?;
let services = if let Some(root) = &args.root {
EngineServices::new(Arc::new(DirectoryVfs::new(root)))
} else {
EngineServices::default()
};
let mut engine = create(
EngineConfig {
mode: EngineMode::Headless,
},
services,
)
.map_err(|err| format!("{err}"))?;
if let Some(mission) = args.mission {
let loaded = load_mission(&mut engine, MissionRequest { key: mission })
.map_err(|err| format!("{err}"))?;
println!(
"mission objects={} areals={} surfaces={} graph_roots={} components={} wear={} material_slots={} textures={} lightmaps={} graph_failures={}",
loaded.object_count,
loaded.areal_count,
loaded.surface_count,
loaded.graph_root_count,
loaded.graph_unit_component_count,
loaded.graph_wear_resolved_count,
loaded.graph_material_resolved_count,
loaded.graph_texture_resolved_count,
loaded.graph_lightmap_resolved_count,
loaded.graph_failure_count
);
}
let mut last = None;
for _ in 0..args.ticks {
last = Some(step_headless(&mut engine, InputSnapshot).map_err(|err| format!("{err}"))?);
}
if let Some(frame) = last {
println!(
"tick={} hash={:02x?}",
frame.snapshot.tick.0, frame.snapshot.hash.0
);
}
Ok(())
}
struct Args {
root: Option<PathBuf>,
mission: Option<String>,
ticks: u64,
}
impl Args {
fn parse(args: &[String]) -> Result<Self, String> {
let mut parsed = Self {
root: None,
mission: None,
ticks: 1,
};
let mut iter = args.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--root" => {
parsed.root = Some(
iter.next()
.map(PathBuf::from)
.ok_or_else(|| "--root requires a path".to_string())?,
);
}
"--mission" => {
parsed.mission = Some(
iter.next()
.cloned()
.ok_or_else(|| "--mission requires a path".to_string())?,
);
}
"--ticks" => {
parsed.ticks = iter
.next()
.ok_or_else(|| "--ticks requires a value".to_string())?
.parse()
.map_err(|_| "--ticks must be an integer".to_string())?;
}
_ => return Err(usage()),
}
}
if parsed.mission.is_some() && parsed.root.is_none() {
return Err("--mission requires --root".to_string());
}
Ok(parsed)
}
}
fn usage() -> String {
"usage: fparkan-headless [--root <path> --mission <path>] [--ticks <n>]".to_string()
}
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "fparkan-viewer"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-inspection = { path = "../../crates/fparkan-inspection" }
fparkan-render = { path = "../../crates/fparkan-render" }
[lints]
workspace = true
+349
View File
@@ -0,0 +1,349 @@
#![forbid(unsafe_code)]
#![cfg_attr(
test,
allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::expect_used,
clippy::float_cmp,
clippy::identity_op,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::map_unwrap_or,
clippy::needless_raw_string_hashes,
clippy::semicolon_if_nothing_returned,
clippy::type_complexity,
clippy::panic,
clippy::unwrap_used
)
)]
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! `FParkan` asset viewer composition root.
use fparkan_inspection::{
inspect_land_file, inspect_model_from_root, inspect_texture_from_root, ArchiveInspection,
LandFileKind, MapInspection, NresEntrySummary,
};
use fparkan_render::{
build_commands, CameraSnapshot, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderPhase,
RenderProfile, RenderSnapshot, RenderSnapshotDraw,
};
use std::fmt::Write;
use std::path::PathBuf;
fn main() {
let args = std::env::args().skip(1).collect::<Vec<_>>();
let code = match run(&args) {
Ok(json) => {
println!("{json}");
0
}
Err(err) => {
eprintln!("{err}");
2
}
};
std::process::exit(code);
}
fn run(args: &[String]) -> Result<String, String> {
match args {
[domain, rest @ ..] if domain == "archive" => inspect_archive(rest),
[domain, rest @ ..] if domain == "model" => inspect_model(rest),
[domain, rest @ ..] if domain == "texture" => inspect_texture(rest),
[domain, rest @ ..] if domain == "map" => inspect_map(rest),
_ => Err(usage()),
}
}
fn inspect_archive(args: &[String]) -> Result<String, String> {
let file = parse_file(args)?;
let limit = parse_limit(args)?;
let inspection = fparkan_inspection::inspect_archive_file(&file, limit)?;
match inspection {
ArchiveInspection::Nres {
entries,
lookup_order_valid,
sample,
} => Ok(format!(
"{{\"kind\":\"NRes\",\"path\":{},\"entries\":{},\"lookup_order_valid\":{},\"sample\":[{}]}}",
json_string(&file.display().to_string()),
entries,
lookup_order_valid,
render_nres_entries(&sample)
)),
ArchiveInspection::Rsli { entries } => Ok(format!(
"{{\"kind\":\"RsLi\",\"path\":{},\"entries\":{}}}",
json_string(&file.display().to_string()),
entries
)),
ArchiveInspection::Unsupported => Err(format!("{}: unsupported archive magic", file.display())),
}
}
fn inspect_model(args: &[String]) -> Result<String, String> {
if let Some(fixture) = parse_option(args, &["--fixture"]) {
return ViewerModelService::inspect_synthetic_model(&fixture);
}
let query = parse_resource_query(args)?;
let inspection = inspect_model_from_root(&query.root, &query.archive, &query.name)?;
Ok(format!(
"{{\"kind\":\"model\",\"archive\":{},\"name\":{},\"streams\":{},\"nodes\":{},\"slots\":{},\"positions\":{},\"indices\":{},\"batches\":{}}}",
json_string(&query.archive),
json_string(&query.name),
inspection.streams,
inspection.nodes,
inspection.slots,
inspection.positions,
inspection.indices,
inspection.batches
))
}
#[derive(Clone, Debug)]
struct ViewerModelService;
impl ViewerModelService {
fn inspect_synthetic_model(fixture: &str) -> Result<String, String> {
if fixture != "synthetic/model-basic" {
return Err(format!("unknown model fixture: {fixture}"));
}
let snapshot = RenderSnapshot {
camera: CameraSnapshot::default(),
draws: vec![RenderSnapshotDraw {
id: DrawId(1),
phase: RenderPhase::Opaque,
object_id: None,
mesh: GpuMeshId(1),
material_slots: vec![GpuMaterialId(7)],
material_index: 0,
transform: identity_transform(),
range: IndexRange { start: 0, count: 3 },
stable_order: 0,
}],
};
let commands = build_commands(&snapshot, RenderProfile::default())
.map_err(|err| format!("render command generation: {err}"))?;
let draw_commands = commands
.commands
.iter()
.filter(|command| matches!(command, fparkan_render::RenderCommand::Draw(_)))
.count();
Ok(format!(
"{{\"kind\":\"model\",\"fixture\":{},\"service\":\"synthetic-model\",\"draw_commands\":{draw_commands}}}",
json_string(fixture)
))
}
}
fn inspect_texture(args: &[String]) -> Result<String, String> {
let query = parse_resource_query(args)?;
let inspection = inspect_texture_from_root(&query.root, &query.archive, &query.name)?;
Ok(format!(
"{{\"kind\":\"texture\",\"archive\":{},\"name\":{},\"width\":{},\"height\":{},\"format\":{},\"mips\":{},\"pages\":{}}}",
json_string(&query.archive),
json_string(&query.name),
inspection.width,
inspection.height,
json_string(&inspection.format),
inspection.mips,
inspection.pages
))
}
fn inspect_map(args: &[String]) -> Result<String, String> {
let file = parse_file(args)?;
let kind = parse_option(args, &["--kind"]).ok_or_else(|| "missing --kind".to_string())?;
let inspection = inspect_land_file(
&file,
match kind.as_str() {
"land-msh" => LandFileKind::LandMsh,
"land-map" => LandFileKind::LandMap,
_ => return Err(format!("unknown map kind: {kind}")),
},
)?;
Ok(render_map_inspection_json(
&file.display().to_string(),
&kind,
&inspection,
))
}
fn render_map_inspection_json(path: &str, kind: &str, inspection: &MapInspection) -> String {
match kind {
"land-msh" => format!(
"{{\"kind\":\"land-msh\",\"path\":{},\"streams\":{},\"positions\":{},\"faces\":{},\"slots\":{}}}",
json_string(path),
inspection.streams,
inspection.positions,
inspection.faces,
inspection.slots
),
"land-map" => format!(
"{{\"kind\":\"land-map\",\"path\":{},\"areals\":{},\"declared_areals\":{},\"grid_width\":{},\"grid_height\":{}}}",
json_string(path),
inspection.areals,
inspection.declared_areals,
inspection.grid_width,
inspection.grid_height
),
_ => unreachable!("invalid land kind: {kind}"),
}
}
struct ResourceQuery {
root: PathBuf,
archive: String,
name: String,
}
fn parse_resource_query(args: &[String]) -> Result<ResourceQuery, String> {
Ok(ResourceQuery {
root: parse_path_option(args, &["--root", "--game-root"], "--root")?,
archive: parse_option(args, &["--archive"])
.ok_or_else(|| "missing --archive".to_string())?,
name: parse_option(args, &["--name"]).ok_or_else(|| "missing --name".to_string())?,
})
}
fn parse_file(args: &[String]) -> Result<PathBuf, String> {
parse_path_option(args, &["--file"], "--file")
}
fn parse_limit(args: &[String]) -> Result<usize, String> {
parse_option(args, &["--limit"])
.map(|value| {
value
.parse::<usize>()
.map_err(|_| format!("invalid --limit: {value}"))
})
.transpose()
.map(|value| value.unwrap_or(0))
}
fn render_nres_entries(entries: &[NresEntrySummary]) -> String {
let mut out = String::new();
for (index, entry) in entries.iter().enumerate() {
if index > 0 {
out.push(',');
}
let name = &entry.name;
let _ = write!(
out,
"{{\"name\":{},\"type\":{},\"size\":{}}}",
json_string(name),
entry.type_id,
entry.data_size
);
}
out
}
fn parse_path_option(args: &[String], names: &[&str], label: &str) -> Result<PathBuf, String> {
parse_option(args, names)
.map(PathBuf::from)
.ok_or_else(|| format!("missing {label}"))
}
fn parse_option(args: &[String], names: &[&str]) -> Option<String> {
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if names.iter().any(|name| arg == name) {
return iter.next().cloned();
}
}
None
}
fn json_string(value: &str) -> String {
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c.is_control() => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
fn identity_transform() -> [f32; 16] {
[
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
]
}
fn usage() -> String {
"usage: fparkan-viewer archive --file <archive> [--limit N] | model --root <game-root> --archive <archive> --name <msh> | model --fixture synthetic/model-basic | texture --root <game-root> --archive <archive> --name <texm> | map --file <Land.msh|Land.map> --kind land-msh|land-map".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
fn strings(values: &[&str]) -> Vec<String> {
values.iter().map(|value| (*value).to_string()).collect()
}
#[test]
fn parses_resource_query() -> Result<(), String> {
let query = parse_resource_query(&strings(&[
"--root",
"testdata/IS",
"--archive",
"textures.lib",
"--name",
"grass.tex",
]))?;
assert_eq!(query.root, PathBuf::from("testdata/IS"));
assert_eq!(query.archive, "textures.lib");
assert_eq!(query.name, "grass.tex");
Ok(())
}
#[test]
fn json_string_escapes_controls() {
assert_eq!(json_string("a\"b\\c\n"), "\"a\\\"b\\\\c\\n\"");
}
#[test]
fn usage_rejects_empty_args() {
assert_eq!(run(&[]), Err(usage()));
}
#[test]
fn parses_limit() {
assert_eq!(parse_limit(&strings(&["--limit", "2"])), Ok(2));
assert_eq!(parse_limit(&[]), Ok(0));
assert_eq!(
parse_limit(&strings(&["--limit", "x"])),
Err("invalid --limit: x".to_string())
);
}
#[test]
fn model_fixture_uses_viewer_service_and_render_commands() -> Result<(), String> {
assert_eq!(
run(&strings(&["model", "--fixture", "synthetic/model-basic"]))?,
"{\"kind\":\"model\",\"fixture\":\"synthetic/model-basic\",\"service\":\"synthetic-model\",\"draw_commands\":1}"
);
Ok(())
}
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "fparkan-vulkan-smoke"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-platform = { path = "../../crates/fparkan-platform" }
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" }
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "fparkan-animation"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "fparkan-assets"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-material = { path = "../fparkan-material" }
fparkan-msh = { path = "../fparkan-msh" }
fparkan-nres = { path = "../fparkan-nres" }
fparkan-path = { path = "../fparkan-path" }
fparkan-mission-format = { path = "../fparkan-mission-format" }
fparkan-prototype = { path = "../fparkan-prototype" }
fparkan-resource = { path = "../fparkan-resource" }
fparkan-texm = { path = "../fparkan-texm" }
fparkan-terrain = { path = "../fparkan-terrain" }
fparkan-terrain-format = { path = "../fparkan-terrain-format" }
[dev-dependencies]
fparkan-vfs = { path = "../fparkan-vfs" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "fparkan-binary"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
[lints]
workspace = true
+519
View File
@@ -0,0 +1,519 @@
#![forbid(unsafe_code)]
#![cfg_attr(
test,
allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::expect_used,
clippy::float_cmp,
clippy::identity_op,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::map_unwrap_or,
clippy::needless_raw_string_hashes,
clippy::semicolon_if_nothing_returned,
clippy::type_complexity,
clippy::panic,
clippy::unwrap_used
)
)]
//! Bounded little-endian binary cursor and checked layout helpers.
use std::fmt;
/// SHA-256 digest bytes.
pub type Sha256Digest = [u8; 32];
/// Parser limits shared by binary formats.
#[derive(Clone, Copy, Debug)]
pub struct Limits {
/// Maximum file bytes.
pub max_file_bytes: u64,
/// Maximum entries.
pub max_entries: u32,
/// Maximum string bytes.
pub max_string_bytes: u32,
/// Maximum array items.
pub max_array_items: u32,
/// Maximum recursion depth.
pub max_recursion_depth: u16,
/// Maximum decoded bytes.
pub max_decoded_bytes: u64,
}
impl Default for Limits {
fn default() -> Self {
Self {
max_file_bytes: 256 * 1024 * 1024,
max_entries: 1_000_000,
max_string_bytes: 64 * 1024,
max_array_items: 1_000_000,
max_recursion_depth: 64,
max_decoded_bytes: 512 * 1024 * 1024,
}
}
}
/// Decode error.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DecodeError {
/// Input ended before requested bytes.
UnexpectedEof {
/// Offset where read was attempted.
offset: u64,
/// Required byte count.
needed: u64,
/// Remaining byte count.
remaining: u64,
},
/// Arithmetic overflow.
IntegerOverflow,
/// Count exceeds limit.
LimitExceeded {
/// Declared count.
count: u64,
/// Configured limit.
limit: u64,
},
/// Cursor did not end at EOF.
TrailingBytes {
/// Offset where EOF was expected.
offset: u64,
/// Remaining byte count.
remaining: u64,
},
/// Invalid data.
Invalid(&'static str),
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnexpectedEof {
offset,
needed,
remaining,
} => write!(
f,
"unexpected EOF at {offset}: need {needed}, have {remaining}"
),
Self::IntegerOverflow => write!(f, "integer overflow"),
Self::LimitExceeded { count, limit } => {
write!(f, "count {count} exceeds limit {limit}")
}
Self::TrailingBytes { offset, remaining } => {
write!(f, "trailing bytes at {offset}: {remaining}")
}
Self::Invalid(reason) => write!(f, "invalid data: {reason}"),
}
}
}
impl std::error::Error for DecodeError {}
/// Cursor checkpoint.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Checkpoint(pub u64);
/// Bounded cursor.
#[derive(Clone, Debug)]
pub struct Cursor<'a> {
bytes: &'a [u8],
offset: usize,
}
impl<'a> Cursor<'a> {
/// Creates a cursor.
#[must_use]
pub fn new(bytes: &'a [u8]) -> Self {
Self { bytes, offset: 0 }
}
/// Current offset.
#[must_use]
pub fn offset(&self) -> u64 {
self.offset as u64
}
/// Remaining bytes.
#[must_use]
pub fn remaining(&self) -> usize {
self.bytes.len().saturating_sub(self.offset)
}
/// Creates a checkpoint.
#[must_use]
pub fn checkpoint(&self) -> Checkpoint {
Checkpoint(self.offset())
}
/// Reads exact bytes.
///
/// # Errors
///
/// Returns [`DecodeError::IntegerOverflow`] if the requested end offset
/// overflows, or [`DecodeError::UnexpectedEof`] if there are not enough
/// bytes remaining.
pub fn read_exact(&mut self, len: usize) -> Result<&'a [u8], DecodeError> {
let end = self
.offset
.checked_add(len)
.ok_or(DecodeError::IntegerOverflow)?;
if end > self.bytes.len() {
return Err(DecodeError::UnexpectedEof {
offset: self.offset(),
needed: len as u64,
remaining: self.remaining() as u64,
});
}
let out = &self.bytes[self.offset..end];
self.offset = end;
Ok(out)
}
/// Reads a little-endian u16.
///
/// # Errors
///
/// Returns [`DecodeError`] if two bytes cannot be read.
pub fn read_u16_le(&mut self) -> Result<u16, DecodeError> {
let b = self.read_exact(2)?;
Ok(u16::from_le_bytes([b[0], b[1]]))
}
/// Reads a little-endian u32.
///
/// # Errors
///
/// Returns [`DecodeError`] if four bytes cannot be read.
pub fn read_u32_le(&mut self) -> Result<u32, DecodeError> {
let b = self.read_exact(4)?;
Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
/// Reads a little-endian i32.
///
/// # Errors
///
/// Returns [`DecodeError`] if four bytes cannot be read.
pub fn read_i32_le(&mut self) -> Result<i32, DecodeError> {
let b = self.read_exact(4)?;
Ok(i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
/// Reads a little-endian f32.
///
/// # Errors
///
/// Returns [`DecodeError`] if four bytes cannot be read.
pub fn read_f32_le(&mut self) -> Result<f32, DecodeError> {
Ok(f32::from_bits(self.read_u32_le()?))
}
/// Requires exact EOF.
///
/// # Errors
///
/// Returns [`DecodeError::TrailingBytes`] when unread bytes remain.
pub fn require_eof(&self) -> Result<(), DecodeError> {
if self.remaining() == 0 {
Ok(())
} else {
Err(DecodeError::TrailingBytes {
offset: self.offset(),
remaining: self.remaining() as u64,
})
}
}
}
/// Validates `count * stride <= remaining` and returns bytes as usize.
///
/// # Errors
///
/// Returns [`DecodeError::IntegerOverflow`] on arithmetic or conversion
/// overflow, or [`DecodeError::UnexpectedEof`] when the declared byte count is
/// larger than the remaining bounded input.
pub fn checked_count_bytes(count: u64, stride: u64, remaining: u64) -> Result<usize, DecodeError> {
let bytes = count
.checked_mul(stride)
.ok_or(DecodeError::IntegerOverflow)?;
if bytes > remaining {
return Err(DecodeError::UnexpectedEof {
offset: 0,
needed: bytes,
remaining,
});
}
usize::try_from(bytes).map_err(|_| DecodeError::IntegerOverflow)
}
/// Validates a declared allocation size before constructing the allocation.
///
/// # Errors
///
/// Returns [`DecodeError::LimitExceeded`] when `declared` is larger than
/// `limit`, or [`DecodeError::IntegerOverflow`] when the accepted size cannot
/// be represented by the host `usize`.
pub fn checked_allocation_len(declared: u64, limit: u64) -> Result<usize, DecodeError> {
if declared > limit {
return Err(DecodeError::LimitExceeded {
count: declared,
limit,
});
}
usize::try_from(declared).map_err(|_| DecodeError::IntegerOverflow)
}
/// Reads length-prefixed bytes.
///
/// # Errors
///
/// Returns [`DecodeError`] if the length cannot be read, exceeds `max`, or the
/// declared payload is truncated.
pub fn read_lp_bytes(cursor: &mut Cursor<'_>, max: u32) -> Result<Vec<u8>, DecodeError> {
let len = cursor.read_u32_le()?;
if len > max {
return Err(DecodeError::LimitExceeded {
count: u64::from(len),
limit: u64::from(max),
});
}
let len = checked_allocation_len(u64::from(len), u64::from(max))?;
Ok(cursor.read_exact(len)?.to_vec())
}
/// Computes a SHA-256 content digest without external dependencies.
#[must_use]
pub fn sha256(bytes: &[u8]) -> Sha256Digest {
const K: [u32; 64] = [
0x428a_2f98,
0x7137_4491,
0xb5c0_fbcf,
0xe9b5_dba5,
0x3956_c25b,
0x59f1_11f1,
0x923f_82a4,
0xab1c_5ed5,
0xd807_aa98,
0x1283_5b01,
0x2431_85be,
0x550c_7dc3,
0x72be_5d74,
0x80de_b1fe,
0x9bdc_06a7,
0xc19b_f174,
0xe49b_69c1,
0xefbe_4786,
0x0fc1_9dc6,
0x240c_a1cc,
0x2de9_2c6f,
0x4a74_84aa,
0x5cb0_a9dc,
0x76f9_88da,
0x983e_5152,
0xa831_c66d,
0xb003_27c8,
0xbf59_7fc7,
0xc6e0_0bf3,
0xd5a7_9147,
0x06ca_6351,
0x1429_2967,
0x27b7_0a85,
0x2e1b_2138,
0x4d2c_6dfc,
0x5338_0d13,
0x650a_7354,
0x766a_0abb,
0x81c2_c92e,
0x9272_2c85,
0xa2bf_e8a1,
0xa81a_664b,
0xc24b_8b70,
0xc76c_51a3,
0xd192_e819,
0xd699_0624,
0xf40e_3585,
0x106a_a070,
0x19a4_c116,
0x1e37_6c08,
0x2748_774c,
0x34b0_bcb5,
0x391c_0cb3,
0x4ed8_aa4a,
0x5b9c_ca4f,
0x682e_6ff3,
0x748f_82ee,
0x78a5_636f,
0x84c8_7814,
0x8cc7_0208,
0x90be_fffa,
0xa450_6ceb,
0xbef9_a3f7,
0xc671_78f2,
];
let mut h = [
0x6a09_e667,
0xbb67_ae85,
0x3c6e_f372,
0xa54f_f53a,
0x510e_527f,
0x9b05_688c,
0x1f83_d9ab,
0x5be0_cd19,
];
let bit_len = (bytes.len() as u64).wrapping_mul(8);
let mut chunks = bytes.chunks_exact(64);
for chunk in &mut chunks {
compress_sha256_chunk(&mut h, chunk, &K);
}
let tail = chunks.remainder();
let mut block = [0u8; 128];
block[..tail.len()].copy_from_slice(tail);
block[tail.len()] = 0x80;
let padded_len = if tail.len() < 56 { 64 } else { 128 };
block[padded_len - 8..padded_len].copy_from_slice(&bit_len.to_be_bytes());
for chunk in block[..padded_len].chunks_exact(64) {
compress_sha256_chunk(&mut h, chunk, &K);
}
let mut out = [0u8; 32];
for (idx, word) in h.iter().enumerate() {
out[idx * 4..idx * 4 + 4].copy_from_slice(&word.to_be_bytes());
}
out
}
/// Renders a SHA-256 digest as lowercase hexadecimal.
#[must_use]
pub fn sha256_hex(digest: &Sha256Digest) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(64);
for byte in digest {
out.push(char::from(HEX[usize::from(byte >> 4)]));
out.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
out
}
#[allow(clippy::many_single_char_names)]
fn compress_sha256_chunk(h: &mut [u32; 8], chunk: &[u8], k: &[u32; 64]) {
let mut w = [0u32; 64];
for (idx, word) in w.iter_mut().take(16).enumerate() {
let base = idx * 4;
*word = u32::from_be_bytes([
chunk[base],
chunk[base + 1],
chunk[base + 2],
chunk[base + 3],
]);
}
for idx in 16..64 {
let s0 = w[idx - 15].rotate_right(7) ^ w[idx - 15].rotate_right(18) ^ (w[idx - 15] >> 3);
let s1 = w[idx - 2].rotate_right(17) ^ w[idx - 2].rotate_right(19) ^ (w[idx - 2] >> 10);
w[idx] = w[idx - 16]
.wrapping_add(s0)
.wrapping_add(w[idx - 7])
.wrapping_add(s1);
}
let mut a = h[0];
let mut b = h[1];
let mut c = h[2];
let mut d = h[3];
let mut e = h[4];
let mut f = h[5];
let mut g = h[6];
let mut hh = h[7];
for idx in 0..64 {
let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
let ch = (e & f) ^ ((!e) & g);
let temp1 = hh
.wrapping_add(s1)
.wrapping_add(ch)
.wrapping_add(k[idx])
.wrapping_add(w[idx]);
let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
let maj = (a & b) ^ (a & c) ^ (b & c);
let temp2 = s0.wrapping_add(maj);
hh = g;
g = f;
f = e;
e = d.wrapping_add(temp1);
d = c;
c = b;
b = a;
a = temp1.wrapping_add(temp2);
}
h[0] = h[0].wrapping_add(a);
h[1] = h[1].wrapping_add(b);
h[2] = h[2].wrapping_add(c);
h[3] = h[3].wrapping_add(d);
h[4] = h[4].wrapping_add(e);
h[5] = h[5].wrapping_add(f);
h[6] = h[6].wrapping_add(g);
h[7] = h[7].wrapping_add(hh);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_count_stride_overflow() {
assert_eq!(
checked_count_bytes(u64::MAX, 2, u64::MAX),
Err(DecodeError::IntegerOverflow)
);
}
#[test]
fn exact_eof_reports_trailing() {
let mut cursor = Cursor::new(&[1, 2]);
assert_eq!(cursor.read_exact(1).expect("byte"), &[1]);
assert!(matches!(
cursor.require_eof(),
Err(DecodeError::TrailingBytes { .. })
));
}
#[test]
fn rejects_oversized_declared_allocation_before_read() {
assert_eq!(
checked_allocation_len(1025, 1024),
Err(DecodeError::LimitExceeded {
count: 1025,
limit: 1024
})
);
let bytes = 2048u32.to_le_bytes();
let mut cursor = Cursor::new(&bytes);
assert_eq!(
read_lp_bytes(&mut cursor, 1024),
Err(DecodeError::LimitExceeded {
count: 2048,
limit: 1024
})
);
assert_eq!(cursor.offset(), 4);
}
#[test]
fn sha256_matches_known_vectors() {
assert_eq!(
sha256_hex(&sha256(b"")),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(
sha256_hex(&sha256(b"abc")),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
}
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "fparkan-corpus"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-binary = { path = "../fparkan-binary" }
fparkan-fx = { path = "../fparkan-fx" }
fparkan-material = { path = "../fparkan-material" }
fparkan-msh = { path = "../fparkan-msh" }
fparkan-mission-format = { path = "../fparkan-mission-format" }
fparkan-nres = { path = "../fparkan-nres" }
fparkan-prototype = { path = "../fparkan-prototype" }
fparkan-path = { path = "../fparkan-path" }
fparkan-rsli = { path = "../fparkan-rsli" }
fparkan-texm = { path = "../fparkan-texm" }
fparkan-terrain-format = { path = "../fparkan-terrain-format" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "fparkan-diagnostics"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[lints]
workspace = true
+249
View File
@@ -0,0 +1,249 @@
#![forbid(unsafe_code)]
#![cfg_attr(
test,
allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::expect_used,
clippy::float_cmp,
clippy::identity_op,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::map_unwrap_or,
clippy::needless_raw_string_hashes,
clippy::semicolon_if_nothing_returned,
clippy::type_complexity,
clippy::panic,
clippy::unwrap_used
)
)]
//! Structured diagnostics shared by `FParkan` crates.
use serde::Serialize;
/// Diagnostic severity.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
/// Informational note.
Info,
/// Recoverable warning.
Warning,
/// Error for the current operation.
Error,
/// Fatal error for the current run.
Fatal,
}
/// Evidence level for a contract or interpretation.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum EvidenceStatus {
/// Described by project documentation.
Documented,
/// Verified by synthetic fixtures.
SyntheticVerified,
/// Verified against the licensed corpus.
CorpusVerified,
/// Verified by runtime capture.
RuntimeCaptured,
/// Working hypothesis; not a runtime contract.
Hypothesis,
}
/// Operation phase where a diagnostic was produced.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Phase {
/// Discovery.
Discover,
/// Read.
Read,
/// Parse.
Parse,
/// Validate.
Validate,
/// Resolve.
Resolve,
/// Prepare.
Prepare,
/// Construct.
Construct,
/// Register.
Register,
/// Simulate.
Simulate,
/// Render.
Render,
}
/// Byte span in an input source.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub struct SourceSpan {
/// Start offset.
pub offset: u64,
/// Length in bytes.
pub length: u64,
}
/// Stable diagnostic code.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
pub struct DiagnosticCode(pub &'static str);
/// Context attached to a diagnostic.
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
pub struct DiagnosticContext {
/// Phase.
#[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<Phase>,
/// Redacted or logical path.
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
/// Archive entry name.
#[serde(skip_serializing_if = "Option::is_none")]
pub archive_entry: Option<String>,
/// Object/prototype key.
#[serde(skip_serializing_if = "Option::is_none")]
pub object_key: Option<String>,
/// Input span.
#[serde(skip_serializing_if = "Option::is_none")]
pub span: Option<SourceSpan>,
}
/// Structured diagnostic with cause chain.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct Diagnostic {
/// Stable code.
pub code: DiagnosticCode,
/// Severity.
pub severity: Severity,
/// Human message.
pub message: String,
/// Context.
pub context: DiagnosticContext,
/// Causes.
pub causes: Vec<Diagnostic>,
}
/// Creates a diagnostic with default error severity.
#[must_use]
pub fn diagnostic(code: DiagnosticCode, message: impl Into<String>) -> Diagnostic {
Diagnostic {
code,
severity: Severity::Error,
message: message.into(),
context: DiagnosticContext::default(),
causes: Vec::new(),
}
}
impl Diagnostic {
/// Returns a copy with severity changed.
#[must_use]
pub fn with_severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
/// Returns a copy with context changed.
#[must_use]
pub fn with_context(mut self, context: DiagnosticContext) -> Self {
self.context = context;
self
}
/// Adds a cause.
pub fn push_cause(&mut self, cause: Diagnostic) {
self.causes.push(cause);
}
}
/// Renders a compact human-readable diagnostic.
#[must_use]
pub fn render_human(diagnostic: &Diagnostic) -> String {
let mut out = format!(
"{:?} {}: {}",
diagnostic.severity, diagnostic.code.0, diagnostic.message
);
if let Some(path) = &diagnostic.context.path {
out.push_str(" [");
out.push_str(path);
out.push(']');
}
out
}
/// Renders deterministic JSON using the typed diagnostic schema.
#[must_use]
pub fn render_json(diagnostic: &Diagnostic) -> String {
match serde_json::to_string(diagnostic) {
Ok(json) => json,
Err(err) => format!("{{\"error\":\"diagnostic serialization failed: {err}\"}}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_is_stable() {
let d = diagnostic(DiagnosticCode("S0-DIAG-001"), "keeps context").with_context(
DiagnosticContext {
phase: Some(Phase::Parse),
..DiagnosticContext::default()
},
);
assert_eq!(
render_json(&d),
"{\"code\":\"S0-DIAG-001\",\"severity\":\"error\",\"message\":\"keeps context\",\"context\":{\"phase\":\"parse\"},\"causes\":[]}"
);
}
#[test]
fn diagnostic_chain_preserves_context() {
let mut root = diagnostic(DiagnosticCode("ROOT"), "root").with_context(DiagnosticContext {
phase: Some(Phase::Resolve),
path: Some("archives/material.lib".to_string()),
archive_entry: Some("MATERIAL.MAT0".to_string()),
object_key: Some("unit/tank".to_string()),
span: Some(SourceSpan {
offset: 12,
length: 4,
}),
});
root.push_cause(diagnostic(DiagnosticCode("CAUSE"), "cause").with_context(
DiagnosticContext {
phase: Some(Phase::Parse),
path: Some("archives/material.lib".to_string()),
span: Some(SourceSpan {
offset: 16,
length: 8,
}),
..DiagnosticContext::default()
},
));
let json = render_json(&root);
assert!(json.contains("\"code\":\"ROOT\""));
assert!(json.contains("\"phase\":\"resolve\""));
assert!(json.contains("\"path\":\"archives/material.lib\""));
assert!(json.contains("\"archive_entry\":\"MATERIAL.MAT0\""));
assert!(json.contains("\"object_key\":\"unit/tank\""));
assert!(json.contains("\"span\":{\"offset\":12,\"length\":4}"));
assert!(json.contains("\"code\":\"CAUSE\""));
assert!(json.contains("\"span\":{\"offset\":16,\"length\":8}"));
}
#[test]
fn json_escapes_all_control_characters() {
let value = diagnostic(DiagnosticCode("S1-H01"), "quote\"\u{0000}tab\tline\r\n");
let json = render_json(&value);
assert!(json.contains("\\u0000"));
assert!(json.contains("\\t"));
assert!(!json.contains('\t'));
assert!(!json.contains('\r'));
}
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "fparkan-fx"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-binary = { path = "../fparkan-binary" }
[dev-dependencies]
fparkan-nres = { path = "../fparkan-nres" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "fparkan-inspection"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-msh = { path = "../fparkan-msh" }
fparkan-nres = { path = "../fparkan-nres" }
fparkan-rsli = { path = "../fparkan-rsli" }
fparkan-resource = { path = "../fparkan-resource" }
fparkan-terrain-format = { path = "../fparkan-terrain-format" }
fparkan-texm = { path = "../fparkan-texm" }
fparkan-vfs = { path = "../fparkan-vfs" }
[lints]
workspace = true
+327
View File
@@ -0,0 +1,327 @@
#![forbid(unsafe_code)]
#![cfg_attr(
test,
allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::expect_used,
clippy::float_cmp,
clippy::identity_op,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::map_unwrap_or,
clippy::needless_raw_string_hashes,
clippy::semicolon_if_nothing_returned,
clippy::type_complexity,
clippy::panic,
clippy::unwrap_used
)
)]
//! Shared inspection helpers for format-backed tooling.
use fparkan_msh::{decode_msh, validate_msh};
use fparkan_nres::{decode as decode_nres, NresDocument, ReadProfile};
use fparkan_resource::{archive_path, resource_name, CachedResourceRepository, ResourceRepository};
use fparkan_rsli::decode as decode_rsli;
use fparkan_terrain_format::{decode_land_map, decode_land_msh};
use fparkan_texm::decode_texm;
use fparkan_vfs::DirectoryVfs;
use std::fs;
use std::path::Path;
use std::sync::Arc;
/// Archive inspection variants.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ArchiveInspection {
/// `NRes` inspection summary.
Nres {
/// Archive entry count.
entries: usize,
/// Lookup order validity.
lookup_order_valid: bool,
/// Entry samples (subject to request limit).
sample: Vec<NresEntrySummary>,
},
/// `RsLi` inspection summary.
Rsli {
/// Archive entry count.
entries: usize,
},
/// Unknown/unsupported archive magic.
Unsupported,
}
/// `NRes` entry summary.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NresEntrySummary {
/// ASCII/legacy resource name.
pub name: String,
/// Entry type identifier.
pub type_id: u32,
/// Declared entry payload size.
pub data_size: u32,
}
/// Model inspection payload.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ModelInspection {
/// Terrain stream/document stream count.
pub streams: usize,
/// Node count.
pub nodes: usize,
/// Slot count.
pub slots: usize,
/// Position count.
pub positions: usize,
/// Index count.
pub indices: usize,
/// Batch count.
pub batches: usize,
}
/// Texture inspection payload.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TextureInspection {
/// Width.
pub width: u32,
/// Height.
pub height: u32,
/// Texture format debug text.
pub format: String,
/// Mip level count.
pub mips: usize,
/// Total page rectangles.
pub pages: usize,
}
/// Land map/msh inspection payload.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MapInspection {
/// Mapped mesh stream count.
pub streams: usize,
/// Slot count.
pub slots: usize,
/// Position count.
pub positions: usize,
/// Face count.
pub faces: usize,
/// Terrain areals.
pub areals: usize,
/// Declared areal count from map metadata.
pub declared_areals: u32,
/// Map grid width.
pub grid_width: u32,
/// Map grid height.
pub grid_height: u32,
}
/// Supported land file kinds.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LandFileKind {
/// `land.msh` payload.
LandMsh,
/// `land.map` payload.
LandMap,
}
/// Inspects a format archive.
///
/// # Errors
///
/// Returns a string error when the archive cannot be read or decoded.
pub fn inspect_archive_file(path: &Path, sample_limit: usize) -> Result<ArchiveInspection, String> {
let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?;
inspect_archive_bytes(&bytes, sample_limit, Some(path))
}
/// Inspects archive bytes and returns a typed summary.
fn inspect_archive_bytes(
bytes: &[u8],
sample_limit: usize,
source: Option<&Path>,
) -> Result<ArchiveInspection, String> {
if bytes.starts_with(b"NRes") {
let document = decode_nres(
Arc::from(bytes.to_vec().into_boxed_slice()),
ReadProfile::Compatible,
)
.map_err(|err| err.to_string())?;
let mut sample = Vec::new();
for entry in document.entries().iter().take(sample_limit) {
sample.push(NresEntrySummary {
name: String::from_utf8_lossy(entry.name_bytes()).to_string(),
type_id: entry.meta().type_id,
data_size: entry.meta().data_size,
});
}
Ok(ArchiveInspection::Nres {
entries: document.entries().len(),
lookup_order_valid: document.lookup_order_valid(),
sample,
})
} else if bytes.get(0..4) == Some(b"NL\0\x01") {
let document = decode_rsli(
Arc::from(bytes.to_vec().into_boxed_slice()),
fparkan_rsli::ReadProfile::Compatible,
)
.map_err(|err| err.to_string())?;
Ok(ArchiveInspection::Rsli {
entries: document.entries().len(),
})
} else {
match source {
Some(path) => Err(format!("{}: unsupported archive magic", path.display())),
None => Err("unsupported archive magic".to_string()),
}
}
}
/// Inspects a model through repository-backed resource lookup.
///
/// # Errors
///
/// Returns a string error when the resource cannot be resolved or parsed as a
/// valid model payload.
pub fn inspect_model_from_root(
root: &Path,
archive: &str,
resource: &str,
) -> Result<ModelInspection, String> {
let bytes = read_resource_bytes(root, archive, resource)?;
let document = decode_nres(bytes, ReadProfile::Compatible).map_err(|err| err.to_string())?;
let msh = decode_msh(&document).map_err(|err| err.to_string())?;
let validated = validate_msh(&msh).map_err(|err| err.to_string())?;
Ok(ModelInspection {
streams: msh.streams().len(),
nodes: validated.node_count,
slots: validated.slots.len(),
positions: validated.positions.len(),
indices: validated.indices.len(),
batches: validated.batches.len(),
})
}
/// Inspects a texture through repository-backed resource lookup.
///
/// # Errors
///
/// Returns a string error when the resource cannot be resolved or parsed as a
/// valid texture payload.
pub fn inspect_texture_from_root(
root: &Path,
archive: &str,
resource: &str,
) -> Result<TextureInspection, String> {
let bytes = read_resource_bytes(root, archive, resource)?;
let document = decode_texm(bytes).map_err(|err| err.to_string())?;
Ok(TextureInspection {
width: document.width(),
height: document.height(),
format: format!("{:?}", document.format()),
mips: document.mip_count(),
pages: document.page_rects().len(),
})
}
/// Inspects a terrain land file by path.
///
/// # Errors
///
/// Returns a string error when the file cannot be read or parsed as the
/// requested terrain payload kind.
pub fn inspect_land_file(path: &Path, kind: LandFileKind) -> Result<MapInspection, String> {
let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?;
let document = decode_nres(Arc::from(bytes.into_boxed_slice()), ReadProfile::Compatible)
.map_err(|err| err.to_string())?;
match kind {
LandFileKind::LandMsh => inspect_land_msh(&document),
LandFileKind::LandMap => inspect_land_map(&document),
}
}
fn inspect_land_msh(document: &NresDocument) -> Result<MapInspection, String> {
let land_msh = decode_land_msh(document).map_err(|err| err.to_string())?;
Ok(MapInspection {
streams: land_msh.streams.len(),
slots: land_msh.slots.slots_raw.len(),
positions: land_msh.positions.len(),
faces: land_msh.faces.len(),
areals: 0,
declared_areals: 0,
grid_width: 0,
grid_height: 0,
})
}
fn inspect_land_map(document: &NresDocument) -> Result<MapInspection, String> {
let land_map = decode_land_map(document).map_err(|err| err.to_string())?;
Ok(MapInspection {
streams: 0,
slots: 0,
positions: 0,
faces: 0,
areals: land_map.areals.len(),
declared_areals: land_map.areal_count,
grid_width: land_map.grid.cells_x,
grid_height: land_map.grid.cells_y,
})
}
fn read_resource_bytes(root: &Path, archive: &str, name: &str) -> Result<Arc<[u8]>, String> {
let repository = CachedResourceRepository::new(Arc::new(DirectoryVfs::new(root)));
let archive_path = archive_path(archive.as_bytes()).map_err(|err| err.to_string())?;
let resource_name = resource_name(name.as_bytes());
let archive_handle = repository
.open_archive(&archive_path)
.map_err(|err| format!("{err}"))?;
let Some(handle) = repository
.find(archive_handle, &resource_name)
.map_err(|err| format!("{err}"))?
else {
return Err(format!(
"resource not found: {archive}/{}",
String::from_utf8_lossy(name.as_bytes())
));
};
let bytes = repository.read(handle).map_err(|err| format!("{err}"))?;
Ok(Arc::from(bytes.into_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
use std::path::PathBuf;
#[test]
fn inspect_rsli_rejects_malformed_archive() {
let dir = temp_dir("inspect");
let path = dir.join("test.rsli");
let mut file = fs::File::create(&path).expect("file");
file.write_all(b"NL\0\x01").expect("magic");
drop(file);
let error = inspect_archive_file(&path, 0).expect_err("malformed archive");
assert!(error.contains("entry table out of bounds"));
}
#[test]
fn nres_entry_summary_fields_are_readable() {
let dir = temp_dir("inspect-nres");
let archive = dir.join("test.nres");
let payload = Vec::from("NRes\x00\x00\x00\x00");
fs::write(&archive, &payload).expect("nres");
let _ = inspect_archive_file(&archive, 2);
}
fn temp_dir(name: &str) -> PathBuf {
let base = PathBuf::from("/tmp")
.join("fparkan-inspection-tests")
.join(name);
let _ = fs::remove_dir_all(&base);
fs::create_dir_all(&base).expect("tmp dir");
base
}
}
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "fparkan-material"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
encoding_rs = "0.8"
fparkan-path = { path = "../fparkan-path" }
fparkan-resource = { path = "../fparkan-resource" }
[dev-dependencies]
fparkan-nres = { path = "../fparkan-nres" }
fparkan-vfs = { path = "../fparkan-vfs" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "fparkan-mission-format"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
encoding_rs = "0.8"
fparkan-binary = { path = "../fparkan-binary" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "fparkan-msh"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
encoding_rs = "0.8"
fparkan-nres = { path = "../fparkan-nres" }
[dev-dependencies]
fparkan-animation = { path = "../fparkan-animation" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "fparkan-nres"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-binary = { path = "../fparkan-binary" }
fparkan-path = { path = "../fparkan-path" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "fparkan-path"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
[lints]
workspace = true
+367
View File
@@ -0,0 +1,367 @@
#![forbid(unsafe_code)]
#![cfg_attr(
test,
allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::expect_used,
clippy::float_cmp,
clippy::identity_op,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::map_unwrap_or,
clippy::needless_raw_string_hashes,
clippy::semicolon_if_nothing_returned,
clippy::type_complexity,
clippy::panic,
clippy::unwrap_used
)
)]
//! Legacy path normalization and ASCII lookup semantics.
use std::fmt;
use std::path::{Path, PathBuf};
/// Original bytes.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OriginalPathBytes(pub Vec<u8>);
impl OriginalPathBytes {
/// Returns the preserved byte image.
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
/// Returns the preserved byte image as an owned vector.
#[must_use]
pub fn into_vec(self) -> Vec<u8> {
self.0
}
}
/// Normalized relative path.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct NormalizedPath {
raw: Vec<u8>,
display: String,
}
impl NormalizedPath {
/// Returns string view.
#[must_use]
pub fn as_str(&self) -> &str {
&self.display
}
/// Returns normalized byte view.
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.raw
}
/// Returns an OS path owned path buffer.
#[must_use]
pub fn as_path(&self) -> PathBuf {
as_os_path_from_bytes(&self.raw)
}
}
/// Normalized path paired with its original byte image.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NormalizedPathWithOriginal {
normalized: NormalizedPath,
original: OriginalPathBytes,
}
impl NormalizedPathWithOriginal {
/// Returns normalized path.
#[must_use]
pub fn normalized(&self) -> &NormalizedPath {
&self.normalized
}
/// Returns original path bytes.
#[must_use]
pub fn original(&self) -> &OriginalPathBytes {
&self.original
}
/// Splits into normalized and original path parts.
#[must_use]
pub fn into_parts(self) -> (NormalizedPath, OriginalPathBytes) {
(self.normalized, self.original)
}
}
/// ASCII lookup key.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct LookupKey(pub Vec<u8>);
/// Resource name bytes.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ResourceName(pub Vec<u8>);
/// Path policy.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PathPolicy {
/// Strict legacy relative resource path.
StrictLegacy,
/// Host compatible relative path.
HostCompatible,
}
/// Path error.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PathError {
/// Empty path.
Empty,
/// Embedded NUL.
EmbeddedNul,
/// Absolute path.
Absolute,
/// Parent traversal.
ParentTraversal,
/// Host path escape.
EscapesRoot,
}
impl fmt::Display for PathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "path is empty"),
Self::EmbeddedNul => write!(f, "path contains an embedded NUL byte"),
Self::Absolute => write!(f, "path must be relative and cannot be absolute"),
Self::ParentTraversal => write!(f, "path attempts to traverse outside its root"),
Self::EscapesRoot => write!(f, "normalized path escapes the configured root"),
}
}
}
impl std::error::Error for PathError {}
/// Normalizes a relative path.
///
/// # Errors
///
/// Returns [`PathError`] when the input is empty, absolute, contains an
/// embedded NUL, attempts parent traversal, or has an invalid drive prefix.
pub fn normalize_relative(raw: &[u8], policy: PathPolicy) -> Result<NormalizedPath, PathError> {
if raw.is_empty() {
return Err(PathError::Empty);
}
if raw.contains(&0) {
return Err(PathError::EmbeddedNul);
}
if raw.starts_with(b"/") || raw.starts_with(b"\\") || has_drive_prefix(raw) {
return Err(PathError::Absolute);
}
let mut parts = Vec::new();
for part in raw.split(|byte| *byte == b'/' || *byte == b'\\') {
if part.is_empty() || part == b"." {
if policy == PathPolicy::StrictLegacy {
return Err(PathError::ParentTraversal);
}
continue;
}
if part == b".." {
return Err(PathError::ParentTraversal);
}
if policy == PathPolicy::StrictLegacy && part.contains(&b':') {
return Err(PathError::Absolute);
}
parts.push(part);
}
if parts.is_empty() {
return Err(PathError::Empty);
}
let mut normalized = Vec::new();
for (index, part) in parts.iter().enumerate() {
if index > 0 {
normalized.push(b'/');
}
normalized.extend_from_slice(part);
}
let display = String::from_utf8_lossy(&normalized).into_owned();
Ok(NormalizedPath {
raw: normalized,
display,
})
}
/// Normalizes a relative path while preserving its original bytes.
///
/// # Errors
///
/// Returns [`PathError`] under the same conditions as [`normalize_relative`].
pub fn normalize_relative_with_original(
raw: &[u8],
policy: PathPolicy,
) -> Result<NormalizedPathWithOriginal, PathError> {
let normalized = normalize_relative(raw, policy)?;
Ok(NormalizedPathWithOriginal {
normalized,
original: OriginalPathBytes(raw.to_vec()),
})
}
fn has_drive_prefix(bytes: &[u8]) -> bool {
bytes.len() >= 2 && bytes[1] == b':' && bytes[0].is_ascii_alphabetic()
}
/// Builds an ASCII-only casefold lookup key.
#[must_use]
pub fn ascii_lookup_key(raw: &[u8]) -> LookupKey {
LookupKey(raw.iter().map(u8::to_ascii_uppercase).collect())
}
/// Ensures relative path does not escape.
///
/// # Errors
///
/// Returns [`PathError::ParentTraversal`] when a normalized segment attempts
/// to address a parent directory.
pub fn reject_escape(rel: &NormalizedPath) -> Result<(), PathError> {
if rel
.as_bytes()
.split(|byte| *byte == b'/')
.any(|part| part == b"..")
{
Err(PathError::ParentTraversal)
} else {
Ok(())
}
}
/// Joins normalized path under root.
///
/// # Errors
///
/// Returns [`PathError`] if the normalized path fails the escape check.
pub fn join_under(root: &Path, rel: &NormalizedPath) -> Result<PathBuf, PathError> {
reject_escape(rel)?;
Ok(root.join(rel.as_path()))
}
#[cfg(unix)]
fn as_os_path_from_bytes(raw: &[u8]) -> PathBuf {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
PathBuf::from(OsString::from_vec(raw.to_vec()))
}
#[cfg(not(unix))]
fn as_os_path_from_bytes(raw: &[u8]) -> PathBuf {
PathBuf::from(String::from_utf8_lossy(raw).into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalizes_separators() {
let p = normalize_relative(b"DATA\\MAPS/INTRO/Land.msh", PathPolicy::StrictLegacy)
.expect("path");
assert_eq!(p.as_str(), "DATA/MAPS/INTRO/Land.msh");
}
#[test]
fn rejects_escape() {
assert_eq!(
normalize_relative(b"DATA/../secret", PathPolicy::StrictLegacy),
Err(PathError::ParentTraversal)
);
}
#[test]
fn rejects_absolute_drive_and_nul_paths() {
assert_eq!(
normalize_relative(b"/DATA/MAPS", PathPolicy::StrictLegacy),
Err(PathError::Absolute)
);
assert_eq!(
normalize_relative(b"C:\\DATA\\MAPS", PathPolicy::StrictLegacy),
Err(PathError::Absolute)
);
assert_eq!(
normalize_relative(b"DATA\0MAPS", PathPolicy::StrictLegacy),
Err(PathError::EmbeddedNul)
);
}
#[test]
fn path_error_display_is_actionable() {
assert_eq!(
PathError::ParentTraversal.to_string(),
"path attempts to traverse outside its root"
);
assert_eq!(
PathError::EmbeddedNul.to_string(),
"path contains an embedded NUL byte"
);
}
#[test]
fn strict_legacy_rejects_host_only_segments() {
assert_eq!(
normalize_relative(b"./DATA/MAPS", PathPolicy::StrictLegacy),
Err(PathError::ParentTraversal)
);
assert_eq!(
normalize_relative(b"DATA//MAPS", PathPolicy::StrictLegacy),
Err(PathError::ParentTraversal)
);
assert_eq!(
normalize_relative(b"DATA/stream:name", PathPolicy::StrictLegacy),
Err(PathError::Absolute)
);
let host = normalize_relative(b"./DATA//MAPS", PathPolicy::HostCompatible).expect("host");
assert_eq!(host.as_str(), "DATA/MAPS");
}
#[test]
fn join_under_keeps_normalized_path_below_root() {
let rel = normalize_relative(b"DATA/MAPS/Land.map", PathPolicy::StrictLegacy)
.expect("relative path");
let joined = join_under(Path::new("/game"), &rel).expect("join");
assert_eq!(joined, PathBuf::from("/game/DATA/MAPS/Land.map"));
}
#[test]
fn ascii_casefold_does_not_unicode_fold() {
assert_eq!(ascii_lookup_key(b"AbZ\xD0"), LookupKey(b"ABZ\xD0".to_vec()));
}
#[test]
fn non_ascii_original_bytes_remain_stable() {
let raw = "DATA/Тест.bin".as_bytes();
let path = normalize_relative_with_original(raw, PathPolicy::StrictLegacy)
.expect("path with non-ASCII UTF-8");
assert_eq!(path.normalized().as_str().as_bytes(), raw);
assert_eq!(path.original().as_bytes(), raw);
assert_eq!(&ascii_lookup_key(raw).0[5..13], &raw[5..13]);
}
#[test]
fn accepts_non_utf8_legacy_bytes() {
let path = normalize_relative(b"DATA/\xFF.bin", PathPolicy::HostCompatible)
.expect("raw legacy bytes");
assert_eq!(path.as_str(), "DATA/\u{FFFD}.bin");
}
#[test]
fn original_separators_and_raw_bytes_are_preserved() {
let raw = b"DATA\\Maps/Intro\\Land.msh";
let path = normalize_relative_with_original(raw, PathPolicy::StrictLegacy).expect("path");
assert_eq!(path.normalized().as_str(), "DATA/Maps/Intro/Land.msh");
assert_eq!(path.original().as_bytes(), raw);
}
}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "fparkan-platform"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
raw-window-handle = "0.6"
[lints]
workspace = true
+233
View File
@@ -0,0 +1,233 @@
#![forbid(unsafe_code)]
#![cfg_attr(
test,
allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::expect_used,
clippy::float_cmp,
clippy::identity_op,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::map_unwrap_or,
clippy::needless_raw_string_hashes,
clippy::semicolon_if_nothing_returned,
clippy::type_complexity,
clippy::panic,
clippy::unwrap_used
)
)]
//! Platform ports for clocks, event sources and window descriptors.
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
/// Monotonic instant measured in milliseconds since process start.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct MonotonicInstant(pub u64);
/// Platform clock.
pub trait MonotonicClock {
/// Current instant.
fn now(&self) -> MonotonicInstant;
}
/// Platform event.
#[derive(Clone, Debug, PartialEq)]
pub enum PlatformEvent {
/// Window/application requested to quit.
QuitRequested,
/// Window focus changed.
FocusChanged {
/// Whether the window is focused.
focused: bool,
},
/// Window resize or move to a new drawable size.
Resize {
/// Drawable width in physical pixels.
width: u32,
/// Drawable height in physical pixels.
height: u32,
},
/// Device pixel ratio changed.
DpiChanged {
/// Logical-to-physical scale factor.
scale: f64,
},
/// Window minimized/hidden.
Minimized {
/// Whether the window is minimized.
minimized: bool,
},
/// Window occlusion state changed.
Occluded {
/// Whether the window is occluded.
occluded: bool,
},
/// Window is being suspended.
Suspended,
/// Window resumed from suspend.
Resumed,
/// Keyboard/scancode input.
KeyboardInput {
/// Platform scancode.
scancode: u32,
/// Pressed state.
pressed: bool,
},
/// Mouse button input.
MouseInput {
/// Mouse button code.
button: u16,
/// Pressed state.
pressed: bool,
/// X position in window coordinates.
x: f64,
/// Y position in window coordinates.
y: f64,
},
/// Mouse cursor movement.
CursorMoved {
/// Cursor x.
x: f64,
/// Cursor y.
y: f64,
},
}
/// Platform error with optional source detail.
#[derive(Debug)]
pub enum PlatformError {
/// Backend/backend-specific failure.
Backend {
/// Operation or subsystem.
context: &'static str,
/// Human-readable details.
message: String,
},
}
impl std::fmt::Display for PlatformError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Backend { context, message } => {
write!(f, "{context}: {message}")
}
}
}
}
impl std::error::Error for PlatformError {}
/// Event source contract for polling platform events.
pub trait EventSource {
/// Polls events.
///
/// # Errors
///
/// Returns [`PlatformError`] when the backend cannot collect events.
fn poll(&mut self, out: &mut Vec<PlatformEvent>) -> Result<(), PlatformError>;
}
/// Physical window size.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PhysicalSize {
/// Width.
pub width: u32,
/// Height.
pub height: u32,
}
/// Window identity as a stable opaque handle token.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct WindowHandle {
/// Opaque integer token.
pub id: u64,
}
/// Native raw window/display handles for render surface creation.
#[derive(Clone, Copy, Debug)]
pub struct NativeWindowHandles {
/// Raw display handle.
pub display: RawDisplayHandle,
/// Raw window handle.
pub window: RawWindowHandle,
}
/// Window presentation and lifecycle port.
///
/// Presentation is not owned by the window abstraction. Render adapters
/// own swapchain and present lifecycle.
pub trait WindowPort {
/// Current drawable size.
fn drawable_size(&self) -> PhysicalSize;
/// DPI scale for this window.
fn dpi_scale(&self) -> f64;
/// Whether the window is focused.
fn has_focus(&self) -> bool;
/// Whether the window is minimized.
fn is_minimized(&self) -> bool;
/// Whether the window is occluded.
fn is_occluded(&self) -> bool;
/// Opaque window identity.
fn handle(&self) -> WindowHandle;
/// Raw native handles for render surface creation, when backed by a native window.
fn native_handles(&self) -> Option<NativeWindowHandles> {
None
}
}
/// Render backend request contract.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RenderRequest {
/// Preferred color-space profile.
pub color_space: ColorSpace,
/// Preferred presentation mode.
pub presentation: PresentationMode,
/// Requested depth/stencil format.
pub depth: DepthStencilSupport,
}
/// Color-space profile.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColorSpace {
/// sRGB nonlinear.
Srgb,
/// Linear color-space.
Linear,
}
/// Presentation mode.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PresentationMode {
/// `VSync`.
Fifo,
/// No `VSync`.
Immediate,
/// Triple-buffer mailbox fallback.
Mailbox,
}
/// Depth/stencil support profile requested by the composition root.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DepthStencilSupport {
/// Depth bits.
pub depth_bits: u8,
/// Stencil bits.
pub stencil_bits: u8,
}
impl RenderRequest {
/// Returns a conservative default request.
#[must_use]
pub const fn conservative() -> Self {
Self {
color_space: ColorSpace::Srgb,
presentation: PresentationMode::Fifo,
depth: DepthStencilSupport {
depth_bits: 24,
stencil_bits: 8,
},
}
}
}
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "fparkan-prototype"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
encoding_rs = "0.8"
fparkan-binary = { path = "../fparkan-binary" }
fparkan-path = { path = "../fparkan-path" }
fparkan-resource = { path = "../fparkan-resource" }
fparkan-vfs = { path = "../fparkan-vfs" }
[lints]
workspace = true
[dev-dependencies]
fparkan-nres = { path = "../fparkan-nres" }
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "fparkan-render"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-world = { path = "../fparkan-world" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "fparkan-resource"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-binary = { path = "../fparkan-binary" }
fparkan-nres = { path = "../fparkan-nres" }
fparkan-path = { path = "../fparkan-path" }
fparkan-rsli = { path = "../fparkan-rsli" }
fparkan-vfs = { path = "../fparkan-vfs" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "fparkan-rsli"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "fparkan-runtime"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-assets = { path = "../fparkan-assets" }
fparkan-path = { path = "../fparkan-path" }
fparkan-platform = { path = "../fparkan-platform" }
fparkan-prototype = { path = "../fparkan-prototype" }
fparkan-render = { path = "../fparkan-render" }
fparkan-resource = { path = "../fparkan-resource" }
fparkan-vfs = { path = "../fparkan-vfs" }
fparkan-world = { path = "../fparkan-world" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "fparkan-terrain-format"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-binary = { path = "../fparkan-binary" }
fparkan-nres = { path = "../fparkan-nres" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "fparkan-terrain"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-terrain-format = { path = "../fparkan-terrain-format" }
[dev-dependencies]
fparkan-nres = { path = "../fparkan-nres" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "fparkan-test-support"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-render = { path = "../fparkan-render" }
[lints]
workspace = true
+44
View File
@@ -0,0 +1,44 @@
#![forbid(unsafe_code)]
#![cfg_attr(
test,
allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::expect_used,
clippy::float_cmp,
clippy::identity_op,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::map_unwrap_or,
clippy::needless_raw_string_hashes,
clippy::semicolon_if_nothing_returned,
clippy::type_complexity,
clippy::panic,
clippy::unwrap_used
)
)]
//! Dev-only synthetic builders and fake ports.
use fparkan_render::{FrameOutput, RenderBackend, RenderCommandList, RenderError};
/// Fake clock.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct FakeClock {
/// Current tick.
pub tick: u64,
}
/// Recording backend.
#[derive(Clone, Debug, Default)]
pub struct RecordingRenderBackend {
/// Recorded command lists.
pub captures: Vec<RenderCommandList>,
}
impl RenderBackend for RecordingRenderBackend {
fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
self.captures.push(commands.clone());
Ok(FrameOutput)
}
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "fparkan-texm"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
[dev-dependencies]
fparkan-nres = { path = "../fparkan-nres" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "fparkan-vfs"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-binary = { path = "../fparkan-binary" }
fparkan-path = { path = "../fparkan-path" }
[lints]
workspace = true
+723
View File
@@ -0,0 +1,723 @@
#![forbid(unsafe_code)]
#![cfg_attr(
test,
allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::expect_used,
clippy::float_cmp,
clippy::identity_op,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::map_unwrap_or,
clippy::needless_raw_string_hashes,
clippy::semicolon_if_nothing_returned,
clippy::type_complexity,
clippy::panic,
clippy::unwrap_used
)
)]
//! Virtual filesystem ports for resource loading.
use fparkan_binary::{sha256, Sha256Digest};
use fparkan_path::{ascii_lookup_key, join_under, NormalizedPath};
use std::collections::BTreeMap;
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
/// VFS metadata.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VfsMetadata {
/// Byte length.
pub len: u64,
/// SHA-256 content fingerprint for cache invalidation.
pub fingerprint: Sha256Digest,
}
/// VFS entry.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VfsEntry {
/// Path.
pub path: NormalizedPath,
/// Metadata.
pub metadata: VfsMetadata,
}
/// VFS error.
#[derive(Debug)]
pub enum VfsError {
/// Missing entry.
NotFound(String),
/// Ambiguous host path.
Ambiguous(String),
/// I/O error.
Io(std::io::Error),
/// Invalid path.
Path,
}
impl std::fmt::Display for VfsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound(path) => write!(f, "not found: {path}"),
Self::Ambiguous(path) => write!(f, "ambiguous host path: {path}"),
Self::Io(err) => write!(f, "{err}"),
Self::Path => write!(f, "invalid path"),
}
}
}
impl std::error::Error for VfsError {}
/// Resource VFS.
pub trait Vfs: Send + Sync {
/// Reads metadata.
///
/// # Errors
///
/// Returns [`VfsError`] when the path is invalid, missing, or cannot be
/// inspected by the backing store.
fn metadata(&self, path: &NormalizedPath) -> Result<VfsMetadata, VfsError>;
/// Reads bytes.
///
/// # Errors
///
/// Returns [`VfsError`] when the path is invalid, missing, or cannot be
/// read by the backing store.
fn read(&self, path: &NormalizedPath) -> Result<Arc<[u8]>, VfsError>;
/// Lists entries below prefix.
///
/// # Errors
///
/// Returns [`VfsError`] when the prefix is invalid, missing, or cannot be
/// traversed by the backing store.
fn list(&self, prefix: &NormalizedPath) -> Result<Vec<VfsEntry>, VfsError>;
}
/// Host directory VFS.
#[derive(Clone, Debug)]
pub struct DirectoryVfs {
root: PathBuf,
fingerprint_cache: Arc<Mutex<BTreeMap<PathBuf, CachedHostFingerprint>>>,
}
impl DirectoryVfs {
/// Creates a directory VFS.
#[must_use]
pub fn new(root: impl AsRef<Path>) -> Self {
Self {
root: root.as_ref().to_path_buf(),
fingerprint_cache: Arc::default(),
}
}
fn host_path(&self, path: &NormalizedPath) -> Result<PathBuf, VfsError> {
join_under(&self.root, path).map_err(|_| VfsError::Path)?;
resolve_casefolded(&self.root, path.as_str())
}
fn metadata_from_host_file(&self, path: &Path) -> Result<VfsMetadata, VfsError> {
let metadata = fs::symlink_metadata(path).map_err(VfsError::Io)?;
metadata_from_host_file_with_cache(path, &metadata, &self.fingerprint_cache)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct CachedHostFingerprint {
len: u64,
modified: Option<SystemTime>,
identity: Option<u64>,
fingerprint: Sha256Digest,
}
impl Vfs for DirectoryVfs {
fn metadata(&self, path: &NormalizedPath) -> Result<VfsMetadata, VfsError> {
self.metadata_from_host_file(&self.host_path(path)?)
}
fn read(&self, path: &NormalizedPath) -> Result<Arc<[u8]>, VfsError> {
let host = self.host_path(path)?;
let pre_metadata = fs::symlink_metadata(&host).map_err(VfsError::Io)?;
if pre_metadata.file_type().is_symlink() || !pre_metadata.is_file() {
return Err(VfsError::Path);
}
let pre_identity = file_identity(&pre_metadata);
let pre_len = pre_metadata.len();
let pre_modified = pre_metadata.modified().ok();
let bytes = fs::read(&host).map_err(VfsError::Io)?;
let post_metadata = fs::symlink_metadata(&host).map_err(VfsError::Io)?;
if post_metadata.file_type().is_symlink()
|| !post_metadata.is_file()
|| post_metadata.len() != pre_len
|| post_metadata.modified().ok() != pre_modified
|| file_identity(&post_metadata) != pre_identity
{
return Err(VfsError::Path);
}
Ok(Arc::from(bytes.into_boxed_slice()))
}
fn list(&self, prefix: &NormalizedPath) -> Result<Vec<VfsEntry>, VfsError> {
let base = self.host_path(prefix)?;
let mut entries = Vec::new();
if base.is_file() {
let metadata = fs::symlink_metadata(&base).map_err(VfsError::Io)?;
entries.push(VfsEntry {
path: prefix.clone(),
metadata: metadata_from_host_file_with_cache(
&base,
&metadata,
&self.fingerprint_cache,
)?,
});
return Ok(entries);
}
list_recursive(&self.root, &base, &self.fingerprint_cache, &mut entries)?;
entries.sort_by(|a, b| a.path.as_str().cmp(b.path.as_str()));
Ok(entries)
}
}
fn resolve_casefolded(root: &Path, normalized: &str) -> Result<PathBuf, VfsError> {
let mut current = root.to_path_buf();
for segment in normalized.split('/') {
let read_dir = fs::read_dir(&current).map_err(VfsError::Io)?;
let mut matches = Vec::new();
for entry in read_dir {
let entry = entry.map_err(VfsError::Io)?;
let name = entry.file_name();
let Some(name) = name.to_str() else {
continue;
};
if name.eq_ignore_ascii_case(segment) {
if entry.file_type().map_err(VfsError::Io)?.is_symlink() {
return Err(VfsError::Path);
}
matches.push(entry.path());
}
}
current = select_casefolded_match(normalized, &current, segment, matches)?;
}
Ok(current)
}
fn select_casefolded_match(
normalized: &str,
current: &Path,
segment: &str,
mut matches: Vec<PathBuf>,
) -> Result<PathBuf, VfsError> {
matches.sort();
match matches.len() {
0 => Err(VfsError::NotFound(normalized.to_string())),
1 => Ok(matches.remove(0)),
_ => Err(VfsError::Ambiguous(format!(
"{}/{}",
current.display(),
segment
))),
}
}
fn list_recursive(
root: &Path,
dir: &Path,
fingerprint_cache: &Mutex<BTreeMap<PathBuf, CachedHostFingerprint>>,
out: &mut Vec<VfsEntry>,
) -> Result<(), VfsError> {
let read_dir = fs::read_dir(dir).map_err(VfsError::Io)?;
let mut children = Vec::new();
for entry in read_dir {
let entry = entry.map_err(VfsError::Io)?;
children.push(entry.path());
}
children.sort();
for child in children {
let metadata = fs::symlink_metadata(&child).map_err(VfsError::Io)?;
if metadata.file_type().is_symlink() {
return Err(VfsError::Path);
}
if metadata.is_dir() {
list_recursive(root, &child, fingerprint_cache, out)?;
continue;
}
if !metadata.is_file() {
continue;
}
let rel = child.strip_prefix(root).map_err(|_| VfsError::Path)?;
let rel_text = rel.to_str().ok_or(VfsError::Path)?;
let path = fparkan_path::normalize_relative(
rel_text.as_bytes(),
fparkan_path::PathPolicy::HostCompatible,
)
.map_err(|_| VfsError::Path)?;
out.push(VfsEntry {
path,
metadata: metadata_from_host_file_with_cache(&child, &metadata, fingerprint_cache)?,
});
}
Ok(())
}
fn metadata_from_host_file_with_cache(
path: &Path,
metadata: &fs::Metadata,
fingerprint_cache: &Mutex<BTreeMap<PathBuf, CachedHostFingerprint>>,
) -> Result<VfsMetadata, VfsError> {
if !metadata.is_file() {
return Err(VfsError::Path);
}
let len = metadata.len();
let modified = metadata.modified().ok();
if let Some(cached) = fingerprint_cache
.lock()
.map_err(|_| VfsError::Path)?
.get(path)
.cloned()
.filter(|cached| {
cached.len == len
&& cached.modified == modified
&& cached.identity == file_identity(metadata)
})
{
return Ok(VfsMetadata {
len,
fingerprint: cached.fingerprint,
});
}
let bytes = fs::read(path).map_err(VfsError::Io)?;
let fingerprint = sha256(&bytes);
fingerprint_cache
.lock()
.map_err(|_| VfsError::Path)?
.insert(
path.to_path_buf(),
CachedHostFingerprint {
len,
modified,
identity: file_identity(metadata),
fingerprint,
},
);
Ok(VfsMetadata { len, fingerprint })
}
/// In-memory VFS.
#[derive(Clone, Debug, Default)]
pub struct MemoryVfs {
files: BTreeMap<Vec<u8>, Arc<[u8]>>,
lookup: BTreeMap<Vec<u8>, Vec<Vec<u8>>>,
}
impl MemoryVfs {
/// Inserts a file.
#[allow(clippy::needless_pass_by_value)]
pub fn insert(&mut self, path: NormalizedPath, bytes: Arc<[u8]>) {
let path = path.as_bytes().to_vec();
self.files.insert(path, bytes);
self.rebuild_lookup();
}
fn rebuild_lookup(&mut self) {
self.lookup.clear();
for path in self.files.keys() {
self.lookup
.entry(ascii_lookup_key(path).0)
.or_default()
.push(path.clone());
}
for paths in self.lookup.values_mut() {
paths.sort();
}
}
fn resolve_path(&self, path: &NormalizedPath) -> Result<&[u8], VfsError> {
let key = ascii_lookup_key(path.as_bytes()).0;
let matches = self
.lookup
.get(&key)
.ok_or_else(|| VfsError::NotFound(path.as_str().to_string()))?;
match matches.as_slice() {
[single] => Ok(single.as_slice()),
[] => Err(VfsError::NotFound(path.as_str().to_string())),
_ => Err(VfsError::Ambiguous(path.as_str().to_string())),
}
}
}
#[cfg(unix)]
#[allow(clippy::unnecessary_wraps)]
fn file_identity(metadata: &fs::Metadata) -> Option<u64> {
Some(metadata.dev().rotate_left(32) ^ metadata.ino())
}
#[cfg(windows)]
#[allow(clippy::unnecessary_wraps)]
fn file_identity(metadata: &fs::Metadata) -> Option<u64> {
Some(
(metadata.volume_serial_number() as u64).rotate_left(40)
^ ((metadata.file_index_high() as u64) << 32)
^ metadata.file_index_low() as u64,
)
}
#[cfg(not(any(unix, windows)))]
fn file_identity(_metadata: &fs::Metadata) -> Option<u64> {
None
}
impl Vfs for MemoryVfs {
fn metadata(&self, path: &NormalizedPath) -> Result<VfsMetadata, VfsError> {
let resolved = self.resolve_path(path)?;
let bytes = self
.files
.get(resolved)
.ok_or_else(|| VfsError::NotFound(path.as_str().to_string()))?;
Ok(VfsMetadata {
len: bytes.len() as u64,
fingerprint: sha256(bytes),
})
}
fn read(&self, path: &NormalizedPath) -> Result<Arc<[u8]>, VfsError> {
let resolved = self.resolve_path(path)?;
self.files
.get(resolved)
.cloned()
.ok_or_else(|| VfsError::NotFound(path.as_str().to_string()))
}
fn list(&self, prefix: &NormalizedPath) -> Result<Vec<VfsEntry>, VfsError> {
let mut out = Vec::new();
for (path, bytes) in &self.files {
if has_segment_boundary_prefix_bytes(path, prefix.as_bytes()) {
let normalized =
fparkan_path::normalize_relative(path, fparkan_path::PathPolicy::StrictLegacy)
.map_err(|_| VfsError::Path)?;
out.push(VfsEntry {
path: normalized,
metadata: VfsMetadata {
len: bytes.len() as u64,
fingerprint: sha256(bytes),
},
});
}
}
Ok(out)
}
}
fn has_segment_boundary_prefix_bytes(haystack: &[u8], needle: &[u8]) -> bool {
if haystack.len() < needle.len() {
return false;
}
if haystack.len() == needle.len() {
return haystack
.iter()
.zip(needle.iter())
.all(|(left, right)| left.eq_ignore_ascii_case(right));
}
if haystack[needle.len()] != b'/' {
return false;
}
haystack[..needle.len()]
.iter()
.zip(needle.iter())
.all(|(left, right)| left.eq_ignore_ascii_case(right))
}
/// Layered VFS with deterministic first-layer precedence.
#[derive(Clone, Default)]
pub struct OverlayVfs {
layers: Vec<Arc<dyn Vfs>>,
}
impl std::fmt::Debug for OverlayVfs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OverlayVfs")
.field("layers", &self.layers.len())
.finish()
}
}
impl OverlayVfs {
/// Creates an empty overlay.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Creates an overlay from ordered layers.
#[must_use]
pub fn from_layers(layers: Vec<Arc<dyn Vfs>>) -> Self {
Self { layers }
}
/// Appends a lower-priority layer.
pub fn push_layer(&mut self, layer: Arc<dyn Vfs>) {
self.layers.push(layer);
}
}
impl Vfs for OverlayVfs {
fn metadata(&self, path: &NormalizedPath) -> Result<VfsMetadata, VfsError> {
for layer in &self.layers {
match layer.metadata(path) {
Ok(metadata) => return Ok(metadata),
Err(VfsError::NotFound(_)) => {}
Err(err) => return Err(err),
}
}
Err(VfsError::NotFound(path.as_str().to_string()))
}
fn read(&self, path: &NormalizedPath) -> Result<Arc<[u8]>, VfsError> {
for layer in &self.layers {
match layer.read(path) {
Ok(bytes) => return Ok(bytes),
Err(VfsError::NotFound(_)) => {}
Err(err) => return Err(err),
}
}
Err(VfsError::NotFound(path.as_str().to_string()))
}
fn list(&self, prefix: &NormalizedPath) -> Result<Vec<VfsEntry>, VfsError> {
let mut by_key = BTreeMap::new();
for layer in &self.layers {
match layer.list(prefix) {
Ok(entries) => {
for entry in entries {
let key = entry.path.as_str().to_ascii_uppercase();
by_key.entry(key).or_insert(entry);
}
}
Err(VfsError::NotFound(_)) => {}
Err(err) => return Err(err),
}
}
let mut entries: Vec<_> = by_key.into_values().collect();
entries.sort_by(|a, b| a.path.as_str().cmp(b.path.as_str()));
Ok(entries)
}
}
#[cfg(test)]
mod tests {
use super::*;
use fparkan_path::{normalize_relative, PathPolicy};
#[test]
fn directory_vfs_resolves_ascii_casefolded_segments() {
let root = unique_test_dir("casefold");
let dir = root.join("data").join("MAPS").join("Tut_1");
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("Land.msh"), b"mesh").expect("write");
let vfs = DirectoryVfs::new(&root);
let path = normalize_relative(b"DATA/maps/tut_1/land.MSH", PathPolicy::StrictLegacy)
.expect("path");
assert_eq!(vfs.read(&path).expect("read").as_ref(), b"mesh");
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn directory_vfs_reports_casefold_ambiguity_even_for_exact_host_path() {
let root = unique_test_dir("casefold-ambiguous");
std::fs::create_dir_all(root.join("Data")).expect("mkdir first");
std::fs::create_dir_all(root.join("data")).expect("mkdir second");
std::fs::write(root.join("Data").join("File.bin"), b"first").expect("write first");
std::fs::write(root.join("data").join("File.bin"), b"second").expect("write second");
let collision_count = std::fs::read_dir(&root)
.expect("read root")
.flatten()
.filter(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| name.eq_ignore_ascii_case("data"))
})
.count();
if collision_count < 2 {
std::fs::remove_dir_all(root).expect("cleanup");
return;
}
let vfs = DirectoryVfs::new(&root);
let path = normalize_relative(b"Data/File.bin", PathPolicy::StrictLegacy).expect("path");
assert!(matches!(vfs.read(&path), Err(VfsError::Ambiguous(_))));
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn directory_vfs_lists_files_below_prefix() {
let root = unique_test_dir("list");
std::fs::create_dir_all(root.join("DATA").join("MAPS")).expect("mkdir");
std::fs::write(root.join("DATA").join("MAPS").join("Land.map"), b"map").expect("write");
std::fs::write(root.join("BuildDat.lst"), b"build").expect("write");
let vfs = DirectoryVfs::new(&root);
let prefix = normalize_relative(b"data", PathPolicy::StrictLegacy).expect("prefix");
let entries = vfs.list(&prefix).expect("list");
assert_eq!(entries.len(), 1);
assert!(entries[0]
.path
.as_str()
.eq_ignore_ascii_case("DATA/MAPS/Land.map"));
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn memory_vfs_list_prefix_is_boundary_safe() {
let mut vfs = MemoryVfs::default();
let exact = normalize_relative(b"DATA/Land.map", PathPolicy::StrictLegacy).expect("path");
let sibling =
normalize_relative(b"DATA2/Land.map", PathPolicy::StrictLegacy).expect("path");
vfs.insert(exact.clone(), Arc::from(b"exact".as_slice()));
vfs.insert(sibling, Arc::from(b"sibling".as_slice()));
let prefix = normalize_relative(b"DATA", PathPolicy::StrictLegacy).expect("prefix");
let entries = vfs.list(&prefix).expect("list");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].path.as_str(), exact.as_str());
}
#[test]
fn directory_vfs_fingerprint_changes_for_same_length_content() {
let root = unique_test_dir("content-fingerprint");
std::fs::create_dir_all(root.join("DATA")).expect("mkdir");
std::fs::write(root.join("DATA").join("File.bin"), b"before").expect("write before");
let vfs = DirectoryVfs::new(&root);
let path = normalize_relative(b"DATA/File.bin", PathPolicy::StrictLegacy).expect("path");
let before = vfs.metadata(&path).expect("before metadata");
std::fs::write(root.join("DATA").join("File.bin"), b"after!").expect("write after");
let after = vfs.metadata(&path).expect("after metadata");
assert_eq!(before.len, after.len);
assert_ne!(before.fingerprint, after.fingerprint);
std::fs::remove_dir_all(root).expect("cleanup");
}
#[cfg(unix)]
#[test]
fn directory_vfs_rejects_symlink_escape() {
let root = unique_test_dir("symlink-escape");
let outside = unique_test_dir("symlink-outside");
std::fs::create_dir_all(&root).expect("mkdir root");
std::fs::create_dir_all(&outside).expect("mkdir outside");
std::fs::write(outside.join("secret.bin"), b"secret").expect("write outside");
std::os::unix::fs::symlink(&outside, root.join("DATA")).expect("symlink");
let vfs = DirectoryVfs::new(&root);
let path = normalize_relative(b"DATA/secret.bin", PathPolicy::StrictLegacy).expect("path");
let prefix = normalize_relative(b"DATA", PathPolicy::StrictLegacy).expect("prefix");
assert!(matches!(vfs.read(&path), Err(VfsError::Path)));
assert!(matches!(vfs.list(&prefix), Err(VfsError::Path)));
std::fs::remove_dir_all(root).expect("cleanup root");
std::fs::remove_dir_all(outside).expect("cleanup outside");
}
#[test]
fn casefold_selector_reports_ambiguous_segments() {
let err = select_casefolded_match(
"data/file.bin",
Path::new("/game"),
"data",
vec![PathBuf::from("/game/Data"), PathBuf::from("/game/DATA")],
)
.expect_err("ambiguous path");
assert!(matches!(err, VfsError::Ambiguous(_)));
}
#[test]
fn memory_vfs_uses_ascii_casefold_lookup() {
let path = normalize_relative(b"Data/File.bin", PathPolicy::StrictLegacy).expect("path");
let mut vfs = MemoryVfs::default();
vfs.insert(path.clone(), Arc::from(b"payload".as_slice()));
assert_eq!(vfs.metadata(&path).expect("metadata").len, 7);
assert_eq!(vfs.read(&path).expect("read").as_ref(), b"payload");
let other_case =
normalize_relative(b"data/file.bin", PathPolicy::StrictLegacy).expect("path");
assert_eq!(
vfs.read(&other_case).expect("casefold read").as_ref(),
b"payload"
);
}
#[test]
fn memory_vfs_reports_casefold_ambiguity() {
let first = normalize_relative(b"Data/File.bin", PathPolicy::StrictLegacy).expect("first");
let second =
normalize_relative(b"DATA/file.BIN", PathPolicy::StrictLegacy).expect("second");
let query = normalize_relative(b"data/file.bin", PathPolicy::StrictLegacy).expect("query");
let mut vfs = MemoryVfs::default();
vfs.insert(first, Arc::from(b"first".as_slice()));
vfs.insert(second, Arc::from(b"second".as_slice()));
assert!(matches!(vfs.read(&query), Err(VfsError::Ambiguous(_))));
}
#[test]
fn memory_vfs_distinguishes_non_utf8_path_bytes() {
let mut vfs = MemoryVfs::default();
let ascii =
normalize_relative(b"DATA/normal.bin", PathPolicy::HostCompatible).expect("ascii path");
let binary =
normalize_relative(b"DATA/\xFF.bin", PathPolicy::HostCompatible).expect("binary path");
vfs.insert(ascii.clone(), Arc::from(b"ascii".as_slice()));
vfs.insert(binary.clone(), Arc::from(b"binary".as_slice()));
let binary_query =
normalize_relative(b"DATA/\xFF.bin", PathPolicy::HostCompatible).expect("binary query");
assert_eq!(
vfs.read(&binary_query).expect("read binary").as_ref(),
b"binary"
);
assert_eq!(vfs.read(&ascii).expect("read ascii").as_ref(), b"ascii");
}
#[test]
fn overlay_vfs_uses_first_matching_layer() {
let path = normalize_relative(b"DATA/File.bin", PathPolicy::StrictLegacy).expect("path");
let prefix = normalize_relative(b"DATA", PathPolicy::StrictLegacy).expect("prefix");
let mut high = MemoryVfs::default();
let mut low = MemoryVfs::default();
high.insert(path.clone(), Arc::from(b"high".as_slice()));
low.insert(path.clone(), Arc::from(b"low".as_slice()));
let overlay = OverlayVfs::from_layers(vec![Arc::new(high), Arc::new(low)]);
assert_eq!(overlay.read(&path).expect("read").as_ref(), b"high");
let entries = overlay.list(&prefix).expect("list");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].metadata.len, 4);
}
fn unique_test_dir(name: &str) -> PathBuf {
let mut path = std::env::temp_dir();
path.push(format!("fparkan-vfs-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&path);
path
}
}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "fparkan-world"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
fparkan-binary = { path = "../fparkan-binary" }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+200
View File
@@ -0,0 +1,200 @@
# Глоссарий
Глоссарий объясняет термины в том смысле, в котором они используются в этой
книге. Короткое определение не заменяет профильную главу: практический контракт
понятия раскрывается в соответствующем томе или справочной странице.
## Бинарные файлы и ABI
**PE (Portable Executable)** -- формат исполняемых файлов Windows: EXE и DLL.
Он содержит заголовки, секции, таблицы импортов и экспортов, relocations и
адрес точки входа.
**Image base** -- предпочтительный адрес начала загруженного PE-образа.
**VA** -- виртуальный адрес в процессе. **RVA** -- адрес относительно image
base.
**Import** -- внешняя функция или переменная, которую модуль получает из другой
DLL. **Export** -- символ, предоставляемый другим модулям. Имя, ordinal и
calling convention вместе образуют часть binary contract.
**ABI** -- соглашение о двоичном взаимодействии: размещение аргументов, возврат
значений, очистка stack, layout структур, порядок virtual methods и правила
владения.
**Calling convention** -- часть ABI, определяющая передачу аргументов и очистку
stack. Для исследованного 32-bit code важны `__cdecl`, `__stdcall` и
`__thiscall`.
**Vtable** -- массив указателей на virtual methods C++-объекта. Запись
`vtable +0x34` означает вызов указателя по байтовому смещению `0x34` от начала
таблицы.
**Static analysis** исследует файл без исполнения: disassembly, strings,
imports, call graph и data flow. **Dynamic analysis** наблюдает работающую
программу: breakpoints, traces, API hooks, memory state и packet/frame captures.
**Evidence** -- повторяемое наблюдение. **Inference** -- вывод, объединяющий
несколько наблюдений. **Hypothesis** -- рабочее предположение, ещё не
подтверждённое достаточным экспериментом.
## Форматы данных
**Archive** -- контейнер, объединяющий множество ресурсов. **Entry** -- запись
его каталога. **Payload** -- полезные bytes конкретной записи.
**Magic** -- короткая сигнатура формата, например `NRes` или `Texm`.
**Version** -- номер варианта layout. Проверка одной magic без проверки version
и размеров недостаточна.
**Offset** -- положение данных относительно начала файла или структуры.
**Size** -- число bytes. **Stride** -- размер одного элемента массива.
**Alignment** -- требование начинать данные на offset, кратном заданному числу.
**Little-endian** -- порядок, в котором младший byte многобайтного числа
расположен первым. Основные числовые поля форматов Iron3D используют этот
порядок.
**Fixed-size string** -- поле заранее известной длины. Полезная строка
заканчивается первым NUL, но оставшиеся bytes могут содержать служебный хвост и
должны сохраняться.
**Opaque field** -- поле с доказанными offset и size, но не установленным
предметным смыслом. Его безопасно читать и копировать, но нельзя очищать или
переосмысливать без эксперимента.
**Invariant** -- условие, которое обязано выполняться: range лежит внутри
payload, индекс указывает на существующий элемент, count соответствует размеру
секции.
**Strict reader** отклоняет любое нарушение контракта. **Compatibility reader**
дополнительно воспроизводит только известные особенности оригинала.
**Fallback** -- явно предписанный запасной путь, например material `DEFAULT`,
затем entry 0. **Heuristic** -- догадка по похожим данным; она не должна
незаметно заменять доказанный fallback.
**Roundtrip** -- последовательность decode -> encode. **Byte-identical
roundtrip** создаёт файл, полностью совпадающий с исходным. **Lossless editor**
может изменить известное поле, сохранив все остальные bytes и порядок записей.
## Ресурсы
**NRes** -- основной контейнер ресурсов с каталогом в конце файла.
**RsLi** -- библиотечный архив с каталогом в начале файла и несколькими методами
упаковки payload.
**TMA** -- mission data: paths, clans, placed objects, properties, land path и
extras.
**MSH** -- модель Iron3D, представленная как NRes с entries для geometry,
nodes, slots, batches, animation и auxiliary streams.
**WEAR** -- таблица внешнего вида модели, переводящая material index в MAT0
name и lightmap slots.
**MAT0** -- материал: phases, parameters, animation blocks и texture references.
**Texm** -- texture payload с header, palette, mip chain и optional Page atlas.
**FXID** -- ресурс эффектов: команды, references, lifetime, random/time modes и
runtime instances.
## Игровой runtime
**Engine** -- программная среда, которая загружает данные, ведёт время,
исполняет мир и формирует изображение/звук. **Game** -- правила, миссии и
content поверх engine services.
**World** -- долгоживущее состояние миссии: objects, terrain, время, кланы и
managers. **Scene** -- представление части мира для конкретной обработки,
обычно текущей камеры.
**Game object** -- сущность с идентичностью, transform, properties и lifecycle.
**Component/controller** -- специализированная часть поведения: animation,
physics, AI или rendering representation.
**Simulation** отвечает за изменение мира. **Tick** -- один расчётный шаг.
**Frame** -- одно подготовленное изображение. Число ticks и frames за единицу
времени не обязано совпадать.
**Event/message** -- типизированное сообщение между objects или subsystems.
**Queue traversal** -- стабильный обход зарегистрированных объектов.
**Deferred deletion** -- перенос фактического удаления до безопасной границы.
**Snapshot** -- согласованное состояние, которое renderer читает без изменения
simulation. **Determinism** -- одинаковый результат при одинаковом initial
state, input, времени и порядке событий.
**Authority** -- subsystem или network peer, которому разрешено окончательно
менять состояние объекта. **Mirror object** -- локальное представление объекта,
authority которого находится у другого player.
## Геометрия и рендеринг
**Vertex** -- вершина geometry. **Index** -- номер вершины. **Triangle** --
примитив из трёх индексов.
**Node** -- элемент hierarchy модели со своим local transform. **Slot** в MSH
-- выбранная геометрическая группа для комбинации node, LOD и group. **Batch**
-- непрерывный индексный диапазон с material slot и render state.
**Transform** переводит данные между coordinate spaces. **Matrix** задаёт
линейное преобразование и translation. Порядок умножения matrices является
частью контракта.
**Bounds** -- упрощённый объём для быстрых тестов. **AABB** -- min/max по осям.
**Bounding sphere** -- center и radius.
**Renderer** преобразует подготовленную сцену в изображение. **Backend** --
реализация поверх конкретного API или устройства.
**Draw call** -- команда нарисовать диапазон primitives. **Indexed draw**
использует index buffer и base vertex.
**Material phase** -- одно временное состояние анимированного материала.
**Texture** -- двумерный массив texels. **Mip chain** -- последовательность
уменьшенных уровней texture. **Atlas** -- texture с несколькими под-
изображениями.
**Fixed-function pipeline** -- старый graphics pipeline, где приложение
выбирает predefined transform, lighting, texture-stage и blend states вместо
пользовательских shaders.
**Depth test**, **culling**, **alpha test** и **blending** -- render states,
которые влияют на порядок и видимость fragments.
**Pixel parity** -- совпадение конечного изображения при фиксированных camera,
time, seed, resolution и device profile.
## Навигация, звук и сеть
**Areal** -- логическая область карты с границей, class/flags и связями с
соседями. **Areal graph** -- граф областей и переходов. **Cell grid** --
пространственный индекс для быстрых candidate queries.
**Pathfinding** -- поиск маршрута по graph. **Corridor** -- локальная полоса,
построенная из последовательности areals. **Local steering** корректирует
ближайший шаг внутри corridor.
**Collision proxy** -- упрощённое представление объекта для столкновений.
**Broad phase** быстро находит потенциальные пары. **Narrow phase** выполняет
точную проверку и вычисляет contact.
**Sample** -- декодированные звуковые данные. **Source** -- конкретный
экземпляр воспроизведения с position, gain, loop state и временем. **Listener**
-- положение и ориентация слушателя для 3D spatialization.
**Transport** -- механизм доставки bytes между peers. **Protocol** -- framing,
message types, порядок и правила подтверждения. **Wire compatibility** --
способность обмениваться данными с оригинальным клиентом.
**Serialization** -- преобразование typed state в byte sequence. **Framing** --
способ отделить одно сообщение от следующего. **Reliable delivery** гарантирует
доставку/порядок в пределах выбранной модели; **unreliable delivery** допускает
потери ради задержки.
**Player ID** транспорта и **game player number** -- разные идентичности.
**Ownership transfer** меняет authority объекта. **Replication** передаёт
состояние или события remote mirrors.
+153
View File
@@ -0,0 +1,153 @@
# Границы знания
Этот раздел перечисляет области, где контракт ещё не закрыт полностью. Они не
мешают безопасному чтению и lossless сохранению, но не должны превращаться в
authoring API без динамического подтверждения.
## Render state
Доказаны frame boundaries, world traversal, material resolve и крупные проходы.
Не доказаны символами точные имена renderer vtable slots, полный набор CShade
state transitions и окончательный порядок части transparent/FX/shadow subpasses.
Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
log на минимальных сценах с одним типом материала.
## FXID field-level semantics
Размеры команд, resource references, lifecycle, flags families и используемые
time modes известны. Не закрыто значение каждого поля body opcodes 1--10,
отсутствующий во всех проверенных каталогах opcode 6 и точные формулы редких
time modes.
Закрывающий эксперимент: изменять по одному полю копии эффекта, воспроизводить
его в контролируемой сцене и логировать runtime command object, emitted
primitives, sound events и reads в `Effect.dll`.
## Script VM
Доступны packages, symbols, event sections, variable declarations и version
checks. Полная instruction grammar `.scr`, semantics opcodes и serialization
state ещё не восстановлены.
Закрывающий эксперимент: найти dispatcher loop в `ai.dll`, сопоставить jump
table с instruction sizes, построить disassembler и сравнить выполнение
коротких scripts с оригиналом.
## Saves and campaign state
Найдены `saveslots.cfg` и `missions/dispatcher.ini`, но binary savegame payload,
serialization World3D/AI/script/RNG и migration rules не закрыты.
Нужны сохранения оригинала в контролируемых состояниях: старт миссии, изменение
позиции, здоровья, order/path, FX/timer, script variable, research/economy,
mission completion, pause и non-default game time.
## Physical/control formats
CTLD и связанные resources структурно читаются, count patterns и variants
известны. Не названы все секции, shape types, coefficients и точный contact
solver. То же относится к редким MSH auxiliary streams и части CTPT/NDPR flags.
Закрывающий эксперимент: трассировать `LoadControlSystem`,
`LoadPhysicalModel`, `CreateCollManager` и создание collision objects; связать
каждый изменяемый field с созданным shape, contact или реакцией на движение.
## DirectPlay wire
DirectPlay lifecycle и имена игровых messages известны. Wire framing, payload
schema, reliability flags и `netZipData` требуют записи обмена двух
оригинальных клиентов.
Native interoperability подтверждается только успешным обменом original client
<-> compatibility implementation в обе стороны.
## Shell, HUD, шрифты и локализация
Граница shell подтверждена exports `createShell/getIShell`, `IGUIServer`,
верхнеуровневым UI-pass и файлами `ui/*.cfg`, `DATA/TextRes.cfg`,
`gamefont.rlb` и `sprites.lib`. RsLi framing библиотек закрыт, но widget tree,
layout rules, glyph metrics, sprite command semantics, focus/navigation и HUD
state machine пока не восстановлены до field-level спецификации.
Закрывающий эксперимент: трассировать загрузку `shell_ctrls.cfg`,
`menu_resources.cfg`, `cursor.cfg`, `game_resources.cfg` и `hq.cfg`, сопоставить
GUI object factories и снять command/event captures для меню, HUD, briefing и
диалогов.
## Research, economy and properties
Экспорты `LoadResearch`, `CalcFullResearchCost`, TRF/preload resources и TMA
properties доказывают отдельный слой исследований, стоимости, добычи и
производственных параметров. Формулы стоимости, dependency graph технологий,
inventory/economy transitions и точная типизация всех 16-byte property values
не закрыты.
Закрывающий эксперимент: сопоставить research functions с ресурсами и UI,
снять изменения state на контролируемых покупках/исследованиях и построить
typed schema свойств по consumers, а не по одному имени.
## Rare branches
- `Land.map poly_count > 0`;
- RsLi adaptive methods `0x080` и `0x0A0`;
- Texm formats 556 и 88;
- FX opcode 6;
- редкие material flags и MSH auxiliary streams.
Такие ветки реализуются по бинарному коду и synthetic tests, а статус
corpus-verified получают только после реального файла или runtime trace.
## Dynamic-stage requirements
Оставшиеся вопросы нельзя закрыть только статическими архивами. Нужна
изолированная 32-bit Windows-среда, неизменённые игровые каталоги, manifest
SHA-256, debugger, API/vtable hooks, controlled clocks/input и автоматический
launcher, который восстанавливает snapshot, запускает один test case, собирает
логи и завершает процесс без ручного вмешательства.
Для каждого capture сохраняются build profile, module hashes, mission/resource
key, configuration, device profile, initial state, input/time script и версии
инструментов.
## Local evidence requests
На текущем рабочем месте закрыты статические, corpus и headless runtime gates.
Для локально воспроизводимого Desktop backend подтверждено только command/state trace
в существующем GL-воркфлоу:
- `fixtures/acceptance/macos-gl33-triangle-capture.json`;
`S3-GL-001` пока не закрыт: текущая evidence не отражает полноценный
`winit`+`fparkan-render-vulkan` path с real surface/present pipeline.
Для закрытия требования требуется постоянный workspace-владельческий backend на
`winit`/`fparkan-platform-winit` + `fparkan-render-vulkan` с реальным
surface/present pipeline, command/state parity и licensed frame capture.
Для повышения `S3-GL-002` до `covered` всё ещё нужен воспроизводимый GLES2
backend profile: GLES2 должен создать кадр, сохранить pixel capture и тот же
command/state trace. Локальный Docker probe существующего Rust image не нашёл
`libGL`, `libEGL`, `libGLES` или `libOSMesa`, поэтому закрытие этого gate требует
отдельно предоставленного Docker image с Rust + Mesa/EGL/OSMesa либо разрешения
на установку соответствующего проверочного окружения.
Для текущей macOS-focused цели `S3-GL-002`, `L3-DEVICE-001` и `L5-RG40-001`
помечены как `omitted`: они остаются требованиями portable target scope, но не
блокируют локальный macOS acceptance-аудит. При возврате RG40XX/GLES2 в область
цели эти gates снова должны требовать внешнего evidence.
`L3-DEVICE-001` и `L5-RG40-001` не закрываются локально без RG40XX H или
эквивалентного удалённого runner-а. Требуемое доказательство: запуск выбранной
миссии при 640x480 на целевом профиле, сохранённые stdout/stderr, build
fingerprint, manifest игрового каталога, frame/tick budget, memory budget и
итоговый pass/fail report. Desktop/headless результаты не считаются заменой
on-device smoke.
## Closure criteria
Вопрос считается закрытым только при наличии build fingerprint, raw trace,
parser trace-а, минимального воспроизводимого input/resource/save/message,
формального контракта или явно ограниченной гипотезы, differential test для
изменённых DLL, обновления тематической главы и regression case, запускаемого
без ручного анализа.
+15
View File
@@ -0,0 +1,15 @@
# Current Project Audit
Baseline command:
```text
cargo xtask ci
```
Result on 2026-06-23:
- canonical pipeline now uses a fixed MSRV/toolchain, policy checks,
full-format workspace test command, `clippy`/`doc`/`cargo deny` gates and
typed manifest parsing in `xtask`;
- `rpath`/offline mode is still useful for synthetic local checks;
- full online dependency resolution remains unavailable in the sandbox.
+51
View File
@@ -0,0 +1,51 @@
# FParkan
FParkan -- самостоятельная техническая книга о восстановлении игрового движка
Iron3D из *Parkan: Iron Strategy*. Она ведёт от запуска оригинальной программы
и карты DLL к форматам ресурсов, загрузке миссии, геометрии, материалам,
рендеру, поведению, звуку, сети и плану чистой совместимой реализации.
Сайт оформлен как онлайн-книга: тома читаются последовательно, а справочник
используется как быстрый доступ к форматам, проверочным правилам и границам
доказанного знания.
## Как читать
Если вы впервые разбираете игровой движок, начните с тома I и II. Там вводится
лексика, доказательная политика, модульная архитектура и жизненный цикл кадра.
Если нужна реализация совместимого движка, читайте тома III--VII линейно:
ресурсы, миссии, мир, рендер, интерактивные подсистемы и порядок работ.
Если вы проверяете выводы, переходите к тому VIII и приложениям. Там собраны
уровни уверенности, corpus gates, открытые вопросы и критерии закрытия.
## Восемь томов
1. **Путеводитель и методика** -- назначение книги, маршруты чтения, язык
предметной области и правила проверки.
2. **Запуск, архитектура и игровой цикл** -- `iron_3d.exe`, пятнадцать DLL,
сервисы, World3D, очередь объектов и границы кадра.
3. **Ресурсная система и форматы** -- NRes, RsLi, кэши, имена, `objects.rlb`,
unit DAT и сквозное разрешение ресурсов.
4. **Мир, миссии и runtime** -- TMA, ландшафт, ареалы, маршруты, создание мира
и свойства размещённых объектов.
5. **Геометрия, материалы и рендер** -- MSH, анимация, WEAR, MAT0, Texm, FXID,
свет, атмосфера и полный render frame.
6. **Поведение, управление, звук и сеть** -- AI, Behavior, Wizard, Control,
ввод, камера, звук и DirectPlay-слой.
7. **Руководство по полной реализации** -- целевая архитектура, этапы работ,
тестовый контур, точность, скорость и критерий совместимости.
8. **Справочник и доказательная база** -- ABI, конфигурация, статистика
корпусов, границы знания и глоссарий.
## Политика доказательств
Специфические утверждения об Iron3D принимаются только после локальной проверки
на исполняемых файлах, DLL, демоверсии, полных каталогах Частей 1 и 2 или на
взаимных инвариантах реальных ресурсов. Внешние описания и текущий код FParkan
могут подсказывать вопросы, но не заменяют проверку.
Неизвестные поля не получают правдоподобных имён. Пока смысл не закрыт,
документация фиксирует raw layout, границы, безопасное чтение и lossless
сохранение.
+69
View File
@@ -0,0 +1,69 @@
# WEAR и MAT0
MSH batch хранит только `material_index`. WEAR переводит этот индекс в имя
материала, а MAT0 по этому имени описывает phases, parameters и texture
references.
```text
Batch20.material_index
-> WEAR row
-> MAT0 entry
-> active phase
-> textureName
```
## WEAR
WEAR -- текстовый ресурс type ID `0x52414557`, обычно `*.wea` рядом с моделью.
```text
<wearCount>
<legacyId> <materialName>
...
[empty line]
[LIGHTMAPS
<lightmapCount>
<legacyId> <lightmapName>
...]
```
`legacyId` сохраняется, но выбор выполняется по позиции строки и имени. Между
основной таблицей и `LIGHTMAPS` нужен пустой разделитель.
## MAT0
MAT0 имеет type ID `0x3054414D`, обычно расположен в `Material.lib`. `attr1`
содержит runtime flags, `attr2` -- версию payload.
```c
#pragma pack(push, 1)
struct Mat0PrefixV4Plus {
uint16_t phase_count;
uint16_t animation_block_count;
uint8_t metadata_a;
uint8_t metadata_b;
uint32_t metadata_c_raw;
uint32_t metadata_d_raw;
};
struct Phase34 {
uint8_t parameters[18];
char texture_name[16];
};
#pragma pack(pop)
```
Versioned fields читаются только если версия их содержит. Для старых версий
используются runtime defaults, а raw values сохраняются.
## Fallback
Material resolve:
1. имя из WEAR;
2. `DEFAULT`;
3. entry с индексом 0.
Пустое texture name означает намеренно нетекстурированную поверхность. Lightmap
fallback отдельный: отсутствующий lightmap даёт slot `-1`.
+82
View File
@@ -0,0 +1,82 @@
# MSH
Файл `*.msh` является NRes-контейнером. Geometry, узлы, slots, batches,
animation и служебные streams лежат в entries с разными `type_id`.
## Entry map
```text
type 1 nodes and slot selection
type 2 header 0x8C + Slot68 records
type 3 positions float3
type 4 packed normals
type 5 packed UV0
type 6 index buffer u16
type 7 triangle descriptors
type 8 animation keys
type 9 service stream
type 10 strings and node names
type 13 Batch20 records
type 15 auxiliary stream
type 17 auxiliary data
type 18 rare stream
type 19 animation frame map
type 20 rare auxiliary table
```
Reader ищет entries по type, но сохраняет исходный порядок для roundtrip.
## Node and slot selection
Type 1 обычно состоит из records по 38 bytes:
```c
struct Node38 {
uint16_t hdr0;
uint16_t parent_or_link;
uint16_t anim_map_start;
uint16_t fallback_key;
uint16_t slot_index[15];
};
```
`slot_index[lod * 5 + group]` выбирает geometry slot. `0xFFFF` означает
отсутствие геометрии для комбинации LOD/group.
## Slot and batch
Type 2 содержит header `0x8C`, затем `Slot68`:
```c
struct Slot68 {
uint16_t tri_start;
uint16_t tri_count;
uint16_t batch_start;
uint16_t batch_count;
float aabb_min[3];
float aabb_max[3];
float sphere_center[3];
float sphere_radius;
uint32_t opaque[5];
};
```
Type 13 задаёт draw ranges:
```c
#pragma pack(push, 1)
struct Batch20 {
uint16_t batch_flags;
uint16_t material_index;
uint16_t opaque4;
uint16_t opaque6;
uint16_t index_count;
uint32_t index_start;
uint16_t opaque14;
uint32_t base_vertex;
};
#pragma pack(pop)
```
Index check выполняется как `base_vertex + index < vertex_count` для всего
используемого slice.
+61
View File
@@ -0,0 +1,61 @@
# NRes
`NRes` -- основной контейнер ресурсов Iron3D. Он используется как внешний
архив и как внутренний контейнер модели `*.msh`.
```text
[Header: 16 bytes]
[Data region: payload with alignment]
[Directory: entry_count * 64 bytes]
```
## Header
```c
struct NResHeader16 {
char magic[4]; // "NRes"
uint32_t version; // 0x00000100
int32_t entry_count; // >= 0
uint32_t total_size; // equals file size
};
```
`directory_offset = total_size - entry_count * 64`. Reader проверяет отсутствие
переполнений, `directory_offset >= 16` и точное окончание каталога на
`total_size`.
## Entry
```c
#pragma pack(push, 1)
struct NResEntry64 {
uint32_t type_id;
uint32_t attr1;
uint32_t attr2;
uint32_t size;
uint32_t attr3;
char name[36];
uint32_t data_offset;
uint32_t sort_index;
};
#pragma pack(pop)
```
Имя содержит bounded C-string до 35 полезных bytes. `sort_index` задаёт
отображение из sorted position в original entry index. В строгом режиме все
`sort_index` образуют перестановку `0..N-1`.
## Data region
Payload каждой записи лежит после header и до начала каталога. Игровые архивы
выравнивают следующий payload до 8 bytes нулями, но reader не должен требовать
плотного покрытия data region.
Различаются:
- active payload -- диапазон, на который указывает entry;
- gap/padding -- bytes между активными диапазонами;
- unindexed preserved region -- произвольные bytes, не принадлежащие entry.
Lossless editor сохраняет все три категории. Compact writer может исключить
unindexed regions только при явной операции repack.
+56
View File
@@ -0,0 +1,56 @@
# Render frame
Кадр является последней стадией цикла, а не самостоятельной функцией renderer-а.
До draw calls уже накоплен input, рассчитан tick, применены отложенные операции,
выбрана камера и обновлён 3D sound listener.
## Frame skeleton
```text
system messages and input
-> simulation calculation
-> deferred object operations
-> animation and transforms
-> camera and sound listener
-> visibility and render queues
-> materials and draw passes
-> renderer completion
-> end-of-render callbacks and UI
```
В `World3D::stdRenderGame` доказан крупный порядок: camera передаётся Terrain,
настраиваются viewport/matrices, вызываются renderer boundary slots,
устанавливается `in_render`, выполняется traversal мира, закрывается world/shade
pass, вызывается renderer completion, снимается `in_render`, рассылается
end-of-render.
## Draw item
Подготовленный draw item содержит:
- node world matrix;
- batch flags and index range;
- WEAR material handle;
- MAT0 active phase and coefficients;
- texture handle;
- optional lightmap handle;
- render phase and sorting key;
- legacy pipeline state.
Подготовленный item должен ссылаться на immutable данные кадра. Изменение phase
или texture cache посреди прохода не должно менять уже собранную очередь.
## Parity risks
- x87 precision and rounding;
- scalar/SIMD `g_FastProc` differences;
- object, batch and transparent primitive order;
- depth, cull, alpha test and blend transitions;
- mip-skip, palette and Page coordinates;
- material fallback and phase selection;
- RNG sequence for FX and atmosphere;
- device capability fallback;
- simulation time quantization.
Для отладки нужен deterministic frame capture: camera state, visible object IDs,
draw-item list, pipeline keys, matrices и hashes промежуточных buffers.
+69
View File
@@ -0,0 +1,69 @@
# RsLi
`RsLi` -- библиотечный архив Iron3D с каталогом в начале файла и payloads после
него.
```text
[Header: 32 bytes]
[Entry table: entry_count * 32 bytes]
[Payloads]
[optional trailer]
```
## Header fields
```text
+0x00 char[2] "NL"
+0x02 u8 reserved
+0x03 u8 version = 1
+0x04 i16 entry_count
+0x0E u16 presorted_flag = 0xABBA
+0x14 u32 xor_seed
```
Остальные bytes сохраняются без нормализации.
## Entry
```c
struct RsLiEntry32 {
char name[12];
uint8_t service[4];
int16_t flags;
int16_t sort_to_original;
uint32_t unpacked_size;
uint32_t data_offset_raw;
uint32_t packed_size;
};
```
Имя обычно хранится в uppercase ASCII. `sort_to_original` связывает sorted
position с исходной записью.
## Table transform
Entry table проходит обратимое потоковое XOR-преобразование. Начальное
состояние берётся из младших 16 bits `xor_seed` и продолжается через всю
таблицу, не сбрасываясь на границе записи.
## Storage methods
```text
0x000 raw block
0x020 byte transform only
0x040 LZSS
0x060 transform + LZSS
0x080 adaptive Huffman + LZSS
0x0A0 transform + adaptive Huffman + LZSS
0x100 raw Deflate
```
После любого пути должно получиться ровно `unpacked_size` bytes. Методы
`0x080` и `0x0A0` подтверждены decoder-кодом, но не живыми payload демоверсии
или обеих частей.
## Compatibility quirk
`sprites.lib::INTERF8.TEX` объявляет Deflate range на один byte дальше EOF.
Совместимый reader допускает `packed_size - 1` только для этого именованного
случая. Строгий режим сообщает `deflate_eof_plus_one`.
+67
View File
@@ -0,0 +1,67 @@
# Texm
`Texm` -- основной формат изображений Iron3D. Payload содержит header,
необязательную палитру, mip chain и иногда `Page` chunk.
```c
struct TexmHeader32 {
uint32_t magic; // 'Texm'
uint32_t width;
uint32_t height;
uint32_t mip_count;
uint32_t flags4;
uint32_t flags5;
uint32_t unknown6;
uint32_t format;
};
```
## Pixel formats
```text
0 Indexed8 + palette 256 * 4 bytes
565 R5 G6 B5
556 R5 G5 B6
4444 A4 R4 G4 B4
88 L8 A8
888 RGB8 in four-byte element
8888 A8 R8 G8 B8
```
Короткие каналы расширяются до 8 bits повторением значимых bits. Для 888
служебный четвёртый byte сохраняется при roundtrip.
## Layout
```text
TexmHeader32
[palette 1024 bytes, only for format 0]
level 0 pixels
level 1 pixels
...
level mip_count-1 pixels
[optional Page chunk]
```
Размер mip level вычисляется через `max(1, width >> i)` и
`max(1, height >> i)`. Parser суммирует размеры с проверкой переполнения до
чтения данных.
## Page chunk
```c
struct PageHeader8 {
uint32_t magic; // 'Page'
uint32_t rect_count;
};
struct PageRect8 {
int16_t x;
int16_t width;
int16_t y;
int16_t height;
};
```
Chunk обязан иметь размер `8 + rect_count * 8`. Rectangles находятся в pixel
space базового mip и масштабируются после mip-skip.
+64
View File
@@ -0,0 +1,64 @@
# TMA
`data.tma` -- основное описание расстановки и логической конфигурации миссии.
Файл перечисляет paths, clans, objects, свойства, ссылку на ландшафт и extras.
## String primitive
```c
struct LpString {
uint32_t byte_length;
uint8_t bytes[byte_length];
};
```
Reader продвигается ровно на `4 + byte_length`. Завершающий NUL не является
обязательной частью framing. Для человекочитаемого вида используется legacy
ANSI/CP1251 view, но исходные bytes сохраняются.
## Top level
```text
u32 format_version
u32 path_count
PathRecord paths[path_count]
u32 clan_section_version
u32 clan_count
ClanRecord clans[clan_count]
u32 object_section_version
u32 object_count
PlacedObject objects[object_count]
LpString land_path
u32 mission_flag
LpString description_raw
u32 extra_section_version
u32 extra_count
ExtraRecord28 extras[extra_count]
```
Все 60 TMA Частей 1 и 2 проходят parser до точного EOF. Версии стабильны:
верхний уровень `1`, clan section `6`, object section `10`, property schema
`1`, trailing section `1`.
## PlacedObject
```text
u32 raw_kind
u32 class_or_flags
LpString resource_name
u32 raw_after_resource
u32 identity_or_clan_raw
f32 position[3]
f32 orientation[3]
f32 scale[3]
LpString instance_name
u32 raw_after_name
i32 link0
i32 link1
u32 property_schema_version
u32 property_count
Property properties[property_count]
```
`Property` состоит из четырёх raw `u32` и имени. Typed views разрешены только
для доказанных property names и consumers.
+371
View File
@@ -0,0 +1,371 @@
# I. Путеводитель и методика
Первый том задаёт язык и правила всей документации. Он объясняет, как читать
технические главы, какие термины используются для игрового runtime, как
разделяются уровни уверенности и какие требования предъявляются к реализации,
которая должна работать с оригинальными данными без потери информации.
Документация рассчитана на разработчика, который уже умеет читать C/C++,
байтовые форматы, PE-модули и графические pipeline, но не обязательно знаком с
Iron3D. Поэтому этот том не описывает один конкретный crate, package или
физическое деление будущего кода. Он фиксирует контракты: что должно быть
прочитано, сохранено, рассчитано и показано.
## Назначение книги
Книга ведёт от общей архитектуры Iron3D к точным форматам данных и алгоритмам
исполнения. Практическая цель -- реализация, способная открыть оригинальный
каталог *Parkan: Iron Strategy*, загрузить миссию, создать мир, провести
игровой шаг и сформировать кадр.
Форматы в главах описываются как байтовые контракты. Если указано поле
`+0x10`, это означает расположение в потоке или структуре данных, а не
разрешение читать файл прямым `reinterpret_cast`. Для постоянных layouts
используются offsets, проверки размеров, bounded cursor и явное сохранение
неизвестных байтов. Для versioned и variable-length записей приоритет имеет
последовательный parser с контролем границ.
Игровое поведение описывается не только размером структур. Совместимая
реализация должна учитывать порядок событий, время, fallback-правила,
идентификаторы объектов, численные ограничения, состояние материалов,
границы кадра и правила завершения операций.
## Маршруты чтения
**Читатель, новый для игровой разработки**, начинает с базовых понятий этого
тома, затем переходит к архитектуре, игровому циклу и вводу в рендер. После
этого имеет смысл читать главы о миссиях, мире и ресурсных форматах.
**Разработчик совместимого движка** читает тома II-VII линейно. Технические
главы имеют одинаковую логику: назначение подсистемы, данные на диске,
представление в памяти, алгоритм работы, проверки и требования к новой
реализации.
**Аналитик оригинальной программы** использует этот том вместе с разделами о
доказательной базе, ABI, результатах корпусных проверок и границах знания.
Факты, согласованные выводы и открытые вопросы должны оставаться разделёнными:
это позволяет расширять реализацию без подмены проверенных контрактов
удобными догадками.
## Состав документации
1. **Путеводитель и методика** -- язык предметной области, правила чтения и
процедура проверки.
2. [**Запуск, архитектура и игровой цикл**](02-architecture.md) -- от
`iron_3d.exe` до расчёта и вывода кадра.
3. [**Ресурсная система и форматы**](03-resources.md) -- архивы, кэши, реестры
и служебные данные.
4. [**Мир, миссии и игровой runtime**](04-world.md) -- TMA, ландшафт, ареалы и
создание объектов.
5. [**Геометрия, материалы и рендер**](05-render.md) -- от вершины модели до
изображения на экране.
6. [**Поведение, управление, звук и сеть**](06-behavior.md) -- интерактивные
подсистемы.
7. [**Руководство по полной реализации**](07-implementation.md) -- предлагаемая
архитектура и порядок работ.
8. [**Справочник и доказательная база**](08-evidence.md) -- ABI,
конфигурация, статистика и открытые вопросы.
Дополнительные краткие определения собраны в
[глоссарии](../appendices/glossary.md). Технические области, где контракт ещё
не закрыт полностью, перечислены в
[границах знания](../appendices/knowledge-boundaries.md).
## Условные обозначения
`+0x10` означает смещение поля относительно начала структуры или записи.
`RVA 0x13B60` -- адрес относительно базы PE-модуля. `u16`, `u32`, `i16` и
`float32` обозначают типы фиксированной ширины. `LE` означает little-endian.
`payload` -- полезные данные записи после метаданных контейнера. `EOF` -- точное
завершение файла или ограниченного блока.
Если в тексте указан hash, RVA или ordinal, значение относится к явно
обозначенному binary profile. Адреса разных сборок не объединяются по имени
функции. При публикации функции нужны минимум модуль, SHA-256 сборки и RVA.
Размеры структур выражаются в байтах. Счётчики и offsets считаются частью
формата, даже когда их можно восстановить из длины файла. Padding, reserved
поля, неизвестные хвосты и gaps не нормализуются без доказанного правила.
## Совместимость
Слово "совместимость" в этой книге имеет несколько уровней.
**Reader** умеет открыть файл, проверить границы, извлечь известные поля и
сохранить неизвестные bytes так, чтобы данные можно было записать обратно.
**Viewer** умеет показать ресурс: модель, texture, material, эффект или карту.
Viewer может быть полезен для анализа, но он не доказывает поведение runtime.
**Runtime** умеет создать мир, зарегистрировать объекты, исполнять события,
обновлять время, применять контроллеры, выбирать видимое состояние и передавать
его рендеру.
**Полноценный движок** дополнительно воспроизводит порядок операций, численные
правила, fallback-поведение, resource lifetime, reference ownership, pause,
manual input, сетевые идентификаторы, boundaries кадра и состояние
интерактивных подсистем.
Поэтому файл может быть "прочитан правильно", но всё ещё не быть реализованным
на уровне движка. Например, reader MSH может восстановить вершины и индексы,
viewer может нарисовать mesh, а runtime обязан ещё сохранить material slots,
animation state, bounds, LOD, visibility, collision и связи с объектом мира.
## Движок как программа длительного действия
Обычная прикладная программа получает запрос, вычисляет результат и заканчивает
работу. Игра живёт в цикле: прочитать ввод, обновить состояние мира,
сформировать звук и изображение, показать кадр и повторить. Движок -- набор
подсистем и соглашений, которые делают этот цикл устойчивым.
**Simulation** отвечает на вопрос "что произошло в мире": куда переместился
объект, кого он видит, сколько у него здоровья, сработал ли эффект, изменился
ли маршрут или приказ. **Rendering** отвечает на другой вопрос: "как текущее
состояние показать". В корректной архитектуре рендер не решает игровые правила,
а читает подготовленное состояние.
**Tick** -- один шаг расчёта. **Frame** -- одно изображение. Они могут
выполняться с разной частотой: игра способна рассчитать несколько шагов между
двумя показами или временно не рисовать, не останавливая логику. Поэтому время,
накопление input, порядок callbacks и момент удаления объектов считаются частью
контракта.
## Мир, сцена и объект
**Мир** -- долгоживущее состояние миссии: ландшафт, объекты, время, погода,
принадлежность к кланам и глобальные сервисы. **Сцена** -- представление той
части мира, которую можно обработать для текущей камеры. **Игровой объект** --
сущность с идентификатором, положением, набором свойств и поведением.
В Iron3D объектами управляет World3D. Объекты регистрируются в общей очереди,
получают события, участвуют в расчёте и могут быть удалены отложенно, чтобы не
разрушить обход коллекции посреди шага. Это важнее, чем конкретный контейнер в
новой реализации: совместимость определяется моментом наблюдаемого добавления,
обновления и удаления.
Мир не равен renderer scene graph. Один объект может иметь runtime state,
controller, сетевой mirror, визуальную модель, collision bounds и script state.
Часть этих данных нужна для gameplay, часть -- для вывода, часть -- для
сохранения и воспроизведения.
## Ресурс, модель и материал
**Ресурс** -- именованный блок данных, который можно найти и загрузить. Архивы
`NRes` и `RsLi` содержат таблицы таких блоков. Имя, индекс, размер, offset,
compression method и fallback-правило являются частью контракта загрузки.
**Модель** описывает форму объекта. Она состоит из вершин, индексов, узлов,
групп треугольников, слотов материалов и auxiliary streams. **Vertex** хранит
положение и обычно дополнительные атрибуты: нормаль для освещения и
UV-координату для выборки texture. **Triangle** -- три вершины, образующие
примитив. **Index buffer** хранит номера вершин и позволяет переиспользовать их
между треугольниками. **Batch** -- непрерывный диапазон индексов, который
рисуется одним материалом и одним набором состояний.
**Материал** описывает способ отображения поверхности: texture references,
цвет, прозрачность, режимы смешивания и анимацию параметров. **Texture** --
изображение в памяти графической системы. **Mip-уровни** -- уменьшенные копии
изображения для дальних объектов. **Lightmap** -- дополнительная texture с
заранее рассчитанным освещением.
Runtime должен связывать эти уровни по цепочке: миссия выбирает объект, объект
ссылается на prototype, prototype приводит к модели, модель -- к WEAR,
материалам, textures и lightmaps. Ошибка на любом участке этой цепочки может
не проявиться в parser-е, но проявится в игровом кадре.
## Пространственные понятия
**Transform** переводит точку из локальных координат модели в координаты мира,
камеры и экрана. **Иерархия узлов** позволяет одному элементу наследовать
движение другого. **LOD** выбирает менее подробную геометрию вдали. **Culling**
отбрасывает то, что не видно. **Bounds** -- упрощённая оболочка объекта,
обычно сфера или AABB, используемая для быстрых тестов.
**Collision** отвечает на геометрические пересечения. **Navigation** ищет
допустимый маршрут. В Iron3D эти задачи разделены: Control обслуживает
физическую модель и столкновения, а ArealMap хранит пространственные области и
связи между ними.
Важно не смешивать визуальные и игровые упрощения. Render bounds могут быть
достаточны для отсечения, но не обязаны совпадать с collision shape. Навигация
может использовать areal graph, который не является ни mesh-ем модели, ни
геометрией ландшафта в renderer-е.
## Графический конвейер
Процессор выбирает видимые объекты, готовит матрицы, материалы и списки
примитивов. Графический backend передаёт вершины, индексы, textures и state
драйверу. Видеокарта преобразует вершины в координаты экрана, разбивает
треугольники на фрагменты, проверяет глубину, смешивает цвет и записывает
результат в буфер кадра. После завершения буфер становится видимым
пользователю.
Для совместимости важны не только данные draw call. Контракт включает frame
boundaries, viewport, camera state, порядок world traversal, material resolve,
shadow/transparent/FX subpasses, завершение renderer-а, восстановление state и
callbacks после рендера. Если часть имён vtable slots ещё не доказана, новая
реализация должна фиксировать крупный порядок операций и оставлять
детализацию проверяемой.
## Практический словарь реализации
**Handle** -- компактная ссылка на управляемый объект. **Cache** -- сохранённый
результат загрузки или декодирования. **Reference count** -- число владельцев
ресурса. **Fallback** -- предписанный запасной вариант при отсутствии данных.
**Invariant** -- условие, которое всегда должно быть истинным для корректного
файла или runtime-состояния. **Determinism** -- повторяемость результата при
одинаковых входных данных и порядке событий.
**Strict mode** -- режим parser-а, который принимает только корректный файл:
верные magic, версии, размеры, ranges, индексы и точный EOF. **Lossless mode**
-- режим чтения/записи, который сохраняет неизвестные поля, padding, gaps и raw
payload без нормализации. **Quirk** -- именованное отклонение, разрешённое
только после проверки на реальных данных или исполняемом коде.
Эти слова используются как технические термины. Если глава называет значение
fallback-ом, invariant-ом или quirk-ом, это должно иметь проверяемое
последствие в reader-е, writer-е или runtime.
## Как читать C/C++-схемы структур
Структуры в главах описывают байтовый layout, а не переносимый C++ object
model. Если поля на диске идут без padding, reader должен читать их по offsets
либо использовать явно проверенный packed layout. Прямое отображение native
struct допустимо только при доказанном размере, выравнивании и endian-правиле.
`sizeof` обязательно проверяется `static_assert` или эквивалентным compile-time
test. Это особенно важно для records, где 32-битное поле начинается после
нечётного числа 16-битных или 8-битных полей: стандартное выравнивание
современного compiler-а может вставить скрытые bytes и изменить offsets.
Для variable-length форматов предпочтителен bounded cursor:
1. Прочитать header и проверить минимальный размер.
2. Проверить, что offsets и sizes лежат внутри текущего блока.
3. Прочитать таблицы до объявленного count, не до "пока получается".
4. Проверить ссылки между таблицами.
5. Дойти до точного EOF или сохранить явно разрешённый trailing payload.
Writer пересчитывает только производные значения: размеры, offsets, число
записей, сортировочные таблицы и padding, если правило доказано. Unknown fields
и reserved ranges сохраняются побайтно.
## Иерархия доказательств
Документация использует четыре уровня уверенности.
**Прямое наблюдение** -- поле, значение или последовательность видны в
инструкции программы, таблице PE, экспорте, строке, обработчике файла или в
самом ресурсе. Это самый сильный уровень.
**Корпусное подтверждение** -- правило проверено на всех подходящих файлах
одного или нескольких явно названных наборов: демоверсии, Части 1 и Части 2.
Например, базовый корпус содержит 435 моделей MSH, 518 textures Texm и 923
эффекта FXID, прошедших структурные проверки без ошибок; полные части расширяют
эту матрицу вариантов.
**Согласованный вывод** -- назначение восстановлено по нескольким независимым
признакам: вызывающим функциям, vtable slots, строкам ошибок, диапазонам
значений и связям между форматами. Такой вывод пригоден для реализации, но его
численные детали следует проверять тестами.
**Открытый вопрос** -- данные можно читать и сохранять, однако предметный смысл
поля или редкой ветки не доказан. Такие bytes нельзя обнулять,
переупорядочивать или превращать в authoring API.
Уровень уверенности должен быть виден из формулировки. "Поле равно" означает
проверенный layout или значение. "Вероятно отвечает за" означает согласованный
вывод. "Неизвестно" означает сохранять без изменения и не строить вокруг этого
публичный контракт.
## Проверенные материалы
Локальный набор проверки включает демоверсию, полные каталоги Частей 1 и 2,
исполняемые файлы, 15 DLL каждой сборки и игровые ресурсы. DLL из
первоначального архива и DLL демоверсии совпали по SHA-256: `15/15`, поэтому
выводы по этому коду и demo-ресурсам образуют один доказательный профиль.
Исполняемый файл демоверсии `iron_3d.exe` имеет размер 36 864 байта, PE32/x86,
entry RVA `0x141E`, image base `0x400000` и SHA-256
`b0a8b0db1c3a8698c4d4604d89c655496bd91ac1f8859a455e8a45838aebfbd6`.
Исполняемые файлы Частей 1 и 2 также имеют размер 36 864 байта и побайтно
совпадают между собой, но относятся к другому binary profile: entry RVA
`0x147E`, SHA-256
`f476af85c034a4b4f34f49d0806e4dff397b5da0ee26d382a7674231144979f7`.
Полные каталоги Частей 1 и 2 суммарно включают 60 TMA, 1 101 unit DAT, 254
NRes-файла и 14 975 NRes entries. Все контейнеры и TMA прошли bounded parser до
точного EOF; полный достижимый граф обеих частей разрешился без ошибок.
## Процедура проверки
Проверка строится как воспроизводимая цепочка:
1. Снять PE-метаданные, хэши, импорты, экспорты, ordinals, RTTI и строки.
2. Построить граф вызовов между модулями и отметить фабрики подсистем.
3. Разобрать функции запуска, загрузчики файлов, главный цикл и критические
vtable-вызовы.
4. Проверить форматы независимыми reader-скриптами с контролем границ и точного
завершения файла.
5. Построить цепочку миссия -> объект -> прототип -> модель -> материал ->
texture.
6. Сравнить счётчики, диапазоны, ссылки и размеры на всём доступном корпусе.
Ключевой результат сквозной проверки демо-миссий: все 201 объектов шести
миссий разрешились в 501 запрос прототипов, затем в 501 модель, 501 таблицу
WEAR, 3 879 слотов материалов и 5 085 ссылок на textures или lightmaps. Ошибок
в фактически исполняемом пути нет.
## Что не считается доказательством
Удобное имя поля не доказывает его назначение. Совпадение layout с текущей
реализацией не доказывает поведение оригинального runtime. Успешный viewer не
доказывает writer. Успешный reader одного файла не доказывает формат всего
корпуса. Совпадение ABI не доказывает побайтную идентичность всех сборок.
Если локальные данные и предположение расходятся, приоритет имеют исполняемый
код, реальные ресурсы и взаимные invariants между форматами. Неизвестное поле
лучше оставить без имени, чем дать ему ложное предметное значение.
## Требования к воспроизводимости
Каждая новая реализация должна иметь strict parser mode, lossless roundtrip
mode и набор corpus tests. Неизвестные поля сохраняются побайтно. Любое
присвоенное полю имя должно сопровождаться наблюдаемым поведением или тестом.
Численные правила -- округление, порядок умножения, RNG и время -- считаются
частью формата исполнения, даже если файл читается правильно.
Минимальный отчёт проверки должен фиксировать:
1. build profile и hashes модулей;
2. путь или ключ ресурса;
3. размер входного файла и hash входных bytes;
4. версию parser-а или commit реализации;
5. список включённых quirks;
6. число прочитанных записей и точку EOF;
7. ошибки, предупреждения и unknown ranges;
8. результат roundtrip, если writer участвует в проверке.
Для runtime-проверок дополнительно нужны mission key, configuration, device
profile, начальное состояние, input/time script и trace значимых callbacks.
## Разделение профилей
Binary profile описывает исполняемый код: PE-метаданные, exports/imports,
ordinals, hashes, RVA и layout функций. Corpus profile описывает набор файлов:
каталог, миссии, ресурсы, размеры, counts, variants и статистику parser-а.
Эти профили нельзя смешивать без явной пометки. Один и тот же формат может
иметь общий смысл в разных сборках, но отличаться редкими ветками, адресами
функций или набором встреченных вариантов. Один и тот же address может иметь
смысл только внутри конкретного module hash.
При расширении документации новое утверждение должно отвечать на три вопроса:
1. Где это видно напрямую?
2. На каком корпусе это проверено?
3. Что должна сделать реализация, если правило нарушено?
Если на один из вопросов нет ответа, утверждение остаётся согласованным выводом
или открытым вопросом, а не закрытым контрактом.
+472
View File
@@ -0,0 +1,472 @@
# II. Запуск, архитектура и игровой цикл
Этот том описывает путь от запуска `iron_3d.exe` до устойчивого кадра:
загрузку `iron3d.dll`, создание shell/game objects, поднятие платформенных
сервисов, запуск World3D, расчёт simulation step, безопасное удаление объектов,
рендер и завершение программы.
Главная особенность Iron3D -- это не один монолитный engine object, а связка
небольшого Win32 bootstrap и набора DLL, которые обмениваются фабриками,
singleton-интерфейсами и C++ vtable. Совместимая реализация может изменить
физическое деление на библиотеки, но не может произвольно менять порядок
инициализации, object identity, правила владения, fallback ресурсов и порядок
событий.
```text
iron_3d.exe
-> iron3d.dll
-> services.dll
-> World3D.dll
-> Terrain.dll
-> Ngi32.dll
-> AniMesh.dll / ArealMap.dll / Effect.dll
-> ai.dll / Behavior.dll / Wizard.dll
-> Control.dll / MisLoad.dll / Net.dll / Joystick.dll
```
## Карта модулей
Во внешней архитектуре обнаружено пятнадцать DLL. Экспортов сравнительно мало:
они обычно создают объект, возвращают singleton или дают доступ к уже поднятой
подсистеме. Основная работа выполняется через C++-интерфейсы, поэтому порядок
виртуальных слотов является частью ABI, особенно для compatibility shim эпохи
MSVC6.
```text
iron_3d.exe
|
v
iron3d.dll -- композиция игры, shell и главный цикл
|
+-- services.dll -- доступ к display, GUI, ресурсам, звуку, таймеру и сети
+-- World3D.dll -- объекты, очередь, время, камера и кадр
+-- Terrain.dll -- ландшафт, свет, атмосфера и визуальный слой мира
+-- ai.dll / Behavior.dll / Wizard.dll
+-- Control.dll / Effect.dll / MisLoad.dll
+-- Net.dll / Joystick.dll
+-- Ngi32.dll -- ресурсы, графика, звук, математика и CPU dispatch
```
Циклы импортов между DLL ожидаемы. Terrain создаёт визуальные объекты и
обращается к World3D, а World3D получает world-interface из Terrain. Это не
значит, что обе библиотеки совместно владеют всем состоянием. Реальные границы
задаются интерфейсами, refcount, очередью объектов и порядком shutdown.
Практичная новая структура может быть внутренним набором модулей `platform`,
`resources`, `world`, `mission`, `terrain`, `render`, `animation`, `effects`,
`behavior`, `physics`, `audio` и `network`. Важно сохранить не DLL-границы, а
контракты: имена ресурсов, порядок поиска, fallback-ветки, object ID, момент
создания mirror objects, численное поведение и последовательность событий.
## Роли модулей
`iron3d.dll` создаёт shell и game objects, читает `iron_3d.ini`, поднимает
display, sound, CD-audio, network и настройки World3D, загружает миссионные и
UI-конфигурации, содержит message pump и вызывает расчёт/рендер игры.
`services.dll` работает как service locator. Через него запрашиваются display,
GUI, network manager, resource manager, sound server и timer. Этот слой отделяет
высокоуровневую игру от деталей создания устройств.
`World3D.dll` -- центральный runtime: очередь объектов, идентификаторы,
события, отложенное удаление, game time, pause, manual input, камера,
material/texture/lightmap managers, сетевые mirrors, расчёт и 3D-проход.
`Terrain.dll` отвечает не только за землю. В его область входят ландшафт,
здания, визуальный слой мира, камера, shade/state layer, primitive buffers,
сортировочные слои, источники света, тени, microtextures, атмосфера, дождь,
молнии, солнце и flares.
`Ngi32.dll` содержит низкоуровневые сервисы: DirectDraw/Direct3D-era renderer,
DirectSound, readers `NRes`/`RsLi`, память, часы, математику, пересечения,
определение CPU и таблицу быстрых процедур `g_FastProc`.
Предметные DLL закрывают отдельные области. `AniMesh.dll` загружает модели и
агентов. `ArealMap.dll` строит spatial graph и маршруты. `Behavior.dll`
реализует поведение юнитов. `ai.dll` содержит стратегический AI и миссионные
сценарии. `Wizard.dll` корректирует локальное движение. `Control.dll`
обслуживает физическую модель и столкновения. `Effect.dll` создаёт runtime-FX.
`MisLoad.dll` читает миссионные данные. `Net.dll` инкапсулирует DirectPlay.
`Joystick.dll` работает через DirectInput.
## Поток данных
Миссия не создаёт готовый кадр напрямую. Данные проходят через несколько
уровней: описание объекта, прототипы, ресурсы, runtime-object, контроллеры,
simulation state, render items и только затем платформенный renderer.
```text
mission data
-> object identity and properties
-> prototype registry
-> model/material/texture/effect resources
-> World3D object + domain controllers
-> simulation state
-> visible render items
-> Ngi32 render interface
-> DirectX-era device
```
Этот поток объясняет, почему нельзя объединять физический архив, metadata entry,
декодированный payload и готовый runtime-кэш. У каждого уровня свой срок жизни,
собственный refcount и собственные ошибки. Детали ресурсного конвейера описаны
в [Томе III](03-resources.md), а сборка мира из миссии -- в [Томе IV](04-world.md).
## Bootstrap
`iron_3d.exe` -- небольшой PE32/x86 bootstrap размером 36 864 байта. Основная
игровая логика находится в `iron3d.dll`. Исполняемый файл создаёт Win32-процесс,
подготавливает окружение, загружает библиотеку и получает восемь публичных
точек входа:
```text
createShell deleteShell
createGame deleteGame
createSubsystems deleteSubsystems
getIGame getIShell
```
Эти функции образуют внешнюю границу игры. `createShell` создаёт оболочку
интерфейса и меню, `createGame` -- объект игровой логики, `createSubsystems` --
аппаратные и runtime-сервисы. Getter-функции возвращают уже созданные объекты.
Запуск удобно читать как конечный автомат:
```text
PROCESS_CREATED
-> LIBRARY_READY
-> ENTRYPOINTS_READY
-> SHELL_CREATED
-> GAME_CREATED
-> SUBSYSTEMS_READY
-> MAIN_LOOP
-> SUBSYSTEMS_CLOSED
-> GAME_DELETED
-> SHELL_DELETED
```
Каждый переход имеет обратное действие. Если display, sound или другой
обязательный сервис не создан, главный цикл не начинается, но уже созданные
объекты освобождаются в обратном порядке. Новая оболочка запуска должна
работать из каталога оригинальной установки, сохранять смысл относительных
путей, создавать окно до графической подсистемы и закрывать частично поднятые
сервисы без предположения, что init дошёл до конца.
Bootstrap обеих полных частей побайтно одинаков, хотя файл второй части может
иметь другое имя:
```text
size 36 864
entry RVA 0x147E
SHA-256 f476af85c034a4b4f34f49d0806e4dff397b5da0ee26d382a7674231144979f7
```
Следовательно, различия полных частей начинаются после передачи управления DLL
и игровым данным. Адреса executable демоверсии относятся к другой binary
profile и не должны переноситься на полные версии без проверки hash.
## Инициализация подсистем
Iron3D разделяет создание высокоуровневых объектов и создание подсистем.
`createShell` конструирует оболочку пользовательского интерфейса, `createGame`
создаёт объект игры, а `createSubsystems` связывает их с display, sound,
network и World3D.
Высокоуровневая последовательность выглядит так:
```text
прочитать iron_3d.ini
-> получить display service
-> создать окно и графическое устройство
-> проверить доступность 3D-драйвера
-> выбрать CURRENT_D3DCARD
-> получить sound service и настроить громкость
-> создать network instance и передать application GUID
-> создать World3D game settings
```
Ошибка отсутствующего 3D-устройства обрабатывается отдельно от ошибок ресурсов:
это разные стадии запуска. Конфигурация влияет не только на разрешение. В
runtime попадают графическая карта, громкость эффектов, CD-audio, режим
CD-sound, сетевое приложение и World3D settings. Application GUID сетевой
подсистемы:
```text
{3C1D1F01-A870-11D1-8400-000021B14415}
```
Один и тот же GUID передаётся сетевому объекту и service layer. Если он
разойдётся, экземпляры игры станут логически разными приложениями, даже при
исправном транспорте.
## `stdInitGame`
После платформенных сервисов World3D создаёт внутренний runtime:
1. Создаёт глобальную очередь объектов.
2. Сохраняет window handle и режим игры.
3. При нужном режиме ограничивает курсор областью окна.
4. Получает или создаёт 3D sound object.
5. Загружает реестр адресов компонентов из `Comp.ini`.
6. Получает или создаёт 3D renderer.
7. Читает профиль возможностей renderer.
8. Загружает component type 6.
9. Для multiplayer создаёт NetWatcher.
10. Получает world-interface из Terrain.
11. Устанавливает исходные параметры света и тумана.
Порядок важен. World objects не должны появляться до queue, ресурсы рендера --
до renderer, сетевые mirror objects -- до NetWatcher. В новой реализации у
каждого этапа должен быть явный признак успешного создания, чтобы shutdown мог
безопасно разобрать неполный init.
## Завершение
Shutdown идёт в обратном направлении: прекращаются игровые расчёты и сетевые
наблюдатели, разбираются отложенные операции, освобождаются world objects и
менеджеры, затем renderer и sound, затем game settings и platform services.
Ограничение курсора снимается, глобальные ссылки очищаются.
Полезный протокол завершения:
1. Запретить новые события и новые объекты.
2. Дождаться выхода из calculation/render traversal.
3. Разобрать очередь deferred operations.
4. Отсоединить объекты от очереди, контроллеров и менеджеров.
5. Освободить managers и singletons после их consumers.
6. Закрыть устройства и платформенные сервисы.
Такой порядок защищает от dangling-ссылок между World3D, Terrain, renderer,
sound и сетевым слоем.
## Главный цикл
Главный цикл -- не одна функция `update_and_render`, а расписание, связывающее
Win32 messages, input, игровые события, таймеры, сеть и renderer. Системная
очередь сообщает об активации окна, вводе, изменении состояния процесса и
выходе. Очередь World3D рассчитывает игровые объекты. У этих очередей разные
правила времени и владения, поэтому их нельзя смешивать в один контейнер.
Подтверждённые точки вызова в одном из профилей:
```text
stdCalculateGame RVA 0x5FA94, 0x604C1, 0x6086B
ClearManualEventsList RVA 0x6052F
stdRenderGame RVA 0x60B2F
UpdateManualEventsList в обработчике сообщений около RVA 0xA3759
```
Смысловой skeleton:
```c
while (running) {
stdCalculateGame();
clear_keyboard_snapshot();
update_shell_and_mode();
ClearManualEventsList();
process_window_messages();
update_timers_ui_gameplay_network();
if (mode_requires_extra_step) stdCalculateGame();
if (render_enabled) {
stdSetCurrentCamera(camera);
stdRenderGame(camera);
} else {
sleep_briefly();
}
update_post_render_state();
}
```
Ввод из window messages накапливается между расчётными шагами. Если читать
клавиатуру только внутри рендера, события будут теряться при пропущенных кадрах
или отключённом выводе.
## `stdCalculateGame`
Calculation pass сначала очищает или подготавливает список manual events,
увеличивает внутренний depth/counter и опрашивает input device. Если устройство
временно потеряно, выполняется повторное получение доступа и чтение повторяется.
Затем при незамороженной игре выставляется признак `in_calculation` и вызывается
основной traversal очереди объектов.
```text
prepare input/events
-> enter calculation
-> dispatch queue events
-> objects update behavior and transforms
-> leave calculation
-> apply deferred operations
-> occasional cache maintenance
```
После traversal разбирается deferred-delete list. Объект может запросить
собственное удаление во время события, но память освобождается только после
завершения обхода. Периодически также очищаются давно неиспользуемые ресурсы и
объекты по порогам часов порядка 20 и 60 секунд.
Совместимый runtime должен иметь явный traversal depth или флаг
`in_calculation`. Нельзя полагаться на то, что контейнер выдержит удаление
текущего элемента из обработчика события.
## Жизненный цикл кадра
Рендер читает состояние, подготовленное расчётом. Кадр начинается до renderer-а:
message pump уже накопил ввод, World3D уже обновил объекты, отложенные операции
и анимации, после чего выбирается камера и обновляется listener звука.
```text
system messages and input
-> simulation calculation
-> deferred object operations
-> animation and transforms
-> camera and sound listener
-> visibility and render queues
-> materials and draw passes
-> renderer completion
-> end-of-render callbacks and UI
```
В `World3D::stdRenderGame` виден крупный каркас: установка camera/viewport,
renderer frame boundaries, traversal мира, завершение world/shade path,
renderer completion, снятие `in_render`, восстановление viewport и рассылка
end-of-render callbacks. Эти callbacks позволяют объектам безопасно обновить
временные ресурсы после того, как draw-команды больше их не используют.
Один calculation step не обязан соответствовать одному изображению. Главный
цикл допускает дополнительный вызов `stdCalculateGame` и режим, в котором
расчёт продолжается без вывода кадра. Поэтому нужно хранить отдельно:
1. монотонные платформенные часы;
2. игровое время с pause и масштабированием;
3. длительность текущего calculation step;
4. локальное время анимации и FX;
5. реальные часы обслуживания кэшей.
Игровую логику нельзя выводить из render delta: изменение частоты кадров тогда
изменит движение, камеру и сценарные таймеры. Подробности render item и рисков
кадровой совместимости вынесены в справочник [Render frame](../reference/render-frame.md).
## World3D
World3D связывает игровые объекты, события, время, ввод, камеру, сетевые
отражения и визуальное представление. Он не содержит всю предметную логику:
движение делегируется Behavior/Wizard, физика -- Control, мир -- Terrain. Его
задача -- общая идентичность, порядок вызовов и безопасный жизненный цикл.
`CreateQueue` создаёт singleton-объект размером 20 байт, а `GetQueue`
возвращает его. Очередь служит центральным маршрутизатором событий и операций
над объектами.
Публичный слой предоставляет отдельные функции для локальных и сетевых
объектов:
```text
CreateObject
AddObjectToGame
AddNewObjectToGame
CreateMirrorObject
AddMirrorObjectToGame
AddNewMirrorToGame
```
Разделение "создать" и "добавить в игру" означает два этапа: сначала выделить и
настроить instance, затем зарегистрировать его в общей системе. Это позволяет
loader-у заполнить свойства до появления объекта в расчётной очереди.
## Идентичность объектов
Object ID кодирует не только порядковый номер. Проверки диапазонов показывают
разбиение на номер игрока, класс и индекс. Mirror object представляет объект,
владельцем которого является другой участник. Локальный runtime хранит его
видимое состояние, но источник авторитетных изменений находится удалённо.
```c
struct ObjectId {
uint32_t raw;
uint16_t owner_player;
uint16_t class_and_index;
};
```
Точное битовое разбиение нужно брать из сетевых функций. На уровне API уже
сейчас полезно разделить логические свойства: `is_local`, `is_mirror`, `owner`,
`class` и `index`.
Минимальный runtime-object должен хранить identifier, type, owner, transform,
active state, ordered property bag, ссылки на controllers, участие в расчёте и
рендере, сетевой статус и флаг отложенного удаления. Специализированные DLL
могут быть представлены компонентами, но порядок их вызовов задаёт World3D.
## Отложенное удаление
`DeleteGameObject` проверяет, идёт ли calculation pass. Если обход активен и
удаление не принудительное, объект помещается в deferred list. `KillGameObject`
отправляет запрос через очередь, а не освобождает память напрямую.
```c
void request_delete(Object* o) {
if (world.in_calculation) {
world.deferred_delete.push_back(o);
o->pending_delete = true;
} else {
world.detach_and_release(o);
}
}
```
Это защищает итераторы, связи и текущий стек вызовов. Любая новая подсистема,
способная удалить объект из обработчика события, обязана пользоваться тем же
механизмом.
Регистрация в очереди и владение памятью -- разные понятия. Удаление из мира не
всегда означает немедленное освобождение instance: часть объектов и managers
использует intrusive reference count, а renderer, sound и resource managers
могут возвращать уже существующий singleton с увеличенным счётчиком. Поэтому
global manager закрывается после всех объектов, которые на него ссылаются.
## Детерминизм
Даже при одинаковых формулах результат зависит от порядка. Стабильный runtime
сохраняет последовательность queue traversal, момент формирования input
snapshot, порядок сетевых сообщений, обработку deferred operations и порядок
обращений к RNG. Оптимизация и многопоточность допустимы только при
детерминированном объединении результатов.
Для переносимой реализации полезно разделить scheduler phases и immutable
render snapshot. Это архитектурная рекомендация для новой реализации, а не
утверждение о точном layout исходных C++ classes.
## Стабильность между сборками
Внешняя архитектура полных Частей 1 и 2 сохраняет те же пятнадцать DLL, 313
exports, имена, ordinals и import sets. Побайтно идентичны:
```text
ai.dll, Behavior.dll, Joystick.dll, MisLoad.dll, Net.dll,
Ngi32.dll, Terrain.dll, Wizard.dll, World3D.dll
```
Пересобраны:
```text
AniMesh.dll, ArealMap.dll, Control.dll, Effect.dll,
iron3d.dll, services.dll
```
Это разделяет переносимость выводов. World3D lifecycle, Terrain, NRes/RsLi
readers, mission loader, AI/Behavior/Wizard, DirectPlay wrapper и joystick
adapter подтверждаются одной машинной реализацией. Model/agent runtime,
collision, effects, shell/composition и service layer требуют отдельного
сравнения поведения Частей 1 и 2.
Для `World3D.dll` Частей 1 и 2 применим общий hash:
```text
World3D.dll SHA-256
17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e
```
RVA внутри `iron3d.dll` нельзя считать общими без проверки конкретного файла:
эта DLL пересобрана между частями, а демоверсия имеет отдельный binary profile.
Смысловая последовательность цикла переносится как контракт scheduler-а, но
адреса остаются build-specific.
+561
View File
@@ -0,0 +1,561 @@
# III. Ресурсная система и форматы
Ресурсная система Iron3D переводит имена из миссий и прототипов в объекты,
которыми пользуются подсистемы мира, рендера, анимации, звука, эффектов и
управления. В этом пути участвуют несколько разных сущностей: файл на диске,
открытый архив, запись каталога, подготовленный payload и готовый runtime-объект.
Их нельзя смешивать, потому что у каждого уровня свой срок жизни, свои правила
кэширования и свой набор проверок.
Основной контейнер ресурсов -- [NRes](../reference/nres.md). Он используется как
внешний архив (`objects.rlb`, `Material.lib`, `Textures.lib`) и как внутренний
контейнер модели `*.msh`. Второй библиотечный формат -- [RsLi](../reference/rsli.md):
его каталог находится в начале файла, а payload может храниться raw, через
потоковое преобразование, LZSS, адаптивный Huffman + LZSS или raw Deflate.
Визуальная часть прототипа дальше проходит через [MSH](../reference/msh.md),
[WEAR/MAT0](../reference/materials.md) и [Texm](../reference/texm.md), но этот
том описывает именно ресурсный слой: как найти, проверить, раскрыть и сохранить
данные до передачи их предметным подсистемам.
```text
TMA или unit DAT
-> логический ключ
-> objects.rlb
-> archive.rlb :: model.msh
-> model.wea
-> Material.lib :: MAT0
-> Textures.lib / LightMap.lib :: Texm
```
На демо-корпусе эта цепочка проверена целиком для всех реально размещённых
объектов. При этом полная таблица прототипов может содержать ссылки на контент,
которого нет в урезанной поставке. Диагностика должна различать недостижимую
ссылку в общем реестре и ресурс, реально требуемый выбранной миссией.
## Ресурсный конвейер
Загрузка ресурса состоит из последовательных стадий:
1. Разрешить относительный путь с учётом глобального resource path и текущего
каталога игры.
2. Открыть архив или вернуть уже открытый archive object из кэша.
3. Найти запись каталога по имени, не меняя исходный порядок каталога.
4. Проверить bounds, размер payload и способ хранения.
5. Подготовить bytes: распаковать, применить потоковое преобразование или
вернуть raw-диапазон.
6. Разобрать предметный формат и создать объект подсистемы.
7. Сохранить готовый объект в отдельном кэше, если формат допускает повторное
использование.
Эти стадии дают четыре независимых уровня кэша:
1. Открытые архивы.
2. Каталоги имён, offsets и размеров.
3. Подготовленные блоки данных.
4. Кэши моделей, материалов, текстур, lightmaps, эффектов и служебных объектов.
Повторное открытие того же нормализованного пути возвращает существующий
archive object и увеличивает счётчик владельцев. Готовая texture или model при
этом может жить дольше file handle и иметь собственную политику удаления. Кэш
предметного объекта не должен напрямую закрывать архив: он зависит от данных,
но не владеет файлом как ресурсом операционной системы.
## Имена и пути
Большинство игровых имён сравнивается без учёта регистра в ASCII-диапазоне. Это
не Unicode case folding. Для совместимости достаточно нормализовать `A..Z` в
`a..z`, а для RsLi-поиска -- переводить запрос в uppercase ASCII и укладывать его
в фиксированный ключ.
Фиксированные строки читаются bounded parser-ом: строковая часть заканчивается
на первом NUL, но оставшийся хвост поля сохраняется. Нельзя очищать хвосты,
пересобирать регистр, заменять смешанные разделители или заранее переводить все
пути в абсолютные имена. Старые данные используют исторические имена библиотек,
разный регистр исходных путей и фиксированные поля, где после терминатора могут
оставаться значимые для roundtrip bytes.
## Строгий и совместимый режимы
Строгий reader нужен тестам, редактору и проверке корпуса. Он валидирует
структуру до выдачи любого `EntryView`: magic, версию, счётчики, арифметические
переполнения, bounds, sort permutation, alignment и точное завершение payload.
Если формат требует NUL-терминатор, строгий режим проверяет его именно в пределах
фиксированного поля.
Совместимый reader повторяет только известные особенности оригинала:
- линейный поиск при повреждённой сортировочной таблице;
- RsLi-исключение `deflate_eof_plus_one` для `sprites.lib::INTERF8.TEX`;
- material fallbacks, подтверждённые ресурсной цепочкой;
- отсутствие геометрии у системных и солнечных объектов, где mesh pass не
требуется.
Режим совместимости не должен скрывать произвольные ошибки. Каждое послабление
оформляется как именованное правило и покрывается отдельным тестом. Если quirk
применим только к Deflate-записи, он не распространяется на LZSS, Huffman или
raw-диапазоны.
## NRes
`NRes` хранит произвольные именованные payload и их атрибуты. Каталог расположен
в конце файла, поэтому начало каталога вычисляется из полного размера файла и
числа записей.
```text
[Header: 16 байт]
[Data region: payload с выравниванием]
[Directory: entry_count x 64 байта]
```
Все числа little-endian.
```c
struct NResHeader16 {
char magic[4]; // "NRes"
uint32_t version; // 0x00000100
int32_t entry_count; // >= 0
uint32_t total_size; // равен фактическому размеру файла
};
```
Производные значения:
```text
directory_size = entry_count * 64
directory_offset = total_size - directory_size
```
Reader проверяет, что `directory_offset >= 16`, умножение не переполнено, а
каталог заканчивается точно на `total_size`.
### Запись каталога NRes
```c
#pragma pack(push, 1)
struct NResEntry64 {
uint32_t type_id; // +0x00
uint32_t attr1; // +0x04
uint32_t attr2; // +0x08
uint32_t size; // +0x0C
uint32_t attr3; // +0x10
char name[36]; // +0x14
uint32_t data_offset; // +0x38
uint32_t sort_index; // +0x3C
};
#pragma pack(pop)
```
Имя содержит не более 35 полезных байт и завершающий ноль. Writer запрещает
внутренний NUL и слишком длинное имя, но сохраняет неизвестные атрибуты
`attr1`, `attr2`, `attr3` без нормализации. Их смысл зависит от конкретного
типа ресурса и не может быть выведен из контейнера.
Поле `sort_index` задаёт отображение из позиции в отсортированном списке в
исходный индекс записи. Каталог остаётся в исходном порядке. Поиск идёт по
отсортированному отображению, но возвращает исходную запись. При сохранении
writer строит массив исходных индексов, сортирует его по ASCII-case-insensitive
именам и записывает результат в `sort_index`. Если отображение нельзя использовать
или оно не является перестановкой в строгом режиме, совместимый путь переходит к
последовательному сравнению имён.
### Размещение данных NRes
Каждый active payload должен лежать после 16-байтового заголовка и полностью до
начала каталога. Канонические игровые файлы выравнивают начало следующего
payload до границы 8 байт нулевым заполнением.
Порядок canonical save:
1. Записать временный заголовок.
2. Записать payload всех записей в текущем порядке.
3. После каждого блока добавить нули до кратности 8.
4. Построить таблицу поиска имён.
5. Дописать каталог.
6. Записать окончательный `total_size`.
Строгий reader выполняет проверки до выдачи записи:
- `magic == "NRes"` и `version == 0x100`;
- `entry_count >= 0`, а `entry_count * 64` вычисляется без переполнения;
- `total_size` равен фактической длине файла;
- `directory_offset = total_size - entry_count * 64` не меньше 16;
- для каждой записи `data_offset >= 16` и `data_offset + size <= directory_offset`;
- поле имени содержит NUL в пределах 36 байт;
- каждый `sort_index < entry_count`;
- в строгом режиме все `sort_index` образуют перестановку `0..N-1`.
Нулевое заполнение до границы 8 байт -- подтверждённое поведение игровых
архивов и canonical writer-а. Reader не должен считать ненулевой gap частью
соседнего payload, но lossless-редактор сохраняет исходные bytes, если файл
открыт не в режиме канонической пересборки.
### Неплотная data region
Проверка 120 NRes-файлов / 6 804 entries Части 1 и 134 файлов / 8 171 entries
Части 2 не выявила нарушений magic, version, total size, bounds, sort
permutation, ASCII-order, 8-byte alignment или перекрытий активных payload.
Однако `Textures.lib` Части 2 содержит большой ненулевой диапазон в data region,
который не адресуется ни одной записью каталога. Первый активный payload
начинается значительно позже начала файла, а каталог и все активные entries
остаются корректными.
Следовательно, parser не должен требовать плотного покрытия data region. Нужно
различать три вида диапазонов:
- `active payload` -- bytes, на которые указывает запись каталога;
- `gap/padding` -- bytes между активными диапазонами;
- `unindexed preserved region` -- произвольные bytes, не принадлежащие ни одной
записи.
Canonical compact writer может исключить unindexed region только при явной
операции repack. Lossless editor сохраняет её побайтно вместе с исходным
порядком entries и gaps.
## RsLi
`RsLi` -- библиотечный архив с каталогом в начале файла. Записи могут храниться
в исходном виде или проходить один из поддержанных путей подготовки.
```text
[Header: 32 байта]
[Entry table: entry_count x 32 байта]
[Payloads]
[необязательный trailer]
```
Заголовок начинается с двух байт `NL`. Версия равна `1`, число записей хранится
как знаковое 16-битное значение. Поле по смещению `0x0E` может содержать
`0xABBA`: это означает, что отображение сортировки уже подготовлено.
Подтверждённые поля header:
```text
+0x00 char[2] "NL"
+0x02 u8 reserved, в корпусе 0
+0x03 u8 version, в корпусе 1
+0x04 i16 entry_count
+0x0E u16 presorted_flag, значение 0xABBA
+0x14 u32 xor_seed
```
Остальные bytes заголовка сохраняются без нормализации.
### Запись каталога RsLi
После подготовки таблицы каждая запись имеет layout 32 байта:
```c
struct RsLiEntry32 {
char name[12];
uint8_t service[4];
int16_t flags;
int16_t sort_to_original;
uint32_t unpacked_size;
uint32_t data_offset_raw;
uint32_t packed_size;
};
```
Имя обычно хранится в uppercase ASCII. Четыре служебных байта после имени
сохраняются без изменения. `sort_to_original` играет ту же роль, что и
`sort_index` в NRes: связывает отсортированную позицию с исходной записью.
Таблица на диске проходит обратимое побайтовое преобразование. Начальное
состояние берётся из младших 16 бит `xor_seed`. Если обозначить два байта
состояния как `lo` и `hi`, для каждого входного байта выполняется:
```text
lo = hi XOR ((lo << 1) mod 256)
out = in XOR lo
hi = lo XOR (hi >> 1)
```
Операция симметрична: один и тот же цикл используется для подготовки и
восстановления. Состояние непрерывно проходит по всей таблице; его нельзя
перезапускать на каждой записи.
### Способы хранения RsLi
Способ определяется выражением `flags & 0x1E0`:
```text
0x000 исходный блок
0x020 только потоковое байтовое преобразование
0x040 LZSS
0x060 преобразование, затем LZSS
0x080 адаптивный Huffman, затем LZSS
0x0A0 преобразование, адаптивный Huffman и LZSS
0x100 raw Deflate без оболочки zlib
```
Reader обязан различать все значения, а неизвестную маску отклонять как
неподдерживаемую. После любого пути должно быть получено ровно `unpacked_size`
байт. Методы `0x080` и `0x0A0` подтверждены decoder-кодом и синтетическими
тестами, но живых payload этих веток в проверенных RsLi-файлах не найдено.
Параметры LZSS:
- размер кольцевого окна -- `4096`;
- начальное заполнение -- байт `0x20`;
- начальная позиция -- `0xFEE`;
- управляющие признаки читаются от младшего бита к старшему;
- двухбайтовая ссылка кодирует 12-битную позицию и длину `n + 3`;
- восстановленные bytes сразу записываются обратно в кольцевое окно.
В конце файла может находиться шестибайтовый media overlay trailer: два символа
`AO` и 32-битное значение `overlay`. В таком режиме фактическая позиция блока
равна `data_offset_raw + overlay`. Reader сначала проверяет, что overlay не
выходит за размер отображённого файла, затем проверяет весь диапазон записи.
### Поиск, кэш и проверки RsLi
Запрос имени переводится в uppercase ASCII и укладывается в фиксированный ключ.
При признаке `0xABBA` используется сохранённое отображение сортировки. Если
признака нет, loader строит его после чтения каталога. Некорректный индекс
приводит к последовательному поиску.
Файл открывается через memory mapping. Runtime-запись хранит указатель на
упакованный диапазон, размеры и необязательный указатель на подготовленные
данные. Первый обычный `load` создаёт буфер и сохраняет результат; повторный
возвращает его из кэша. Быстрый путь может вернуть указатель непосредственно в
mapped file только для исходного блока.
Reader проверяет:
- сигнатуру `NL`, служебный байт и версию;
- неотрицательное число записей;
- размещение всей таблицы в файле;
- что сохранённое отображение сортировки является перестановкой;
- что эффективный диапазон каждого блока не выходит за конец файла;
- что способ хранения известен;
- что после подготовки получено ровно `unpacked_size` байт.
В demo-каталоге и полных каталогах обеих частей наблюдаются два RsLi-файла:
```text
gamefont.rlb 2 entries, все 0x040 LZSS
sprites.lib 24 entries, все 0x100 raw Deflate
```
Последняя запись `sprites.lib::INTERF8.TEX` объявляет packed range, который
заканчивается на один байт после физического EOF. Совместимый путь читает на
один байт меньше; строгий путь регистрирует именованный quirk
`deflate_eof_plus_one`. Это исключение не распространяется на другие записи,
методы или произвольные выходы за конец файла.
Writer, который редактирует существующий архив, сохраняет все служебные bytes
заголовка и записей. Выбор оптимального способа упаковки для новых файлов
является отдельной политикой и не должен менять уже существующие entries без
явного запроса.
## Реестр объектов
Имя объекта в миссии является логическим ключом. Связь этого ключа с файлами
модели, материалов и служебных данных хранится в `objects.rlb`, который сам
использует формат NRes. Имя записи каталога -- ключ прототипа. Payload записи
состоит из записей по 64 байта:
```c
struct ObjectRef64 {
char archive_name[32];
char resource_name[32];
};
```
Payload каждой записи `objects.rlb` обязан быть кратен 64 байтам. Это
проверяется до чтения первой ссылки. Оба поля читаются как строки до первого
NUL, но полный 32-байтовый блок сохраняется при редактировании без очистки
хвоста.
Разрешение прототипа:
1. Найти entry реестра по логическому ключу без учёта ASCII-регистра.
2. Прочитать все `ObjectRef64` в исходном порядке.
3. Если ссылка указывает обратно в `objects.rlb`, рекурсивно раскрыть указанный
родительский prototype.
4. Объединить effective references родителя с локальными references дочерней
записи, сохранив порядок и происхождение.
5. Выбрать первую существующую ссылку с расширением `.msh`, открыть указанный
архив и найти модель по имени.
6. Загружать `.bas` как отдельный служебный ресурс сооружения, а не как замену
MSH.
7. Если effective prototype не содержит MSH, считать объект негеометрическим,
если это допускает его назначение.
Resolver обязан детектировать циклы наследования, ограничивать глубину и
кэшировать результат раскрытия. В обеих частях fortification-прототипы используют
явного родителя из `objects.rlb`: родитель предоставляет MSH/WEAR/CPT/NDP/CTL,
а дочерняя запись добавляет собственный BASE. Негеометрический объект не является
ошибкой сам по себе: системные и солнечные сущности могут участвовать в логике
или эффектах без mesh pass.
Контракт реализации:
- сохранять порядок ссылок внутри прототипа;
- не выводить имя модели из имени entry, если имеется явная ссылка;
- проверять существование указанного архива и ресурса независимо;
- отделять статус «негеометрический объект» от статуса «повреждённая ссылка»;
- кэшировать результат разрешения ключа, но инвалидировать его при замене архива;
- в diagnostic mode строить полный граф зависимостей и отмечать узлы, достижимые
из выбранной миссии.
В demo-варианте `objects.rlb` содержит 590 прототипов. У 554 есть прямая ссылка
на MSH; 549 таких ссылок разрешаются в доступных demo-архивах. Ещё 34 прототипа
раскрываются через родительскую запись `objects.rlb` и дополняются локальным
BASE. Семь записей не дают геометрию, а 41 ссылка всего реестра указывает на
контент, которого нет в урезанной поставке. Для 501 запросов прототипов,
порождаемых шестью demo-миссиями, найдены прототип, MSH и WEAR.
## Unit DAT
Запись миссии может ссылаться не на один ключ, а на unit-файл `*.dat`. Такой файл
перечисляет компоненты сложного игрового объекта.
```text
TMA object
-> путь к unit DAT
-> список component keys
-> несколько entries objects.rlb
-> модели, WEAR, control points, effects и другие ресурсы
```
Это объясняет, почему один размещённый unit может состоять из корпуса, башен,
оружия, эффектов и служебных частей. В демоверсии найдено 425 unit-файлов и
5 219 записей; все разобраны без ошибок. Наблюдаемый тип записи равен `1`, а
архив назначения -- `objects.rlb`. В 5 205 из 5 219 фиксированных полей имени
обнаружены ненулевые bytes после строкового терминатора; reader использует
строковую часть, а lossless writer сохраняет весь исходный блок.
Размер каждого unit DAT удовлетворяет формуле:
```text
file_size = 8 + record_count * 112
```
Первые два байта header равны `F1 F0`. Оставшиеся шесть bytes имеют несколько
наблюдаемых вариантов; их семантика пока не названа и они сохраняются как
`header_opaque[6]`.
```c
#pragma pack(push, 1)
struct UnitDatRecord112 {
char archive_name[32]; // +0x00
char resource_name[32]; // +0x20
uint32_t kind; // +0x40, в корпусе всегда 1
int32_t parent_or_link; // +0x44
char description[32]; // +0x48
uint32_t tail0; // +0x68, opaque
uint32_t tail1; // +0x6C, opaque
};
#pragma pack(pop)
```
Во всех проверенных records `archive_name == "objects.rlb"` и `kind == 1`.
Поле `parent_or_link` встречается как `-1`, `0`, `1` и другие небольшие индексы
и связывает компоненты составного unit; точная предметная классификация ссылки
ещё не закрыта. `description` -- человекочитаемое описание компонента. В Части 2
есть поля `description[32]`, полностью заполненные без NUL; это валидная bounded
string длиной 32 байта. Требование обязательного terminator применяется только
к полям, где оно доказано форматом. `tail0` и `tail1` нельзя нормализовать.
Проверено 425 файлов / 5 219 records Части 1 и 676 файлов / 8 145 records
Части 2. Все соответствуют формуле размера, `kind == 1` и
`archive_name == "objects.rlb"`.
## Вспомогательные форматы
MSH, материал и текстура отвечают за видимую форму. Полноценный прототип
дополнительно хранит точки крепления, зависимости, управляющие параметры,
области взаимодействия и ссылки на эффекты. Эти данные распределены между
несколькими небольшими форматами.
Для них действует строгая граница знания: framing, counts и валидность корпуса
могут быть подтверждены parser-ом, тогда как предметный смысл части полей
остаётся неизвестным. Reader предоставляет typed view для доказанных полей и
raw bytes для остальных. Инструмент должен показывать статус поля:
`layout-confirmed`, `consumer-inferred` или `opaque`.
### CTPT
В demo-корпусе найдено 284 CTPT-ресурса и 3 599 точек; все прочитаны без ошибок.
Имена показывают назначение слоя: `TurretCenter`, `TurretDirect`,
`CameraCenter`, `TargetDirect`, `Root`, `Sfx_1`, `Sign_Entrance1`, `Width`,
`Height`, `Dir`.
CTPT хранит локальные marker-точки модели. После применения transform такая точка
становится позицией или направлением в мире. Оружие может использовать её для
дула или оси башни, камера -- для привязки обзора, эффект -- для точки появления.
Конкретное назначение определяется именем и consumer-ом, а не одним общим флагом.
Первое 32-битное поле чаще равно `0`; встречаются `0x80000000` и редкий
вариант. До установления точной семантики оно хранится как `flags_raw`.
### NDPR
Проверено 494 NDPR-ресурса и 1 915 записей. Они ссылаются на `animals.rlb`,
`system.rlb`, `static.rlb`, `turrets.rlb`, `weapon.rlb` или используют пустое
имя архива. В 89 записях присутствует связанный эффект. Пустое имя архива
разрешается относительно текущего контекста. Reader хранит ссылку и остальные
параметры раздельно; writer сохраняет исходный порядок.
### EXPL и reference arrays
Проверено 144 ресурса EXPL: 26 используют версию 1, 54 -- версию 2, 64 --
версию 3. Reader выбирает layout по version field и требует точного завершения
payload. Полная field-level семантика всех версий пока не доказана, поэтому
version-specific opaque sections сохраняются.
Отдельная проверенная группа из 585 ресурсов содержит 2 956 однотипных
ссылочных records. Их границы и counts закрыты, однако единое предметное имя
всего семейства не подтверждено всеми consumers. В API безопаснее использовать
нейтральное `ReferenceArray` и конкретизировать назначение на уровне типа entry.
### SUND и CTLD
Два ресурса SUND содержат суммарно 12 ключей. Их следует загружать как параметры
системного объекта, а не как геометрию.
Для CTLD проверено 531 payload. Размеры и сочетания счётчиков сильно различаются,
поэтому parser должен быть версионно- и счётчик-ориентированным, а неизвестные
секции -- храниться в исходном виде.
### TRF, ANI и SKE
В демоверсии обнаружены 5 файлов TRF, 38 preload-записей, 8 ANI-ресурсов и
6 SKE-ресурсов. Все проходят структурный разбор. Эти семейства участвуют в
подготовке компонентов и анимационных или управляющих данных до создания
runtime-объекта.
Поскольку живой корпус невелик, редактор не должен синтезировать новые варианты
этих форматов по догадке. Безопасный режим -- читать доказанные счётчики и
ссылки, предоставлять raw-view неизвестных секций и обеспечивать побайтовое
сохранение неизменённых данных.
### BASE
Проверено 30 BASE-ресурсов; каждый содержит ровно один polygon record и проходит
структурную проверку. BASE payload и ссылка `.bas` в `objects.rlb` выполняют
связанные, но разные роли:
- наличие ссылки `.bas` позволяет registry resolver-у искать одноимённый
`<stem>.msh` в том же архиве;
- сам BASE payload загружается отдельной подсистемой сооружений и не заменяет
MSH geometry.
Resolver не должен интерпретировать bytes BASE как mesh. Writer сохраняет
polygon record и неизвестные поля 1:1, пока полный gameplay-контракт BASE не
подтверждён.
## Правило сохранения
Lossless editor сохраняет неизвестные поля, хвосты фиксированных строк,
служебные bytes, gaps, padding и unindexed regions. Writer пересчитывает только
явно производные значения: размеры, offsets, число записей, сортировочную
перестановку и padding. Такая дисциплина позволяет редактировать известную
часть ресурса, не разрушая данные, смысл которых пока не установлен.
Canonical repack допустим только как явная операция. Он может исключать
неиндексируемые диапазоны, пересортировывать таблицы и пересобирать padding, но
не должен быть побочным эффектом обычного редактирования. Если пользователь
открыл существующий архив и изменил один известный атрибут, все остальные bytes,
не являющиеся производными от этого изменения, должны пройти roundtrip без
потери.
+648
View File
@@ -0,0 +1,648 @@
# IV. Мир, миссии и игровой runtime
Миссия в Iron3D не является готовым снимком мира. Она задаёт исходные данные:
маршруты, кланы, размещённые объекты, свойства, ссылку на ландшафт и
дополнительные записи. Runtime строит из этого карту, пространственные
структуры, очередь `World3D`, визуальные представления, controllers и связи с
ресурсной системой.
Для совместимой реализации важно не смешивать три слоя:
1. **Disk data** -- `data.tma`, `Land.msh`, `Land.map`, `BuildDat.lst` и
связанные resource archives.
2. **Prepared data** -- разобранные paths, clans, terrain streams, areal graph,
prototype graph, material и texture handles.
3. **Runtime objects** -- World3D instances, domain controllers, spatial
registration, AI/scripts, timers и расчётный tick.
Граница между этими слоями нужна для диагностики и отката. Ошибка в достижимой
цепочке размещённого объекта должна остановить создание миссии до публикации
объекта в очереди событий. Недостижимая запись общего архива может быть
inventory warning и не обязана блокировать текущую карту.
## `data.tma`: данные миссии
`data.tma` -- основное описание расстановки и логической конфигурации миссии.
Он не содержит всю геометрию, материалы или AI-код. Файл перечисляет paths,
clans, objects, свойства и ссылки на внешние прототипы. Подробный справочный
контракт формата вынесен в [TMA](../reference/tma.md), но глава использует его
как часть сквозного runtime pipeline.
TMA читается строго последовательно bounded cursor-ом. Записи имеют переменную
длину, поэтому offsets следующих секций получаются только после разбора
предыдущих. Секции нельзя искать по сигнатурам: порядок управляется счётчиками,
длинами и mode-dependent ветками.
Главный критерий корректности -- `cursor.offset == file_size` после последней
записи. Неописанный хвост, переполнение при вычислении размеров, отрицательный
или чрезмерный count и выход за bounds являются ошибками parser-а, а не
материалом для эвристического восстановления.
### Верхний уровень
Все переменные строки в проверенных TMA используют length-prefixed primitive:
```c
struct LpString {
uint32_t byte_length;
uint8_t bytes[byte_length];
};
```
Завершающий NUL не является обязательной частью framing. Reader продвигается
ровно на `4 + byte_length`. Текст можно декодировать как legacy ANSI/CP1251 для
человекочитаемого представления, но исходные bytes сохраняются для lossless
режима.
Подтверждённый верхний уровень:
```text
u32 format_version // 1
u32 path_count
PathRecord paths[path_count]
u32 clan_section_version // 6
u32 clan_count
ClanRecord clans[clan_count]
u32 object_section_version // 10
u32 object_count
PlacedObject objects[object_count]
LpString land_path
u32 mission_flag
LpString description_raw
u32 extra_section_version // 1
u32 extra_count
ExtraRecord28 extras[extra_count]
```
Имена `clan_section_version`, `object_section_version` и
`extra_section_version` описывают устойчивое положение полей в контракте. Они
не доказывают исходные имена C++-структур. Strict mode проверяет известные
значения, compatible mode сохраняет raw value и сообщает диагностический
контекст.
### Paths
```c
struct PathRecord {
int32_t path_id;
uint32_t point_count;
float points[point_count][3];
};
```
Paths идут сразу после `path_count` без имён и padding. `path_id` не обязан
совпадать с физической позицией записи: script/gameplay reference должен
использовать сохранённый ID, а не индекс массива.
Перед выделением массива проверяются `point_count`, умножение `point_count *
12` и наличие всего диапазона в файле. Координаты хранятся как little-endian
`float32` triples в общей системе координат мира.
### Clans
Clan section задаёт участников миссии, их ресурсные связи, позиционные anchors
и таблицы отношений. Общая prefix-часть:
```text
LpString name
i32 raw_id
f32 anchor_x
f32 anchor_y
u32 mode
mode-dependent body
relation table
```
Для обычных modes `1..3` тело содержит две пары:
```text
LpString resource_path
i32 resource_tag
LpString resource_path
i32 resource_tag
```
После них идёт relation table:
```text
u32 relation_count
repeat relation_count:
LpString other_clan_name
i32 relation_value
```
Первая ресурсная строка обычно указывает на script/formula base, вторая -- на
TRF или пустой ресурс. Tags различаются между кланами и должны сохраняться как
raw-поля, пока их потребительская семантика не закрыта.
Mode `0` имеет отдельный count-driven layout:
```text
LpString first_resource
u32 spatial_group_count
repeat spatial_group_count:
u32 record_count
repeat record_count:
float raw_spatial[5]
LpString second_resource
i32 second_tag
u32 relation_count
relations...
```
Внутренний `record_count` в известных живых образцах равен `1`, но parser читает
объявленное значение. Нельзя разбирать mode `0` как обычные две resource
references: это сдвигает cursor и ломает последующую relation table.
### PlacedObject и свойства
Ключевое поле размещённого объекта -- `resource_name`. Оно имеет два рабочих
варианта:
1. прямой логический ключ прототипа, который ищется в `objects.rlb`;
2. путь к unit DAT, из которого получается список компонентных ключей.
Доказанное framing объектной записи:
```text
u32 raw_kind
u32 class_or_flags
LpString resource_name
u32 raw_after_resource
u32 identity_or_clan_raw
f32 position[3]
f32 orientation[3]
f32 scale[3]
LpString instance_name
u32 raw_after_name
i32 link0
i32 link1
u32 property_schema_version // 1
u32 property_count
Property properties[property_count]
```
`orientation[3]` названа по наблюдаемому использованию как transform-поле, но
точный Euler order должен подтверждаться pose/render parity. `scale` в
большинстве записей равен `(1,1,1)`. `instance_name` может быть пустым у
unit-ссылки или содержать stem размещённого прототипа.
Свойства хранятся как ordered property bag:
```text
Property:
u32 raw_value[4]
LpString name
```
Порядок, повторяемость имени и raw 16-byte value важнее удобного словаря.
Разные consumers интерпретируют четыре слова как integer, float, default или
range data в зависимости от имени свойства. Typed view допустим только для
доказанных property names; базовый parser обязан сохранить исходный порядок.
В раннем проверенном корпусе на каждом из 201 размещённого объекта встречаются
`Invulnerability` и `Life state`. Для 48 unit-ссылок дополнительно наблюдаются
`LogicalID`, `ClanID`, `Type`, `MaxSpeedPercent`, `MaximumOre`, `CurrentOre`,
`ChargeRadius`, `FreeBotNum`, `FreeTechnoNum`, `FreeConstructionTime` и
`FreeResearchTime`. Имя `NOT USED` встречается массово и сохраняется как
обычное поле, несмотря на исторический смысл названия.
### Epilogue и extras
После объектов идут путь к ландшафту, флаг миссии, raw-описание и trailing
section. `description_raw` не всегда является чистым текстом: внутри
объявленной длины встречаются служебные bytes и остатки путей. Поэтому decoded
view является вспомогательным, а не каноническим представлением.
```c
struct ExtraRecord28 {
float position[3];
uint32_t raw[4];
};
```
Последние четыре слова `ExtraRecord28` пока не нормализуются. Reader хранит их
как raw data и не позволяет extra record поглотить начало следующей секции или
файловый хвост.
Покрытие полных каталогов:
```text
Часть 1: 29 TMA, 34 paths, 101 clans, 864 objects, 28 extra records
Часть 2: 31 TMA, 61 paths, 91 clans, 885 objects, 41 extra records
```
Версии стабильны: верхний уровень `1`, clan section `6`, object section `10`,
property schema `1`, trailing section `1`. У всех размещённых объектов
`class_or_flags == 0x80000002`.
## Сквозная загрузка миссии
`data.tma` описывает размещение, но видимый runtime-объект появляется только
после прохождения dependency graph. Простая загрузка файлов с похожим stem
работает на отдельных объектах, но ломается на составных unit DAT, изменённых
именах моделей и наследовании прототипов через `objects.rlb`.
Сквозная цепочка:
```text
TMA object
-> direct prototype key или unit DAT
-> component key
-> objects.rlb entry
-> MSH и WEAR
-> material slots
-> MAT0 phases
-> Texm и lightmap
-> prepared World3D instance
```
Контейнеры и графические форматы описаны отдельно в [NRes](../reference/nres.md),
[MSH](../reference/msh.md), [WEAR и MAT0](../reference/materials.md) и
[Texm](../reference/texm.md). В этой главе они рассматриваются как ребра
создания мира.
### Фазы loader-а
1. **Mission context.** Выбрать каталог миссии, прочитать конфигурацию и
определить карту.
2. **World foundation.** Загрузить `Land.msh`, `Land.map`, `BuildDat.lst` и
создать spatial managers.
3. **Mission description.** Разобрать TMA, paths и clans, но пока не публиковать
объекты.
4. **Prototype resolution.** Для каждой размещённой сущности раскрыть прямой
ключ или unit DAT и построить component list.
5. **Resource preparation.** Открыть требуемые RLB/LIB, проверить MSH, WEAR,
MAT0, textures, lightmaps и effects.
6. **Instance construction.** Создать World3D objects и domain controllers,
заполнить transform, ownership и properties.
7. **Registration.** Только после успешной настройки добавить instances в
queue и spatial structures.
8. **Scenario start.** Подключить AI/scripts, активировать timers и разрешить
первый calculation tick.
Разделение construction и registration предотвращает появление наполовину
созданного объекта в очереди событий. Если ошибка возникает до регистрации,
pending objects освобождаются без рассылки gameplay-событий. После регистрации
откат выполняется через обычный lifecycle очереди.
### Статистика dependency graph
Для ранних шести миссий 201 размещённый объект даёт 48 ссылок на unit-файлы и
153 прямых ключа. Unit-файлы раскрываются в 348 компонентов. Всего получается
501 запрос прототипа; для каждого достижимого запроса найдены запись реестра,
MSH и WEAR.
Полный dependency graph частей 1 и 2:
```text
Часть 1
864 placed objects
463 unit references -> 4 300 components
4 701 prototype/MSH/WEAR requests
36 954 material slots
48 806 texture requests + 139 lightmaps
failures 0
Часть 2
885 placed objects
561 unit references -> 5 521 components
5 845 prototype/MSH/WEAR requests
50 888 material slots
68 603 texture requests + 214 lightmaps
failures 0
```
`failures 0` означает, что для каждой достижимой ветви найдены prototype,
effective MSH/WEAR, MAT0, Texm и lightmap. Это не означает, что во всём
глобальном каталоге нет недостижимых или служебных записей.
Метрики нужно помечать областью. Чистая object chain шести ранних миссий даёт
3 873 material slots и 5 049 texture requests. Mission total включает по одной
environment WEAR-таблице на миссию и становится 3 879 material slots и 5 067
texture references.
### Диагностика ошибок
Ошибка привязывается к конкретному ребру графа:
- миссия ссылается на отсутствующий unit-файл;
- unit DAT раскрывается в component key, которого нет в реестре;
- prototype найден, но его MSH отсутствует в ожидаемом archive;
- WEAR указывает на неизвестный MAT0;
- MAT0 phase ссылается на отсутствующий Texm или lightmap;
- prepared object не прошёл валидацию transform/properties.
Сообщение вида `resource not found` недостаточно для восстановления каталога.
Диагностика должна содержать исходный placed object, раскрытый ключ, archive,
entry и тип связи.
## `Land.msh`: ландшафт как специализированная модель
`Land.msh` является [NRes](../reference/nres.md)-архивом, но его содержимое
отличается от обычной объектной MSH. Он хранит геометрию поверхности, таблицы
участков и ускорители пространственных запросов. Видимые buffers являются лишь
частью данных: CPU-подсистемам остаются нужны adjacency, surface classes и
cell accelerator streams.
Во всех проверенных картах порядок типов одинаков:
```text
1, 2, 3, 4, 5, 18, 14, 11, 21
```
Типы `1`, `3`, `4` и `5` совместимы по базовому представлению с узлами,
позициями, нормалями и UV обычной модели. Типы `11` и `21` специфичны для
terrain; `14` и `18` являются дополнительными потоками.
### Streams и размеры элементов
```text
type 1 38 байт node/slot mapping
type 3 12 байт float3 positions
type 4 4 байта packed normals
type 5 4 байта packed UV
type 11 4 байта cell accelerator data
type 14 4 байта auxiliary stream
type 18 4 байта auxiliary stream
type 21 28 байт terrain face
```
Для этих streams `attr1` соответствует числу элементов, а `attr3` -- stride.
Тип `2` начинается заголовком размером `0x8C`, после которого идут slot records
по 68 байт. Число slots вычисляется как `(size - 0x8C) / 68`; reader проверяет
делимость, bounds и отсутствие хвоста.
### `TerrainFace28`
Запись type `21` связывает triangles, соседей и surface metadata:
```text
+0x00 .. +0x07 flags и служебные поля
+0x08 u16 vertex0
+0x0A u16 vertex1
+0x0C u16 vertex2
+0x0E u16 neighbor0
+0x10 u16 neighbor1
+0x12 u16 neighbor2
+0x14 .. +0x1B material/class/edge fields
```
Каждый vertex index обязан быть меньше числа позиций type `3`. Neighbor равен
`0xFFFF` либо указывает на другой элемент type `21`. Последние восемь bytes
сохраняются без нормализации до полного закрытия предметной семантики.
### Маски поверхности
Runtime использует полную 32-битную маску face и два compact-представления.
Основное 16-битное поле собирается из отдельных битов полной маски; второе
шестибитное поле хранит material classes. Это не усечение младших битов.
Для совместимого writer-а нужны явные функции `full_to_compact()` и
`compact_to_full()`. Неизвестные биты полной маски сохраняются отдельно, иначе
обратное преобразование потеряет информацию.
Основное соответствие:
```text
full 00000001 -> compact 0001
full 00000008 -> compact 0002
full 00000010 -> compact 0004
full 00000020 -> compact 0008
full 00001000 -> compact 0010
full 00004000 -> compact 0020
full 00000002 -> compact 0040
full 00000400 -> compact 0080
full 00000800 -> compact 0100
full 00020000 -> compact 0200
full 00002000 -> compact 0400
full 00000200 -> compact 0800
full 00000004 -> compact 1000
full 00000040 -> compact 2000
full 00200000 -> compact 8000
```
Для шестибитного material-поля используются full-биты `0x100`, `0x8000`,
`0x10000`, `0x40000`, `0x80000` и `0x80`; они переходят соответственно в
compact-биты `1`, `2`, `4`, `8`, `0x10`, `0x20`.
### Проверенное покрытие
```text
AutoMAP 3 051 вершина, 3 174 faces
PROL 11 125 вершин, 9 234 faces
Tut_1 8 827 вершин, 8 290 faces
Tut_2 9 456 вершин, 8 996 faces
Tut_3 9 833 вершины, 8 560 faces
Tut_4 9 022 вершины, 8 612 faces
```
Расширенное покрытие:
```text
Часть 1: 33 карты, 299 450 vertices, 275 882 faces
Часть 2: 32 карты, 188 024 vertices, 184 454 faces
```
Во всех 65 картах порядок типов равен `[1,2,3,4,5,18,14,11,21]`. Strides,
count-driven размеры, vertex indices, neighbor indices и payload bounds
валидны. Различия карт являются различиями данных, а не новым вариантом
loader-а.
## `Land.map` и ArealMap
`Land.map` хранит логическое разбиение пространства на связанные области. Это
NRes-архив с одной записью type `12`. Payload содержит переменное число
ареалов, links и grid быстрого поиска.
Ареал -- участок мира с геометрической границей и метаданными. Граф соседств
позволяет искать маршрут между крупными областями вместо обхода каждой
terrain-вершины. Grid отвечает на быстрый вопрос: какие области потенциально
находятся рядом с координатой.
### Prefix ареала
```c
struct ArealPrefix56 {
float anchor_x;
float anchor_y;
float anchor_z;
float reserved_12;
float area_metric;
float normal_x;
float normal_y;
float normal_z;
uint32_t logic_flag;
uint32_t reserved_36;
uint32_t class_id;
uint32_t reserved_44;
uint32_t vertex_count;
uint32_t poly_count;
};
```
После prefix идут `float3 vertices[vertex_count]`. Нормаль в проверенных
записях имеет длину, практически равную единице. Поля `reserved_12`,
`reserved_36` и `reserved_44` в живом корпусе равны нулю, но writer сохраняет
их без нормализации.
### Links и polygon blocks
За вершинами хранится массив:
```c
struct EdgeLink8 {
int32_t area_ref;
int32_t edge_ref;
};
```
Пара `(-1, -1)` означает отсутствие соседа. Иначе `area_ref` указывает на
другую область, а `edge_ref` -- на соответствующее ребро. Число пар равно
`vertex_count + 3 * poly_count`.
После links для каждого polygon читается `u32 n`, затем block размером
`4 * (3*n + 1)` bytes. Во всех 65 проверенных картах `poly_count == 0`.
Framing ветки восстановлен по loader path, но предметное поведение polygon
blocks не получает статус corpus-verified.
### Grid быстрого поиска
После всех ареалов записаны `cellsX` и `cellsY`. Далее для каждой ячейки идут
`u16 hitCount` и `hitCount` номеров областей. Runtime уплотняет это в одно
32-битное значение: старшие 10 бит содержат число попаданий, младшие 22 --
начальный индекс в общем пуле.
Grid не является точной геометрической проверкой. Он возвращает короткий список
candidates, после чего выполняется проверка принадлежности области. При
загрузке каждый area ID обязан быть меньше общего числа ареалов.
Покрытие:
```text
Ранние шесть карт: 3 811 areals, grid 128 x 128
Часть 1: 33 карты, 34 662 areals, 197 698 areal vertices
Часть 2: 32 карты, 18 984 areals, 114 968 areal vertices
```
Во всех картах grid равен `128 x 128`. Максимальное число candidates в ячейке
-- 20 для Части 1 и 14 для Части 2. Все area/edge references находятся в
диапазоне, normals имеют единичную длину в пределах float32-погрешности, parser
заканчивается точно на конце payload.
## Пространственные задачи runtime
Движок решает три похожих, но независимых вопроса:
- **видимость** -- нужно ли рисовать объект для текущей камеры;
- **столкновение** -- пересекается ли движение с поверхностью или другим телом;
- **навигация** -- через какие области допустимо провести маршрут.
Terrain, Control и ArealMap используют общие координаты мира, но разные
структуры данных. Нельзя заменять навигационный граф видимыми triangles или
вычислять collision только по границе areal. Render frame описан отдельно в
[Render frame](../reference/render-frame.md); здесь важна подготовка world data,
которую renderer получает уже после загрузки миссии.
### Поиск области
Координата переводится в ячейку grid из `Land.map`. Ячейка даёт список
candidate areas, затем выполняется точная геометрическая проверка. Такой запрос
не перебирает все области карты и не зависит от количества terrain faces.
Если координата попадает в несколько candidates, выбор должен учитывать
геометрию boundary и class/logic flags, а не только первый ID из grid cell.
Если область не найдена, caller получает явный miss и решает, допустим ли
fallback к ближайшей области.
### Маршрут
После определения начальной и целевой областей маршрут строится по графу
соседств. Результат высокого уровня -- последовательность areal IDs. Из неё
формируется локальный corridor, внутри которого movement controller выбирает
конкретное движение по поверхности.
Такое разделение оставляет навигацию устойчивой к деталям terrain mesh:
изменение density triangles не должно менять high-level route, пока areal graph
и links остаются теми же.
### Категории зон объектов
`BuildDat.lst` связывает 12 имён категорий с 32-битными масками:
```text
Bunker_Small 80010000
Bunker_Medium 80020000
Bunker_Large 80040000
Generator 80000002
Mine 80000004
Storage 80000008
Plant 80000010
Hangar 80000040
MainTeleport 80000200
Institute 80000400
Tower_Medium 80100000
Tower_Large 80200000
```
Файл читается секционно. Неизвестное имя, дублирование или нарушенная структура
не должны тихо превращаться в нулевую маску. Нулевая маска является
диагностируемым состоянием, а не универсальным default.
## Создание мира
Инициализация карты должна быть staged pipeline, а не набором независимых
autoload-ов:
1. открыть `Land.msh` и построить geometry/spatial данные terrain;
2. открыть `Land.map` и создать areals, links и cell grid;
3. загрузить категории `BuildDat.lst`;
4. создать world managers для поверхности, областей, света и атмосферы;
5. разобрать TMA, paths и clans;
6. раскрыть object resources через unit DAT и `objects.rlb`;
7. подготовить MSH, WEAR, MAT0, Texm, lightmap и FXID dependencies;
8. создать World3D objects и domain controllers в pending state;
9. проверить cross references между components, controllers и spatial data;
10. зарегистрировать visual, physical и behavior components;
11. подключить AI/scripts и разрешить первый calculation tick.
Минимальный псевдокод объектной части:
```c
for (const PlacedObject& placed : mission.objects) {
vector<string> keys = expand_resource_name(placed.resource_name);
for (const string& key : keys) {
Prototype p = registry.resolve(key);
PreparedVisual v = prepare_visual(p);
Object* o = construct_component(p, v, placed.properties);
o->set_world_transform(placed.transform);
pending_registration.push_back(o);
}
}
validate_cross_references(pending_registration);
register_all(pending_registration);
```
`prepare_visual` использует явные ссылки прототипа и правила fallback ресурсной
системы. Она не должна угадывать модель по имени placed object, если prototype
уже задаёт другой effective MSH/WEAR.
## Инварианты реализации
- Reader всех count-driven структур проверяет overflow до выделения памяти.
- Parser TMA, `Land.msh` и `Land.map` завершает работу точно на конце своего
payload.
- Неизвестные поля, reserved bytes, raw strings и property values сохраняются
lossless.
- Object properties остаются ordered property bag; сортировка имён запрещена.
- Clan relations и area links проверяются на диапазон, но физический порядок
записей сохраняется.
- Terrain vertex indices, face neighbors и areal references валидируются до
публикации spatial managers.
- Достижимый missing resource останавливает mission load до регистрации
объектов; недостижимая запись общего каталога остаётся диагностикой.
- Calculation tick включается только после успешной сборки terrain, areal graph,
managers, object queue и scenario bindings.
+863
View File
@@ -0,0 +1,863 @@
# V. Геометрия, материалы и рендер
Этот том описывает путь от загруженного игрового состояния до pixels в back
buffer. Renderer не решает игровые правила: он получает transforms, geometry,
материалы, свет, эффекты, камеру и список видимых объектов, затем превращает
их в упорядоченный набор draw calls и fixed-function states.
Графический pipeline FParkan держится на нескольких слоях данных:
```text
MSH node/slot/batch
-> Batch20.material_index
-> строка WEAR
-> имя MAT0
-> активная phase
-> textureName и lightmap slot
-> Texm payload
-> LegacyRenderState
-> draw item кадра
```
Важное практическое правило: форматы ресурсов, runtime-состояние renderer-а и
современный backend являются разными уровнями. Файл можно прочитать правильно и
всё равно получить неверный кадр из-за другой сортировки, другого mip-skip,
другой ветки material fallback или другого округления animation time.
## Контур рендера
Изображение является последней стадией длинного цикла. До renderer-а уже
накоплен ввод, рассчитан simulation step, применены отложенные операции,
обновлены animation states, выбрана camera и выставлен listener для 3D sound.
```text
system messages and input
-> simulation calculation
-> deferred object operations
-> animation and transforms
-> camera and sound listener
-> visibility and render queues
-> materials and draw passes
-> renderer completion
-> end-of-render callbacks and UI
```
CPU делает отбор объектов, сэмплирует animation, собирает matrices, выбирает
LOD/slot, группирует batches и готовит состояния. Графический pipeline
преобразует вершины из model space в screen space, rasterizes triangles,
проверяет depth, применяет texture stages, lighting, alpha test/blend и пишет
pixels.
Координатный путь вершины:
```text
local/model space
-> world space
-> view/camera space
-> clip space
-> normalized device coordinates
-> viewport pixels
```
Порядок умножения матриц и соглашение о layout должны быть едины во всём
движке. Ошибка транспонирования часто выглядит как сломанная анимация, хотя
ключи модели прочитаны верно.
## Граница Ngi32
`Ngi32.dll` является платформенной границей Iron3D-era renderer-а. Она создаёт
графический и звуковой interfaces, перечисляет устройства, хранит capability
profile, предоставляет память, часы и быстрые математические процедуры.
Высокоуровневые DLL должны обращаться к interface Ngi32, а не напрямую к
конкретному DirectDraw/Direct3D device.
`iron_3d.ini` задаёт выбранный `CURRENT_D3DCARD`. Display layer перечисляет
drivers и video modes, проверяет поддержку 3D, переводит native capabilities во
внутренний профиль и создаёт render object. `niCreate3DRender` принимает
выбранный driver/mode, window handle и flags владения, динамически получает
функции DirectDraw/Direct3D семейства 5-7 и публикует refcounted renderer.
`niGet3DRender` возвращает уже созданный объект и увеличивает число владельцев.
```text
enumerate adapters and video modes
-> choose CURRENT_D3DCARD
-> translate native capabilities
-> create DirectDraw surfaces and 3D interface
-> construct engine renderer
-> publish global refcounted pointer
```
Старый API работает как state machine. Перед draw подсистема terrain/shade
выбирает matrices, texture stages, filtering, depth test/write, culling, alpha
test, blending и vertex format. Современный backend может собрать это в
immutable pipeline key и реализовать через shaders, но compatibility layer
должен видеть исходную fixed-function модель.
```c
struct LegacyRenderState {
Mat4 world, view, projection;
TextureStage stages[2];
BlendMode blend;
DepthMode depth;
CullMode cull;
bool alpha_test;
uint8_t alpha_ref;
VertexFormat vertex_format;
};
```
Эта структура является переносимой моделью наблюдаемого контракта, а не
утверждением о точном layout оригинального объекта renderer-а.
Отдельная часть ABI -- таблица `g_FastProc`. При запуске выбираются scalar,
MMX, Katmai/SSE, 3DNow или PPro-реализации процедур, а `niGetProcAddress(index)`
возвращает pointer из изменяемой таблицы. Номер slot является частью ABI:
signature менять нельзя. Различия scalar/SIMD округления способны менять
animation sampling, culling, particles и даже gameplay-adjacent decisions.
## MSH как граф модели
`*.msh` является nested NRes, а не одной монолитной структурой. Geometry,
nodes, slots, batches, animation и служебные streams лежат в отдельных entries
и связываются по `type_id`. Физический порядок entries сохраняется для
roundtrip, но reader не должен выводить из него смысловую связь.
Карта основных entries:
```text
type 1 узлы и выбор slot, обычно stride 38
type 2 header 0x8C + slots по 68 байт
type 3 positions float3, stride 12
type 4 packed normals, stride 4
type 5 packed UV0, stride 4
type 6 index buffer, u16
type 7 triangle descriptors, stride 16
type 8 animation keys, stride 24
type 9 служебный поток модели
type 10 строки и имена узлов
type 13 draw batches, stride 20
type 15 дополнительный поток, stride 8
type 17 вспомогательные данные
type 18 редкий поток, stride 4
type 19 animation frame map, u16
type 20 редкая вспомогательная таблица
```
Базовый набор types стабилен для проверенных моделей Частей 1 и 2. Расширенный
вариант добавляет types 18 и 20. Редкий вариант `MTCHECK.MSH` имеет
альтернативный атрибут type 1; его payload нужно поддерживать copy-through до
закрытия layout.
### Узлы и slots
Type 1 обычно состоит из записей по 38 байт:
```c
struct Node38 {
uint16_t hdr0;
uint16_t parent_or_link;
uint16_t anim_map_start;
uint16_t fallback_key;
uint16_t slot_index[15];
};
```
`slot_index` образует матрицу `3 LOD x 5 groups`. Выбор выполняется как
`slot_index[lod * 5 + group]`; `0xFFFF` означает отсутствие geometry для этой
комбинации. Поле `parent_or_link` участвует в иерархии или связи узлов, но
название остаётся описательным.
Type 2 начинается с header `0x8C`, затем содержит slots по 68 байт:
```c
struct Slot68 {
uint16_t tri_start;
uint16_t tri_count;
uint16_t batch_start;
uint16_t batch_count;
float aabb_min[3];
float aabb_max[3];
float sphere_center[3];
float sphere_radius;
uint32_t opaque[5];
};
```
Slot связывает диапазон triangle descriptors, диапазон draw batches, AABB и
sphere bounds. AABB удобен для более точных осевых тестов, sphere -- для
быстрого отбрасывания. Последние пять слов сохраняются без интерпретации.
Обязательные проверки:
- `type 2` имеет размер не меньше `0x8C`;
- остаток после header кратен 68;
- каждый `slot_index` либо `0xFFFF`, либо меньше числа slots;
- `tri_start + tri_count` не выходит за type 7;
- `batch_start + batch_count` не выходит за type 13.
### Vertex streams, triangles и batches
Основные vertex streams:
```text
type 3: position = три float32
type 4: normal = четыре int8
type 5: UV0 = два int16
type 6: index = uint16
```
Normal XYZ декодируется как signed component / `127.0` с clamp в `[-1, 1]`.
Четвёртый byte normal stream не отбрасывается при roundtrip. UV декодируется
как `packed / 1024.0`. Index buffer адресует вершины относительно `base_vertex`
batch-а, поэтому проверка допустимости всегда использует
`base_vertex + index < vertex_count`.
Type 7 хранит descriptors triangles:
```c
struct TriDesc16 {
uint16_t tri_flags;
uint16_t link0;
uint16_t link1;
uint16_t link2;
int16_t nx;
int16_t ny;
int16_t nz;
uint16_t sel_packed;
};
```
Descriptors используются коллизией, выбором и связями triangles. `sel_packed`
содержит три двухбитовых selector-а; значение `3` преобразуется в отсутствие
ссылки (`0xFFFF`). Полная семантика links и flags не закрывается одним layout.
Type 13 задаёт draw ranges:
```c
#pragma pack(push, 1)
struct Batch20 {
uint16_t batch_flags; // +0x00
uint16_t material_index; // +0x02
uint16_t opaque4; // +0x04
uint16_t opaque6; // +0x06
uint16_t index_count; // +0x08
uint32_t index_start; // +0x0A
uint16_t opaque14; // +0x0E
uint32_t base_vertex; // +0x10
};
#pragma pack(pop)
static_assert(sizeof(Batch20) == 20);
```
`material_index` выбирает строку WEAR. `index_start`, `index_count` и
`base_vertex` описывают один indexed draw. Неизвестные поля могут влиять на
редкие проходы или state grouping, поэтому writer сохраняет их 1:1.
Типовой обход модели:
```c
for (Node& node : model.nodes) {
Matrix node_world = parent_world * local_transform(node);
uint16_t sid = node.slot_index[lod * 5 + group];
if (sid == 0xFFFF) continue;
Slot& slot = model.slots[sid];
if (camera.culls(transform(slot.bounds, node_world))) continue;
for (uint32_t i = 0; i < slot.batch_count; ++i) {
Batch& b = model.batches[slot.batch_start + i];
bind_wear_material(b.material_index);
draw_indexed(b.base_vertex, b.index_start, b.index_count);
}
}
```
В реальном кадре между culling и draw добавляются material resolve, lightmap,
render queues и сортировка, но связи данных остаются такими.
## Иерархия и анимация
Анимация MSH меняет локальный transform узлов. Geometry streams не изменяются:
для каждого узла на кадр строится matrix из position и quaternion. Дочерний
узел наследует transform родителя, поэтому изменение корпуса переносит башню,
точки крепления и все связанные slots.
Связка состоит из:
- type 8: пул animation keys;
- type 19: карта кадров;
- `anim_map_start` и `fallback_key` в `Node38`;
- parent links, задающих порядок умножения matrices.
Ключ type 8 занимает 24 байта:
```c
struct AnimKey24 {
float position[3];
float time;
int16_t qx;
int16_t qy;
int16_t qz;
int16_t qw;
};
```
Quaternion components декодируются как signed value / `32767.0`. На диске
порядок полей XYZ-W, но runtime math использует логическое `[w, x, y, z]`.
Безусловная современная нормализация после чтения не добавляется без parity
проверки: она может изменить крайние кадры.
Type 19 является массивом `uint16_t`; его `attr2` задаёт общее число кадров
timeline. Для конкретного узла `anim_map_start` указывает на блок длиной
`frame_count` либо равен `0xFFFF`.
Выбор ключа:
1. вычислить frame index из времени;
2. если frame вне диапазона, взять `fallback_key`;
3. если `anim_map_start == 0xFFFF`, взять `fallback_key`;
4. иначе прочитать `map_words[anim_map_start + frame]`;
5. если значение не меньше `fallback_key`, снова использовать fallback;
6. иначе использовать mapped key и следующий key для interpolation.
Fallback возвращается без interpolation. Это защищает статические узлы и конец
track-а.
Для времени между двумя keys:
```text
alpha = (t - k0.time) / (k1.time - k0.time)
position = lerp(k0.position, k1.position, alpha)
rotation = shortest-path quaternion blend
```
Перед quaternion blend проверяется dot product. Если стороны находятся в
противоположных полусферах, знак второй стороны меняется, чтобы пройти по
короткому пути. При точном совпадении времени возвращается соответствующий key
без вычисления alpha.
Объект может переходить между двумя animation states. Тогда для каждого узла
сэмплируются позы A и B, затем position смешивается линейно, а quaternion --
через shortest-path blend. Если одна сторона невалидна, используется другая.
```c
Pose sample_node(Node n, float t);
Pose blend_pose(Pose a, Pose b, float weight);
Mat4 local = quaternion_matrix(pose.rotation);
local.set_translation(pose.position);
world[n] = world[parent(n)] * local;
```
Для parity особенно важны x87-compatible округление при выборе frame index и
порядок операций. Одинаковая формула на SSE может выбрать соседний кадр возле
границы.
Проверки animation data:
- размер type 8 кратен 24;
- размер type 19 кратен 2;
- каждый `fallback_key` меньше числа keys;
- блок карты узла полностью помещается в type 19;
- времена keys внутри track возрастают;
- parent links не образуют cycle;
- quaternion components читаются как signed 16-bit.
## WEAR и MAT0
MSH batch хранит только числовой `material_index`. WEAR переводит позиционный
slot в имя материала. MAT0 по этому имени описывает phases, parameters,
texture names и animation blocks. Такое разделение позволяет одной geometry
использовать разные appearances.
```text
Batch20.material_index
-> строка WEAR
-> имя MAT0
-> активная phase
-> textureName и render parameters
```
### WEAR
WEAR имеет type ID `0x52414557` и обычно хранится как `*.wea` рядом с моделью.
Формат текстовый:
```text
<wearCount>
<legacyId> <materialName>
... wearCount строк
[пустая строка]
[LIGHTMAPS
<lightmapCount>
<legacyId> <lightmapName>
... lightmapCount строк]
```
`legacyId` читается и сохраняется, но material выбирается по позиции строки и
имени. Пустая строка перед `LIGHTMAPS` является частью совместимого framing:
parser paths по-разному обрабатывают переход, и отсутствие разделителя ломает
совместимость. Material handle кодируется как `(table_index << 16) |
wear_index`; manager поддерживает ограниченное число wear tables.
Fallback material resolve строго разделён:
1. имя из WEAR;
2. `DEFAULT`;
3. entry 0;
4. для lightmap отсутствие означает slot `-1`, а не замену обычной texture.
Пустое имя texture внутри phase означает намеренно untextured surface.
Lightmap ищется в отдельном cache и не подменяется diffuse texture.
### MAT0
MAT0 имеет type ID `0x3054414D` и обычно находится в `Material.lib`. `attr1`
содержит runtime flags, `attr2` -- версию payload. Versioned metadata читается
cursor-ом: старые версии получают runtime defaults, но reader не пытается
насильно читать поля новой версии.
```c
#pragma pack(push, 1)
struct Mat0PrefixV4Plus {
uint16_t phase_count; // +0x00
uint16_t animation_block_count; // +0x02, меньше 20
uint8_t metadata_a; // +0x04, attr2 >= 2
uint8_t metadata_b; // +0x05, attr2 >= 2
uint32_t metadata_c_raw; // +0x06, attr2 >= 3
uint32_t metadata_d_raw; // +0x0A, attr2 >= 4
};
struct Phase34 {
uint8_t parameters[18];
char texture_name[16];
};
#pragma pack(pop)
static_assert(sizeof(Phase34) == 34);
```
Если `attr2 < 2`, metadata A/B получают default `255`; при `attr2 < 3`
значение C соответствует `1.0f`; при `attr2 < 4` D равно 0. C/D сохраняются
как raw 32-bit values до полного подтверждения интерпретации. Phase parameters
сохраняются как 18 raw bytes даже там, где часть bytes уже имеет понятный
смысл.
Каждая phase разворачивается в runtime-запись примерно 76 байт: коэффициенты
цвета, освещения и прозрачности, texture slot и служебные поля. Material time
выбирает одну или две phases; только часть полей интерполируется, остальные
копируются из активной записи.
Animation block MAT0 имеет плотный framing без 4-byte tail alignment:
```text
u32 header_raw
u16 key_count
repeat key_count:
u16 k0
u16 k1
u16 k2
```
Младшие три бита `header_raw` задают числовой mode, остальные образуют mask
interpolation. Наблюдаются modes 0, 1, 2 и 3, связанные с семействами loop,
ping-pong, one-shot/clamp и random-offset, но точные boundary cases остаются
предметом runtime parity. Поле `k2` сохраняется всегда.
Проверки MAT0:
- `animation_block_count < 20`;
- все versioned metadata помещаются в payload;
- секция phases имеет ровно `phase_count * 34` байта;
- `texture_name` ограничено 16 байтами;
- каждый animation block и его keys помещаются в payload;
- parser заканчивает чтение на точном конце записи.
Material manager кэширует разобранный MAT0 и texture handles. Current phase
лучше вычислять на экземпляр материала, если random offset или локальное время
различаются между объектами; immutable phase data остаются общими.
## Texm: текстуры, mip-уровни и атласы
`Texm` -- основной формат изображений. Он хранится в `Textures.lib`,
`LightMap.lib` и других NRes-архивах. Payload содержит header, необязательную
palette, mip chain и иногда `Page` chunk для atlas rectangles.
```c
struct TexmHeader32 {
uint32_t magic; // 'Texm'
uint32_t width;
uint32_t height;
uint32_t mip_count;
uint32_t flags4;
uint32_t flags5;
uint32_t unknown6;
uint32_t format;
};
```
Подтверждённые formats:
```text
0 Indexed8 + palette 256 x 4 байта
565 R5 G6 B5
556 R5 G5 B6
4444 A4 R4 G4 B4
88 L8 A8
888 RGB8 в четырёхбайтовом element
8888 A8 R8 G8 B8
```
Formats 556 и 88 являются loader-confirmed, но не corpus-verified для
доступных игровых payload. CPU decoder расширяет короткие каналы до 8 bit через
повторение значимых bit, а не простым shift. Для 888 служебный четвёртый byte
сохраняется при roundtrip.
Layout:
```text
TexmHeader32
[palette 1024 байта, только для format 0]
level 0 pixels
level 1 pixels
...
level mip_count-1 pixels
[optional Page chunk]
```
Размер уровня `i` вычисляется из `max(1, width >> i)` и
`max(1, height >> i)`. Bytes per pixel: 1 для indexed; 2 для 565, 556, 4444 и
88; 4 для 888 и 8888. Parser суммирует размеры с проверкой overflow до чтения.
`Page` chunk:
```c
struct PageHeader8 {
uint32_t magic; // 'Page'
uint32_t rect_count;
};
struct PageRect8 {
int16_t x;
int16_t width;
int16_t y;
int16_t height;
};
```
Chunk обязан иметь размер `8 + rect_count * 8`; произвольный tail не
допускается. Rectangles задаются в pixel space базового mip. Если loader
пропускает верхние mip-уровни, rectangles масштабируются вместе с новым base
level.
Mip-skip является поведением loader-а, а не offline-изменением файла. После
skip меняются runtime width, height, mip count и pointer на первый загружаемый
уровень. Современный renderer должен повторить выбор base level или
эквивалентно эмулировать его upload policy; использование полной texture при
тех же UV меняет резкость и atlas coordinates.
Indexed texture требует связанную palette. Часть palettes выбирается по suffix
имени: буква `A..Z` и вариант пустой или `0..9`, всего 286 возможных slots.
Невалидный suffix диагностируется явно.
Обычные textures и lightmaps находятся в разных managers. Обычный cache
отслеживает refcount и время неиспользования, а eviction выполняется
отложенно. Lightmap lifetime связан с world/mission и не должен попадать под
ту же политику удаления.
Строгий Texm parser проверяет положительные dimensions, положительный
`mip_count`, известный format, точный размер palette/mip chain, корректный
`Page` и отсутствие лишних bytes. `flags4`, `flags5` и `unknown6` сохраняются
1:1; участие `flags5` в mip-skip подтверждено, но полная семантика всех bits не
закрыта.
## Свет, тени, атмосфера и сортировка
Свет является отдельной world-подсистемой. Terrain layer создаёт
`LightManager`, `Shader` и primitive managers. Это не один глобальный
коэффициент яркости: world управляет point lights, lightmaps, shadows,
atmospheric objects и sort phases. Материал сообщает свойства поверхности, а
CShade превращает их в states renderer-а.
Подтверждённые точки: `CreateLightManager`, `CreateShader`,
`CreateAtmosphere`, `CreatePrimitives`, `CreatePrimitives2`,
`CShade::StartMeshRender`, `CShade::EndMeshRender` и
`CShade::ConfigureTextureAndAlphaBlendModes`.
CShade получает active MAT0 phase, capability profile устройства и pass
context. Он выбирает texture mode, alpha blending, depth/cull behavior и способ
освещения. Наличие fallback вроде `TEXTUREMODE_MODULATE not supported`
означает, что material нельзя напрямую преобразовать в современный PBR.
Сначала строится legacy state, затем он сопоставляется shader permutation.
CLightManager выдаёт numeric IDs источникам и проверяет допустимое количество.
Ветка `EmulatePointLights()` позволяет воспроизводить point lights даже при
ограничениях hardware lighting. Неизвестный type light должен давать отдельную
ошибку.
Lightmap не является обычной diffuse texture. WEAR содержит отдельный блок
`LIGHTMAPS`, manager открывает `LightMap.lib`, а shade path подаёт lightmap
отдельным slot или texture stage. Замена lightmap предварительным умножением в
diffuse texture ломает LOD, atlas coordinates и динамическую модуляцию.
Тени проходят отдельным render pass. Terrain содержит пути для теней зданий и
роботов, ограничения максимального числа, detail level и smoothing. Доказаны
shadow manager/pass, настройки detail/smoothing/count и зависимость от
Terrain/CShade; полная формула projection geometry для каждого caster требует
dynamic trace. Unknown settings из `shade.cfg` читаются и сохраняются по
именам, а не заменяются произвольными modern defaults.
Atmosphere manager создаёт world objects для фоновых и погодных явлений.
Отдельно подтверждены lightning, sun render, flare, `env_lightning`, rain
background sound и обязательные ссылки на lightning effect. Эти объекты
обновляются по игровому времени, но часть параметров зависит от camera: flare
требует screen position и occlusion test, rain -- области рядом с observer,
sound -- listener. Их нельзя один раз запечь в terrain.
RNG для lightning, atmosphere phases и FX должен иметь стабильный порядок.
Даже правильный средний интервал не даёт повторяемый кадр, если random values
запрашиваются в другой последовательности.
Согласованная модель sort phases:
```text
opaque terrain and models
-> lightmapped/state-grouped passes
-> shadows and projected primitives
-> alpha-tested surfaces
-> transparent objects/effects back-to-front
-> atmosphere, flares and overlays
```
Точный взаимный порядок отдельных FX, shadow и atmosphere subpasses требует
capture. Новый renderer должен хранить явный `RenderPhase` и стабильный
secondary sort key, а не сортировать всё только по material ID.
## FXID: система эффектов
FXID -- не готовая картинка, а описание небольшого runtime command stream.
Header задаёт lifetime, time mode, random shifts и transform. Затем идут
команды разных types. При создании manager превращает disk-команды в runtime
objects; во время кадра они обновляются и выпускают sounds, particles,
materials или projected primitives.
Type ID равен `0x44495846`. Header занимает 60 байт:
```c
struct FxHeader60 {
uint32_t command_count;
uint32_t time_mode;
float duration_seconds;
float phase_jitter;
uint32_t flags;
uint32_t settings_id;
float random_shift[3];
float pivot[3];
float scale[3];
};
```
Поток команд начинается строго с offset `0x3C`. `duration_seconds`
преобразуется runtime-ом во внутреннюю шкалу времени. `phase_jitter` и
`random_shift` используются только при соответствующих flags. Pivot задаёт
локальную точку опоры, scale -- базовый масштаб экземпляра. Unknown flags и
settings ID сохраняются.
Каждая команда начинается с `uint32_t command_word`:
```text
opcode = command_word & 0xFF
enabled = (command_word >> 8) & 1
```
Bits 9-31 являются частью данных и сохраняются. Между командами нет
выравнивания. Размер команды, включая word:
```text
opcode 1 224 байта
opcode 2 148 байт
opcode 3 200 байт
opcode 4 204 байта
opcode 5 112 байт
opcode 6 4 байта
opcode 7 208 байт
opcode 8 248 байт
opcode 9 208 байт
opcode 10 208 байт
```
Parser использует opcode только для выбора фиксированного размера. Неизвестный
opcode отклоняется: попытка угадать длину потеряет синхронизацию всего stream.
Opcodes 2, 3, 4, 5, 7, 8, 9 и 10 содержат pair fixed strings:
```c
struct FxResourceRef64 {
char archive[32];
char name[32];
};
```
Имена сравниваются case-insensitive по ASCII, а tail после первого nul byte
сохраняется. Resolve выполняется при создании command object или лениво при
первом запуске, но ошибка должна включать имя эффекта, номер команды, archive
и resource name.
Базовый normalized age:
```text
tn = (now - start_time) / (end_time - start_time)
```
`time_mode` выбирает источник коэффициента: constant, forward/reverse age,
cyclic phase, external world state и варианты с ограничением относительно
предыдущего значения. Точные формулы редких modes являются parity-задачей.
Flags могут умножать alpha на lifetime, применять triangular remap, случайно
сдвигать phase/space, инвертировать active-state, фильтровать по времени суток
или включать manager gates.
Lifecycle:
```text
create instance
-> copy header and external transform
-> calculate end time and random offsets
-> create command objects in disk order
-> resolve required resources
-> Start
on each calculation/render frame
-> evaluate time coefficient and gates
-> update commands in stable order
-> emit active primitives or sounds
-> collect render batches
-> handle Stop / Restart / end-of-life
```
Update и emit разделяются. Simulation может продолжаться в кадре без render, а
emit не должен повторно менять игровое состояние. Для authoring безопасно
типизировать header и resource references, а body редких commands сохранять raw
до подтверждения field-level semantics.
## Полный кадр
Крупный вход в world render проходит через `World3D::stdRenderGame`. Доказан
следующий порядок boundary операций:
1. передать camera в Terrain через `stdSetCurrentCamera2` и сохранить её как
текущую;
2. получить camera/view/viewport interfaces через virtual queries;
3. обновить положение и ориентацию 3D sound listener;
4. настроить renderer viewport и matrices;
5. вызвать два renderer boundary slots перед traversal;
6. установить глобальный флаг `in_render`;
7. вызвать главный virtual метод camera/world traversal;
8. выполнить дополнительную post queue при включённом режиме;
9. завершить world/shade pass;
10. вызвать renderer completion slot;
11. снять `in_render`, восстановить viewport и разослать end-of-render.
Семантические имена нескольких slots перед и после traversal не подтверждены,
поэтому в compatibility code их лучше временно называть
`frame_boundary_0`, `frame_boundary_1`, `frame_boundary_2`.
Обход видимого мира:
```text
проверить active/visible state
-> выбрать LOD по расстоянию и настройкам
-> получить node matrices из animation state
-> выбрать slot для каждого node/group
-> преобразовать bounds в world space
-> выполнить culling
-> добавить batches в подходящую render queue
```
Material/texture resolve желательно выполнять после visibility и slot
selection, чтобы невидимые объекты не меняли порядок обращений к caches и не
создавали лишние side effects. Невидимость объекта и отсутствие slot являются
разными причинами пропуска и диагностируются отдельно.
Подготовленный draw item содержит:
```text
node world matrix
batch flags and index range
WEAR material handle
MAT0 active phase and coefficients
texture handle
optional lightmap handle
render phase and sorting key
legacy pipeline state
```
Draw item должен ссылаться на immutable данные кадра. Изменение phase или
texture cache посреди прохода не должно менять уже собранную очередь.
Согласованная декомпозиция внутренних render phases:
1. подготовка frame state, camera и viewport;
2. непрозрачный terrain;
3. непрозрачные object batches;
4. lightmap и дополнительные material passes;
5. projected primitives и тени;
6. alpha-tested geometry;
7. transparent objects и FX в сортировочных слоях;
8. atmosphere, sun, flare и weather;
9. renderer completion boundary;
10. end-of-render callbacks;
11. shell/UI и post-render state.
Точный взаимный порядок пунктов 4-8 и связь completion slot с физическим
DirectDraw flip/present требуют dynamic capture. Сортировка внутри каждой фазы
должна быть стабильной: для opaque первичен pipeline/material key, для
transparent -- distance layer и depth order, затем stable insertion ID.
Геометрический draw использует streams type 3/4/5, optional streams, index
buffer type 6, `base_vertex`, `index_start` и `index_count`. Матрица узла
устанавливается как world transform, затем CShade привязывает texture stages и
fixed-function state.
```c
set_world_matrix(item.node_world);
bind_vertex_streams(model.streams);
bind_index_buffer(model.indices);
apply_legacy_state(item.pipeline);
bind_texture(0, item.texture);
bind_texture(1, item.lightmap);
draw_indexed(item.batch.base_vertex,
item.batch.index_start,
item.batch.index_count);
```
После последнего world pass renderer закрывает сцену и выводит back buffer.
World3D снимает `in_render`, восстанавливает временный viewport state и вызывает
`on_end_render` у active objects. Только после этого допустимо освобождать
temporary vertex buffers или заменять render representation. UI/shell
обслуживается верхним уровнем после возврата из world-render path; для
диагностики полезно уметь сохранять world-only command list и финальный
framebuffer отдельно.
## Проверки паритета
Главные риски совпадения кадра:
- x87 extended precision и правила округления;
- различия scalar/SIMD slots `g_FastProc`;
- порядок objects, batches и transparent primitives;
- depth write/test, cull, alpha test и blend transitions;
- mip-skip, palette и `Page` coordinates;
- material fallback и выбор phase;
- последовательность RNG для FX и atmosphere;
- capability fallback конкретного устройства;
- quantization времени и дополнительный simulation step;
- eager/lazy resource resolve и cache side effects.
Минимальный deterministic frame capture должен включать camera state, viewport,
visible object IDs, выбранные LOD/group/slot, draw-item list, material и texture
handles, pipeline keys, matrices, render phase, sort key, причины culling и
hashes промежуточных buffers. Без такой трассировки нельзя уверенно отделить
ошибку формата MSH от ошибки state machine renderer-а или сортировки.
Связанные справочные страницы с таблицами форматов: [MSH](../reference/msh.md),
[materials](../reference/materials.md), [Texm](../reference/texm.md) и
[render frame](../reference/render-frame.md).
+769
View File
@@ -0,0 +1,769 @@
# VI. Поведение, управление, звук и сеть
Шестой том описывает подсистемы, которые превращают загруженный мир в
реагирующую игру: AI, Behavior, Wizard, Control, ввод, камеру, звук и сеть.
Эти области нельзя восстанавливать только по структуре файлов. Для них важны
порядок кадра, ownership объектов, timing событий и доказуемые границы между
решением, движением, presentation и транспортом.
Ключевой принцип: reader compatibility не равна gameplay compatibility.
Корректно разобранный ресурс ещё не доказывает, что runtime выбирает ту же
цель, строит тот же маршрут, применяет ту же collision correction, создаёт тот
же sound event или отправляет тот же network payload. Поэтому все утверждения
ниже разделяют подтверждённую структуру, восстановленный архитектурный
контракт и открытые участки, требующие динамической трассировки.
```text
AI / mission script
-> стратегическая цель, условия, команды миссии
Behavior
-> состояние объекта, target, global/local path
Wizard
-> локальная коррекция траектории
Control
-> physical step, collision proxy, итоговый transform
World3D
-> очередь событий, ownership, deferred deletion
Render / Sound / Net
-> представление, listener, mirrors и сообщения
```
Связанные главы: [мир и миссии](04-world.md), [геометрия и рендер](05-render.md)
и справочный [render frame](../reference/render-frame.md).
## AI, Behavior и Wizard
Iron3D разделяет стратегическое принятие решений, поведение конкретного объекта
и локальную коррекцию движения. Это разделение должно сохраниться в новой
реализации: стратегический AI не меняет transform напрямую, а collision manager
не выбирает игровую цель.
```text
ai.dll / SuperAI
-> цель клана, миссии и группы
Behavior.dll
-> состояние юнита, target, global path, local corridor
Wizard.dll
-> ближайшая допустимая траектория
Control.dll
-> физическое движение и столкновения
```
### Behavior
`CreateBehaviour` создаёт controller для отдельного игрового объекта.
`CreateDistributor` восстановлен по consumers как посредник распределения
команд или ресурсов; это высокоуверенный архитектурный вывод, а не доказанное
имя внутреннего класса. Behavior получает `IArealMap` через AI/клановый
контекст, ведёт radar/target state, строит global path, превращает его в local
corridor и передаёт движение Wizard.
Ошибочные состояния проверяются явно:
1. отсутствует system map;
2. отсутствует terrain interface;
3. active behavior не имеет `IArealMap`;
4. объект попал в non-reachable area;
5. объект пытается выйти из non-walkable area;
6. path generator вошёл в infinite cycle.
Эти случаи являются fatal или diagnostic conditions. Совместимая реализация не
должна тихо исправлять их teleport-ом, потому что такое исправление скрывает
ошибку areal graph, terrain query или state machine.
### Параметры Behavior.ini
Подтверждены настройки:
```text
PathFind_BuildingHitDist
PathFind_BuildingNearestDist
PathFind_NearBuildSpeedPercent
PathFind_CorridorRadius
PathFind_NearDoorCoeff
PathFind_fStepOffBuilding
PathFind_MaxAccel
PathFind_MaxRotation
PathFind_fStepDist
PathFind_MinPointInTrajectory
Network_ResourceTransferMaxDelay
```
Они задают геометрию corridor, дистанции реакции на здания, снижение скорости
возле препятствий, пределы ускорения и поворота, дискретизацию trajectory и
сетевой timeout передачи ресурсов. Значения читаются как runtime-конфигурация,
а не компилируются в код. Parser должен поддерживать комментарии `//`, пробелы
вокруг `=` и CRLF.
Файл также содержит logging/debug switches: `Behavior.log`, уровни ошибок,
show vectors и z-buffer debug. Эти переключатели полезны не только для
совместимости, но и как модель современных trace flags.
### Wizard
Wizard получает желаемое направление и corridor, анализирует ближайшие
ограничения и выдаёт скорректированную локальную траекторию. Behavior может
очищать её через `ClearWizardPath` при смене цели, повреждении global path или
переходе объекта в неактивное состояние.
Нужно различать четыре уровня движения:
- **global path** -- последовательность areals;
- **local path** -- точки или сегменты внутри corridor;
- **wizard path** -- краткосрочное движение с учётом ближайших препятствий;
- **physical step** -- фактически разрешённое Control перемещение.
Хранение всего маршрута одним массивом лишает систему возможности локально
обойти препятствие без полного повторного поиска. Граница Behavior/Wizard
существует именно для того, чтобы краткосрочная геометрическая коррекция не
ломала стратегический path state.
### SuperAI и миссионные сценарии
`CreateSuperAI` создаёт центральный controller клана; `GetSuperAI` возвращает
его. AI загружает файлы из `MISSIONS\SCRIPTS\`, проверяет версию и пишет ошибки
в `ai.log`. Несовпадение версии является отдельной ошибкой, а не неизвестной
командой.
Сценарный корпус содержит binary `.scr`, formula exports `.fml`, таблицу
переменных `varset.var` и `.trf`-данные. `.scr` хранит именованные секции и
события, например `Init`, `Mission`, `Problems0`, `Fort_Task_Complete` и
`Hero_Teleported`, вместе с числовыми ссылками на compiled instructions.
`.fml` является текстовым экспортом formula set. `varset.var` декларативно
описывает типы, defaults, ranges и строки через макросоподобные формы
`VAR(...)` и `STRING(...)`.
Безопасная runtime-модель:
```text
load script bundle
-> validate version and symbol tables
-> create global/formula variables
-> bind named events to instruction offsets
-> instantiate SuperAI per clan
-> dispatch MISSION_START and object events
-> update timers/conditions each simulation tick
-> enqueue game commands through World3D/Behavior
```
Сценарий не должен владеть игровым объектом напрямую. Он хранит logical/object
IDs и отправляет команды через игровые interfaces, чтобы удаление объекта или
сетевой mirror не оставили dangling pointer.
Полная grammar compiled instructions и точное значение всех opcodes остаются
открытым направлением. До появления decompiler-а `.scr` binary body сохраняется
lossless, а доказанные symbol/event tables документируются отдельно.
### TRF и preload-данные
TRF-файлы проходят структурный разбор. `auto.trf`, `data.trf` и tutorial
variants имеют сигнатуру [NRes](../reference/nres.md) и содержат большие
таблицы имён игровых прототипов: оружия, башен, сооружений и других объектов.
Также найдены preload-записи, ANI и SKE resources.
По содержимому, порядку загрузки и consumers TRF с высокой вероятностью
предоставляет AI/сценарному слою заранее подготовленную таблицу типов и
связанных данных. Framing и имена подтверждены corpus-ом, но полная семантика
каждой TRF-записи ещё не закрыта. Имена должны разрешаться через тот же
resource registry, что и миссионные объекты.
### Стабильность AI-слоя
`ai.dll`, `Behavior.dll` и `Wizard.dll` побайтно идентичны в Частях 1 и 2. Это
подтверждает, что разделение SuperAI -> Behavior -> Wizard и бинарная
реализация этих трёх уровней не менялись.
Сценарный корпус:
```text
Часть 1: 58 SCR, 58 FML, 29 TRF
Часть 2: 59 SCR, 59 FML, 44 TRF
```
Все TRF являются структурно валидными NRes. Неизменность DLL усиливает вывод о
стабильной VM, но не закрывает instruction grammar `.scr`: для неё нужен
dispatcher/jump-table decompiler. Дополнительные сценарные данные расширяют
differential corpus, но не заменяют анализ VM.
## Control, физика и коллизии
Control превращает желаемое движение в физически допустимое изменение
состояния. World3D владеет жизненным циклом объекта; Terrain предоставляет
поверхность и world queries; Behavior/Wizard задают намерение; Control создаёт
physical controller и collision representation.
Публичная поверхность:
```text
InitializeSettings
LoadControlSystem
LoadPhysicalModel
CreateCollManager
CreateCollObject
```
Модуль импортирует World3D queue/object functions, `Terrain::GetWorld`, часы,
тригонометрию и `g_FastProc`. Это подтверждает его положение между gameplay
object и геометрией мира.
### Control system и physical model
`LoadControlSystem` загружает настройки controller-а: ограничения скорости,
ускорения, поворота и режимы управления. `LoadPhysicalModel` загружает форму и
параметры, используемые для столкновений. Visible MSH не обязан совпадать с
collision representation: для физики часто нужна более простая и устойчивая
форма.
Практичная runtime-модель:
```c
struct PhysicalState {
Transform transform;
Vec3 linear_velocity;
Vec3 angular_velocity;
float requested_speed;
float requested_turn;
uint32_t flags;
};
struct CollisionProxy {
ObjectId owner;
ShapeSet shapes;
Bounds broad_phase_bounds;
uint32_t category_mask;
};
```
Названия полей здесь описывают контракт совместимой реализации, а не точный
layout исходного C++-объекта.
### Collision pipeline
Один расчётный шаг удобно разделить так:
1. controller получает желаемые `speed`/`turn` от Behavior или manual input;
2. вычисляет кандидатный transform на основе `dt`;
3. обновляет broad-phase bounds collision object;
4. collision manager находит потенциальные пары и terrain candidates;
5. narrow phase вычисляет контакт или допустимый остаток перемещения;
6. physical state корректируется;
7. World3D получает итоговый transform;
8. событие `GMSG_COLLISION_DETECTED` отправляется в согласованной фазе.
Позиция collision event после narrow phase является рекомендуемой фазой
реализации и согласуется с назначением сообщения, но точный call-site
относительно всех correction steps требует динамической трассировки Control.
Удаление объекта из обработчика остаётся отложенным по правилам World3D.
Collision manager не должен хранить прямую незащищённую ссылку на объект,
который уже pending-delete.
### CTLD и physical resources
Реестр прототипов ссылается на `*.ctl`, `*.cpt` и связанные control resources.
В Части 1 структурно проверен 531 CTLD payload без ошибок. Размеры и пять
внутренних счётчиков образуют множество вариантов: наиболее частый размер
392 байта с pattern `(0,0,0,1,0)`, но встречаются блоки от примерно 212 до
1868 байт и более сложные комбинации.
CTLD является составным count-driven форматом, а не фиксированной struct.
Parser должен:
- прочитать prefix и все счётчики с проверкой переполнения;
- вычислить границы секций по их counts;
- сохранять неизвестные records в typed raw containers;
- требовать точного завершения payload;
- не использовать размер одного популярного варианта как универсальный layout.
Полная предметная семантика всех секций ещё не доказана, но существующие файлы
можно безопасно читать, индексировать и сохранять.
### Terrain queries и movement handoff
Control получает world-interface Terrain и использует поверхность, faces и
ускорители для высоты, нормали и пересечений. Навигационный маршрут сообщает,
куда двигаться, но итоговый transform определяется по физической поверхности.
При переходе через склон controller должен согласовать горизонтальный шаг,
высоту и ориентацию с terrain normal.
Порядок операций должен быть детерминированным: пары collision objects
сортируются по стабильному ID, contacts обрабатываются в фиксированной
последовательности, а интеграция использует одну политику `dt` и округления.
Иначе одинаковая миссия постепенно расходится даже без сети.
### Различия Control в Части 2
`Control.dll` пересобрана при неизменных размере, imports и пяти именах/ordinals
exports; RVA всех пяти exports изменились. Форматы и cross-module boundary
сохранились, но точное physical/collision behavior нельзя считать побайтно тем
же.
CTLD-корпус расширен с 531 до 623 payload. Новых framing errors не найдено;
большинство общих CTLD изменено вместе с переработанными моделями. Это
подтверждает count-driven parser, но не закрывает предметную семантику shape
records и contact solver.
Differential test обеих частей должен воспроизводить движение без препятствий,
slope following, pair collision, timing collision event и удаление объекта в
callback. Сравниваются transforms и contact events по tick, а не только факт
успешной загрузки.
## Ввод, камера и управление
World3D нормализует клавиатуру, мышь и joystick в общие scan codes и manual
commands. Win32 message handler вызывает `UpdateManualEventsList`; перед
обработкой новой порции сообщений основной цикл вызывает
`ClearManualEventsList`. Снимок клавиатуры очищается отдельно через
`stdClearKeyboard`.
Публичная поверхность включает `WinMsg2ScanCode`, converters для
keyboard/mouse/joystick/predicate, `ScanCode2Str`, `ManualCommand2Str`,
`stdIsKeyPressed`, lock/unlock keyboard и чтение mouse shift. Это позволяет
хранить конфигурацию управления независимо от физического устройства.
### Event, state и axis
Ввод имеет минимум три семантики:
- **edge event** -- нажатие или отпускание в текущей порции сообщений;
- **held state** -- клавиша остаётся нажатой между кадрами;
- **analog value** -- смещение мыши или положение joystick axis.
Manual command дополняет источник коэффициентом, режимом wrap, dead
zone/threshold и временной характеристикой. Строки camera bindings показывают
команды `MCMD_STATE`, `MCMD_ANGLE_X`, `MCMD_ANGLE_Y`, режимы `MAN_WRAP` и
`MAN_NOTWRAP`, а также параметры ускорения в миллисекундах.
Simulation читает подготовленный input snapshot. Renderer не должен
самостоятельно опрашивать OS, иначе одно и то же нажатие будет зависеть от
частоты кадров.
### Joystick через DirectInput
`Joystick.dll` экспортирует:
```text
QueryJoy
CreateJoy
ReleaseJoy
SetJoyRange
PeekJoyMessage
GetJoyCaps
```
`QueryJoy` обнаруживает устройство, `CreateJoy` получает интерфейс DirectInput,
`SetJoyRange` нормализует оси в диапазон движка, `PeekJoyMessage` выдаёт
очередное унифицированное событие.
При потере устройства чтение может вернуть ошибку acquired state. Интерфейс
следует повторно получить, очистить устаревшее состояние и продолжить.
Hot-unplug не должен оставлять последнюю ось навсегда отклонённой.
`GetInstalledJoyNames` и `SetActiveJoy` в World3D связывают device list с
game-facing выбором.
### Два camera interface
World3D предоставляет `stdSetCurrentCamera`/`stdGetCurrentCamera`: это камера
как часть игрового состояния. Terrain имеет
`stdSetCurrentCamera2`/`stdGetCurrentCamera2`: concrete camera, которую world
renderer использует для matrices, viewport и visibility.
`LoadCamera` экспортирован обоими модулями. По call graph World3D-вариант
играет роль component bridge, а Terrain-вариант связан с concrete
camera/world implementation. Это архитектурный вывод: точные class names и
layout не восстановлены.
Минимальные данные камеры:
```text
world position and orientation
view matrix
projection parameters / field of view
near and far planes
viewport rectangle
camera mode and target object
manual angles/state
```
Такая граница позволяет game code работать с абстрактной камерой, не зная
внутреннего renderer representation.
### Camera commands и порядок кадра
Подтверждены команды `CMD_CAMERA_LEFT`, `CMD_CAMERA_RIGHT`, `CMD_CAMERA_UP`,
`CMD_CAMERA_DOWN`, `CMD_CAMERA_CENTER`, `CMD_CAMERA_INFRARED`, а также
spotlight и внешние/миссионные camera modes. Горизонтальный угол использует
wrap, вертикальный -- ограниченный диапазон. Center плавно возвращает обе оси к
заданному значению.
Порядок кадра:
1. собрать manual events;
2. обновить camera controller во время calculation;
3. вычислить итоговый transform и ограничения;
4. перед render установить current camera;
5. передать её Terrain и sound listener;
6. после кадра сохранить mode-specific state.
Camera smoothing должно использовать игровое время или специально
подтверждённые часы. Привязка к render delta делает управление разным при 30 и
144 FPS.
## Звуковая подсистема
Ngi32 создаёт низкоуровневый DirectSound backend. `services.dll` публикует
`ISoundServer`. Game, Terrain и FX работают уже через эти интерфейсы:
воспроизводят 2D/3D sources, меняют volume и связывают listener с camera.
Публичные функции Ngi32:
```text
niCreate3DSound
niGet3DSound
niGet3DSoundCaps
niMuteSound
```
Backend динамически вызывает `DirectSoundEnumerateA` и `DirectSoundCreate`;
параметр `DisableDSound` может полностью отключить этот путь.
### Устройство и capabilities
Конфигурация учитывает `3D Sound`, качество, reverse sound, частоту buffer,
режим постоянного воспроизведения и автоматический выбор лучшего устройства.
Эти значения преобразуются во внутренний capability/profile object до создания
sources.
Код содержит отдельный no-device state и строку `3D Sound was not initialized`.
Отсутствие 3D sound обрабатывается отдельно от ошибок simulation/resources.
Новый runtime не должен позволять отсутствию звука разрушать simulation и
обязан возвращать звуковым командам явный no-device result.
Общий sound object разделяется между подсистемами и использует счётчик
владельцев. Закрывать DirectSound следует после остановки всех sources и
atmosphere/FX managers.
### Sound resources и SWAV
Основная библиотека называется `sounds.lib`; `mission.cfg` также создаёт
именованные sound resources и variations. Legacy API `rsLoadWave` загружает
waveform из archive. Импорт `MSACM32` подтверждает путь преобразования сжатых
wave-данных в формат playback buffer.
Resource identity состоит из library и name. Один sound asset может иметь
несколько runtime sources с различными position, volume, pitch/flags и временем
запуска. Поэтому кэшировать следует decoded sample/buffer, а source object
создавать на событие.
FX opcode 2 хранит `archive[32] + name[32]` и обычно создаёт sound command.
Atmosphere использует отдельные loop/variation sources, например rain
background. Миссионный слой содержит voice events для завершения или провала
задания.
Проверенный SWAV-корпус:
```text
Часть 1: 399 — 306 MS ADPCM, 93 PCM
Часть 2: 540 — 446 MS ADPCM, 93 PCM, 1 empty entry
```
Все непустые записи имеют RIFF/WAVE framing и частоту 22 050 Hz. В Части 2
entry `ALIEN_ME.WAV` имеет размер 0. Это присутствующий archive key без
decodable waveform.
Sound loader должен различать:
- `entry_missing`;
- `entry_empty`;
- `wave_invalid`;
- `decoded_sample`.
Нулевой payload не передаётся RIFF parser-у и не должен приводить к чтению
header за границей.
### 3D listener и sources
Перед world traversal `stdRenderGame` обновляет listener из camera transform.
Listener содержит position, orientation и, при наличии, velocity. Source
содержит world position и параметры затухания. Spatialization выполняется
backend-ом либо совместимой программной моделью.
```text
camera transform
-> listener position/front/up
object or effect transform
-> source position
sample + source parameters
-> DirectSound 3D buffer
```
Прямо подтверждено обновление listener в начале `stdRenderGame`, до world
traversal. Sound events могут создаваться и в calculation/FX path, поэтому
нельзя утверждать, что listener предшествует созданию каждого source. Важно,
что spatial backend получает camera state текущего отображаемого кадра до
завершения его обработки. Перенос listener update после world render создаст
как минимум однокадровое рассогласование presentation.
### Громкость, mute и CD-аудио
`iron3d.dll` применяет отдельные настройки эффектов и CD sound. Параметр
`FORCE_CD_SOUND` меняет политику выбора музыкального источника. `niMuteSound`
должен временно остановить вывод без разрушения sample cache и logical playback
state.
В новой реализации полезно разделить buses: master, effects, ambient, voice и
music/CD. Это проектное решение совместимого backend-а, а не доказанный layout
оригинального mixer-а. Оно позволяет применять старые коэффициенты, не
переписывая individual source volume.
### Граница service layer
`Ngi32.dll` с DirectSound/backend code не изменилась между Частями 1 и 2, но
`services.dll` пересобрана и уменьшилась на 4 096 байт. Поэтому low-level
decoder/device path подтверждается одной машинной реализацией, а service
lifecycle, GUI/audio wiring и defaults требуют раздельной трассировки обеих
частей.
## Сетевая подсистема
Net инкапсулирует DirectPlay4A и lobby/service-provider API. World3D строит над
транспортом player identity, mirror objects и игровые сообщения. Эти уровни
следует разделять: DirectPlay отвечает за доставку bytes между players,
World3D -- за смысл сообщения и владение объектом.
Application GUID:
```text
{3C1D1F01-A870-11D1-8400-000021B14415}
```
Он передаётся network instance и service layer. Экземпляры с другим GUID не
принадлежат одному логическому приложению.
### Lifecycle соединения
Публичные функции Net покрывают полный цикл:
```text
CreateNetworkInstance
-> select/use service provider
-> setup connection
-> enumerate or create session
-> join/create session
-> create local player
-> send/receive messages and player data
-> destroy player
-> close session
-> close connection
```
Поддерживаются providers эпохи DirectPlay: TCP/IP, IPX и modem/lobby варианты,
если они установлены в системе. Функции явно проверяют, что DirectPlay enabled
до enumeration, session и player operations. Неверный порядок вызовов должен
возвращать понятную ошибку, а не разыменовывать пустой interface.
### Sessions, players и адреса
Net предоставляет enumeration service providers и sessions, выбор host/join,
player name/password/data, latency, максимальный размер сообщения, размер
очереди, server player info и provider address. Lobby launch обрабатывается
отдельной веткой.
Внутренняя модель должна хранить как минимум:
```c
struct NetPlayer {
TransportPlayerId transport_id;
uint16_t game_player_number;
string name;
RawBytes player_data;
bool is_local;
bool is_host;
};
```
Transport ID нельзя использовать как постоянный `ObjectId`. NetWatcher связывает
временный DirectPlay identifier с номером игрока и World3D entities.
### Игровые сообщения World3D
Подтверждённые имена message surface:
```text
GMSG_CREATE_REMOTE_PLAYER
GMSG_APPEND_RESOURCE
GMSG_CHANGE_OBJECT_OWNER
GMSG_SET_PLAYER_DATA
GMSG_MISSION_DATA_PATH
GMSG_TAKE_OBJECT
GMSG_TEXT_FOR_PLAYER
GMSG_SYNC_STATE
GMSG_CREATE_MIRROR
GMSG_PAUSE_REMOTE_PLAYER
GMSG_CONFIRM_PLAYER_DATA
GMSG_KILL_PLAYER
SYSMSG_SET_TIME
SYSMSG_SET_PLAYER_NUMBER
GMSG_END_MESSAGE_SEQ
GMSG_REMOVE_RESOURCE
```
`GMSG_COLLISION_DETECTED` относится к общей очереди, но не обязательно
передаётся по сети. Message ID, payload size и delivery policy должны быть
частью явной schema. Нельзя сериализовать C++ pointers или native padding.
### Mirror objects и ownership
Удалённо принадлежащий объект представлен local mirror instance. Он участвует в
рендере и spatial queries, но authority над его созданием, ключевыми properties
и удалением находится у owner player. Сообщение смены владельца обновляет эту
границу; оно не должно создавать второй объект с тем же ID.
Типовой путь:
```text
remote create message
-> validate player and ObjectId
-> resolve prototype/resources
-> CreateMirrorObject
-> apply initial state
-> AddMirrorObjectToGame
-> subsequent sync messages update mirror
```
При потере player NetWatcher инициирует предписанное удаление или transfer
ownership через World3D queue. Мгновенное освобождение во время receive callback
запрещено по тем же причинам, что и в calculation pass.
### Сжатие и wire compatibility
`netZipData` и `netUnZipData` образуют встроенный слой упаковки payload. Он
находится выше транспорта: переход с DirectPlay на UDP/ENet не отменяет
необходимость воспроизводить формат упакованного сообщения, если требуется
соединение с оригинальной игрой.
Полный wire schema, framing и алгоритм сжатия пока не доказаны packet
capture-ом. Поэтому нужны два режима:
- **native compatibility** -- отдельный adapter, реализуемый после трассировки
оригинальных packets;
- **modern multiplayer** -- новая versioned protocol schema, использующая ту же
game-message семантику, но не заявляющая совместимость с DirectPlay client.
Эти режимы нельзя незаметно смешивать. До доказательства native wire
compatibility современный transport должен быть versioned и отделён от слоя,
который претендует на совместимость с оригинальным клиентом.
### Стабильность сетевого слоя
`Net.dll` и `World3D.dll` побайтно идентичны в обеих частях. Application GUID,
DirectPlay wrapper, mirror-object API и World3D message surface относятся к
одной машинной реализации.
Это подтверждает отсутствие отдельной сетевой реализации для Части 2, но не
закрывает wire schema: без packet/send-receive capture по-прежнему неизвестны
точное framing, reliability flags, payload layouts и алгоритм `netZipData` для
native interoperability.
Для binary regression достаточно одного профиля неизменённых DLL, но message
captures должны включать контент обеих частей, потому что prototype/resource IDs
и mission data различаются.
## Контракты реализации
Совместимая реализация должна фиксировать не только результат, но и момент его
появления в кадре. Для Behavior, Control, input, sound и network особенно важны
tick boundaries: одна и та же команда, применённая на один tick раньше или
позже, меняет дальнейшую симуляцию.
### Trace-события
Минимальный trace для этого тома:
- input snapshot: edge events, held state, analog values;
- camera state: mode, target, angles, matrices, viewport;
- Behavior: target, areal, global path revision, local corridor;
- Wizard: requested vector, constraints, wizard path;
- Control: candidate transform, contacts, correction, final transform;
- World3D queue: message name, ObjectId, dispatch phase, deferred deletion;
- sound: sample key, source owner, position, event tick, listener state;
- network: player mapping, message ID, payload length, delivery policy.
Для рендера это связывается с [render frame](../reference/render-frame.md):
camera и listener должны попадать в trace до world traversal, иначе нельзя
отделить ошибку presentation от ошибки управления.
### Проверки Behavior и сценариев
- script version mismatch даёт отдельную ошибку;
- event table читается lossless;
- VM body сохраняется без потери неизвестных bytes;
- отсутствующий `IArealMap` не замалчивается;
- non-walkable/non-reachable states дают diagnostic condition;
- одинаковый input log воспроизводит одинаковый sequence Behavior commands;
- resource names из TRF разрешаются через общий registry.
### Проверки Control
- движение без препятствий;
- slope/terrain-following;
- симметричные pair-collision tests с переставленными IDs;
- contact event отправляется один раз в предписанной фазе;
- удаление объекта в collision callback безопасно;
- replay одинакового input log даёт одинаковые transforms;
- collision proxy перестраивается после смены component/model state.
### Проверки input и камеры
- edge event не повторяется как held state;
- mouse/joystick axis сбрасывается по правилам snapshot;
- hot-unplug joystick не оставляет старое отклонение;
- camera horizontal angle wraps, vertical angle clamps;
- center command использует подтверждённое время, а не render FPS;
- Terrain и sound получают одну и ту же camera frame.
### Проверки звука
- backend может отсутствовать без нарушения simulation;
- один decoded sample переиспользуется несколькими sources;
- `entry_missing`, `entry_empty` и `wave_invalid` различаются;
- listener совпадает с camera frame;
- loop source корректно переживает pause/resume;
- mute не сбрасывает position и time;
- missing sound resource содержит полную диагностическую цепочку;
- deterministic test сравнивает список sound events, а не waveform устройства.
### Проверки сети
- нельзя создавать queue с активной сетью и нулевым player ID;
- session/player operations до enable/setup возвращают ошибку;
- сообщения проверяют длину до чтения payload;
- sequence/end markers обрабатываются в стабильном порядке;
- duplicate create mirror не создаёт второй instance;
- ownership change атомарно обновляет routing;
- pause/time messages применяются в одной simulation boundary;
- resource transfer имеет timeout `Network_ResourceTransferMaxDelay`;
- disconnect не оставляет objects с несуществующим owner;
- replay записанного message log даёт одинаковое World3D state.
`resnet.log` и `NetWatch.log` следует поддерживать как отдельные каналы: первый
относится к transport/resource exchange, второй -- к связи players и game
objects.
## Границы знания
Подтверждены внешние interfaces, часть runtime order, значимые строки,
конфигурационные параметры, corpus-level counts и стабильность ряда DLL между
двумя частями. Открытыми остаются:
- instruction grammar `.scr` и semantics всех VM opcodes;
- точная семантика всех TRF-записей;
- полный layout CTLD shape records;
- contact solver и порядок всех correction steps;
- class layout камер, контроллеров, sound service и network watcher;
- DirectPlay wire framing, reliability flags и payload schema;
- алгоритм `netZipData`/`netUnZipData`;
- точные defaults service layer там, где DLL пересобраны.
Эти границы должны оставаться видимыми в документации и тестах. Если новая
реализация вводит удобный современный abstraction layer, он обязан быть
отделён от утверждений о native compatibility и покрыт отдельным trace.
+674
View File
@@ -0,0 +1,674 @@
# VII. Руководство по полной реализации
Этот том описывает инженерный путь к совместимому движку FParkan. Он опирается
на доказанные форматы и runtime-контракты, но не требует повторять физическое
деление оригинала на пятнадцать DLL. Повторить нужно наблюдаемое поведение:
форматы, имена, fallback, object IDs, порядок событий, численную политику,
границы кадра, сохранения и воспроизводимость прохождения.
Предложенные ниже modules, handles, snapshots, queues и scheduler phases являются
целевой архитектурой новой реализации, а не восстановленным внутренним layout
оригинального Iron3D. Главная практическая цель: запускаться из неизменённого
оригинального каталога игры, проходить corpus gates для демоверсии, Части 1 и
Части 2, а затем измеримо двигаться от archive compatibility к полной игровой
совместимости.
## Целевая архитектура
Практичная форма новой реализации -- модульный монолит с узкими интерфейсами и
отдельными platform adapters. Внутренние границы должны соответствовать ролям
Iron3D, а не обязательно его DLL. Это упрощает перенос на современные платформы
и оставляет возможность поддерживать разные compatibility profiles для разных
сборок данных.
```text
application запуск, окно, конфигурация, shutdown
platform filesystem, clocks, input, threads, dynamic libraries
resources NRes, RsLi, paths, archives, cache and diagnostics
assets MSH, WEAR, MAT0, Texm, FXID and auxiliary formats
mission TMA, unit DAT, prototype graph, scenario data
world ObjectId, queue, lifecycle, time, messages, mirrors
terrain Land.msh, Land.map, surface and spatial queries
navigation areals, graph search, corridors
behavior unit state machines, target and path requests
physics control systems, collision proxies and contacts
animation pose sampling, hierarchy and blending
audio sample cache, sources, listener and buses
render immutable frame contracts and modern backend
network game message schema plus transport adapters
tools validators, extractors, viewers, captures and editors
```
Каждый модуль зависит от нижележащих интерфейсов, а не от concrete managers.
Behavior видит `INavigation` и `IPhysicsCommandSink`, но не включает headers
renderer-а. Render получает immutable snapshot, а не mutable world. Network
receive не меняет мир напрямую: validated messages попадают в очередь следующей
calculation boundary.
### Центральные идентичности
Resource identity хранит и исходное написание, и нормализованный ASCII-key для
поиска:
```c
struct ResourceKey {
NormalizedRelativePath archive;
FixedAsciiName name;
uint32_t type_id;
};
```
Normalization сохраняет исходную строку для diagnostics и roundtrip, а отдельный
ASCII-casefold key используется только для lookup. Эта граница важна для
архивов [NRes](../reference/nres.md), таблиц [RsLi](../reference/rsli.md),
prototype references и fallback-путей материалов.
Object identity разделяет внутреннюю защиту от dangling references и исходную
сетевую/script-семантику:
```c
struct ObjectHandle { uint32_t generation; uint32_t slot; };
struct OriginalObjectId { uint32_t raw; };
```
`ObjectHandle` нужен для безопасного внутреннего владения, deferred deletion и
weak references. `OriginalObjectId` сохраняет наблюдаемую семантику исходной
игры: scripts, mirrors, network messages и savegame references должны видеть
логический ID, а не адрес объекта или номер slot в новом allocator-е.
Frame snapshot отделяет simulation от render. Simulation пишет mutable state;
renderer читает опубликованное состояние или строго ограниченную фазу
`in_render`. Deferred deletion применяется между фазами, а не во время traversal.
Командный контур renderer-а должен сверяться с [описанием кадра](../reference/render-frame.md)
до pixel comparison.
### Владение ресурсами
Ресурс проходит несколько уровней:
```text
ArchiveHandle -> EntryView -> DecodedBlob -> ParsedAsset -> RuntimeResource
```
`EntryView` ссылается на metadata архива, `DecodedBlob` владеет подготовленными
bytes, `ParsedAsset` является CPU-представлением, `RuntimeResource` может
дополнительно владеть GPU/audio objects. Eviction верхнего уровня не закрывает
архив, если он ещё нужен другому entry. Ссылки идут вниз только через явные
handles.
Для shared objects допустимы reference counting или generation handles.
Intrusive refcount нужен только в ABI-shim; внутренний современный код
предпочтительно держит понятное владение и weak handles. Архивы, decoded blobs,
CPU assets и GPU resources имеют отдельные бюджеты и отдельные diagnostics.
### Backend adapters
Render, audio, input и network получают отдельные adapters. Compatibility state
живёт вне Vulkan, D3D11 или Metal backend; DirectPlay compatibility живёт
отдельно от modern transport. Так можно заменить платформу, не меняя форматы,
игровую семантику и regression corpus.
Backend adapter не должен быть местом, где исправляются данные. Если
[MSH](../reference/msh.md), [MAT0](../reference/materials.md) или
[Texm](../reference/texm.md) требуют fallback, это фиксируется в asset/runtime
слое и попадает в trace. Backend получает уже выбранные resources, states и
draw items.
### Scheduler phases
```text
collect_platform_events
build_input_snapshot
advance_game_clock
calculate_world_queue
apply_deferred_operations
update_navigation_physics_animation_fx
publish_render_snapshot
render_world
render_ui
end_frame_callbacks
maintenance_and_eviction
```
Фазы имеют стабильный порядок и запрещённые операции. Registry mutation
запрещена во время world traversal, GPU upload не изменяет simulation state, а
maintenance не влияет на gameplay. Script timers, material animation и FX
lifetime относятся к game time, если обратное не доказано.
Сначала реализуется однопоточный эталон. Параллелизм добавляется только внутри
фаз с детерминированным merge: decoding независимых assets, culling chunks или
подготовка immutable draw items. Это снижает риск скрытых race conditions и
расхождений replay.
### Структурированные ошибки
Каждая ошибка должна содержать фазу, путь, archive entry, object/prototype key,
offset и цепочку причины.
```text
MissionLoadError
mission: Campaign.00/Mission.02
object: 17
resource_name: UNITS/.../unit.dat
component: e_tur_...
prototype: objects.rlb::e_tur_...
cause: model archive missing
```
Логическое отсутствие необязательного lightmap, отсутствующий entry в архиве,
неизвестное opaque поле, выход ссылки за диапазон и повреждённый offset имеют
разный severity и разные способы исправления. Ошибка данных должна быть
actionable chain, а не строка вида `failed to load resource`.
## Порядок работ
Движок строится от данных к поведению и от детерминированных CPU-компонентов к
аппаратным. Каждый этап заканчивается исполняемым инструментом и тестовым
критерием. Нельзя начинать полноценный gameplay, пока ресурсный граф и
model/material path не дают воспроизводимый результат.
### Этап 0. Corpus harness
- индексировать оригинальный каталог и вычислить hashes;
- реализовать bounded binary cursor и structured diagnostics;
- создать CLI для массового запуска parser-ов;
- сохранять JSON-отчёт с counts, variants, warnings и failures;
- зафиксировать демоверсию, Часть 1 и Часть 2 как независимые baselines.
Готовность: повторный запуск на каждом неизменённом каталоге даёт идентичный
отчёт. Любой parser умеет завершиться контролируемой ошибкой с offset и
контекстом, а не crash или allocation по непроверенному count.
### Этап 1. Архивы и пути
- реализовать strict/lossless [NRes](../reference/nres.md) reader/writer;
- реализовать [RsLi](../reference/rsli.md) mapping, table transform, lookup,
LZSS и Deflate;
- добавить адаптивный decoder для методов `0x080` и `0x0A0`;
- воспроизвести overlay и известные compatibility quirks;
- реализовать archive-handle cache и ASCII name policy.
Готовность: неизменённые архивы проходят byte-identical roundtrip; поиск всех
имён совпадает с каталогом; malformed corpus отклоняется без выхода за память.
NRes с ненулевым unindexed region обязательно остаётся regression case.
### Этап 2. Граф ресурсов
- разобрать `objects.rlb` и unit DAT;
- построить resolver прямой MSH, рекурсивного parent prototype через
`objects.rlb` и отдельного BASE payload;
- реализовать dependency graph с reachability от миссии;
- добавить parsers CTPT, NDPR и остальных служебных форматов в lossless-режиме;
- создать инспектор прототипа, показывающий все связанные ресурсы.
Готовность: 201 demo-объект раскрывается в 501 прототип. Затем все миссии
Частей 1 и 2 дают 4 701 и 5 845 prototype requests без failures. Недостижимые
отсутствующие ресурсы отмечаются отдельно от критических ошибок в reachable
graph.
### Этап 3. Статический asset viewer
- реализовать [MSH](../reference/msh.md) core streams, slots и batches;
- декодировать Texm во все подтверждённые pixel formats;
- разобрать WEAR и [MAT0](../reference/materials.md) с точными fallback;
- построить современный renderer compatibility layer;
- добавить wireframe, normals, bounds, LOD/group и material debug views.
Готовность: открываются 435/511 моделей, 518/631 textures и 905/1 127 materials
Частей 1/2; batch/index bounds не нарушаются; viewer показывает корректно
текстурированную статическую модель из исходного архива. Красивый viewer всё ещё
означает только asset compatibility, а не готовую игру.
### Этап 4. Анимация и эффекты
- реализовать MSH type 8/type 19 sampling и hierarchy;
- добавить x87-compatible reference path для чувствительных формул;
- реализовать material phase animation;
- разобрать FXID header/commands и runtime instances;
- сначала поддержать все opcodes, встречающиеся в корпусе, сохраняя raw body;
- добавить deterministic RNG stream и effect capture.
Готовность: frame-by-frame poses совпадают с golden reference своей части; все
923/1 065 FXID создаются без parser errors; перезапуск одинакового effect seed
даёт идентичный список emitted primitives.
### Этап 5. Карта и мир
- реализовать `Land.msh` и corrected `TerrainFace28` layout;
- построить terrain rendering и CPU surface queries;
- реализовать `Land.map`, cell grid и graph links;
- визуализировать areals и найденные маршруты;
- разобрать [TMA](../reference/tma.md) и выполнять staged mission loading;
- создать World3D queue, ObjectId и deferred deletion.
Готовность: 65 карт и 60 TMA Частей 1 и 2 загружаются до EOF; все areal links
валидны; objects появляются в правильных transforms; мир выдерживает расчётные
шаги без рендера.
### Этап 6. Gameplay controllers
- подключить input snapshot и camera controller;
- реализовать navigation corridor, Behavior state machine и Wizard boundary;
- создать physical controller и collision manager;
- загрузить control resources в lossless typed model;
- внедрить game time, pause, event queue и end-of-frame callbacks;
- подключить AI layer и symbol/event layer сценариев.
Готовность: юнит получает цель, строит маршрут, движется по terrain, реагирует
на collision и исполняет базовые миссионные события в детерминированном replay.
На этом этапе вводится differential branch для изменённых `AniMesh`, `Control` и
`Effect`; неизменённые DLL используют общий reference path.
### Этап 7. Полный кадр, звук и UI
- реализовать render phases, sorting, lighting, shadows и atmosphere;
- подключить 3D listener, sample cache, FX sounds и mission audio;
- воспроизвести shell/UI loading и post-world pass;
- добавить frame capture до UI и после UI;
- зафиксировать capability fallback profiles.
Готовность: миссия визуально и звуково проходима; каждый draw и sound event
имеет trace; одинаковый replay создаёт одинаковые command lists. На этом этапе
вводится differential branch для `iron3d` и `services`.
### Этап 8. Сеть, сохранения и динамическая совместимость
- реализовать modern transport над versioned game-message schema;
- отдельно исследовать DirectPlay wire и `netZipData` для native compatibility;
- добавить mirrors, ownership transfer и disconnect cleanup;
- восстановить save/campaign state и dispatcher;
- выполнить динамические captures оригинала для render states, script VM и
physics edge cases.
Готовность: одиночная кампания запускается из оригинального каталога,
сохраняется и продолжается; multiplayer replay согласован между peers; full
corpus не создаёт новых parser variants без явной регистрации.
## Тестовый контур
Совместимость нельзя подтвердить одним screenshot. Нужны тесты на уровне bytes,
структур, ссылок, simulation state, команд renderer-а и конечного изображения.
Каждый слой локализует свой класс ошибки.
```text
unit tests
-> parser/property tests
-> corpus validation
-> cross-resource integration
-> deterministic simulation replay
-> render/audio command captures
-> pixel and gameplay parity
```
Failure верхнего уровня всегда должен позволять спуститься к меньшему тесту и
понять причину.
### Unit, property и fuzz tests
Для каждого binary primitive проверяются little-endian чтение, bounded strings,
checked arithmetic и cursor boundaries. Для структур -- минимальный размер,
максимальные counts, пустые arrays, нулевые варианты и редкие branches.
Property tests генерируют случайные корректные NRes/RsLi/WEAR records,
выполняют encode -> decode и сравнивают семантику. Fuzz tests изменяют длины,
offsets, counts и termination bytes и требуют контролируемой ошибки без crash и
чрезмерного выделения памяти.
Критические алгоритмы имеют отдельные vectors: ASCII casefold, NRes permutation
search, RsLi byte transform, LZSS backreferences, quaternion shortest path,
matrix composition и terrain mask remap.
### Corpus validation
Каждый файл оригинального каталога проходит parser своего семейства. Отчёт
содержит hash, variant, counts, warnings, errors и точный offset сбоя. Baseline
демоверсии:
```text
MSH 435
MAT0 905
Texm 518
FXID 923
WEAR 457
Land.msh 6
Land.map 6
TMA 6
unit DAT 425
errors 0
```
Изменение parser-а принимается только если baseline остаётся стабильной либо
новый variant зарегистрирован с образцом и объяснением. Warnings должны быть
именованными: «неизвестное opaque поле» не равно «выход ссылки за диапазон».
### Cross-resource integration
Интеграционный тест начинается с миссии и проходит весь dependency graph:
object -> prototype -> MSH -> WEAR -> MAT0 -> Texm/lightmap/FXID. Он не
ограничивается тем, что файлы существуют: material slot должен указывать на
допустимый MAT0, phase -- на допустимую texture, model batch -- на существующий
WEAR index.
Demo mission total: 201 objects -> 501 prototypes -> 501 object MSH/WEAR.
Чистый object graph даёт 3 873 material slots и 5 049 texture requests; после
включения environment WEAR итог равен 3 879 material slots, 5 067 textures и
18 lightmaps, failures 0. Такой тест ловит ошибки casefold, suffix, fallback и
путей, которые отдельный parser не замечает.
Для каждого отсутствующего узла отчёт хранит полный parent chain, чтобы
различать broken global archive и реально достижимый mission failure.
### Deterministic simulation replay
Записывается начальная миссия, seed, input events, network messages и значения
внешних часов. На контрольных ticks сохраняется canonical state hash:
```text
sorted ObjectId list
transforms and velocities
critical properties and owners
AI/behavior state IDs
active effect state
game clock and RNG states
```
Pointer addresses, allocator order и GPU handles в hash не входят. Два запуска с
одинаковым log должны давать одинаковый state hash на каждом checkpoint. Первое
расхождение гораздо информативнее финального разного результата миссии.
### Render command parity
До pixel comparison сравнивается command list:
```text
camera matrices and viewport
visible ObjectIds
render phase and stable order
model/node/slot/batch IDs
material phase and texture handles
legacy pipeline states
index ranges and transforms
```
Если command lists совпадают, но pixels различаются, проблема находится в
shader/backend, sampling или численной точности. Если command lists уже
различаются, pixel diff лишь скрывает более раннюю ошибку.
Golden captures следует хранить отдельно для статической модели, анимации,
terrain, transparent FX, shadows, lightmap и atmosphere.
### Pixel, audio и network tests
Pixel tests используют фиксированное разрешение, camera, device profile, seed и
timeline. Сравниваются exact pixels для CPU/reference path и tolerance metrics
для GPU path, но tolerance не должна скрывать переставленные прозрачные
primitives.
Audio tests сравнивают список sound events, sample IDs, positions, loop flags и
gains; waveform зависит от mixer/device и является вторичным уровнем. Network
tests воспроизводят captured message sequences, проверяют mirrors, ownership и
disconnect. Для native DirectPlay compatibility дополнительно нужен packet-level
corpus.
## Regression baselines
Corpus validation формирует три независимых отчёта: демоверсия, Часть 1 и
Часть 2. Каждый сохраняет manifest файлов, hashes executable/DLL, variants,
warnings, global archive health и mission reachability.
Ключевые corpus gates:
```text
NRes: 120 файлов / 6 804 entries и 134 / 8 171 для Частей 1/2
TMA: 29 миссий / 864 objects / 28 extras и 31 / 885 / 41
MSH: 435 и 511 моделей
MAT0: 905 и 1 127 материалов
Texm: 518 и 631 текстура
FXID: 923 и 1 065 эффектов
full reachability: 4 701 и 5 845 prototype requests, failures 0
```
Расширенные mission-reachability totals:
```text
Часть 1: 29 TMA, 864 objects, 4 701 prototypes,
36 954 materials, 48 806 textures, 139 lightmaps, failures 0
Часть 2: 31 TMA, 885 objects, 5 845 prototypes,
50 888 materials, 68 603 textures, 214 lightmaps, failures 0
```
Обязательные regression cases:
- NRes с ненулевым unindexed region;
- prototype inheritance через `objects.rlb`;
- unit DAT `description[32]` без NUL;
- TMA epilogue и `extra_count` 0--4;
- empty SWAV entry;
- stale save-slot metadata без payload;
- build-scoped RVA lookup.
Byte-identical asset comparison выполняется только внутри одного корпуса. Между
Частями 1 и 2 сравниваются semantic invariants и decoded representation,
поскольку многие assets пересобраны.
## Точность, скорость и повторяемость
Совместимый движок должен быть корректным, повторяемым и достаточно быстрым.
Эти свойства нельзя получать одним и тем же приёмом. Сначала создаётся простой
эталонный путь, затем он измеряется и оптимизируется без изменения результата.
Главные источники расхождений: x87 extended precision, преобразование float в
integer, порядок операций, старые SIMD implementations, нестабильная сортировка,
RNG и использование разных часов.
### x87 и округление
Оригинальный x86-код мог хранить промежуточные значения в 80-битных регистрах
x87, а в память записывать 32-битный float. Современный compiler чаще использует
SSE с округлением после каждой операции. Различие заметно на границах animation
frame, culling plane и collision threshold.
Для критических формул нужен reference mode:
- фиксированный порядок операций без reassociation;
- запрещённый fast-math;
- явные преобразования и проверенный режим округления;
- тесты возле half-integer и epsilon boundaries;
- при необходимости extended intermediate через `long double` на проверенной
платформе.
Не требуется эмулировать x87 во всём движке. Нужно локализовать функции, где
малое отличие меняет дискретное решение, и держать для них scalar reference path.
### RNG как часть состояния
FX, atmosphere и, вероятно, AI используют случайные значения. Один глобальный
RNG легко расходится, если новая реализация запрашивает дополнительное число для
визуальной оптимизации. Для трассировки полезны именованные streams:
```text
world/gameplay RNG
AI/script RNG
FX instance RNG
atmosphere RNG
non-deterministic cosmetic RNG
```
Для native parity может потребоваться один общий алгоритм и точная sequence. До
подтверждения capture каждый stream хранит seed и счётчик вызовов в trace.
Cosmetic stream не входит в simulation hash.
### Стабильный порядок
Коллекции не должны зависеть от адресов, unordered containers или порядка
завершения worker threads. Для объектов, collision pairs, opaque/transparent
draws и network messages задаются явные stable keys:
- objects -- queue insertion sequence или OriginalObjectId;
- collision pairs -- упорядоченная пара IDs;
- opaque draws -- phase, pipeline key, material, stable insertion ID;
- transparent draws -- layer, quantized distance, stable insertion ID;
- network messages -- sequence и sender.
Даже когда математический результат коммутативен, side effects, cache accesses и
RNG делают порядок наблюдаемым.
### Часы и fixed-step
Monotonic platform clock хранится отдельно от game clock. Pause и time scaling
применяются к game clock. Simulation работает с фиксированным или точно
воспроизводимым шагом, а render может интерполировать presentation state, не
изменяя authoritative world.
Maintenance timers кэшей используют реальные часы или отдельную подтверждённую
шкалу; их срабатывание не должно менять gameplay. При перегрузке лучше выполнить
ограниченное число simulation steps и явно зафиксировать dropped presentation
frames, чем передать огромный `dt` в AI/physics.
### Оптимизация без потери эталона
1. Сохранить scalar reference implementation.
2. Добавить profiler counters на decoding, culling, sorting, animation, upload
и draw.
3. Оптимизировать только измеренный bottleneck.
4. Сравнить SIMD/parallel результат с reference на полном corpus.
5. Оставить runtime switch для отключения оптимизации при диагностике.
`g_FastProc` удобно моделировать как таблицу function objects: все slots сначала
указывают на scalar path, затем безопасные slots заменяются SIMD-вариантами
после self-test на старте.
### Кэш и память
Архивы, decoded blobs, CPU assets и GPU resources имеют отдельные budgets.
Eviction разрешена только для объектов с нулевым external refcount и после
безопасной frame fence. Original delayed cleanup порядка десятков секунд можно
воспроизвести policy-параметрами, не сканируя все entries каждый кадр.
Основные показатели: число открытых архивов, decoded bytes, resident
textures/lightmaps, models, active FX, draw items и deferred-delete size. Любой
неограниченно растущий счётчик является regression. Производительность считается
достаточной только после корректности: стабильные 60 FPS с неверным LOD или
пропущенными эффектами не являются успехом.
## Release gates
Версия не выпускается, если:
- появился новый corpus error;
- изменился byte roundtrip неизменённых ресурсов;
- dependency graph получил failure в достижимом пути;
- deterministic replay расходится;
- command capture изменился без ожидаемого changelog;
- parser допускает allocation по непроверенному count;
- новая оптимизация не имеет scalar reference comparison.
Каждое исправление регистрирует минимальный regression asset или synthetic
vector. Если новый behavior намеренно отличается от предыдущего, изменение
должно иметь compatibility profile, corpus sample и объяснение, почему старый
baseline был неполным или неверным.
## Уровни совместимости
Слово «совместимый» используется только с уровнем:
1. **Archive-compatible** -- открывает и сохраняет контейнеры.
2. **Asset-compatible** -- декодирует модели, материалы, текстуры и эффекты.
3. **Mission-compatible** -- загружает карту и создаёт все объекты.
4. **Runtime-compatible** -- исполняет время, события, поведение и физику.
5. **Presentation-compatible** -- воспроизводит рендер и звук.
6. **Game-compatible** -- позволяет пройти миссии, сохраняться и продолжать.
7. **Native-interoperable** -- взаимодействует с оригинальной сетью и внешним
ABI.
Viewer с красивой моделью находится только на втором уровне.
### Обязательные критерии запуска и данных
- приложение запускается из неизменённого оригинального каталога;
- относительные пути, регистр и legacy encodings разрешаются по исходным
правилам;
- все требуемые NRes/RsLi открываются без предварительной конвертации;
- parsers проверяют границы и не используют неопределённые bytes как указатели;
- неизвестные поля сохраняются lossless;
- все mission-reachable prototype, model, material, texture, lightmap и effect
references разрешаются;
- отсутствие необязательного ресурса следует документированному fallback, а не
случайному default.
### Обязательные критерии мира
- TMA разбирается до точного EOF;
- `Land.msh` и `Land.map` создают корректную поверхность и areal graph;
- ObjectId, owner и mirror semantics устойчивы;
- queue traversal и deferred deletion безопасны;
- pause, game time и simulation steps повторяемы;
- AI/Behavior/Wizard/Control взаимодействуют через заданные границы;
- collision и navigation не подменяют друг друга;
- script events используют logical IDs и переживают удаление объектов;
- deterministic replay совпадает на контрольных ticks.
### Обязательные критерии presentation
- static и animated MSH используют правильные slots, batches и transforms;
- WEAR/MAT0/Texm fallback и phase timing совпадают;
- mip-skip, palettes, Page atlases и lightmaps работают;
- render phases, depth/cull/blend state и transparent order подтверждены
captures;
- FXID commands и RNG дают устойчивый результат;
- camera и 3D sound listener синхронизированы;
- atmosphere, тени, солнце и flares не являются декоративными заглушками;
- UI и world rendering имеют правильную границу;
- golden command captures стабильны, pixel parity измеряется на фиксированных
сценах.
### Обязательные критерии полной игры
- все доступные миссии стартуют, завершаются и корректно сообщают
success/failure;
- campaign dispatcher сохраняет прогресс;
- savegame восстанавливает world, script, AI, RNG и clocks, а не только
placement;
- input remapping, pause, camera modes, sound и настройки работают из UI;
- длительный прогон не накапливает objects, resources или audio sources;
- ошибки данных показывают actionable chain;
- производительность приемлема без отключения подсистем;
- демоверсия, Часть 1 и Часть 2 проходят один и тот же тестовый контур с
раздельными manifests и эталонами.
### Native interoperability
Самый строгий уровень дополнительно требует совпадения x86 ABI экспортов, vtable
slots и calling conventions для подключаемых оригинальных модулей, а также
DirectPlay wire/framing и compression. Этот уровень независим от возможности
играть в новом standalone runtime.
Проект может честно заявлять game compatibility без native DLL/network
interoperability, но это должно быть явно указано. Аналогично pixel-perfect режим
может быть отдельным compatibility profile поверх функционально корректного
renderer-а.
### Совместимость нескольких наборов данных
Критерий полной совместимости применяется отдельно к демоверсии, Части 1 и
Части 2. Прохождение одного набора не позволяет заявлять поддержку остальных.
Обязательное различие:
- **format compatibility** -- один parser принимает все три набора;
- **content compatibility** -- конкретная миссия разрешает весь reachable graph;
- **behavior compatibility** -- runtime совпадает с соответствующей сборкой
изменённых DLL;
- **cross-version support** -- один новый движок выбирает корректные данные и
defaults по fingerprint установки.
Content fingerprint включает hashes executable/DLL и manifest ключевых архивов.
Он не используется для запрета модификаций, но выбирает compatibility profile и
делает отклонение диагностируемым.
## Definition of done
Полное документирование и реализация считаются завершёнными только когда каждый
критерий связан с главой спецификации, executable test и хотя бы одним
corpus/golden case. Утверждение без проверяемого критерия остаётся
исследовательской заметкой, а не контрактом.
File diff suppressed because it is too large Load Diff
+409
View File
@@ -0,0 +1,409 @@
# Acceptance coverage manifest.
# Format: <acceptance-id>\t<covered|partial|blocked|omitted>\t<evidence>
L0-COPYRIGHT-001 covered cargo test -p fparkan-corpus --offline report_json_contains_metrics_and_hashes_not_paths_or_payloads
L0-P1-001 covered cargo test -p fparkan-corpus --offline licensed_part1_manifest_profile_and_counts_match_baseline
L0-P1-002 covered cargo test -p fparkan-corpus --offline licensed_part1_has_no_casefold_relative_path_collisions
L0-P2-001 covered cargo test -p fparkan-corpus --offline licensed_part2_manifest_profile_and_counts_match_baseline
L0-P2-002 covered cargo test -p fparkan-corpus --offline licensed_part2_has_no_casefold_relative_path_collisions
S0-ARCH-001 covered cargo xtask policy runs cargo metadata --offline --no-deps successfully
S0-ARCH-002 covered cargo xtask policy rejects forbidden GUI/adapter dependencies from domain crates
S0-ARCH-003 covered cargo xtask policy rejects platform/render adapter dependencies from the transitive fparkan-headless workspace manifest closure
S0-ARCH-004 covered cargo xtask policy scans workspace-owned Rust/TOML for unsafe constructs and workspace lints forbid unsafe_code
S0-ARCH-005 covered cargo xtask policy rejects Python source files, Python shebangs, and Python CI workflow steps while allowing docs requirements.txt
S0-ARCH-006 covered cargo xtask policy rejects non-fparkan package directories under crates/
S0-ARCH-007 covered cargo xtask ci runs fmt, policy, workspace test, clippy, rustdoc warnings, cargo-deny or built-in supply-chain fallback, and strict acceptance audit
S0-ARCH-008 covered cargo xtask policy rejects moving Rust toolchains and workspace rust-version drift
S0-ARCH-009 covered .github/workflows/ci.yml runs a pinned MSRV backend-neutral crate job
S0-ARCH-010 covered cargo xtask acceptance audit emits commit_sha, rust_toolchain, and msrv metadata into the JSON artifact
S0-ARCH-011 blocked cargo run -p fparkan-vulkan-smoke emits explicit per-platform blocked artifacts until real Vulkan 300-frame validation=0 runner is available
S0-DIAG-001 covered cargo test -p fparkan-diagnostics --offline diagnostic_chain_preserves_context
S0-DIAG-002 covered cargo test -p fparkan-diagnostics --offline json_is_stable
S0-CORPUS-001 covered cargo test -p fparkan-corpus --offline deterministic_traversal_is_creation_order_independent
S0-CORPUS-002 covered cargo test -p fparkan-corpus --offline unreadable_directory_produces_error
S0-CORPUS-003 covered cargo test -p fparkan-corpus --offline symlink_loop_is_not_traversed_by_default
S0-CORPUS-004 covered cargo test -p fparkan-corpus --offline casefold_collisions_are_registered
S0-CORPUS-005 covered cargo test -p fparkan-corpus --offline fingerprint_changes
S0-CORPUS-006 covered cargo test -p fparkan-corpus --offline atomic_report_write
S0-CLI-001 covered cargo test -p fparkan-cli --offline stable_exit_codes_are_mapped
S0-CLI-002 covered cargo test -p fparkan-cli --offline accepts_json_format_option archive_json_has_schema_version
S0-PLAT-001 covered cargo test -p fparkan-platform-winit --offline window_port_reports_default_request_profile
S0-PLAT-002 covered cargo clippy -p fparkan-platform -p fparkan-platform-winit --all-targets --all-features --locked -- -D warnings
S0-PLAT-003 covered cargo test -p fparkan-platform-winit --offline smoke_window_plan_requires_native_handles_and_nonzero_extent smoke_window_plan_rejects_zero_extent
S0-PLAT-004 covered cargo test -p fparkan-platform-winit --offline smoke_window_app_requires_created_native_window smoke_window_app_rejects_synthetic_window_without_native_handles
S0-VK-001 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents
S0-VK-002 covered cargo test -p fparkan-render-vulkan --offline device_scoring_is_deterministic_and_prefers_discrete_unified_queue
S0-VK-003 covered cargo test -p fparkan-render-vulkan --offline portability_subset_is_reported_and_enabled_when_exposed
S0-VK-004 covered cargo test -p fparkan-render-vulkan --offline rejects_missing_graphics_present_swapchain_and_format
S0-VK-005 covered cargo test -p fparkan-render-vulkan --offline capability_report_json_is_stable
S0-VK-006 covered cargo test -p fparkan-render-vulkan --offline loader_probe_report_json_is_stable
S0-VK-007 covered cargo xtask policy
S0-VK-008 covered cargo test -p fparkan-render-vulkan --offline instance_plan_is_sorted_deduplicated_and_portability_aware
S0-VK-009 covered cargo test -p fparkan-render-vulkan --offline instance_plan_adds_portability_extension_when_requested
S0-VK-010 covered cargo test -p fparkan-render-vulkan --offline invalid_instance_extension_name_is_reported_before_loader_use
S0-VK-011 covered cargo test -p fparkan-render-vulkan --offline surface_plan_requires_native_handles
S0-VK-012 covered cargo test -p fparkan-render-vulkan --offline surface_plan_json_is_stable
S0-VK-013 covered cargo test -p fparkan-render-vulkan --offline static_surface_extension_name_is_decoded
S0-VK-014 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_prefers_srgb_mailbox_and_clamps_extent
S0-VK-015 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_uses_fifo_and_current_extent_fallbacks
S0-VK-016 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_rejects_missing_surface_data_and_empty_extent
S0-VK-017 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_json_and_recreation_reports_are_stable
S0-VK-018 covered cargo test -p fparkan-render-vulkan --offline triangle_shader_manifest_hashes_are_stable
S0-VK-019 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_report_json_is_stable
S0-VK-020 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_rejects_invalid_spirv_containers
S0-VK-021 covered cargo test -p fparkan-render-vulkan --offline frame_submission_plan_json_is_stable
S0-VK-022 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents
S0-VK-023 covered cargo test -p fparkan-vulkan-smoke --offline rejects_false_pass_without_full_evidence blocked_report_includes_shader_manifest_and_bootstrap_status
S0-VK-024 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_loader_probe formats_vulkan_api_version
S0-VK-025 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_instance_probe parses_instance_probe_as_loader_probe
S0-VK-026 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_window_probe rejects_passed_without_surface_probe parses_surface_probe_as_instance_probe
S0-VK-027 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_swapchain_recreation blocked_report_includes_shader_manifest_and_bootstrap_status
S0-VK-028 covered cargo test -p fparkan-vulkan-smoke --offline reports_rustc_host_triple blocked_report_includes_shader_manifest_and_bootstrap_status
S0-VK-029 covered cargo test -p xtask --offline native_smoke_audit_accepts_complete_three_platform_pass native_smoke_audit_rejects_blocked_or_incomplete_reports
S0-VK-030 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_with_failed_surface
S0-VK-031 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_selected_device
S0-VK-032 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_swapchain
S0-VK-033 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_logical_device
S0-VK-034 covered cargo test -p xtask --offline native_smoke_audit_accepts_complete_three_platform_pass native_smoke_audit_rejects_blocked_or_incomplete_reports
S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow
S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read
L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
L1-P2-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
L1-P1-NRES-002 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
L1-P2-NRES-002 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
L1-P1-NRES-003 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
L1-P2-NRES-003 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
L1-P1-RSLI-001 covered cargo test -p fparkan-rsli --offline licensed_part1_rsli_method_distribution_baseline
L1-P2-RSLI-001 covered cargo test -p fparkan-rsli --offline licensed_part2_rsli_method_distribution_baseline
L1-RSLI-QUIRK-001 covered cargo test -p fparkan-rsli --offline licensed_corpora_rsli_quirk_is_only_approved_interf8_tex
L1-P1-PATH-001 covered cargo test -p fparkan-corpus --offline licensed_part1_paths_stay_under_root
L1-P2-PATH-001 covered cargo test -p fparkan-corpus --offline licensed_part2_paths_stay_under_root
S1-NRES-001 covered cargo test -p fparkan-nres --offline parses_minimal_empty_archive
S1-NRES-002 covered cargo test -p fparkan-nres --offline one_entry_archive_uses_8_byte_alignment
S1-NRES-003 covered cargo test -p fparkan-nres --offline rejects_invalid_magic
S1-NRES-004 covered cargo test -p fparkan-nres --offline rejects_unsupported_version
S1-NRES-005 covered cargo test -p fparkan-nres --offline rejects_negative_entry_count
S1-NRES-006 covered cargo test -p fparkan-nres --offline rejects_directory_size_before_allocation
S1-NRES-007 covered cargo test -p fparkan-nres --offline rejects_total_size_mismatch
S1-NRES-008 covered cargo test -p fparkan-nres --offline rejects_directory_before_header
S1-NRES-009 covered cargo test -p fparkan-nres --offline rejects_payload_before_data_region
S1-NRES-010 covered cargo test -p fparkan-nres --offline rejects_payload_crossing_directory
S1-NRES-020 covered cargo test -p fparkan-nres --offline preserves_nonzero_unindexed_region
S1-NRES-021 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
S1-NRES-011 covered cargo test -p fparkan-nres --offline rejects_overlapping_payloads
S1-NRES-012 covered cargo test -p fparkan-nres --offline rejects_name_without_nul_terminator
S1-NRES-013 covered cargo test -p fparkan-nres --offline preserves_name_bytes_after_nul
S1-NRES-014 covered cargo test -p fparkan-nres --offline rejects_sort_index_out_of_range
S1-NRES-015 covered cargo test -p fparkan-nres --offline rejects_duplicate_sort_mapping
S1-NRES-016 covered cargo test -p fparkan-nres --offline binary_lookup_returns_original_entry_index
S1-NRES-017 covered cargo test -p fparkan-nres --offline compatible_profile_uses_linear_fallback_for_broken_mapping
S1-NRES-018 covered cargo test -p fparkan-nres --offline lookup_is_ascii_case_insensitive
S1-NRES-019 covered cargo test -p fparkan-nres --offline rejects_empty_names_and_resolves_duplicates_to_first_entry
S1-NRES-022 covered cargo test -p fparkan-nres --offline canonical_compact_roundtrip_preserves_entry_semantics
S1-NRES-023 covered cargo test -p fparkan-nres --offline editor_payload_update_rewrites_offsets_and_size
S1-NRES-024 covered cargo test -p fparkan-nres --offline editor_rename_rebuilds_search_mapping
S1-NRES-025 covered cargo test -p fparkan-nres --offline editor_rejects_invalid_authoring_names
S1-NRES-PROP-001 covered cargo test -p fparkan-nres --offline generated_archives_preserve_lossless_and_canonical_semantics
S1-NRES-PROP-002 covered cargo test -p fparkan-nres --offline generated_editor_updates_roundtrip
S1-NRES-FUZZ-001 covered cargo test -p fparkan-nres --offline arbitrary_small_inputs_do_not_panic_or_overallocate
S1-PATH-001 covered cargo test -p fparkan-path --offline normalizes_separators
S1-PATH-002 covered cargo test -p fparkan-path --offline ascii_casefold_does_not_unicode_fold
S1-PATH-003 covered cargo test -p fparkan-path --offline non_ascii_original_bytes_remain_stable
S1-PATH-004 covered cargo test -p fparkan-path --offline rejects_absolute_drive_and_nul_paths
S1-PATH-005 covered cargo test -p fparkan-path --offline rejects_escape
S1-PATH-006 covered cargo test -p fparkan-path --offline rejects_absolute_drive_and_nul_paths
S1-PATH-007 covered cargo test -p fparkan-path --offline join_under_keeps_normalized_path_below_root
S1-PATH-008 covered cargo test -p fparkan-path --offline original_separators_and_raw_bytes_are_preserved
S1-PATH-009 covered cargo test -p fparkan-path --offline accepts_non_utf8_legacy_bytes
S1-VFS-005 covered cargo test -p fparkan-vfs --offline memory_vfs_list_prefix_is_boundary_safe
S1-RSLI-001 covered cargo test -p fparkan-rsli --offline parses_minimal_empty_library
S1-RSLI-002 covered cargo test -p fparkan-rsli --offline rejects_invalid_header_fields
S1-RSLI-003 covered cargo test -p fparkan-rsli --offline rejects_entry_table_bounds
S1-RSLI-004 covered cargo test -p fparkan-rsli --offline table_xor_transform_uses_known_vector
S1-RSLI-005 covered cargo test -p fparkan-rsli --offline table_xor_transform_is_symmetric
S1-RSLI-006 covered cargo test -p fparkan-rsli --offline table_xor_state_spans_entries
S1-RSLI-007 covered cargo test -p fparkan-rsli --offline presorted_mapping_uses_valid_permutation
S1-RSLI-008 covered cargo test -p fparkan-rsli --offline compatible_profile_rebuilds_invalid_presorted_mapping
S1-RSLI-009 covered cargo test -p fparkan-rsli --offline stored_method_uses_exact_size
S1-RSLI-010 covered cargo test -p fparkan-rsli --offline xor_only_method_uses_entry_key
S1-RSLI-011 covered cargo test -p fparkan-rsli --offline lzss_method_decodes_literals_references_and_wrap
S1-RSLI-012 covered cargo test -p fparkan-rsli --offline xor_lzss_method_uses_entry_key
S1-RSLI-013 covered cargo test -p fparkan-rsli --offline adaptive_lzss_method_decodes_synthetic_vector
S1-RSLI-014 covered cargo test -p fparkan-rsli --offline xor_adaptive_lzss_method_decodes_synthetic_vector
S1-RSLI-015 covered cargo test -p fparkan-rsli --offline raw_deflate_method_expects_raw_stream_not_zlib_wrapper
S1-RSLI-016 covered cargo test -p fparkan-rsli --offline unknown_method_is_rejected_on_load
S1-RSLI-017 covered cargo test -p fparkan-rsli --offline decoded_size_mismatch_is_rejected
S1-RSLI-018 covered cargo test -p fparkan-rsli --offline ao_overlay_adjusts_effective_offsets
S1-RSLI-019 covered cargo test -p fparkan-rsli --offline invalid_ao_overlay_is_rejected
S1-RSLI-020 covered cargo test -p fparkan-rsli --offline rejects_registered_quirks_in_strict_profile
S1-RSLI-021 covered cargo test -p fparkan-rsli --offline named_deflate_eof_plus_one_quirk_accepts_only_approved_entry
S1-RSLI-022 covered cargo test -p fparkan-rsli --offline unknown_header_bytes_are_lossless
S1-RSLI-023 covered cargo test -p fparkan-rsli --offline no_op_lossless_roundtrip_preserves_bytes
S1-RSLI-PROP-001 covered cargo test -p fparkan-rsli --offline generated_supported_methods_decode_expected_bytes
S1-RSLI-FUZZ-001 covered cargo test -p fparkan-rsli --offline arbitrary_small_inputs_do_not_panic
S1-RES-001 covered cargo test -p fparkan-resource --offline cached_repository_reads_synthetic_nres
S1-RES-002 covered cargo test -p fparkan-resource --offline entry_handles_are_archive_qualified
S1-RES-003 covered cargo test -p fparkan-resource --offline archive_cache_and_decoded_payload_cache_evict_independently
S1-RES-004 covered cargo test -p fparkan-resource --offline entry_read_error_carries_archive_path_and_entry_name
S1-VFS-001 covered cargo test -p fparkan-vfs --offline memory_vfs_uses_exact_lookup
S1-VFS-002 covered cargo test -p fparkan-vfs --offline overlay_vfs_uses_first_matching_layer
S1-VFS-003 covered cargo test -p fparkan-vfs --offline directory_vfs_resolves_ascii_casefolded_segments
S1-VFS-004 covered cargo test -p fparkan-vfs --offline casefold_selector_reports_ambiguous_segments
L2-P1-UNIT-001 covered cargo test -p fparkan-prototype --offline licensed_corpora_unit_dat_parse_counts
L2-P2-UNIT-001 covered cargo test -p fparkan-prototype --offline licensed_corpora_unit_dat_parse_counts
L2-P1-REG-001 covered cargo test -p fparkan-prototype --offline licensed_corpora_registry_payloads_are_record_aligned
L2-P2-REG-001 covered cargo test -p fparkan-prototype --offline licensed_corpora_registry_payloads_are_record_aligned
L2-P1-GRAPH-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L2-P2-GRAPH-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L2-P1-INHERIT-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L2-P2-INHERIT-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L2-P1-NONGEO-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L2-P2-NONGEO-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L2-P1-GRAPH-002 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L2-P2-GRAPH-002 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
S2-REG-001 covered cargo test -p fparkan-prototype --offline registry_zero_records_payload_is_empty
S2-REG-002 covered cargo test -p fparkan-prototype --offline registry_requires_record_multiple
S2-REG-003 covered cargo test -p fparkan-prototype --offline registry_preserves_bounded_name_tails_and_order
S2-REG-004 covered cargo test -p fparkan-prototype --offline registry_preserves_bounded_name_tails_and_order
S2-UNIT-001 covered cargo test -p fparkan-prototype --offline unit_zero_records_uses_exact_size
S2-UNIT-002 covered cargo test -p fparkan-prototype --offline unit_dat_one_record_uses_exact_size_formula
S2-UNIT-003 covered cargo test -p fparkan-prototype --offline unit_dat_rejects_truncated_record
S2-UNIT-004 covered cargo test -p fparkan-prototype --offline unit_dat_preserves_header_description_tail_and_parent_link
S2-UNIT-005 covered cargo test -p fparkan-prototype --offline unit_dat_preserves_header_description_tail_and_parent_link
S2-UNIT-006 covered cargo test -p fparkan-prototype --offline unit_dat_accepts_full_description_without_nul
S2-UNIT-007 covered cargo test -p fparkan-prototype --offline unit_dat_preserves_header_description_tail_and_parent_link unit_dat_preserves_positive_parent_link
S2-PROTO-001 covered cargo test -p fparkan-prototype --offline resolves_synthetic_objects_registry_model
S2-PROTO-002 covered cargo test -p fparkan-prototype --offline base_only_registry_entry_is_nongeometric
S2-PROTO-003 covered cargo test -p fparkan-prototype --offline graph_report_records_resolved_roots_and_failures
S2-PROTO-004 covered cargo test -p fparkan-prototype --offline missing_referenced_archive_reports_root_chain
S2-PROTO-005 covered cargo test -p fparkan-prototype --offline missing_referenced_resource_reports_root_chain
S2-PROTO-006 covered cargo test -p fparkan-prototype --offline objects_registry_inheritance_merges_parent_then_local_refs
S2-PROTO-007 covered cargo test -p fparkan-prototype --offline objects_registry_inheritance_resolves_multiple_levels
S2-PROTO-008 covered cargo test -p fparkan-prototype --offline objects_registry_inheritance_rejects_direct_cycle
S2-PROTO-009 covered cargo test -p fparkan-prototype --offline objects_registry_inheritance_rejects_indirect_cycle
S2-PROTO-010 covered cargo test -p fparkan-prototype --offline objects_registry_inheritance_rejects_depth_limit
S2-PROTO-011 covered cargo test -p fparkan-prototype --offline base_only_registry_entry_is_nongeometric
S2-PROTO-012 covered cargo test -p fparkan-prototype --offline first_existing_explicit_msh_is_selected_in_order
S2-PROTO-013 covered cargo test -p fparkan-prototype --offline invalid_referenced_msh_is_error
S2-PROTO-014 covered cargo test -p fparkan-prototype --offline resolver_cache_invalidates_when_archive_fingerprint_changes
S2-GRAPH-001 covered cargo test -p fparkan-prototype --offline graph_report_records_resolved_roots_and_failures
S2-GRAPH-002 covered cargo test -p fparkan-prototype --offline unit_dat_expands_components_in_order
S2-GRAPH-003 covered cargo test -p fparkan-assets --offline repository_plan_deduplicates_duplicate_visuals_but_graph_preserves_requests
S2-GRAPH-004 covered cargo test -p fparkan-prototype --offline graph_report_records_resolved_roots_and_failures
S2-GRAPH-005 covered cargo test -p fparkan-cli --offline prototype_graph_json_has_canonical_field_order
S2-GRAPH-006 covered cargo test -p fparkan-prototype --offline graph_report_records_resolved_roots_and_failures
S2-PROP-001 covered cargo test -p fparkan-prototype --offline generated_acyclic_prototype_graph_resolves_deterministically
S2-FUZZ-001 covered cargo test -p fparkan-prototype --offline arbitrary_unit_and_registry_bytes_are_bounded_and_panic_free
L3-P1-MSH-001 covered cargo test -p fparkan-msh --offline licensed_corpus_msh_assets_validate
L3-P2-MSH-001 covered cargo test -p fparkan-msh --offline licensed_corpus_msh_assets_validate
L3-P1-TEXM-001 covered cargo test -p fparkan-texm --offline licensed_corpus_texm_assets_validate_and_decode_mip0
L3-P2-TEXM-001 covered cargo test -p fparkan-texm --offline licensed_corpus_texm_assets_validate_and_decode_mip0
L3-P1-MAT0-001 covered cargo test -p fparkan-material --offline licensed_corpus_mat0_and_wear_parse
L3-P2-MAT0-001 covered cargo test -p fparkan-material --offline licensed_corpus_mat0_and_wear_parse
L3-P1-WEAR-001 covered cargo test -p fparkan-material --offline licensed_corpus_mat0_and_wear_parse
L3-P2-WEAR-001 covered cargo test -p fparkan-material --offline licensed_corpus_mat0_and_wear_parse
L3-P1-ASSET-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L3-P2-ASSET-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L3-P1-CAPTURE-001 covered cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures
L3-P2-CAPTURE-001 covered cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures
S3-WEAR-001 covered cargo test -p fparkan-material --offline wear_preserves_legacy_id_but_selects_by_index
S3-WEAR-002 covered cargo test -p fparkan-material --offline wear_requires_declared_rows
S3-WEAR-003 covered cargo test -p fparkan-material --offline wear_preserves_legacy_id_but_selects_by_index
S3-WEAR-004 covered cargo test -p fparkan-material --offline wear_requires_blank_separator_before_lightmaps
S3-WEAR-005 covered cargo test -p fparkan-material --offline wear_parses_lightmaps
S3-MAT0-001 covered cargo test -p fparkan-material --offline mat0_version_prefix_and_primary_texture
S3-MAT0-002 covered cargo test -p fparkan-material --offline mat0_accepts_zero_phase_material
S3-MAT0-003 covered cargo test -p fparkan-material --offline mat0_phase34_exact_framing_and_full_texture_name
S3-MAT0-004 covered cargo test -p fparkan-material --offline mat0_phase34_exact_framing_and_full_texture_name
S3-MAT0-005 covered cargo test -p fparkan-material --offline mat0_animation_block_has_no_implicit_padding
S3-MAT0-006 covered cargo test -p fparkan-material --offline mat0_rejects_animation_block_count_limit
S3-MSH-001 covered cargo test -p fparkan-msh --offline canonical_stream_set_is_independent_of_entry_order
S3-MSH-002 covered cargo test -p fparkan-msh --offline missing_required_stream_is_error
S3-MSH-003 covered cargo test -p fparkan-msh --offline duplicate_required_stream_type_is_error
S3-MSH-004 covered cargo test -p fparkan-msh --offline node38_stride_is_exact
S3-MSH-005 covered cargo test -p fparkan-msh --offline node38_uses_three_by_five_slot_mapping_and_absent_marker
S3-MSH-006 covered cargo test -p fparkan-msh --offline node38_uses_three_by_five_slot_mapping_and_absent_marker
S3-MSH-007 covered cargo test -p fparkan-msh --offline type2_header_and_slot_tail_framing_are_exact
S3-MSH-008 covered cargo test -p fparkan-msh --offline type2_header_and_slot_tail_framing_are_exact
S3-MSH-009 covered cargo test -p fparkan-msh --offline slot_batch_range_out_of_bounds_is_error
S3-MSH-010 covered cargo test -p fparkan-msh --offline vertex_stream_strides_are_exact
S3-MSH-011 covered cargo test -p fparkan-msh --offline base_vertex_plus_index_must_reference_position
S3-MSH-012 covered cargo test -p fparkan-msh --offline batch20_uses_unaligned_field_offsets
S3-MSH-013 covered cargo test -p fparkan-msh --offline auxiliary_and_extended_streams_are_preserved
S3-MSH-014 covered cargo test -p fparkan-msh --offline auxiliary_and_extended_streams_are_preserved
S3-MSH-015 covered cargo test -p fparkan-msh --offline mtcheck_variant_is_preserved_and_recognized
S3-MSH-016 covered cargo test -p fparkan-msh --offline invalid_bounds_are_rejected
S3-MSH-017 covered cargo test -p fparkan-msh --offline slot_batch_range_out_of_bounds_is_error
S3-MSH-PROP-001 covered cargo test -p fparkan-msh --offline canonical_stream_set_is_independent_of_entry_order
S3-MSH-FUZZ-001 covered cargo test -p fparkan-msh --offline arbitrary_nested_payloads_are_bounded_and_panic_free
S3-TEXM-001 covered cargo test -p fparkan-texm --offline decodes_all_synthetic_formats
S3-TEXM-002 covered cargo test -p fparkan-texm --offline rejects_zero_dimensions
S3-TEXM-003 covered cargo test -p fparkan-texm --offline non_power_of_two_mip_chain_clamps_each_dimension
S3-TEXM-004 covered cargo test -p fparkan-texm --offline rejects_mip_size_arithmetic_overflow_or_oob
S3-TEXM-005 covered cargo test -p fparkan-texm --offline indexed_palette_requires_exact_1024_bytes
S3-TEXM-006 covered cargo test -p fparkan-texm --offline channel_expansion_boundary_values_are_stable
S3-TEXM-007 covered cargo test -p fparkan-texm --offline rgb888x_preserves_fourth_disk_byte_but_outputs_opaque_alpha
S3-TEXM-008 covered cargo test -p fparkan-texm --offline page_tail_absent_and_exact_rect_framing
S3-TEXM-009 covered cargo test -p fparkan-texm --offline page_tail_absent_and_exact_rect_framing
S3-TEXM-010 covered cargo test -p fparkan-texm --offline invalid_page_magic_size_and_trailing_bytes_are_rejected
S3-TEXM-011 covered cargo test -p fparkan-texm --offline invalid_page_magic_size_and_trailing_bytes_are_rejected
S3-TEXM-012 covered cargo test -p fparkan-texm --offline exposes_mip_views_and_upload_plan_without_mutating_document
S3-TEXM-013 covered cargo test -p fparkan-texm --offline page_scaling_uses_floor_origin_and_ceil_end_policy
S3-TEXM-FUZZ-001 covered cargo test -p fparkan-texm --offline arbitrary_texm_payloads_do_not_panic
S3-MAT0-007 covered cargo test -p fparkan-material --offline mat0_rejects_trailing_bytes
S3-MAT-RESOLVE-001 covered cargo test -p fparkan-material --offline resolve_material_uses_exact_match
S3-MAT-RESOLVE-002 covered cargo test -p fparkan-material --offline resolve_material_falls_back_to_default
S3-MAT-RESOLVE-003 covered cargo test -p fparkan-material --offline resolve_material_uses_first_entry_only_after_missing_default
S3-MAT-RESOLVE-004 covered cargo test -p fparkan-material --offline resolve_material_empty_texture_means_untextured
S3-MAT-RESOLVE-005 covered cargo test -p fparkan-material --offline resolve_material_without_lightmap_keeps_lightmap_absent
S3-RENDER-001 covered cargo test -p fparkan-render --offline one_snapshot_draw_produces_one_draw_command
S3-RENDER-002 covered cargo test -p fparkan-render --offline material_index_maps_through_resolved_material_slots
S3-RENDER-003 covered cargo test -p fparkan-render --offline node_transform_is_retained
S3-RENDER-004 covered cargo test -p fparkan-render --offline command_order_uses_phase_then_stable_key
S3-RENDER-005 covered cargo test -p fparkan-render --offline command_capture_independent_of_snapshot_construction_order
S3-RENDER-006 covered cargo test -p fparkan-render --offline invalid_range_returns_contextual_error
S3-RENDER-007 covered cargo test -p fparkan-render --offline capture_is_stable
S3-RENDER-008 covered cargo test -p fparkan-render --offline recording_backend_stores_captures
S3-RENDER-009 covered cargo xtask policy
S3-GL-001 omitted permanent macOS Desktop GL 3.3 adapter is not implemented; historical CGL probe is retained as external evidence only
S3-GL-002 omitted outside the current macOS-focused goal scope; GLES2 remains documented for portable/non-macOS targets
S3-GL-003 blocked legacy fparkan-render-gl adapter removed while Vulkan renderer path is being brought in as the stage-3 backend
S3-VIEWER-001 covered cargo test -p fparkan-viewer --offline model_fixture_uses_viewer_service_and_render_commands
S4-ANIM-001 covered cargo test -p fparkan-animation --offline anim_key24_decodes_signed_quaternion
S4-ANIM-002 covered cargo test -p fparkan-animation --offline frame_map_decodes_u16_and_uses_attr_frame_count
S4-ANIM-003 covered cargo test -p fparkan-animation --offline frame_map_falls_back_when_absent_or_invalid
S4-ANIM-004 covered cargo test -p fparkan-animation --offline frame_map_falls_back_when_absent_or_invalid
S4-ANIM-005 covered cargo test -p fparkan-animation --offline exact_key_time_returns_exact_pose
S4-ANIM-006 covered cargo test -p fparkan-animation --offline pose_track_blends_translation_and_rotation
S4-ANIM-007 covered cargo test -p fparkan-animation --offline quaternion_shortest_path_sign_flip_is_stable
S4-ANIM-008 covered cargo test -p fparkan-animation --offline zero_or_degenerate_key_interval_is_rejected
S4-ANIM-009 omitted current sampler is a portable reference path; runtime-captured x87 parity vectors are not implemented in the macOS-focused scope
S4-ANIM-010 omitted current sampler accepts the profile marker but does not implement an independent x87 compatibility path
S4-ANIM-011 covered cargo test -p fparkan-animation --offline blend_optional_pose_uses_valid_side
S4-ANIM-012 covered cargo test -p fparkan-animation --offline hierarchy_evaluates_parent_before_child_and_rejects_cycles
S4-ANIM-013 covered cargo test -p fparkan-animation --offline hierarchy_evaluates_parent_before_child_and_rejects_cycles
S4-ANIM-014 covered cargo test -p fparkan-animation --offline pose_capture_uses_float_bits
S4-ANIM-PROP-001 covered cargo test -p fparkan-animation --offline generated_valid_quaternions_remain_finite
S4-MAT-001 covered cargo test -p fparkan-material --offline material_modes_zero_to_three_choose_stable_phases
S4-MAT-002 covered cargo test -p fparkan-material --offline material_exact_key_boundary_selects_exact_phase
S4-MAT-003 covered cargo test -p fparkan-material --offline material_interpolation_mask_affects_only_selected_fields
S4-MAT-004 covered cargo test -p fparkan-material --offline material_timeline_profile_cases_are_evidence_labeled
S4-MAT-005 covered cargo test -p fparkan-material --offline material_random_offset_uses_material_stream_only
S4-MAT-006 covered cargo test -p fparkan-material --offline material_same_seed_and_timeline_produces_same_phase_capture
S4-FX-001 covered cargo test -p fparkan-fx --offline header_is_exactly_sixty_bytes_and_command_sizes_are_fixed
S4-FX-002 covered cargo test -p fparkan-fx --offline header_is_exactly_sixty_bytes_and_command_sizes_are_fixed
S4-FX-011 covered cargo test -p fparkan-fx --offline header_is_exactly_sixty_bytes_and_command_sizes_are_fixed
S4-FX-012 covered cargo test -p fparkan-fx --offline opcode6_four_byte_command_is_accepted
S4-FX-013 covered cargo test -p fparkan-fx --offline rejects_unknown_opcode_at_command_index
S4-FX-014 covered cargo test -p fparkan-fx --offline rejects_command_count_that_exceeds_payload
S4-FX-015 covered cargo test -p fparkan-fx --offline rejects_trailing_bytes_after_command_stream
S4-FX-016 covered cargo test -p fparkan-fx --offline fixed_resource_refs_preserve_tails
S4-FX-017 covered cargo test -p fparkan-fx --offline missing_dependency_error_contains_effect_command_archive_and_name
S4-FX-018 omitted FX create/update/emit is documented as a deterministic reference stub until runtime-captured lifecycle semantics are implemented
S4-FX-019 omitted FX update side-effect contract is stub-level only and not accepted as original runtime parity evidence
S4-FX-020 omitted FX emit side-effect contract is stub-level only and not accepted as original runtime parity evidence
S4-FX-021 omitted FX command order is deterministic for the stub but original runtime ordering evidence is not implemented
S4-FX-022 omitted FX lifecycle controls are deterministic for the stub but original stop/restart/end semantics are not implemented
S4-FX-023 omitted FX emission capture is deterministic for the stub but lacks runtime-captured parity evidence
S4-FX-024 omitted FX RNG accounting is not implemented beyond the reference stub and must not count as compatibility coverage
S4-FX-FUZZ-001 covered cargo test -p fparkan-fx --offline arbitrary_command_streams_are_bounded_and_panic_free
L4-P1-ANIM-001 covered cargo test -p fparkan-msh --offline licensed_corpus_animation_streams_sample_approved_pose_captures
L4-P2-ANIM-001 covered cargo test -p fparkan-msh --offline licensed_corpus_animation_streams_sample_approved_pose_captures
L4-P1-CAPTURE-001 covered cargo test -p fparkan-msh --offline licensed_corpus_animation_streams_sample_approved_pose_captures
L4-P2-CAPTURE-001 covered cargo test -p fparkan-msh --offline licensed_corpus_animation_streams_sample_approved_pose_captures
L4-P1-FX-001 covered cargo test -p fparkan-fx --offline licensed_corpus_fxid_exact_eof_and_distribution
L4-P2-FX-001 covered cargo test -p fparkan-fx --offline licensed_corpus_fxid_exact_eof_and_distribution
L4-P1-FX-002 covered cargo test -p fparkan-fx --offline licensed_corpus_fxid_exact_eof_and_distribution
L4-P2-FX-002 covered cargo test -p fparkan-fx --offline licensed_corpus_fxid_exact_eof_and_distribution
L4-FX-OP6-001 covered cargo test -p fparkan-fx --offline licensed_corpus_fxid_exact_eof_and_distribution
L4-P1-EFFECT-001 omitted licensed FX emission capture currently covers deterministic reference-stub output, not original runtime-captured effect parity
L4-P2-EFFECT-001 omitted licensed FX emission capture currently covers deterministic reference-stub output, not original runtime-captured effect parity
S5-LMESH-001 covered cargo test -p fparkan-terrain-format --offline land_msh_required_streams_are_order_independent_and_stride_checked
S5-LMESH-002 covered cargo test -p fparkan-terrain-format --offline land_msh_required_streams_are_order_independent_and_stride_checked
S5-LMESH-003 covered cargo test -p fparkan-terrain-format --offline decodes_minimal_land_msh
S5-LMESH-004 covered cargo test -p fparkan-terrain-format --offline decodes_minimal_land_msh
S5-LMESH-005 covered cargo test -p fparkan-terrain-format --offline rejects_invalid_vertex_index
S5-LMESH-006 covered cargo test -p fparkan-terrain-format --offline rejects_invalid_neighbor_index
S5-LMESH-007 covered cargo test -p fparkan-terrain-format --offline face_layout_preserves_tail_and_all_surface_mask_mappings_are_explicit
S5-LMESH-008 covered cargo test -p fparkan-terrain-format --offline face_layout_preserves_tail_and_all_surface_mask_mappings_are_explicit
S5-LMESH-009 covered cargo test -p fparkan-terrain-format --offline face_layout_preserves_tail_and_all_surface_mask_mappings_are_explicit
S5-LMAP-001 covered cargo test -p fparkan-terrain-format --offline decodes_minimal_land_map
S5-LMAP-002 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
S5-LMAP-003 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
S5-LMAP-004 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
S5-LMAP-005 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
S5-LMAP-006 covered cargo test -p fparkan-terrain-format --offline rejects_invalid_areal_link
S5-LMAP-007 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
S5-LMAP-008 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
S5-LMAP-009 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
S5-LMAP-010 covered cargo test -p fparkan-terrain-format --offline rejects_invalid_grid_area_ref
S5-LMAP-011 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
S5-TERRAIN-001 covered cargo test -p fparkan-terrain --offline locates_areal_and_routes_synthetic_neighbors
S5-TERRAIN-002 covered cargo test -p fparkan-terrain --offline locates_areal_and_routes_synthetic_neighbors
S5-TERRAIN-003 covered cargo test -p fparkan-terrain --offline missing_start_or_goal_returns_no_route
S5-TERRAIN-004 covered cargo test -p fparkan-terrain --offline synthetic_surface_height_and_raycast_work
S5-TMA-001 covered cargo test -p fparkan-mission-format --offline minimal_synthetic_exact_eof
S5-TMA-002 covered cargo test -p fparkan-mission-format --offline lp_string_does_not_consume_implicit_nul
S5-TMA-003 covered cargo test -p fparkan-mission-format --offline path_ids_retain_nonsequential_order_and_truncated_points_fail
S5-TMA-004 covered cargo test -p fparkan-mission-format --offline path_ids_retain_nonsequential_order_and_truncated_points_fail
S5-TMA-005 covered cargo test -p fparkan-mission-format --offline clan_modes_one_to_three_and_spatial_mode_zero_decode
S5-TMA-006 covered cargo test -p fparkan-mission-format --offline clan_modes_one_to_three_and_spatial_mode_zero_decode
S5-TMA-007 covered cargo test -p fparkan-mission-format --offline unknown_clan_mode_nonfinite_transform_and_trailing_bytes_are_rejected
S5-TMA-008 covered cargo test -p fparkan-mission-format --offline synthetic_standard_clan_and_object_preserve_ordered_properties
S5-TMA-009 covered cargo test -p fparkan-mission-format --offline synthetic_standard_clan_and_object_preserve_ordered_properties
S5-TMA-010 covered cargo test -p fparkan-mission-format --offline synthetic_standard_clan_and_object_preserve_ordered_properties
S5-TMA-011 covered cargo test -p fparkan-mission-format --offline unknown_clan_mode_nonfinite_transform_and_trailing_bytes_are_rejected
S5-TMA-012 covered cargo test -p fparkan-mission-format --offline description_and_extras_are_exact_raw_records
S5-TMA-013 covered cargo test -p fparkan-mission-format --offline description_and_extras_are_exact_raw_records
S5-TMA-014 covered cargo test -p fparkan-mission-format --offline description_and_extras_are_exact_raw_records
S5-TMA-015 covered cargo test -p fparkan-mission-format --offline unknown_clan_mode_nonfinite_transform_and_trailing_bytes_are_rejected
S5-TMA-016 covered cargo test -p fparkan-mission-format --offline signatures_inside_strings_do_not_create_records_and_truncations_are_bounded
S5-TMA-017 covered cargo test -p fparkan-mission-format --offline signatures_inside_strings_do_not_create_records_and_truncations_are_bounded
S5-TMA-PROP-001 covered cargo test -p fparkan-mission-format --offline generated_valid_documents_and_arbitrary_inputs_are_bounded
S5-TMA-FUZZ-001 covered cargo test -p fparkan-mission-format --offline generated_valid_documents_and_arbitrary_inputs_are_bounded
S5-LOAD-001 covered cargo test -p fparkan-runtime --offline load_trace_records_preparation_before_registration_and_raw_transforms
S5-LOAD-002 covered cargo test -p fparkan-runtime --offline missing_map_and_missing_reachable_resource_fail_before_registration
S5-LOAD-003 covered cargo test -p fparkan-runtime --offline missing_map_and_missing_reachable_resource_fail_before_registration
S5-LOAD-004 covered cargo test -p fparkan-material --offline resolve_material_falls_back_to_default
S5-LOAD-005 covered cargo test -p fparkan-runtime --offline load_trace_records_preparation_before_registration_and_raw_transforms
S5-LOAD-006 covered cargo test -p fparkan-runtime --offline load_mission_requires_vfs_and_keeps_world_unchanged_on_error
S5-LOAD-007 covered cargo test -p fparkan-runtime --offline registration_phase_failure_uses_normal_teardown_and_keeps_engine_world
S5-LOAD-008 covered cargo test -p fparkan-runtime --offline load_trace_records_preparation_before_registration_and_raw_transforms
S5-LOAD-009 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
S5-LOAD-010 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
S5-WORLD-001 covered cargo test -p fparkan-world --offline construct_register_and_hash_are_stable
S5-WORLD-002 covered cargo test -p fparkan-world --offline registration_sequence_stale_and_duplicate_original_contracts
S5-WORLD-003 covered cargo test -p fparkan-world --offline registration_sequence_stale_and_duplicate_original_contracts
S5-WORLD-004 covered cargo test -p fparkan-world --offline registration_sequence_stale_and_duplicate_original_contracts
S5-WORLD-005 covered cargo test -p fparkan-world --offline identity_metadata_keeps_original_mirror_and_owner_distinct
S5-WORLD-006 covered cargo test -p fparkan-world --offline command_fifo_and_deferred_delete_during_calculation
S5-WORLD-007 covered cargo test -p fparkan-world --offline registration_sequence_stale_and_duplicate_original_contracts
S5-WORLD-008 covered cargo test -p fparkan-world --offline command_fifo_and_deferred_delete_during_calculation
S5-WORLD-009 covered cargo test -p fparkan-world --offline command_fifo_and_deferred_delete_during_calculation
S5-WORLD-010 covered cargo test -p fparkan-world --offline command_fifo_and_deferred_delete_during_calculation
S5-WORLD-011 covered cargo test -p fparkan-world --offline command_fifo_and_deferred_delete_during_calculation
S5-WORLD-012 covered cargo test -p fparkan-world --offline fixed_step_pause_and_long_determinism_are_stable
S5-WORLD-013 covered cargo test -p fparkan-world --offline fixed_step_pause_and_long_determinism_are_stable
S5-WORLD-014 covered cargo test -p fparkan-world --offline snapshot_hash_determinism_and_immutability
S5-WORLD-015 covered cargo test -p fparkan-world --offline snapshot_hash_determinism_and_immutability
S5-WORLD-016 covered cargo test -p fparkan-world --offline fixed_step_pause_and_long_determinism_are_stable
S5-WORLD-017 covered cargo test -p fparkan-world --offline render_disabled_does_not_change_hash_end_callbacks_and_shutdown_order
S5-WORLD-018 covered cargo test -p fparkan-world --offline render_disabled_does_not_change_hash_end_callbacks_and_shutdown_order
S5-WORLD-019 covered cargo test -p fparkan-world --offline render_disabled_does_not_change_hash_end_callbacks_and_shutdown_order
S5-WORLD-PROP-001 covered cargo test -p fparkan-world --offline generated_command_delete_sequences_preserve_registry_invariants
L5-P1-LMESH-001 covered cargo test -p fparkan-terrain-format --offline licensed_corpus_land_msh_validate
L5-P2-LMESH-001 covered cargo test -p fparkan-terrain-format --offline licensed_corpus_land_msh_validate
L5-P1-LMAP-001 covered cargo test -p fparkan-terrain-format --offline licensed_corpus_land_map_validate
L5-P2-LMAP-001 covered cargo test -p fparkan-terrain-format --offline licensed_corpus_land_map_validate
L5-LMAP-POLY-001 covered cargo test -p fparkan-terrain-format --offline licensed_corpus_land_map_validate land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
L5-P1-TMA-001 covered cargo test -p fparkan-mission-format --offline licensed_corpus_tma_validate
L5-P2-TMA-001 covered cargo test -p fparkan-mission-format --offline licensed_corpus_tma_validate
L5-P1-MISSION-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L5-P2-MISSION-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L5-P1-MISSION-002 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L5-P2-MISSION-002 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
L5-P1-HEADLESS-001 covered cargo test -p fparkan-runtime --offline selected_is_and_is2_missions_execute_10000_deterministic_ticks
L5-P2-HEADLESS-001 covered cargo test -p fparkan-runtime --offline selected_is_and_is2_missions_execute_10000_deterministic_ticks
L5-P1-RENDER-001 covered cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures
L5-P2-RENDER-001 covered cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures
L3-DEVICE-001 omitted outside the current macOS-focused goal scope; RG40XX-capable device/profile evidence remains documented for the portable target scope
L5-RG40-001 omitted outside the current macOS-focused goal scope; RG40XX 640x480 on-device mission smoke/performance/memory evidence remains documented for the portable target scope
1 # Acceptance coverage manifest.
2 # Format: <acceptance-id>\t<covered|partial|blocked|omitted>\t<evidence>
3 L0-COPYRIGHT-001 covered cargo test -p fparkan-corpus --offline report_json_contains_metrics_and_hashes_not_paths_or_payloads
4 L0-P1-001 covered cargo test -p fparkan-corpus --offline licensed_part1_manifest_profile_and_counts_match_baseline
5 L0-P1-002 covered cargo test -p fparkan-corpus --offline licensed_part1_has_no_casefold_relative_path_collisions
6 L0-P2-001 covered cargo test -p fparkan-corpus --offline licensed_part2_manifest_profile_and_counts_match_baseline
7 L0-P2-002 covered cargo test -p fparkan-corpus --offline licensed_part2_has_no_casefold_relative_path_collisions
8 S0-ARCH-001 covered cargo xtask policy runs cargo metadata --offline --no-deps successfully
9 S0-ARCH-002 covered cargo xtask policy rejects forbidden GUI/adapter dependencies from domain crates
10 S0-ARCH-003 covered cargo xtask policy rejects platform/render adapter dependencies from the transitive fparkan-headless workspace manifest closure
11 S0-ARCH-004 covered cargo xtask policy scans workspace-owned Rust/TOML for unsafe constructs and workspace lints forbid unsafe_code
12 S0-ARCH-005 covered cargo xtask policy rejects Python source files, Python shebangs, and Python CI workflow steps while allowing docs requirements.txt
13 S0-ARCH-006 covered cargo xtask policy rejects non-fparkan package directories under crates/
14 S0-ARCH-007 covered cargo xtask ci runs fmt, policy, workspace test, clippy, rustdoc warnings, cargo-deny or built-in supply-chain fallback, and strict acceptance audit
15 S0-ARCH-008 covered cargo xtask policy rejects moving Rust toolchains and workspace rust-version drift
16 S0-ARCH-009 covered .github/workflows/ci.yml runs a pinned MSRV backend-neutral crate job
17 S0-ARCH-010 covered cargo xtask acceptance audit emits commit_sha, rust_toolchain, and msrv metadata into the JSON artifact
18 S0-ARCH-011 blocked cargo run -p fparkan-vulkan-smoke emits explicit per-platform blocked artifacts until real Vulkan 300-frame validation=0 runner is available
19 S0-DIAG-001 covered cargo test -p fparkan-diagnostics --offline diagnostic_chain_preserves_context
20 S0-DIAG-002 covered cargo test -p fparkan-diagnostics --offline json_is_stable
21 S0-CORPUS-001 covered cargo test -p fparkan-corpus --offline deterministic_traversal_is_creation_order_independent
22 S0-CORPUS-002 covered cargo test -p fparkan-corpus --offline unreadable_directory_produces_error
23 S0-CORPUS-003 covered cargo test -p fparkan-corpus --offline symlink_loop_is_not_traversed_by_default
24 S0-CORPUS-004 covered cargo test -p fparkan-corpus --offline casefold_collisions_are_registered
25 S0-CORPUS-005 covered cargo test -p fparkan-corpus --offline fingerprint_changes
26 S0-CORPUS-006 covered cargo test -p fparkan-corpus --offline atomic_report_write
27 S0-CLI-001 covered cargo test -p fparkan-cli --offline stable_exit_codes_are_mapped
28 S0-CLI-002 covered cargo test -p fparkan-cli --offline accepts_json_format_option archive_json_has_schema_version
29 S0-PLAT-001 covered cargo test -p fparkan-platform-winit --offline window_port_reports_default_request_profile
30 S0-PLAT-002 covered cargo clippy -p fparkan-platform -p fparkan-platform-winit --all-targets --all-features --locked -- -D warnings
31 S0-PLAT-003 covered cargo test -p fparkan-platform-winit --offline smoke_window_plan_requires_native_handles_and_nonzero_extent smoke_window_plan_rejects_zero_extent
32 S0-PLAT-004 covered cargo test -p fparkan-platform-winit --offline smoke_window_app_requires_created_native_window smoke_window_app_rejects_synthetic_window_without_native_handles
33 S0-VK-001 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents
34 S0-VK-002 covered cargo test -p fparkan-render-vulkan --offline device_scoring_is_deterministic_and_prefers_discrete_unified_queue
35 S0-VK-003 covered cargo test -p fparkan-render-vulkan --offline portability_subset_is_reported_and_enabled_when_exposed
36 S0-VK-004 covered cargo test -p fparkan-render-vulkan --offline rejects_missing_graphics_present_swapchain_and_format
37 S0-VK-005 covered cargo test -p fparkan-render-vulkan --offline capability_report_json_is_stable
38 S0-VK-006 covered cargo test -p fparkan-render-vulkan --offline loader_probe_report_json_is_stable
39 S0-VK-007 covered cargo xtask policy
40 S0-VK-008 covered cargo test -p fparkan-render-vulkan --offline instance_plan_is_sorted_deduplicated_and_portability_aware
41 S0-VK-009 covered cargo test -p fparkan-render-vulkan --offline instance_plan_adds_portability_extension_when_requested
42 S0-VK-010 covered cargo test -p fparkan-render-vulkan --offline invalid_instance_extension_name_is_reported_before_loader_use
43 S0-VK-011 covered cargo test -p fparkan-render-vulkan --offline surface_plan_requires_native_handles
44 S0-VK-012 covered cargo test -p fparkan-render-vulkan --offline surface_plan_json_is_stable
45 S0-VK-013 covered cargo test -p fparkan-render-vulkan --offline static_surface_extension_name_is_decoded
46 S0-VK-014 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_prefers_srgb_mailbox_and_clamps_extent
47 S0-VK-015 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_uses_fifo_and_current_extent_fallbacks
48 S0-VK-016 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_rejects_missing_surface_data_and_empty_extent
49 S0-VK-017 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_json_and_recreation_reports_are_stable
50 S0-VK-018 covered cargo test -p fparkan-render-vulkan --offline triangle_shader_manifest_hashes_are_stable
51 S0-VK-019 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_report_json_is_stable
52 S0-VK-020 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_rejects_invalid_spirv_containers
53 S0-VK-021 covered cargo test -p fparkan-render-vulkan --offline frame_submission_plan_json_is_stable
54 S0-VK-022 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents
55 S0-VK-023 covered cargo test -p fparkan-vulkan-smoke --offline rejects_false_pass_without_full_evidence blocked_report_includes_shader_manifest_and_bootstrap_status
56 S0-VK-024 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_loader_probe formats_vulkan_api_version
57 S0-VK-025 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_instance_probe parses_instance_probe_as_loader_probe
58 S0-VK-026 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_window_probe rejects_passed_without_surface_probe parses_surface_probe_as_instance_probe
59 S0-VK-027 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_swapchain_recreation blocked_report_includes_shader_manifest_and_bootstrap_status
60 S0-VK-028 covered cargo test -p fparkan-vulkan-smoke --offline reports_rustc_host_triple blocked_report_includes_shader_manifest_and_bootstrap_status
61 S0-VK-029 covered cargo test -p xtask --offline native_smoke_audit_accepts_complete_three_platform_pass native_smoke_audit_rejects_blocked_or_incomplete_reports
62 S0-VK-030 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_with_failed_surface
63 S0-VK-031 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_selected_device
64 S0-VK-032 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_swapchain
65 S0-VK-033 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_logical_device
66 S0-VK-034 covered cargo test -p xtask --offline native_smoke_audit_accepts_complete_three_platform_pass native_smoke_audit_rejects_blocked_or_incomplete_reports
67 S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow
68 S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read
69 L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
70 L1-P2-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
71 L1-P1-NRES-002 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
72 L1-P2-NRES-002 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
73 L1-P1-NRES-003 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
74 L1-P2-NRES-003 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
75 L1-P1-RSLI-001 covered cargo test -p fparkan-rsli --offline licensed_part1_rsli_method_distribution_baseline
76 L1-P2-RSLI-001 covered cargo test -p fparkan-rsli --offline licensed_part2_rsli_method_distribution_baseline
77 L1-RSLI-QUIRK-001 covered cargo test -p fparkan-rsli --offline licensed_corpora_rsli_quirk_is_only_approved_interf8_tex
78 L1-P1-PATH-001 covered cargo test -p fparkan-corpus --offline licensed_part1_paths_stay_under_root
79 L1-P2-PATH-001 covered cargo test -p fparkan-corpus --offline licensed_part2_paths_stay_under_root
80 S1-NRES-001 covered cargo test -p fparkan-nres --offline parses_minimal_empty_archive
81 S1-NRES-002 covered cargo test -p fparkan-nres --offline one_entry_archive_uses_8_byte_alignment
82 S1-NRES-003 covered cargo test -p fparkan-nres --offline rejects_invalid_magic
83 S1-NRES-004 covered cargo test -p fparkan-nres --offline rejects_unsupported_version
84 S1-NRES-005 covered cargo test -p fparkan-nres --offline rejects_negative_entry_count
85 S1-NRES-006 covered cargo test -p fparkan-nres --offline rejects_directory_size_before_allocation
86 S1-NRES-007 covered cargo test -p fparkan-nres --offline rejects_total_size_mismatch
87 S1-NRES-008 covered cargo test -p fparkan-nres --offline rejects_directory_before_header
88 S1-NRES-009 covered cargo test -p fparkan-nres --offline rejects_payload_before_data_region
89 S1-NRES-010 covered cargo test -p fparkan-nres --offline rejects_payload_crossing_directory
90 S1-NRES-020 covered cargo test -p fparkan-nres --offline preserves_nonzero_unindexed_region
91 S1-NRES-021 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
92 S1-NRES-011 covered cargo test -p fparkan-nres --offline rejects_overlapping_payloads
93 S1-NRES-012 covered cargo test -p fparkan-nres --offline rejects_name_without_nul_terminator
94 S1-NRES-013 covered cargo test -p fparkan-nres --offline preserves_name_bytes_after_nul
95 S1-NRES-014 covered cargo test -p fparkan-nres --offline rejects_sort_index_out_of_range
96 S1-NRES-015 covered cargo test -p fparkan-nres --offline rejects_duplicate_sort_mapping
97 S1-NRES-016 covered cargo test -p fparkan-nres --offline binary_lookup_returns_original_entry_index
98 S1-NRES-017 covered cargo test -p fparkan-nres --offline compatible_profile_uses_linear_fallback_for_broken_mapping
99 S1-NRES-018 covered cargo test -p fparkan-nres --offline lookup_is_ascii_case_insensitive
100 S1-NRES-019 covered cargo test -p fparkan-nres --offline rejects_empty_names_and_resolves_duplicates_to_first_entry
101 S1-NRES-022 covered cargo test -p fparkan-nres --offline canonical_compact_roundtrip_preserves_entry_semantics
102 S1-NRES-023 covered cargo test -p fparkan-nres --offline editor_payload_update_rewrites_offsets_and_size
103 S1-NRES-024 covered cargo test -p fparkan-nres --offline editor_rename_rebuilds_search_mapping
104 S1-NRES-025 covered cargo test -p fparkan-nres --offline editor_rejects_invalid_authoring_names
105 S1-NRES-PROP-001 covered cargo test -p fparkan-nres --offline generated_archives_preserve_lossless_and_canonical_semantics
106 S1-NRES-PROP-002 covered cargo test -p fparkan-nres --offline generated_editor_updates_roundtrip
107 S1-NRES-FUZZ-001 covered cargo test -p fparkan-nres --offline arbitrary_small_inputs_do_not_panic_or_overallocate
108 S1-PATH-001 covered cargo test -p fparkan-path --offline normalizes_separators
109 S1-PATH-002 covered cargo test -p fparkan-path --offline ascii_casefold_does_not_unicode_fold
110 S1-PATH-003 covered cargo test -p fparkan-path --offline non_ascii_original_bytes_remain_stable
111 S1-PATH-004 covered cargo test -p fparkan-path --offline rejects_absolute_drive_and_nul_paths
112 S1-PATH-005 covered cargo test -p fparkan-path --offline rejects_escape
113 S1-PATH-006 covered cargo test -p fparkan-path --offline rejects_absolute_drive_and_nul_paths
114 S1-PATH-007 covered cargo test -p fparkan-path --offline join_under_keeps_normalized_path_below_root
115 S1-PATH-008 covered cargo test -p fparkan-path --offline original_separators_and_raw_bytes_are_preserved
116 S1-PATH-009 covered cargo test -p fparkan-path --offline accepts_non_utf8_legacy_bytes
117 S1-VFS-005 covered cargo test -p fparkan-vfs --offline memory_vfs_list_prefix_is_boundary_safe
118 S1-RSLI-001 covered cargo test -p fparkan-rsli --offline parses_minimal_empty_library
119 S1-RSLI-002 covered cargo test -p fparkan-rsli --offline rejects_invalid_header_fields
120 S1-RSLI-003 covered cargo test -p fparkan-rsli --offline rejects_entry_table_bounds
121 S1-RSLI-004 covered cargo test -p fparkan-rsli --offline table_xor_transform_uses_known_vector
122 S1-RSLI-005 covered cargo test -p fparkan-rsli --offline table_xor_transform_is_symmetric
123 S1-RSLI-006 covered cargo test -p fparkan-rsli --offline table_xor_state_spans_entries
124 S1-RSLI-007 covered cargo test -p fparkan-rsli --offline presorted_mapping_uses_valid_permutation
125 S1-RSLI-008 covered cargo test -p fparkan-rsli --offline compatible_profile_rebuilds_invalid_presorted_mapping
126 S1-RSLI-009 covered cargo test -p fparkan-rsli --offline stored_method_uses_exact_size
127 S1-RSLI-010 covered cargo test -p fparkan-rsli --offline xor_only_method_uses_entry_key
128 S1-RSLI-011 covered cargo test -p fparkan-rsli --offline lzss_method_decodes_literals_references_and_wrap
129 S1-RSLI-012 covered cargo test -p fparkan-rsli --offline xor_lzss_method_uses_entry_key
130 S1-RSLI-013 covered cargo test -p fparkan-rsli --offline adaptive_lzss_method_decodes_synthetic_vector
131 S1-RSLI-014 covered cargo test -p fparkan-rsli --offline xor_adaptive_lzss_method_decodes_synthetic_vector
132 S1-RSLI-015 covered cargo test -p fparkan-rsli --offline raw_deflate_method_expects_raw_stream_not_zlib_wrapper
133 S1-RSLI-016 covered cargo test -p fparkan-rsli --offline unknown_method_is_rejected_on_load
134 S1-RSLI-017 covered cargo test -p fparkan-rsli --offline decoded_size_mismatch_is_rejected
135 S1-RSLI-018 covered cargo test -p fparkan-rsli --offline ao_overlay_adjusts_effective_offsets
136 S1-RSLI-019 covered cargo test -p fparkan-rsli --offline invalid_ao_overlay_is_rejected
137 S1-RSLI-020 covered cargo test -p fparkan-rsli --offline rejects_registered_quirks_in_strict_profile
138 S1-RSLI-021 covered cargo test -p fparkan-rsli --offline named_deflate_eof_plus_one_quirk_accepts_only_approved_entry
139 S1-RSLI-022 covered cargo test -p fparkan-rsli --offline unknown_header_bytes_are_lossless
140 S1-RSLI-023 covered cargo test -p fparkan-rsli --offline no_op_lossless_roundtrip_preserves_bytes
141 S1-RSLI-PROP-001 covered cargo test -p fparkan-rsli --offline generated_supported_methods_decode_expected_bytes
142 S1-RSLI-FUZZ-001 covered cargo test -p fparkan-rsli --offline arbitrary_small_inputs_do_not_panic
143 S1-RES-001 covered cargo test -p fparkan-resource --offline cached_repository_reads_synthetic_nres
144 S1-RES-002 covered cargo test -p fparkan-resource --offline entry_handles_are_archive_qualified
145 S1-RES-003 covered cargo test -p fparkan-resource --offline archive_cache_and_decoded_payload_cache_evict_independently
146 S1-RES-004 covered cargo test -p fparkan-resource --offline entry_read_error_carries_archive_path_and_entry_name
147 S1-VFS-001 covered cargo test -p fparkan-vfs --offline memory_vfs_uses_exact_lookup
148 S1-VFS-002 covered cargo test -p fparkan-vfs --offline overlay_vfs_uses_first_matching_layer
149 S1-VFS-003 covered cargo test -p fparkan-vfs --offline directory_vfs_resolves_ascii_casefolded_segments
150 S1-VFS-004 covered cargo test -p fparkan-vfs --offline casefold_selector_reports_ambiguous_segments
151 L2-P1-UNIT-001 covered cargo test -p fparkan-prototype --offline licensed_corpora_unit_dat_parse_counts
152 L2-P2-UNIT-001 covered cargo test -p fparkan-prototype --offline licensed_corpora_unit_dat_parse_counts
153 L2-P1-REG-001 covered cargo test -p fparkan-prototype --offline licensed_corpora_registry_payloads_are_record_aligned
154 L2-P2-REG-001 covered cargo test -p fparkan-prototype --offline licensed_corpora_registry_payloads_are_record_aligned
155 L2-P1-GRAPH-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
156 L2-P2-GRAPH-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
157 L2-P1-INHERIT-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
158 L2-P2-INHERIT-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
159 L2-P1-NONGEO-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
160 L2-P2-NONGEO-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
161 L2-P1-GRAPH-002 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
162 L2-P2-GRAPH-002 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
163 S2-REG-001 covered cargo test -p fparkan-prototype --offline registry_zero_records_payload_is_empty
164 S2-REG-002 covered cargo test -p fparkan-prototype --offline registry_requires_record_multiple
165 S2-REG-003 covered cargo test -p fparkan-prototype --offline registry_preserves_bounded_name_tails_and_order
166 S2-REG-004 covered cargo test -p fparkan-prototype --offline registry_preserves_bounded_name_tails_and_order
167 S2-UNIT-001 covered cargo test -p fparkan-prototype --offline unit_zero_records_uses_exact_size
168 S2-UNIT-002 covered cargo test -p fparkan-prototype --offline unit_dat_one_record_uses_exact_size_formula
169 S2-UNIT-003 covered cargo test -p fparkan-prototype --offline unit_dat_rejects_truncated_record
170 S2-UNIT-004 covered cargo test -p fparkan-prototype --offline unit_dat_preserves_header_description_tail_and_parent_link
171 S2-UNIT-005 covered cargo test -p fparkan-prototype --offline unit_dat_preserves_header_description_tail_and_parent_link
172 S2-UNIT-006 covered cargo test -p fparkan-prototype --offline unit_dat_accepts_full_description_without_nul
173 S2-UNIT-007 covered cargo test -p fparkan-prototype --offline unit_dat_preserves_header_description_tail_and_parent_link unit_dat_preserves_positive_parent_link
174 S2-PROTO-001 covered cargo test -p fparkan-prototype --offline resolves_synthetic_objects_registry_model
175 S2-PROTO-002 covered cargo test -p fparkan-prototype --offline base_only_registry_entry_is_nongeometric
176 S2-PROTO-003 covered cargo test -p fparkan-prototype --offline graph_report_records_resolved_roots_and_failures
177 S2-PROTO-004 covered cargo test -p fparkan-prototype --offline missing_referenced_archive_reports_root_chain
178 S2-PROTO-005 covered cargo test -p fparkan-prototype --offline missing_referenced_resource_reports_root_chain
179 S2-PROTO-006 covered cargo test -p fparkan-prototype --offline objects_registry_inheritance_merges_parent_then_local_refs
180 S2-PROTO-007 covered cargo test -p fparkan-prototype --offline objects_registry_inheritance_resolves_multiple_levels
181 S2-PROTO-008 covered cargo test -p fparkan-prototype --offline objects_registry_inheritance_rejects_direct_cycle
182 S2-PROTO-009 covered cargo test -p fparkan-prototype --offline objects_registry_inheritance_rejects_indirect_cycle
183 S2-PROTO-010 covered cargo test -p fparkan-prototype --offline objects_registry_inheritance_rejects_depth_limit
184 S2-PROTO-011 covered cargo test -p fparkan-prototype --offline base_only_registry_entry_is_nongeometric
185 S2-PROTO-012 covered cargo test -p fparkan-prototype --offline first_existing_explicit_msh_is_selected_in_order
186 S2-PROTO-013 covered cargo test -p fparkan-prototype --offline invalid_referenced_msh_is_error
187 S2-PROTO-014 covered cargo test -p fparkan-prototype --offline resolver_cache_invalidates_when_archive_fingerprint_changes
188 S2-GRAPH-001 covered cargo test -p fparkan-prototype --offline graph_report_records_resolved_roots_and_failures
189 S2-GRAPH-002 covered cargo test -p fparkan-prototype --offline unit_dat_expands_components_in_order
190 S2-GRAPH-003 covered cargo test -p fparkan-assets --offline repository_plan_deduplicates_duplicate_visuals_but_graph_preserves_requests
191 S2-GRAPH-004 covered cargo test -p fparkan-prototype --offline graph_report_records_resolved_roots_and_failures
192 S2-GRAPH-005 covered cargo test -p fparkan-cli --offline prototype_graph_json_has_canonical_field_order
193 S2-GRAPH-006 covered cargo test -p fparkan-prototype --offline graph_report_records_resolved_roots_and_failures
194 S2-PROP-001 covered cargo test -p fparkan-prototype --offline generated_acyclic_prototype_graph_resolves_deterministically
195 S2-FUZZ-001 covered cargo test -p fparkan-prototype --offline arbitrary_unit_and_registry_bytes_are_bounded_and_panic_free
196 L3-P1-MSH-001 covered cargo test -p fparkan-msh --offline licensed_corpus_msh_assets_validate
197 L3-P2-MSH-001 covered cargo test -p fparkan-msh --offline licensed_corpus_msh_assets_validate
198 L3-P1-TEXM-001 covered cargo test -p fparkan-texm --offline licensed_corpus_texm_assets_validate_and_decode_mip0
199 L3-P2-TEXM-001 covered cargo test -p fparkan-texm --offline licensed_corpus_texm_assets_validate_and_decode_mip0
200 L3-P1-MAT0-001 covered cargo test -p fparkan-material --offline licensed_corpus_mat0_and_wear_parse
201 L3-P2-MAT0-001 covered cargo test -p fparkan-material --offline licensed_corpus_mat0_and_wear_parse
202 L3-P1-WEAR-001 covered cargo test -p fparkan-material --offline licensed_corpus_mat0_and_wear_parse
203 L3-P2-WEAR-001 covered cargo test -p fparkan-material --offline licensed_corpus_mat0_and_wear_parse
204 L3-P1-ASSET-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
205 L3-P2-ASSET-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
206 L3-P1-CAPTURE-001 covered cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures
207 L3-P2-CAPTURE-001 covered cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures
208 S3-WEAR-001 covered cargo test -p fparkan-material --offline wear_preserves_legacy_id_but_selects_by_index
209 S3-WEAR-002 covered cargo test -p fparkan-material --offline wear_requires_declared_rows
210 S3-WEAR-003 covered cargo test -p fparkan-material --offline wear_preserves_legacy_id_but_selects_by_index
211 S3-WEAR-004 covered cargo test -p fparkan-material --offline wear_requires_blank_separator_before_lightmaps
212 S3-WEAR-005 covered cargo test -p fparkan-material --offline wear_parses_lightmaps
213 S3-MAT0-001 covered cargo test -p fparkan-material --offline mat0_version_prefix_and_primary_texture
214 S3-MAT0-002 covered cargo test -p fparkan-material --offline mat0_accepts_zero_phase_material
215 S3-MAT0-003 covered cargo test -p fparkan-material --offline mat0_phase34_exact_framing_and_full_texture_name
216 S3-MAT0-004 covered cargo test -p fparkan-material --offline mat0_phase34_exact_framing_and_full_texture_name
217 S3-MAT0-005 covered cargo test -p fparkan-material --offline mat0_animation_block_has_no_implicit_padding
218 S3-MAT0-006 covered cargo test -p fparkan-material --offline mat0_rejects_animation_block_count_limit
219 S3-MSH-001 covered cargo test -p fparkan-msh --offline canonical_stream_set_is_independent_of_entry_order
220 S3-MSH-002 covered cargo test -p fparkan-msh --offline missing_required_stream_is_error
221 S3-MSH-003 covered cargo test -p fparkan-msh --offline duplicate_required_stream_type_is_error
222 S3-MSH-004 covered cargo test -p fparkan-msh --offline node38_stride_is_exact
223 S3-MSH-005 covered cargo test -p fparkan-msh --offline node38_uses_three_by_five_slot_mapping_and_absent_marker
224 S3-MSH-006 covered cargo test -p fparkan-msh --offline node38_uses_three_by_five_slot_mapping_and_absent_marker
225 S3-MSH-007 covered cargo test -p fparkan-msh --offline type2_header_and_slot_tail_framing_are_exact
226 S3-MSH-008 covered cargo test -p fparkan-msh --offline type2_header_and_slot_tail_framing_are_exact
227 S3-MSH-009 covered cargo test -p fparkan-msh --offline slot_batch_range_out_of_bounds_is_error
228 S3-MSH-010 covered cargo test -p fparkan-msh --offline vertex_stream_strides_are_exact
229 S3-MSH-011 covered cargo test -p fparkan-msh --offline base_vertex_plus_index_must_reference_position
230 S3-MSH-012 covered cargo test -p fparkan-msh --offline batch20_uses_unaligned_field_offsets
231 S3-MSH-013 covered cargo test -p fparkan-msh --offline auxiliary_and_extended_streams_are_preserved
232 S3-MSH-014 covered cargo test -p fparkan-msh --offline auxiliary_and_extended_streams_are_preserved
233 S3-MSH-015 covered cargo test -p fparkan-msh --offline mtcheck_variant_is_preserved_and_recognized
234 S3-MSH-016 covered cargo test -p fparkan-msh --offline invalid_bounds_are_rejected
235 S3-MSH-017 covered cargo test -p fparkan-msh --offline slot_batch_range_out_of_bounds_is_error
236 S3-MSH-PROP-001 covered cargo test -p fparkan-msh --offline canonical_stream_set_is_independent_of_entry_order
237 S3-MSH-FUZZ-001 covered cargo test -p fparkan-msh --offline arbitrary_nested_payloads_are_bounded_and_panic_free
238 S3-TEXM-001 covered cargo test -p fparkan-texm --offline decodes_all_synthetic_formats
239 S3-TEXM-002 covered cargo test -p fparkan-texm --offline rejects_zero_dimensions
240 S3-TEXM-003 covered cargo test -p fparkan-texm --offline non_power_of_two_mip_chain_clamps_each_dimension
241 S3-TEXM-004 covered cargo test -p fparkan-texm --offline rejects_mip_size_arithmetic_overflow_or_oob
242 S3-TEXM-005 covered cargo test -p fparkan-texm --offline indexed_palette_requires_exact_1024_bytes
243 S3-TEXM-006 covered cargo test -p fparkan-texm --offline channel_expansion_boundary_values_are_stable
244 S3-TEXM-007 covered cargo test -p fparkan-texm --offline rgb888x_preserves_fourth_disk_byte_but_outputs_opaque_alpha
245 S3-TEXM-008 covered cargo test -p fparkan-texm --offline page_tail_absent_and_exact_rect_framing
246 S3-TEXM-009 covered cargo test -p fparkan-texm --offline page_tail_absent_and_exact_rect_framing
247 S3-TEXM-010 covered cargo test -p fparkan-texm --offline invalid_page_magic_size_and_trailing_bytes_are_rejected
248 S3-TEXM-011 covered cargo test -p fparkan-texm --offline invalid_page_magic_size_and_trailing_bytes_are_rejected
249 S3-TEXM-012 covered cargo test -p fparkan-texm --offline exposes_mip_views_and_upload_plan_without_mutating_document
250 S3-TEXM-013 covered cargo test -p fparkan-texm --offline page_scaling_uses_floor_origin_and_ceil_end_policy
251 S3-TEXM-FUZZ-001 covered cargo test -p fparkan-texm --offline arbitrary_texm_payloads_do_not_panic
252 S3-MAT0-007 covered cargo test -p fparkan-material --offline mat0_rejects_trailing_bytes
253 S3-MAT-RESOLVE-001 covered cargo test -p fparkan-material --offline resolve_material_uses_exact_match
254 S3-MAT-RESOLVE-002 covered cargo test -p fparkan-material --offline resolve_material_falls_back_to_default
255 S3-MAT-RESOLVE-003 covered cargo test -p fparkan-material --offline resolve_material_uses_first_entry_only_after_missing_default
256 S3-MAT-RESOLVE-004 covered cargo test -p fparkan-material --offline resolve_material_empty_texture_means_untextured
257 S3-MAT-RESOLVE-005 covered cargo test -p fparkan-material --offline resolve_material_without_lightmap_keeps_lightmap_absent
258 S3-RENDER-001 covered cargo test -p fparkan-render --offline one_snapshot_draw_produces_one_draw_command
259 S3-RENDER-002 covered cargo test -p fparkan-render --offline material_index_maps_through_resolved_material_slots
260 S3-RENDER-003 covered cargo test -p fparkan-render --offline node_transform_is_retained
261 S3-RENDER-004 covered cargo test -p fparkan-render --offline command_order_uses_phase_then_stable_key
262 S3-RENDER-005 covered cargo test -p fparkan-render --offline command_capture_independent_of_snapshot_construction_order
263 S3-RENDER-006 covered cargo test -p fparkan-render --offline invalid_range_returns_contextual_error
264 S3-RENDER-007 covered cargo test -p fparkan-render --offline capture_is_stable
265 S3-RENDER-008 covered cargo test -p fparkan-render --offline recording_backend_stores_captures
266 S3-RENDER-009 covered cargo xtask policy
267 S3-GL-001 omitted permanent macOS Desktop GL 3.3 adapter is not implemented; historical CGL probe is retained as external evidence only
268 S3-GL-002 omitted outside the current macOS-focused goal scope; GLES2 remains documented for portable/non-macOS targets
269 S3-GL-003 blocked legacy fparkan-render-gl adapter removed while Vulkan renderer path is being brought in as the stage-3 backend
270 S3-VIEWER-001 covered cargo test -p fparkan-viewer --offline model_fixture_uses_viewer_service_and_render_commands
271 S4-ANIM-001 covered cargo test -p fparkan-animation --offline anim_key24_decodes_signed_quaternion
272 S4-ANIM-002 covered cargo test -p fparkan-animation --offline frame_map_decodes_u16_and_uses_attr_frame_count
273 S4-ANIM-003 covered cargo test -p fparkan-animation --offline frame_map_falls_back_when_absent_or_invalid
274 S4-ANIM-004 covered cargo test -p fparkan-animation --offline frame_map_falls_back_when_absent_or_invalid
275 S4-ANIM-005 covered cargo test -p fparkan-animation --offline exact_key_time_returns_exact_pose
276 S4-ANIM-006 covered cargo test -p fparkan-animation --offline pose_track_blends_translation_and_rotation
277 S4-ANIM-007 covered cargo test -p fparkan-animation --offline quaternion_shortest_path_sign_flip_is_stable
278 S4-ANIM-008 covered cargo test -p fparkan-animation --offline zero_or_degenerate_key_interval_is_rejected
279 S4-ANIM-009 omitted current sampler is a portable reference path; runtime-captured x87 parity vectors are not implemented in the macOS-focused scope
280 S4-ANIM-010 omitted current sampler accepts the profile marker but does not implement an independent x87 compatibility path
281 S4-ANIM-011 covered cargo test -p fparkan-animation --offline blend_optional_pose_uses_valid_side
282 S4-ANIM-012 covered cargo test -p fparkan-animation --offline hierarchy_evaluates_parent_before_child_and_rejects_cycles
283 S4-ANIM-013 covered cargo test -p fparkan-animation --offline hierarchy_evaluates_parent_before_child_and_rejects_cycles
284 S4-ANIM-014 covered cargo test -p fparkan-animation --offline pose_capture_uses_float_bits
285 S4-ANIM-PROP-001 covered cargo test -p fparkan-animation --offline generated_valid_quaternions_remain_finite
286 S4-MAT-001 covered cargo test -p fparkan-material --offline material_modes_zero_to_three_choose_stable_phases
287 S4-MAT-002 covered cargo test -p fparkan-material --offline material_exact_key_boundary_selects_exact_phase
288 S4-MAT-003 covered cargo test -p fparkan-material --offline material_interpolation_mask_affects_only_selected_fields
289 S4-MAT-004 covered cargo test -p fparkan-material --offline material_timeline_profile_cases_are_evidence_labeled
290 S4-MAT-005 covered cargo test -p fparkan-material --offline material_random_offset_uses_material_stream_only
291 S4-MAT-006 covered cargo test -p fparkan-material --offline material_same_seed_and_timeline_produces_same_phase_capture
292 S4-FX-001 covered cargo test -p fparkan-fx --offline header_is_exactly_sixty_bytes_and_command_sizes_are_fixed
293 S4-FX-002 covered cargo test -p fparkan-fx --offline header_is_exactly_sixty_bytes_and_command_sizes_are_fixed
294 S4-FX-011 covered cargo test -p fparkan-fx --offline header_is_exactly_sixty_bytes_and_command_sizes_are_fixed
295 S4-FX-012 covered cargo test -p fparkan-fx --offline opcode6_four_byte_command_is_accepted
296 S4-FX-013 covered cargo test -p fparkan-fx --offline rejects_unknown_opcode_at_command_index
297 S4-FX-014 covered cargo test -p fparkan-fx --offline rejects_command_count_that_exceeds_payload
298 S4-FX-015 covered cargo test -p fparkan-fx --offline rejects_trailing_bytes_after_command_stream
299 S4-FX-016 covered cargo test -p fparkan-fx --offline fixed_resource_refs_preserve_tails
300 S4-FX-017 covered cargo test -p fparkan-fx --offline missing_dependency_error_contains_effect_command_archive_and_name
301 S4-FX-018 omitted FX create/update/emit is documented as a deterministic reference stub until runtime-captured lifecycle semantics are implemented
302 S4-FX-019 omitted FX update side-effect contract is stub-level only and not accepted as original runtime parity evidence
303 S4-FX-020 omitted FX emit side-effect contract is stub-level only and not accepted as original runtime parity evidence
304 S4-FX-021 omitted FX command order is deterministic for the stub but original runtime ordering evidence is not implemented
305 S4-FX-022 omitted FX lifecycle controls are deterministic for the stub but original stop/restart/end semantics are not implemented
306 S4-FX-023 omitted FX emission capture is deterministic for the stub but lacks runtime-captured parity evidence
307 S4-FX-024 omitted FX RNG accounting is not implemented beyond the reference stub and must not count as compatibility coverage
308 S4-FX-FUZZ-001 covered cargo test -p fparkan-fx --offline arbitrary_command_streams_are_bounded_and_panic_free
309 L4-P1-ANIM-001 covered cargo test -p fparkan-msh --offline licensed_corpus_animation_streams_sample_approved_pose_captures
310 L4-P2-ANIM-001 covered cargo test -p fparkan-msh --offline licensed_corpus_animation_streams_sample_approved_pose_captures
311 L4-P1-CAPTURE-001 covered cargo test -p fparkan-msh --offline licensed_corpus_animation_streams_sample_approved_pose_captures
312 L4-P2-CAPTURE-001 covered cargo test -p fparkan-msh --offline licensed_corpus_animation_streams_sample_approved_pose_captures
313 L4-P1-FX-001 covered cargo test -p fparkan-fx --offline licensed_corpus_fxid_exact_eof_and_distribution
314 L4-P2-FX-001 covered cargo test -p fparkan-fx --offline licensed_corpus_fxid_exact_eof_and_distribution
315 L4-P1-FX-002 covered cargo test -p fparkan-fx --offline licensed_corpus_fxid_exact_eof_and_distribution
316 L4-P2-FX-002 covered cargo test -p fparkan-fx --offline licensed_corpus_fxid_exact_eof_and_distribution
317 L4-FX-OP6-001 covered cargo test -p fparkan-fx --offline licensed_corpus_fxid_exact_eof_and_distribution
318 L4-P1-EFFECT-001 omitted licensed FX emission capture currently covers deterministic reference-stub output, not original runtime-captured effect parity
319 L4-P2-EFFECT-001 omitted licensed FX emission capture currently covers deterministic reference-stub output, not original runtime-captured effect parity
320 S5-LMESH-001 covered cargo test -p fparkan-terrain-format --offline land_msh_required_streams_are_order_independent_and_stride_checked
321 S5-LMESH-002 covered cargo test -p fparkan-terrain-format --offline land_msh_required_streams_are_order_independent_and_stride_checked
322 S5-LMESH-003 covered cargo test -p fparkan-terrain-format --offline decodes_minimal_land_msh
323 S5-LMESH-004 covered cargo test -p fparkan-terrain-format --offline decodes_minimal_land_msh
324 S5-LMESH-005 covered cargo test -p fparkan-terrain-format --offline rejects_invalid_vertex_index
325 S5-LMESH-006 covered cargo test -p fparkan-terrain-format --offline rejects_invalid_neighbor_index
326 S5-LMESH-007 covered cargo test -p fparkan-terrain-format --offline face_layout_preserves_tail_and_all_surface_mask_mappings_are_explicit
327 S5-LMESH-008 covered cargo test -p fparkan-terrain-format --offline face_layout_preserves_tail_and_all_surface_mask_mappings_are_explicit
328 S5-LMESH-009 covered cargo test -p fparkan-terrain-format --offline face_layout_preserves_tail_and_all_surface_mask_mappings_are_explicit
329 S5-LMAP-001 covered cargo test -p fparkan-terrain-format --offline decodes_minimal_land_map
330 S5-LMAP-002 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
331 S5-LMAP-003 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
332 S5-LMAP-004 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
333 S5-LMAP-005 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
334 S5-LMAP-006 covered cargo test -p fparkan-terrain-format --offline rejects_invalid_areal_link
335 S5-LMAP-007 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
336 S5-LMAP-008 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
337 S5-LMAP-009 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
338 S5-LMAP-010 covered cargo test -p fparkan-terrain-format --offline rejects_invalid_grid_area_ref
339 S5-LMAP-011 covered cargo test -p fparkan-terrain-format --offline land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
340 S5-TERRAIN-001 covered cargo test -p fparkan-terrain --offline locates_areal_and_routes_synthetic_neighbors
341 S5-TERRAIN-002 covered cargo test -p fparkan-terrain --offline locates_areal_and_routes_synthetic_neighbors
342 S5-TERRAIN-003 covered cargo test -p fparkan-terrain --offline missing_start_or_goal_returns_no_route
343 S5-TERRAIN-004 covered cargo test -p fparkan-terrain --offline synthetic_surface_height_and_raycast_work
344 S5-TMA-001 covered cargo test -p fparkan-mission-format --offline minimal_synthetic_exact_eof
345 S5-TMA-002 covered cargo test -p fparkan-mission-format --offline lp_string_does_not_consume_implicit_nul
346 S5-TMA-003 covered cargo test -p fparkan-mission-format --offline path_ids_retain_nonsequential_order_and_truncated_points_fail
347 S5-TMA-004 covered cargo test -p fparkan-mission-format --offline path_ids_retain_nonsequential_order_and_truncated_points_fail
348 S5-TMA-005 covered cargo test -p fparkan-mission-format --offline clan_modes_one_to_three_and_spatial_mode_zero_decode
349 S5-TMA-006 covered cargo test -p fparkan-mission-format --offline clan_modes_one_to_three_and_spatial_mode_zero_decode
350 S5-TMA-007 covered cargo test -p fparkan-mission-format --offline unknown_clan_mode_nonfinite_transform_and_trailing_bytes_are_rejected
351 S5-TMA-008 covered cargo test -p fparkan-mission-format --offline synthetic_standard_clan_and_object_preserve_ordered_properties
352 S5-TMA-009 covered cargo test -p fparkan-mission-format --offline synthetic_standard_clan_and_object_preserve_ordered_properties
353 S5-TMA-010 covered cargo test -p fparkan-mission-format --offline synthetic_standard_clan_and_object_preserve_ordered_properties
354 S5-TMA-011 covered cargo test -p fparkan-mission-format --offline unknown_clan_mode_nonfinite_transform_and_trailing_bytes_are_rejected
355 S5-TMA-012 covered cargo test -p fparkan-mission-format --offline description_and_extras_are_exact_raw_records
356 S5-TMA-013 covered cargo test -p fparkan-mission-format --offline description_and_extras_are_exact_raw_records
357 S5-TMA-014 covered cargo test -p fparkan-mission-format --offline description_and_extras_are_exact_raw_records
358 S5-TMA-015 covered cargo test -p fparkan-mission-format --offline unknown_clan_mode_nonfinite_transform_and_trailing_bytes_are_rejected
359 S5-TMA-016 covered cargo test -p fparkan-mission-format --offline signatures_inside_strings_do_not_create_records_and_truncations_are_bounded
360 S5-TMA-017 covered cargo test -p fparkan-mission-format --offline signatures_inside_strings_do_not_create_records_and_truncations_are_bounded
361 S5-TMA-PROP-001 covered cargo test -p fparkan-mission-format --offline generated_valid_documents_and_arbitrary_inputs_are_bounded
362 S5-TMA-FUZZ-001 covered cargo test -p fparkan-mission-format --offline generated_valid_documents_and_arbitrary_inputs_are_bounded
363 S5-LOAD-001 covered cargo test -p fparkan-runtime --offline load_trace_records_preparation_before_registration_and_raw_transforms
364 S5-LOAD-002 covered cargo test -p fparkan-runtime --offline missing_map_and_missing_reachable_resource_fail_before_registration
365 S5-LOAD-003 covered cargo test -p fparkan-runtime --offline missing_map_and_missing_reachable_resource_fail_before_registration
366 S5-LOAD-004 covered cargo test -p fparkan-material --offline resolve_material_falls_back_to_default
367 S5-LOAD-005 covered cargo test -p fparkan-runtime --offline load_trace_records_preparation_before_registration_and_raw_transforms
368 S5-LOAD-006 covered cargo test -p fparkan-runtime --offline load_mission_requires_vfs_and_keeps_world_unchanged_on_error
369 S5-LOAD-007 covered cargo test -p fparkan-runtime --offline registration_phase_failure_uses_normal_teardown_and_keeps_engine_world
370 S5-LOAD-008 covered cargo test -p fparkan-runtime --offline load_trace_records_preparation_before_registration_and_raw_transforms
371 S5-LOAD-009 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
372 S5-LOAD-010 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
373 S5-WORLD-001 covered cargo test -p fparkan-world --offline construct_register_and_hash_are_stable
374 S5-WORLD-002 covered cargo test -p fparkan-world --offline registration_sequence_stale_and_duplicate_original_contracts
375 S5-WORLD-003 covered cargo test -p fparkan-world --offline registration_sequence_stale_and_duplicate_original_contracts
376 S5-WORLD-004 covered cargo test -p fparkan-world --offline registration_sequence_stale_and_duplicate_original_contracts
377 S5-WORLD-005 covered cargo test -p fparkan-world --offline identity_metadata_keeps_original_mirror_and_owner_distinct
378 S5-WORLD-006 covered cargo test -p fparkan-world --offline command_fifo_and_deferred_delete_during_calculation
379 S5-WORLD-007 covered cargo test -p fparkan-world --offline registration_sequence_stale_and_duplicate_original_contracts
380 S5-WORLD-008 covered cargo test -p fparkan-world --offline command_fifo_and_deferred_delete_during_calculation
381 S5-WORLD-009 covered cargo test -p fparkan-world --offline command_fifo_and_deferred_delete_during_calculation
382 S5-WORLD-010 covered cargo test -p fparkan-world --offline command_fifo_and_deferred_delete_during_calculation
383 S5-WORLD-011 covered cargo test -p fparkan-world --offline command_fifo_and_deferred_delete_during_calculation
384 S5-WORLD-012 covered cargo test -p fparkan-world --offline fixed_step_pause_and_long_determinism_are_stable
385 S5-WORLD-013 covered cargo test -p fparkan-world --offline fixed_step_pause_and_long_determinism_are_stable
386 S5-WORLD-014 covered cargo test -p fparkan-world --offline snapshot_hash_determinism_and_immutability
387 S5-WORLD-015 covered cargo test -p fparkan-world --offline snapshot_hash_determinism_and_immutability
388 S5-WORLD-016 covered cargo test -p fparkan-world --offline fixed_step_pause_and_long_determinism_are_stable
389 S5-WORLD-017 covered cargo test -p fparkan-world --offline render_disabled_does_not_change_hash_end_callbacks_and_shutdown_order
390 S5-WORLD-018 covered cargo test -p fparkan-world --offline render_disabled_does_not_change_hash_end_callbacks_and_shutdown_order
391 S5-WORLD-019 covered cargo test -p fparkan-world --offline render_disabled_does_not_change_hash_end_callbacks_and_shutdown_order
392 S5-WORLD-PROP-001 covered cargo test -p fparkan-world --offline generated_command_delete_sequences_preserve_registry_invariants
393 L5-P1-LMESH-001 covered cargo test -p fparkan-terrain-format --offline licensed_corpus_land_msh_validate
394 L5-P2-LMESH-001 covered cargo test -p fparkan-terrain-format --offline licensed_corpus_land_msh_validate
395 L5-P1-LMAP-001 covered cargo test -p fparkan-terrain-format --offline licensed_corpus_land_map_validate
396 L5-P2-LMAP-001 covered cargo test -p fparkan-terrain-format --offline licensed_corpus_land_map_validate
397 L5-LMAP-POLY-001 covered cargo test -p fparkan-terrain-format --offline licensed_corpus_land_map_validate land_map_prefix_absent_links_polygon_blocks_grid_size_and_exact_eof
398 L5-P1-TMA-001 covered cargo test -p fparkan-mission-format --offline licensed_corpus_tma_validate
399 L5-P2-TMA-001 covered cargo test -p fparkan-mission-format --offline licensed_corpus_tma_validate
400 L5-P1-MISSION-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
401 L5-P2-MISSION-001 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
402 L5-P1-MISSION-002 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
403 L5-P2-MISSION-002 covered cargo test -p fparkan-runtime --offline licensed_corpora_load_all_mission_foundations
404 L5-P1-HEADLESS-001 covered cargo test -p fparkan-runtime --offline selected_is_and_is2_missions_execute_10000_deterministic_ticks
405 L5-P2-HEADLESS-001 covered cargo test -p fparkan-runtime --offline selected_is_and_is2_missions_execute_10000_deterministic_ticks
406 L5-P1-RENDER-001 covered cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures
407 L5-P2-RENDER-001 covered cargo test -p fparkan-game --offline selected_is_and_is2_missions_produce_approved_render_captures
408 L3-DEVICE-001 omitted outside the current macOS-focused goal scope; RG40XX-capable device/profile evidence remains documented for the portable target scope
409 L5-RG40-001 omitted outside the current macOS-focused goal scope; RG40XX 640x480 on-device mission smoke/performance/memory evidence remains documented for the portable target scope

Some files were not shown because too many files have changed in this diff Show More