Files
AdamuSw/src/GameLogic/CastleSiege/CastleSiegeGuardsmanTalkPlugIn.cs
Acentech Dev 6f7e58ff35 refactor(castle-siege): drive the cycle on the client's state numbers and persist guilds by id
Moves AdaMu's working Castle Siege onto the upstream data model that the
previous commit introduced, without changing how the siege plays.

State model
- CastleSiegePhase is replaced by DataModel's CastleSiegeState, whose values are
  exactly what the game client's CASTLESIEGE_STATE enum expects. The cycle now
  runs Idle1(0) -> RegisterGuild(1) -> Ready(6) -> Start(7) -> End(8) ->
  EndCycle(9) -> Idle1(0).
- Idle2(2), RegisterMark(3), Idle3(4) and Notify(5) keep their numbers for client
  compatibility but are never entered: AdaMu registers guilds directly and has no
  Mark of Lord step.

Guild identity
- Guilds are now identified by their persistent Guid instead of by name, so a
  rename (or a delete and re-create under the same name) can no longer hand
  castle ownership to the wrong guild. Names are carried alongside only for
  display and for the packets that send a name to the client.
- Interfaces.Guild deliberately has no id and the guild server's short ids are
  in-memory only, so the persistent id is resolved through the guild name once
  and cached per process. This avoids adding a method to IGuildServer, which
  upstream keeps changing.

Persistence
- The castle owner is stored in the CastleSiegeData row and the registrations in
  CastleSiegeGuildRegistration rows, replacing the previous plugin-configuration
  JSON blob. Only the current state and when it started still ride on the plugin
  configuration, because they have no column in the upstream schema.

Castle NPCs
- The hard-coded gate, catapult, crown and switch coordinates are gone. They are
  read from GameConfiguration.CastleSiegeConfiguration, seeded by
  CastleSiegeInitializer. Definitions flagged IsPersistedToDatabase are the
  breakable defenses and count towards the throne, which additionally brings in
  the 4 guardian statues the previous implementation did not spawn.
- The crown hold time now comes from the seeded configuration instead of the
  plugin settings.

The AdaMu operational settings (cycle durations, registration fee, designated
server id, auto-open schedule) moved to a renamed CastleSiegeSettings class, so
they no longer collide with upstream's CastleSiegeConfiguration entity.

Verified: full server build succeeds with 0 errors.
Not yet done: the 0xB2 0x00 CastleSiegeState request handler, and the docker /
local run.
2026-08-04 03:37:37 +03:00

94 lines
4.0 KiB
C#

// <copyright file="CastleSiegeGuardsmanTalkPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.CastleSiege;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Handles talking to the Castle Siege 'Guardsman' NPC (number 224): registers the talking
/// player's guild for the current siege while the state machine is in the registration phase.
/// P2 first increment — guild marks, entrance fee and minimum-guild requirements come later.
/// </summary>
[Guid("CA5710A0-7A1B-4C2D-8E3F-000000000224")]
[PlugIn]
[Display(Name = "Castle Siege Guardsman", Description = "Registers a guild for the Castle Siege when talking to the Guardsman (NPC 224).")]
public class CastleSiegeGuardsmanTalkPlugIn : IPlayerTalkToNpcPlugIn
{
/// <summary>Gets the NPC number of the Castle Siege 'Guardsman'.</summary>
public static short GuardsmanNpcNumber => 224;
/// <inheritdoc />
public async ValueTask PlayerTalksToNpcAsync(Player player, NonPlayerCharacter npc, NpcTalkEventArgs eventArgs)
{
if (npc.Definition.Number != GuardsmanNpcNumber)
{
return;
}
// We handle the dialog ourselves (no client-side window), so suppress the default "not implemented" message.
eventArgs.HasBeenHandled = true;
var context = CastleSiegeEventPlugIn.TryGetContext(player.GameContext);
if (context is null)
{
await ShowAsync(player, "Castle Siege is not active on this server.").ConfigureAwait(false);
return;
}
if (!CastleSiegeEventPlugIn.IsCastleSiegeServer(player.GameContext))
{
await ShowAsync(player, $"The Castle Siege runs on server {context.Configuration.CastleSiegeServerId}. Switch to that server to register and fight.").ConfigureAwait(false);
return;
}
if (context.State != CastleSiegeState.RegisterGuild)
{
await ShowAsync(player, "Castle Siege registration is not open right now.").ConfigureAwait(false);
return;
}
if (player.GuildStatus is not { } guildStatus || guildStatus.Position != GuildPosition.GuildMaster)
{
await ShowAsync(player, "Only a guild master can register for the Castle Siege.").ConfigureAwait(false);
return;
}
// Registrations are keyed on the guild's persistent id, so a later rename cannot detach them.
if (await CastleSiegeEventPlugIn.GetPersistentGuildAsync(player).ConfigureAwait(false) is not { } guild)
{
await ShowAsync(player, "Your guild could not be resolved. Please try again in a moment.").ConfigureAwait(false);
return;
}
var guildName = guild.Name;
if (context.IsRegistered(guild.Id))
{
await ShowAsync(player, $"Your guild '{guildName}' is already registered for the Castle Siege.").ConfigureAwait(false);
return;
}
var fee = context.Configuration.RegistrationFee;
if (fee > 0 && !player.TryRemoveMoney(fee))
{
await ShowAsync(player, $"You need {fee} zen to register your guild for the Castle Siege.").ConfigureAwait(false);
return;
}
context.RegisterGuild(guild.Id, guildName);
await ShowAsync(player, fee > 0
? $"Your guild '{guildName}' is registered for the Castle Siege. ({fee} zen paid)"
: $"Your guild '{guildName}' is registered for the Castle Siege.").ConfigureAwait(false);
}
private static ValueTask ShowAsync(Player player, string text)
=> player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(text, MessageType.BlueNormal));
}