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,152 @@
// <copyright file="CastleSiegeConfiguration.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Annotations;
using MUnique.OpenMU.DataModel.Configuration.Items;
/// <summary>
/// Main configuration for the castle siege event.
/// </summary>
[Cloneable]
public partial class CastleSiegeConfiguration
{
/// <summary>
/// Gets or sets a value indicating whether the castle siege feature is enabled.
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Gets or sets the number of seconds a guild must hold the crown to capture the castle.
/// </summary>
public int CrownHoldTimeSeconds { get; set; } = 30;
/// <summary>
/// Gets or sets the minimum combined level of a guild master required to register for the siege.
/// </summary>
public int RegisterMinLevel { get; set; } = 200;
/// <summary>
/// Gets or sets the minimum number of guild members required to register for the siege.
/// </summary>
public int RegisterMinMembers { get; set; } = 20;
/// <summary>
/// Gets or sets the minimum number of seconds a participant must be present in the battle to be eligible for a reward.
/// </summary>
public int ParticipantRewardMinSeconds { get; set; }
/// <summary>
/// Gets or sets the maximum number of attacking alliance slots.
/// </summary>
public int MaxAttackingGuilds { get; set; } = 3;
/// <summary>
/// Gets or sets the guild score awarded to the guild that wins the siege.
/// </summary>
public int GuildScoreCastleSiege { get; set; }
/// <summary>
/// Gets or sets the guild score awarded to alliance member guilds of the winning side.
/// </summary>
public int GuildScoreCastleSiegeMembers { get; set; }
/// <summary>
/// Gets or sets the Zen cost for the castle owner to re-purchase a destroyed gate.
/// </summary>
public int GateBuyPrice { get; set; }
/// <summary>
/// Gets or sets the Zen cost for the castle owner to re-purchase a destroyed statue.
/// </summary>
public int StatueBuyPrice { get; set; }
/// <summary>
/// Gets or sets the map definition for the Valley of Loren (map 30), where the siege takes place.
/// </summary>
public virtual GameMapDefinition? CastleSiegeMapDefinition { get; set; }
/// <summary>
/// Gets or sets the map definition for the Land of Trials (map 31), the castle-owner's exclusive zone.
/// </summary>
public virtual GameMapDefinition? LandOfTrialsMapDefinition { get; set; }
/// <summary>
/// Gets or sets the item definition for the participation reward item.
/// </summary>
public virtual ItemDefinition? RewardItemDefinition { get; set; }
/// <summary>
/// Gets or sets the schedule entries that define when each siege state begins.
/// </summary>
[MemberOfAggregate]
public virtual ICollection<CastleSiegeStateScheduleEntry> StateSchedule { get; protected set; } = null!;
/// <summary>
/// Gets or sets the definitions for all castle siege NPCs (gates, statues, etc.).
/// </summary>
[MemberOfAggregate]
public virtual ICollection<CastleSiegeNpcDefinition> NpcDefinitions { get; protected set; } = null!;
/// <summary>
/// Gets or sets the upgrade levels for gate defense.
/// </summary>
[MemberOfAggregate]
public virtual ICollection<CastleSiegeUpgradeDefinition> GateDefenseUpgrades { get; protected set; } = null!;
/// <summary>
/// Gets or sets the upgrade levels for gate maximum HP.
/// </summary>
[MemberOfAggregate]
public virtual ICollection<CastleSiegeUpgradeDefinition> GateLifeUpgrades { get; protected set; } = null!;
/// <summary>
/// Gets or sets the upgrade levels for statue defense.
/// </summary>
[MemberOfAggregate]
public virtual ICollection<CastleSiegeUpgradeDefinition> StatueDefenseUpgrades { get; protected set; } = null!;
/// <summary>
/// Gets or sets the upgrade levels for statue maximum HP.
/// </summary>
[MemberOfAggregate]
public virtual ICollection<CastleSiegeUpgradeDefinition> StatueLifeUpgrades { get; protected set; } = null!;
/// <summary>
/// Gets or sets the upgrade levels for statue HP regeneration.
/// </summary>
[MemberOfAggregate]
public virtual ICollection<CastleSiegeUpgradeDefinition> StatueRegenUpgrades { get; protected set; } = null!;
/// <summary>
/// Gets or sets the zones on the siege map where attacking siege machines may be placed.
/// </summary>
[MemberOfAggregate]
public virtual ICollection<CastleSiegeZoneDefinition> AttackMachineZones { get; protected set; } = null!;
/// <summary>
/// Gets or sets the zones on the siege map where defensive siege machines may be placed.
/// </summary>
[MemberOfAggregate]
public virtual ICollection<CastleSiegeZoneDefinition> DefenseMachineZones { get; protected set; } = null!;
/// <summary>
/// Gets or sets the zone where defending players respawn during the siege.
/// </summary>
[MemberOfAggregate]
public virtual CastleSiegeZoneDefinition? DefenseRespawnArea { get; set; }
/// <summary>
/// Gets or sets the zone where attacking players respawn during the siege.
/// </summary>
[MemberOfAggregate]
public virtual CastleSiegeZoneDefinition? AttackRespawnArea { get; set; }
/// <inheritdoc />
public override string ToString()
{
return "Castle Siege Configuration";
}
}

