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.
This commit is contained in:
@@ -30,9 +30,11 @@ public class CastleSiegePhaseChatCommandPlugIn : IChatCommandPlugIn
|
||||
public async ValueTask HandleCommandAsync(Player player, string command)
|
||||
{
|
||||
var parts = command.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length < 2 || !Enum.TryParse<CastleSiegePhase>(parts[1], true, out var phase))
|
||||
// The cycle uses the original Season 6 state names the client knows. AdaMu drives only this subset;
|
||||
// Idle2, RegisterMark, Idle3 and Notify exist for client compatibility but are never entered.
|
||||
if (parts.Length < 2 || !Enum.TryParse<CastleSiegeState>(parts[1], true, out var phase))
|
||||
{
|
||||
await player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync("Usage: /csphase <Ownership|Registration|Preparation|Siege|Settlement>", MessageType.BlueNormal)).ConfigureAwait(false);
|
||||
await player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync("Usage: /csphase <Idle1|RegisterGuild|Ready|Start|End|EndCycle>", MessageType.BlueNormal)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,7 +45,7 @@ public class CastleSiegePhaseChatCommandPlugIn : IChatCommandPlugIn
|
||||
return;
|
||||
}
|
||||
|
||||
await context.ForcePhaseAsync(phase, DateTime.UtcNow).ConfigureAwait(false);
|
||||
await context.ForceStateAsync(phase, DateTime.UtcNow).ConfigureAwait(false);
|
||||
await player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync($"Castle Siege: phase set to {phase}.", MessageType.BlueNormal)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,22 @@ public class CastleSiegeSetOwnerChatCommandPlugIn : IChatCommandPlugIn
|
||||
return;
|
||||
}
|
||||
|
||||
context.SetOwner(string.IsNullOrWhiteSpace(owner) ? null : owner);
|
||||
await player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync($"Castle Siege owner set to {owner ?? "(none)"}.", MessageType.BlueNormal)).ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(owner))
|
||||
{
|
||||
context.SetOwner(null, null);
|
||||
await player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync("Castle Siege owner cleared.", MessageType.BlueNormal)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ownership is stored by the guild's persistent id, so the name given here is resolved once.
|
||||
var guildId = await CastleSiegeEventPlugIn.ResolveGuildIdByNameAsync(player.GameContext, owner).ConfigureAwait(false);
|
||||
if (guildId is not { } id)
|
||||
{
|
||||
await player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync($"No guild named '{owner}' was found.", MessageType.BlueNormal)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
context.SetOwner(id, owner);
|
||||
await player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync($"Castle Siege owner set to {owner}.", MessageType.BlueNormal)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.GameLogic.CastleSiege;
|
||||
using MUnique.OpenMU.GameLogic.NPC;
|
||||
using MUnique.OpenMU.GameLogic.Views;
|
||||
@@ -15,65 +16,50 @@ using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Pathfinding;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using CastleSiegeDefinition = MUnique.OpenMU.DataModel.Configuration.CastleSiegeConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Drives the Castle Siege phase state machine: ticks it every second and carries its configuration.
|
||||
/// State is per-<see cref="IGameContext"/> and kept in memory (P1: no persistence).
|
||||
/// When the siege phase starts (P3), warps registered guild members to the Valley of Loren battle map.
|
||||
/// Drives the Castle Siege state machine: ticks it every second and carries its operational settings.
|
||||
/// <para>
|
||||
/// The cycle uses the original Season 6 state numbers the client expects. The castle owner and the guild
|
||||
/// registrations are persisted in real database tables (<see cref="CastleSiegeData"/> and
|
||||
/// <see cref="CastleSiegeGuildRegistration"/>) and keyed by the guild's persistent <see cref="Guid"/>, so a
|
||||
/// guild rename can no longer move castle ownership to the wrong guild. Only the current state and when it
|
||||
/// started ride on the plugin's custom-configuration JSON.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Castle NPCs (gates, statues, catapults, the crown and its switches) are read from
|
||||
/// <see cref="GameConfiguration.CastleSiegeConfiguration"/>, which the CastleSiegeInitializer seeds, instead
|
||||
/// of being hard-coded here.
|
||||
/// </para>
|
||||
/// When the siege starts, registered guild members are warped to the Valley of Loren battle map.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Display(Name = nameof(CastleSiegeEventPlugIn), Description = "Castle Siege event (phase state machine, scheduling, siege warp).")]
|
||||
[Display(Name = nameof(CastleSiegeEventPlugIn), Description = "Castle Siege event (state machine, scheduling, siege warp).")]
|
||||
[Guid("6E2C8B41-9A4D-4C2E-9E7B-1F2A3B4C5D60")]
|
||||
public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration<CastleSiegeConfiguration>, ISupportDefaultCustomConfiguration
|
||||
public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration<CastleSiegeSettings>, ISupportDefaultCustomConfiguration
|
||||
{
|
||||
private const ushort ValleyOfLorenMapNumber = 30;
|
||||
private const short CastleGateNumber = 277; // client MONSTER_CASTLE_GATE1 -> renders a real gate + blocks terrain until broken
|
||||
private const short GateTemplateNumber = 131; // BloodCastle "Castle Gate" destructible (has HP) — HP template for 277
|
||||
private const short GateTemplateNumber = 131; // BloodCastle "Castle Gate" destructible (has HP) — HP template for the CS gates.
|
||||
private const short CrownNumber = 216;
|
||||
private const short CatapultAttackNumber = 221; // client MONSTER_SLINGSHOT_ATTACK
|
||||
private const short CatapultDefenseNumber = 222; // client MONSTER_SLINGSHOT_DEFENSE
|
||||
private const int SwitchHoldRange = 3;
|
||||
|
||||
// Castle defenses spawned at siege start — the 6 REAL castle gates (client g_byGateLocation).
|
||||
// The client renders them closed and blocks the terrain until each is broken. All must fall before the throne.
|
||||
private static readonly (short Number, byte X, byte Y)[] DefenseSpawns =
|
||||
{
|
||||
(CastleGateNumber, 67, 114),
|
||||
(CastleGateNumber, 93, 114),
|
||||
(CastleGateNumber, 119, 114),
|
||||
(CastleGateNumber, 81, 161),
|
||||
(CastleGateNumber, 107, 161),
|
||||
(CastleGateNumber, 93, 204),
|
||||
};
|
||||
|
||||
// Siege weapons spawned at siege start purely for war atmosphere. The client renders monster 221/222 as
|
||||
// catapults (attacker/defender). They are NOT counted as defenses (destroying them doesn't open the throne).
|
||||
private const short CatapultAttackNumber = 221; // client MONSTER_SLINGSHOT_ATTACK
|
||||
private const short CatapultDefenseNumber = 222; // client MONSTER_SLINGSHOT_DEFENSE
|
||||
private static readonly (short Number, byte X, byte Y)[] CatapultSpawns =
|
||||
{
|
||||
(CatapultDefenseNumber, 80, 140),
|
||||
(CatapultDefenseNumber, 110, 140),
|
||||
(CatapultDefenseNumber, 93, 178),
|
||||
(CatapultAttackNumber, 74, 100),
|
||||
(CatapultAttackNumber, 112, 100),
|
||||
};
|
||||
|
||||
// Crown Switch positions on Valley of Loren (from the map init) — held by standing on them: (number, x, y).
|
||||
private static readonly (short SwitchNumber, byte X, byte Y)[] SwitchPositions =
|
||||
{
|
||||
(217, 167, 194),
|
||||
(218, 184, 195),
|
||||
};
|
||||
|
||||
// The Crown (NPC 216) position on Valley of Loren — the guild master holds it here to capture the throne.
|
||||
private static readonly Point CrownPosition = new(176, 212);
|
||||
private const int CrownHoldRange = 4;
|
||||
|
||||
private static readonly ConcurrentDictionary<IGameContext, CastleSiegeContext> Contexts = new();
|
||||
|
||||
private string? _cachedFlagOwner;
|
||||
/// <summary>
|
||||
/// Maps the in-memory guild id (assigned by the guild server, not stable across restarts) to the guild's
|
||||
/// persistent identifier. Populated lazily; a miss costs one database lookup per guild per process.
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<uint, Guid> PersistentGuildIds = new();
|
||||
|
||||
private Guid? _cachedFlagOwner;
|
||||
private byte[]? _cachedFlagLogo;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CastleSiegeConfiguration? Configuration { get; set; }
|
||||
public CastleSiegeSettings? Configuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Castle Siege context for a game context, if the periodic tick has initialized it.
|
||||
@@ -99,41 +85,80 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
||||
return serverContext.Id == context.Configuration.CastleSiegeServerId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the persistent identifier of the player's guild, or <see langword="null"/> when the player is
|
||||
/// not in a guild or the guild cannot be resolved.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="Interfaces.Guild"/> deliberately carries no id: the guild server assigns short ids in memory
|
||||
/// only. The persistent <see cref="Guid"/> is therefore resolved through the guild name and cached, which
|
||||
/// avoids adding a method to <see cref="IGuildServer"/> that upstream would keep changing.
|
||||
/// </remarks>
|
||||
/// <param name="player">The player.</param>
|
||||
public static async ValueTask<(Guid Id, string Name)?> GetPersistentGuildAsync(Player player)
|
||||
{
|
||||
if (player.GuildStatus is not { } guildStatus
|
||||
|| player.GameContext is not IGameServerContext serverContext)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var guild = await serverContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false);
|
||||
if (guild?.Name is not { Length: > 0 } name)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (PersistentGuildIds.TryGetValue(guildStatus.GuildId, out var cached))
|
||||
{
|
||||
return (cached, name);
|
||||
}
|
||||
|
||||
var resolved = await ResolveGuildIdByNameAsync(player.GameContext, name).ConfigureAwait(false);
|
||||
if (resolved is not { } id)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
PersistentGuildIds[guildStatus.GuildId] = id;
|
||||
return (id, name);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object CreateDefaultConfig() => new CastleSiegeConfiguration();
|
||||
public object CreateDefaultConfig() => new CastleSiegeSettings();
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ExecuteTaskAsync(GameContext gameContext)
|
||||
{
|
||||
var context = Contexts.GetOrAdd(gameContext, gc =>
|
||||
{
|
||||
var config = this.Configuration ?? new CastleSiegeConfiguration();
|
||||
var created = new CastleSiegeContext(config);
|
||||
var settings = this.Configuration ?? new CastleSiegeSettings();
|
||||
var created = new CastleSiegeContext(settings);
|
||||
|
||||
// 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);
|
||||
// Restore the cycle bookkeeping BEFORE subscribing, so restoring doesn't announce states or
|
||||
// re-spawn defenses. The owner and registrations are loaded from the database right after.
|
||||
created.RestoreState(null, null, settings.PersistedState, settings.PersistedStateStartedUtc, null);
|
||||
_ = LoadPersistedStateAsync(gc, created);
|
||||
|
||||
// Announce phase changes to the whole server and, when the siege begins, warp registered members.
|
||||
created.PhaseChanged += phase => _ = OnPhaseChangedAsync(gc, created, phase);
|
||||
created.StateChanged += state => _ = OnStateChangedAsync(gc, created, state);
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
// Point the context at the current (possibly AdminPanel-edited) config so schedule/durations are live.
|
||||
if (this.Configuration is { } liveConfig)
|
||||
// Point the context at the current (possibly AdminPanel-edited) settings so schedule/durations are live.
|
||||
if (this.Configuration is { } liveSettings)
|
||||
{
|
||||
context.UpdateConfiguration(liveConfig);
|
||||
context.UpdateConfiguration(liveSettings);
|
||||
}
|
||||
|
||||
// With multiple game servers each has its own map instances, so the siege must run on ONE designated
|
||||
// server (CastleSiegeServerId). Other servers skip the siege entirely — they only mirror the shared
|
||||
// castle owner from the config so the hunting-map gate + castle flag rewards still work everywhere.
|
||||
// castle owner from the database so the hunting-map gate + castle flag rewards still work everywhere.
|
||||
if (!IsCastleSiegeServer(gameContext))
|
||||
{
|
||||
context.SyncOwnerFromConfig();
|
||||
if (DateTime.UtcNow.Second % 15 == 0)
|
||||
{
|
||||
await LoadPersistedStateAsync(gameContext, context).ConfigureAwait(false);
|
||||
await this.BroadcastCastleFlagAsync(gameContext, context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -143,18 +168,18 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
||||
await context.TickAsync(DateTime.UtcNow).ConfigureAwait(false);
|
||||
|
||||
// During the siege, evaluate the Crown Switches (held by standing on them) and the throne capture every tick.
|
||||
if (context.Phase == CastleSiegePhase.Siege)
|
||||
if (context.IsSiegeRunning)
|
||||
{
|
||||
await ProcessSiegeTickAsync(gameContext, context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Persist owner/phase/registrations to the database whenever they changed, so they survive a restart.
|
||||
// Persist owner/state/registrations whenever they changed, so they survive a restart.
|
||||
if (context.ConsumeDirty())
|
||||
{
|
||||
await this.PersistStateAsync(gameContext, context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Keep the owner guild's logo painted on the castle flags for anyone on the castle map (any phase).
|
||||
// Keep the owner guild's logo painted on the castle flags for anyone on the castle map (any state).
|
||||
if (DateTime.UtcNow.Second % 15 == 0)
|
||||
{
|
||||
await this.BroadcastCastleFlagAsync(gameContext, context).ConfigureAwait(false);
|
||||
@@ -171,25 +196,95 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task OnPhaseChangedAsync(IGameContext gameContext, CastleSiegeContext context, CastleSiegePhase phase)
|
||||
/// <summary>Gets the seeded Castle Siege definition, or <see langword="null"/> when it was not initialized.</summary>
|
||||
/// <param name="gameContext">The game context.</param>
|
||||
private static CastleSiegeDefinition? GetDefinition(IGameContext gameContext)
|
||||
=> gameContext.Configuration.CastleSiegeConfiguration;
|
||||
|
||||
/// <summary>Resolves a guild's persistent identifier from its name, or null when there is no such guild.</summary>
|
||||
/// <param name="gameContext">The game context.</param>
|
||||
/// <param name="guildName">The guild name.</param>
|
||||
internal static async ValueTask<Guid?> ResolveGuildIdByNameAsync(IGameContext gameContext, string guildName)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (phase)
|
||||
using var context = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(DataModel.Entities.Guild), false, gameContext.Configuration);
|
||||
var guilds = await context.GetAsync<DataModel.Entities.Guild>().ConfigureAwait(false);
|
||||
return guilds.FirstOrDefault(guild => guild.Name == guildName)?.Id;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
||||
.LogError(ex, "Castle Siege: could not resolve the persistent id of guild '{guildName}'.", guildName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask<string?> ResolveGuildNameByIdAsync(IGameContext gameContext, Guid guildId)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var context = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(DataModel.Entities.Guild), false, gameContext.Configuration);
|
||||
var guild = await context.GetByIdAsync<DataModel.Entities.Guild>(guildId).ConfigureAwait(false);
|
||||
return guild?.Name;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
||||
.LogError(ex, "Castle Siege: could not resolve the name of guild {guildId}.", guildId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Loads the persisted castle owner and guild registrations from the database into the context.</summary>
|
||||
private static async ValueTask LoadPersistedStateAsync(IGameContext gameContext, CastleSiegeContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var dataContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CastleSiegeData), false, gameContext.Configuration);
|
||||
var data = (await dataContext.GetAsync<CastleSiegeData>().ConfigureAwait(false)).FirstOrDefault();
|
||||
|
||||
Guid? ownerId = data?.IsOccupied == true ? data.OwnerGuildId : null;
|
||||
var ownerName = ownerId is { } id ? await ResolveGuildNameByIdAsync(gameContext, id).ConfigureAwait(false) : null;
|
||||
|
||||
using var registrationContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CastleSiegeGuildRegistration), false, gameContext.Configuration);
|
||||
var registrations = await registrationContext.GetAsync<CastleSiegeGuildRegistration>().ConfigureAwait(false);
|
||||
|
||||
var restored = new List<KeyValuePair<Guid, string>>();
|
||||
foreach (var registration in registrations)
|
||||
{
|
||||
case CastleSiegePhase.Registration:
|
||||
var name = await ResolveGuildNameByIdAsync(gameContext, registration.GuildId).ConfigureAwait(false);
|
||||
restored.Add(new KeyValuePair<Guid, string>(registration.GuildId, name ?? registration.GuildId.ToString()));
|
||||
}
|
||||
|
||||
context.RestoreState(ownerId, ownerName, context.State, context.StateStartedUtc, restored);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
||||
.LogError(ex, "Castle Siege: error while loading the persisted state.");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task OnStateChangedAsync(IGameContext gameContext, CastleSiegeContext context, CastleSiegeState state)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case CastleSiegeState.RegisterGuild:
|
||||
await AnnounceAsync(gameContext, "Castle Siege registration is now open! Guild masters, register at the Guardsman in the Valley of Loren.").ConfigureAwait(false);
|
||||
break;
|
||||
case CastleSiegePhase.Siege:
|
||||
case CastleSiegeState.Start:
|
||||
await AnnounceAsync(gameContext, "The Castle Siege has begun! Break the castle gates and guardian statues, then hold BOTH Crown Switches to take the throne!").ConfigureAwait(false);
|
||||
await SpawnCastleDefensesAsync(gameContext, context).ConfigureAwait(false);
|
||||
await WarpRegisteredMembersToSiegeAsync(gameContext, context).ConfigureAwait(false);
|
||||
break;
|
||||
case CastleSiegePhase.Settlement:
|
||||
case CastleSiegeState.End:
|
||||
// Stop the on-map countdown for everyone still on the battle map.
|
||||
await BroadcastSiegeStateAsync(gameContext, false, 0, 0).ConfigureAwait(false);
|
||||
break;
|
||||
case CastleSiegePhase.Ownership when context.OwnerGuildName is { } owner:
|
||||
case CastleSiegeState.Idle1 when context.OwnerGuildName is { } owner:
|
||||
await AnnounceAsync(gameContext, $"The Castle Siege has ended. The castle now belongs to the guild '{owner}'!").ConfigureAwait(false);
|
||||
break;
|
||||
default:
|
||||
@@ -199,52 +294,7 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
||||
catch (Exception ex)
|
||||
{
|
||||
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
||||
.LogError(ex, "Castle Siege: error handling phase change to {phase}.", phase);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Find our plugin-configuration row via the in-memory config graph to get its id.
|
||||
var pluginTypeId = typeof(CastleSiegeEventPlugIn).GUID;
|
||||
var inMemory = gameContext.Configuration.PlugInConfigurations.FirstOrDefault(c => c.TypeId == pluginTypeId);
|
||||
if (inMemory is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Load a fresh, change-tracked copy of that row in its own (non-caching) context, rewrite its
|
||||
// custom-configuration JSON, and save — this is what actually persists to PostgreSQL.
|
||||
using var ctx = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(PlugInConfiguration), false, gameContext.Configuration);
|
||||
var row = await ctx.GetByIdAsync<PlugInConfiguration>(inMemory.GetId()).ConfigureAwait(false);
|
||||
if (row is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
row.SetConfiguration(config, gameContext.PlugInManager.CustomConfigReferenceHandler);
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
||||
.LogInformation("Castle Siege: persisted state (owner={owner}, phase={phase}).", config.PersistedOwnerGuildName ?? "(none)", config.PersistedPhase);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
||||
.LogError(ex, "Castle Siege: error while persisting state to the database.");
|
||||
.LogError(ex, "Castle Siege: error handling state change to {state}.", state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,52 +319,36 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the castle defenses from the seeded NPC definitions. Definitions flagged
|
||||
/// <see cref="CastleSiegeNpcDefinition.IsPersistedToDatabase"/> are the breakable defenses (gates and
|
||||
/// guardian statues) and are counted towards the throne; the catapults are pure war atmosphere.
|
||||
/// </summary>
|
||||
private static async Task SpawnCastleDefensesAsync(IGameContext gameContext, CastleSiegeContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (gameContext is not GameContext concrete)
|
||||
{
|
||||
context.SetDefenseCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
|
||||
if (map is null)
|
||||
if (gameContext is not GameContext concrete
|
||||
|| GetDefinition(gameContext) is not { } definition
|
||||
|| await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false) is not { } map)
|
||||
{
|
||||
context.SetDefenseCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
var template = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == GateTemplateNumber);
|
||||
var spawned = 0;
|
||||
for (var i = 0; i < DefenseSpawns.Length; i++)
|
||||
var index = 0;
|
||||
foreach (var npc in definition.NpcDefinitions.Where(n => n.IsPersistedToDatabase && n.MonsterDefinition is not null))
|
||||
{
|
||||
var spawn = DefenseSpawns[i];
|
||||
var definition = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == spawn.Number);
|
||||
if (definition is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// The stock CS gate (277) definition has no HP and isn't destructible; make it breakable
|
||||
// The stock CS gate/statue definitions have no HP and aren't destructible; make them breakable
|
||||
// by borrowing the HP/attributes of a working destructible (BloodCastle gate 131).
|
||||
var template = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == GateTemplateNumber);
|
||||
EnsureDestructible(definition, template);
|
||||
EnsureDestructible(npc.MonsterDefinition!, template);
|
||||
|
||||
var spawnArea = new MonsterSpawnArea
|
||||
{
|
||||
MonsterDefinition = definition,
|
||||
Quantity = 1,
|
||||
X1 = spawn.X,
|
||||
X2 = spawn.X,
|
||||
Y1 = spawn.Y,
|
||||
Y2 = spawn.Y,
|
||||
Direction = Direction.South,
|
||||
SpawnTrigger = SpawnTrigger.OnceAtEventStart,
|
||||
};
|
||||
|
||||
var npc = await concrete.MapInitializer.InitializeSpawnAsync(1000 + i, map, spawnArea).ConfigureAwait(false);
|
||||
if (npc is AttackableNpcBase attackable)
|
||||
var monster = await concrete.MapInitializer
|
||||
.InitializeSpawnAsync(1000 + index++, map, CreateSpawnArea(npc))
|
||||
.ConfigureAwait(false);
|
||||
if (monster is AttackableNpcBase attackable)
|
||||
{
|
||||
attackable.Died += (_, _) => context.NotifyDefenseDestroyed();
|
||||
spawned++;
|
||||
@@ -323,29 +357,12 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
||||
|
||||
context.SetDefenseCount(spawned);
|
||||
|
||||
// Spawn the catapults (siege weapons) for war atmosphere — not counted as defenses.
|
||||
for (var i = 0; i < CatapultSpawns.Length; i++)
|
||||
index = 0;
|
||||
foreach (var npc in definition.NpcDefinitions.Where(IsCatapult))
|
||||
{
|
||||
var spawn = CatapultSpawns[i];
|
||||
var definition = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == spawn.Number);
|
||||
if (definition is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var spawnArea = new MonsterSpawnArea
|
||||
{
|
||||
MonsterDefinition = definition,
|
||||
Quantity = 1,
|
||||
X1 = spawn.X,
|
||||
X2 = spawn.X,
|
||||
Y1 = spawn.Y,
|
||||
Y2 = spawn.Y,
|
||||
Direction = Direction.South,
|
||||
SpawnTrigger = SpawnTrigger.OnceAtEventStart,
|
||||
};
|
||||
|
||||
await concrete.MapInitializer.InitializeSpawnAsync(2000 + i, map, spawnArea).ConfigureAwait(false);
|
||||
await concrete.MapInitializer
|
||||
.InitializeSpawnAsync(2000 + index++, map, CreateSpawnArea(npc))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -356,27 +373,53 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsCatapult(CastleSiegeNpcDefinition npc)
|
||||
=> npc.MonsterDefinition is { } monster
|
||||
&& (monster.Number == CatapultAttackNumber || monster.Number == CatapultDefenseNumber);
|
||||
|
||||
private static MonsterSpawnArea CreateSpawnArea(CastleSiegeNpcDefinition npc) => new()
|
||||
{
|
||||
MonsterDefinition = npc.MonsterDefinition,
|
||||
Quantity = 1,
|
||||
X1 = npc.SpawnX,
|
||||
X2 = npc.SpawnX,
|
||||
Y1 = npc.SpawnY,
|
||||
Y2 = npc.SpawnY,
|
||||
Direction = npc.Direction,
|
||||
SpawnTrigger = SpawnTrigger.OnceAtEventStart,
|
||||
};
|
||||
|
||||
private static Point? GetNpcPosition(IGameContext gameContext, short monsterNumber)
|
||||
{
|
||||
var npc = GetDefinition(gameContext)?.NpcDefinitions
|
||||
.FirstOrDefault(n => n.MonsterDefinition?.Number == monsterNumber);
|
||||
return npc is null ? null : new Point(npc.SpawnX, npc.SpawnY);
|
||||
}
|
||||
|
||||
private static async Task ProcessSiegeTickAsync(IGameContext gameContext, CastleSiegeContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
|
||||
if (map is null)
|
||||
if (await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false) is not { } map)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Each Crown Switch is held by whichever registered guild currently has a member standing on it.
|
||||
foreach (var (switchNumber, x, y) in SwitchPositions)
|
||||
foreach (var switchNumber in CastleSiegeContext.SwitchNumbers)
|
||||
{
|
||||
string? holder = null;
|
||||
var nearby = map.GetAttackablesInRange(new Point(x, y), SwitchHoldRange).OfType<Player>();
|
||||
foreach (var player in nearby)
|
||||
if (GetNpcPosition(gameContext, switchNumber) is not { } position)
|
||||
{
|
||||
var guildName = await GetGuildNameAsync(player).ConfigureAwait(false);
|
||||
if (guildName is not null && context.RegisteredGuilds.Contains(guildName))
|
||||
continue;
|
||||
}
|
||||
|
||||
Guid? holder = null;
|
||||
foreach (var player in map.GetAttackablesInRange(position, SwitchHoldRange).OfType<Player>())
|
||||
{
|
||||
if (await GetPersistentGuildAsync(player).ConfigureAwait(false) is { } guild
|
||||
&& context.IsRegistered(guild.Id))
|
||||
{
|
||||
holder = guildName;
|
||||
holder = guild.Id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -386,24 +429,31 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Crown-hold capture: when a guild holds both switches with every gate down, the crown shield drops;
|
||||
// that guild's master then holds the crown for CrownHoldDuration to take the throne (contestable).
|
||||
// Crown-hold capture: when a guild holds both switches with every defense down, the crown shield
|
||||
// drops; that guild's master then holds the crown for the configured time to take the throne.
|
||||
var eligible = context.GetShieldEligibleGuild();
|
||||
Player? masterPlayer = null;
|
||||
if (eligible is not null)
|
||||
string? eligibleName = null;
|
||||
if (eligible is { } eligibleId && GetNpcPosition(gameContext, CrownNumber) is { } crownPosition)
|
||||
{
|
||||
foreach (var player in map.GetAttackablesInRange(CrownPosition, CrownHoldRange).OfType<Player>())
|
||||
foreach (var player in map.GetAttackablesInRange(crownPosition, CrownHoldRange).OfType<Player>())
|
||||
{
|
||||
if (player.GuildStatus?.Position == GuildPosition.GuildMaster
|
||||
&& await GetGuildNameAsync(player).ConfigureAwait(false) == eligible)
|
||||
if (player.GuildStatus?.Position != GuildPosition.GuildMaster)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await GetPersistentGuildAsync(player).ConfigureAwait(false) is { } guild && guild.Id == eligibleId)
|
||||
{
|
||||
masterPlayer = player;
|
||||
eligibleName = guild.Name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var crown = context.TickCrownHold(eligible, masterPlayer is not null, now, context.Configuration.CrownHoldDuration);
|
||||
var holdDuration = TimeSpan.FromSeconds(GetDefinition(gameContext)?.CrownHoldTimeSeconds ?? 60);
|
||||
var crown = context.TickCrownHold(eligible, eligibleName, masterPlayer is not null, now, holdDuration);
|
||||
|
||||
// Shield drop/raise -> everyone on the battle map, but only when it flips (the packet pops a modal).
|
||||
if (crown.ShieldChanged)
|
||||
@@ -414,13 +464,13 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
||||
switch (crown.Event)
|
||||
{
|
||||
case CrownEvent.HoldStarted when masterPlayer is not null:
|
||||
// The 60-second registration panel is shown ONLY to the master taking the crown.
|
||||
// The registration panel is shown ONLY to the master taking the crown.
|
||||
await masterPlayer.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.SetCrownRegistAsync(0, 0)).ConfigureAwait(false);
|
||||
break;
|
||||
case CrownEvent.HoldReset when masterPlayer is not null:
|
||||
await masterPlayer.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.SetCrownRegistAsync(2, 0)).ConfigureAwait(false);
|
||||
break;
|
||||
case CrownEvent.Captured when crown.Guild is { } captured:
|
||||
case CrownEvent.Captured when crown.GuildName is { } captured:
|
||||
await ForEachOnBattleMapAsync(gameContext, p => p.AnnounceSealCapturedAsync(captured)).ConfigureAwait(false);
|
||||
await AnnounceAsync(gameContext, $"Guild '{captured}' has taken the Crown and now holds the throne!").ConfigureAwait(false);
|
||||
break;
|
||||
@@ -430,7 +480,7 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
||||
|
||||
// Keep the client's on-map countdown armed and in sync. Resend every 10s so players who just
|
||||
// loaded the battle map pick it up, without visibly resetting the second-counter too often.
|
||||
if ((int)(now - context.PhaseStartedUtc).TotalSeconds % 10 == 0)
|
||||
if ((int)(now - context.StateStartedUtc).TotalSeconds % 10 == 0)
|
||||
{
|
||||
var remaining = context.GetRemainingSiegeTime(now);
|
||||
var totalMinutes = (int)Math.Ceiling(remaining.TotalMinutes);
|
||||
@@ -470,16 +520,131 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
||||
? player.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(action).AsTask()
|
||||
: Task.CompletedTask);
|
||||
|
||||
private async ValueTask BroadcastCastleFlagAsync(IGameContext gameContext, CastleSiegeContext context)
|
||||
private static async Task WarpRegisteredMembersToSiegeAsync(IGameContext gameContext, CastleSiegeContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (context.OwnerGuildName is not { Length: > 0 } owner)
|
||||
var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
|
||||
if (map?.SafeZoneSpawnGate is not { } gate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var logo = await this.GetOwnerLogoAsync(gameContext, owner).ConfigureAwait(false);
|
||||
await gameContext.ForEachPlayerAsync(async player =>
|
||||
{
|
||||
if (await GetPersistentGuildAsync(player).ConfigureAwait(false) is { } guild
|
||||
&& context.IsRegistered(guild.Id))
|
||||
{
|
||||
await player.WarpToAsync(gate).ConfigureAwait(false);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
||||
.LogError(ex, "Castle Siege: error while warping registered members to the battle map.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the castle owner and the guild registrations to their database tables, and the cycle
|
||||
/// bookkeeping (current state and when it started) to the plugin's custom-configuration JSON.
|
||||
/// </summary>
|
||||
private async ValueTask PersistStateAsync(GameContext gameContext, CastleSiegeContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.PersistOwnerAsync(gameContext, context).ConfigureAwait(false);
|
||||
await PersistRegistrationsAsync(gameContext, context).ConfigureAwait(false);
|
||||
await this.PersistCycleBookkeepingAsync(gameContext, context).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
||||
.LogError(ex, "Castle Siege: error while persisting state to the database.");
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask PersistOwnerAsync(GameContext gameContext, CastleSiegeContext context)
|
||||
{
|
||||
using var dataContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CastleSiegeData), false, gameContext.Configuration);
|
||||
var data = (await dataContext.GetAsync<CastleSiegeData>().ConfigureAwait(false)).FirstOrDefault()
|
||||
?? dataContext.CreateNew<CastleSiegeData>();
|
||||
|
||||
data.OwnerGuildId = context.OwnerGuildId;
|
||||
data.IsOccupied = context.OwnerGuildId is not null;
|
||||
await dataContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
||||
.LogInformation(
|
||||
"Castle Siege: persisted owner={owner} state={state}.",
|
||||
context.OwnerGuildName ?? "(none)",
|
||||
context.State);
|
||||
}
|
||||
|
||||
private static async ValueTask PersistRegistrationsAsync(GameContext gameContext, CastleSiegeContext context)
|
||||
{
|
||||
using var registrationContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CastleSiegeGuildRegistration), false, gameContext.Configuration);
|
||||
var existing = (await registrationContext.GetAsync<CastleSiegeGuildRegistration>().ConfigureAwait(false)).ToList();
|
||||
var wanted = context.RegisteredGuildIds.ToHashSet();
|
||||
|
||||
foreach (var registration in existing)
|
||||
{
|
||||
if (!wanted.Remove(registration.GuildId))
|
||||
{
|
||||
await registrationContext.DeleteAsync(registration).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var guildId in wanted)
|
||||
{
|
||||
var registration = registrationContext.CreateNew<CastleSiegeGuildRegistration>();
|
||||
registration.GuildId = guildId;
|
||||
}
|
||||
|
||||
await registrationContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask PersistCycleBookkeepingAsync(GameContext gameContext, CastleSiegeContext context)
|
||||
{
|
||||
if (this.Configuration is not { } settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
settings.PersistedState = context.State;
|
||||
settings.PersistedStateStartedUtc = context.StateStartedUtc;
|
||||
|
||||
// Find our plugin-configuration row via the in-memory config graph to get its id, then rewrite its
|
||||
// custom-configuration JSON in its own (non-caching) context.
|
||||
var pluginTypeId = typeof(CastleSiegeEventPlugIn).GUID;
|
||||
var inMemory = gameContext.Configuration.PlugInConfigurations.FirstOrDefault(c => c.TypeId == pluginTypeId);
|
||||
if (inMemory is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var ctx = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(PlugInConfiguration), false, gameContext.Configuration);
|
||||
var row = await ctx.GetByIdAsync<PlugInConfiguration>(inMemory.GetId()).ConfigureAwait(false);
|
||||
if (row is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
row.SetConfiguration(settings, gameContext.PlugInManager.CustomConfigReferenceHandler);
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask BroadcastCastleFlagAsync(IGameContext gameContext, CastleSiegeContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (context.OwnerGuildId is not { } owner)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var logo = await this.GetOwnerLogoAsync(gameContext, owner, context.OwnerGuildName).ConfigureAwait(false);
|
||||
if (logo is null)
|
||||
{
|
||||
return;
|
||||
@@ -500,77 +665,32 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<byte[]?> GetOwnerLogoAsync(IGameContext gameContext, string ownerName)
|
||||
private async ValueTask<byte[]?> GetOwnerLogoAsync(IGameContext gameContext, Guid ownerId, string? ownerName)
|
||||
{
|
||||
if (this._cachedFlagOwner == ownerName && this._cachedFlagLogo is not null)
|
||||
if (this._cachedFlagOwner == ownerId && this._cachedFlagLogo is not null)
|
||||
{
|
||||
return this._cachedFlagLogo;
|
||||
}
|
||||
|
||||
if (gameContext is not IGameServerContext serverContext)
|
||||
if (gameContext is not IGameServerContext serverContext || ownerName is not { Length: > 0 })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var guildId = await serverContext.GuildServer.GetGuildIdByNameAsync(ownerName).ConfigureAwait(false);
|
||||
if (guildId == 0)
|
||||
var shortGuildId = await serverContext.GuildServer.GetGuildIdByNameAsync(ownerName).ConfigureAwait(false);
|
||||
if (shortGuildId == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var guild = await serverContext.GuildServer.GetGuildAsync(guildId).ConfigureAwait(false);
|
||||
var guild = await serverContext.GuildServer.GetGuildAsync(shortGuildId).ConfigureAwait(false);
|
||||
if (guild?.Logo is not { Length: > 0 } logo)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
this._cachedFlagOwner = ownerName;
|
||||
this._cachedFlagOwner = ownerId;
|
||||
this._cachedFlagLogo = logo;
|
||||
return logo;
|
||||
}
|
||||
|
||||
private static async Task WarpRegisteredMembersToSiegeAsync(IGameContext gameContext, CastleSiegeContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
|
||||
if (map?.SafeZoneSpawnGate is not { } gate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await gameContext.ForEachPlayerAsync(async player =>
|
||||
{
|
||||
var guildName = await GetGuildNameAsync(player).ConfigureAwait(false);
|
||||
if (guildName is not null && context.RegisteredGuilds.Contains(guildName))
|
||||
{
|
||||
await player.WarpToAsync(gate).ConfigureAwait(false);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
||||
.LogError(ex, "Castle Siege: error while warping registered members to the battle map.");
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask<string?> GetGuildNameAsync(Player player)
|
||||
{
|
||||
if (player.GuildStatus is not { } guildStatus)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (player.GameContext is IGameServerContext serverContext)
|
||||
{
|
||||
var guild = await serverContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false);
|
||||
if (guild?.Name is { Length: > 0 } name)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return guildStatus.GuildId.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user