Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion AI_HANDOFF.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions AgainstRomeModifier.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@
<InternalsVisibleTo Include="AgainstRomeModifier.Tests" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="src.Shared\AgainstRome.Shared.csproj" />
<ProjectReference Include="src.MapEditor\AgainstRomeMapEditor.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>
<Target Name="CopyMapEditor" AfterTargets="Build;Publish">
<ItemGroup>
<MapEditorOutput Include="src.MapEditor\bin\$(Configuration)\net8.0-windows\AgainstRomeMapEditor.exe" />
<MapEditorOutput Include="src.MapEditor\bin\$(Configuration)\net8.0-windows\AgainstRomeMapEditor.dll" />
<MapEditorOutput Include="src.MapEditor\bin\$(Configuration)\net8.0-windows\AgainstRomeMapEditor.runtimeconfig.json" />
<MapEditorOutput Include="src.MapEditor\bin\$(Configuration)\net8.0-windows\AgainstRome.Shared.dll" />
</ItemGroup>
<Copy SourceFiles="@(MapEditorOutput)" DestinationFolder="$(OutDir)" SkipUnchangedFiles="true" Condition="Exists('%(MapEditorOutput.Identity)')" />
</Target>
<ItemGroup>
<Compile Remove="src\Core\GameLZSS.cs" />
<Compile Remove="src\Core\FileRollbackScope.cs" />
<Compile Remove="src\Core\Services\SafeFileWriter.cs" />
<Compile Remove="src.Shared\**" />
<Compile Remove="src.MapEditor\**" />
<Compile Remove="re_workspace\**" />
<Compile Remove="scratch\**" />
<Compile Remove="tests\**" />
Expand Down
6 changes: 6 additions & 0 deletions AgainstRomeModifier.slnx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<Solution>
<Project Path="AgainstRomeModifier.csproj" />
<Project Path="src.MapEditor/AgainstRomeMapEditor.csproj">
<Build Solution="Debug|*" Project="false" />
</Project>
<Project Path="src.Shared/AgainstRome.Shared.csproj">
<Build Solution="Debug|*" Project="false" />
</Project>
<Project Path="tests/AgainstRomeModifier.Tests/AgainstRomeModifier.Tests.csproj" />
</Solution>
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ The author of this project is a devoted player who loved *Against Rome* many yea
- **10x Town Hall HP**: Reversible switch to multiply hit-points (HP) of all Town Halls (`Hau` structures) in `objdef.dau` by 10x (successfully runtime-verified in-game).
- **Endless-Mode AI Ultimate Mode**: Reversible, bounded BCI patches for the five `ENDL_*` maps. The current safe military configuration is 20 units per reinforcement party, 5-second respawn timing, and an active-party limit of 8; original loop pacing and the two settlement-cleanup safeguards remain intact. It never uses the previously rejected unconditional gate bypass. See [`endless-mode-ai.md`](docs/reverse-engineering/endless-mode-ai.md) for evidence, legacy migration, and runtime limits.
- **Free Construction & Production**: Free construction, production, upgrades, and spell costs through `ress.ini` modification.
- **5x Spell Damage**: Reversible `cl_script.ini` patch for six configured active-spell values; successfully runtime-verified in-game.
- **Unit Stat Editing**: Adjust only HP, damage, VW, AW, sight, and cooldown through `objdef.dau`. Movement speed, attack range, spell radius, and priest sight/casting distance are deliberately owned by the independent experimental modifiers, not by custom troop layers.
- **Experimental Stat Modifiers**: 3× ranged range, 2× unit movement, entire-map priest casting distance, 3× spell effect radius, higher projectile arcs, and ranged-accuracy boost. These are static-verified; the two projectile features still need dedicated runtime-regression observations.
- **Troop Presets & One-Click Control**: `.artroop` exports use the six-field `HP,Dmg,VW,AW,Sight,Relt` format. Legacy nine-field imports remain readable but their removed fields are discarded; one-click buttons enable or disable all registered features.
Expand Down
380 changes: 380 additions & 0 deletions docs/map-editor-spec.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion docs/reverse-engineering/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Against Rome Reverse Engineering Notes
# Against Rome Reverse Engineering Notes

