Implement Castle Siege registration and mark system - #862
Conversation
sven-n
left a comment
There was a problem hiding this comment.
Review — Castle Siege registration & mark system
I verified the four new S→C packet layouts against the MuMain client (WSclient.h / WSclient.cpp) and reviewed the game-logic, handler and view code.
What's good
- Packet layouts are correct.
PMSG_ANS_REGCASTLESIEGE(13),PMSG_ANS_GIVEUPCASTLESIEGE(14),PMSG_ANS_GUILDREGINFO(19) andPMSG_ANS_REGGUILDMARK(17) match the XML field offsets 1:1, including the 8-byte guild name and the mark count: the client assemblesbtGuildMark4..1into a DWORD LSB-first, soIntegerBigEndianis right. - Result codes match the client.
CastleSiegeRegistrationResult0–8 maps exactly ontoReceiveBCReg, andCastleSiegeUnregistrationResult0–3 ontoReceiveBCGiveUp(incl.NotRegistered= 2,WrongState= 3). - Clean separation action / view interface / remote view, everything guarded by
ExecutionLock, and good test coverage (validation matrix, persistence round-trip after a simulated restart, subcodes, backpack-index translation, serialization).
Issues worth addressing
-
Mark registration throws away two client-supported error codes.
ReceiveBCRegMarkhandles0= failed,1= ok,2= "This guild has not participated in castle siege",3= "Incorrect item was registered". The view interface only carriesbool success, so both specific messages can never be shown — the guild-not-registered case and the wrong-item case both surface as the generic failure. Consider aCastleSiegeMarkRegistrationResultenum mirroring 0–3, the same way registration/unregistration already do. -
The Emblem can be destroyed without the mark being counted. In
CastleSiegeRegisterMarkAction,DestroyInventoryItemAsyncruns first andIncrementMarksAsyncthrowsInvalidOperationExceptionif the registration row disappeared meanwhile — the item is gone, the mark is not persisted, and the exception escapes into the packet handler (no view response is sent either). Increment first, or catch and restore/report. -
Full
Guildtable load per B2 request, inside the siege lock.CastleSiegeContext.GetPersistentGuildIdAsyncdoescontext.GetAsync<Guild>()and a client-sideFirstOrDefaulton every register/unregister/state/mark packet, and it runs while holdingExecutionLock— the same semaphore the state-machine timer uses. Any player can spam B2-03 and serialize a full-table scan against the siege loop.GuildServeralready maintains theGuid→uintmapping (_guildIdMapping); exposing the persistent id onInterfaces.Guild/IGuildServer(or adding a by-name query next toGuildWithNameExistsAsync) would remove the scan entirely. -
Multi-game-server duplicate registrations.
RegisteredGuildsandExecutionLockare per-process. A guild master connected to server B doesn't see server A's cached registration, so theAlreadyRegistered/RegistrationOrderlogic can produce a second row for the same guild. If that's an accepted limitation inherited from #861, a note in the class docs would help. -
The mark item likely can't exist in the shipped configuration.
EmblemGroup = 14, EmblemNumber = 21, EmblemLevel = 3— inVersionSeasonSix/Items/Misc.cs, 14/21 is Rena, created without aMaximumItemLevel, so a level-3 instance can't be produced by the initialization data. Either seed the Sign/Emblem of Lord definition or (preferably) make it a configurableItemDefinitionreference onCastleSiegeConfiguration, next toRegisterMinLevel/RegisterMinMembers. Also worth unifying the naming: the PR body says "Sign of Lord", the code says "Emblem of Lord", the packet docs say "guild mark".
Smaller points
CastleSiegeMarkRegistrationHandlerPlugIn.HandlePacketAsyncreadsItemIndex(index 4) with nopacket.Length < CastleSiegeMarkRegistration.Lengthguard; the repo's convention (e.g.AnimationHandlerPlugIn,CharacterMoveBaseHandlerPlugIn) is to check first, otherwise a short B2 04 packet throws out of the handler.- The three new
RemoteView/CastleSiegeplugins lack the[Display(Name = ..., Description = ..., ResourceType = typeof(PlugInResources))]attribute that every other remote-view plugin (and the message handlers in this very PR) carries — the resx additions only cover the handlers. checked((byte)Math.Min(registration.RegistrationOrder, byte.MaxValue)): after theMath.Minthecheckedcan never trip; a plain cast reads better (and the silent cap at 255 deserves a comment).CastleSiegeGuildResolver.ResolveRegistrationGuildAsyncfalls back toGetGuildIdByNameAsync, which only searches guilds currently loaded in memory and returns0when not found — the resultingRuntimeIdcan be a meaningless0. Nothing consumes it on that path today; consider dropping it from the reference for the read-only query.CastleSiegeUnregisterGuildActiondiscards the resolver's result and reports the genericFailedwhere the register path distinguishesNoGuild/InvalidGuild. That's forced by the client's 0–3 range, but a one-line comment would prevent a future "bug fix".RegistrationOrderismax + 1over the in-memory set, so ranks are not compacted after an unregistration — presumably intended, just flagging.
Status
GitHub reports the PR as conflicting with master (mergeable_state: dirty) — it needs a rebase/merge before it can be evaluated by CI.
Generated by Claude Code
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
b3b4704 to
c45c754
Compare
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.
sven-n
left a comment
There was a problem hiding this comment.
Second pass — commit 875352d ("Address Castle Siege registration review")
Thanks, all five main points and every smaller note from the previous round are addressed, most of them with a regression test:
CastleSiegeMarkRegistrationResult(0–3) now mirrorsReceiveBCRegMarkexactly — verified again against the client, incl. the "not participated" and "incorrect item" cases.- The Sign of Lord is no longer destroyed before the mark is persisted;
IncrementMarksAsyncreturnsint?and evicts the stale cache entry, covered byMarkRegistrationPreservesItemWhenRegistrationDisappearedAsync. - The full
Guildtable scan is gone:IGuildServer.GetPersistentGuildIdAsync(uint)reads the guild server's in-memory dictionary, with Dapr host/client wiring. Nice. - The item is configuration-driven (
SignOfLordItemDefinition/SignOfLordItemLevel), with initializer, update plug-in (UpdateVersion 102) and EF migration. - Packet-length guard,
[Display]attributes on the three remote views, the deadchecked, the multi-server remark and theRegistrationOrdercomment are all in.
New points from this revision
-
Alliance members may now be reported as not registered.
ResolveRegistrationGuildIdAsyncresolves the alliance viaGetGuildIdByNameAsync, which only searchesGuildServer._guildDictionary— guilds that are loaded because a member is online. If the alliance master guild has nobody online, that returns0, the method returnsnull, and a member of a registered alliance getsNotRegistered/ rank 0. The previous name-based persistence lookup did not have this hole. The guild container already holds theDataModel.Entities.Guild(which has theGuid), so anIGuildServeroverload that resolves the alliance master's persistent id directly — or a by-name persistence lookup as fallback — would close it. Worth a test with an offline alliance master. -
InitializeRegistrationoverwrites customised configuration. It's now called unconditionally fromCreateCastleSiegeConfiguration, including theexistingConfigurationbranch, so an admin's ownSignOfLordItemDefinition/SignOfLordItemLevelis reset on every initialization run. Consider only filling it whenSignOfLordItemDefinition is null. Related:this.GameConfiguration.Items.Single(...)throws on any configuration that has no 14/21 item —SingleOrDefaultplus an early return would be friendlier, especially since the update plug-in isIsMandatory. -
The Sign of Lord is still not obtainable from the shipped data. Raising Rena's
MaximumItemLevelto 3 only permits a level-3 instance to exist — I checkedDefaultDropGenerator.GenerateItemDrop, which leaves the level at 0 for a plainDropItemGroup, so the Rena drop wiring is not affected (good), but nothing creates a level-3 item either. Is that intentionally left to GM item creation / a later PR? A line in the PR description would help. -
Item destruction is now the last step, so the mirror-image failure exists: if
DestroyInventoryItemAsyncthrows, the mark is already persisted and the item is kept. Much less severe than the original ordering, but a short comment (or a compensating decrement) would document the choice. -
Scope has grown well past the description. This revision pulls in the complete Castle Siege server-to-client packet set (~10k generated lines, 30+ new
docs/Packetsfiles,ClientToServerPackets.xmlchanges, the EF migration andCastleSiegePacketDefinitionTests). The PR body still says it deliberately contains "only the four response definitions required by this registration phase, so it can be reviewed independently of #736" — that's no longer accurate, and the overlap with #736 now needs an explicit decision on which PR owns those definitions. Please update the description. -
Still conflicting with
master; after the rebase the new migration's timestamp/ordering andUpdateVersion = 102should be re-checked against whatever landed meanwhile.
Generated by Claude Code
Resolve alliance registrations directly through persistent alliance-master identities, including when the master has no online members. Preserve customized Sign of Lord settings, tolerate configurations without the default item, document persistence-before-consumption semantics, and extend regression coverage.
sven-n
left a comment
There was a problem hiding this comment.
Third pass — commit efda253 ("Refine Castle Siege registration edge cases")
Everything from the second round is resolved, and the PR is now in good shape:
- Offline alliance master is fixed properly:
IGuildServer.GetPersistentAllianceMasterGuildIdAsyncresolves the master's persistent id straight from the guild container (with Dapr host/client wiring), soGetGuildIdByNameAsyncis out of the path entirely. TheGuildAllianceTestcase that takes the master's last member offline is exactly the right regression test, and assertingGetGuildIdByNameAsyncis never called in the game-logic test is a nice touch. The registration path stores under the alliance master's own persistent id, so writer and reader agree. - Initializer no longer overwrites a customised
SignOfLordItemDefinition, andSingleOrDefault+ early return means a configuration without item 14/21 initializes instead of throwing — both covered inTestInitializationWithEfCore. - The persist-before-destroy ordering is now documented with its trade-off.
- Scope is clean again: with the packet set merged through #863, the diff is only game logic, guild-server API, configuration/migration and tests. Description updated accordingly, the Sign of Lord availability question is answered explicitly, base is
clean, and both checks (Azure build, Codacy) are green.
Remaining nits (non-blocking)
GuildServer.GetPersistentAllianceMasterGuildIdAsyncusesguild.Guild.AllianceGuild is Guild allianceMaster ? allianceMaster.Id : guild.Guild.Id. SinceAllianceGuildis typed asInterfaces.Guild, an instance that isn't aDataModel.Entities.Guildsilently falls back to the guild's own id — i.e. it would quietly bucket an alliance member under the wrong registration rather than failing. Ais nullcheck plus an explicit cast would make that case loud instead of subtly wrong.- In
TestInitializationWithEfCore, the trailing restore block (re-addingsignOfLordand resetting the configuration) has no assertions after it — if it's only there so later parts of the fixture stay valid, a one-line comment saying so would help; otherwise it can go. GuildAllianceTestfully-qualifiesMUnique.OpenMU.GuildServer.GuildServer.OfflineServerId; ausingalias would read better.
Nothing here blocks a merge from my side — I'd leave the formal approval to you.
Generated by Claude Code
sven-n
left a comment
There was a problem hiding this comment.
Approving — all review findings from the three rounds are addressed, the packet layouts and result codes are verified against the MuMain client, the scope is back to registration logic plus configuration/migration and tests, and CI is green. The remaining nits are non-blocking and can be picked up in a follow-up.
Generated by Claude Code
Summary
Implements the Castle Siege guild registration and Sign of Lord registration system.
The 30-minute registration notifications are provided by the state-machine implementation merged in #861.
Packet dependency
The complete Castle Siege packet definitions were merged into master through #863 as part of #731. This PR consumes those definitions and does not add or modify packet XML, generated packet artifacts, or packet documentation.
Sign of Lord availability
The default Season 6 configuration uses item 14/21 at level 3 as the Sign of Lord and permits that item level.
This PR intentionally does not add a drop source. Obtaining the Sign of Lord remains a server-configuration, administrator-tooling, or follow-up gameplay concern.
Validation
0–8.0–3.Closes #723