crimsonland 0.1.0.dev12__tar.gz → 0.1.0.dev14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. crimsonland-0.1.0.dev14/PKG-INFO +197 -0
  2. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/pyproject.toml +6 -1
  3. crimsonland-0.1.0.dev14/readme.md +184 -0
  4. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/assets_fetch.py +23 -8
  5. crimsonland-0.1.0.dev14/src/crimson/frontend/high_scores_layout.py +26 -0
  6. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/frontend/menu.py +22 -20
  7. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/frontend/panels/base.py +14 -26
  8. crimsonland-0.1.0.dev14/src/crimson/frontend/panels/controls.py +219 -0
  9. crimsonland-0.1.0.dev14/src/crimson/frontend/panels/credits.py +221 -0
  10. crimsonland-0.1.0.dev14/src/crimson/frontend/panels/databases.py +307 -0
  11. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/frontend/panels/options.py +4 -3
  12. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/frontend/panels/play_game.py +4 -4
  13. crimsonland-0.1.0.dev14/src/crimson/frontend/panels/stats.py +308 -0
  14. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/game.py +219 -81
  15. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/modes/quest_mode.py +10 -9
  16. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/modes/survival_mode.py +10 -9
  17. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/modes/tutorial_mode.py +10 -4
  18. crimsonland-0.1.0.dev14/src/crimson/ui/menu_panel.py +127 -0
  19. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/ui/perk_menu.py +54 -89
  20. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/ui/quest_results.py +24 -18
  21. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/perk_menu_debug.py +2 -2
  22. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/perks.py +2 -2
  23. crimsonland-0.1.0.dev12/PKG-INFO +0 -9
  24. crimsonland-0.1.0.dev12/src/crimson/frontend/panels/controls.py +0 -130
  25. crimsonland-0.1.0.dev12/src/crimson/frontend/panels/stats.py +0 -349
  26. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/__init__.py +0 -0
  27. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/atlas.py +0 -0
  28. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/audio_router.py +0 -0
  29. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/bonuses.py +0 -0
  30. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/camera.py +0 -0
  31. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/cli.py +0 -0
  32. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/creatures/__init__.py +0 -0
  33. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/creatures/ai.py +0 -0
  34. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/creatures/anim.py +0 -0
  35. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/creatures/damage.py +0 -0
  36. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/creatures/runtime.py +0 -0
  37. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/creatures/spawn.py +0 -0
  38. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/debug.py +0 -0
  39. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/demo.py +0 -0
  40. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/demo_trial.py +0 -0
  41. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/effects.py +0 -0
  42. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/effects_atlas.py +0 -0
  43. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/frontend/__init__.py +0 -0
  44. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/frontend/assets.py +0 -0
  45. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/frontend/boot.py +0 -0
  46. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/frontend/panels/__init__.py +0 -0
  47. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/frontend/panels/mods.py +0 -0
  48. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/frontend/pause_menu.py +0 -0
  49. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/frontend/transitions.py +0 -0
  50. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/game_modes.py +0 -0
  51. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/game_world.py +0 -0
  52. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/gameplay.py +0 -0
  53. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/input_codes.py +0 -0
  54. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/modes/__init__.py +0 -0
  55. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/modes/base_gameplay_mode.py +0 -0
  56. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/modes/rush_mode.py +0 -0
  57. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/modes/typo_mode.py +0 -0
  58. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/paths.py +0 -0
  59. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/perks.py +0 -0
  60. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/persistence/__init__.py +0 -0
  61. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/persistence/highscores.py +0 -0
  62. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/persistence/save_status.py +0 -0
  63. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/player_damage.py +0 -0
  64. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/projectiles.py +0 -0
  65. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/quests/__init__.py +0 -0
  66. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/quests/helpers.py +0 -0
  67. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/quests/registry.py +0 -0
  68. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/quests/results.py +0 -0
  69. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/quests/runtime.py +0 -0
  70. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/quests/tier1.py +0 -0
  71. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/quests/tier2.py +0 -0
  72. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/quests/tier3.py +0 -0
  73. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/quests/tier4.py +0 -0
  74. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/quests/tier5.py +0 -0
  75. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/quests/timeline.py +0 -0
  76. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/quests/types.py +0 -0
  77. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/render/__init__.py +0 -0
  78. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/render/terrain_fx.py +0 -0
  79. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/render/world_renderer.py +0 -0
  80. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/sim/__init__.py +0 -0
  81. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/sim/world_defs.py +0 -0
  82. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/sim/world_state.py +0 -0
  83. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/terrain_assets.py +0 -0
  84. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/tutorial/__init__.py +0 -0
  85. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/tutorial/timeline.py +0 -0
  86. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/typo/__init__.py +0 -0
  87. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/typo/names.py +0 -0
  88. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/typo/player.py +0 -0
  89. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/typo/spawns.py +0 -0
  90. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/typo/typing.py +0 -0
  91. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/ui/__init__.py +0 -0
  92. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/ui/cursor.py +0 -0
  93. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/ui/demo_trial_overlay.py +0 -0
  94. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/ui/game_over.py +0 -0
  95. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/ui/hud.py +0 -0
  96. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/ui/shadow.py +0 -0
  97. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/__init__.py +0 -0
  98. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/aim_debug.py +0 -0
  99. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/animations.py +0 -0
  100. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/arsenal_debug.py +0 -0
  101. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/audio_bootstrap.py +0 -0
  102. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/bonuses.py +0 -0
  103. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/camera_debug.py +0 -0
  104. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/camera_shake.py +0 -0
  105. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/corpse_stamp_debug.py +0 -0
  106. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/decals_debug.py +0 -0
  107. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/empty.py +0 -0
  108. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/fonts.py +0 -0
  109. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/game_over.py +0 -0
  110. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/ground.py +0 -0
  111. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/lighting_debug.py +0 -0
  112. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/particles.py +0 -0
  113. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/player.py +0 -0
  114. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/player_sprite_debug.py +0 -0
  115. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/projectile_fx.py +0 -0
  116. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/projectile_render_debug.py +0 -0
  117. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/projectiles.py +0 -0
  118. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/quest_title_overlay.py +0 -0
  119. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/registry.py +0 -0
  120. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/rush.py +0 -0
  121. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/small_font_debug.py +0 -0
  122. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/spawn_plan.py +0 -0
  123. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/sprites.py +0 -0
  124. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/survival.py +0 -0
  125. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/terrain.py +0 -0
  126. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/ui.py +0 -0
  127. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/views/wicons.py +0 -0
  128. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/weapon_sfx.py +0 -0
  129. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/crimson/weapons.py +0 -0
  130. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/__init__.py +0 -0
  131. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/app.py +0 -0
  132. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/assets.py +0 -0
  133. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/audio.py +0 -0
  134. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/config.py +0 -0
  135. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/console.py +0 -0
  136. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/fonts/__init__.py +0 -0
  137. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/fonts/grim_mono.py +0 -0
  138. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/fonts/small.py +0 -0
  139. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/input.py +0 -0
  140. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/jaz.py +0 -0
  141. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/math.py +0 -0
  142. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/music.py +0 -0
  143. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/paq.py +0 -0
  144. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/rand.py +0 -0
  145. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/sfx.py +0 -0
  146. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/sfx_map.py +0 -0
  147. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/terrain_render.py +0 -0
  148. {crimsonland-0.1.0.dev12 → crimsonland-0.1.0.dev14}/src/grim/view.py +0 -0