> Index reviewed 2026-07-11. This is evidence documentation, not permission to write original game files directly. Product changes must be made in the modifier and verified through its fixture/apply/restore workflow.

Expand All @@ -11,6 +11,7 @@ and patch code.

- `../../TechDoc.md`: current Chinese technical specification, integrating the cross-feature maintenance chronology, failure cases, and safety contracts.
- `file-formats.md`: compressed game files, wrappers, and encodings.
- `map-formats.md`: static BMP-grid facts, explicitly unverified map-layer semantics, and the controlled experiments required before any terrain or collision write feature.
- `endless-mode-ai.md`: endless AI spawn, timing, gate, job-slot, party
lifecycle, and defeat-recovery findings.
- `bci0-opcodes.md`: `BCI0` script container layout and the partial bytecode
Expand Down
19 changes: 13 additions & 6 deletions docs/reverse-engineering/known-patches.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,20 +434,27 @@

- Files: `SYSTEM/DATA_MP/DEFAULTS/objdef.dau` + `SYSTEM/DATA_MP/DEFAULTS/partgeo.dau`
- objdef: every weapon slot with `w*_akti == 1` and `w*_emit > 0` gets
`w*_emit ×1.5` (rounded; 16.16 fixed vertical launch speed — bows 7208960,
spear thrower 6356992, catapults 10158080 / 9306112).
`w*_emit ×2` (rounded; 16.16 fixed vertical launch speed — bows 7208960,
spear thrower 6356992, catapults 10158080 / 9306112). All original emit
values fit their cell widths up to ×10 (verified against the full objdef).
- partgeo: the six projectile rows (`Wurfspeer00`, `Wurfaxt00`,
`Katapultstein00`, `Katapultstein01`, `Pfeil00`, `Spiess00`) get `ysub`
(gravity, column 12) ×1.5. Patcher aborts unless exactly six rows match.
(gravity, column 12) ×2. Patcher aborts unless exactly six rows match.
- Same-factor scaling keeps landing point and flight time unchanged while the
arc apex (≈ emit²/(2·ysub)) rises ×1.5. Mechanism evidence:
arc apex (≈ emit²/(2·ysub)) rises by the same factor (bows ≈68 → ≈136
height units). The factor lives in `ObjdefPatcher.ArcEmitMultiplier`
(shared by `PartgeoPatcher.ArcYsubMultiplier`). Mechanism evidence:
`docs/reverse-engineering/projectile-ballistics.md`.
- Runtime verified 2026-07-12 at ×10 (user confirmed visibly higher arcs with
unchanged landing points); shipped factor tuned down to ×2 by user request.
1.5 was tried first and is visually indistinguishable — keep the factor ≥2.
- partgeo.dau is not in the embedded Backup.zip: auto-heal captures it from
the game directory only after an `IsPartgeoOriginal` check (Pfeil00
ysub == 5832704) and immediately creates the physical `.bak`.
- Detection: all projectile weapon rows must equal original×1.5
- Detection: all projectile weapon rows must equal original×2
(`FeatureDetector.HasProjectileWeaponScale`).
- Status: implemented, unit/integration tested; runtime verification pending.
- Status: implemented, unit/integration tested, runtime verified (at ×10);
promoted out of the Experimental UI group into Resource & Combat Upgrades.

### Ranged Accuracy Boost (RangedAccuracy)

Expand Down
69 changes: 69 additions & 0 deletions docs/reverse-engineering/map-formats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 地圖格式與 Phase 3 閘門

> 狀態:靜態驗證與候選假設(2026-07-11)。本文件不是直接修改原始遊戲檔的授權;所有產品功能必須經修改器的受控寫入/還原流程。

