Conversation
…ills Implements the full Illusion Temple event as a team-based PvP mini game: - Stone Statue (NPC 380) holds a sacred relic; a player talks to it to become the carrier, delivers it to his team's storage (383/384) to score, and drops it on death or when leaving. - Two teams (Allied/Illusion Forces), assigned and spawned on game start, with a live per-player state update (own team positions, relic carrier, remaining time) and an end-of-game result screen. - A team needs at least 2 points, and more than the opposing team, to be declared the winner - matching the original event (a 1:0 finish is a draw, like in the reference server). - Experience is granted automatically to the winning team at game end; other reward types (item drops) are only granted once the player explicitly claims them (0xBF05), closing the result dialog - mirroring the original "click Close to be compensated" flow. - Skill points (start at 10, cap 90): +1 for killing an enemy player, +2 for killing a roaming arena monster. They fuel four special skills (210 Order of Protection, 211 Restraint, 212 Tracking, 213 Weaken), each costing 10 points, requested via a dedicated 0xBF02 packet. - MiniGameDefinition.MinimumPlayerCount is now admin-configurable (EF migration included) instead of hardcoded per event type. - Fixed a base MiniGameContext bug where players stuck on an event map with too few participants were never moved back to safezone. New server<->client packets: IllusionTempleEventState, HolyItemRelics, SkillUsageResult, SkillPointUpdate, SkillEnded, RewardRequest handling, and corrected byte layouts for IllusionTempleState/Result (missing RelicCarrierId field, wrong array offset, and a 3-byte padding gap in PlayerResult that the original client's C struct alignment expects). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Registers the periodic mini game start plugin (every 2 hours, matching the official Webzen server's Illusion Temple schedule) and its game server state tracking, plus a chat command for game masters to start an Illusion Temple match on demand for testing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Corrects the statue/guardian/relic-box spawn data on all six temple maps, verified against a working Season 6 Episode 3 server's spawn list: two stone statue positions (not three), added the two decorative team guardian NPCs (381/382), and fixed both relic storage box coordinates, which were off by one tile. Adds the 32 roaming "Illusion Sorc. Spirit" arena monster spawns (NPC 386-399, cycling per temple level) to temples 1-5 - temple 6 has none, matching the reference data. Also sets each temple's safezone map to Devias explicitly: without it, a temple's own spawn gate made BaseMapInitializer default the safezone to the temple map itself, so a player warped to "safezone" (e.g. when too few players joined) was simply sent back into the arena instead of actually leaving it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Existing servers won't get the Illusion Temple changes above just from an EF migration - the mini game definitions, monster/item data and map spawns are seed data, normally only created on a fresh install. This update plugin applies all of it to an already-running database instead: - Fixes the "Illusion Sorcerer Covenant"/"Scroll of Blood" ticket item numbers (50/51 were swapped - IllusionTempleInitializer expects the ticket at Group 13, Number 51), by correcting the Number field on the existing item entities so already-owned instances keep working. - Adds the sacred relic item (Group 14, Number 64) if missing. - Adds the 14 arena monster definitions (386-399) and their spawns, the corrected statue/guardian/box spawns, and the safezone map fix - all matching the fresh-install map data from the previous commit. - Adds the two special-skill magic effects (210/211). - Creates the mini game definitions on a database that doesn't have Illusion Temple at all yet, or backfills MinimumPlayerCount on ones that already do. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers entrance/entry timing, finishing early when too few players remain, team assignment for even and odd player counts, game start, the statue/relic pickup-death-pickup loop, scoring for the carrier's own team vs. the enemy's, and the end-of-game reward flow (experience granted immediately, an item reward only once claimed). MiniGameContext auto-starts a real-time countdown on construction (clamped to at least 30s), far too slow for a test suite, so these tests build a fully wired IllusionTempleContext and drive its lifecycle hooks directly via reflection instead of waiting on the real timers. Extends GameContextTestHelper.CreateGameContext with an optional IDropGenerator parameter (needed to test item rewards) and a MaximumLevel default (needed for AddExperienceAsync to actually grant anything - it no-ops above the configured max level, which defaulted to 0). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Removed the unused _activeStatue field (only ever written, never read) and made the two team spawn coordinate fields readonly, since neither is ever reassigned after construction. - Renamed a local variable in TeleportToStartCoordinatesAsync that was shadowing the illusionForcesCoordinates field. - In the test file: documented why the two reflection calls that bypass accessibility are safe (test-only code, hardcoded member names, no external input), replaced a switch without a default case with if/else, dropped a redundant explicit default-value argument, gave the fake IDropGenerator's genuinely unused interface parameters discard-style names, and made the test spawn-area helper actually use its "number" parameter to build a stable Guid. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The DyingDropsTheRelicAndItCanBePickedUpAgain test was failing deterministically: the test entered players via TryEnterAsync and the client map-change handshake, but skipped the WarpToAsync to the event entrance that EnterMiniGameAction performs in between. Without it the players stayed on the default map instead of the mini game's own map instance, so the dropped relic landed on a map the event isn't subscribed to and OnItemDroppedOnMap - the single place that clears the relic carrier - never ran. The entry helper now mirrors the real server flow, and the whole suite passes. Codacy follow-ups: - Moved the usings of IllusionTempleContext inside the namespace, as the rest of the code base does, and dropped two that were genuinely unused. The System.Collections.Concurrent one is kept - it is used by the ConcurrentDictionary fields. - Documented the two reflection helpers with SuppressMessage justifications instead of only comments: both are test-only, operate on types from this solution, and are passed hardcoded member names. - Gave the fake IDropGenerator its interface parameter names back and justified them at class level - they are mandated by the interface and deliberately ignored. - Added the missing else branch in AwaitResultAsync. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Blocking: - Added the missing <summary> tag on UpdateVersion.IllusionTempleData (the number collision with AddCastleSiegeData resolved itself when the branch was rebased - it's 104 now). - OnObjectRemovedFromMapAsync no longer warps the leaving player: it runs from inside WarpToAsync, so warping again nested the warps and left the player on two maps with duplicate map-change packets, while also bypassing the per-temple safezone. It's now pure state cleanup; leaving is done by ClaimRewardAsync and the base class's exit handling. - ToIllusionTempleEnterResult actually maps its parameter now, instead of reporting success for every refusal. - Item rewards reached only the first winner, because DoesRewardApply compares parties and a party is disposed once fewer than two members remain. The winner-related predicates moved into overridable IsWinner/IsInWinningParty/IsWinnerOrInWinningParty methods, which the illusion temple answers by team - so every member of the winning team is rewarded. - The 20 second preparation delay is configurable (PreparationDuration). The tests set it to zero, which brings the suite from ~4.8 minutes down to ~5 seconds. Correctness: - Team mates are spread over consecutive tiles again - the previous code incremented a copy of a readonly field, so everyone stacked on one tile. Typo in the comment fixed as well. - Dropped the redundant "player.Party = null", which bypassed KickMySelfAsync and left the player in the old party's member array. - Restraint and Weaken only hit opponents now, and using a special skill requires a running event and a living caster. - The reported experience mirrors what is actually granted, including the per-remaining-second reward type. - Talking to the statue claims the carrier slot atomically, so two players can't both walk away with a relic, and the item is only created after the inventory-space check succeeds (with the orphan deleted if it doesn't). - The relic is taken away when its carrier leaves, so it can't stay in an inventory after the match. - The Ended and WaitingRoom event states are sent now, and the skill-ended view plugin is wired to the magic effect's timeout. - The hardcoded Devias gate is gone; leaving uses the map's configured safezone. Consistency: - The mini game entry refusals are localized PlayerMessage resources instead of hardcoded English strings (including the backtick typo). - Fixed copy-paste documentation in the chat command, the game server state and MiniGameContext.GetSpawnGate. - The chat command reports back when the event isn't configured, which also resolves its CS1998. - Collapsed the identical 383/384 cases in TalkNpcAction and indented the switch properly. - IllusionTempleTeam moved to the MiniGames namespace, matching its folder. - Removed the whitespace noise in MiniGameContext and Player. - GameContextTestHelper takes the maximum level as a parameter instead of changing it globally for every test. - Deduplicated the relic's group/number checks behind IsRelicDefinition. - The update plugin removes the obsolete 658-668 statue spawns, so existing databases don't keep the placeholders next to the new ones. - FinishesWhenTooFewPlayersRemainAsync asserts that the match actually ends, not just the precondition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Character class: The result screen showed the same wrong class for every player. A packet capture confirmed the server sends correct, distinct values, so the client decodes them differently than assumed: it reads the class line from the lower nibble (0 Dark Wizard, 1 Dark Knight, 2 Fairy Elf, 3 Magic Gladiator, 4 Dark Lord, 5 Summoner, 6 Rage Fighter) and the evolution step from the upper one, while the internal numbering packs both the other way around. Two players of different lines therefore collapsed onto the same entry whenever the sent values happened to share their lower nibble - both were shown as the Magic Gladiator line's master class. The conversion is inverted accordingly and verified on a live client. Uninitialized packet bytes: Each player entry carries three alignment bytes which are never written, and the pipe buffer isn't zeroed, so leftovers of previous packets went out on the wire. The buffer is cleared before writing now. Entry dialog couldn't be opened a second time: The player state is only reset back to EnteredWorld after the dialog has been shown, so a failure while querying the temple user counts left the player stuck in NpcDialogOpened - and opening the dialog requires exactly that transition. The reset moved into a finally block. The user count view plugin also missed the connection check every other view plugin has, which is one way that query could throw. Statue could stay locked for the rest of the match: Claiming the carrier slot before looking up the relic item meant an exception (e.g. a database without the item) left the slot claimed forever, and nobody could pick up the relic anymore. The claim is now released on every failure path, and a missing item definition is logged instead of thrown. Also reverted the WaitingRoom event state which was sent on entry: it was added on the assumption that the unused enum value belongs there, without evidence from the reference server, and it sent a new packet to the client before the player was even on the event map. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings the Illusion Temple mini game (PR #893 by @bulgarashi) up to date with master and fixes the findings from its review. Adapted to the mini game refactorings on master: - The context is created by MiniGameManager instead of GameContext. - The reward flow goes through MiniGameRewardService: it gained an optional reward filter, so a game can hand out its rewards in more than one step, and the winner checks are routed through two new overridable predicates. - The migration is renumbered behind the migrations master added in the meantime, and its model snapshot is refreshed accordingly. Review fixes: - The character class conversion for the result screen reported Duel Master, Lord Emperor and Fist Master with an evolution step the client doesn't know, because those three lines have no second class. Covered by a new test over every character class. - The "event ended" state is sent after the result has been shown, so it can't close the dialog whose Close button triggers the reward request. - The relic carrier is claimed atomically when picking the relic up from the ground, like it already was at the statue. - The granted experience is measured instead of recalculated, so the score board also reflects experience rates and the maximum level. - The enter handler refuses an inventory slot which isn't in the inventory, instead of wrapping it around into an unrelated one. - Skill-end notifications are subscribed after the effect was accepted. - Naming and member ordering, file names matching their types, missing plugin display resources, and a few stale comments. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYEj5xw6iAJKEoDX3hyMZh
Deploying openmudocs with
|
| Latest commit: |
92c2bce
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://d5bc0e22.openmudocs.pages.dev |
| Branch Preview URL: | https://claude-pr-893-review-19p1pr.openmudocs.pages.dev |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYEj5xw6iAJKEoDX3hyMZh
Resolves the conflicts with the Doppelganger event which landed on master: both events add their own enter-result conversion, initializer, plugin resources and update version, so both sides are kept and the illusion temple update moves to version 116, behind the doppelganger one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYEj5xw6iAJKEoDX3hyMZh
DeliveringTheRelicScoresAPointAsync failed intermittently with "An item with the same key has already been added. Key: IIllusionTempleStateViewPlugin". The illusion temple pushes its state to the players from two tasks: the cyclic update loop which runs while the match is on, and the immediate push after a point was scored. MiniGamePlayerRegistry.ForEachAsync fans the players out with WhenAll, so both reach InvokeViewPlugInAsync for the same player at the same time. On the first request for a view plugin, both missed the lookup and both added it, and one of them threw. The real container keeps its plugins in a ConcurrentDictionary and only reads from them at that point, so this is not a problem in the game itself - only the mock, which creates its plugins on demand from a plain dictionary, wasn't safe for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYEj5xw6iAJKEoDX3hyMZh
Both sides added an update version and an entry to the season 6 configuration initializer, so the illusion temple update moves to version 118, behind the item rule and dark horse ones, and its initializer runs before the item rules which master wants to run last. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYEj5xw6iAJKEoDX3hyMZh
The Azure build failure, and local verificationThe The temple pushes its state to the players from two tasks: the cyclic update loop that runs during a match, and the immediate push right after a point is scored. This is not a problem in the game itself. Evidence: before the fix the failure hit 2 of 9 full runs of Full local test runNow that I have an SDK, the whole suite has actually been executed against current master (merged in 0782fc2), rather than just compiled:
That includes the 37 Illusion Temple tests and the new What's still needed from a maintainerThe Master has also moved three times since this PR opened (Doppelganger, item rules, dark horse), and each time the update version and the season 6 initializer list collided. Resolved each time, most recently to Generated by Claude Code |
Correction: the
|
| PR | MUnique.OpenMU |
Merged |
|---|---|---|
| #989 Discord notification workflow | failure (build 4899, 15:22) | yes, 15:24 |
| #983 item rule flags | failure (build 4893, 13:24) | yes, 14:55 |
| #973 Doppelganger event | failure (build 4860) | yes |
| #974 (this one) | failure (builds 4894, 4903) | — |
Each of those was red before it merged, so this is a condition of the pipeline rather than anything the Illusion Temple work introduces. I'm standing down on it: I can't read the Azure log (the project needs authentication) and I can't re-run the check, so there is nothing further I can do here without a maintainer. If someone pastes the failing task's output I'll happily chase it, but I'm not going to keep pushing speculative changes at an error I can't see.
To be clear about what this does not excuse: the first Azure failure led me to a genuine bug, which is fixed in 3d6cb7d and stands on its own — MockViewPlugInContainer was doing a check-then-add into a plain Dictionary while the temple pushes state to a player from two tasks at once. I reproduced that locally (2 failures in 9 full runs), and after the fix 8 of 8 full runs and 25 of 25 focused runs were clean.
Everything I can verify is green on the current head (0782fc2d, master merged in at bb17089f):
dotnet build --configuration Release -p:ci=true— 0 errors- all nine test projects pass, 1070 in
MUnique.OpenMU.Testsalone - the GitHub Actions
buildcheck is green on every push - no merge conflict
Generated by Claude Code
The Raklion event took update version 118 and added its own plugin resources, so the illusion temple update moves to 119 and both resource entries are kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UYEj5xw6iAJKEoDX3hyMZh
Continues @bulgarashi's Illusion Temple work from #893, which has gone quiet. Master moved on quite a bit since that branch was last updated, so this brings it up to date and fixes the findings from the review on #893.
The author's commits are preserved: this branch is a merge of
feature/illusion-temple-mini-gameinto currentmaster, so the history and attribution stay intact. #893 can be closed in favour of this one, or this can be pushed to that branch instead if @bulgarashi would rather keep it — happy either way.Adapting to master
The mini game code was refactored on master since the branch was written:
MiniGameManager— the context factory moved out ofGameContext, so theIllusionTemplecase was added there instead.MiniGameRewardService— reward granting moved out ofMiniGameContext. The temple needs to grant its rewards in two steps (experience when the match ends, so it can be shown in the result packet; the rest only when the player claims it), soGiveRewardsAndGetBonusScoreAsyncgained an optional reward filter. Existing callers are unaffected.MiniGameContextgot two overridable predicates,IsWinnerandIsInWinningParty, which the reward service now uses. The default implementations are behaviour-preserving for every other mini game; I checked the rewritten conditions against the original boolean expressions forWinner,Loser,WinningPartyandWinnerOrInWinningParty.PlayerMapTransitions/spawn gate plug-ins — the branch had already been adapted to these by the author; onlyGetSpawnGatehad to be re-added toMiniGameContext.AddMiniGameMinimumPlayerCountwas renumbered behindAddNetworkObservation, which master added in the meantime, and its model snapshot was refreshed so the migration chain stays consistent.Review fixes
Character class on the result screen. The conversion packed the evolution step into the upper nibble as
internal % 4, which is right for the lines with three tiers but wrong for Magic Gladiator, Dark Lord and Rage Fighter: they have no second class, so their master classes sit at internal step 1 (Duel Master 13, Lord Emperor 17, Fist Master 25) and were sent with a step the client doesn't define. NewIllusionTempleCharacterClassTestscovers all 18 character classes."Event ended" ordering. The
Endedstate is now sent after the result has been shown. The packet's own documentation says the client closes the event interface on it, and the result dialog's Close button is what triggers the reward request in the first place.Relic carrier race on pickup. Picking the relic up off the ground claimed the carrier slot with a plain check-then-set, while the statue already used
Interlocked.CompareExchange. Both paths now claim it the same way.Reported experience. The score board's experience was recalculated from the reward definitions, which ignored the server's experience rates and the maximum level. It's now measured from the character's actual experience around the grant.
Enter handler bounds. An
ItemSlotbelow the inventory (i.e. an equipment slot) turned into a large index when cast tobyte, pointing at an unrelated slot. It's refused instead.Skill-end notification. Subscribed after the effect list accepted the effect, so an immediately replaced effect can't report itself as ended before it applied.
Smaller things. Field naming (
_camelCase), constant and static-field ordering, file names matching their type names (…Plugin.cs→…PlugIn.cs), the two sub-packet handlers that were missing their[Display]resources, an unused using, and several stale or copy-pasted comments.Deliberately not in this PR
IllusionTempleSkillRequest.TargetObjectIndexis a single byte. Every other client-to-server skill packet encodes the target as a two-byte big-endian id right after the skill id, and the packet's 8 bytes are exactlyheader + short skill + short targetwith nothing left for theDistancebyte the definition claims. If that's right, the handler reads only the high byte and the two targeted skills (Restraint, Weaken) can't resolve their target. The definition predates this work and I can't verify it without a client capture, so I left it alone rather than guess at a protocol change. Worth one capture of a Restraint cast — if it is a big-endian short, only the XML needs changing;UseSkillAsyncalready takes aushort.IllusionTempleHolyItemRelicsfield lengths. ItsNamefield has no declared length, so the generator emits both a fixed packetLengthand aGetRequiredSize. It works because the view plugin sizes the span exactly, but it should get<Length>10</Length>likePlayerResult.Namedid. Left out because I had no way to run the XSLT generation here, and hand-editing generated packet structs seemed worse than leaving a known nit.MiniGameOpeningStateRequestActionstill answers "event not implemented yet" for Illusion Temple in its fallback branch. That branch is only reached when the start plug-in is missing or no temple matches the player's level, but the message is stale now and could use a better one.Testing
No .NET SDK was available in my environment, so I could not build or run the tests — CI is the first real verification. The changes were made against the refactored APIs by reading, and every base-class member the context uses was cross-checked against
MiniGameContext. Please treat the CI result as the gate here rather than my word.🤖 Generated with Claude Code
https://claude.ai/code/session_01UYEj5xw6iAJKEoDX3hyMZh
Generated by Claude Code