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.
With multiple game servers each has its own map instances, so the siege ran
independently on all of them - a guild could win uncontested on an empty server's
Valley of Loren. Now the siege (tick/spawn/battle/registration) runs only on the
server whose Id == config.CastleSiegeServerId (AdminPanel-editable). Other servers
skip the siege and just mirror the shared castle owner from config so the hunting-map
gate + castle flag rewards still work everywhere. The Guardsman on non-siege servers
tells players which server to switch to.
- The occupier can no longer re-register its own throne (canCapture = eligible != occupier),
which caused a repeating 60s panel loop; only a DIFFERENT guild can contest.
- The 60s registration panel (0xB2/0x15) is now sent ONLY to the master on the crown,
not broadcast to switch-holders/other players. Shield(0x16) + capture(0x18) still broadcast.
+1 test (15 total).
The shield packet pops a modal message box on the client; sending it every tick
re-opened the modal continuously and froze player input at siege start. Track the
shield state in the context and only push the packet when it actually flips.
Break all gates + hold both switches (defenses down) -> the Crown shield drops
(C1 B2 16=0). The guild master then stands on the Crown (176,212) and holds for
CrownHoldDuration (default 60s, client shows a 60s countdown via C1 B2 15) to
capture; capture broadcasts C1 B2 18 + golden text and sets the occupier. Losing a
switch or leaving the crown resets the hold (contestable until the siege timer ends).
Sinior (223) is now informational guidance. +2 tests (14 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the auto-schedule from context-owned state to the plugin config (single source
of truth): RegistrationOpenDays + RegistrationOpenTimes + Registration/Preparation/
SiegeDuration are all editable in the AdminPanel plugin config and take effect live
(context refreshes its config reference each tick via UpdateConfiguration). /csschedule
now writes the config. Removed the duplicate Persisted schedule fields. 12 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Client already renders the siege-map countdown but never received the packets.
New ICastleSiegeStatusViewPlugIn + RemoteView impl send the raw S6 packets:
C1 B2 17 (battle start/stop flag, arms the countdown) and C1 B2 1E (remaining
hour/minute). CastleSiegeEventPlugIn broadcasts them to players on the battle map
every 10s during Siege and sends stop at Settlement. No client changes. +1 test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The siege can now auto-open registration on scheduled days/time instead of only
manual /csphase. Schedule is context-owned state (like owner), persisted across
restarts via the same config-JSON path. New GM command /csschedule sets/views/clears
it (e.g. /csschedule Sunday 20:00, UTC). +2 unit tests (11 total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>