## 已靜態驗證

以工作區的五張原版 `MAPS/ENDL_000` 至 `ENDL_004` 為唯讀樣本:

| 檔案 | 格式 | 尺寸 | 穩定結論 |
|---|---:|---:|---|
| `vertex.bmp` | 24-bit BMP | 257×257 | 頂點格候選圖層;不是灰階單通道圖。|
| `boden.bmp` | 24-bit BMP | 257×257 | 三通道幾乎相同的灰階圖;用途尚未確認。|
| `emboss.bmp` | 24-bit BMP | 257×257 | 陰影/光照候選圖層;有些原版圖全黑。|
| `smooth.bmp` | 24-bit BMP | 257×257 | 灰階遮罩候選圖層。|
| `collision.bmp` | 24-bit BMP | 256×256 | 三通道相同的純灰階、含大面積 0 與 255 區塊。|
| `minimap.bmp` | 24-bit BMP | 256×256 | 彩色預覽圖,可安全唯讀顯示。|

`boden.txt` 是 64×64 tile 名稱表,而 `boden.ini [Heightmapstep]` 的原版值為 4。因此 `64 × 4 + 1 = 257` 與頂點圖尺寸吻合,且 `64 × 4 = 256` 與 tile 圖尺寸吻合。這個格網關係是**結構事實**;不代表任一 BMP 色值的語意已被證實。

## 樣本通道觀察

- `collision.bmp` 在五張圖均為 R=G=B;平均亮度介於約 10 至 52,標準差 48 至 102,確認不是彩色資料。
- `boden.bmp` 在五張圖均近乎 R=G=B;`emboss.bmp` 與 `smooth.bmp` 亦主要為灰階,但 `vertex.bmp` 的三通道明顯不同。
- `ENDL_000/001/002/003/004` 的 `vertex.bmp` 通道範圍分別不同,不能使用固定「某一顏色等於高度」公式。
- 目視 `ENDL_000`:`vertex.bmp` 的低色彩區與 minimap 水域/地勢特徵有表面關聯,但這只是候選相關性,尚未具備寫入資格。
- 2026-07-12 追加唯讀跨層目視:`KAMP_000/boden.bmp` 的連續灰階坡面、河谷與人工高低差和 `minimap.bmp` 明確對齊;`vertex.bmp` 則更接近彩色地表快取。這足以讓離線 renderer 使用 `boden.bmp` 的局部梯度產生只讀 hill-shading,但仍不足以推導世界高度單位或授權高度寫入。
- 同日比對 `KAMP_000`、`ENDL_000`、`MP_000`、`HIST_000`:`Heightmapstep` 均為 4,`Waterlevel / Heightmapstep` 分別為 62、30、30、36,與各圖 `boden.bmp` 河谷低灰階區吻合。離線 renderer 因此以此門檻和 `WaterColor` 產生只讀水面遮罩;此證據仍只授權顯示,不授權直接改寫高度圖。
- SDL 唯讀解析確認 `[settlement] refpos` 加上各 `[objectNNNN] pos` 得到物件世界座標;連續柵欄以 64 世界單位排列,而 16,384 世界單位對應 256 地圖像素,因此 `world / 64` 可直接落到地圖像素。`KAMP_000/TEAM_7.sdl` 解析出 138 個有效物件,`ENDL_000` 八個聚落 SDL 合計 613 個。離線 renderer 依此顯示建築/單位/其他物件;目前只讀,不把尚未完成 round-trip 驗證的 SDL 物件編輯暴露給玩家。

## 未證實,禁止寫入