View File

@@ -0,0 +1,36 @@
// <copyright file="CastleSiegeJoinSide.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.DataModel.Configuration;
/// <summary>
/// Defines the side (defending or attacking) a guild or NPC belongs to in the castle siege.
/// </summary>
public enum CastleSiegeJoinSide : byte
{
/// <summary>
/// No side assigned.
/// </summary>
None = 0,
/// <summary>
/// The defending guild side.
/// </summary>
Defense = 1,
/// <summary>
/// The first attacking alliance slot.
/// </summary>
Attack1 = 2,
/// <summary>
/// The second attacking alliance slot.
/// </summary>
Attack2 = 3,
/// <summary>
/// The third attacking alliance slot.
/// </summary>
Attack3 = 4,
}

View File

@@ -0,0 +1,55 @@
// <copyright file="CastleSiegeNpcDefinition.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Annotations;
/// <summary>
/// Defines a castle siege NPC instance, including its spawn location, side, and persistence settings.
/// </summary>
[Cloneable]
public partial class CastleSiegeNpcDefinition
{
/// <summary>
/// Gets or sets the monster definition template for this NPC.
/// </summary>
public virtual MonsterDefinition? MonsterDefinition { get; set; }
/// <summary>
/// Gets or sets the unique instance identifier within its NPC type.
/// </summary>
public byte InstanceId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this NPC's state is persisted to the database between sieges.
/// </summary>
public bool IsPersistedToDatabase { get; set; }
/// <summary>
/// Gets or sets the default join side this NPC belongs to.
/// </summary>
public CastleSiegeJoinSide DefaultSide { get; set; }
/// <summary>
/// Gets or sets the X coordinate of the NPC's spawn position.
/// </summary>
public byte SpawnX { get; set; }
/// <summary>
/// Gets or sets the Y coordinate of the NPC's spawn position.
/// </summary>
public byte SpawnY { get; set; }
/// <summary>
/// Gets or sets the facing direction of the NPC at spawn.
/// </summary>
public Direction Direction { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"{this.MonsterDefinition} #{this.InstanceId} at ({this.SpawnX},{this.SpawnY})";
}
}

View File

@@ -0,0 +1,61 @@
// <copyright file="CastleSiegeState.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.DataModel.Configuration;
/// <summary>
/// The state of the castle siege event cycle.
/// </summary>
public enum CastleSiegeState : byte
{
/// <summary>
/// Idle state before guild registration opens.
/// </summary>
Idle1 = 0,
/// <summary>
/// Guilds may register for the siege.
/// </summary>
RegisterGuild = 1,
/// <summary>
/// Idle state after guild registration.
/// </summary>
Idle2 = 2,
/// <summary>
/// Guilds may register emblems (Marks of Lord) to determine the attacking guilds.
/// </summary>
RegisterMark = 3,
/// <summary>
/// Idle state after mark registration.
/// </summary>
Idle3 = 4,
/// <summary>
/// Players are notified that the siege is about to start.
/// </summary>
Notify = 5,
/// <summary>
/// The siege map is prepared and entry is allowed.
/// </summary>
Ready = 6,
/// <summary>
/// The siege battle is in progress.
/// </summary>
Start = 7,
/// <summary>
/// The siege battle has ended and results are being processed.
/// </summary>
End = 8,
/// <summary>
/// The full siege cycle has completed.
/// </summary>
EndCycle = 9,
}