@@ -0,0 +1,197 @@
1
+ Metadata-Version: 2.3
2
+ Name: crimsonland
3
+ Version: 0.1.0.dev14
4
+ Requires-Dist: construct>=2.10.70
5
+ Requires-Dist: pillow>=12.1.0
6
+ Requires-Dist: platformdirs>=4.5.1
7
+ Requires-Dist: raylib>=5.5.0.4
8
+ Requires-Dist: typer>=0.21.1
9
+ Requires-Python: >=3.13
10
+ Project-URL: Documentation, https://crimson.banteg.xyz/
11
+ Project-URL: Repository, https://github.com/banteg/crimson
12
+ Description-Content-Type: text/markdown
13
+
14
+ # Crimsonland 1.9.93 decompilation + rewrite
15
+
16
+ This repository is a **reverse engineering + high‑fidelity reimplementation** of **Crimsonland 1.9.93 (2003)**.
17
+
18
+ - **Target build:** `v1.9.93` (GOG "Crimsonland Classic") — see [docs/provenance.md](docs/provenance.md) for exact hashes.
19
+ - **Rewrite:** a runnable reference implementation in **Python + raylib** under `src/`.
20
+ - **Analysis:** decompiles, name/type maps, and runtime evidence under `analysis/`.
21
+ - **Docs:** long-form notes and parity tracking under `docs/` (start at [docs/index.md](docs/index.md)).
22
+
23
+ The north star is **behavioral parity** with the original Windows build: timings, RNG, UI/layout quirks, asset decoding, and gameplay rules should match as closely as practical.
24
+
25
+ **[Read the full story](https://banteg.xyz/posts/crimsonland/)** of how this project came together: reverse engineering workflow, custom asset formats, AI-assisted decompilation, and game preservation philosophy.
26
+
27
+ ---
28
+
29
+ ## Quick start
30
+
31
+ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) package manager.
32
+
33
+ ### Run the latest packaged build
34
+
35
+ If you just want to play the rewrite:
36
+
37
+ ```bash
38
+ uvx crimsonland@latest
39
+ ```
40
+
41
+ ### Run from a checkout
42
+
43
+ ```bash
44
+ gh repo clone banteg/crimson
45
+ cd crimson
46
+ uv run crimson
47
+ ```
48
+
49
+ ### Keep runtime files local to the repo
50
+
51
+ By default, runtime files (e.g. `crimson.cfg`, `game.cfg`, highscores, logs, downloaded PAQs) live in your per-user data dir.
52
+ To keep everything under this checkout:
53
+
54
+ ```bash
55
+ export CRIMSON_RUNTIME_DIR="$PWD/artifacts/runtime"
56
+ mkdir -p artifacts/runtime
57
+ uv run crimson
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Assets + binaries
63
+
64
+ There are two separate “inputs” to this repo:
65
+
66
+ 1. **Runtime assets for the rewrite** (PAQ archives)
67
+ 2. **Original Windows binaries for reverse engineering** (`crimsonland.exe`, `grim.dll`, …)
68
+
69
+ We keep them out of git and expect a local layout like:
70
+
71
+ ```text
72
+ game_bins/
73
+ crimsonland/
74
+ 1.9.93-gog/
75
+ crimsonland.exe
76
+ grim.dll
77
+ crimson.paq
78
+ music.paq
79
+ sfx.paq
80
+ artifacts/
81
+ runtime/ # optional: where you run the rewrite (cfg/status/paqs)
82
+ assets/ # optional: extracted PAQs for inspection/tools
83
+ ```
84
+
85
+ ### Running the rewrite
86
+
87
+ The rewrite loads the assets from original archives:
88
+
89
+ - `crimson.paq`
90
+ - `music.paq`
91
+ - `sfx.paq`
92
+
93
+ ### Extracted assets
94
+
95
+ For inspection/diffs/tools, you can extract PAQs into a filesystem tree:
96
+
97
+ ```bash
98
+ uv run crimson extract crimsonland_1.9.93 artifacts/assets
99
+ ```
100
+
101
+ Same as the original, many loaders can work from either:
102
+
103
+ - **PAQ-backed assets** (preferred when available), or
104
+ - the **extracted filesystem layout** under `artifacts/assets/`.
105
+
106
+ ---
107
+
108
+ ## CLI cheat sheet
109
+
110
+ Everything is exposed via the `crimson` CLI (alias: `crimsonland`):
111
+
112
+ ```bash
113
+ uv run crimson # run the game (default command)
114
+ uv run crimson view ui # debug views / sandboxes
115
+ uv run crimson quests 1.1 # print quest spawn script
116
+ uv run crimson config # inspect crimson.cfg
117
+ uv run crimson extract <game_dir> artifacts/assets
118
+ ```
119
+
120
+ Useful flags:
121
+
122
+ - `--base-dir PATH` / `CRIMSON_RUNTIME_DIR=...` — where saves/config/logs live
123
+ - `--assets-dir PATH` — where `.paq` archives (or extracted assets) are loaded from
124
+ - `--seed N` — deterministic runs for parity testing
125
+ - `--demo` — enable shareware/demo paths
126
+ - `--no-intro` — skip logos/intro music
127
+
128
+ ---
129
+
130
+ ## Docs
131
+
132
+ Docs are authored in `docs/` and built as a static site at https://crimson.banteg.xyz/
133
+
134
+ For development, it's useful to have a live local build:
135
+
136
+ ```
137
+ uv tool install zensical
138
+ zensical serve
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Development
144
+
145
+ ### Tests
146
+
147
+ ```bash
148
+ uv run pytest
149
+ ```
150
+
151
+ ### Lint / checks
152
+
153
+ ```bash
154
+ uv run lint-imports
155
+ uv run python scripts/check_asset_loader_usage.py
156
+ ```
157
+
158
+ ### `justfile` shortcuts
159
+
160
+ If you have `just` installed:
161
+
162
+ ```bash
163
+ just --list
164
+ just test
165
+ just docs-build
166
+ just ghidra-exe
167
+ just ghidra-grim
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Reverse engineering workflow
173
+
174
+ High level:
175
+
176
+ - **Static analysis is the source of truth.**
177
+ - Update names/types in [analysis/ghidra/maps/](analysis/ghidra/maps/).
178
+ - Treat [analysis/ghidra/raw/](analysis/ghidra/raw/) as generated output (regenerate; do not hand-edit).
179
+ - **Runtime tooling** (Frida / WinDbg) validates ambiguous behavior and captures ground truth.
180
+ - Evidence summaries live under [analysis/frida/](analysis/frida/).
181
+
182
+ ---
183
+
184
+ ## Contributing notes
185
+
186
+ - Keep changes small and reviewable (one subsystem/feature at a time).
187
+ - Prefer *measured parity* (captures/logs/deterministic tests) over “looks right”.
188
+ - When porting float constants from decompilation, prefer the intended value
189
+ (e.g. `0.6` instead of `0.6000000238418579` when it’s clearly a float32 artifact).
190
+
191
+ ---
192
+
193
+ ## Legal
194
+
195
+ This project is an independent reverse engineering and reimplementation effort for preservation, research, and compatibility.
196
+
197
+ No original assets or binaries are included. Use your own legally obtained copy.
@@ -1,6 +1,7 @@
1
1
  [project]
2
2
  name = "crimsonland"
3
- version = "0.1.0.dev12"
3
+ version = "0.1.0.dev14"
4
+ readme = { file = "readme.md", content-type = "text/markdown" }
4
5
  requires-python = ">=3.13"
5
6
  dependencies = [
6
7
  "construct>=2.10.70",
@@ -10,6 +11,10 @@ dependencies = [
10
11
  "typer>=0.21.1",
11
12
  ]
12
13
 
14
+ [project.urls]
15
+ Documentation = "https://crimson.banteg.xyz/"
16
+ Repository = "https://github.com/banteg/crimson"
17
+
13
18
  [project.scripts]
14
19
  crimsonland = "crimson.cli:main"
15
20
  crimson = "crimson.cli:main"
@@ -0,0 +1,184 @@
1
+ # Crimsonland 1.9.93 decompilation + rewrite
2
+
3
+ This repository is a **reverse engineering + high‑fidelity reimplementation** of **Crimsonland 1.9.93 (2003)**.
4
+
5
+ - **Target build:** `v1.9.93` (GOG "Crimsonland Classic") — see [docs/provenance.md](docs/provenance.md) for exact hashes.
6
+ - **Rewrite:** a runnable reference implementation in **Python + raylib** under `src/`.
7
+ - **Analysis:** decompiles, name/type maps, and runtime evidence under `analysis/`.
8
+ - **Docs:** long-form notes and parity tracking under `docs/` (start at [docs/index.md](docs/index.md)).
9
+
10
+ The north star is **behavioral parity** with the original Windows build: timings, RNG, UI/layout quirks, asset decoding, and gameplay rules should match as closely as practical.
11
+
12
+ **[Read the full story](https://banteg.xyz/posts/crimsonland/)** of how this project came together: reverse engineering workflow, custom asset formats, AI-assisted decompilation, and game preservation philosophy.
13
+
14
+ ---
15
+
16
+ ## Quick start
17
+
18
+ Install [uv](https://docs.astral.sh/uv/getting-started/installation/) package manager.
19
+
20
+ ### Run the latest packaged build
21
+
22
+ If you just want to play the rewrite:
23
+
24
+ ```bash
25
+ uvx crimsonland@latest
26
+ ```
27
+
28
+ ### Run from a checkout
29
+
30
+ ```bash
31
+ gh repo clone banteg/crimson
32
+ cd crimson
33
+ uv run crimson
34
+ ```
35
+
36
+ ### Keep runtime files local to the repo
37
+
38
+ By default, runtime files (e.g. `crimson.cfg`, `game.cfg`, highscores, logs, downloaded PAQs) live in your per-user data dir.
39
+ To keep everything under this checkout:
40
+
41
+ ```bash
42
+ export CRIMSON_RUNTIME_DIR="$PWD/artifacts/runtime"
43
+ mkdir -p artifacts/runtime
44
+ uv run crimson
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Assets + binaries
50
+
51
+ There are two separate “inputs” to this repo:
52
+
53
+ 1. **Runtime assets for the rewrite** (PAQ archives)
54
+ 2. **Original Windows binaries for reverse engineering** (`crimsonland.exe`, `grim.dll`, …)
55
+
56
+ We keep them out of git and expect a local layout like:
57
+
58
+ ```text
59
+ game_bins/
60
+ crimsonland/
61
+ 1.9.93-gog/
62
+ crimsonland.exe
63
+ grim.dll
64
+ crimson.paq
65
+ music.paq
66
+ sfx.paq
67
+ artifacts/
68
+ runtime/ # optional: where you run the rewrite (cfg/status/paqs)
69
+ assets/ # optional: extracted PAQs for inspection/tools
70
+ ```
71
+
72
+ ### Running the rewrite
73
+
74
+ The rewrite loads the assets from original archives:
75
+
76
+ - `crimson.paq`
77
+ - `music.paq`
78
+ - `sfx.paq`
79
+
80
+ ### Extracted assets
81
+
82
+ For inspection/diffs/tools, you can extract PAQs into a filesystem tree:
83
+
84
+ ```bash
85
+ uv run crimson extract crimsonland_1.9.93 artifacts/assets
86
+ ```
87
+
88
+ Same as the original, many loaders can work from either:
89
+
90
+ - **PAQ-backed assets** (preferred when available), or
91
+ - the **extracted filesystem layout** under `artifacts/assets/`.
92
+
93
+ ---
94
+
95
+ ## CLI cheat sheet
96
+
97
+ Everything is exposed via the `crimson` CLI (alias: `crimsonland`):
98
+
99
+ ```bash
100
+ uv run crimson # run the game (default command)
101
+ uv run crimson view ui # debug views / sandboxes
102
+ uv run crimson quests 1.1 # print quest spawn script
103
+ uv run crimson config # inspect crimson.cfg
104
+ uv run crimson extract <game_dir> artifacts/assets
105
+ ```
106
+
107
+ Useful flags:
108
+
109
+ - `--base-dir PATH` / `CRIMSON_RUNTIME_DIR=...` — where saves/config/logs live
110
+ - `--assets-dir PATH` — where `.paq` archives (or extracted assets) are loaded from
111
+ - `--seed N` — deterministic runs for parity testing
112
+ - `--demo` — enable shareware/demo paths
113
+ - `--no-intro` — skip logos/intro music
114
+
115
+ ---
116
+
117
+ ## Docs
118
+
119
+ Docs are authored in `docs/` and built as a static site at https://crimson.banteg.xyz/
120
+
121
+ For development, it's useful to have a live local build:
122
+
123
+ ```
124
+ uv tool install zensical
125
+ zensical serve
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Development
131
+
132
+ ### Tests
133
+
134
+ ```bash
135
+ uv run pytest
136
+ ```
137
+
138
+ ### Lint / checks
139
+
140
+ ```bash
141
+ uv run lint-imports
142
+ uv run python scripts/check_asset_loader_usage.py
143
+ ```
144
+
145
+ ### `justfile` shortcuts
146
+
147
+ If you have `just` installed:
148
+
149
+ ```bash
150
+ just --list
151
+ just test
152
+ just docs-build
153
+ just ghidra-exe
154
+ just ghidra-grim
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Reverse engineering workflow
160
+
161
+ High level:
162
+
163
+ - **Static analysis is the source of truth.**
164
+ - Update names/types in [analysis/ghidra/maps/](analysis/ghidra/maps/).
165
+ - Treat [analysis/ghidra/raw/](analysis/ghidra/raw/) as generated output (regenerate; do not hand-edit).
166
+ - **Runtime tooling** (Frida / WinDbg) validates ambiguous behavior and captures ground truth.
167
+ - Evidence summaries live under [analysis/frida/](analysis/frida/).
168
+
169
+ ---
170
+
171
+ ## Contributing notes
172
+
173
+ - Keep changes small and reviewable (one subsystem/feature at a time).
174
+ - Prefer *measured parity* (captures/logs/deterministic tests) over “looks right”.
175
+ - When porting float constants from decompilation, prefer the intended value
176
+ (e.g. `0.6` instead of `0.6000000238418579` when it’s clearly a float32 artifact).
177
+
178
+ ---
179
+
180
+ ## Legal
181
+
182
+ This project is an independent reverse engineering and reimplementation effort for preservation, research, and compatibility.
183
+
184
+ No original assets or binaries are included. Use your own legally obtained copy.
@@ -2,7 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from pathlib import Path
5
+ import os
5
6
  import shutil
