From c45c7546b9e86a0d2c486c005b4457631e1662c2 Mon Sep 17 00:00:00 2001
From: Zylkien <283095668+Zylkien@users.noreply.github.com>
Date: Thu, 6 Aug 2026 17:57:04 +0200
Subject: [PATCH 1/3] Implement Castle Siege registration and mark handling
Add guild and alliance registration, unregistration, registration-state queries, and Sign of Lord submission with persistence. Add B2 request handlers, client-compatible response packets and remote views, generated packet documentation, and regression tests for validation, restart recovery, inventory-slot translation, and serialization.
# Conflicts:
# docs/Packets/ServerToClient.md
# src/Network/Packets/ServerToClient/ConnectionExtensions.cs
# src/Network/Packets/ServerToClient/ServerToClientPackets.cs
# src/Network/Packets/ServerToClient/ServerToClientPackets.xml
# src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs
---
.../Actions/CastleSiegeGuildReference.cs | 13 +
.../Actions/CastleSiegeGuildResolver.cs | 81 ++++
.../Actions/CastleSiegeRegisterGuildAction.cs | 86 ++++
.../Actions/CastleSiegeRegisterMarkAction.cs | 74 ++++
.../CastleSiegeRegistrationStateAction.cs | 57 +++
.../CastleSiegeUnregisterGuildAction.cs | 67 +++
.../CastleSiege/CastleSiegeContext.cs | 77 ++++
.../CastleSiegeRegistrationResult.cs | 56 +++
.../CastleSiegeRegistrationStateResult.cs | 26 ++
.../CastleSiegeUnregistrationResult.cs | 31 ++
...CastleSiegeMarkRegistrationResultPlugIn.cs | 19 +
.../ICastleSiegeRegistrationResultPlugIn.cs | 29 ++
.../ICastleSiegeRegistrationStatePlugIn.cs | 26 ++
.../CastleSiegeGroupHandlerPlugIn.cs | 40 ++
.../CastleSiege/CastleSiegeHandlerContext.cs | 29 ++
...astleSiegeMarkRegistrationHandlerPlugIn.cs | 61 +++
.../CastleSiegeRegistrationHandlerPlugIn.cs | 35 ++
...stleSiegeRegistrationStateHandlerPlugIn.cs | 35 ++
.../CastleSiegeUnregisterHandlerPlugIn.cs | 39 ++
.../Properties/PlugInResources.Designer.cs | 90 ++++
.../Properties/PlugInResources.resx | 30 ++
...CastleSiegeMarkRegistrationResultPlugIn.cs | 34 ++
.../CastleSiegeRegistrationResultPlugIn.cs | 41 ++
.../CastleSiegeRegistrationStatePlugIn.cs | 41 ++
.../CastleSiegeRegistrationRemoteViewTests.cs | 82 ++++
.../CastleSiegeRegistrationTests.cs | 408 ++++++++++++++++++
.../CastleSiegeRemoteViewTestHelper.cs | 47 ++
27 files changed, 1654 insertions(+)
create mode 100644 src/GameLogic/CastleSiege/Actions/CastleSiegeGuildReference.cs
create mode 100644 src/GameLogic/CastleSiege/Actions/CastleSiegeGuildResolver.cs
create mode 100644 src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterGuildAction.cs
create mode 100644 src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterMarkAction.cs
create mode 100644 src/GameLogic/CastleSiege/Actions/CastleSiegeRegistrationStateAction.cs
create mode 100644 src/GameLogic/CastleSiege/Actions/CastleSiegeUnregisterGuildAction.cs
create mode 100644 src/GameLogic/Views/CastleSiege/CastleSiegeRegistrationResult.cs
create mode 100644 src/GameLogic/Views/CastleSiege/CastleSiegeRegistrationStateResult.cs
create mode 100644 src/GameLogic/Views/CastleSiege/CastleSiegeUnregistrationResult.cs
create mode 100644 src/GameLogic/Views/CastleSiege/ICastleSiegeMarkRegistrationResultPlugIn.cs
create mode 100644 src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationResultPlugIn.cs
create mode 100644 src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationStatePlugIn.cs
create mode 100644 src/GameServer/MessageHandler/CastleSiege/CastleSiegeGroupHandlerPlugIn.cs
create mode 100644 src/GameServer/MessageHandler/CastleSiege/CastleSiegeHandlerContext.cs
create mode 100644 src/GameServer/MessageHandler/CastleSiege/CastleSiegeMarkRegistrationHandlerPlugIn.cs
create mode 100644 src/GameServer/MessageHandler/CastleSiege/CastleSiegeRegistrationHandlerPlugIn.cs
create mode 100644 src/GameServer/MessageHandler/CastleSiege/CastleSiegeRegistrationStateHandlerPlugIn.cs
create mode 100644 src/GameServer/MessageHandler/CastleSiege/CastleSiegeUnregisterHandlerPlugIn.cs
create mode 100644 src/GameServer/RemoteView/CastleSiege/CastleSiegeMarkRegistrationResultPlugIn.cs
create mode 100644 src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationResultPlugIn.cs
create mode 100644 src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationStatePlugIn.cs
create mode 100644 tests/MUnique.OpenMU.Tests/CastleSiegeRegistrationRemoteViewTests.cs
create mode 100644 tests/MUnique.OpenMU.Tests/CastleSiegeRegistrationTests.cs
create mode 100644 tests/MUnique.OpenMU.Tests/CastleSiegeRemoteViewTestHelper.cs
diff --git a/src/GameLogic/CastleSiege/Actions/CastleSiegeGuildReference.cs b/src/GameLogic/CastleSiege/Actions/CastleSiegeGuildReference.cs
new file mode 100644
index 0000000000..0421851a3d
--- /dev/null
+++ b/src/GameLogic/CastleSiege/Actions/CastleSiegeGuildReference.cs
@@ -0,0 +1,13 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.CastleSiege.Actions;
+
+///
+/// A Castle Siege guild identity spanning runtime and persistent identifiers.
+///
+/// The runtime guild identifier.
+/// The persistent guild identifier.
+/// The guild name.
+internal sealed record CastleSiegeGuildReference(uint RuntimeId, Guid PersistentId, string Name);
diff --git a/src/GameLogic/CastleSiege/Actions/CastleSiegeGuildResolver.cs b/src/GameLogic/CastleSiege/Actions/CastleSiegeGuildResolver.cs
new file mode 100644
index 0000000000..fc1e7ac337
--- /dev/null
+++ b/src/GameLogic/CastleSiege/Actions/CastleSiegeGuildResolver.cs
@@ -0,0 +1,81 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.CastleSiege.Actions;
+
+using MUnique.OpenMU.GameLogic.Views.CastleSiege;
+using MUnique.OpenMU.Interfaces;
+
+///
+/// Resolves runtime guild information to its persistent Castle Siege identity.
+///
+internal static class CastleSiegeGuildResolver
+{
+ ///
+ /// Resolves a guild which is allowed to mutate a Castle Siege registration.
+ ///
+ /// The requesting player.
+ /// The Castle Siege context.
+ /// The resolved guild and validation result.
+ public static async ValueTask<(CastleSiegeGuildReference? Guild, CastleSiegeRegistrationResult Result)> ResolveAuthorizedGuildAsync(
+ Player player,
+ CastleSiegeContext context)
+ {
+ if (player.GuildStatus is not { } guildStatus)
+ {
+ return (null, CastleSiegeRegistrationResult.NoGuild);
+ }
+
+ if (guildStatus.Position != GuildPosition.GuildMaster
+ || player.GameContext is not IGameServerContext gameServerContext)
+ {
+ return (null, CastleSiegeRegistrationResult.InvalidGuild);
+ }
+
+ if (await gameServerContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false) is not { Name: not null } guild)
+ {
+ return (null, CastleSiegeRegistrationResult.InvalidGuild);
+ }
+
+ if (guild.AllianceGuild is not null
+ && !await gameServerContext.GuildServer.IsAllianceMasterAsync(guildStatus.GuildId).ConfigureAwait(false))
+ {
+ return (null, CastleSiegeRegistrationResult.InvalidGuild);
+ }
+
+ if (await context.GetPersistentGuildIdAsync(guild.Name).ConfigureAwait(false) is not { } persistentGuildId)
+ {
+ return (null, CastleSiegeRegistrationResult.InvalidGuild);
+ }
+
+ return (new(guildStatus.GuildId, persistentGuildId, guild.Name), CastleSiegeRegistrationResult.Success);
+ }
+
+ ///
+ /// Resolves the registration identity visible to any member of a guild or alliance.
+ ///
+ /// The requesting player.
+ /// The Castle Siege context.
+ /// The resolved registration identity, or .
+ public static async ValueTask ResolveRegistrationGuildAsync(Player player, CastleSiegeContext context)
+ {
+ if (player.GuildStatus is not { } guildStatus
+ || player.GameContext is not IGameServerContext gameServerContext
+ || await gameServerContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false) is not { Name: not null } guild)
+ {
+ return null;
+ }
+
+ var registrationGuildName = guild.AllianceGuild?.Name ?? guild.Name;
+ if (await context.GetPersistentGuildIdAsync(registrationGuildName).ConfigureAwait(false) is not { } persistentGuildId)
+ {
+ return null;
+ }
+
+ var registrationGuildId = string.Equals(registrationGuildName, guild.Name, StringComparison.OrdinalIgnoreCase)
+ ? guildStatus.GuildId
+ : await gameServerContext.GuildServer.GetGuildIdByNameAsync(registrationGuildName).ConfigureAwait(false);
+ return new(registrationGuildId, persistentGuildId, registrationGuildName);
+ }
+}
diff --git a/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterGuildAction.cs b/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterGuildAction.cs
new file mode 100644
index 0000000000..02123c8d45
--- /dev/null
+++ b/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterGuildAction.cs
@@ -0,0 +1,86 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.CastleSiege.Actions;
+
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.GameLogic.Attributes;
+using MUnique.OpenMU.GameLogic.Views.CastleSiege;
+
+///
+/// Validates and processes Castle Siege guild registrations.
+///
+public class CastleSiegeRegisterGuildAction
+{
+ ///
+ /// Tries to register the player's guild or alliance for Castle Siege.
+ ///
+ /// The requesting player.
+ /// The Castle Siege context, if initialized.
+ public async ValueTask RegisterAsync(Player player, CastleSiegeContext? context)
+ {
+ var (result, guildName) = await RegisterCoreAsync(player, context).ConfigureAwait(false);
+ await player.InvokeViewPlugInAsync(
+ view => view.ShowRegistrationResultAsync(result, guildName)).ConfigureAwait(false);
+ }
+
+ private static async ValueTask<(CastleSiegeRegistrationResult Result, string GuildName)> RegisterCoreAsync(
+ Player player,
+ CastleSiegeContext? context)
+ {
+ if (context is not { Configuration.Enabled: true })
+ {
+ return (CastleSiegeRegistrationResult.Failed, string.Empty);
+ }
+
+ await context.ExecutionLock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ if (context.CurrentState != CastleSiegeState.RegisterGuild)
+ {
+ return (CastleSiegeRegistrationResult.NotRegistrationPeriod, string.Empty);
+ }
+
+ var (guild, resolutionResult) = await CastleSiegeGuildResolver.ResolveAuthorizedGuildAsync(player, context).ConfigureAwait(false);
+ if (guild is null)
+ {
+ return (resolutionResult, string.Empty);
+ }
+
+ var combinedLevel = player.Level + (int)(player.Attributes?[Stats.MasterLevel] ?? 0);
+ if (combinedLevel < context.Configuration.RegisterMinLevel)
+ {
+ return (CastleSiegeRegistrationResult.LevelInsufficient, guild.Name);
+ }
+
+ if (player.GameContext is not IGameServerContext gameServerContext)
+ {
+ return (CastleSiegeRegistrationResult.InvalidGuild, guild.Name);
+ }
+
+ var members = await gameServerContext.GuildServer.GetGuildListAsync(guild.RuntimeId).ConfigureAwait(false);
+ if (members.Count < context.Configuration.RegisterMinMembers)
+ {
+ return (CastleSiegeRegistrationResult.NotEnoughMembers, guild.Name);
+ }
+
+ if (context.RegisteredGuilds.ContainsKey(guild.PersistentId))
+ {
+ return (CastleSiegeRegistrationResult.AlreadyRegistered, guild.Name);
+ }
+
+ if (context.SiegeData.OwnerGuildId == guild.PersistentId)
+ {
+ return (CastleSiegeRegistrationResult.IsDefender, guild.Name);
+ }
+
+ await context.AddRegistrationAsync(guild.PersistentId, guild.Name).ConfigureAwait(false);
+ return (CastleSiegeRegistrationResult.Success, guild.Name);
+ }
+ finally
+ {
+ context.ExecutionLock.Release();
+ }
+ }
+}
diff --git a/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterMarkAction.cs b/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterMarkAction.cs
new file mode 100644
index 0000000000..4f998f01d2
--- /dev/null
+++ b/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterMarkAction.cs
@@ -0,0 +1,74 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.CastleSiege.Actions;
+
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.GameLogic.Views.CastleSiege;
+
+///
+/// Validates and processes Emblem of Lord submissions.
+///
+public class CastleSiegeRegisterMarkAction
+{
+ private const byte EmblemGroup = 14;
+ private const short EmblemNumber = 21;
+ private const byte EmblemLevel = 3;
+
+ ///
+ /// Tries to submit the Emblem of Lord from an inventory slot.
+ ///
+ /// The requesting player.
+ /// The Castle Siege context, if initialized.
+ /// The inventory slot.
+ public async ValueTask RegisterMarkAsync(Player player, CastleSiegeContext? context, byte inventorySlot)
+ {
+ var (success, guildName, marks) = await RegisterMarkCoreAsync(player, context, inventorySlot).ConfigureAwait(false);
+ await player.InvokeViewPlugInAsync(
+ view => view.ShowMarkRegistrationResultAsync(success, guildName, marks)).ConfigureAwait(false);
+ }
+
+ private static async ValueTask<(bool Success, string GuildName, int Marks)> RegisterMarkCoreAsync(
+ Player player,
+ CastleSiegeContext? context,
+ byte inventorySlot)
+ {
+ if (context is not { Configuration.Enabled: true })
+ {
+ return (false, string.Empty, 0);
+ }
+
+ await context.ExecutionLock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ if (context.CurrentState != CastleSiegeState.RegisterMark)
+ {
+ return (false, string.Empty, 0);
+ }
+
+ var (guild, _) = await CastleSiegeGuildResolver.ResolveAuthorizedGuildAsync(player, context).ConfigureAwait(false);
+ if (guild is null
+ || !context.RegisteredGuilds.TryGetValue(guild.PersistentId, out var registration))
+ {
+ return (false, guild?.Name ?? string.Empty, 0);
+ }
+
+ var emblem = player.Inventory?.GetItem(inventorySlot);
+ if (emblem is null
+ || emblem.Definition is not { Group: EmblemGroup, Number: EmblemNumber }
+ || emblem.Level != EmblemLevel)
+ {
+ return (false, guild.Name, registration.Marks);
+ }
+
+ await player.DestroyInventoryItemAsync(emblem).ConfigureAwait(false);
+ var marks = await context.IncrementMarksAsync(registration).ConfigureAwait(false);
+ return (true, guild.Name, marks);
+ }
+ finally
+ {
+ context.ExecutionLock.Release();
+ }
+ }
+}
diff --git a/src/GameLogic/CastleSiege/Actions/CastleSiegeRegistrationStateAction.cs b/src/GameLogic/CastleSiege/Actions/CastleSiegeRegistrationStateAction.cs
new file mode 100644
index 0000000000..9aeb141cfa
--- /dev/null
+++ b/src/GameLogic/CastleSiege/Actions/CastleSiegeRegistrationStateAction.cs
@@ -0,0 +1,57 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.CastleSiege.Actions;
+
+using MUnique.OpenMU.GameLogic.Views.CastleSiege;
+
+///
+/// Returns the Castle Siege registration state visible to a player.
+///
+public class CastleSiegeRegistrationStateAction
+{
+ ///
+ /// Sends the player's guild or alliance registration state.
+ ///
+ /// The requesting player.
+ /// The Castle Siege context, if initialized.
+ public async ValueTask ShowStateAsync(Player player, CastleSiegeContext? context)
+ {
+ var (result, guildName, marks, registrationRank) = await GetStateAsync(player, context).ConfigureAwait(false);
+ await player.InvokeViewPlugInAsync(
+ view => view.ShowRegistrationStateAsync(result, guildName, marks, false, registrationRank)).ConfigureAwait(false);
+ }
+
+ private static async ValueTask<(
+ CastleSiegeRegistrationStateResult Result,
+ string GuildName,
+ int Marks,
+ byte RegistrationRank)> GetStateAsync(
+ Player player,
+ CastleSiegeContext? context)
+ {
+ if (context is not { Configuration.Enabled: true })
+ {
+ return (CastleSiegeRegistrationStateResult.Unavailable, string.Empty, 0, 0);
+ }
+
+ await context.ExecutionLock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ var guild = await CastleSiegeGuildResolver.ResolveRegistrationGuildAsync(player, context).ConfigureAwait(false);
+ return guild is not null
+ && context.RegisteredGuilds.TryGetValue(guild.PersistentId, out var registration)
+ ? (
+ CastleSiegeRegistrationStateResult.Registered,
+ registration.GuildName,
+ registration.Marks,
+ checked((byte)Math.Min(registration.RegistrationOrder, byte.MaxValue)))
+ : (CastleSiegeRegistrationStateResult.NotRegistered, string.Empty, 0, (byte)0);
+ }
+ finally
+ {
+ context.ExecutionLock.Release();
+ }
+ }
+}
diff --git a/src/GameLogic/CastleSiege/Actions/CastleSiegeUnregisterGuildAction.cs b/src/GameLogic/CastleSiege/Actions/CastleSiegeUnregisterGuildAction.cs
new file mode 100644
index 0000000000..5caccfae6a
--- /dev/null
+++ b/src/GameLogic/CastleSiege/Actions/CastleSiegeUnregisterGuildAction.cs
@@ -0,0 +1,67 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.CastleSiege.Actions;
+
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.GameLogic.Views.CastleSiege;
+
+///
+/// Validates and processes Castle Siege guild unregistrations.
+///
+public class CastleSiegeUnregisterGuildAction
+{
+ ///
+ /// Tries to unregister the player's guild or alliance.
+ ///
+ /// The requesting player.
+ /// The Castle Siege context, if initialized.
+ /// Whether the guild requests to give up its registration.
+ public async ValueTask UnregisterAsync(
+ Player player,
+ CastleSiegeContext? context,
+ bool isGivingUp)
+ {
+ var (result, guildName) = await UnregisterCoreAsync(player, context).ConfigureAwait(false);
+ await player.InvokeViewPlugInAsync(
+ view => view.ShowUnregistrationResultAsync(result, isGivingUp, guildName)).ConfigureAwait(false);
+ }
+
+ private static async ValueTask<(CastleSiegeUnregistrationResult Result, string GuildName)> UnregisterCoreAsync(
+ Player player,
+ CastleSiegeContext? context)
+ {
+ if (context is not { Configuration.Enabled: true })
+ {
+ return (CastleSiegeUnregistrationResult.Failed, string.Empty);
+ }
+
+ await context.ExecutionLock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ if (context.CurrentState != CastleSiegeState.RegisterGuild)
+ {
+ return (CastleSiegeUnregistrationResult.WrongState, string.Empty);
+ }
+
+ var (guild, _) = await CastleSiegeGuildResolver.ResolveAuthorizedGuildAsync(player, context).ConfigureAwait(false);
+ if (guild is null)
+ {
+ return (CastleSiegeUnregistrationResult.Failed, string.Empty);
+ }
+
+ if (!context.RegisteredGuilds.TryGetValue(guild.PersistentId, out var registration))
+ {
+ return (CastleSiegeUnregistrationResult.NotRegistered, guild.Name);
+ }
+
+ await context.RemoveRegistrationAsync(registration).ConfigureAwait(false);
+ return (CastleSiegeUnregistrationResult.Success, guild.Name);
+ }
+ finally
+ {
+ context.ExecutionLock.Release();
+ }
+ }
+}
diff --git a/src/GameLogic/CastleSiege/CastleSiegeContext.cs b/src/GameLogic/CastleSiege/CastleSiegeContext.cs
index 4f1eeba933..a33249add0 100644
--- a/src/GameLogic/CastleSiege/CastleSiegeContext.cs
+++ b/src/GameLogic/CastleSiege/CastleSiegeContext.cs
@@ -254,6 +254,83 @@ public async ValueTask ClearRegistrationsAsync()
this.RegisteredGuilds.Clear();
}
+ ///
+ /// Resolves the persistent identifier of a guild by its unique name.
+ ///
+ /// The guild name.
+ /// The persistent identifier, or if the guild was not found.
+ internal async ValueTask GetPersistentGuildIdAsync(string guildName)
+ {
+ using var context = this._gameContext.PersistenceContextProvider.CreateNewTypedContext(
+ typeof(Guild),
+ false,
+ this._gameContext.Configuration);
+ return (await context.GetAsync().ConfigureAwait(false))
+ .FirstOrDefault(guild => string.Equals(guild.Name, guildName, StringComparison.OrdinalIgnoreCase))
+ ?.Id;
+ }
+
+ ///
+ /// Creates and persists a guild registration.
+ ///
+ /// The persistent guild identifier.
+ /// The guild name.
+ /// The created registration.
+ internal async ValueTask AddRegistrationAsync(Guid guildId, string guildName)
+ {
+ using var context = this._gameContext.PersistenceContextProvider.CreateNewTypedContext(
+ typeof(CastleSiegeGuildRegistration),
+ false,
+ this._gameContext.Configuration);
+ var registration = context.CreateNew();
+ registration.GuildId = guildId;
+ registration.GuildName = guildName;
+ registration.RegistrationOrder = this.RegisteredGuilds.Count == 0
+ ? 1
+ : this.RegisteredGuilds.Values.Max(entry => entry.RegistrationOrder) + 1;
+ await context.SaveChangesAsync().ConfigureAwait(false);
+ this.RegisteredGuilds[guildId] = registration;
+ return registration;
+ }
+
+ ///
+ /// Deletes a persisted guild registration.
+ ///
+ /// The registration.
+ internal async ValueTask RemoveRegistrationAsync(CastleSiegeGuildRegistration registration)
+ {
+ using var context = this._gameContext.PersistenceContextProvider.CreateNewTypedContext(
+ typeof(CastleSiegeGuildRegistration),
+ false,
+ this._gameContext.Configuration);
+ if (await context.GetByIdAsync(registration.Id).ConfigureAwait(false) is { } persistentRegistration)
+ {
+ await context.DeleteAsync(persistentRegistration).ConfigureAwait(false);
+ await context.SaveChangesAsync().ConfigureAwait(false);
+ }
+
+ this.RegisteredGuilds.TryRemove(registration.GuildId, out _);
+ }
+
+ ///
+ /// Increments and persists the submitted mark count.
+ ///
+ /// The registration.
+ /// The updated mark count.
+ internal async ValueTask IncrementMarksAsync(CastleSiegeGuildRegistration registration)
+ {
+ using var context = this._gameContext.PersistenceContextProvider.CreateNewTypedContext(
+ typeof(CastleSiegeGuildRegistration),
+ false,
+ this._gameContext.Configuration);
+ var persistentRegistration = await context.GetByIdAsync(registration.Id).ConfigureAwait(false)
+ ?? throw new InvalidOperationException("The Castle Siege guild registration no longer exists.");
+ persistentRegistration.Marks++;
+ await context.SaveChangesAsync().ConfigureAwait(false);
+ registration.Marks = persistentRegistration.Marks;
+ return registration.Marks;
+ }
+
///
/// Initializes the context at the state which contains .
///
diff --git a/src/GameLogic/Views/CastleSiege/CastleSiegeRegistrationResult.cs b/src/GameLogic/Views/CastleSiege/CastleSiegeRegistrationResult.cs
new file mode 100644
index 0000000000..39f3f5d1b1
--- /dev/null
+++ b/src/GameLogic/Views/CastleSiege/CastleSiegeRegistrationResult.cs
@@ -0,0 +1,56 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.Views.CastleSiege;
+
+///
+/// The result of a Castle Siege guild registration request.
+///
+public enum CastleSiegeRegistrationResult : byte
+{
+ ///
+ /// The request failed for an unspecified reason.
+ ///
+ Failed = 0,
+
+ ///
+ /// The guild was registered successfully.
+ ///
+ Success = 1,
+
+ ///
+ /// The guild is already registered.
+ ///
+ AlreadyRegistered = 2,
+
+ ///
+ /// The guild is the current castle defender.
+ ///
+ IsDefender = 3,
+
+ ///
+ /// The guild or the requesting guild member is invalid.
+ ///
+ InvalidGuild = 4,
+
+ ///
+ /// The player's combined level is insufficient.
+ ///
+ LevelInsufficient = 5,
+
+ ///
+ /// The player is not a guild member.
+ ///
+ NoGuild = 6,
+
+ ///
+ /// Guild registration is currently closed.
+ ///
+ NotRegistrationPeriod = 7,
+
+ ///
+ /// The guild has too few members.
+ ///
+ NotEnoughMembers = 8,
+}
diff --git a/src/GameLogic/Views/CastleSiege/CastleSiegeRegistrationStateResult.cs b/src/GameLogic/Views/CastleSiege/CastleSiegeRegistrationStateResult.cs
new file mode 100644
index 0000000000..a50e7e8809
--- /dev/null
+++ b/src/GameLogic/Views/CastleSiege/CastleSiegeRegistrationStateResult.cs
@@ -0,0 +1,26 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.Views.CastleSiege;
+
+///
+/// The result of a Castle Siege registration-state request.
+///
+public enum CastleSiegeRegistrationStateResult : byte
+{
+ ///
+ /// The guild is not registered.
+ ///
+ NotRegistered,
+
+ ///
+ /// The guild is registered.
+ ///
+ Registered,
+
+ ///
+ /// The registration state is unavailable.
+ ///
+ Unavailable,
+}
diff --git a/src/GameLogic/Views/CastleSiege/CastleSiegeUnregistrationResult.cs b/src/GameLogic/Views/CastleSiege/CastleSiegeUnregistrationResult.cs
new file mode 100644
index 0000000000..56e9a2e22b
--- /dev/null
+++ b/src/GameLogic/Views/CastleSiege/CastleSiegeUnregistrationResult.cs
@@ -0,0 +1,31 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.Views.CastleSiege;
+
+///
+/// The result of a Castle Siege guild-unregistration request.
+///
+public enum CastleSiegeUnregistrationResult : byte
+{
+ ///
+ /// The request failed.
+ ///
+ Failed,
+
+ ///
+ /// The guild was unregistered.
+ ///
+ Success,
+
+ ///
+ /// The guild was not registered.
+ ///
+ NotRegistered,
+
+ ///
+ /// Guilds cannot unregister during the current state.
+ ///
+ WrongState,
+}
diff --git a/src/GameLogic/Views/CastleSiege/ICastleSiegeMarkRegistrationResultPlugIn.cs b/src/GameLogic/Views/CastleSiege/ICastleSiegeMarkRegistrationResultPlugIn.cs
new file mode 100644
index 0000000000..9b1f463bda
--- /dev/null
+++ b/src/GameLogic/Views/CastleSiege/ICastleSiegeMarkRegistrationResultPlugIn.cs
@@ -0,0 +1,19 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.Views.CastleSiege;
+
+///
+/// A view which reports Emblem of Lord submission results.
+///
+public interface ICastleSiegeMarkRegistrationResultPlugIn : IViewPlugIn
+{
+ ///
+ /// Shows the mark registration result.
+ ///
+ /// Whether an Emblem of Lord was submitted.
+ /// The registered guild name.
+ /// The updated mark count.
+ ValueTask ShowMarkRegistrationResultAsync(bool success, string guildName, int marks);
+}
diff --git a/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationResultPlugIn.cs b/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationResultPlugIn.cs
new file mode 100644
index 0000000000..a9038f333d
--- /dev/null
+++ b/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationResultPlugIn.cs
@@ -0,0 +1,29 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.Views.CastleSiege;
+
+///
+/// A view which reports Castle Siege guild registration results.
+///
+public interface ICastleSiegeRegistrationResultPlugIn : IViewPlugIn
+{
+ ///
+ /// Shows a guild registration result.
+ ///
+ /// The result.
+ /// The guild name.
+ ValueTask ShowRegistrationResultAsync(CastleSiegeRegistrationResult result, string guildName);
+
+ ///
+ /// Shows a guild unregistration result.
+ ///
+ /// The result.
+ /// Whether the guild requested to give up its registration.
+ /// The guild name.
+ ValueTask ShowUnregistrationResultAsync(
+ CastleSiegeUnregistrationResult result,
+ bool isGivingUp,
+ string guildName);
+}
diff --git a/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationStatePlugIn.cs b/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationStatePlugIn.cs
new file mode 100644
index 0000000000..fb9cac66a6
--- /dev/null
+++ b/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationStatePlugIn.cs
@@ -0,0 +1,26 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.Views.CastleSiege;
+
+///
+/// A view which reports the registration state of a guild.
+///
+public interface ICastleSiegeRegistrationStatePlugIn : IViewPlugIn
+{
+ ///
+ /// Shows the registration state.
+ ///
+ /// The query result.
+ /// The registered guild name, or an empty string.
+ /// The number of submitted Emblems of Lord.
+ /// Whether the guild gave up its registration.
+ /// The registration rank, or zero when not registered.
+ ValueTask ShowRegistrationStateAsync(
+ CastleSiegeRegistrationStateResult result,
+ string guildName,
+ int marks,
+ bool isGivingUp,
+ byte registrationRank);
+}
diff --git a/src/GameServer/MessageHandler/CastleSiege/CastleSiegeGroupHandlerPlugIn.cs b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeGroupHandlerPlugIn.cs
new file mode 100644
index 0000000000..d808bf432e
--- /dev/null
+++ b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeGroupHandlerPlugIn.cs
@@ -0,0 +1,40 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.MessageHandler.CastleSiege;
+
+using System.Runtime.InteropServices;
+using Microsoft.Extensions.Logging;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Packet handler for Castle Siege packets with the 0xB2 identifier.
+///
+[PlugIn]
+[Display(Name = nameof(PlugInResources.CastleSiegeGroupHandlerPlugIn_Name), Description = nameof(PlugInResources.CastleSiegeGroupHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
+[Guid("2438FBA6-4962-4BC7-8784-69F89AB53D8F")]
+internal class CastleSiegeGroupHandlerPlugIn : GroupPacketHandlerPlugIn
+{
+ ///
+ /// The group key.
+ ///
+ internal const byte GroupKey = 0xB2;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The client version provider.
+ /// The plugin manager.
+ /// The logger factory.
+ public CastleSiegeGroupHandlerPlugIn(IClientVersionProvider clientVersionProvider, PlugInManager manager, ILoggerFactory loggerFactory)
+ : base(clientVersionProvider, manager, loggerFactory)
+ {
+ }
+
+ ///
+ public override bool IsEncryptionExpected => false;
+
+ ///
+ public override byte Key => GroupKey;
+}
diff --git a/src/GameServer/MessageHandler/CastleSiege/CastleSiegeHandlerContext.cs b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeHandlerContext.cs
new file mode 100644
index 0000000000..89dee2e9b8
--- /dev/null
+++ b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeHandlerContext.cs
@@ -0,0 +1,29 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.MessageHandler.CastleSiege;
+
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameLogic.CastleSiege;
+using MUnique.OpenMU.GameLogic.PlugIns;
+
+///
+/// Resolves the active Castle Siege context for packet handlers.
+///
+internal static class CastleSiegeHandlerContext
+{
+ ///
+ /// Gets the initialized context for a player's game context.
+ ///
+ /// The player.
+ /// The Castle Siege context, or .
+ public static CastleSiegeContext? Get(Player player)
+ {
+ return player.GameContext.PlugInManager
+ .GetActivePlugInsOf()
+ .OfType()
+ .FirstOrDefault()
+ ?.GetContext(player.GameContext);
+ }
+}
diff --git a/src/GameServer/MessageHandler/CastleSiege/CastleSiegeMarkRegistrationHandlerPlugIn.cs b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeMarkRegistrationHandlerPlugIn.cs
new file mode 100644
index 0000000000..5346ba0862
--- /dev/null
+++ b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeMarkRegistrationHandlerPlugIn.cs
@@ -0,0 +1,61 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.MessageHandler.CastleSiege;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.DataModel;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameLogic.CastleSiege.Actions;
+using MUnique.OpenMU.Network.Packets.ClientToServer;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Handles Emblem of Lord registration requests.
+///
+[PlugIn]
+[Display(Name = nameof(PlugInResources.CastleSiegeMarkRegistrationHandlerPlugIn_Name), Description = nameof(PlugInResources.CastleSiegeMarkRegistrationHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
+[Guid("676913C3-502D-409D-84FA-6BA283ACF349")]
+[BelongsToGroup(CastleSiegeGroupHandlerPlugIn.GroupKey)]
+internal class CastleSiegeMarkRegistrationHandlerPlugIn : ISubPacketHandlerPlugIn
+{
+ private readonly CastleSiegeRegisterMarkAction _action = new();
+
+ ///
+ public bool IsEncryptionExpected => false;
+
+ ///
+ public byte Key => CastleSiegeMarkRegistration.SubCode;
+
+ ///
+ public ValueTask HandlePacketAsync(Player player, Memory packet)
+ {
+ CastleSiegeMarkRegistration request = packet;
+ if (!TryGetInventorySlot(request.ItemIndex, out var inventorySlot))
+ {
+ return ValueTask.CompletedTask;
+ }
+
+ return this._action.RegisterMarkAsync(player, CastleSiegeHandlerContext.Get(player), inventorySlot);
+ }
+
+ ///
+ /// Translates the client-side backpack-grid index to OpenMU's inventory slot, which includes equipment slots.
+ ///
+ /// The zero-based backpack-grid index sent by the client.
+ /// The translated OpenMU inventory slot.
+ /// if the translated slot fits into the packet slot range; otherwise, .
+ internal static bool TryGetInventorySlot(byte clientItemIndex, out byte inventorySlot)
+ {
+ var translatedSlot = clientItemIndex + InventoryConstants.EquippableSlotsCount;
+ if (translatedSlot > byte.MaxValue)
+ {
+ inventorySlot = default;
+ return false;
+ }
+
+ inventorySlot = (byte)translatedSlot;
+ return true;
+ }
+}
diff --git a/src/GameServer/MessageHandler/CastleSiege/CastleSiegeRegistrationHandlerPlugIn.cs b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeRegistrationHandlerPlugIn.cs
new file mode 100644
index 0000000000..eca43f7c81
--- /dev/null
+++ b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeRegistrationHandlerPlugIn.cs
@@ -0,0 +1,35 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.MessageHandler.CastleSiege;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameLogic.CastleSiege.Actions;
+using MUnique.OpenMU.Network.Packets.ClientToServer;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Handles Castle Siege guild registration requests.
+///
+[PlugIn]
+[Display(Name = nameof(PlugInResources.CastleSiegeRegistrationHandlerPlugIn_Name), Description = nameof(PlugInResources.CastleSiegeRegistrationHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
+[Guid("7315C1FD-EE79-490E-B9ED-E0ECC6E87F37")]
+[BelongsToGroup(CastleSiegeGroupHandlerPlugIn.GroupKey)]
+internal class CastleSiegeRegistrationHandlerPlugIn : ISubPacketHandlerPlugIn
+{
+ private readonly CastleSiegeRegisterGuildAction _action = new();
+
+ ///
+ public bool IsEncryptionExpected => false;
+
+ ///
+ public byte Key => CastleSiegeRegistrationRequest.SubCode;
+
+ ///
+ public ValueTask HandlePacketAsync(Player player, Memory packet)
+ {
+ return this._action.RegisterAsync(player, CastleSiegeHandlerContext.Get(player));
+ }
+}
diff --git a/src/GameServer/MessageHandler/CastleSiege/CastleSiegeRegistrationStateHandlerPlugIn.cs b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeRegistrationStateHandlerPlugIn.cs
new file mode 100644
index 0000000000..424e2f7916
--- /dev/null
+++ b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeRegistrationStateHandlerPlugIn.cs
@@ -0,0 +1,35 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.MessageHandler.CastleSiege;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameLogic.CastleSiege.Actions;
+using MUnique.OpenMU.Network.Packets.ClientToServer;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Handles Castle Siege registration-state requests.
+///
+[PlugIn]
+[Display(Name = nameof(PlugInResources.CastleSiegeRegistrationStateHandlerPlugIn_Name), Description = nameof(PlugInResources.CastleSiegeRegistrationStateHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
+[Guid("94E8D09A-7DBC-41B0-8A85-CA31F032B0FF")]
+[BelongsToGroup(CastleSiegeGroupHandlerPlugIn.GroupKey)]
+internal class CastleSiegeRegistrationStateHandlerPlugIn : ISubPacketHandlerPlugIn
+{
+ private readonly CastleSiegeRegistrationStateAction _action = new();
+
+ ///
+ public bool IsEncryptionExpected => false;
+
+ ///
+ public byte Key => CastleSiegeRegistrationStateRequest.SubCode;
+
+ ///
+ public ValueTask HandlePacketAsync(Player player, Memory packet)
+ {
+ return this._action.ShowStateAsync(player, CastleSiegeHandlerContext.Get(player));
+ }
+}
diff --git a/src/GameServer/MessageHandler/CastleSiege/CastleSiegeUnregisterHandlerPlugIn.cs b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeUnregisterHandlerPlugIn.cs
new file mode 100644
index 0000000000..e7ee4f3c7c
--- /dev/null
+++ b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeUnregisterHandlerPlugIn.cs
@@ -0,0 +1,39 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.MessageHandler.CastleSiege;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameLogic.CastleSiege.Actions;
+using MUnique.OpenMU.Network.Packets.ClientToServer;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// Handles Castle Siege guild unregistration requests.
+///
+[PlugIn]
+[Display(Name = nameof(PlugInResources.CastleSiegeUnregisterHandlerPlugIn_Name), Description = nameof(PlugInResources.CastleSiegeUnregisterHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
+[Guid("F8DFF1B0-1DDA-4988-AAC4-65004A7FF087")]
+[BelongsToGroup(CastleSiegeGroupHandlerPlugIn.GroupKey)]
+internal class CastleSiegeUnregisterHandlerPlugIn : ISubPacketHandlerPlugIn
+{
+ private readonly CastleSiegeUnregisterGuildAction _action = new();
+
+ ///
+ public bool IsEncryptionExpected => false;
+
+ ///
+ public byte Key => CastleSiegeUnregisterRequest.SubCode;
+
+ ///
+ public ValueTask HandlePacketAsync(Player player, Memory packet)
+ {
+ var request = new CastleSiegeUnregisterRequest(packet);
+ return this._action.UnregisterAsync(
+ player,
+ CastleSiegeHandlerContext.Get(player),
+ request.IsGivingUp);
+ }
+}
diff --git a/src/GameServer/Properties/PlugInResources.Designer.cs b/src/GameServer/Properties/PlugInResources.Designer.cs
index 5f87604332..9edda71b7e 100644
--- a/src/GameServer/Properties/PlugInResources.Designer.cs
+++ b/src/GameServer/Properties/PlugInResources.Designer.cs
@@ -617,6 +617,96 @@ public static string CancelGuildCreationHandlerPlugIn_Name {
return ResourceManager.GetString("CancelGuildCreationHandlerPlugIn_Name", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to Routes Castle Siege packet subcodes..
+ ///
+ public static string CastleSiegeGroupHandlerPlugIn_Description {
+ get {
+ return ResourceManager.GetString("CastleSiegeGroupHandlerPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Siege Packet Group Handler.
+ ///
+ public static string CastleSiegeGroupHandlerPlugIn_Name {
+ get {
+ return ResourceManager.GetString("CastleSiegeGroupHandlerPlugIn_Name", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Handles Castle Siege Emblem of Lord registration requests..
+ ///
+ public static string CastleSiegeMarkRegistrationHandlerPlugIn_Description {
+ get {
+ return ResourceManager.GetString("CastleSiegeMarkRegistrationHandlerPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Siege Mark Registration Handler.
+ ///
+ public static string CastleSiegeMarkRegistrationHandlerPlugIn_Name {
+ get {
+ return ResourceManager.GetString("CastleSiegeMarkRegistrationHandlerPlugIn_Name", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Handles Castle Siege guild registration requests..
+ ///
+ public static string CastleSiegeRegistrationHandlerPlugIn_Description {
+ get {
+ return ResourceManager.GetString("CastleSiegeRegistrationHandlerPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Siege Registration Handler.
+ ///
+ public static string CastleSiegeRegistrationHandlerPlugIn_Name {
+ get {
+ return ResourceManager.GetString("CastleSiegeRegistrationHandlerPlugIn_Name", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Handles Castle Siege registration-state requests..
+ ///
+ public static string CastleSiegeRegistrationStateHandlerPlugIn_Description {
+ get {
+ return ResourceManager.GetString("CastleSiegeRegistrationStateHandlerPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Siege Registration State Handler.
+ ///
+ public static string CastleSiegeRegistrationStateHandlerPlugIn_Name {
+ get {
+ return ResourceManager.GetString("CastleSiegeRegistrationStateHandlerPlugIn_Name", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Handles Castle Siege guild unregistration requests..
+ ///
+ public static string CastleSiegeUnregisterHandlerPlugIn_Description {
+ get {
+ return ResourceManager.GetString("CastleSiegeUnregisterHandlerPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Siege Unregister Handler.
+ ///
+ public static string CastleSiegeUnregisterHandlerPlugIn_Name {
+ get {
+ return ResourceManager.GetString("CastleSiegeUnregisterHandlerPlugIn_Name", resourceCulture);
+ }
+ }
///
/// Looks up a localized string similar to Handler for online state change packets..
diff --git a/src/GameServer/Properties/PlugInResources.resx b/src/GameServer/Properties/PlugInResources.resx
index e420009985..f032c760ba 100644
--- a/src/GameServer/Properties/PlugInResources.resx
+++ b/src/GameServer/Properties/PlugInResources.resx
@@ -1947,6 +1947,36 @@
Packet handler for character packets (0xF3 identifier).
+
+ Castle Siege Packet Group Handler
+
+
+ Routes Castle Siege packet subcodes.
+
+
+ Castle Siege Registration Handler
+
+
+ Handles Castle Siege guild registration requests.
+
+
+ Castle Siege Unregister Handler
+
+
+ Handles Castle Siege guild unregistration requests.
+
+
+ Castle Siege Registration State Handler
+
+
+ Handles Castle Siege registration-state requests.
+
+
+ Castle Siege Mark Registration Handler
+
+
+ Handles Castle Siege Emblem of Lord registration requests.
+
Character - Key Configuration
diff --git a/src/GameServer/RemoteView/CastleSiege/CastleSiegeMarkRegistrationResultPlugIn.cs b/src/GameServer/RemoteView/CastleSiege/CastleSiegeMarkRegistrationResultPlugIn.cs
new file mode 100644
index 0000000000..e4e321f706
--- /dev/null
+++ b/src/GameServer/RemoteView/CastleSiege/CastleSiegeMarkRegistrationResultPlugIn.cs
@@ -0,0 +1,34 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.RemoteView.CastleSiege;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.GameLogic.Views.CastleSiege;
+using MUnique.OpenMU.Network.Packets.ServerToClient;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// The default implementation of the
+/// which forwards mark registration results to the game client.
+///
+[PlugIn]
+[Guid("12ED701D-B865-4D2B-B044-445E662BFE0E")]
+public class CastleSiegeMarkRegistrationResultPlugIn : ICastleSiegeMarkRegistrationResultPlugIn
+{
+ private readonly RemotePlayer _player;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The player.
+ public CastleSiegeMarkRegistrationResultPlugIn(RemotePlayer player) => this._player = player;
+
+ ///
+ public ValueTask ShowMarkRegistrationResultAsync(bool success, string guildName, int marks)
+ => this._player.Connection.SendCastleSiegeMarkRegistrationResponseAsync(
+ success ? (byte)1 : (byte)0,
+ guildName,
+ checked((uint)marks));
+}
diff --git a/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationResultPlugIn.cs b/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationResultPlugIn.cs
new file mode 100644
index 0000000000..ebbaffb0d5
--- /dev/null
+++ b/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationResultPlugIn.cs
@@ -0,0 +1,41 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.RemoteView.CastleSiege;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.GameLogic.Views.CastleSiege;
+using MUnique.OpenMU.Network.Packets.ServerToClient;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// The default implementation of the
+/// which forwards registration results to the game client.
+///
+[PlugIn]
+[Guid("1FF48BAE-9F33-4316-B4C0-D6082653C383")]
+public class CastleSiegeRegistrationResultPlugIn : ICastleSiegeRegistrationResultPlugIn
+{
+ private readonly RemotePlayer _player;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The player.
+ public CastleSiegeRegistrationResultPlugIn(RemotePlayer player) => this._player = player;
+
+ ///
+ public ValueTask ShowRegistrationResultAsync(CastleSiegeRegistrationResult result, string guildName)
+ => this._player.Connection.SendCastleSiegeRegistrationResponseAsync((byte)result, guildName);
+
+ ///
+ public ValueTask ShowUnregistrationResultAsync(
+ CastleSiegeUnregistrationResult result,
+ bool isGivingUp,
+ string guildName)
+ => this._player.Connection.SendCastleSiegeUnregisterResponseAsync(
+ (byte)result,
+ isGivingUp,
+ guildName);
+}
diff --git a/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationStatePlugIn.cs b/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationStatePlugIn.cs
new file mode 100644
index 0000000000..cc9fc3f6e2
--- /dev/null
+++ b/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationStatePlugIn.cs
@@ -0,0 +1,41 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.RemoteView.CastleSiege;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.GameLogic.Views.CastleSiege;
+using MUnique.OpenMU.Network.Packets.ServerToClient;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// The default implementation of the
+/// which forwards registration state to the game client.
+///
+[PlugIn]
+[Guid("1CE1AF15-92BB-4022-BC71-189C125F4531")]
+public class CastleSiegeRegistrationStatePlugIn : ICastleSiegeRegistrationStatePlugIn
+{
+ private readonly RemotePlayer _player;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The player.
+ public CastleSiegeRegistrationStatePlugIn(RemotePlayer player) => this._player = player;
+
+ ///
+ public ValueTask ShowRegistrationStateAsync(
+ CastleSiegeRegistrationStateResult result,
+ string guildName,
+ int marks,
+ bool isGivingUp,
+ byte registrationRank)
+ => this._player.Connection.SendCastleSiegeRegistrationStateResponseAsync(
+ (byte)result,
+ guildName,
+ checked((uint)marks),
+ isGivingUp,
+ registrationRank);
+}
diff --git a/tests/MUnique.OpenMU.Tests/CastleSiegeRegistrationRemoteViewTests.cs b/tests/MUnique.OpenMU.Tests/CastleSiegeRegistrationRemoteViewTests.cs
new file mode 100644
index 0000000000..ec1a185698
--- /dev/null
+++ b/tests/MUnique.OpenMU.Tests/CastleSiegeRegistrationRemoteViewTests.cs
@@ -0,0 +1,82 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Tests;
+
+using MUnique.OpenMU.GameLogic.Views.CastleSiege;
+using MUnique.OpenMU.GameServer.RemoteView.CastleSiege;
+using MUnique.OpenMU.Network.Packets.ServerToClient;
+
+///
+/// Tests Castle Siege registration remote-view packet serialization.
+///
+[TestFixture]
+public class CastleSiegeRegistrationRemoteViewTests
+{
+ ///
+ /// Verifies registration, unregistration, state and mark response packets.
+ ///
+ [Test]
+ public async ValueTask SerializeRegistrationResponsesAsync()
+ {
+ var (player, output) = CastleSiegeRemoteViewTestHelper.CreatePlayer();
+
+ await new CastleSiegeRegistrationResultPlugIn(player)
+ .ShowRegistrationResultAsync(CastleSiegeRegistrationResult.AlreadyRegistered, "GuildA")
+ .ConfigureAwait(false);
+ await new CastleSiegeRegistrationResultPlugIn(player)
+ .ShowUnregistrationResultAsync(CastleSiegeUnregistrationResult.Success, true, "GuildA")
+ .ConfigureAwait(false);
+ await new CastleSiegeRegistrationStatePlugIn(player)
+ .ShowRegistrationStateAsync(
+ CastleSiegeRegistrationStateResult.Registered,
+ "GuildA",
+ 21,
+ false,
+ 4)
+ .ConfigureAwait(false);
+ await new CastleSiegeMarkRegistrationResultPlugIn(player)
+ .ShowMarkRegistrationResultAsync(true, "GuildA", 22)
+ .ConfigureAwait(false);
+
+ var data = output.ToArray().AsMemory();
+ Assert.That(
+ data.Length,
+ Is.EqualTo(
+ CastleSiegeRegistrationResponse.Length
+ + CastleSiegeUnregisterResponse.Length
+ + CastleSiegeRegistrationStateResponse.Length
+ + CastleSiegeMarkRegistrationResponse.Length));
+
+ var registration = (CastleSiegeRegistrationResponse)data[..CastleSiegeRegistrationResponse.Length];
+ Assert.That(registration.Result, Is.EqualTo((byte)CastleSiegeRegistrationResult.AlreadyRegistered));
+ Assert.That(registration.GuildName, Is.EqualTo("GuildA"));
+
+ var offset = CastleSiegeRegistrationResponse.Length;
+ var unregistration = (CastleSiegeUnregisterResponse)data.Slice(
+ offset,
+ CastleSiegeUnregisterResponse.Length);
+ Assert.That(unregistration.Result, Is.EqualTo((byte)CastleSiegeUnregistrationResult.Success));
+ Assert.That(unregistration.IsGivingUp, Is.True);
+ Assert.That(unregistration.GuildName, Is.EqualTo("GuildA"));
+
+ offset += CastleSiegeUnregisterResponse.Length;
+ var state = (CastleSiegeRegistrationStateResponse)data.Slice(
+ offset,
+ CastleSiegeRegistrationStateResponse.Length);
+ Assert.That(state.Result, Is.EqualTo((byte)CastleSiegeRegistrationStateResult.Registered));
+ Assert.That(state.GuildName, Is.EqualTo("GuildA"));
+ Assert.That(state.GuildMarkCount, Is.EqualTo(21));
+ Assert.That(state.IsGivingUp, Is.False);
+ Assert.That(state.RegistrationRank, Is.EqualTo(4));
+
+ offset += CastleSiegeRegistrationStateResponse.Length;
+ var mark = (CastleSiegeMarkRegistrationResponse)data.Slice(
+ offset,
+ CastleSiegeMarkRegistrationResponse.Length);
+ Assert.That(mark.Result, Is.EqualTo(1));
+ Assert.That(mark.GuildName, Is.EqualTo("GuildA"));
+ Assert.That(mark.GuildMarkCount, Is.EqualTo(22));
+ }
+}
diff --git a/tests/MUnique.OpenMU.Tests/CastleSiegeRegistrationTests.cs b/tests/MUnique.OpenMU.Tests/CastleSiegeRegistrationTests.cs
new file mode 100644
index 0000000000..68e817d7b7
--- /dev/null
+++ b/tests/MUnique.OpenMU.Tests/CastleSiegeRegistrationTests.cs
@@ -0,0 +1,408 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Tests;
+
+using System.Collections.Immutable;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.DataModel.Configuration.Items;
+using MUnique.OpenMU.DataModel.Entities;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameLogic.Attributes;
+using MUnique.OpenMU.GameLogic.CastleSiege;
+using MUnique.OpenMU.GameLogic.CastleSiege.Actions;
+using MUnique.OpenMU.GameLogic.Views.CastleSiege;
+using MUnique.OpenMU.GameServer;
+using MUnique.OpenMU.GameServer.MessageHandler.CastleSiege;
+using MUnique.OpenMU.Interfaces;
+using MUnique.OpenMU.Persistence;
+using MUnique.OpenMU.Persistence.InMemory;
+using MUnique.OpenMU.PlugIns;
+using BasicModel = MUnique.OpenMU.Persistence.BasicModel;
+using RuntimeGuild = MUnique.OpenMU.Interfaces.Guild;
+
+///
+/// Tests Castle Siege guild and Emblem of Lord registration.
+///
+[TestFixture]
+public class CastleSiegeRegistrationTests
+{
+ private const uint RuntimeGuildId = 42;
+ private const string GuildName = "TestGuild";
+
+ ///
+ /// Verifies the protocol result values documented by the Castle Siege registration issue.
+ ///
+ [Test]
+ public void RegistrationResultValuesMatchProtocol()
+ {
+ Assert.Multiple(() =>
+ {
+ Assert.That((byte)CastleSiegeRegistrationResult.Failed, Is.Zero);
+ Assert.That((byte)CastleSiegeRegistrationResult.Success, Is.EqualTo(1));
+ Assert.That((byte)CastleSiegeRegistrationResult.AlreadyRegistered, Is.EqualTo(2));
+ Assert.That((byte)CastleSiegeRegistrationResult.IsDefender, Is.EqualTo(3));
+ Assert.That((byte)CastleSiegeRegistrationResult.InvalidGuild, Is.EqualTo(4));
+ Assert.That((byte)CastleSiegeRegistrationResult.LevelInsufficient, Is.EqualTo(5));
+ Assert.That((byte)CastleSiegeRegistrationResult.NoGuild, Is.EqualTo(6));
+ Assert.That((byte)CastleSiegeRegistrationResult.NotRegistrationPeriod, Is.EqualTo(7));
+ Assert.That((byte)CastleSiegeRegistrationResult.NotEnoughMembers, Is.EqualTo(8));
+ });
+ }
+
+ ///
+ /// Verifies registration validation, persistence, and duplicate detection.
+ ///
+ [Test]
+ public async ValueTask RegistrationValidatesAndPersistsAsync()
+ {
+ var fixture = await CreateFixtureAsync().ConfigureAwait(false);
+ var view = fixture.Player.ViewPlugIns.GetPlugIn()!;
+ var action = new CastleSiegeRegisterGuildAction();
+
+ await action.RegisterAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ Mock.Get(view).Verify(
+ plugIn => plugIn.ShowRegistrationResultAsync(CastleSiegeRegistrationResult.NoGuild, string.Empty),
+ Times.Once);
+
+ fixture.Player.GuildStatus = new GuildMemberStatus(RuntimeGuildId, GuildPosition.NormalMember);
+ await action.RegisterAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ Mock.Get(view).Verify(
+ plugIn => plugIn.ShowRegistrationResultAsync(CastleSiegeRegistrationResult.InvalidGuild, string.Empty),
+ Times.Once);
+
+ fixture.Player.GuildStatus = new GuildMemberStatus(RuntimeGuildId, GuildPosition.GuildMaster);
+ await action.RegisterAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ Mock.Get(view).Verify(
+ plugIn => plugIn.ShowRegistrationResultAsync(CastleSiegeRegistrationResult.LevelInsufficient, GuildName),
+ Times.Once);
+
+ fixture.Player.Attributes![Stats.Level] = 400;
+ fixture.GuildMembers.Clear();
+ await action.RegisterAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ Mock.Get(view).Verify(
+ plugIn => plugIn.ShowRegistrationResultAsync(CastleSiegeRegistrationResult.NotEnoughMembers, GuildName),
+ Times.Once);
+
+ fixture.GuildMembers.AddRange([new(), new()]);
+ await action.RegisterAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ await action.RegisterAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ Mock.Get(view).Verify(
+ plugIn => plugIn.ShowRegistrationResultAsync(CastleSiegeRegistrationResult.Success, GuildName),
+ Times.Once);
+ Mock.Get(view).Verify(
+ plugIn => plugIn.ShowRegistrationResultAsync(CastleSiegeRegistrationResult.AlreadyRegistered, GuildName),
+ Times.Once);
+
+ Assert.That(fixture.Context.RegisteredGuilds, Contains.Key(fixture.PersistentGuildId));
+ Assert.That(fixture.Context.RegisteredGuilds[fixture.PersistentGuildId].RegistrationOrder, Is.EqualTo(1));
+
+ using var persistenceContext = fixture.PersistenceContextProvider.CreateNewTypedContext(
+ typeof(CastleSiegeGuildRegistration),
+ false,
+ fixture.GameConfiguration);
+ var registration = (await persistenceContext.GetAsync().ConfigureAwait(false)).Single();
+ Assert.That(registration.GuildId, Is.EqualTo(fixture.PersistentGuildId));
+ Assert.That(registration.GuildName, Is.EqualTo(GuildName));
+
+ var restartedContext = new CastleSiegeContext(fixture.GameServerContext, fixture.CastleSiegeConfiguration);
+ await restartedContext.InitializeAsync(new DateTime(2026, 8, 3, 12, 0, 0, DateTimeKind.Utc)).ConfigureAwait(false);
+ Assert.That(restartedContext.RegisteredGuilds, Contains.Key(fixture.PersistentGuildId));
+ }
+
+ ///
+ /// Verifies the registration-period and defender validation results.
+ ///
+ [Test]
+ public async ValueTask RegistrationRejectsClosedPeriodAndDefenderAsync()
+ {
+ var fixture = await CreateFixtureAsync().ConfigureAwait(false);
+ var view = fixture.Player.ViewPlugIns.GetPlugIn()!;
+ var action = new CastleSiegeRegisterGuildAction();
+
+ fixture.CastleSiegeConfiguration.Enabled = false;
+ await action.RegisterAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ Mock.Get(view).Verify(
+ plugIn => plugIn.ShowRegistrationResultAsync(CastleSiegeRegistrationResult.Failed, string.Empty),
+ Times.Once);
+ fixture.CastleSiegeConfiguration.Enabled = true;
+
+ fixture.Player.GuildStatus = new GuildMemberStatus(RuntimeGuildId, GuildPosition.GuildMaster);
+ fixture.Player.Attributes![Stats.Level] = 400;
+
+ fixture.Context.SetPeriod(fixture.Context.Schedule.GetCurrentPeriod(new DateTime(2026, 8, 4, 12, 0, 0, DateTimeKind.Utc)));
+ await action.RegisterAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ Mock.Get(view).Verify(
+ plugIn => plugIn.ShowRegistrationResultAsync(CastleSiegeRegistrationResult.NotRegistrationPeriod, string.Empty),
+ Times.Once);
+
+ fixture.Context.SetPeriod(fixture.Context.Schedule.GetCurrentPeriod(new DateTime(2026, 8, 3, 12, 0, 0, DateTimeKind.Utc)));
+ fixture.Context.SiegeData.OwnerGuildId = fixture.PersistentGuildId;
+ await action.RegisterAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ Mock.Get(view).Verify(
+ plugIn => plugIn.ShowRegistrationResultAsync(CastleSiegeRegistrationResult.IsDefender, GuildName),
+ Times.Once);
+ }
+
+ ///
+ /// Verifies that unregistration removes both runtime and persistent registration state.
+ ///
+ [Test]
+ public async ValueTask UnregistrationRemovesPersistentRegistrationAsync()
+ {
+ var fixture = await CreateRegisteredFixtureAsync().ConfigureAwait(false);
+ var view = fixture.Player.ViewPlugIns.GetPlugIn()!;
+
+ await new CastleSiegeUnregisterGuildAction().UnregisterAsync(fixture.Player, fixture.Context, true).ConfigureAwait(false);
+
+ Mock.Get(view).Verify(
+ plugIn => plugIn.ShowUnregistrationResultAsync(
+ CastleSiegeUnregistrationResult.Success,
+ true,
+ GuildName),
+ Times.Once);
+ Assert.That(fixture.Context.RegisteredGuilds, Is.Empty);
+ using var persistenceContext = fixture.PersistenceContextProvider.CreateNewTypedContext(
+ typeof(CastleSiegeGuildRegistration),
+ false,
+ fixture.GameConfiguration);
+ Assert.That(await persistenceContext.GetAsync().ConfigureAwait(false), Is.Empty);
+ }
+
+ ///
+ /// Verifies Emblem validation, consumption, mark persistence, and registration-state queries.
+ ///
+ [Test]
+ public async ValueTask MarkRegistrationConsumesValidEmblemAndPersistsCountAsync()
+ {
+ const byte itemSlot = 20;
+ var fixture = await CreateRegisteredFixtureAsync().ConfigureAwait(false);
+ fixture.Context.SetPeriod(fixture.Context.Schedule.GetCurrentPeriod(new DateTime(2026, 8, 4, 12, 0, 0, DateTimeKind.Utc)));
+ var markView = fixture.Player.ViewPlugIns.GetPlugIn()!;
+ var item = fixture.Player.PersistenceContext.CreateNew- ();
+ item.Definition = new ItemDefinition
+ {
+ Group = 14,
+ Number = 21,
+ Width = 1,
+ Height = 1,
+ };
+ item.Level = 2;
+ await fixture.Player.Inventory!.AddItemAsync(itemSlot, item).ConfigureAwait(false);
+
+ var action = new CastleSiegeRegisterMarkAction();
+ await action.RegisterMarkAsync(fixture.Player, fixture.Context, itemSlot).ConfigureAwait(false);
+ Mock.Get(markView).Verify(
+ plugIn => plugIn.ShowMarkRegistrationResultAsync(false, GuildName, 0),
+ Times.Once);
+ Assert.That(fixture.Player.Inventory.GetItem(itemSlot), Is.SameAs(item));
+
+ item.Level = 3;
+ await action.RegisterMarkAsync(fixture.Player, fixture.Context, itemSlot).ConfigureAwait(false);
+ Mock.Get(markView).Verify(
+ plugIn => plugIn.ShowMarkRegistrationResultAsync(true, GuildName, 1),
+ Times.Once);
+ Assert.That(fixture.Player.Inventory.GetItem(itemSlot), Is.Null);
+ Assert.That(fixture.Context.RegisteredGuilds[fixture.PersistentGuildId].Marks, Is.EqualTo(1));
+
+ using var persistenceContext = fixture.PersistenceContextProvider.CreateNewTypedContext(
+ typeof(CastleSiegeGuildRegistration),
+ false,
+ fixture.GameConfiguration);
+ var registration = (await persistenceContext.GetAsync().ConfigureAwait(false)).Single();
+ Assert.That(registration.Marks, Is.EqualTo(1));
+
+ var stateView = fixture.Player.ViewPlugIns.GetPlugIn()!;
+ await new CastleSiegeRegistrationStateAction().ShowStateAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ Mock.Get(stateView).Verify(
+ plugIn => plugIn.ShowRegistrationStateAsync(
+ CastleSiegeRegistrationStateResult.Registered,
+ GuildName,
+ 1,
+ false,
+ 1),
+ Times.Once);
+ }
+
+ ///
+ /// Verifies that alliance members can query, but cannot mutate, the alliance master's registration.
+ ///
+ [Test]
+ public async ValueTask AllianceMemberUsesMasterRegistrationForQueriesOnlyAsync()
+ {
+ const uint allianceMemberGuildId = 43;
+ var fixture = await CreateFixtureAsync().ConfigureAwait(false);
+ var allianceMaster = new RuntimeGuild { Name = GuildName };
+ allianceMaster.AllianceGuild = allianceMaster;
+ fixture.GuildServer
+ .Setup(server => server.GetGuildAsync(RuntimeGuildId))
+ .Returns(new ValueTask(allianceMaster));
+ fixture.Player.GuildStatus = new GuildMemberStatus(RuntimeGuildId, GuildPosition.GuildMaster);
+ fixture.Player.Attributes![Stats.Level] = 400;
+
+ var registrationView = fixture.Player.ViewPlugIns.GetPlugIn()!;
+ await new CastleSiegeRegisterGuildAction().RegisterAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ Mock.Get(registrationView).Verify(
+ plugIn => plugIn.ShowRegistrationResultAsync(CastleSiegeRegistrationResult.Success, GuildName),
+ Times.Once);
+
+ var allianceMember = new RuntimeGuild { Name = "Member", AllianceGuild = allianceMaster };
+ fixture.GuildServer
+ .Setup(server => server.GetGuildAsync(allianceMemberGuildId))
+ .Returns(new ValueTask(allianceMember));
+ fixture.GuildServer
+ .Setup(server => server.IsAllianceMasterAsync(allianceMemberGuildId))
+ .Returns(new ValueTask(false));
+ fixture.Player.GuildStatus = new GuildMemberStatus(allianceMemberGuildId, GuildPosition.GuildMaster);
+
+ await new CastleSiegeRegisterGuildAction().RegisterAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ Mock.Get(registrationView).Verify(
+ plugIn => plugIn.ShowRegistrationResultAsync(CastleSiegeRegistrationResult.InvalidGuild, string.Empty),
+ Times.Once);
+
+ var stateView = fixture.Player.ViewPlugIns.GetPlugIn()!;
+ await new CastleSiegeRegistrationStateAction().ShowStateAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ Mock.Get(stateView).Verify(
+ plugIn => plugIn.ShowRegistrationStateAsync(
+ CastleSiegeRegistrationStateResult.Registered,
+ GuildName,
+ 0,
+ false,
+ 1),
+ Times.Once);
+ }
+
+ ///
+ /// Verifies the four client request subcodes handled by this issue.
+ ///
+ [Test]
+ public void RequestHandlersUseExpectedSubcodes()
+ {
+ Assert.Multiple(() =>
+ {
+ Assert.That(new CastleSiegeRegistrationHandlerPlugIn().Key, Is.EqualTo(0x01));
+ Assert.That(new CastleSiegeUnregisterHandlerPlugIn().Key, Is.EqualTo(0x02));
+ Assert.That(new CastleSiegeRegistrationStateHandlerPlugIn().Key, Is.EqualTo(0x03));
+ Assert.That(new CastleSiegeMarkRegistrationHandlerPlugIn().Key, Is.EqualTo(0x04));
+ });
+ }
+
+ ///
+ /// Verifies that the zero-based backpack-grid index sent by MuMain is translated past the equipment slots.
+ ///
+ [Test]
+ public void MarkRegistrationHandlerTranslatesClientBackpackIndex()
+ {
+ Assert.Multiple(() =>
+ {
+ Assert.That(CastleSiegeMarkRegistrationHandlerPlugIn.TryGetInventorySlot(8, out var inventorySlot), Is.True);
+ Assert.That(inventorySlot, Is.EqualTo(20));
+ Assert.That(CastleSiegeMarkRegistrationHandlerPlugIn.TryGetInventorySlot(byte.MaxValue, out _), Is.False);
+ });
+ }
+
+ private static async ValueTask CreateRegisteredFixtureAsync()
+ {
+ var fixture = await CreateFixtureAsync().ConfigureAwait(false);
+ fixture.Player.GuildStatus = new GuildMemberStatus(RuntimeGuildId, GuildPosition.GuildMaster);
+ fixture.Player.Attributes![Stats.Level] = 400;
+ await new CastleSiegeRegisterGuildAction().RegisterAsync(fixture.Player, fixture.Context).ConfigureAwait(false);
+ return fixture;
+ }
+
+ private static async ValueTask CreateFixtureAsync()
+ {
+ var persistenceContextProvider = new InMemoryPersistenceContextProvider();
+ BasicModel.GameConfiguration gameConfiguration;
+ BasicModel.CastleSiegeConfiguration castleSiegeConfiguration;
+ Guid persistentGuildId;
+ using (var persistenceContext = persistenceContextProvider.CreateNewContext())
+ {
+ gameConfiguration = persistenceContext.CreateNew();
+ gameConfiguration.Maps.Add(new BasicModel.GameMapDefinition());
+ castleSiegeConfiguration = persistenceContext.CreateNew();
+ castleSiegeConfiguration.Enabled = true;
+ castleSiegeConfiguration.RegisterMinLevel = 200;
+ castleSiegeConfiguration.RegisterMinMembers = 2;
+ castleSiegeConfiguration.StateSchedule.Add(new BasicModel.CastleSiegeStateScheduleEntry
+ {
+ State = CastleSiegeState.RegisterGuild,
+ DayOfWeek = DayOfWeek.Monday,
+ });
+ castleSiegeConfiguration.StateSchedule.Add(new BasicModel.CastleSiegeStateScheduleEntry
+ {
+ State = CastleSiegeState.RegisterMark,
+ DayOfWeek = DayOfWeek.Tuesday,
+ });
+ gameConfiguration.CastleSiegeConfiguration = castleSiegeConfiguration;
+ persistenceContext.CreateNew();
+ var persistentGuild = persistenceContext.CreateNew();
+ persistentGuild.Name = GuildName;
+ persistentGuildId = persistentGuild.Id;
+ await persistenceContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+
+ var guildMembers = new List { new(), new() };
+ var guildServer = new Mock();
+ guildServer
+ .Setup(server => server.GetGuildAsync(RuntimeGuildId))
+ .Returns(new ValueTask(new RuntimeGuild { Name = GuildName }));
+ guildServer
+ .Setup(server => server.GetGuildListAsync(RuntimeGuildId))
+ .Returns(() => new ValueTask>(guildMembers.ToImmutableList()));
+ guildServer
+ .Setup(server => server.IsAllianceMasterAsync(RuntimeGuildId))
+ .Returns(new ValueTask(true));
+ guildServer
+ .Setup(server => server.GetGuildIdByNameAsync(GuildName))
+ .Returns(new ValueTask(RuntimeGuildId));
+
+ var mapInitializer = new MapInitializer(gameConfiguration, new NullLogger(), NullDropGenerator.Instance, null);
+ var gameServerContext = new GameServerContext(
+ new BasicModel.GameServerDefinition
+ {
+ GameConfiguration = gameConfiguration,
+ ServerConfiguration = new BasicModel.GameServerConfiguration(),
+ },
+ guildServer.Object,
+ new Mock().Object,
+ new Mock().Object,
+ new Mock().Object,
+ persistenceContextProvider,
+ mapInitializer,
+ NullLoggerFactory.Instance,
+ new PlugInManager([], NullLoggerFactory.Instance, null, null),
+ NullDropGenerator.Instance,
+ new ConfigurationChangeMediator());
+ mapInitializer.PlugInManager = gameServerContext.PlugInManager;
+ mapInitializer.PathFinderPool = gameServerContext.PathFinderPool;
+
+ var player = await PlayerTestHelper.CreatePlayerAsync(gameServerContext).ConfigureAwait(false);
+ var castleSiegeContext = new CastleSiegeContext(gameServerContext, castleSiegeConfiguration);
+ await castleSiegeContext.InitializeAsync(new DateTime(2026, 8, 3, 12, 0, 0, DateTimeKind.Utc)).ConfigureAwait(false);
+ return new(
+ persistenceContextProvider,
+ gameConfiguration,
+ castleSiegeConfiguration,
+ gameServerContext,
+ castleSiegeContext,
+ player,
+ persistentGuildId,
+ guildMembers,
+ guildServer);
+ }
+
+ private sealed record TestFixture(
+ InMemoryPersistenceContextProvider PersistenceContextProvider,
+ GameConfiguration GameConfiguration,
+ CastleSiegeConfiguration CastleSiegeConfiguration,
+ GameServerContext GameServerContext,
+ CastleSiegeContext Context,
+ Player Player,
+ Guid PersistentGuildId,
+ List GuildMembers,
+ Mock GuildServer);
+}
diff --git a/tests/MUnique.OpenMU.Tests/CastleSiegeRemoteViewTestHelper.cs b/tests/MUnique.OpenMU.Tests/CastleSiegeRemoteViewTestHelper.cs
new file mode 100644
index 0000000000..fdde0c1e3d
--- /dev/null
+++ b/tests/MUnique.OpenMU.Tests/CastleSiegeRemoteViewTestHelper.cs
@@ -0,0 +1,47 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Tests;
+
+using System.IO;
+using System.IO.Pipelines;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameServer;
+using MUnique.OpenMU.GameServer.RemoteView;
+using MUnique.OpenMU.Network;
+using MUnique.OpenMU.Persistence;
+using MUnique.OpenMU.PlugIns;
+using Nito.AsyncEx;
+
+///
+/// Creates remote players with an in-memory packet output for Castle Siege view tests.
+///
+internal static class CastleSiegeRemoteViewTestHelper
+{
+ ///
+ /// Creates a remote player and its packet output stream.
+ ///
+ /// The remote player and output stream.
+ internal static (RemotePlayer Player, MemoryStream Output) CreatePlayer()
+ {
+ var output = new MemoryStream();
+ var writer = PipeWriter.Create(output, new StreamPipeWriterOptions(leaveOpen: true));
+ var connection = new Mock();
+ connection.SetupGet(c => c.Connected).Returns(true);
+ connection.SetupGet(c => c.Output).Returns(writer);
+ connection.SetupGet(c => c.OutputLock).Returns(new AsyncLock());
+
+ var manager = new PlugInManager(null, new NullLoggerFactory(), null, null);
+ var gameContext = new Mock();
+ gameContext.Setup(c => c.PersistenceContextProvider)
+ .Returns(new Mock().Object);
+ gameContext.Setup(c => c.Configuration).Returns(new GameConfiguration());
+ gameContext.Setup(c => c.PlugInManager).Returns(manager);
+ gameContext.Setup(c => c.LoggerFactory).Returns(new NullLoggerFactory());
+ return (new RemotePlayer(gameContext.Object, connection.Object, default), output);
+ }
+}
From 875352df6b9e42aa2319522fc4f4438d54d572cb Mon Sep 17 00:00:00 2001
From: Zylkien <283095668+Zylkien@users.noreply.github.com>
Date: Thu, 6 Aug 2026 19:57:51 +0200
Subject: [PATCH 2/3] Address Castle Siege registration review
Rebase the registration flow on the merged Castle Siege packet definitions and address the review findings.
Preserve Sign of Lord items until mark persistence succeeds, expose the complete client result codes, replace guild-table scans with direct guild identity lookup, and make Sign of Lord registration configurable for new and existing databases. Add packet safeguards, plug-in metadata, documentation, migrations, and regression coverage.
---
.../GuildServer.Host/GuildServerController.cs | 13 +-
src/Dapr/ServerClients/GuildServer.cs | 16 +-
.../Configuration/CastleSiegeConfiguration.cs | 10 +
.../Actions/CastleSiegeGuildResolver.cs | 25 +-
.../Actions/CastleSiegeRegisterGuildAction.cs | 3 +-
.../Actions/CastleSiegeRegisterMarkAction.cs | 43 +-
.../CastleSiegeRegistrationStateAction.cs | 24 +-
.../CastleSiegeUnregisterGuildAction.cs | 4 +-
.../CastleSiege/CastleSiegeContext.cs | 36 +-
.../CastleSiegeMarkRegistrationResult.cs | 31 +
...CastleSiegeMarkRegistrationResultPlugIn.cs | 7 +-
.../ICastleSiegeRegistrationResultPlugIn.cs | 2 +
.../ICastleSiegeRegistrationStatePlugIn.cs | 3 +-
...astleSiegeMarkRegistrationHandlerPlugIn.cs | 7 +-
.../Properties/PlugInResources.Designer.cs | 56 +-
.../Properties/PlugInResources.resx | 20 +-
...CastleSiegeMarkRegistrationResultPlugIn.cs | 5 +-
.../CastleSiegeRegistrationResultPlugIn.cs | 1 +
.../CastleSiegeRegistrationStatePlugIn.cs | 1 +
src/GuildServer/GuildServer.cs | 11 +-
src/Interfaces/IGuildServer.cs | 9 +-
.../CastleSiegeConfiguration.Generated.cs | 18 +
.../ModelBuilder/CastleSiegeExtensions.cs | 4 +
...nfigureCastleSiegeRegistration.Designer.cs | 5805 +++++++++++++++++
...161456_ConfigureCastleSiegeRegistration.cs | 74 +
.../EntityDataContextModelSnapshot.cs | 17 +
.../CastleSiegeConfiguration.Generated.cs | 26 +
...gureCastleSiegeRegistrationUpdatePlugIn.cs | 56 +
.../Initialization/Updates/UpdateVersion.cs | 5 +
.../Events/CastleSiegeInitializer.cs | 18 +
.../TestInitializationWithEfCore.cs | 21 +
.../CastleSiegeRegistrationRemoteViewTests.cs | 4 +-
.../CastleSiegeRegistrationTests.cs | 92 +-
33 files changed, 6369 insertions(+), 98 deletions(-)
create mode 100644 src/GameLogic/Views/CastleSiege/CastleSiegeMarkRegistrationResult.cs
create mode 100644 src/Persistence/EntityFramework/Migrations/20260806161456_ConfigureCastleSiegeRegistration.Designer.cs
create mode 100644 src/Persistence/EntityFramework/Migrations/20260806161456_ConfigureCastleSiegeRegistration.cs
create mode 100644 src/Persistence/Initialization/Updates/ConfigureCastleSiegeRegistrationUpdatePlugIn.cs
diff --git a/src/Dapr/GuildServer.Host/GuildServerController.cs b/src/Dapr/GuildServer.Host/GuildServerController.cs
index 77c392fce8..802f0aab7c 100644
--- a/src/Dapr/GuildServer.Host/GuildServerController.cs
+++ b/src/Dapr/GuildServer.Host/GuildServerController.cs
@@ -56,6 +56,17 @@ public ValueTask GuildExistsAsync([FromBody] string guildName)
return this._guildServer.GetGuildAsync(guildId);
}
+ ///
+ /// Gets the persistent identifier of a guild by its runtime identifier.
+ ///
+ /// The runtime guild identifier.
+ /// The persistent guild identifier, or if the guild was not found.
+ [HttpPost(nameof(IGuildServer.GetPersistentGuildIdAsync))]
+ public ValueTask GetPersistentGuildIdAsync([FromBody] uint guildId)
+ {
+ return this._guildServer.GetPersistentGuildIdAsync(guildId);
+ }
+
///
/// Gets the guild id by the guild name.
///
@@ -160,4 +171,4 @@ public ValueTask IncreaseGuildScoreAsync([FromBody] uint guildId)
{
return this._guildServer.IncreaseGuildScoreAsync(guildId);
}
-}
\ No newline at end of file
+}
diff --git a/src/Dapr/ServerClients/GuildServer.cs b/src/Dapr/ServerClients/GuildServer.cs
index 165b594baa..e5eab98f6a 100644
--- a/src/Dapr/ServerClients/GuildServer.cs
+++ b/src/Dapr/ServerClients/GuildServer.cs
@@ -58,6 +58,20 @@ public async ValueTask GuildExistsAsync(string guildName)
}
}
+ ///
+ public async ValueTask GetPersistentGuildIdAsync(uint guildId)
+ {
+ try
+ {
+ return await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.GetPersistentGuildIdAsync), guildId).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ this._logger.LogError(ex, "Unexpected error when retrieving a persistent guild identifier.");
+ return null;
+ }
+ }
+
///
public async ValueTask GetGuildIdByNameAsync(string guildName)
{
@@ -263,4 +277,4 @@ public async ValueTask GetGuildRelationshipAsync(uint guild1,
return GuildRelationship.None;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/DataModel/Configuration/CastleSiegeConfiguration.cs b/src/DataModel/Configuration/CastleSiegeConfiguration.cs
index 595d673559..651d46ad56 100644
--- a/src/DataModel/Configuration/CastleSiegeConfiguration.cs
+++ b/src/DataModel/Configuration/CastleSiegeConfiguration.cs
@@ -33,6 +33,16 @@ public partial class CastleSiegeConfiguration
///
public int RegisterMinMembers { get; set; } = 20;
+ ///
+ /// Gets or sets the item definition used for Sign of Lord registration.
+ ///
+ public virtual ItemDefinition? SignOfLordItemDefinition { get; set; }
+
+ ///
+ /// Gets or sets the required level of a Sign of Lord item.
+ ///
+ public byte SignOfLordItemLevel { get; set; } = 3;
+
///
/// Gets or sets the minimum number of seconds a participant must be present in the battle to be eligible for a reward.
///
diff --git a/src/GameLogic/CastleSiege/Actions/CastleSiegeGuildResolver.cs b/src/GameLogic/CastleSiege/Actions/CastleSiegeGuildResolver.cs
index fc1e7ac337..f1eb479e55 100644
--- a/src/GameLogic/CastleSiege/Actions/CastleSiegeGuildResolver.cs
+++ b/src/GameLogic/CastleSiege/Actions/CastleSiegeGuildResolver.cs
@@ -16,11 +16,9 @@ internal static class CastleSiegeGuildResolver
/// Resolves a guild which is allowed to mutate a Castle Siege registration.
///
/// The requesting player.
- /// The Castle Siege context.
/// The resolved guild and validation result.
public static async ValueTask<(CastleSiegeGuildReference? Guild, CastleSiegeRegistrationResult Result)> ResolveAuthorizedGuildAsync(
- Player player,
- CastleSiegeContext context)
+ Player player)
{
if (player.GuildStatus is not { } guildStatus)
{
@@ -44,7 +42,7 @@ internal static class CastleSiegeGuildResolver
return (null, CastleSiegeRegistrationResult.InvalidGuild);
}
- if (await context.GetPersistentGuildIdAsync(guild.Name).ConfigureAwait(false) is not { } persistentGuildId)
+ if (await gameServerContext.GuildServer.GetPersistentGuildIdAsync(guildStatus.GuildId).ConfigureAwait(false) is not { } persistentGuildId)
{
return (null, CastleSiegeRegistrationResult.InvalidGuild);
}
@@ -56,9 +54,8 @@ internal static class CastleSiegeGuildResolver
/// Resolves the registration identity visible to any member of a guild or alliance.
///
/// The requesting player.
- /// The Castle Siege context.
- /// The resolved registration identity, or .
- public static async ValueTask ResolveRegistrationGuildAsync(Player player, CastleSiegeContext context)
+ /// The persistent registration guild identifier, or .
+ public static async ValueTask ResolveRegistrationGuildIdAsync(Player player)
{
if (player.GuildStatus is not { } guildStatus
|| player.GameContext is not IGameServerContext gameServerContext
@@ -67,15 +64,11 @@ internal static class CastleSiegeGuildResolver
return null;
}
- var registrationGuildName = guild.AllianceGuild?.Name ?? guild.Name;
- if (await context.GetPersistentGuildIdAsync(registrationGuildName).ConfigureAwait(false) is not { } persistentGuildId)
- {
- return null;
- }
-
- var registrationGuildId = string.Equals(registrationGuildName, guild.Name, StringComparison.OrdinalIgnoreCase)
+ var registrationGuildId = guild.AllianceGuild is null
? guildStatus.GuildId
- : await gameServerContext.GuildServer.GetGuildIdByNameAsync(registrationGuildName).ConfigureAwait(false);
- return new(registrationGuildId, persistentGuildId, registrationGuildName);
+ : await gameServerContext.GuildServer.GetGuildIdByNameAsync(guild.AllianceGuild.Name!).ConfigureAwait(false);
+ return registrationGuildId == 0
+ ? null
+ : await gameServerContext.GuildServer.GetPersistentGuildIdAsync(registrationGuildId).ConfigureAwait(false);
}
}
diff --git a/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterGuildAction.cs b/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterGuildAction.cs
index 02123c8d45..ef4e07bee8 100644
--- a/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterGuildAction.cs
+++ b/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterGuildAction.cs
@@ -18,6 +18,7 @@ public class CastleSiegeRegisterGuildAction
///
/// The requesting player.
/// The Castle Siege context, if initialized.
+ /// A task which represents the asynchronous operation.
public async ValueTask RegisterAsync(Player player, CastleSiegeContext? context)
{
var (result, guildName) = await RegisterCoreAsync(player, context).ConfigureAwait(false);
@@ -42,7 +43,7 @@ await player.InvokeViewPlugInAsync(
return (CastleSiegeRegistrationResult.NotRegistrationPeriod, string.Empty);
}
- var (guild, resolutionResult) = await CastleSiegeGuildResolver.ResolveAuthorizedGuildAsync(player, context).ConfigureAwait(false);
+ var (guild, resolutionResult) = await CastleSiegeGuildResolver.ResolveAuthorizedGuildAsync(player).ConfigureAwait(false);
if (guild is null)
{
return (resolutionResult, string.Empty);
diff --git a/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterMarkAction.cs b/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterMarkAction.cs
index 4f998f01d2..d80e421940 100644
--- a/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterMarkAction.cs
+++ b/src/GameLogic/CastleSiege/Actions/CastleSiegeRegisterMarkAction.cs
@@ -8,35 +8,32 @@ namespace MUnique.OpenMU.GameLogic.CastleSiege.Actions;
using MUnique.OpenMU.GameLogic.Views.CastleSiege;
///
-/// Validates and processes Emblem of Lord submissions.
+/// Validates and processes Sign of Lord submissions.
///
public class CastleSiegeRegisterMarkAction
{
- private const byte EmblemGroup = 14;
- private const short EmblemNumber = 21;
- private const byte EmblemLevel = 3;
-
///
- /// Tries to submit the Emblem of Lord from an inventory slot.
+ /// Tries to submit the Sign of Lord from an inventory slot.
///
/// The requesting player.
/// The Castle Siege context, if initialized.
/// The inventory slot.
+ /// A task which represents the asynchronous operation.
public async ValueTask RegisterMarkAsync(Player player, CastleSiegeContext? context, byte inventorySlot)
{
- var (success, guildName, marks) = await RegisterMarkCoreAsync(player, context, inventorySlot).ConfigureAwait(false);
+ var (result, guildName, marks) = await RegisterMarkCoreAsync(player, context, inventorySlot).ConfigureAwait(false);
await player.InvokeViewPlugInAsync(
- view => view.ShowMarkRegistrationResultAsync(success, guildName, marks)).ConfigureAwait(false);
+ view => view.ShowMarkRegistrationResultAsync(result, guildName, marks)).ConfigureAwait(false);
}
- private static async ValueTask<(bool Success, string GuildName, int Marks)> RegisterMarkCoreAsync(
+ private static async ValueTask<(CastleSiegeMarkRegistrationResult Result, string GuildName, int Marks)> RegisterMarkCoreAsync(
Player player,
CastleSiegeContext? context,
byte inventorySlot)
{
if (context is not { Configuration.Enabled: true })
{
- return (false, string.Empty, 0);
+ return (CastleSiegeMarkRegistrationResult.Failed, string.Empty, 0);
}
await context.ExecutionLock.WaitAsync().ConfigureAwait(false);
@@ -44,27 +41,31 @@ await player.InvokeViewPlugInAsync(
{
if (context.CurrentState != CastleSiegeState.RegisterMark)
{
- return (false, string.Empty, 0);
+ return (CastleSiegeMarkRegistrationResult.Failed, string.Empty, 0);
}
- var (guild, _) = await CastleSiegeGuildResolver.ResolveAuthorizedGuildAsync(player, context).ConfigureAwait(false);
+ var (guild, _) = await CastleSiegeGuildResolver.ResolveAuthorizedGuildAsync(player).ConfigureAwait(false);
if (guild is null
|| !context.RegisteredGuilds.TryGetValue(guild.PersistentId, out var registration))
{
- return (false, guild?.Name ?? string.Empty, 0);
+ return (CastleSiegeMarkRegistrationResult.GuildNotRegistered, guild?.Name ?? string.Empty, 0);
+ }
+
+ var signOfLord = player.Inventory?.GetItem(inventorySlot);
+ if (signOfLord is null
+ || signOfLord.Definition != context.Configuration.SignOfLordItemDefinition
+ || signOfLord.Level != context.Configuration.SignOfLordItemLevel)
+ {
+ return (CastleSiegeMarkRegistrationResult.IncorrectItem, guild.Name, registration.Marks);
}
- var emblem = player.Inventory?.GetItem(inventorySlot);
- if (emblem is null
- || emblem.Definition is not { Group: EmblemGroup, Number: EmblemNumber }
- || emblem.Level != EmblemLevel)
+ if (await context.IncrementMarksAsync(registration).ConfigureAwait(false) is not { } marks)
{
- return (false, guild.Name, registration.Marks);
+ return (CastleSiegeMarkRegistrationResult.GuildNotRegistered, guild.Name, registration.Marks);
}
- await player.DestroyInventoryItemAsync(emblem).ConfigureAwait(false);
- var marks = await context.IncrementMarksAsync(registration).ConfigureAwait(false);
- return (true, guild.Name, marks);
+ await player.DestroyInventoryItemAsync(signOfLord).ConfigureAwait(false);
+ return (CastleSiegeMarkRegistrationResult.Success, guild.Name, marks);
}
finally
{
diff --git a/src/GameLogic/CastleSiege/Actions/CastleSiegeRegistrationStateAction.cs b/src/GameLogic/CastleSiege/Actions/CastleSiegeRegistrationStateAction.cs
index 9aeb141cfa..ea08da07a3 100644
--- a/src/GameLogic/CastleSiege/Actions/CastleSiegeRegistrationStateAction.cs
+++ b/src/GameLogic/CastleSiege/Actions/CastleSiegeRegistrationStateAction.cs
@@ -16,6 +16,7 @@ public class CastleSiegeRegistrationStateAction
///
/// The requesting player.
/// The Castle Siege context, if initialized.
+ /// A task which represents the asynchronous operation.
public async ValueTask ShowStateAsync(Player player, CastleSiegeContext? context)
{
var (result, guildName, marks, registrationRank) = await GetStateAsync(player, context).ConfigureAwait(false);
@@ -39,15 +40,20 @@ await player.InvokeViewPlugInAsync(
await context.ExecutionLock.WaitAsync().ConfigureAwait(false);
try
{
- var guild = await CastleSiegeGuildResolver.ResolveRegistrationGuildAsync(player, context).ConfigureAwait(false);
- return guild is not null
- && context.RegisteredGuilds.TryGetValue(guild.PersistentId, out var registration)
- ? (
- CastleSiegeRegistrationStateResult.Registered,
- registration.GuildName,
- registration.Marks,
- checked((byte)Math.Min(registration.RegistrationOrder, byte.MaxValue)))
- : (CastleSiegeRegistrationStateResult.NotRegistered, string.Empty, 0, (byte)0);
+ var guildId = await CastleSiegeGuildResolver.ResolveRegistrationGuildIdAsync(player).ConfigureAwait(false);
+ if (guildId is null
+ || !context.RegisteredGuilds.TryGetValue(guildId.Value, out var registration))
+ {
+ return (CastleSiegeRegistrationStateResult.NotRegistered, string.Empty, 0, (byte)0);
+ }
+
+ // MuMain carries the rank in one byte, so later registrations use the highest representable rank.
+ var registrationRank = (byte)Math.Min(registration.RegistrationOrder, byte.MaxValue);
+ return (
+ CastleSiegeRegistrationStateResult.Registered,
+ registration.GuildName,
+ registration.Marks,
+ registrationRank);
}
finally
{
diff --git a/src/GameLogic/CastleSiege/Actions/CastleSiegeUnregisterGuildAction.cs b/src/GameLogic/CastleSiege/Actions/CastleSiegeUnregisterGuildAction.cs
index 5caccfae6a..196bfa401b 100644
--- a/src/GameLogic/CastleSiege/Actions/CastleSiegeUnregisterGuildAction.cs
+++ b/src/GameLogic/CastleSiege/Actions/CastleSiegeUnregisterGuildAction.cs
@@ -18,6 +18,7 @@ public class CastleSiegeUnregisterGuildAction
/// The requesting player.
/// The Castle Siege context, if initialized.
/// Whether the guild requests to give up its registration.
+ /// A task which represents the asynchronous operation.
public async ValueTask UnregisterAsync(
Player player,
CastleSiegeContext? context,
@@ -45,9 +46,10 @@ await player.InvokeViewPlugInAsync(
return (CastleSiegeUnregistrationResult.WrongState, string.Empty);
}
- var (guild, _) = await CastleSiegeGuildResolver.ResolveAuthorizedGuildAsync(player, context).ConfigureAwait(false);
+ var (guild, _) = await CastleSiegeGuildResolver.ResolveAuthorizedGuildAsync(player).ConfigureAwait(false);
if (guild is null)
{
+ // The client protocol has no separate NoGuild or InvalidGuild result for unregistration.
return (CastleSiegeUnregistrationResult.Failed, string.Empty);
}
diff --git a/src/GameLogic/CastleSiege/CastleSiegeContext.cs b/src/GameLogic/CastleSiege/CastleSiegeContext.cs
index a33249add0..69131d33fc 100644
--- a/src/GameLogic/CastleSiege/CastleSiegeContext.cs
+++ b/src/GameLogic/CastleSiege/CastleSiegeContext.cs
@@ -11,6 +11,10 @@ namespace MUnique.OpenMU.GameLogic.CastleSiege;
///
/// Holds the runtime state of Castle Siege for one game context.
///
+///
+/// Runtime registrations and are scoped to this game context. Deployments which run
+/// multiple game-server processes require external coordination when registrations are changed concurrently.
+///
public class CastleSiegeContext : IEventStateProvider
{
private readonly IGameContext _gameContext;
@@ -254,22 +258,6 @@ public async ValueTask ClearRegistrationsAsync()
this.RegisteredGuilds.Clear();
}
- ///
- /// Resolves the persistent identifier of a guild by its unique name.
- ///
- /// The guild name.
- /// The persistent identifier, or if the guild was not found.
- internal async ValueTask GetPersistentGuildIdAsync(string guildName)
- {
- using var context = this._gameContext.PersistenceContextProvider.CreateNewTypedContext(
- typeof(Guild),
- false,
- this._gameContext.Configuration);
- return (await context.GetAsync().ConfigureAwait(false))
- .FirstOrDefault(guild => string.Equals(guild.Name, guildName, StringComparison.OrdinalIgnoreCase))
- ?.Id;
- }
-
///
/// Creates and persists a guild registration.
///
@@ -285,7 +273,9 @@ internal async ValueTask AddRegistrationAsync(Guid
var registration = context.CreateNew();
registration.GuildId = guildId;
registration.GuildName = guildName;
- registration.RegistrationOrder = this.RegisteredGuilds.Count == 0
+
+ // Registration orders stay monotonic for one cycle and are intentionally not compacted after unregistration.
+ registration.RegistrationOrder = this.RegisteredGuilds.IsEmpty
? 1
: this.RegisteredGuilds.Values.Max(entry => entry.RegistrationOrder) + 1;
await context.SaveChangesAsync().ConfigureAwait(false);
@@ -316,15 +306,19 @@ internal async ValueTask RemoveRegistrationAsync(CastleSiegeGuildRegistration re
/// Increments and persists the submitted mark count.
///
/// The registration.
- /// The updated mark count.
- internal async ValueTask IncrementMarksAsync(CastleSiegeGuildRegistration registration)
+ /// The updated mark count, or if the registration no longer exists.
+ internal async ValueTask IncrementMarksAsync(CastleSiegeGuildRegistration registration)
{
using var context = this._gameContext.PersistenceContextProvider.CreateNewTypedContext(
typeof(CastleSiegeGuildRegistration),
false,
this._gameContext.Configuration);
- var persistentRegistration = await context.GetByIdAsync(registration.Id).ConfigureAwait(false)
- ?? throw new InvalidOperationException("The Castle Siege guild registration no longer exists.");
+ if (await context.GetByIdAsync(registration.Id).ConfigureAwait(false) is not { } persistentRegistration)
+ {
+ this.RegisteredGuilds.TryRemove(registration.GuildId, out _);
+ return null;
+ }
+
persistentRegistration.Marks++;
await context.SaveChangesAsync().ConfigureAwait(false);
registration.Marks = persistentRegistration.Marks;
diff --git a/src/GameLogic/Views/CastleSiege/CastleSiegeMarkRegistrationResult.cs b/src/GameLogic/Views/CastleSiege/CastleSiegeMarkRegistrationResult.cs
new file mode 100644
index 0000000000..4b58f03f40
--- /dev/null
+++ b/src/GameLogic/Views/CastleSiege/CastleSiegeMarkRegistrationResult.cs
@@ -0,0 +1,31 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.Views.CastleSiege;
+
+///
+/// The result of a Sign of Lord registration request.
+///
+public enum CastleSiegeMarkRegistrationResult : byte
+{
+ ///
+ /// The registration failed for an unspecified reason.
+ ///
+ Failed = 0,
+
+ ///
+ /// The Sign of Lord was registered successfully.
+ ///
+ Success = 1,
+
+ ///
+ /// The guild does not participate in the Castle Siege.
+ ///
+ GuildNotRegistered = 2,
+
+ ///
+ /// The selected inventory item is not a Sign of Lord.
+ ///
+ IncorrectItem = 3,
+}
diff --git a/src/GameLogic/Views/CastleSiege/ICastleSiegeMarkRegistrationResultPlugIn.cs b/src/GameLogic/Views/CastleSiege/ICastleSiegeMarkRegistrationResultPlugIn.cs
index 9b1f463bda..d0580b4af6 100644
--- a/src/GameLogic/Views/CastleSiege/ICastleSiegeMarkRegistrationResultPlugIn.cs
+++ b/src/GameLogic/Views/CastleSiege/ICastleSiegeMarkRegistrationResultPlugIn.cs
@@ -5,15 +5,16 @@
namespace MUnique.OpenMU.GameLogic.Views.CastleSiege;
///
-/// A view which reports Emblem of Lord submission results.
+/// A view which reports Sign of Lord submission results.
///
public interface ICastleSiegeMarkRegistrationResultPlugIn : IViewPlugIn
{
///
/// Shows the mark registration result.
///
- /// Whether an Emblem of Lord was submitted.
+ /// The registration result.
/// The registered guild name.
/// The updated mark count.
- ValueTask ShowMarkRegistrationResultAsync(bool success, string guildName, int marks);
+ /// A task which represents the asynchronous operation.
+ ValueTask ShowMarkRegistrationResultAsync(CastleSiegeMarkRegistrationResult result, string guildName, int marks);
}
diff --git a/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationResultPlugIn.cs b/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationResultPlugIn.cs
index a9038f333d..44c3d6bf4e 100644
--- a/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationResultPlugIn.cs
+++ b/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationResultPlugIn.cs
@@ -14,6 +14,7 @@ public interface ICastleSiegeRegistrationResultPlugIn : IViewPlugIn
///
/// The result.
/// The guild name.
+ /// A task which represents the asynchronous operation.
ValueTask ShowRegistrationResultAsync(CastleSiegeRegistrationResult result, string guildName);
///
@@ -22,6 +23,7 @@ public interface ICastleSiegeRegistrationResultPlugIn : IViewPlugIn
/// The result.
/// Whether the guild requested to give up its registration.
/// The guild name.
+ /// A task which represents the asynchronous operation.
ValueTask ShowUnregistrationResultAsync(
CastleSiegeUnregistrationResult result,
bool isGivingUp,
diff --git a/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationStatePlugIn.cs b/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationStatePlugIn.cs
index fb9cac66a6..f74724221d 100644
--- a/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationStatePlugIn.cs
+++ b/src/GameLogic/Views/CastleSiege/ICastleSiegeRegistrationStatePlugIn.cs
@@ -14,9 +14,10 @@ public interface ICastleSiegeRegistrationStatePlugIn : IViewPlugIn
///
/// The query result.
/// The registered guild name, or an empty string.
- /// The number of submitted Emblems of Lord.
+ /// The number of submitted Signs of Lord.
/// Whether the guild gave up its registration.
/// The registration rank, or zero when not registered.
+ /// A task which represents the asynchronous operation.
ValueTask ShowRegistrationStateAsync(
CastleSiegeRegistrationStateResult result,
string guildName,
diff --git a/src/GameServer/MessageHandler/CastleSiege/CastleSiegeMarkRegistrationHandlerPlugIn.cs b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeMarkRegistrationHandlerPlugIn.cs
index 5346ba0862..9de8c85b30 100644
--- a/src/GameServer/MessageHandler/CastleSiege/CastleSiegeMarkRegistrationHandlerPlugIn.cs
+++ b/src/GameServer/MessageHandler/CastleSiege/CastleSiegeMarkRegistrationHandlerPlugIn.cs
@@ -12,7 +12,7 @@ namespace MUnique.OpenMU.GameServer.MessageHandler.CastleSiege;
using MUnique.OpenMU.PlugIns;
///
-/// Handles Emblem of Lord registration requests.
+/// Handles Sign of Lord registration requests.
///
[PlugIn]
[Display(Name = nameof(PlugInResources.CastleSiegeMarkRegistrationHandlerPlugIn_Name), Description = nameof(PlugInResources.CastleSiegeMarkRegistrationHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
@@ -31,6 +31,11 @@ internal class CastleSiegeMarkRegistrationHandlerPlugIn : ISubPacketHandlerPlugI
///
public ValueTask HandlePacketAsync(Player player, Memory packet)
{
+ if (packet.Length < CastleSiegeMarkRegistration.Length)
+ {
+ return ValueTask.CompletedTask;
+ }
+
CastleSiegeMarkRegistration request = packet;
if (!TryGetInventorySlot(request.ItemIndex, out var inventorySlot))
{
diff --git a/src/GameServer/Properties/PlugInResources.Designer.cs b/src/GameServer/Properties/PlugInResources.Designer.cs
index 9edda71b7e..134bc7aeb1 100644
--- a/src/GameServer/Properties/PlugInResources.Designer.cs
+++ b/src/GameServer/Properties/PlugInResources.Designer.cs
@@ -637,7 +637,7 @@ public static string CastleSiegeGroupHandlerPlugIn_Name {
}
///
- /// Looks up a localized string similar to Handles Castle Siege Emblem of Lord registration requests..
+ /// Looks up a localized string similar to Handles Castle Siege Sign of Lord registration requests..
///
public static string CastleSiegeMarkRegistrationHandlerPlugIn_Description {
get {
@@ -654,6 +654,24 @@ public static string CastleSiegeMarkRegistrationHandlerPlugIn_Name {
}
}
+ ///
+ /// Looks up a localized string similar to Sends Sign of Lord registration results to the game client..
+ ///
+ public static string CastleSiegeMarkRegistrationResultPlugIn_Description {
+ get {
+ return ResourceManager.GetString("CastleSiegeMarkRegistrationResultPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Siege Mark Registration View.
+ ///
+ public static string CastleSiegeMarkRegistrationResultPlugIn_Name {
+ get {
+ return ResourceManager.GetString("CastleSiegeMarkRegistrationResultPlugIn_Name", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Handles Castle Siege guild registration requests..
///
@@ -672,6 +690,24 @@ public static string CastleSiegeRegistrationHandlerPlugIn_Name {
}
}
+ ///
+ /// Looks up a localized string similar to Sends Castle Siege registration results to the game client..
+ ///
+ public static string CastleSiegeRegistrationResultPlugIn_Description {
+ get {
+ return ResourceManager.GetString("CastleSiegeRegistrationResultPlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Siege Registration Result View.
+ ///
+ public static string CastleSiegeRegistrationResultPlugIn_Name {
+ get {
+ return ResourceManager.GetString("CastleSiegeRegistrationResultPlugIn_Name", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Handles Castle Siege registration-state requests..
///
@@ -690,6 +726,24 @@ public static string CastleSiegeRegistrationStateHandlerPlugIn_Name {
}
}
+ ///
+ /// Looks up a localized string similar to Sends Castle Siege registration state to the game client..
+ ///
+ public static string CastleSiegeRegistrationStatePlugIn_Description {
+ get {
+ return ResourceManager.GetString("CastleSiegeRegistrationStatePlugIn_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Castle Siege Registration State View.
+ ///
+ public static string CastleSiegeRegistrationStatePlugIn_Name {
+ get {
+ return ResourceManager.GetString("CastleSiegeRegistrationStatePlugIn_Name", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Handles Castle Siege guild unregistration requests..
///
diff --git a/src/GameServer/Properties/PlugInResources.resx b/src/GameServer/Properties/PlugInResources.resx
index f032c760ba..8968bbd513 100644
--- a/src/GameServer/Properties/PlugInResources.resx
+++ b/src/GameServer/Properties/PlugInResources.resx
@@ -1975,7 +1975,25 @@
Castle Siege Mark Registration Handler
- Handles Castle Siege Emblem of Lord registration requests.
+ Handles Castle Siege Sign of Lord registration requests.
+
+
+ Castle Siege Mark Registration View
+
+
+ Sends Sign of Lord registration results to the game client.
+
+
+ Castle Siege Registration Result View
+
+
+ Sends Castle Siege registration results to the game client.
+
+
+ Castle Siege Registration State View
+
+
+ Sends Castle Siege registration state to the game client.
Character - Key Configuration
diff --git a/src/GameServer/RemoteView/CastleSiege/CastleSiegeMarkRegistrationResultPlugIn.cs b/src/GameServer/RemoteView/CastleSiege/CastleSiegeMarkRegistrationResultPlugIn.cs
index e4e321f706..839d79ec44 100644
--- a/src/GameServer/RemoteView/CastleSiege/CastleSiegeMarkRegistrationResultPlugIn.cs
+++ b/src/GameServer/RemoteView/CastleSiege/CastleSiegeMarkRegistrationResultPlugIn.cs
@@ -14,6 +14,7 @@ namespace MUnique.OpenMU.GameServer.RemoteView.CastleSiege;
/// which forwards mark registration results to the game client.
///
[PlugIn]
+[Display(Name = nameof(PlugInResources.CastleSiegeMarkRegistrationResultPlugIn_Name), Description = nameof(PlugInResources.CastleSiegeMarkRegistrationResultPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("12ED701D-B865-4D2B-B044-445E662BFE0E")]
public class CastleSiegeMarkRegistrationResultPlugIn : ICastleSiegeMarkRegistrationResultPlugIn
{
@@ -26,9 +27,9 @@ public class CastleSiegeMarkRegistrationResultPlugIn : ICastleSiegeMarkRegistrat
public CastleSiegeMarkRegistrationResultPlugIn(RemotePlayer player) => this._player = player;
///
- public ValueTask ShowMarkRegistrationResultAsync(bool success, string guildName, int marks)
+ public ValueTask ShowMarkRegistrationResultAsync(CastleSiegeMarkRegistrationResult result, string guildName, int marks)
=> this._player.Connection.SendCastleSiegeMarkRegistrationResponseAsync(
- success ? (byte)1 : (byte)0,
+ (byte)result,
guildName,
checked((uint)marks));
}
diff --git a/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationResultPlugIn.cs b/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationResultPlugIn.cs
index ebbaffb0d5..f47802c46c 100644
--- a/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationResultPlugIn.cs
+++ b/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationResultPlugIn.cs
@@ -14,6 +14,7 @@ namespace MUnique.OpenMU.GameServer.RemoteView.CastleSiege;
/// which forwards registration results to the game client.
///
[PlugIn]
+[Display(Name = nameof(PlugInResources.CastleSiegeRegistrationResultPlugIn_Name), Description = nameof(PlugInResources.CastleSiegeRegistrationResultPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("1FF48BAE-9F33-4316-B4C0-D6082653C383")]
public class CastleSiegeRegistrationResultPlugIn : ICastleSiegeRegistrationResultPlugIn
{
diff --git a/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationStatePlugIn.cs b/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationStatePlugIn.cs
index cc9fc3f6e2..a213b75ea3 100644
--- a/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationStatePlugIn.cs
+++ b/src/GameServer/RemoteView/CastleSiege/CastleSiegeRegistrationStatePlugIn.cs
@@ -14,6 +14,7 @@ namespace MUnique.OpenMU.GameServer.RemoteView.CastleSiege;
/// which forwards registration state to the game client.
///
[PlugIn]
+[Display(Name = nameof(PlugInResources.CastleSiegeRegistrationStatePlugIn_Name), Description = nameof(PlugInResources.CastleSiegeRegistrationStatePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("1CE1AF15-92BB-4022-BC71-189C125F4531")]
public class CastleSiegeRegistrationStatePlugIn : ICastleSiegeRegistrationStatePlugIn
{
diff --git a/src/GuildServer/GuildServer.cs b/src/GuildServer/GuildServer.cs
index 40ec99c753..d24e317168 100644
--- a/src/GuildServer/GuildServer.cs
+++ b/src/GuildServer/GuildServer.cs
@@ -70,6 +70,15 @@ public async ValueTask GuildExistsAsync(string guildName)
return null;
}
+ ///
+ public ValueTask GetPersistentGuildIdAsync(uint guildId)
+ {
+ return ValueTask.FromResult(
+ this._guildDictionary.TryGetValue(guildId, out var guild)
+ ? (Guid?)guild.Guild.Id
+ : null);
+ }
+
///
public async ValueTask GetGuildIdByNameAsync(string guildName)
{
@@ -784,4 +793,4 @@ private IReadOnlyList GetAllianceMemberIds(uint guildId)
.Select(kvp => kvp.Key)
.ToList();
}
-}
\ No newline at end of file
+}
diff --git a/src/Interfaces/IGuildServer.cs b/src/Interfaces/IGuildServer.cs
index cf87570fcd..9316092ac6 100644
--- a/src/Interfaces/IGuildServer.cs
+++ b/src/Interfaces/IGuildServer.cs
@@ -97,6 +97,13 @@ public interface IGuildServer
/// The guild.
ValueTask GetGuildAsync(uint guildId);
+ ///
+ /// Gets the persistent identifier of a guild by its runtime identifier.
+ ///
+ /// The runtime guild identifier.
+ /// The persistent guild identifier, or if the guild was not found.
+ ValueTask GetPersistentGuildIdAsync(uint guildId);
+
///
/// Gets the guild id by the guild name.
///
@@ -243,4 +250,4 @@ public class GuildListEntry
/// Gets or sets the players position in the guild.
///
public GuildPosition PlayerPosition { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Persistence/BasicModel/CastleSiegeConfiguration.Generated.cs b/src/Persistence/BasicModel/CastleSiegeConfiguration.Generated.cs
index 54c37fd730..d187b25b72 100644
--- a/src/Persistence/BasicModel/CastleSiegeConfiguration.Generated.cs
+++ b/src/Persistence/BasicModel/CastleSiegeConfiguration.Generated.cs
@@ -214,6 +214,24 @@ protected set
}
}
+ ///
+ /// Gets the raw object of .
+ ///
+ [System.Text.Json.Serialization.JsonPropertyName("signOfLordItemDefinition")]
+ public ItemDefinition RawSignOfLordItemDefinition
+ {
+ get => base.SignOfLordItemDefinition as ItemDefinition;
+ set => base.SignOfLordItemDefinition = value;
+ }
+
+ ///
+ [System.Text.Json.Serialization.JsonIgnore]
+ public override MUnique.OpenMU.DataModel.Configuration.Items.ItemDefinition SignOfLordItemDefinition
+ {
+ get => base.SignOfLordItemDefinition;
+ set => base.SignOfLordItemDefinition = value;
+ }
+
///
/// Gets the raw object of .
///
diff --git a/src/Persistence/EntityFramework/Extensions/ModelBuilder/CastleSiegeExtensions.cs b/src/Persistence/EntityFramework/Extensions/ModelBuilder/CastleSiegeExtensions.cs
index d01cf7d051..b49c57d021 100644
--- a/src/Persistence/EntityFramework/Extensions/ModelBuilder/CastleSiegeExtensions.cs
+++ b/src/Persistence/EntityFramework/Extensions/ModelBuilder/CastleSiegeExtensions.cs
@@ -22,6 +22,7 @@ public static void Apply(this EntityTypeBuilder builde
builder.Property(configuration => configuration.CrownHoldTimeSeconds).HasDefaultValue(30);
builder.Property(configuration => configuration.RegisterMinLevel).HasDefaultValue(200);
builder.Property(configuration => configuration.RegisterMinMembers).HasDefaultValue(20);
+ builder.Property(configuration => configuration.SignOfLordItemLevel).HasDefaultValue((byte)3);
builder.Property(configuration => configuration.MaxAttackingGuilds).HasDefaultValue(3);
builder.HasOne(configuration => configuration.RawCastleSiegeMapDefinition)
@@ -33,6 +34,9 @@ public static void Apply(this EntityTypeBuilder builde
builder.HasOne(configuration => configuration.RawRewardItemDefinition)
.WithMany()
.OnDelete(DeleteBehavior.Restrict);
+ builder.HasOne(configuration => configuration.RawSignOfLordItemDefinition)
+ .WithMany()
+ .OnDelete(DeleteBehavior.Restrict);
}
///
diff --git a/src/Persistence/EntityFramework/Migrations/20260806161456_ConfigureCastleSiegeRegistration.Designer.cs b/src/Persistence/EntityFramework/Migrations/20260806161456_ConfigureCastleSiegeRegistration.Designer.cs
new file mode 100644
index 0000000000..b1975ab842
--- /dev/null
+++ b/src/Persistence/EntityFramework/Migrations/20260806161456_ConfigureCastleSiegeRegistration.Designer.cs
@@ -0,0 +1,5805 @@
+//
+using System;
+using MUnique.OpenMU.Persistence.EntityFramework;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
+{
+ [DbContext(typeof(EntityDataContext))]
+ [Migration("20260806161456_ConfigureCastleSiegeRegistration")]
+ partial class ConfigureCastleSiegeRegistration
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.2")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ChatBanUntil")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EMail")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsBot")
+ .HasColumnType("boolean");
+
+ b.Property("IsTemplate")
+ .HasColumnType("boolean");
+
+ b.Property("IsVaultExtended")
+ .HasColumnType("boolean");
+
+ b.Property("LanguageIsoCode")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(3)
+ .HasColumnType("character varying(3)")
+ .HasDefaultValue("en");
+
+ b.Property("LoginName")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("PasswordHash")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("RegistrationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("SecurityCode")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("State")
+ .HasColumnType("integer");
+
+ b.Property("TimeZone")
+ .HasColumnType("smallint");
+
+ b.Property("VaultId")
+ .HasColumnType("uuid");
+
+ b.Property("VaultPassword")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LoginName")
+ .IsUnique();
+
+ b.HasIndex("VaultId")
+ .IsUnique();
+
+ b.ToTable("Account", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AccountCharacterClass", b =>
+ {
+ b.Property("AccountId")
+ .HasColumnType("uuid");
+
+ b.Property("CharacterClassId")
+ .HasColumnType("uuid");
+
+ b.HasKey("AccountId", "CharacterClassId");
+
+ b.HasIndex("CharacterClassId");
+
+ b.ToTable("AccountCharacterClass", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CharacterClassId")
+ .HasColumnType("uuid");
+
+ b.Property("FullAncientSetEquipped")
+ .HasColumnType("boolean");
+
+ b.Property("Pose")
+ .HasColumnType("smallint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CharacterClassId");
+
+ b.ToTable("AppearanceData", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AreaSkillSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("DelayBetweenHits")
+ .HasColumnType("interval");
+
+ b.Property("DelayPerOneDistance")
+ .HasColumnType("interval");
+
+ b.Property("EffectRange")
+ .HasColumnType("integer");
+
+ b.Property("FrustumDistance")
+ .HasColumnType("real");
+
+ b.Property("FrustumEndWidth")
+ .HasColumnType("real");
+
+ b.Property("FrustumStartWidth")
+ .HasColumnType("real");
+
+ b.Property("HitChancePerDistanceMultiplier")
+ .HasColumnType("real");
+
+ b.Property("MaximumNumberOfHitsPerAttack")
+ .HasColumnType("integer");
+
+ b.Property("MaximumNumberOfHitsPerTarget")
+ .HasColumnType("integer");
+
+ b.Property("MinimumNumberOfHitsPerAttack")
+ .HasColumnType("integer");
+
+ b.Property("MinimumNumberOfHitsPerTarget")
+ .HasColumnType("integer");
+
+ b.Property("ProjectileCount")
+ .HasColumnType("integer");
+
+ b.Property("TargetAreaDiameter")
+ .HasColumnType("real");
+
+ b.Property("UseDeferredHits")
+ .HasColumnType("boolean");
+
+ b.Property("UseFrustumFilter")
+ .HasColumnType("boolean");
+
+ b.Property("UseTargetAreaFilter")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("AreaSkillSettings", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Designation")
+ .HasColumnType("text");
+
+ b.Property("GameConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("MaximumValue")
+ .HasColumnType("real");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GameConfigurationId");
+
+ b.ToTable("AttributeDefinition", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRelationship", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AggregateType")
+ .HasColumnType("integer");
+
+ b.Property("CharacterClassId")
+ .HasColumnType("uuid");
+
+ b.Property("GameConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("InputAttributeId")
+ .HasColumnType("uuid");
+
+ b.Property("InputOperand")
+ .HasColumnType("real");
+
+ b.Property("InputOperator")
+ .HasColumnType("integer");
+
+ b.Property("OperandAttributeId")
+ .HasColumnType("uuid");
+
+ b.Property("PowerUpDefinitionValueId")
+ .HasColumnType("uuid");
+
+ b.Property("SkillId")
+ .HasColumnType("uuid");
+
+ b.Property("TargetAttributeId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CharacterClassId");
+
+ b.HasIndex("GameConfigurationId");
+
+ b.HasIndex("InputAttributeId");
+
+ b.HasIndex("OperandAttributeId");
+
+ b.HasIndex("PowerUpDefinitionValueId");
+
+ b.HasIndex("SkillId");
+
+ b.HasIndex("TargetAttributeId");
+
+ b.ToTable("AttributeRelationship", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRequirement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AttributeId")
+ .HasColumnType("uuid");
+
+ b.Property("GameMapDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("ItemDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("MinimumValue")
+ .HasColumnType("integer");
+
+ b.Property("SkillId")
+ .HasColumnType("uuid");
+
+ b.Property("SkillId1")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AttributeId");
+
+ b.HasIndex("GameMapDefinitionId");
+
+ b.HasIndex("ItemDefinitionId");
+
+ b.HasIndex("SkillId");
+
+ b.HasIndex("SkillId1");
+
+ b.ToTable("AttributeRequirement", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("GroundId")
+ .HasColumnType("uuid");
+
+ b.Property("LeftGoalId")
+ .HasColumnType("uuid");
+
+ b.Property("LeftTeamSpawnPointX")
+ .HasColumnType("smallint");
+
+ b.Property("LeftTeamSpawnPointY")
+ .HasColumnType("smallint");
+
+ b.Property("RightGoalId")
+ .HasColumnType("uuid");
+
+ b.Property("RightTeamSpawnPointX")
+ .HasColumnType("smallint");
+
+ b.Property("RightTeamSpawnPointY")
+ .HasColumnType("smallint");
+
+ b.Property("Type")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroundId")
+ .IsUnique();
+
+ b.HasIndex("LeftGoalId")
+ .IsUnique();
+
+ b.HasIndex("RightGoalId")
+ .IsUnique();
+
+ b.ToTable("BattleZoneDefinition", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("MagicEffectDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("MaximumLevel")
+ .HasColumnType("integer");
+
+ b.Property("MinimumLevel")
+ .HasColumnType("integer");
+
+ b.Property("MonsterDefinitionId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MagicEffectDefinitionId")
+ .IsUnique();
+
+ b.HasIndex("MonsterDefinitionId");
+
+ b.ToTable("Buff", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AttackRespawnAreaId")
+ .HasColumnType("uuid");
+
+ b.Property("CastleSiegeMapDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("CrownHoldTimeSeconds")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(30);
+
+ b.Property("DefenseRespawnAreaId")
+ .HasColumnType("uuid");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("GateBuyPrice")
+ .HasColumnType("integer");
+
+ b.Property("GuildScoreCastleSiege")
+ .HasColumnType("integer");
+
+ b.Property("GuildScoreCastleSiegeMembers")
+ .HasColumnType("integer");
+
+ b.Property("LandOfTrialsMapDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("MaxAttackingGuilds")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(3);
+
+ b.Property("ParticipantRewardMinSeconds")
+ .HasColumnType("integer");
+
+ b.Property("RegisterMinLevel")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(200);
+
+ b.Property("RegisterMinMembers")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(20);
+
+ b.Property("RewardItemDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("SignOfLordItemDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("SignOfLordItemLevel")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("smallint")
+ .HasDefaultValue((byte)3);
+
+ b.Property("StatueBuyPrice")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AttackRespawnAreaId")
+ .IsUnique();
+
+ b.HasIndex("CastleSiegeMapDefinitionId");
+
+ b.HasIndex("DefenseRespawnAreaId")
+ .IsUnique();
+
+ b.HasIndex("LandOfTrialsMapDefinitionId");
+
+ b.HasIndex("RewardItemDefinitionId");
+
+ b.HasIndex("SignOfLordItemDefinitionId");
+
+ b.ToTable("CastleSiegeConfiguration", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("IsHuntZoneEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("IsOccupied")
+ .HasColumnType("boolean");
+
+ b.Property("OwnerGuildId")
+ .HasColumnType("uuid");
+
+ b.Property("TaxChaos")
+ .HasColumnType("smallint");
+
+ b.Property("TaxHunt")
+ .HasColumnType("integer");
+
+ b.Property("TaxStore")
+ .HasColumnType("smallint");
+
+ b.Property("TributeMoney")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerGuildId");
+
+ b.ToTable("CastleSiegeData", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuildRegistration", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("GuildId")
+ .HasColumnType("uuid");
+
+ b.Property("GuildName")
+ .IsRequired()
+ .HasMaxLength(8)
+ .HasColumnType("character varying(8)");
+
+ b.Property("Marks")
+ .HasColumnType("integer");
+
+ b.Property("RegistrationOrder")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GuildId")
+ .IsUnique();
+
+ b.ToTable("CastleSiegeGuildRegistration", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CastleSiegeConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("DefaultSide")
+ .HasColumnType("smallint");
+
+ b.Property("Direction")
+ .HasColumnType("integer");
+
+ b.Property("InstanceId")
+ .HasColumnType("smallint");
+
+ b.Property("IsPersistedToDatabase")
+ .HasColumnType("boolean");
+
+ b.Property("MonsterDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("SpawnX")
+ .HasColumnType("smallint");
+
+ b.Property("SpawnY")
+ .HasColumnType("smallint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CastleSiegeConfigurationId");
+
+ b.HasIndex("MonsterDefinitionId", "InstanceId");
+
+ b.ToTable("CastleSiegeNpcDefinition", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcState", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CastleSiegeDataId")
+ .HasColumnType("uuid");
+
+ b.Property("CurrentHp")
+ .HasColumnType("integer");
+
+ b.Property("DefenseLevel")
+ .HasColumnType("smallint");
+
+ b.Property("InstanceId")
+ .HasColumnType("smallint");
+
+ b.Property("LifeLevel")
+ .HasColumnType("smallint");
+
+ b.Property("MonsterNumber")
+ .HasColumnType("smallint");
+
+ b.Property("RegenLevel")
+ .HasColumnType("smallint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CastleSiegeDataId");
+
+ b.HasIndex("MonsterNumber", "InstanceId")
+ .IsUnique();
+
+ b.ToTable("CastleSiegeNpcState", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeStateScheduleEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CastleSiegeConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("DayOfWeek")
+ .HasColumnType("integer");
+
+ b.Property("Hour")
+ .HasColumnType("smallint");
+
+ b.Property("Minute")
+ .HasColumnType("smallint");
+
+ b.Property("State")
+ .HasColumnType("smallint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CastleSiegeConfigurationId");
+
+ b.ToTable("CastleSiegeStateScheduleEntry", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeUpgradeDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CastleSiegeConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("CastleSiegeConfigurationId1")
+ .HasColumnType("uuid");
+
+ b.Property("CastleSiegeConfigurationId2")
+ .HasColumnType("uuid");
+
+ b.Property("CastleSiegeConfigurationId3")
+ .HasColumnType("uuid");
+
+ b.Property("CastleSiegeConfigurationId4")
+ .HasColumnType("uuid");
+
+ b.Property("Level")
+ .HasColumnType("smallint");
+
+ b.Property("RequiredJewelOfGuardianCount")
+ .HasColumnType("integer");
+
+ b.Property("RequiredZen")
+ .HasColumnType("integer");
+
+ b.Property("Value")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CastleSiegeConfigurationId");
+
+ b.HasIndex("CastleSiegeConfigurationId1");
+
+ b.HasIndex("CastleSiegeConfigurationId2");
+
+ b.HasIndex("CastleSiegeConfigurationId3");
+
+ b.HasIndex("CastleSiegeConfigurationId4");
+
+ b.ToTable("CastleSiegeUpgradeDefinition", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CastleSiegeConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("CastleSiegeConfigurationId1")
+ .HasColumnType("uuid");
+
+ b.Property("X1")
+ .HasColumnType("smallint");
+
+ b.Property("X2")
+ .HasColumnType("smallint");
+
+ b.Property("Y1")
+ .HasColumnType("smallint");
+
+ b.Property("Y2")
+ .HasColumnType("smallint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CastleSiegeConfigurationId");
+
+ b.HasIndex("CastleSiegeConfigurationId1");
+
+ b.ToTable("CastleSiegeZoneDefinition", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AccountId")
+ .HasColumnType("uuid");
+
+ b.Property("CharacterClassId")
+ .HasColumnType("uuid");
+
+ b.Property("CharacterSlot")
+ .HasColumnType("smallint");
+
+ b.Property("CharacterStatus")
+ .HasColumnType("integer");
+
+ b.Property("CreateDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CurrentMapId")
+ .HasColumnType("uuid");
+
+ b.Property("Experience")
+ .HasColumnType("bigint");
+
+ b.Property("InventoryExtensions")
+ .HasColumnType("integer");
+
+ b.Property("InventoryId")
+ .HasColumnType("uuid");
+
+ b.Property("IsStoreOpened")
+ .HasColumnType("boolean");
+
+ b.Property("KeyConfiguration")
+ .HasColumnType("bytea");
+
+ b.Property("LevelUpPoints")
+ .HasColumnType("integer");
+
+ b.Property("MasterExperience")
+ .HasColumnType("bigint");
+
+ b.Property("MasterLevelUpPoints")
+ .HasColumnType("integer");
+
+ b.Property("MuHelperConfiguration")
+ .HasColumnType("bytea");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("PlayerKillCount")
+ .HasColumnType("integer");
+
+ b.Property("Pose")
+ .HasColumnType("smallint");
+
+ b.Property("PositionX")
+ .HasColumnType("smallint");
+
+ b.Property("PositionY")
+ .HasColumnType("smallint");
+
+ b.Property("State")
+ .HasColumnType("integer");
+
+ b.Property("StateRemainingSeconds")
+ .HasColumnType("integer");
+
+ b.Property("StoreName")
+ .HasColumnType("text");
+
+ b.Property("UsedFruitPoints")
+ .HasColumnType("integer");
+
+ b.Property("UsedNegFruitPoints")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccountId");
+
+ b.HasIndex("CharacterClassId");
+
+ b.HasIndex("CurrentMapId");
+
+ b.HasIndex("InventoryId")
+ .IsUnique();
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("Character", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CanGetCreated")
+ .HasColumnType("boolean");
+
+ b.Property("ComboDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("CreationAllowedFlag")
+ .HasColumnType("smallint");
+
+ b.Property("FruitCalculation")
+ .HasColumnType("integer");
+
+ b.Property("GameConfigurationId")
+ .HasColumnType("uuid");
+
+ b.Property("HomeMapId")
+ .HasColumnType("uuid");
+
+ b.Property("IsMasterClass")
+ .HasColumnType("boolean");
+
+ b.Property("LevelRequirementByCreation")
+ .HasColumnType("smallint");
+
+ b.Property("LevelWarpRequirementReductionPercent")
+ .HasColumnType("integer");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("NextGenerationClassId")
+ .HasColumnType("uuid");
+
+ b.Property("Number")
+ .HasColumnType("smallint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ComboDefinitionId")
+ .IsUnique();
+
+ b.HasIndex("GameConfigurationId");
+
+ b.HasIndex("HomeMapId");
+
+ b.HasIndex("NextGenerationClassId");
+
+ b.ToTable("CharacterClass", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterDropItemGroup", b =>
+ {
+ b.Property("CharacterId")
+ .HasColumnType("uuid");
+
+ b.Property("DropItemGroupId")
+ .HasColumnType("uuid");
+
+ b.HasKey("CharacterId", "DropItemGroupId");
+
+ b.HasIndex("DropItemGroupId");
+
+ b.ToTable("CharacterDropItemGroup", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ActiveQuestId")
+ .HasColumnType("uuid");
+
+ b.Property("CharacterId")
+ .HasColumnType("uuid");
+
+ b.Property("ClientActionPerformed")
+ .HasColumnType("boolean");
+
+ b.Property("Group")
+ .HasColumnType("smallint");
+
+ b.Property("LastFinishedQuestId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ActiveQuestId");
+
+ b.HasIndex("CharacterId");
+
+ b.HasIndex("LastFinishedQuestId");
+
+ b.ToTable("CharacterQuestState", "data");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ClientCleanUpInterval")
+ .HasColumnType("interval");
+
+ b.Property("ClientTimeout")
+ .HasColumnType("interval");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("MaximumConnections")
+ .HasColumnType("integer");
+
+ b.Property("RoomCleanUpInterval")
+ .HasColumnType("interval");
+
+ b.Property("ServerId")
+ .HasColumnType("smallint");
+
+ b.HasKey("Id");
+
+ b.ToTable("ChatServerDefinition", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerEndpoint", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ChatServerDefinitionId")
+ .HasColumnType("uuid");
+
+ b.Property("ClientId")
+ .HasColumnType("uuid");
+
+ b.Property("NetworkPort")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChatServerDefinitionId");
+
+ b.HasIndex("ClientId");
+
+ b.ToTable("ChatServerEndpoint", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CombinationBonusRequirement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ItemOptionCombinationBonusId")
+ .HasColumnType("uuid");
+
+ b.Property("MinimumCount")
+ .HasColumnType("integer");
+
+ b.Property("OptionTypeId")
+ .HasColumnType("uuid");
+
+ b.Property("SubOptionType")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemOptionCombinationBonusId");
+
+ b.HasIndex("OptionTypeId");
+
+ b.ToTable("CombinationBonusRequirement", "config");
+ });
+
+ modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConfigurationUpdate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property