View File

@@ -0,0 +1,40 @@
// <copyright file="CastleSiegeStateScheduleEntry.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Annotations;
/// <summary>
/// Defines a scheduled transition to a specific <see cref="CastleSiegeState"/> at a given day and time.
/// </summary>
[Cloneable]
public partial class CastleSiegeStateScheduleEntry
{
/// <summary>
/// Gets or sets the siege state that becomes active at the scheduled time.
/// </summary>
public CastleSiegeState State { get; set; }
/// <summary>
/// Gets or sets the day of the week on which this state transition occurs.
/// </summary>
public DayOfWeek DayOfWeek { get; set; }
/// <summary>
/// Gets or sets the hour (023) at which this state transition occurs.
/// </summary>
public byte Hour { get; set; }
/// <summary>
/// Gets or sets the minute (059) at which this state transition occurs.
/// </summary>
public byte Minute { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"{this.State} on {this.DayOfWeek} at {this.Hour:D2}:{this.Minute:D2}";
}
}

View File

@@ -0,0 +1,40 @@
// <copyright file="CastleSiegeUpgradeDefinition.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Annotations;
/// <summary>
/// Defines one level of an upgrade that the castle owner can apply to a gate or statue NPC.
/// </summary>
[Cloneable]
public partial class CastleSiegeUpgradeDefinition
{
/// <summary>
/// Gets or sets the upgrade level (03), where 0 represents the base/unupgraded state.
/// </summary>
public byte Level { get; set; }
/// <summary>
/// Gets or sets the number of Jewels of Guardian required to perform this upgrade.
/// </summary>
public int RequiredJewelOfGuardianCount { get; set; }
/// <summary>
/// Gets or sets the amount of Zen required to perform this upgrade.
/// </summary>
public int RequiredZen { get; set; }
/// <summary>
/// Gets or sets the resulting stat value granted by this upgrade level (defense or max HP).
/// </summary>
public int Value { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"Level {this.Level}: Value={this.Value}, Jewels={this.RequiredJewelOfGuardianCount}, Zen={this.RequiredZen}";
}
}

View File

@@ -0,0 +1,31 @@
// <copyright file="CastleSiegeUpgradeType.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.DataModel.Configuration;
/// <summary>
/// The type of upgrade applied to a castle siege NPC (gate or statue).
/// </summary>
public enum CastleSiegeUpgradeType : byte
{
/// <summary>
/// No upgrade type assigned.
/// </summary>
Undefined = 0,
/// <summary>
/// Increases the defense stat of the NPC.
/// </summary>
Defense = 1,
/// <summary>
/// Increases the HP regeneration rate of the NPC.
/// </summary>
Regen = 2,
/// <summary>
/// Increases the maximum HP of the NPC.
/// </summary>
Life = 3,
}

View File

@@ -0,0 +1,40 @@
// <copyright file="CastleSiegeZoneDefinition.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Annotations;
/// <summary>
/// Defines a rectangular zone on the castle siege map, used for spawn areas and machine zones.
/// </summary>
[Cloneable]
public partial class CastleSiegeZoneDefinition
{
/// <summary>
/// Gets or sets the top-left X coordinate of the zone.
/// </summary>
public byte X1 { get; set; }
/// <summary>
/// Gets or sets the top-left Y coordinate of the zone.
/// </summary>
public byte Y1 { get; set; }
/// <summary>
/// Gets or sets the bottom-right X coordinate of the zone.
/// </summary>
public byte X2 { get; set; }
/// <summary>
/// Gets or sets the bottom-right Y coordinate of the zone.
/// </summary>
public byte Y2 { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"{this.X1} / {this.Y1} to {this.X2} / {this.Y2}";
}
}

View File