7
+ import tempfile
6
8
  import urllib.request
7
9
 
8
10
  from grim.console import ConsoleState
@@ -19,17 +21,30 @@ class DownloadResult:
19
21
 
20
22
 
21
23
  def _download_file(url: str, dest: Path) -> None:
22
- tmp = dest.with_suffix(dest.suffix + ".tmp")
23
- if tmp.exists():
24
- tmp.unlink()
24
+ tmp_path: Path | None = None
25
25
  try:
26
26
  req = urllib.request.Request(url, headers={"User-Agent": "crimsonland-decompile"})
27
- with urllib.request.urlopen(req, timeout=30) as resp, tmp.open("wb") as handle:
28
- shutil.copyfileobj(resp, handle)
29
- tmp.replace(dest)
27
+ with urllib.request.urlopen(req, timeout=30) as resp:
28
+ with tempfile.NamedTemporaryFile(
29
+ mode="wb",
30
+ delete=False,
31
+ dir=dest.parent,
32
+ prefix=dest.name + ".",
33
+ suffix=".tmp",
34
+ ) as handle:
35
+ tmp_path = Path(handle.name)
36
+ shutil.copyfileobj(resp, handle)
37
+ handle.flush()
38
+ os.fsync(handle.fileno())
39
+ if tmp_path is None:
40
+ raise RuntimeError("assets: temporary file not created")
41
+ tmp_path.replace(dest)
30
42
  finally:
