feat(CS): persist owner/phase/registrations across restarts via plugin config JSON
Some checks failed
.NET Core / build (push) Has been cancelled

Castle Siege state was in-memory, so the castle owner (and thus the P4 hunting-map
reward) reset on every server restart/redeploy. Now the context marks itself dirty
on any persistable change (phase transition, registration, owner set); the plugin's
periodic tick writes a snapshot into its own PlugInConfiguration CustomConfiguration
(a JSON blob already stored in PostgreSQL - no schema migration) and restores it on
startup. Battle state (defenses/switches/occupier) stays transient. +2 unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Acentech Dev
2026-07-15 12:17:34 +03:00
parent f03f6aa7ad
commit 9bdd57703f
4 changed files with 143 additions and 2 deletions

View File

@@ -69,7 +69,12 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
{
var context = Contexts.GetOrAdd(gameContext, gc =>
{
var created = new CastleSiegeContext(this.Configuration ?? new CastleSiegeConfiguration());
var config = this.Configuration ?? new CastleSiegeConfiguration();
var created = new CastleSiegeContext(config);
// Restore state persisted before the last restart (owner/phase/registrations) BEFORE subscribing,
// so restoring doesn't announce phases or re-spawn defenses.
created.RestoreState(config.PersistedOwnerGuildName, config.PersistedPhase, config.PersistedPhaseStartedUtc, config.PersistedRegisteredGuilds);
// Announce phase changes to the whole server and, when the siege begins, warp registered members.
created.PhaseChanged += phase => _ = OnPhaseChangedAsync(gc, created, phase);
@@ -84,6 +89,12 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
{
await ProcessSiegeTickAsync(gameContext, context).ConfigureAwait(false);
}
// Persist owner/phase/registrations to the database whenever they changed, so they survive a restart.
if (context.ConsumeDirty())
{
await this.PersistStateAsync(gameContext, context).ConfigureAwait(false);
}
}
/// <inheritdoc />
@@ -124,6 +135,41 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
}
}
private async ValueTask PersistStateAsync(GameContext gameContext, CastleSiegeContext context)
{
try
{
if (this.Configuration is not { } config)
{
return;
}
// Snapshot the live context into the persisted config fields.
config.PersistedOwnerGuildName = context.OwnerGuildName;
config.PersistedPhase = context.Phase;
config.PersistedPhaseStartedUtc = context.PhaseStartedUtc;
config.PersistedRegisteredGuilds = context.RegisteredGuilds.ToList();
// Write the plugin's custom-configuration JSON row back to the database (durable across restarts).
var pluginTypeId = typeof(CastleSiegeEventPlugIn).GUID;
using var ctx = gameContext.PersistenceContextProvider.CreateNewContext(gameContext.Configuration);
var configurations = await ctx.GetAsync<PlugInConfiguration>().ConfigureAwait(false);
var row = configurations.FirstOrDefault(c => c.TypeId == pluginTypeId);
if (row is null)
{
return;
}
row.SetConfiguration(config, gameContext.PlugInManager.CustomConfigReferenceHandler);
await ctx.SaveChangesAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
.LogError(ex, "Castle Siege: error while persisting state to the database.");
}
}
private static ValueTask AnnounceAsync(IGameContext gameContext, string message)
=> gameContext.ForEachPlayerAsync(player =>
player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, MessageType.GoldenCenter)).AsTask());