1. `vertex.bmp` 哪一個通道(或其組合)代表高度、其量尺與 `Heightmapstep` 的換算方式。
2. `collision.bmp` 的黑、白、灰階是否分別代表可通行、不可通行或其他導航遮罩。
3. `boden.bmp`、`emboss.bmp`、`smooth.bmp` 是否由遊戲重建,或必須與 vertex/材質資料同步修改。
4. `DATA/*.dat`、`cliprect.dat`、`shadows.dat`、`skydens.dat`、`visible.dat` 的權威性及重建規則。
5. SDL `refpos`/`pos` 與 256 像素 minimap 的座標轉換。Phase 2 不得疊加物件位置,避免製造誤導性視圖。

## 原遊戲渲染與內部 TextureEditor

### 可供獨立 renderer 使用的原始資源(2026-07-12)

- `floortex.dat` 是標準 ZIP 容器(副檔名雖為 `.dat`),共 3,005 個 entry;地表圖位於 `SYSTEM/DATA/FLOORTEXTURE/*.bmp`。
- `boden.txt [Texturen]` 的名稱可直接對應上述 BMP basename。例如 `4BJ___51` 與 `L5B09T1A` 均已在容器內找到;抽查圖檔為 128×128、8-bit BMP。
- `alr.dat` 同樣是 ZIP 容器,共 2,075 個 `SYSTEM/DATA/ALR/*.alr` 模型/動畫資源。
- `apt.dat` 是 ZIP 容器,共 222 個 `SYSTEM/DATA/APT/*.apt` 資源;`shad.dat` 是 ZIP 容器,共 2,674 個圖示/陰影資源。
- 唯讀遊戲樣本的 `MAPS` 下共有 73 個具備 `boden.txt` 與 `minimap.bmp` 的可渲染目錄:`KAMP` 34、`MP` 20、`HIST` 10、`ENDL` 5、`TUTOR` 4。
- 因此地圖編輯器應以自行讀取這些容器的離線 renderer 為主,不再以 `minimap.bmp` 或啟動遊戲作為主要預覽。原始資源只從使用者選定的遊戲資料夾唯讀載入,不得加入版本庫。

- EXE 靜態庫在 `FUN_004a4c20`(`0x004a4c20`)包含 `TextureEditor V0.1`、參數清單與鍵盤操作字串;主畫面迴圈 `FUN_0047ae20` 僅在全域模式值為 `9` 時呼叫它。
- 正常模式轉換函式只公開模式 `0、1、2、4、7、8`,目前沒有找到將模式設為 `9` 的正常選單或命令列路徑。
- 該函式處理 buffer、seed、sharpen、blur、combine 等程序影像操作。現有證據較符合內部材質產生/除錯工具,不能視為完整地圖編輯器或安全的 3D 地圖預覽入口。
- 尚未找到原遊戲可直接載入 `ENDL_NNN` 的命令列參數。產品 UI 的 `minimap.bmp`/資料圖層必須標示為「編輯底圖」而非真實遊戲預覽;真實預覽目前只能安全儲存後啟動 `Against_Rome.exe`,再由使用者從無盡模式選單進入目標槽位。

## 下一輪受控實驗

每次只在**自製 `ENDL_005+` 地圖的 modifier workflow**中做一件可還原的小改動,並保留原始與修改後雜湊、遊戲結果與 restore 結果。

1. 複製一張原版地圖,僅在 `vertex.bmp` 中心 3×3 像素改一個通道;三個通道各做一份獨立副本。進入遊戲檢查地形高度,後續還原。
2. 以已證實影響高度的通道,量測色差 1、16、64 對應的世界高度,並跨兩張原圖重複。
3. 只將 `collision.bmp` 一個遠離建築的 4×4 tile 區塊改為 0 或 255,測單位能否步入;不要與 vertex 或其他資料同時修改。
4. 對每個 `DATA/*.dat` 使用「只複製/暫時移除/啟動」測試,記錄遊戲是否重建、載入失敗或無變化。未知檔案不納入編輯器寫入清單。
5. 每個實驗都以新增的自製圖為目標;不得手動改寫安裝目錄的原廠圖。

## 開放功能條件