31
- if tmp.exists():
32
- tmp.unlink()
43
+ if tmp_path is not None:
44
+ try:
45
+ tmp_path.unlink()
46
+ except FileNotFoundError:
47
+ pass
33
48
 
34
49
 
35
50
  def download_missing_paqs(
@@ -0,0 +1,26 @@
1
+ """
2
+ Layout constants for the classic high scores screen (state_id=14).
3
+
4
+ Measured from analysis/frida/ui_render_trace_oracle_1024x768.json.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ # Panel positions are expressed in "panel pos" (pre-offset) space, matching other menu panels:
10
+ # panel_top_left = (panel_pos_x + MENU_PANEL_OFFSET_X, panel_pos_y + y_shift + MENU_PANEL_OFFSET_Y)
11
+
12
+ HS_LEFT_PANEL_POS_X = -119.0
13
+ HS_LEFT_PANEL_POS_Y = 185.0
14
+ HS_LEFT_PANEL_HEIGHT = 378.0
15
+
16
+ HS_RIGHT_PANEL_POS_X = 609.0
17
+ HS_RIGHT_PANEL_POS_Y = 200.0
18
+ HS_RIGHT_PANEL_HEIGHT = 254.0
19
+
20
+ # Buttons inside the left panel (relative to the left panel top-left).
21
+ HS_BUTTON_X = 234.0 # x0=136 at 1024x768
22
+ HS_BUTTON_Y0 = 268.0 # y0=462
23
+ HS_BUTTON_STEP_Y = 33.0
24
+
25
+ HS_BACK_BUTTON_X = 400.0 # x0=302
26
+ HS_BACK_BUTTON_Y = 301.0 # y0=495
@@ -19,8 +19,8 @@ if TYPE_CHECKING:
19
19
  from ..game import GameState
20
20
 
21
21
 
22
- MENU_LABEL_WIDTH = 124.0
23
- MENU_LABEL_HEIGHT = 30.0
22
+ MENU_LABEL_WIDTH = 122.0
23
+ MENU_LABEL_HEIGHT = 28.0
24
24
  MENU_LABEL_ROW_HEIGHT = 32.0
25
25
  MENU_LABEL_ROW_PLAY_GAME = 1
26
26
  MENU_LABEL_ROW_OPTIONS = 2
@@ -31,18 +31,17 @@ MENU_LABEL_ROW_QUIT = 6
31
31
  MENU_LABEL_ROW_BACK = 7
32
32
  MENU_LABEL_BASE_X = -60.0
33
33
  MENU_LABEL_BASE_Y = 210.0
34
- MENU_LABEL_OFFSET_X = 270.0
35
- MENU_LABEL_OFFSET_Y = -38.0
34
+ MENU_LABEL_OFFSET_X = 271.0
35
+ MENU_LABEL_OFFSET_Y = -37.0
36
36
  MENU_LABEL_STEP = 60.0
37
- MENU_ITEM_OFFSET_X = -72.0
38
- MENU_ITEM_OFFSET_Y = -60.0
39
- MENU_PANEL_WIDTH = 512.0
40
- MENU_PANEL_HEIGHT = 256.0
41
- # ui_menu_assets_init:
42
- # - ui_menuPanel starts with offset_x=+20 (ui_element_set_rect)
43
- # - the menu-panel layout copy applies data_48fdb4 -= 116, so offset_x becomes -96
44
- MENU_PANEL_OFFSET_X = -96.0
45
- MENU_PANEL_OFFSET_Y = -82.0
37
+ MENU_ITEM_OFFSET_X = -71.0
38
+ MENU_ITEM_OFFSET_Y = -59.0
39
+ MENU_PANEL_WIDTH = 510.0
40
+ MENU_PANEL_HEIGHT = 254.0
41
+ # Measured from ui_render_trace at 1024x768 (stable timeline):
42
+ # panel top-left is (pos_x + 21, pos_y - 81) and size is 510x254, plus a shadow pass at +7,+7.
43
+ MENU_PANEL_OFFSET_X = 21.0
44
+ MENU_PANEL_OFFSET_Y = -81.0
46
45
  MENU_PANEL_BASE_X = -45.0
47
46
  MENU_PANEL_BASE_Y = 210.0
48
47
  MENU_SCALE_SMALL_THRESHOLD = 640
@@ -52,10 +51,10 @@ MENU_SCALE_SMALL = 0.8
52
51
  MENU_SCALE_LARGE = 1.2
53
52
  MENU_SCALE_SHIFT = 10.0
54
53
 
55
- MENU_SIGN_WIDTH = 573.44
56
- MENU_SIGN_HEIGHT = 143.36
57
- MENU_SIGN_OFFSET_X = -577.44
58
- MENU_SIGN_OFFSET_Y = -62.0
54
+ MENU_SIGN_WIDTH = 571.44
55
+ MENU_SIGN_HEIGHT = 141.36
56
+ MENU_SIGN_OFFSET_X = -576.44
57
+ MENU_SIGN_OFFSET_Y = -61.0
59
58
  MENU_SIGN_POS_Y = 70.0
60
59
  MENU_SIGN_POS_Y_SMALL = 60.0
61
60
  MENU_SIGN_POS_X_PAD = 4.0
@@ -584,21 +583,24 @@ class MenuView:
584
583
  start_ms: int,
585
584
  end_ms: int,
586
585
  width: float,
586
+ direction_flag: int = 0,
587
587
  ) -> tuple[float, float]:
588
588
  # Matches ui_element_update: angle lerps pi/2 -> 0 over [end_ms, start_ms].
589
- # Direction flag (element+0x314) appears to be 0 for main menu elements.
589
+ # direction_flag=0 slides from left (-width -> 0)
590
+ # direction_flag=1 slides from right (+width -> 0)
590
591
  if start_ms <= end_ms or width <= 0.0:
591
592
  return 0.0, 0.0
593
+ dir_sign = 1.0 if int(direction_flag) else -1.0
592
594
  t = self._timeline_ms
593
595
  if t < end_ms:
594
596
  angle = 1.5707964
595
- offset_x = -abs(width)
597
+ offset_x = dir_sign * abs(width)
596
598
  elif t < start_ms:
597
599
  elapsed = t - end_ms
598
600
  span = float(start_ms - end_ms)
599
601
  p = float(elapsed) / span
600
602
  angle = 1.5707964 * (1.0 - p)
601
- offset_x = -((1.0 - p) * abs(width))
603
+ offset_x = dir_sign * ((1.0 - p) * abs(width))
602
604
  else:
603
605
  angle = 0.0
604
606
  offset_x = 0.0
@@ -8,6 +8,7 @@ from grim.assets import PaqTextureCache
8
8
  from grim.audio import play_sfx, update_audio
9
9
  from grim.terrain_render import GroundRenderer
10
10
 
11
+ from ...ui.menu_panel import draw_classic_menu_panel
11
12
  from ..assets import MenuAssets, _ensure_texture_cache, load_menu_assets
12
13
  from ..menu import (
13
14
  MENU_ITEM_OFFSET_X,
@@ -70,6 +71,9 @@ class PanelMenuView:
70
71
  body: str | None = None,
71
72
  panel_pos_x: float = PANEL_POS_X,
72
73
  panel_pos_y: float = PANEL_POS_Y,
74
+ panel_offset_x: float = MENU_PANEL_OFFSET_X,
75
+ panel_offset_y: float = MENU_PANEL_OFFSET_Y,
76
+ panel_height: float = MENU_PANEL_HEIGHT,
73
77
  back_pos_x: float = PANEL_BACK_POS_X,
74
78
  back_pos_y: float = PANEL_BACK_POS_Y,
75
79
  back_action: str = "back_to_menu",
@@ -79,6 +83,9 @@ class PanelMenuView:
79
83
  self._body_lines = (body or "").splitlines()
80
84
  self._panel_pos_x = panel_pos_x
81
85
  self._panel_pos_y = panel_pos_y
86
+ self._panel_offset_x = panel_offset_x
87
+ self._panel_offset_y = panel_offset_y
88
+ self._panel_height = panel_height
82
89
  self._back_pos_x = back_pos_x
83
90
  self._back_pos_y = back_pos_y
84
91
  self._back_action = back_action
@@ -223,40 +230,21 @@ class PanelMenuView:
223
230
  if assets is None or assets.panel is None:
224
231
  return
225
232
  panel = assets.panel
226
- panel_w = MENU_PANEL_WIDTH
227
- panel_h = MENU_PANEL_HEIGHT
228
233
  _angle_rad, slide_x = MenuView._ui_element_anim(
229
234
  self,
230
235
  index=1,
231
236
  start_ms=PANEL_TIMELINE_START_MS,
232
237
  end_ms=PANEL_TIMELINE_END_MS,
233
- width=panel_w * self._menu_item_scale(0)[0],
238
+ width=MENU_PANEL_WIDTH * self._menu_item_scale(0)[0],
234
239
  )
235
240
  item_scale, _local_y_shift = self._menu_item_scale(0)
236
- dst = rl.Rectangle(
237
- self._panel_pos_x + slide_x,
238
- self._panel_pos_y + self._widescreen_y_shift,
239
- panel_w * item_scale,
240
- panel_h * item_scale,
241
- )
242
- origin = rl.Vector2(-(MENU_PANEL_OFFSET_X * item_scale), -(MENU_PANEL_OFFSET_Y * item_scale))
241
+ panel_w = MENU_PANEL_WIDTH * item_scale
242
+ panel_h = float(self._panel_height) * item_scale
243
+ top_left_x = self._panel_pos_x + slide_x + self._panel_offset_x * item_scale
244
+ top_left_y = self._panel_pos_y + self._widescreen_y_shift + self._panel_offset_y * item_scale
245
+ dst = rl.Rectangle(float(top_left_x), float(top_left_y), float(panel_w), float(panel_h))
243
246
  fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
244
- if fx_detail:
245
- MenuView._draw_ui_quad_shadow(
246
- texture=panel,
247
- src=rl.Rectangle(0.0, 0.0, float(panel.width), float(panel.height)),
248
- dst=rl.Rectangle(dst.x + UI_SHADOW_OFFSET, dst.y + UI_SHADOW_OFFSET, dst.width, dst.height),
249
- origin=origin,
250
- rotation_deg=0.0,
251
- )
252
- MenuView._draw_ui_quad(
253
- texture=panel,
254
- src=rl.Rectangle(0.0, 0.0, float(panel.width), float(panel.height)),
255
- dst=dst,
256
- origin=origin,
257
- rotation_deg=0.0,
258
- tint=rl.WHITE,
259
- )
247
+ draw_classic_menu_panel(panel, dst=dst, tint=rl.WHITE, shadow=fx_detail)
260
248
 
261
249
  def _draw_entry(self, entry: MenuEntry) -> None:
262
250
  assets = self._assets