Compare commits
16 Commits
fbe75d55be
...
feature/ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d5d40b753 | ||
|
|
1cadaa08ab | ||
|
|
3e8cd8fdae | ||
|
|
affa4bd73b | ||
|
|
fc9ba0e594 | ||
|
|
d84b29eac3 | ||
|
|
80ec466ab1 | ||
|
|
e3d37467d5 | ||
|
|
7e21fc1fb3 | ||
|
|
4628902f11 | ||
|
|
a61890ef6e | ||
|
|
3b2e26b27c | ||
|
|
1de06d83ab | ||
|
|
2927be1ba9 | ||
|
|
16752f3649 | ||
|
|
ef3bffe39c |
@@ -17,6 +17,10 @@ services:
|
||||
environment:
|
||||
DB_HOST: database
|
||||
ASPNETCORE_URLS: http://+:8080
|
||||
# ConnectServer'in GameServer'i client'a hangi IP ile bildirecegi.
|
||||
# Default: emulator host alias 10.0.2.2 (normal ConnectServer 44405 akisi calissin).
|
||||
# Gercek cihaz icin: RESOLVE_IP=<PC LAN IP> ile override et (orn. 192.168.0.4).
|
||||
RESOLVE_IP: ${RESOLVE_IP:-10.0.2.2}
|
||||
working_dir: /app/
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# C1 F6 0C - QuestProgressExtended (by server)
|
||||
# C2 F6 0C - QuestProgressExtended (by server)
|
||||
|
||||
## Is sent when
|
||||
|
||||
@@ -12,10 +12,10 @@ The client shows the quest progress accordingly.
|
||||
|
||||
| Index | Length | Data Type | Value | Description |
|
||||
|-------|--------|-----------|-------|-------------|
|
||||
| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) |
|
||||
| 1 | 1 | Byte | 272 | Packet header - length of the packet |
|
||||
| 2 | 1 | Byte | 0xF6 | Packet header - packet type identifier |
|
||||
| 3 | 1 | Byte | 0x0C | Packet header - sub packet type identifier |
|
||||
| 0 | 1 | Byte | 0xC2 | [Packet type](PacketTypes.md) |
|
||||
| 1 | 2 | Short | 272 | Packet header - length of the packet |
|
||||
| 3 | 1 | Byte | 0xF6 | Packet header - packet type identifier |
|
||||
| 4 | 1 | Byte | 0x0C | Packet header - sub packet type identifier |
|
||||
| 5 | 1 | Byte | | ConditionCount |
|
||||
| 6 | 1 | Byte | | RewardCount |
|
||||
| 7 | 1 | Byte | | RandomRewardCount |
|
||||
@@ -234,7 +234,7 @@
|
||||
* [C1 F6 0A - AvailableQuests (by server)](C1-F6-0A-AvailableQuests_by-server.md)
|
||||
* [C1 F6 0B - QuestStepInfo (by server)](C1-F6-0B-QuestStepInfo_by-server.md)
|
||||
* [C1 F6 0C - QuestProgress (by server)](C1-F6-0C-QuestProgress_by-server.md)
|
||||
* [C1 F6 0C - QuestProgressExtended (by server)](C1-F6-0C-QuestProgressExtended_by-server.md)
|
||||
* [C2 F6 0C - QuestProgressExtended (by server)](C2-F6-0C-QuestProgressExtended_by-server.md)
|
||||
* [C1 F6 0D - QuestCompletionResponse (by server)](C1-F6-0D-QuestCompletionResponse_by-server.md)
|
||||
* [C1 F6 0F - QuestCancelled (by server)](C1-F6-0F-QuestCancelled_by-server.md)
|
||||
* [C1 F6 1A - QuestStateList (by server)](C1-F6-1A-QuestStateList_by-server.md)
|
||||
|
||||
446
docs/superpowers/plans/2026-07-19-new-maps-arkania-pilot.md
Normal file
446
docs/superpowers/plans/2026-07-19-new-maps-arkania-pilot.md
Normal file
@@ -0,0 +1,446 @@
|
||||
# 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.
|
||||
75
docs/superpowers/specs/2026-07-19-new-maps-import-design.md
Normal file
75
docs/superpowers/specs/2026-07-19-new-maps-import-design.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# New Maps Import — Design (Pilot: Arkania / World 83)
|
||||
|
||||
**Date:** 2026-07-19
|
||||
**Status:** Approved (design), pending implementation plan
|
||||
**Scope of this spec:** Import ONE pilot map (Arkania, world index 83) end-to-end across server (OpenMU), PC client, and web admin panel. The other four maps (Acheron 84, Debenter 85, Uruk 86, Ferea 87) and the mobile client are explicitly out of scope for the pilot and will be replicated from the proven pilot template in follow-up work.
|
||||
|
||||
## Background
|
||||
|
||||
A third-party map pack provides 5 maps with both client and server assets:
|
||||
|
||||
- **Source:** `C:\Users\efpa\Downloads\Arkania Acheron Debenter Uruk Ferea\Arkania Acheron Debenter Uruk Ferea`
|
||||
- **Client Side/Data:** `World83-87` (terrain: `EncTerrain8N.att/.map/.obj`, `TerrainHeight.OZB`, `TerrainLight.OZJ`, tile textures, `minimap.map`), `Object83-87` (object BMD models + textures), `Local/Eng` (`ImgsMapName/*.OZT` map-name images, `Minimap/*.bmd`, `movereq_eng.bmd`).
|
||||
- **Server Side/Data:** `MapManager.txt` (classic C++ MuServer map config — reference only), `Move/Gate.txt`, `Move.txt`, `MoveSummon.txt` (warp gates — reference only), `Terrain/Terrain83-87.att` (walkability/safezone attributes).
|
||||
|
||||
The server-side files are **classic C++ MuServer format** and are NOT consumed by OpenMU — except the `.att` terrain files, which match OpenMU's terrain resource format exactly.
|
||||
|
||||
### Key technical findings (verified)
|
||||
|
||||
1. **Terrain is a drop-in for OpenMU.** OpenMU loads map terrain via `GameMapDefinition.TerrainData`, populated by `_mapDefinition.UpdateTerrainFromResources(TerrainVersionPrefix)` in `BaseMapInitializer`, from an embedded resource `Resources/Terrain{Number}.att`. The pack's server `Terrain83.att` is **65539 bytes, header `00 ff ff 04`** — byte-for-byte the same format as OpenMU's existing `Resources/Terrain1.att` (65539, `00 ff ff 04`). The client's `EncTerrain83.att` (65540, encrypted) is NOT what the server wants; the server-side `.att` is.
|
||||
2. **Indices 82-87 are free** in the current OpenMU (AdamuSw) map set — no collision at 83.
|
||||
3. **Web admin panel needs no code changes.** Once a `GameMapDefinition` (number 83) exists in the DB, it appears automatically in:
|
||||
- `/edit-config-grid/GameMapDefinition` (GameMaps CRUD)
|
||||
- `/map-editor` → visual `MapEditor` (renders terrain from `TerrainData`; `CreateNewSpawnArea`, `CreateNewEnterGate`, `CreateNewExitGate`, duplicate/remove/undo/save)
|
||||
- `/edit-config-grid/WarpInfo` (`/move` warp list), `/edit-config-grid/MonsterDefinition` (monsters)
|
||||
4. **Monster spawns / NPCs are NOT in the pack.** The seeded map is intentionally EMPTY (scenery + walkable + safezone only). Spawns, NPCs and gates are added by the user via the web MapEditor.
|
||||
5. **Index→name ambiguity.** Pack folders are World83-87 (no World82). `MapManager.txt` labels 82=Arkania,83=Acheron,…,87=TeamvsTeam (off-by-one / stock reference). The pack's own title order ("Arkania Acheron Debenter Uruk Ferea") + available folders (83-87) imply **83=Arkania, 84=Acheron, 85=Debenter, 86=Uruk, 87=Ferea**. Pilot uses **World83 = Arkania**, to be confirmed against the pack screenshots during implementation.
|
||||
|
||||
## Chosen approach
|
||||
|
||||
**Approach A — OpenMU-native C# map initializer + terrain resource + DB update-plugin.** This mirrors every existing OpenMU map and the proven Imperial-items dual-path pattern (see `android-add-new-weapon-item`). Rejected alternatives: (B) pure-panel creation — not viable, the 65KB `TerrainData` byte[] cannot be entered via a web form; (C) build a C++→OpenMU map converter to use the MapEditor Import — unnecessary work for a pilot.
|
||||
|
||||
## Components / changes
|
||||
|
||||
Server repo: `AdamuSw` (Gitea, branch off `main`).
|
||||
|
||||
1. **Terrain resource** — copy pack `Server Side/Data/Terrain/Terrain83.att` → `src/Persistence/Initialization/Resources/Terrain83.att`. Verify it is included as an embedded resource by the `.csproj` (the `Resources/*.att` glob); add explicitly if not globbed.
|
||||
2. **Map initializer** — `src/Persistence/Initialization/VersionSeasonSix/Maps/Arkania.cs`:
|
||||
- `Number => 83`
|
||||
- `Name => "Arkania"`
|
||||
- `TerrainVersionPrefix => string.Empty`
|
||||
- Safezone / spawn gate as required by `BaseMapInitializer` (minimal; a single safe spawn gate so players don't spawn on a blocked tile).
|
||||
- Empty `CreateMonsterSpawns` / NPC init (spawns added later via panel).
|
||||
- Follow the existing map-class pattern (many inherit a `Version075.Maps.*` base; Arkania is new so it derives from `BaseMapInitializer` directly, like other SeasonSix-only maps).
|
||||
3. **Registration (fresh seed)** — add `Arkania` to the map initializer list so a zero DB seeds it (alongside `Weapons.Initialize()`-style wiring in the SeasonSix data-initialization).
|
||||
4. **DB update-plugin (existing DB)** — `src/Persistence/Initialization/Updates/AddArkaniaMapUpdateSeason6.cs`:
|
||||
- `UpdateVersion` = next free number (after the Imperial `95`).
|
||||
- Idempotency guard: if a map with `Number == 83` already exists, skip.
|
||||
- Creates the map by invoking the same logic as the initializer.
|
||||
- Applied at runtime only via **AdminPanel → Updates** (per the Imperial finding; `DataUpdateService` is only triggered there). Do NOT call `SaveChanges` inside the plugin.
|
||||
5. **Access / warp (for testing)** — a `WarpInfo` entry ("Arkania", target map 83, coordinates on a safe tile, level/zen cost) so the map is reachable via `/move Arkania`, plus an entrance `ExitGate`. May be seeded or added via panel; seeding it keeps the pilot self-contained for verification.
|
||||
6. **PC client** — copy `World83`, `Object83`, and the `Local/Eng` additions (`ImgsMapName/Arkania.OZT`, `Minimap/Minimap_World83_eng.bmd` if present, merge `movereq_eng.bmd`) into `D:\AdaMu\AdaMu\Data`. Verify the MUnique engine supports world index 83 (terrain/object loading is generic; if a world-index cap or per-index table blocks it, add support in `vendor/MuMain` source and rebuild `Main.exe` per `pc-munique-client-build`).
|
||||
|
||||
## Data flow
|
||||
|
||||
Server loads map 83 from `GameConfiguration` → `UpdateTerrainFromResources` decodes `Terrain83.att` into walkable/safezone grid → player issues `/move Arkania` (or uses the entrance gate) → server places them on the safe spawn tile of map 83 → PC client renders `Data/World83` (terrain + objects) → empty but walkable map. In the web panel, `MapEditor` renders the same terrain from `TerrainData`; the user places `MonsterSpawnArea`/gates/NPCs and saves → they take effect on the live server.
|
||||
|
||||
## Verification
|
||||
|
||||
1. **Server build:** `docker build -f Startup/Dockerfile -t adamu-openmu:dev .` succeeds; map 83 present in built config (symbol/log check). Local `dotnet build` is expected to fail at the source-gen pre-step (normal, per Imperial notes).
|
||||
2. **Web:** map "Arkania" appears in `/edit-config-grid/GameMapDefinition`; `/map-editor` renders its terrain (not blank).
|
||||
3. **PC client:** `/move Arkania` enters the map; terrain and objects render; no crash; safezone correct; player can walk.
|
||||
4. Only after all three pass is the pilot considered done.
|
||||
|
||||
## Out of scope (follow-up, after pilot proven)
|
||||
|
||||
- Maps 84-87 (Acheron/Debenter/Uruk/Ferea) — same template, replicated.
|
||||
- Mobile client asset import + APK considerations.
|
||||
- Authoring actual monster spawns / NPCs / drops (done by the user via panel; not part of the seed).
|
||||
- Any C++→OpenMU map converter.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Index/name mismatch** → wrong map name shown. Mitigation: confirm World83 identity against pack screenshots before finalizing `Name`.
|
||||
- **Engine world-index support** on the MUnique client for index 83 → if unsupported, requires a `vendor/MuMain` change + rebuild. Low likelihood (loading is data-driven) but flagged.
|
||||
- **Terrain resource not globbed** by the csproj → map loads with empty terrain. Mitigation: verify embedded-resource inclusion during step 1.
|
||||
34
docs/superpowers/specs/2026-07-19-poison-wings-design.md
Normal file
34
docs/superpowers/specs/2026-07-19-poison-wings-design.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Add Poison Wing & Poison Cape — Design
|
||||
|
||||
**Date:** 2026-07-19
|
||||
**Status:** Approved
|
||||
**Scope:** Add TWO wing items (basic: render + equippable + base defense). Source pack: `C:\Users\efpa\Downloads\asa devilzmu\asa devilzmu\6` (Wing241, Wing242).
|
||||
|
||||
## Items
|
||||
|
||||
| Item | Slot (group/number) | Model | Name | Classes |
|
||||
|------|---------------------|-------|------|---------|
|
||||
| Wing241 | 12 / 240 | MODEL_WING+240 | Poison Wing | Dark Knight |
|
||||
| Wing242 | 12 / 241 | MODEL_WING+241 | Poison Cape | Dark Lord + Rage Fighter |
|
||||
|
||||
Wings are item group 12; `MODEL_WING = MODEL_ITEM + ITEM_WING (12*512)`, so item (12, N) auto-maps to model `MODEL_WING + N`. Numbers 240/241 are high/free in standard S6. Model files `Wing241.bmd`/`Wing242.bmd` load at `MODEL_WING+240`/`+241` (filename `Wing{N}` where N=number+1).
|
||||
|
||||
## Client (vendor/MuMain → D:\AdaMu\AdaMu)
|
||||
|
||||
1. `ZzzOpenData.cpp OpenItems()`: `AccessModel(MODEL_WING+240, L"Data\\Item\\", L"Wing", 241)` and `+241 → "Wing242"`. `OpenItemTextures()`: `OpenTexture(MODEL_WING+240/241, L"Item\\")`.
|
||||
2. Copy `Wing241.bmd` + its texture, `Wing242.bmd` + its texture → `Data\Item\`. Extract the BMD's referenced texture names (decrypt) and copy the matching files (the pack ships `.ozt`/`.tga`; the engine loads the encrypted `.OZT`/`.OZJ`, so provide those; missing effect textures now log instead of blocking).
|
||||
3. `Item_eng.bmd`: add name/stat records at slots 12/240 ("Poison Wing", DK) and 12/241 ("Poison Cape", DL+RF), via the 84-byte-record binary patcher (see `android-add-new-weapon-item`). Slot field = wing slot.
|
||||
4. Rebuild `main.exe` + deploy.
|
||||
|
||||
## Server (AdamuSw)
|
||||
|
||||
5. `VersionSeasonSix/Items/Wings.cs`: new method `CreatePoisonWings()` with two `CreateWing` calls — `CreateWing(240, W, H, "Poison Wing", ...DK flag...)` and `CreateWing(241, W, H, "Poison Cape", ...DL+RF flags...)`, basic defense + level table, minimal options. W/H from the item icon size (default 12x12 or per pack).
|
||||
6. Register in `Wings.Initialize()` (fresh seed) + update-plugin `AddPoisonWingsUpdateSeason6.cs` (UpdateVersion=98, idempotency guard on 12/240).
|
||||
7. `docker build` + apply via AdminPanel → Updates (existing DB) / auto on fresh seed.
|
||||
|
||||
## Verify
|
||||
|
||||
Give the wings to a character (AdminPanel/inventory edit), equip in the wing slot, confirm: correct name/stats in tooltip (from Item_eng.bmd), 3D model renders on the character's back, class restriction enforced.
|
||||
|
||||
## Out of scope
|
||||
Wing options (excellent), flight, per-level ancient/socket bonuses, balance. Just render + equip + base defense.
|
||||
@@ -202,10 +202,12 @@ public class ResetCharacterAction
|
||||
if (homeMapDef is { }
|
||||
&& await this._player.GameContext.GetMapAsync((ushort)homeMapDef.Number).ConfigureAwait(false) is { SafeZoneSpawnGate: { } spawnGate })
|
||||
{
|
||||
this._player.SelectedCharacter.PositionX = (byte)Rand.NextInt(spawnGate.X1, spawnGate.X2);
|
||||
this._player.SelectedCharacter.PositionY = (byte)Rand.NextInt(spawnGate.Y1, spawnGate.Y2);
|
||||
this._player.SelectedCharacter.CurrentMap = spawnGate.Map;
|
||||
this._player.Rotation = spawnGate.Direction;
|
||||
// ADAMU-CUSTOM: warp instead of writing the position fields directly.
|
||||
// The direct writes moved the player without removing him from the map or sending a
|
||||
// map change, so the client was never told and animated a walk to the new spot: the
|
||||
// character visibly slid across the map. WarpToAsync does the same placement plus the
|
||||
// map removal and the client notification.
|
||||
await this._player.WarpToAsync(spawnGate).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
<None Remove="Resources\075_Terrain5.att" />
|
||||
<None Remove="Resources\075_Terrain8.att" />
|
||||
<None Remove="Resources\Terrain1.att" />
|
||||
<None Remove="Resources\Terrain83.att" />
|
||||
<None Remove="Resources\Terrain84.att" />
|
||||
<None Remove="Resources\Terrain85.att" />
|
||||
<None Remove="Resources\Terrain86.att" />
|
||||
<None Remove="Resources\Terrain87.att" />
|
||||
<None Remove="Resources\Terrain10_1.att" />
|
||||
<None Remove="Resources\Terrain10_2.att" />
|
||||
<None Remove="Resources\Terrain10_3.att" />
|
||||
@@ -103,6 +108,11 @@
|
||||
<EmbeddedResource Include="Resources\075_Terrain5.att" />
|
||||
<EmbeddedResource Include="Resources\075_Terrain8.att" />
|
||||
<EmbeddedResource Include="Resources\Terrain1.att" />
|
||||
<EmbeddedResource Include="Resources\Terrain83.att" />
|
||||
<EmbeddedResource Include="Resources\Terrain84.att" />
|
||||
<EmbeddedResource Include="Resources\Terrain85.att" />
|
||||
<EmbeddedResource Include="Resources\Terrain86.att" />
|
||||
<EmbeddedResource Include="Resources\Terrain87.att" />
|
||||
<EmbeddedResource Include="Resources\Terrain10_4.att" />
|
||||
<EmbeddedResource Include="Resources\Terrain10_3.att" />
|
||||
<EmbeddedResource Include="Resources\Terrain10_2.att" />
|
||||
|
||||
BIN
src/Persistence/Initialization/Resources/Terrain83.att
Normal file
BIN
src/Persistence/Initialization/Resources/Terrain83.att
Normal file
Binary file not shown.
BIN
src/Persistence/Initialization/Resources/Terrain84.att
Normal file
BIN
src/Persistence/Initialization/Resources/Terrain84.att
Normal file
Binary file not shown.
BIN
src/Persistence/Initialization/Resources/Terrain85.att
Normal file
BIN
src/Persistence/Initialization/Resources/Terrain85.att
Normal file
Binary file not shown.
BIN
src/Persistence/Initialization/Resources/Terrain86.att
Normal file
BIN
src/Persistence/Initialization/Resources/Terrain86.att
Normal file
Binary file not shown.
BIN
src/Persistence/Initialization/Resources/Terrain87.att
Normal file
BIN
src/Persistence/Initialization/Resources/Terrain87.att
Normal file
Binary file not shown.
@@ -0,0 +1,91 @@
|
||||
// <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 async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration)
|
||||
{
|
||||
// Idempotency guard: skip if Arkania (82) already exists.
|
||||
if (gameConfiguration.Maps.Any(m => m.Number == Arkania.Number))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var initializer = new Arkania(context, gameConfiguration);
|
||||
initializer.Initialize(); // creates map 82 + loads terrain from Terrain83.att
|
||||
initializer.SetSafezoneMap();
|
||||
|
||||
var arkania = gameConfiguration.Maps.First(m => m.Number == Arkania.Number);
|
||||
|
||||
// Host the map on every game server configuration (fresh seeds do this
|
||||
// automatically, but existing databases need it added explicitly).
|
||||
var serverConfigs = await context.GetAsync<GameServerConfiguration>().ConfigureAwait(false);
|
||||
foreach (var serverConfig in serverConfigs)
|
||||
{
|
||||
if (serverConfig.Maps.All(m => m.Number != Arkania.Number))
|
||||
{
|
||||
serverConfig.Maps.Add(arkania);
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn gate on a walkable tile (center of the map; attr 0x00, verified walkable).
|
||||
var spawnGate = context.CreateNew<ExitGate>();
|
||||
arkania.ExitGates.Add(spawnGate);
|
||||
spawnGate.Map = arkania;
|
||||
spawnGate.X1 = 128;
|
||||
spawnGate.Y1 = 128;
|
||||
spawnGate.X2 = 128;
|
||||
spawnGate.Y2 = 128;
|
||||
spawnGate.IsSpawnGate = true;
|
||||
|
||||
// /move warp entry.
|
||||
if (gameConfiguration.WarpList.All(w => w.Name != Arkania.Name))
|
||||
{
|
||||
var warp = context.CreateNew<WarpInfo>();
|
||||
gameConfiguration.WarpList.Add(warp);
|
||||
warp.Index = 83;
|
||||
warp.Name = Arkania.Name;
|
||||
warp.Costs = 5000;
|
||||
warp.LevelRequirement = 10;
|
||||
warp.Gate = spawnGate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// <copyright file="AddImperialWeapons050UpdateSeason6.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.Items;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Adds two new weapons at item number 50: Imperial Sword (group 0, Magic Gladiator) and
|
||||
/// Imperial Staff (group 5, Wizard), as full working weapons (damage, attack speed, requirements,
|
||||
/// options, equip flags). Placed at free slots so they never collide with standard items.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Display(Name = PlugInName, Description = PlugInDescription)]
|
||||
[Guid("BE9DD7D1-F352-4C56-A646-D78C36978982")]
|
||||
public class AddImperialWeapons050UpdateSeason6 : UpdatePlugInBase
|
||||
{
|
||||
internal const string PlugInName = "Add Imperial Weapons (0/50, 5/50)";
|
||||
internal const string PlugInDescription = "Adds Imperial Sword (0/50) and Imperial Staff (5/50) as fully working weapons.";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override UpdateVersion Version => UpdateVersion.AddImperialWeapons050Season6;
|
||||
|
||||
/// <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, 16, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CS1998
|
||||
protected override async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration)
|
||||
#pragma warning restore CS1998
|
||||
{
|
||||
// Idempotency guard: skip if the Imperial Sword 0/50 already exists.
|
||||
if (gameConfiguration.Items.Any(i => i.Group == 0 && i.Number == 50))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
new Weapons(context, gameConfiguration).CreateImperialWeapons050();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// <copyright file="AddImportedMapsUpdateSeason6.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 four imported maps Acheron (83), Debenter (84), Uruk (85) and Ferea (86):
|
||||
/// the map definitions with their terrain, a spawn gate and a /move warp each, and
|
||||
/// assigns each to the game server configuration so it is hosted and its spawns run.
|
||||
/// Monsters and NPCs are added afterwards via the admin panel.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Display(Name = PlugInName, Description = PlugInDescription)]
|
||||
[Guid("3D9C1B72-7C3F-4E8B-AF14-E29D47A8B012")]
|
||||
public class AddImportedMapsUpdateSeason6 : UpdatePlugInBase
|
||||
{
|
||||
internal const string PlugInName = "Add imported maps (Acheron, Debenter, Uruk, Ferea)";
|
||||
internal const string PlugInDescription = "Adds maps 83-86 (Acheron, Debenter, Uruk, Ferea) with terrain, spawn gate and /move warp, hosted by the game server. Monsters/NPCs are added via the admin panel.";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override UpdateVersion Version => UpdateVersion.AddImportedMapsSeason6;
|
||||
|
||||
/// <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, 15, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration)
|
||||
{
|
||||
var serverConfigs = (await context.GetAsync<GameServerConfiguration>().ConfigureAwait(false)).ToList();
|
||||
|
||||
AddMap(context, gameConfiguration, serverConfigs, new Acheron(context, gameConfiguration), Acheron.Number, Acheron.Name, 84, 127, 126);
|
||||
AddMap(context, gameConfiguration, serverConfigs, new Debenter(context, gameConfiguration), Debenter.Number, Debenter.Name, 85, 132, 105);
|
||||
AddMap(context, gameConfiguration, serverConfigs, new Uruk(context, gameConfiguration), Uruk.Number, Uruk.Name, 86, 128, 128);
|
||||
AddMap(context, gameConfiguration, serverConfigs, new Ferea(context, gameConfiguration), Ferea.Number, Ferea.Name, 87, 128, 128);
|
||||
}
|
||||
|
||||
private static void AddMap(IContext context, GameConfiguration gameConfiguration, IList<GameServerConfiguration> serverConfigs, IMapInitializer initializer, byte number, string name, short warpIndex, byte x, byte y)
|
||||
{
|
||||
// Idempotency guard: skip if the map already exists.
|
||||
if (gameConfiguration.Maps.Any(m => m.Number == number))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
initializer.Initialize(); // creates the map + loads terrain from Terrain{number+1}.att
|
||||
initializer.SetSafezoneMap();
|
||||
|
||||
var map = gameConfiguration.Maps.First(m => m.Number == number);
|
||||
|
||||
// Spawn gate on a walkable tile.
|
||||
var spawnGate = context.CreateNew<ExitGate>();
|
||||
map.ExitGates.Add(spawnGate);
|
||||
spawnGate.Map = map;
|
||||
spawnGate.X1 = x;
|
||||
spawnGate.Y1 = y;
|
||||
spawnGate.X2 = x;
|
||||
spawnGate.Y2 = y;
|
||||
spawnGate.IsSpawnGate = true;
|
||||
|
||||
// /move warp entry.
|
||||
if (gameConfiguration.WarpList.All(w => w.Name != name))
|
||||
{
|
||||
var warp = context.CreateNew<WarpInfo>();
|
||||
gameConfiguration.WarpList.Add(warp);
|
||||
warp.Index = warpIndex;
|
||||
warp.Name = name;
|
||||
warp.Costs = 5000;
|
||||
warp.LevelRequirement = 10;
|
||||
warp.Gate = spawnGate;
|
||||
}
|
||||
|
||||
// Host the map on every game server configuration (fresh seeds do this
|
||||
// automatically, but existing databases need it added explicitly).
|
||||
foreach (var serverConfig in serverConfigs)
|
||||
{
|
||||
if (serverConfig.Maps.All(m => m.Number != number))
|
||||
{
|
||||
serverConfig.Maps.Add(map);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// <copyright file="AddPoisonWingsUpdateSeason6.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.Items;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Adds two imported wings at free item slots: Poison Wing (12/240, Dark Knight) and
|
||||
/// Poison Cape (12/241, Dark Lord + Rage Fighter). Basic wings: base defense, class
|
||||
/// restriction, CanFly; no wing options.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Display(Name = PlugInName, Description = PlugInDescription)]
|
||||
[Guid("7E4A2C63-9B1D-4F8A-A5C6-1D3E8F2B4A07")]
|
||||
public class AddPoisonWingsUpdateSeason6 : UpdatePlugInBase
|
||||
{
|
||||
internal const string PlugInName = "Add Poison Wing & Poison Cape (12/240, 12/241)";
|
||||
internal const string PlugInDescription = "Adds Poison Wing (12/240, Dark Knight) and Poison Cape (12/241, Dark Lord + Rage Fighter) as high-tier wings (defense, 3rd Wing Options, damage increase, fast flight).";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override UpdateVersion Version => UpdateVersion.AddPoisonWingsSeason6;
|
||||
|
||||
/// <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, 17, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CS1998
|
||||
protected override async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration)
|
||||
#pragma warning restore CS1998
|
||||
{
|
||||
// Idempotency guard: skip if Poison Wing (12/240) already exists.
|
||||
if (gameConfiguration.Items.Any(i => i.Group == 12 && i.Number == 240))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
new Wings(context, gameConfiguration).CreatePoisonWings();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// <copyright file="AddWarriorWingsUpdateSeason6.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.Items;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Adds six imported high-tier "Warrior" wings at free item slots (12/242-247), one per class line:
|
||||
/// Wizard (Soul Master), Magic (Duel Master), Elf (High Elf), Summoner (Dimension Master),
|
||||
/// Warrior Cape (Lord Emperor + Fist Master) and Knight (Blade Master). Each carries base defense,
|
||||
/// class restriction, 3rd Wing Options, damage increase and fast flight.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Display(Name = PlugInName, Description = PlugInDescription)]
|
||||
[Guid("8F5B3D74-AC2E-4F9B-B6D7-2E4F9A3C5B18")]
|
||||
public class AddWarriorWingsUpdateSeason6 : UpdatePlugInBase
|
||||
{
|
||||
internal const string PlugInName = "Add Warrior Wings & Cape (12/242-247)";
|
||||
internal const string PlugInDescription = "Adds six high-tier Warrior wings (12/242-247): Wizard, Magic, Elf, Summoner, Warrior Cape (DL+RF) and Knight, each with defense, 3rd Wing Options, damage increase and fast flight.";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override UpdateVersion Version => UpdateVersion.AddWarriorWingsSeason6;
|
||||
|
||||
/// <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, 20, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CS1998
|
||||
protected override async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration)
|
||||
#pragma warning restore CS1998
|
||||
{
|
||||
// Idempotency guard: skip if the first Warrior wing (12/242) already exists.
|
||||
if (gameConfiguration.Items.Any(i => i.Group == 12 && i.Number == 242))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
new Wings(context, gameConfiguration).CreateWarriorWings();
|
||||
}
|
||||
}
|
||||
@@ -479,4 +479,29 @@ public enum UpdateVersion
|
||||
/// The version of the <see cref="AddIsQuestItemFlagPlugIn"/>.
|
||||
/// </summary>
|
||||
AddIsQuestItemFlag = 94,
|
||||
|
||||
/// <summary>
|
||||
/// The version of the <see cref="AddImperialWeapons050UpdateSeason6"/>.
|
||||
/// </summary>
|
||||
AddImperialWeapons050Season6 = 95,
|
||||
|
||||
/// <summary>
|
||||
/// The version of the <see cref="AddArkaniaMapUpdateSeason6"/>.
|
||||
/// </summary>
|
||||
AddArkaniaMapSeason6 = 96,
|
||||
|
||||
/// <summary>
|
||||
/// The version of the <see cref="AddImportedMapsUpdateSeason6"/> (Acheron, Debenter, Uruk, Ferea).
|
||||
/// </summary>
|
||||
AddImportedMapsSeason6 = 97,
|
||||
|
||||
/// <summary>
|
||||
/// The version of the <see cref="AddPoisonWingsUpdateSeason6"/> (Poison Wing 12/240, Poison Cape 12/241).
|
||||
/// </summary>
|
||||
AddPoisonWingsSeason6 = 98,
|
||||
|
||||
/// <summary>
|
||||
/// The version of the <see cref="AddWarriorWingsUpdateSeason6"/> (Warrior Wings/Cape 12/242-247).
|
||||
/// </summary>
|
||||
AddWarriorWingsSeason6 = 99,
|
||||
}
|
||||
@@ -100,6 +100,11 @@ public class GameMapsInitializer : GameMapsInitializerBase
|
||||
yield return typeof(FortressOfImperialGuardian2);
|
||||
yield return typeof(FortressOfImperialGuardian3);
|
||||
yield return typeof(FortressOfImperialGuardian4);
|
||||
yield return typeof(Arkania);
|
||||
yield return typeof(Acheron);
|
||||
yield return typeof(Debenter);
|
||||
yield return typeof(Uruk);
|
||||
yield return typeof(Ferea);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,6 +269,9 @@ internal class Weapons : InitializerBase
|
||||
this.CreateWeapon(5, 34, 0, 0, 1, 4, false, "Raven Stick", 147, 70, 78, 30, 98, 130, 0, 50, 14, 0, 0, 0, 0, 0, 0, 0, 2, 0);
|
||||
this.CreateWeapon(5, 36, 0, 0, 1, 4, false, "Divine Stick of Archangel", 104, 153, 165, 30, 182, 146, 0, 55, 13, 0, 0, 0, 0, 0, 0, 0, 1, 0);
|
||||
|
||||
// ADAMU-CUSTOM: new Imperial weapons at free slots (fresh-DB seed path; existing DBs get them via AddImperialWeapons050UpdateSeason6).
|
||||
this.CreateImperialWeapons050();
|
||||
|
||||
this.AddGuardianOptions();
|
||||
}
|
||||
|
||||
@@ -522,6 +525,25 @@ internal class Weapons : InitializerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates only the two new "Imperial" weapons at item number 50 (Imperial Sword 0/50 and
|
||||
/// Imperial Staff 5/50), reusing <see cref="CreateWeapon"/> and the already-persisted
|
||||
/// item level bonus tables (looked up by name). Intended for a configuration update
|
||||
/// plug-in running against an existing database, so it does NOT recreate the tables.
|
||||
/// </summary>
|
||||
public void CreateImperialWeapons050()
|
||||
{
|
||||
this._weaponDamageIncreaseTable = this.GameConfiguration.ItemLevelBonusTables.First(t => t.Name == "Damage Increase (Weapons)");
|
||||
this._staffRiseTableEven = this.GameConfiguration.ItemLevelBonusTables.First(t => t.Name == "Staff Rise (even)");
|
||||
this._staffRiseTableOdd = this.GameConfiguration.ItemLevelBonusTables.First(t => t.Name == "Staff Rise (odd)");
|
||||
|
||||
// Imperial Sword (copy of 0/28, Magic Gladiator) at number 50.
|
||||
this.CreateWeapon(0, 50, 0, 56, 1, 4, true, "Imperial Sword", 139, 98, 122, 45, 90, 109, 380, 91, 73, 17, 0, 0, 0, 0, 1, 0, 0, 0);
|
||||
|
||||
// Imperial Staff (copy of 5/31, Wizard) at number 50.
|
||||
this.CreateWeapon(5, 50, 0, 0, 1, 4, true, "Imperial Staff", 137, 57, 61, 30, 182, 124, 380, 48, 14, 0, 0, 2, 0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
private void AddGuardianOptions()
|
||||
{
|
||||
var weaponOption = this.GameConfiguration.ItemOptions.First(io => io.PossibleOptions.Any(po => po.OptionType == ItemOptionTypes.GuardianOption && po.Number == (int)ItemGroups.Weapon));
|
||||
|
||||
@@ -102,6 +102,64 @@ public class Wings : WingsInitializerBase
|
||||
this.CreateWing(40, 2, 3, "Cape of Emperor", 150, 45, 220, 400, 0, 0, 0, 0, 3, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.PhysDamage), (0b10, OptionType.Defense)), 39, 24, this._damageIncreaseByLevelTable, thirdWingOptions);
|
||||
this.CreateWing(43, 4, 3, "Wing of Dimension", 150, 45, 220, 400, 0, 0, 0, 0, 0, 3, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.WizDamage), (0b10, OptionType.CurseDamage)), 39, 39, this._damageIncreaseByLevelTable, thirdWingOptions);
|
||||
this.CreateWing(50, 2, 3, "Cape of Overrule", 150, 45, 220, 400, 0, 0, 0, 0, 0, 0, 3, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.PhysDamage), (0b10, OptionType.Defense)), 39, 39, this._damageIncreaseByLevelTable, thirdWingOptions);
|
||||
|
||||
this.CreatePoisonWings();
|
||||
this.CreateWarriorWings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ADAMU-CUSTOM: two imported wings (basic: base defense + class restriction, no wing options).
|
||||
/// Poison Wing (12/240, Dark Knight), Poison Cape (12/241, Dark Lord + Rage Fighter).
|
||||
/// </summary>
|
||||
public void CreatePoisonWings()
|
||||
{
|
||||
// High-tier imported wings. Reuse the EXISTING shared "3rd Wing Options" option set and
|
||||
// level-bonus tables (they already exist in the database) by fetching them from the
|
||||
// configuration instead of re-creating them - re-creating uses fixed Guids and would
|
||||
// collide when this runs as an update on an existing database.
|
||||
var wingOptions = this.GameConfiguration.ItemOptions.First(o => o.Name == "3rd Wing Options");
|
||||
var damageTable = this.GameConfiguration.ItemLevelBonusTables.First(t => t.Name == "Damage Increase (1st and 3rd Wings)");
|
||||
this._defenseBonusByLevelTable ??= this.GameConfiguration.ItemLevelBonusTables.First(t => t.Name == "Defense Bonus (1st and 2nd Wings)");
|
||||
|
||||
// Poison Wing (12/240, Dark Knight): defense 65 + defense-per-level, 3rd Wing Options,
|
||||
// damage increase, HP-recover / phys-damage / defense wing options, fast flight.
|
||||
this.CreateWing(240, 5, 3, "Poison Wing", 150, 65, 220, 100, 0, 1, 0, 0, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.PhysDamage), (0b10, OptionType.Defense)), 42, 42, damageTable, wingOptions, movementSpeed: MovementSpeedConstants.FastWingMovementSpeed);
|
||||
|
||||
// Poison Cape (12/241, Dark Lord + Rage Fighter): defense 60, same rich options.
|
||||
this.CreateWing(241, 5, 3, "Poison Cape", 150, 60, 220, 100, 0, 0, 0, 0, 1, 0, 1, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.PhysDamage), (0b10, OptionType.Defense)), 42, 42, damageTable, wingOptions, movementSpeed: MovementSpeedConstants.FastWingMovementSpeed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ADAMU-CUSTOM: six imported high-tier "Warrior" wings (base defense + 3rd Wing Options + damage
|
||||
/// increase, fast flight). One per class line:
|
||||
/// 242 Wizard (Soul Master), 243 Magic (Duel Master), 244 Elf (High Elf), 245 Summoner (Dimension
|
||||
/// Master), 246 Warrior Cape (Lord Emperor + Fist Master), 247 Knight (Blade Master).
|
||||
/// </summary>
|
||||
public void CreateWarriorWings()
|
||||
{
|
||||
// Reuse the EXISTING shared "3rd Wing Options" set and level-bonus tables (fixed Guids already in
|
||||
// the database) by fetching them, to avoid Guid collisions when this runs as an update.
|
||||
var wingOptions = this.GameConfiguration.ItemOptions.First(o => o.Name == "3rd Wing Options");
|
||||
var damageTable = this.GameConfiguration.ItemLevelBonusTables.First(t => t.Name == "Damage Increase (1st and 3rd Wings)");
|
||||
this._defenseBonusByLevelTable ??= this.GameConfiguration.ItemLevelBonusTables.First(t => t.Name == "Defense Bonus (1st and 2nd Wings)");
|
||||
|
||||
// Warrior Wings Wizard (12/242, Soul Master / Dark Wizard): magic damage option.
|
||||
this.CreateWing(242, 5, 3, "Warrior Wings Wizard", 150, 65, 220, 100, 1, 0, 0, 0, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.WizDamage), (0b10, OptionType.Defense)), 42, 42, damageTable, wingOptions, movementSpeed: MovementSpeedConstants.FastWingMovementSpeed);
|
||||
|
||||
// Warrior Wings Magic (12/243, Duel Master / Magic Gladiator).
|
||||
this.CreateWing(243, 5, 3, "Warrior Wings Magic", 150, 65, 220, 100, 0, 0, 0, 1, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.PhysDamage), (0b10, OptionType.Defense)), 42, 42, damageTable, wingOptions, movementSpeed: MovementSpeedConstants.FastWingMovementSpeed);
|
||||
|
||||
// Warrior Wings Elf (12/244, High Elf).
|
||||
this.CreateWing(244, 5, 3, "Warrior Wings Elf", 150, 65, 220, 100, 0, 0, 1, 0, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.PhysDamage), (0b10, OptionType.Defense)), 42, 42, damageTable, wingOptions, movementSpeed: MovementSpeedConstants.FastWingMovementSpeed);
|
||||
|
||||
// Warrior Wings Summoner (12/245, Dimension Master / Summoner): magic + curse damage.
|
||||
this.CreateWing(245, 5, 3, "Warrior Wings Summoner", 150, 65, 220, 100, 0, 0, 0, 0, 0, 1, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.WizDamage), (0b10, OptionType.CurseDamage)), 42, 42, damageTable, wingOptions, movementSpeed: MovementSpeedConstants.FastWingMovementSpeed);
|
||||
|
||||
// Warrior Cape (12/246, Lord Emperor / Dark Lord + Fist Master / Rage Fighter): defense 60.
|
||||
this.CreateWing(246, 5, 3, "Warrior Cape", 150, 60, 220, 100, 0, 0, 0, 0, 1, 0, 1, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.PhysDamage), (0b10, OptionType.Defense)), 42, 42, damageTable, wingOptions, movementSpeed: MovementSpeedConstants.FastWingMovementSpeed);
|
||||
|
||||
// Warrior Wings Knight (12/247, Blade Master / Dark Knight).
|
||||
this.CreateWing(247, 5, 3, "Warrior Wings Knight", 150, 65, 220, 100, 0, 1, 0, 0, 0, 0, 0, this.BuildOptions((0b00, OptionType.HealthRecover), (0b11, OptionType.PhysDamage), (0b10, OptionType.Defense)), 42, 42, damageTable, wingOptions, movementSpeed: MovementSpeedConstants.FastWingMovementSpeed);
|
||||
}
|
||||
|
||||
private void CreateFeather()
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// <copyright file="Acheron.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 Acheron map (imported map). Seeded empty; monster/NPC
|
||||
/// spawns and gates are added via the admin panel.
|
||||
/// </summary>
|
||||
internal class Acheron : BaseMapInitializer
|
||||
{
|
||||
/// <summary>
|
||||
/// The Number of the Map.
|
||||
/// </summary>
|
||||
internal const byte Number = 83;
|
||||
|
||||
/// <summary>
|
||||
/// The Name of the Map.
|
||||
/// </summary>
|
||||
internal const string Name = "Acheron";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Acheron"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="gameConfiguration">The game configuration.</param>
|
||||
public Acheron(IContext context, GameConfiguration gameConfiguration)
|
||||
: base(context, gameConfiguration)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override byte MapNumber => Number;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string MapName => Name;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// <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. OpenMU map numbers are 0-based; the MU client loads
|
||||
/// world folder <c>World{Number + 1}</c> (e.g. Lorencia 0 -> World1). Arkania's
|
||||
/// client assets are World83, so the server map number is 82.
|
||||
/// </summary>
|
||||
internal const byte Number = 82;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// <copyright file="Debenter.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 Debenter map (imported map). Seeded empty; monster/NPC
|
||||
/// spawns and gates are added via the admin panel.
|
||||
/// </summary>
|
||||
internal class Debenter : BaseMapInitializer
|
||||
{
|
||||
/// <summary>
|
||||
/// The Number of the Map.
|
||||
/// </summary>
|
||||
internal const byte Number = 84;
|
||||
|
||||
/// <summary>
|
||||
/// The Name of the Map.
|
||||
/// </summary>
|
||||
internal const string Name = "Debenter";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Debenter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="gameConfiguration">The game configuration.</param>
|
||||
public Debenter(IContext context, GameConfiguration gameConfiguration)
|
||||
: base(context, gameConfiguration)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override byte MapNumber => Number;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string MapName => Name;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// <copyright file="Ferea.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 Ferea map (imported map). Seeded empty; monster/NPC
|
||||
/// spawns and gates are added via the admin panel.
|
||||
/// </summary>
|
||||
internal class Ferea : BaseMapInitializer
|
||||
{
|
||||
/// <summary>
|
||||
/// The Number of the Map.
|
||||
/// </summary>
|
||||
internal const byte Number = 86;
|
||||
|
||||
/// <summary>
|
||||
/// The Name of the Map.
|
||||
/// </summary>
|
||||
internal const string Name = "Ferea";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Ferea"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="gameConfiguration">The game configuration.</param>
|
||||
public Ferea(IContext context, GameConfiguration gameConfiguration)
|
||||
: base(context, gameConfiguration)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override byte MapNumber => Number;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string MapName => Name;
|
||||
}
|
||||
40
src/Persistence/Initialization/VersionSeasonSix/Maps/Uruk.cs
Normal file
40
src/Persistence/Initialization/VersionSeasonSix/Maps/Uruk.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
// <copyright file="Uruk.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 Uruk map (imported map). Seeded empty; monster/NPC
|
||||
/// spawns and gates are added via the admin panel.
|
||||
/// </summary>
|
||||
internal class Uruk : BaseMapInitializer
|
||||
{
|
||||
/// <summary>
|
||||
/// The Number of the Map.
|
||||
/// </summary>
|
||||
internal const byte Number = 85;
|
||||
|
||||
/// <summary>
|
||||
/// The Name of the Map.
|
||||
/// </summary>
|
||||
internal const string Name = "Uruk";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Uruk"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="gameConfiguration">The game configuration.</param>
|
||||
public Uruk(IContext context, GameConfiguration gameConfiguration)
|
||||
: base(context, gameConfiguration)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override byte MapNumber => Number;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string MapName => Name;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
// <copyright file="ResetCharacterActionTest.cs" company="MUnique">
|
||||
// <copyright file="ResetCharacterActionTest.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Tests;
|
||||
|
||||
using Moq;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.DataModel.Configuration.Items;
|
||||
using MUnique.OpenMU.DataModel.Entities;
|
||||
@@ -11,6 +12,8 @@ using MUnique.OpenMU.GameLogic;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
using MUnique.OpenMU.GameLogic.NPC;
|
||||
using MUnique.OpenMU.GameLogic.Resets;
|
||||
using MUnique.OpenMU.GameLogic.Views.World;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ResetCharacterAction"/>.
|
||||
@@ -131,6 +134,60 @@ public class ResetCharacterActionTest
|
||||
Assert.That(player.SelectedCharacter!.LevelUpPoints, Is.EqualTo(1_800));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a reset with <see cref="ResetConfiguration.MoveHome"/> moves the player through
|
||||
/// the regular warp path, so the client is told about the new position.
|
||||
/// Writing the character's position fields directly moves the player without notifying anyone,
|
||||
/// which makes the character slide across the map on the client instead of respawning.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task MoveHomeWarpsPlayerSoTheClientIsNotifiedAsync()
|
||||
{
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
|
||||
player.Attributes![Stats.Level] = 400;
|
||||
player.Money = 1_000;
|
||||
|
||||
// The home map has to be one whose GameMap instance does not exist yet: the spawn gate is
|
||||
// resolved once, in the GameMap constructor, so a map created earlier would ignore it.
|
||||
var homeMap = CreateMapDefinitionWithSpawnGate(1, 125);
|
||||
player.GameContext.Configuration.Maps.Add(homeMap);
|
||||
Mock.Get(player.SelectedCharacter!.CharacterClass!).Setup(c => c.HomeMap).Returns(homeMap);
|
||||
|
||||
var configuration = this.CreateConfiguration(null);
|
||||
configuration.MoveHome = true;
|
||||
player.GameContext.FeaturePlugIns.AddPlugIn(new ResetFeaturePlugIn { Configuration = configuration }, true);
|
||||
|
||||
var mapChangeView = Mock.Get(player.ViewPlugIns.GetPlugIn<IMapChangePlugIn>()!);
|
||||
var action = new ResetCharacterAction(player);
|
||||
|
||||
await action.ResetCharacterAsync().ConfigureAwait(false);
|
||||
|
||||
Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(1));
|
||||
Assert.That(player.Position, Is.EqualTo(new Point(125, 125)));
|
||||
mapChangeView.Verify(v => v.MapChangeAsync(), Times.Once);
|
||||
}
|
||||
|
||||
private static GameMapDefinition CreateMapDefinitionWithSpawnGate(short number, byte spawnCoordinate)
|
||||
{
|
||||
var mapMock = new Mock<GameMapDefinition>();
|
||||
mapMock.SetupAllProperties();
|
||||
mapMock.Setup(m => m.DropItemGroups).Returns(new List<DropItemGroup>());
|
||||
mapMock.Setup(m => m.MonsterSpawns).Returns(new List<MonsterSpawnArea>());
|
||||
mapMock.Object.TerrainData = new byte[ushort.MaxValue + 3];
|
||||
mapMock.Object.Number = number;
|
||||
var spawnGate = new ExitGate
|
||||
{
|
||||
IsSpawnGate = true,
|
||||
X1 = spawnCoordinate,
|
||||
X2 = spawnCoordinate,
|
||||
Y1 = spawnCoordinate,
|
||||
Y2 = spawnCoordinate,
|
||||
Map = mapMock.Object,
|
||||
};
|
||||
mapMock.Setup(m => m.ExitGates).Returns(new List<ExitGate> { spawnGate });
|
||||
return mapMock.Object;
|
||||
}
|
||||
|
||||
private static async ValueTask AddRequiredItemsAsync(Player player, ItemDefinition requiredItem, int count)
|
||||
{
|
||||
for (byte i = 0; i < count; i++)
|
||||
@@ -165,7 +222,7 @@ public class ResetCharacterActionTest
|
||||
return new NonPlayerCharacter(spawnArea, definition, map);
|
||||
}
|
||||
|
||||
private ResetConfiguration CreateConfiguration(ItemDefinition requiredItem)
|
||||
private ResetConfiguration CreateConfiguration(ItemDefinition? requiredItem)
|
||||
{
|
||||
return new ResetConfiguration
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user