@@ -300,6 +300,12 @@ public partial class GameConfiguration
[MemberOfAggregate]
public virtual ICollection<MiniGameDefinition> MiniGameDefinitions { get; protected set; } = null!;
/// <summary>
/// Gets or sets the castle siege configuration.
/// </summary>
[MemberOfAggregate]
public virtual CastleSiegeConfiguration? CastleSiegeConfiguration { get; set; }
/// <inheritdoc />
public override string ToString()
{

View File

@@ -0,0 +1,67 @@
// <copyright file="CastleSiegeData.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.DataModel.Entities;
/// <summary>
/// Persistent state of the castle siege, stored as a single row across siege cycles.
/// </summary>
[AggregateRoot]
public class CastleSiegeData
{
/// <summary>
/// Gets or sets the unique identifier of this record.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the persistent identifier of the guild that currently owns the castle.
/// <see langword="null"/> when no guild owns the castle.
/// </summary>
public Guid? OwnerGuildId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether any guild currently occupies the castle.
/// </summary>
public bool IsOccupied { get; set; }
/// <summary>
/// Gets or sets the Chaos Machine tax rate applied by the castle owner (03).
/// </summary>
public byte TaxChaos { get; set; }
/// <summary>
/// Gets or sets the personal store tax rate applied by the castle owner (03).
/// </summary>
public byte TaxStore { get; set; }
/// <summary>
/// Gets or sets the entry fee (in Zen) for the castle owner's hunt zone (0300000).
/// </summary>
public int TaxHunt { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the hunt zone (Land of Trials) is currently open to the public.
/// </summary>
public bool IsHuntZoneEnabled { get; set; }
/// <summary>
/// Gets or sets the accumulated tribute money collected from the hunt zone and taxes.
/// </summary>
public long TributeMoney { get; set; }
/// <summary>
/// Gets or sets the persisted states of all castle NPCs.
/// </summary>
[MemberOfAggregate]
public virtual ICollection<CastleSiegeNpcState> NpcStates { get; protected set; } = null!;
/// <inheritdoc />
public override string ToString()
{
return this.IsOccupied
? $"Castle owned by guild {this.OwnerGuildId}"
: "Castle unoccupied";
}
}

View File

@@ -0,0 +1,44 @@
// <copyright file="CastleSiegeGuildRegistration.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.DataModel.Entities;
/// <summary>
/// Stores a guild's registration data for the current castle siege cycle,
/// including the number of emblems submitted to determine attacking guilds.
/// </summary>
[AggregateRoot]
public class CastleSiegeGuildRegistration
{
/// <summary>
/// Gets or sets the unique identifier of this registration record.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the persistent identifier of the registered guild.
/// </summary>
public Guid GuildId { get; set; }
/// <summary>
/// Gets or sets the guild name, denormalized for convenience to avoid extra lookups during siege processing.
/// </summary>
public string GuildName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the number of Emblems of Lord registered by this guild.
/// </summary>
public int Marks { get; set; }
/// <summary>
/// Gets or sets the insertion order of this registration, used for tie-breaking when guilds have equal marks.
/// </summary>
public int RegistrationOrder { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"{this.GuildName} (Marks={this.Marks}, Order={this.RegistrationOrder})";
}
}

View File

@@ -0,0 +1,52 @@
// <copyright file="CastleSiegeNpcState.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.DataModel.Entities;
/// <summary>
/// Persistent state of a single castle siege NPC between siege cycles.
/// </summary>
public class CastleSiegeNpcState
{
/// <summary>
/// Gets or sets the unique identifier of this NPC state.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the monster definition number that identifies the NPC template.
/// </summary>
public short MonsterNumber { get; set; }
/// <summary>
/// Gets or sets the instance identifier matching <see cref="MUnique.OpenMU.DataModel.Configuration.CastleSiegeNpcDefinition.InstanceId"/>.
/// </summary>
public byte InstanceId { get; set; }
/// <summary>
/// Gets or sets the current defense upgrade level (03).
/// </summary>
public byte DefenseLevel { get; set; }
/// <summary>
/// Gets or sets the current HP regeneration upgrade level (03).
/// </summary>
public byte RegenLevel { get; set; }
/// <summary>
/// Gets or sets the current maximum HP upgrade level (03).
/// </summary>
public byte LifeLevel { get; set; }
/// <summary>
/// Gets or sets the current HP of the NPC. A value of 0 means the NPC is destroyed.
/// </summary>
public int CurrentHp { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"NPC {this.MonsterNumber} #{this.InstanceId} (HP={this.CurrentHp})";
}
}