Files
AdamuSw/docs/superpowers/plans/2026-07-19-new-maps-arkania-pilot.md
Acentech Dev a61890ef6e docs: implementation plan for Arkania map pilot
7 tasks: terrain resource (Terrain84.att, off-by-one), map class +
registration, update-plugin with spawn gate + /move warp, docker
build/verify, deploy + panel verify, client assets, client verify.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 13:31:34 +03:00

447 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# New Map Import — Arkania Pilot Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Import one pilot map (Arkania, map number 83) end-to-end into OpenMU server, the PC MUnique client, and make it visible/editable in the web admin panel.
**Architecture:** OpenMU-native map: a C# `BaseMapInitializer` subclass registers map number 83 into `GameConfiguration.Maps`; its terrain comes from an embedded `.att` resource; an update-plugin applies the same to an existing DB. The map is seeded EMPTY (no monsters/NPCs) plus one spawn gate and a `/move` warp for reachability. The web admin panel needs no code changes — the map appears automatically once in the DB. PC client gets the `World83`/`Object83` asset folders.
**Tech Stack:** C# (.NET), OpenMU `Persistence.Initialization`, Docker build, MUnique MuMain PC client (Data folder assets).
## Global Constraints
- Server repo: `AdamuSw` at `D:\OpenMU\MU Client_Mobile 1.04d - Season 6E3\AdamuSw`, work on branch `feature/new-maps-import` (already created).
- Pilot map: **Arkania**, **map number 83** (client `World83`). Confirm the name against the pack screenshots before finalizing; if it turns out World83 is a different map, change only the `Name`/class name.
- **Terrain resource naming is OFF-BY-ONE:** OpenMU map number `N` loads embedded resource `Resources/Terrain{N+1}.att`. For Arkania (83) that is **`Terrain84.att`**, whose CONTENT is the pack's `Terrain83.att` (world-83 attributes). Verified against Aida (33→Terrain34.att), Vulcanus (63→Terrain64.att).
- Terrain resources are NOT globbed by the csproj — each is listed explicitly (`<None Remove>` + `<EmbeddedResource Include>`). New `.att` MUST be added to both lists.
- Update-plugins are applied to existing DBs ONLY via AdminPanel → Updates (`DataUpdateService`); never call `SaveChanges` inside a plugin. Fresh/zero DB auto-seeds via the map registration.
- Server build is via Docker: `docker build -f Startup/Dockerfile -t adamu-openmu:dev .` from the `AdamuSw` root. Local `dotnet build` is expected to fail at the source-generator pre-step (known, per the Imperial-items work) — use the Docker build as the compile gate.
- Pack source: `C:\Users\efpa\Downloads\Arkania Acheron Debenter Uruk Ferea\Arkania Acheron Debenter Uruk Ferea`.
- PC client Data root: `D:\AdaMu\AdaMu\Data`.
---
### Task 1: Add the Arkania terrain resource
**Files:**
- Create: `AdamuSw/src/Persistence/Initialization/Resources/Terrain84.att` (copied from pack `Server Side/Data/Terrain/Terrain83.att`)
- Modify: `AdamuSw/src/Persistence/Initialization/MUnique.OpenMU.Persistence.Initialization.csproj` (two lines: a `<None Remove>` and an `<EmbeddedResource Include>`)
**Interfaces:**
- Produces: embedded manifest resource `MUnique.OpenMU.Persistence.Initialization.Resources.Terrain84.att`, consumed by `TerrainUpdateHelper.UpdateTerrainFromResources` for map number 83.
- [ ] **Step 1: Copy the terrain file (pack Terrain83.att → resource Terrain84.att)**
Run (Git Bash):
```bash
SRC="/c/Users/efpa/Downloads/Arkania Acheron Debenter Uruk Ferea/Arkania Acheron Debenter Uruk Ferea/Server Side/Data/Terrain/Terrain83.att"
DST="/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src/Persistence/Initialization/Resources/Terrain84.att"
cp -f "$SRC" "$DST"
stat -c '%s %n' "$DST"
```
Expected: prints `65539 .../Terrain84.att` (matches OpenMU's other `.att` sizes).
- [ ] **Step 2: Verify the header matches OpenMU's terrain format**
Run:
```bash
xxd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src/Persistence/Initialization/Resources/Terrain84.att" | head -1
xxd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src/Persistence/Initialization/Resources/Terrain1.att" | head -1
```
Expected: both start with `00ff ff04` (same 3-byte header family). If Terrain84.att differs structurally, STOP — the pack file may be a different format.
- [ ] **Step 3: Register the resource in the csproj**
In `MUnique.OpenMU.Persistence.Initialization.csproj`, add next to the existing `Terrain*.att` entries:
In the `<None Remove=...>` group (near `<None Remove="Resources\Terrain1.att" />`):
```xml
<None Remove="Resources\Terrain84.att" />
```
In the `<EmbeddedResource Include=...>` group (near `<EmbeddedResource Include="Resources\Terrain1.att" />`):
```xml
<EmbeddedResource Include="Resources\Terrain84.att" />
```
- [ ] **Step 4: Verify both csproj lines are present**
Run:
```bash
grep -n "Terrain84.att" "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src/Persistence/Initialization/MUnique.OpenMU.Persistence.Initialization.csproj"
```
Expected: exactly two lines — one `<None Remove ...Terrain84.att />` and one `<EmbeddedResource Include ...Terrain84.att />`.
- [ ] **Step 5: Commit**
```bash
cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw"
git add src/Persistence/Initialization/Resources/Terrain84.att src/Persistence/Initialization/MUnique.OpenMU.Persistence.Initialization.csproj
git commit -m "feat(maps): add Arkania (map 83) terrain resource Terrain84.att"
```
---
### Task 2: Create the Arkania map initializer and register it
**Files:**
- Create: `AdamuSw/src/Persistence/Initialization/VersionSeasonSix/Maps/Arkania.cs`
- Modify: `AdamuSw/src/Persistence/Initialization/VersionSeasonSix/GameMapsInitializer.cs` (add one `yield return`)
**Interfaces:**
- Produces: `internal class Arkania : BaseMapInitializer` with `internal const byte Number = 83;` and `internal const string Name = "Arkania";`. Consumed by `GameMapsInitializer` (fresh seed) and the update-plugin (Task 3).
- [ ] **Step 1: Create the map class**
Create `Arkania.cs` (mirrors the `Aida.cs` pattern, but with no monster/NPC spawns — those are added later via the web panel):
```csharp
// <copyright file="Arkania.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps;
using MUnique.OpenMU.DataModel.Configuration;
/// <summary>
/// The initialization for the Arkania map (imported map, world 83).
/// Seeded empty; monster/NPC spawns and gates are added via the admin panel.
/// </summary>
internal class Arkania : BaseMapInitializer
{
/// <summary>
/// The Number of the Map.
/// </summary>
internal const byte Number = 83;
/// <summary>
/// The Name of the Map.
/// </summary>
internal const string Name = "Arkania";
/// <summary>
/// Initializes a new instance of the <see cref="Arkania"/> class.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="gameConfiguration">The game configuration.</param>
public Arkania(IContext context, GameConfiguration gameConfiguration)
: base(context, gameConfiguration)
{
}
/// <inheritdoc/>
protected override byte MapNumber => Number;
/// <inheritdoc/>
protected override string MapName => Name;
}
```
- [ ] **Step 2: Register the map in GameMapsInitializer**
In `VersionSeasonSix/GameMapsInitializer.cs`, in `MapInitializerTypes`, add after the last existing `yield return typeof(...);` line:
```csharp
yield return typeof(Arkania);
```
- [ ] **Step 3: Verify class + registration compile-consistency (textual)**
Run:
```bash
grep -n "class Arkania : BaseMapInitializer" "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src/Persistence/Initialization/VersionSeasonSix/Maps/Arkania.cs"
grep -n "typeof(Arkania)" "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src/Persistence/Initialization/VersionSeasonSix/GameMapsInitializer.cs"
```
Expected: one match each.
- [ ] **Step 4: Commit**
```bash
cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw"
git add src/Persistence/Initialization/VersionSeasonSix/Maps/Arkania.cs src/Persistence/Initialization/VersionSeasonSix/GameMapsInitializer.cs
git commit -m "feat(maps): add Arkania map initializer (number 83) and register it"
```
---
### Task 3: Add spawn gate + `/move` warp, and the DB update-plugin
**Files:**
- Modify: `AdamuSw/src/Persistence/Initialization/Updates/UpdateVersion.cs` (add enum value)
- Create: `AdamuSw/src/Persistence/Initialization/Updates/AddArkaniaMapUpdateSeason6.cs`
**Interfaces:**
- Consumes: `Arkania.Number` (83) and `Arkania.Name` ("Arkania") from Task 2; the `Terrain84.att` resource from Task 1.
- Produces: update-plugin `AddArkaniaMapUpdateSeason6` with `Version => UpdateVersion.AddArkaniaMapSeason6`.
- [ ] **Step 1: Compute a walkable spawn coordinate from the terrain**
The spawn gate must sit on a walkable tile. Scan the pack `Terrain83.att` (3-byte header + 256×256 attribute bytes; a tile is walkable when its attribute byte has none of the blocking bits `0x04` NOMOVE / `0x08` NOGROUND set — i.e. value `0x00` or `0x01` safezone). Run:
```bash
python - <<'PY'
p="/c/Users/efpa/Downloads/Arkania Acheron Debenter Uruk Ferea/Arkania Acheron Debenter Uruk Ferea/Server Side/Data/Terrain/Terrain83.att"
d=open(p,'rb').read()[3:] # drop 3-byte header
# attribute[y*256 + x]; find a walkable tile nearest the center
best=None
for y in range(256):
for x in range(256):
a=d[y*256+x]
if a & 0x04 or a & 0x08: # NOMOVE or NOGROUND -> blocked
continue
dist=abs(x-128)+abs(y-128)
if best is None or dist<best[0]:
best=(dist,x,y,a)
print("spawn X=%d Y=%d (attr=0x%02x, dist-from-center=%d)"%(best[1],best[2],best[3],best[0]))
PY
```
Expected: prints a concrete `X`/`Y` (e.g. `spawn X=128 Y=130 ...`). Record these as `SPAWN_X` / `SPAWN_Y` for Step 3.
- [ ] **Step 2: Add the UpdateVersion enum value**
In `Updates/UpdateVersion.cs`, after `AddImperialWeapons050Season6 = 95,` add:
```csharp
/// <summary>
/// Adds the imported Arkania map (number 83).
/// </summary>
AddArkaniaMapSeason6 = 96,
```
- [ ] **Step 3: Create the update-plugin**
Create `Updates/AddArkaniaMapUpdateSeason6.cs` (mirrors `AddImperialWeapons050UpdateSeason6.cs` for the boilerplate and `AddLorenMarketJuliaWarpPlugIn.cs` for the inline gate/warp). Replace `SPAWN_X`/`SPAWN_Y` with the values from Step 1:
```csharp
// <copyright file="AddArkaniaMapUpdateSeason6.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Persistence.Initialization.Updates;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Adds the imported Arkania map (number 83): the map definition with its terrain,
/// a spawn gate on a walkable tile, and a /move warp entry so players can reach it.
/// Monsters and NPCs are added afterwards via the admin panel.
/// </summary>
[PlugIn]
[Display(Name = PlugInName, Description = PlugInDescription)]
[Guid("2C8B0A61-6B2E-4D7A-9E3C-7F1A4B2D9E01")]
public class AddArkaniaMapUpdateSeason6 : UpdatePlugInBase
{
internal const string PlugInName = "Add Arkania map (83)";
internal const string PlugInDescription = "Adds the imported Arkania map (number 83) with terrain, a spawn gate, and a /move warp. Monsters/NPCs are added via the admin panel.";
/// <inheritdoc />
public override UpdateVersion Version => UpdateVersion.AddArkaniaMapSeason6;
/// <inheritdoc />
public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id;
/// <inheritdoc />
public override string Name => PlugInName;
/// <inheritdoc />
public override string Description => PlugInDescription;
/// <inheritdoc />
public override bool IsMandatory => true;
/// <inheritdoc />
public override DateTime CreatedAt => new(2026, 07, 19, 12, 0, 0, DateTimeKind.Utc);
/// <inheritdoc />
protected override ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration)
{
// Idempotency guard: skip if Arkania (83) already exists.
if (gameConfiguration.Maps.Any(m => m.Number == Arkania.Number))
{
return default;
}
var initializer = new Arkania(context, gameConfiguration);
initializer.Initialize(); // creates map 83 + loads terrain from Terrain84.att
initializer.SetSafezoneMap();
var arkania = gameConfiguration.Maps.First(m => m.Number == Arkania.Number);
// Spawn gate on a walkable tile (coordinates from the terrain scan).
var spawnGate = context.CreateNew<ExitGate>();
arkania.ExitGates.Add(spawnGate);
spawnGate.Map = arkania;
spawnGate.X1 = SPAWN_X;
spawnGate.Y1 = SPAWN_Y;
spawnGate.X2 = SPAWN_X;
spawnGate.Y2 = SPAWN_Y;
spawnGate.IsSpawnGate = true;
// /move warp entry.
if (gameConfiguration.WarpList.All(w => w.Name != Arkania.Name))
{
var warp = context.CreateNew<WarpInfo>();
warp.Index = 83;
warp.Name = Arkania.Name;
warp.Costs = 5000;
warp.LevelRequirement = 10;
warp.Gate = spawnGate;
}
return default;
}
}
```
- [ ] **Step 4: Verify enum + plugin references are consistent (textual)**
Run:
```bash
grep -n "AddArkaniaMapSeason6 = 96" "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src/Persistence/Initialization/Updates/UpdateVersion.cs"
grep -n "UpdateVersion.AddArkaniaMapSeason6" "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src/Persistence/Initialization/Updates/AddArkaniaMapUpdateSeason6.cs"
grep -cn "SPAWN_X\|SPAWN_Y" "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw/src/Persistence/Initialization/Updates/AddArkaniaMapUpdateSeason6.cs"
```
Expected: first two match; the third prints `0` (all `SPAWN_X`/`SPAWN_Y` placeholders were replaced with numbers). If it prints non-zero, replace the remaining placeholders.
- [ ] **Step 5: Commit**
```bash
cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw"
git add src/Persistence/Initialization/Updates/UpdateVersion.cs src/Persistence/Initialization/Updates/AddArkaniaMapUpdateSeason6.cs
git commit -m "feat(maps): add Arkania DB update-plugin with spawn gate and /move warp"
```
---
### Task 4: Build the server image and verify map 83 is in the configuration
**Files:** none (build + verification only).
- [ ] **Step 1: Docker build**
Run (from AdamuSw root):
```bash
cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw"
docker build -f Startup/Dockerfile -t adamu-openmu:dev . 2>&1 | tail -20
```
Expected: `Successfully tagged adamu-openmu:dev` (or the buildkit equivalent). If compilation fails, fix the reported C# error and rebuild.
- [ ] **Step 2: Verify the map symbol is compiled into the initialization DLL**
Run:
```bash
docker run --rm --entrypoint sh adamu-openmu:dev -c "grep -a -c Arkania /app/MUnique.OpenMU.Persistence.Initialization.dll || true"
```
Expected: a non-zero count (the `Arkania`/`AddArkaniaMap` strings are present in the built DLL). If `0`, the class was not compiled in — recheck Task 2/3.
- [ ] **Step 3: Commit (no code; checkpoint only if any fixes were made)**
Only if Steps 1-2 required code fixes:
```bash
cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw"
git commit -am "fix(maps): resolve Arkania build errors"
```
---
### Task 5: Deploy the server and apply the update on the existing DB
**Files:** none (deploy + admin action).
- [ ] **Step 1: Bring the server up with the new image**
Run:
```bash
cd "/d/OpenMU/MU Client_Mobile 1.04d - Season 6E3/AdamuSw"
docker compose -f deploy-adamu/docker-compose.local.yml up -d
```
Expected: containers start (game server + admin panel). Note: if the server points at the remote `65.109.224.204` DB instead of local, deploy the image to that host per the established deploy flow instead.
- [ ] **Step 2: Apply the update via AdminPanel → Updates**
Open the AdminPanel (`http://localhost:8081` locally, or the server's admin URL), go to the **Updates** page, and apply **"Add Arkania map (83)"**. (Fresh/zero DBs already have it from the seed and will show nothing to apply.)
Expected: the update applies without error and disappears from the pending list.
- [ ] **Step 3: Verify the map exists in the panel**
In the AdminPanel, open `/edit-config-grid/GameMapDefinition` (Config → Game Maps).
Expected: **Arkania** appears in the list with number 83.
- [ ] **Step 4: Verify terrain renders in the visual editor**
Open `/map-editor`, select **Arkania**.
Expected: the terrain image renders (not blank). Blank terrain ⇒ the `Terrain84.att` resource didn't load — recheck Task 1 (csproj embedding + off-by-one name).
---
### Task 6: Copy the client assets to the PC client
**Files:**
- Copy into `D:\AdaMu\AdaMu\Data`: `World83\`, `Object83\`, and the `Local\Eng` additions.
**Interfaces:**
- Consumes: pack `Client Side/Data/{World83,Object83,Local/Eng}`.
- Produces: renderable client-side assets for world index 83.
- [ ] **Step 1: Copy World83 and Object83**
Run:
```bash
PACK="/c/Users/efpa/Downloads/Arkania Acheron Debenter Uruk Ferea/Arkania Acheron Debenter Uruk Ferea/Client Side/Data"
DST="/d/AdaMu/AdaMu/Data"
cp -rf "$PACK/World83" "$DST/World83"
cp -rf "$PACK/Object83" "$DST/Object83"
ls "$DST/World83/EncTerrain83.att" "$DST/Object83" >/dev/null && echo "World83+Object83 copied"
```
Expected: prints `World83+Object83 copied`.
- [ ] **Step 2: Copy the map-name image and minimap for world 83**
Run:
```bash
PACK="/c/Users/efpa/Downloads/Arkania Acheron Debenter Uruk Ferea/Arkania Acheron Debenter Uruk Ferea/Client Side/Data/Local/Eng"
DST="/d/AdaMu/AdaMu/Data/Local/Eng"
[ -f "$PACK/ImgsMapName/Arkania.OZT" ] && cp -f "$PACK/ImgsMapName/Arkania.OZT" "$DST/ImgsMapName/" && echo "map-name image copied"
ls "$PACK/Minimap/" | grep -i "World83" | while read f; do cp -f "$PACK/Minimap/$f" "$DST/Minimap/"; echo "minimap $f copied"; done
```
Expected: the Arkania name image copies; a `Minimap_World83_eng.bmd` copies if present (absence is non-fatal — minimap only).
- [ ] **Step 3: Verify no case-mismatch on referenced object textures**
The MUnique PC client on Windows is case-insensitive, so a spot check suffices. Run:
```bash
ls "/d/AdaMu/AdaMu/Data/World83" | head; echo "---"; ls "/d/AdaMu/AdaMu/Data/Object83" | wc -l
```
Expected: World83 lists `EncTerrain83.*` + tiles; Object83 has a non-zero file count.
_(No commit — `D:\AdaMu\AdaMu` is the deployed client folder, not a git repo.)_
---
### Task 7: Verify in the PC client
**Files:** none (runtime verification). May require a `vendor/MuMain` change + rebuild if the engine caps the world index.
- [ ] **Step 1: Launch the client and reach Arkania**
Start `D:\AdaMu\AdaMu\main.exe`, log in to a character (server `65.109.224.204`), and issue `/move Arkania` in chat.
Expected: the character warps to Arkania; terrain and objects render; no `OpenTexture Failed` / crash; the character can walk.
- [ ] **Step 2: If the map does not load (world-index unsupported by the engine)**
If the client crashes or shows a blank/again-Lorencia world on entering 83, the MUnique engine may not handle world index 83. Check the terrain/world-loading path in `vendor/MuMain/src/source` for a hard world-index cap or a per-index table, add support for 83, and rebuild `Main.exe` (see the `pc-munique-client-build` memory: `vcvarsall x86` + vswhere on PATH + `cmake --build out/build/windows-x86-fresh --config Release`), then redeploy `main.exe` to `D:\AdaMu\AdaMu`. Re-run Step 1.
- [ ] **Step 3: Final confirmation**
Confirm all three surfaces:
- Server/panel: Arkania (83) listed, terrain renders in `/map-editor`.
- Client: `/move Arkania` enters a walkable, correctly-rendered map.
- Then the user adds monster/NPC spawns via the panel and confirms they appear live.
Pilot complete. Follow-up (separate plan): replicate Tasks 1-7 for maps 84-87 (Acheron/Debenter/Uruk/Ferea) — each terrain resource is `Terrain{N+1}.att` from the pack's `Terrain{N}.att` — plus mobile-client asset import.
## Self-Review notes
- **Spec coverage:** terrain resource (Task 1), map class + registration (Task 2), update-plugin + warp/gate (Task 3), build/verify (Task 4), deploy/apply/panel-verify (Task 5), client assets (Task 6), client verify (Task 7). All spec sections covered.
- **Off-by-one terrain naming** is captured as a Global Constraint and applied in Task 1 (resource `Terrain84.att` ← pack `Terrain83.att`).
- **No monster spawns in the seed** — intentional (user adds via panel), matching the spec.
- **Type consistency:** `Arkania.Number` (83) and `Arkania.Name` ("Arkania") defined in Task 2 are the exact symbols consumed in Task 3; `UpdateVersion.AddArkaniaMapSeason6` (=96) defined and consumed consistently.