- 高度筆刷:須完成步驟 1–2、至少兩圖重現、提供安全值域與 rollback。
- 碰撞筆刷:須完成步驟 3,並證實黑白方向及灰階行為。
- minimap 自動重繪:須先確認哪個輸入圖層是權威來源及是否需更新快取。
- SDL 疊圖:須以至少三個明確地標校正並驗證世界到像素轉換。
8 changes: 7 additions & 1 deletion docs/reverse-engineering/priest-spells.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,19 +152,25 @@ the `FUN_005b1410` sequence around decompiled-inventory lines 186446-186448.
- Respect the 16-bit signed storage limit (≤ 32767) for all non-Duration
fields. All multiplied values are verified to be safe from overflow.

## Experimental feature verification
## Independent feature verification

The static call/data chain and the modifier test suite verify these targets:

| Feature | Decompilation target | Patched value |
|---|---|---:|
| Spell damage 5x | six configured active-spell `Value` records consumed by `s_getSpecialEffectValue` | original x 5 |
| Healing 10x | `KEL, Spell1, Value` consumed by `s_getSpecialEffectValue` | 65 -> 650 |
| Enhanced resurrection | `KEL, Spell3, Value/Value2` passed to `s_specialEffektCreateUnit` | 50/50 -> 100/100 |
| Ranged unit range 3x | `objdef` `w*_rad1/w*_rad2` (80/81, +8 per weapon) | original x 3 |
| Unit movement speed 2x | `objdef` `Moves/Movsf/Bmovs` (4/23/191) | original x 2 |
| Priest casting distance, entire map | priest `objdef` `Sirad` (24, sight radius) | max distance 30000 |
| Spell effect radius 3x | `cl_script.ini` `[Spells] Radius` | original x 3 |

`SpellDamage5x` was successfully runtime-verified in-game and is promoted from
the Experimental UI group to Resource & Combat Upgrades. It remains an
independent toggle, applies only the six `ClScriptPatcher.DamageSpellKeys`, and
is included by Enable All.

The priest `Radius` value (field index 4 in the EXE spell table) is the
effect/search area used by healing and resurrection, not the caster-to-target
distance. `SpellEntireMap` uses the priest `Sirad` sight-radius field in
Expand Down
16 changes: 16 additions & 0 deletions src.MapEditor/AgainstRomeMapEditor.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<PlatformTarget>x64</PlatformTarget>
<AssemblyName>AgainstRomeMapEditor</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../src.Shared/AgainstRome.Shared.csproj" />
</ItemGroup>
</Project>
42 changes: 42 additions & 0 deletions src.MapEditor/FloorTextureLibrary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.IO.Compression;

namespace AgainstRomeMapEditor;

internal sealed class FloorTextureLibrary : IDisposable
{
private readonly ZipArchive? _archive;
private readonly Dictionary<string, ZipArchiveEntry> _entries = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Bitmap> _textures = new(StringComparer.OrdinalIgnoreCase);

public FloorTextureLibrary(string archivePath)
{
if (!File.Exists(archivePath)) return;
_archive = ZipFile.OpenRead(archivePath);
foreach (ZipArchiveEntry entry in _archive.Entries)
{
if (!entry.FullName.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase)) continue;
string name = Path.GetFileNameWithoutExtension(entry.Name);
if (!string.IsNullOrWhiteSpace(name)) _entries.TryAdd(name, entry);
}
}

public bool IsAvailable => _archive is not null && _entries.Count > 0;

public Bitmap? Get(string textureName)
{
if (_textures.TryGetValue(textureName, out Bitmap? cached)) return cached;
if (!_entries.TryGetValue(textureName, out ZipArchiveEntry? entry)) return null;
using Stream stream = entry.Open();
using var source = new Bitmap(stream);
var texture = new Bitmap(source);
_textures[textureName] = texture;
return texture;
}

public void Dispose()
{
foreach (Bitmap texture in _textures.Values) texture.Dispose();
_textures.Clear();
_archive?.Dispose();
}
}
Loading
Loading