feat(castle-siege): adopt the upstream Castle Siege data model and persistence

Brings in the database layer of upstream OpenMU PRs #754 and #860 without
touching AdaMu's working Castle Siege gameplay. This is purely additive: the
existing 5-phase implementation still runs exactly as before.

What is included:

- DataModel: CastleSiegeState (the original Season 6 values 0-9, which are
  exactly what the game client's CASTLESIEGE_STATE enum expects),
  CastleSiegeJoinSide, and the zone/NPC/upgrade definition types.
- Entities: CastleSiegeData, CastleSiegeGuildRegistration, CastleSiegeNpcState.
  These identify a guild by its persistent Guid rather than by name.
- Generated persistence: 8 BasicModel + 8 EntityFramework model classes,
  CastleSiegeExtensions, and the regenerated ExtendedTypeContext,
  MapsterConfigurator and GameConfiguration partials.
- Migrations: 20260730194321_AddCastleSiege and
  20260801162427_ConfigureCastleSiegePersistence, plus the model snapshot.
- EntityDataContext gains the two DbSets and the five model registrations.
- EntityFrameworkContextBase only publishes configuration changes for entities
  in the configuration schema, so siege state writes are no longer broadcast as
  configuration changes.

AdaMu-specific adaptations:

- UpdateVersion.AddCastleSiegeData is 105, not upstream's 100. AdaMu already
  ships 95-104, and the applied-update bookkeeping is keyed on this value, so a
  collision would skip or re-run updates on live databases.
- CastleSiegeInitializer does not seed a weekly StateSchedule. Upstream drives
  the cycle from a fixed Saturday schedule; AdaMu drives it manually from
  CastleSiegeEventPlugIn and the AdminPanel, so the schedule is left empty and
  nothing reads it.

The seeded NPC definitions match AdaMu's existing hard-coded coordinates
exactly (6 gates, the two crown switches and the crown), and additionally
provide 4 guardian statues, 6 guardsmen and real gate/statue hit point tables
that the current implementation does not have yet.

Two pre-existing migrations were restyled by upstream (copyright header, using
placement, trailing comma). No functional change.

Verified: full server build succeeds with 0 errors.
This commit is contained in:
Acentech Dev
2026-08-04 03:28:10 +03:00
parent 0fdb455cec
commit 3aa9815b10
42 changed files with 15383 additions and 7 deletions

View File

@@ -0,0 +1,228 @@
// <copyright file="CastleSiegeInitializer.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.Events;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps;
/// <summary>
/// Initializes the Castle Siege configuration and persistent state.
/// </summary>
internal sealed class CastleSiegeInitializer : InitializerBase
{
private const short GateMonsterNumber = 277;
private const short StatueMonsterNumber = 283;
/// <summary>
/// Initializes a new instance of the <see cref="CastleSiegeInitializer"/> class.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="gameConfiguration">The game configuration.</param>
public CastleSiegeInitializer(IContext context, GameConfiguration gameConfiguration)
: base(context, gameConfiguration)
{
}
/// <inheritdoc />
public override void Initialize()
{
var configuration = this.InitializeConfiguration();
this.InitializeData(configuration);
}
/// <summary>
/// Initializes the Castle Siege configuration, if it does not exist yet.
/// </summary>
/// <returns>The Castle Siege configuration.</returns>
internal CastleSiegeConfiguration InitializeConfiguration()
{
if (this.GameConfiguration.CastleSiegeConfiguration is { } existingConfiguration)
{
return existingConfiguration;
}
var configuration = this.Context.CreateNew<CastleSiegeConfiguration>();
configuration.Enabled = true;
configuration.CrownHoldTimeSeconds = 30;
configuration.RegisterMinLevel = 200;
configuration.RegisterMinMembers = 20;
configuration.ParticipantRewardMinSeconds = 60;
configuration.MaxAttackingGuilds = 3;
configuration.GuildScoreCastleSiege = 0;
configuration.GuildScoreCastleSiegeMembers = 0;
configuration.GateBuyPrice = 9_500_000;
configuration.StatueBuyPrice = 4_500_000;
configuration.CastleSiegeMapDefinition = this.GameConfiguration.Maps.Single(map => map.Number == ValleyOfLoren.Number);
configuration.LandOfTrialsMapDefinition = this.GameConfiguration.Maps.Single(map => map.Number == LandOfTrials.Number);
// AdaMu deliberately leaves StateSchedule empty. Upstream drives the cycle from a fixed weekly
// schedule; AdaMu drives it from CastleSiegeEventPlugIn (manual /cs commands, AdminPanel durations
// and the optional auto-open times in the plugin configuration). Nothing reads StateSchedule here.
this.InitializeNpcDefinitions(configuration);
this.InitializeUpgradeDefinitions(configuration);
this.InitializeMachineZones(configuration);
configuration.DefenseRespawnArea = this.CreateZone(74, 144, 115, 154);
configuration.AttackRespawnArea = this.CreateZone(35, 11, 144, 48);
this.GameConfiguration.CastleSiegeConfiguration = configuration;
return configuration;
}
/// <summary>
/// Initializes the persistent Castle Siege state.
/// </summary>
/// <param name="configuration">The Castle Siege configuration.</param>
/// <returns>The persistent Castle Siege state.</returns>
internal CastleSiegeData InitializeData(CastleSiegeConfiguration configuration)
{
var data = this.Context.CreateNew<CastleSiegeData>();
data.OwnerGuildId = null;
data.IsOccupied = false;
data.TaxChaos = 0;
data.TaxStore = 0;
data.TaxHunt = 0;
data.IsHuntZoneEnabled = false;
data.TributeMoney = 0;
var gateHitPoints = configuration.GateLifeUpgrades.Single(upgrade => upgrade.Level == 0).Value;
var statueHitPoints = configuration.StatueLifeUpgrades.Single(upgrade => upgrade.Level == 0).Value;
foreach (var npcDefinition in configuration.NpcDefinitions.Where(definition => definition.IsPersistedToDatabase))
{
var npcState = this.Context.CreateNew<CastleSiegeNpcState>();
npcState.MonsterNumber = npcDefinition.MonsterDefinition!.Number;
npcState.InstanceId = npcDefinition.InstanceId;
npcState.DefenseLevel = 0;
npcState.RegenLevel = 0;
npcState.LifeLevel = 0;
npcState.CurrentHp = npcState.MonsterNumber switch
{
GateMonsterNumber => gateHitPoints,
StatueMonsterNumber => statueHitPoints,
_ => throw new InvalidOperationException($"The persisted Castle Siege NPC monster number {npcState.MonsterNumber} is unsupported."),
};
data.NpcStates.Add(npcState);
}
return data;
}
private void InitializeNpcDefinitions(CastleSiegeConfiguration configuration)
{
this.AddNpc(configuration, 216, 1, false, CastleSiegeJoinSide.Attack1, 176, 212, Direction.SouthWest);
this.AddNpc(configuration, 217, 1, false, CastleSiegeJoinSide.Attack1, 167, 194, Direction.NorthWest);
this.AddNpc(configuration, 218, 1, false, CastleSiegeJoinSide.Attack1, 184, 195, Direction.NorthWest);
this.AddNpc(configuration, 219, 1, false, CastleSiegeJoinSide.Defense, 93, 208, Direction.SouthWest);
this.AddNpc(configuration, 219, 2, false, CastleSiegeJoinSide.Defense, 81, 165, Direction.SouthWest);
this.AddNpc(configuration, 219, 3, false, CastleSiegeJoinSide.Defense, 107, 165, Direction.SouthWest);
this.AddNpc(configuration, 219, 4, false, CastleSiegeJoinSide.Defense, 67, 118, Direction.SouthWest);
this.AddNpc(configuration, 219, 5, false, CastleSiegeJoinSide.Defense, 93, 118, Direction.SouthWest);
this.AddNpc(configuration, 219, 6, false, CastleSiegeJoinSide.Defense, 119, 118, Direction.SouthWest);
this.AddNpc(configuration, 221, 1, false, CastleSiegeJoinSide.Attack1, 63, 19, Direction.NorthEast);
this.AddNpc(configuration, 221, 2, false, CastleSiegeJoinSide.Attack1, 119, 19, Direction.NorthEast);
this.AddNpc(configuration, 222, 1, false, CastleSiegeJoinSide.Defense, 80, 188, Direction.SouthWest);
this.AddNpc(configuration, 222, 2, false, CastleSiegeJoinSide.Defense, 105, 188, Direction.SouthWest);
this.AddNpc(configuration, GateMonsterNumber, 1, true, CastleSiegeJoinSide.Defense, 93, 204, Direction.SouthWest);
this.AddNpc(configuration, GateMonsterNumber, 2, true, CastleSiegeJoinSide.Defense, 81, 161, Direction.SouthWest);
this.AddNpc(configuration, GateMonsterNumber, 3, true, CastleSiegeJoinSide.Defense, 107, 161, Direction.SouthWest);
this.AddNpc(configuration, GateMonsterNumber, 4, true, CastleSiegeJoinSide.Defense, 67, 114, Direction.SouthWest);
this.AddNpc(configuration, GateMonsterNumber, 5, true, CastleSiegeJoinSide.Defense, 93, 114, Direction.SouthWest);
this.AddNpc(configuration, GateMonsterNumber, 6, true, CastleSiegeJoinSide.Defense, 119, 114, Direction.SouthWest);
this.AddNpc(configuration, StatueMonsterNumber, 1, true, CastleSiegeJoinSide.Defense, 94, 227, Direction.SouthWest);
this.AddNpc(configuration, StatueMonsterNumber, 2, true, CastleSiegeJoinSide.Defense, 94, 182, Direction.SouthWest);
this.AddNpc(configuration, StatueMonsterNumber, 3, true, CastleSiegeJoinSide.Defense, 82, 130, Direction.SouthWest);
this.AddNpc(configuration, StatueMonsterNumber, 4, true, CastleSiegeJoinSide.Defense, 107, 130, Direction.SouthWest);
}
private void InitializeUpgradeDefinitions(CastleSiegeConfiguration configuration)
{
this.AddUpgrade(configuration.GateDefenseUpgrades, 0, 0, 0, 100);
this.AddUpgrade(configuration.GateDefenseUpgrades, 1, 2, 3_000_000, 180);
this.AddUpgrade(configuration.GateDefenseUpgrades, 2, 3, 3_000_000, 300);
this.AddUpgrade(configuration.GateDefenseUpgrades, 3, 4, 3_000_000, 520);
this.AddUpgrade(configuration.StatueDefenseUpgrades, 0, 0, 0, 80);
this.AddUpgrade(configuration.StatueDefenseUpgrades, 1, 3, 3_000_000, 180);
this.AddUpgrade(configuration.StatueDefenseUpgrades, 2, 5, 3_000_000, 340);
this.AddUpgrade(configuration.StatueDefenseUpgrades, 3, 7, 3_000_000, 550);
this.AddUpgrade(configuration.GateLifeUpgrades, 0, 0, 0, 1_900_000);
this.AddUpgrade(configuration.GateLifeUpgrades, 1, 2, 1_000_000, 2_500_000);
this.AddUpgrade(configuration.GateLifeUpgrades, 2, 3, 1_000_000, 3_500_000);
this.AddUpgrade(configuration.GateLifeUpgrades, 3, 4, 1_000_000, 5_200_000);
this.AddUpgrade(configuration.StatueLifeUpgrades, 0, 0, 0, 1_500_000);
this.AddUpgrade(configuration.StatueLifeUpgrades, 1, 3, 1_000_000, 2_200_000);
this.AddUpgrade(configuration.StatueLifeUpgrades, 2, 5, 1_000_000, 3_400_000);
this.AddUpgrade(configuration.StatueLifeUpgrades, 3, 7, 1_000_000, 5_000_000);
this.AddUpgrade(configuration.StatueRegenUpgrades, 0, 0, 0, 0);
this.AddUpgrade(configuration.StatueRegenUpgrades, 1, 3, 5_000_000, 1);
this.AddUpgrade(configuration.StatueRegenUpgrades, 2, 5, 5_000_000, 2);
this.AddUpgrade(configuration.StatueRegenUpgrades, 3, 7, 5_000_000, 3);
}
private void InitializeMachineZones(CastleSiegeConfiguration configuration)
{
configuration.AttackMachineZones.Add(this.CreateZone(62, 103, 72, 112));
configuration.AttackMachineZones.Add(this.CreateZone(88, 104, 124, 111));
configuration.AttackMachineZones.Add(this.CreateZone(116, 105, 124, 112));
configuration.AttackMachineZones.Add(this.CreateZone(73, 86, 105, 103));
configuration.DefenseMachineZones.Add(this.CreateZone(61, 88, 93, 108));
configuration.DefenseMachineZones.Add(this.CreateZone(92, 89, 127, 111));
configuration.DefenseMachineZones.Add(this.CreateZone(84, 52, 102, 66));
}
private void AddNpc(
CastleSiegeConfiguration configuration,
short monsterNumber,
byte instanceId,
bool isPersisted,
CastleSiegeJoinSide defaultSide,
byte spawnX,
byte spawnY,
Direction direction)
{
var definition = this.Context.CreateNew<CastleSiegeNpcDefinition>();
definition.MonsterDefinition = this.GameConfiguration.Monsters.Single(monster => monster.Number == monsterNumber);
definition.InstanceId = instanceId;
definition.IsPersistedToDatabase = isPersisted;
definition.DefaultSide = defaultSide;
definition.SpawnX = spawnX;
definition.SpawnY = spawnY;
definition.Direction = direction;
configuration.NpcDefinitions.Add(definition);
}
private void AddUpgrade(
ICollection<CastleSiegeUpgradeDefinition> target,
byte level,
int jewelCount,
int zen,
int value)
{
var upgrade = this.Context.CreateNew<CastleSiegeUpgradeDefinition>();
upgrade.Level = level;
upgrade.RequiredJewelOfGuardianCount = jewelCount;
upgrade.RequiredZen = zen;
upgrade.Value = value;
target.Add(upgrade);
}
private CastleSiegeZoneDefinition CreateZone(byte x1, byte y1, byte x2, byte y2)
{
var zone = this.Context.CreateNew<CastleSiegeZoneDefinition>();
zone.X1 = x1;
zone.Y1 = y1;
zone.X2 = x2;
zone.Y2 = y2;
return zone;
}
}