Illusion Temple Mini Game - Event - #893
Conversation
ReviewSubstantial, well-documented feature work — the comments explain why (the client's C-struct padding, the hardcoded arena barriers, the score-table-before-warp ordering) rather than restating the code, and the packet-layout corrections look right. CI is green (Codacy is Blocking1.
So "existing databases get all of the above without a full reinstall" doesn't actually happen. Should be 2. Re-entrant warp in
This fires on the normal end-of-game path, on 3. Every failed entry is reported to the client as success — public static byte ToIllusionTempleEnterResult(this EnterResult enterResult)
{
return 0;
}The parameter is ignored and the remark itself says failures should be 4. Item rewards are silently lost for most winners —
5. The 20s preparation delay runs inside The tests drive Correctness6. 7. 8. Special skills hit your own team. 9. Reported experience is wrong for 10. Relic race and item leak — 11. The relic is never reclaimed at game end. A carrier who never delivers keeps "Cursed Castle Water" in his inventory permanently. 12. Dead paths. 13. Hardcoded Devias exit gate duplicated in Consistency / style
Questions
Generated by Claude Code |
…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>
ab213d5 to
129ec67
Compare
Follow-up review (head
|
| Previous finding | Fix |
|---|---|
UpdateVersion collision with AddCastleSiegeData |
Now IllusionTempleData = 112; I checked the whole enum for duplicate values, there are none. |
Re-entrant warp in OnObjectRemovedFromMapAsync |
Both WarpToAsync(Devias) calls removed, with a remark explaining why the handler must not warp. ClaimRewardAsync uses WarpToSafezoneAsync(), so the per-temple safezone fix actually takes effect. |
| Enter result always reported success | enterResult == EnterResult.Success ? 0 : 1. |
| Item rewards lost once the winning party is disposed | Solved properly with the new IsWinner / IsInWinningParty / IsWinnerOrInWinningParty virtuals. I checked the DoesRewardApply rewrite against the original boolean expressions for Winner, Loser, WinningParty and WinnerOrInWinningParty — all four are logically equivalent, so Blood Castle / Chaos Castle / Devil Square behaviour is unchanged. |
| 20s preparation delay in every test | PreparationDuration is settable and zeroed in CreateContextAsync. |
| All team members on one tile | i / 2 as the in-team index; the max offset (+4) stays inside both spawn rectangles. |
player.Party = null |
Removed. |
| Friendly fire on special skills | IsHostileTarget, plus IsEventRunning and IsAlive checks in UseSkillAsync. |
| Wrong reported experience | ExperiencePerRemainingSeconds now multiplied by the remaining seconds. |
| Relic race / orphan item entity | Interlocked.CompareExchange claims the carrier slot before the first await, with a try/catch that hands the claim back; the item is only created after the space check and DeleteAsync'd if it can't be added. |
| Relic kept forever | RemoveRelicFromInventoryAsync on claim, plus the existing drop path for the carrier. |
Ended / skill-ended never sent |
Both wired up. |
| Hardcoded Devias gates | Gone. |
Plus the localized PlayerMessage resources, the copy-paste doc strings, startcc→startit, GM feedback when the plugin is missing, the TalkNpcAction case collapse, the IllusionTempleTeam namespace, and MaximumLevel becoming an opt-in test parameter instead of a global default. FinishesWhenTooFewPlayersRemainAsync now asserts the state actually leaves Playing.
Two good catches of your own along the way: span.Clear() before writing the result packet (those three alignment bytes per entry were never written, so stale pipe-buffer content went out on the wire), and the try/finally around the entry dialog so a failure can't strand the player in NpcDialogOpened.
Open: character class conversion is wrong for three classes
RemoteView/MiniGames/Extensions.cs:99-106. The remark documents the upper nibble as 0 base, 2 second class, 3 master. That holds for the lines with three tiers, but Magic Gladiator, Dark Lord and Rage Fighter have only two, and their master classes sit at internal % 4 == 1 (all three are in CharacterClassNumber.AllMasters):
| Class | Internal | Produced | Expected |
|---|---|---|---|
| Grand Master | 3 | 0x30 |
0x30 ✅ |
| Blade Master | 7 | 0x31 |
0x31 ✅ |
| High Elf | 11 | 0x32 |
0x32 ✅ |
| Dimension Master | 23 | 0x35 |
0x35 ✅ |
| Duel Master | 13 | 0x13 |
0x33 ❌ |
| Lord Emperor | 17 | 0x14 |
0x34 ❌ |
| Fist Master | 25 | 0x16 |
0x36 ❌ |
Evolution nibble 1 isn't a value the scheme defines, so those three will still render wrong on the score board — the same bug the commit set out to fix, for the three classes that weren't in the test sample. var evolution = step == 1 ? 3 : step; covers it, or an explicit map. A unit test over all 18 CharacterClassNumber values would be worth adding while you're in there.
Open: ordering of Ended vs. the result packet
IllusionTempleContext.cs:958-962 sends IllusionTempleEventStatus.Ended to everyone before base.GameEndedAsync shows the score table. The packet documentation for IllusionTempleEventState says Ended means the client closes the event interface — if that also tears down the result dialog, the client would never send the reward request that ClaimRewardAsync depends on. If you've already seen the result screen work on a live client with this order, ignore this; otherwise sending Ended after the score table (or at exit) is the safer order.
New: the targeted special skills probably can't resolve their target
IllusionTempleSkillRequest is 8 bytes: header, ShortBigEndian SkillNumber at 4-5, then bytes 6 and 7, which the definition models as Byte TargetObjectIndex + Byte Distance. Every other client-to-server skill packet in the same file encodes the target as a two-byte big-endian id right after the skill id:
TargetedSkill:ShortBigEndian SkillId,ShortBigEndian TargetIdTargetedSkill075/TargetedSkill095:Byte SkillIndex,ShortBigEndian TargetIdAreaSkill:ShortBigEndian SkillId, …,ShortBigEndian ExtraTargetId
Eight bytes is exactly header + short skill + short target, with nothing left over for a distance byte. If the field really is a big-endian short, IllusionTempleSkillRequestHandlerPlugIn.cs:41 reads only its high byte, so CurrentMap.GetObject(targetObjectIndex) resolves the wrong object (or none, for any id below 256) — and Restraint (211) and Weaken (213), the only targeted skills, would silently fail through IsHostileTarget. UseSkillAsync already takes a ushort for this parameter, which suggests two bytes were the intent; the byte just widens silently at the call site.
The definition predates this PR (ClientToServer is untouched here, and its siblings still carry <SentWhen>?</SentWhen> placeholders), but this PR is its first consumer. One capture of a Restraint cast would settle it: if it's a big-endian short, change the XML field type and drop Distance — the handler then needs no change at all.
Checked and clean
- Spawn data consistency. Statue pool, both guardians and both storage boxes match exactly between all six map initializers and the update plugin's constants. The 32 arena-monster positions are byte-identical between the update plugin and maps 1-5, and map 6 correctly has none.
- No 0xBF sub-code collision. The three new handlers use
0x00,0x02,0x05; the only other group members are0x17(market place) and0x51(MU Helper), so none is silently dropped byStrategyPlugInProvider— the failure mode that bit the update version. - EF migration. One non-nullable
integerwithdefaultValue: 0, correctDown, snapshot matches, and it's still the newest migration by timestamp after the master merge. - Name-carrying packets don't need clearing.
ByteSpanExtensions.WriteStringcallstarget.Clear()before encoding, soIllusionTempleHolyItemRelicszero-fills its name bytes; the 10-character name cap also rules out an overflow there. - The remaining view plugins (
SkillEnded,SkillPointUpdate,SkillUsageResult,HolyItemRelics) all null-check the connection and follow the codebase write idiom. Packet sizes stay well under the C1 length limit at the 10-player cap (the result packet tops out at 207 bytes).
Minor / carried over
IllusionTempleHolyItemRelicsdeclares<Length>16</Length>but itsNamefield has none, so the generator emits both a fixedLengthand aGetRequiredSize(string), with the getter reading_data.Length - 6. It works because the plugin sizes the span at exactly 16, but adding<Length>10</Length>would make it explicit — the same fix applied toPlayerResult.Name. Its<SentWhen>?</SentWhen>/<CausedReaction>?.</CausedReaction>placeholders are fillable now that the packet has a real implementation.UpdateVersion.cs:567has trailing whitespace. (The othergit diff --checkhits are all in generated files.)OnPlayerPickedUpItemAsyncstill does a plain check-then-set on_relicCarrierwhileTalkToNpcStoneStatueAsyncnow usesCompareExchange. Only one relic is ever in play, so it's near-unreachable, but the asymmetry invites a future bug.NotifyWhenSkillEffectEndssubscribes beforeAddEffectAsync; if the effect list immediately replaces and disposes the effect, the client gets an instant "ended". Cosmetic.- Still open, all cosmetic:
IllusionTempleEnterHandlerPlugin.cs/IllusionTempleUserCountViewPlugin.csfilenames vs. their…PlugIntype names; no[Display]on the reward and skill request handlers;alliedForcesCoordinates/illusionForcesCoordinatesnot_camelCase, and duplicating the rectangles inGetSpawnGate;MinimumWinningScoredeclared between properties;SorcererSpiritPositionsafter a method in all six map files;WaitingRoomnever sent; the arena-monster stat table and spawn coordinates duplicated betweenNpcInitializationand the update plugin (currently in sync, as noted above);FixTicketItemNumbersleaving the item's deterministic Guid at(13, 50); no bounds check onrequest.ItemSlot - EquippableSlotsCountbefore the(byte)cast.
Generated by Claude Code
|
Thanks for this - the Illusion Temple event has been an open issue with zero progress since 2023, so a full implementation is a big contribution. I pulled Build & tests
A few things worth a look
What stood out positively
Nice work overall - happy to take another look once the above is addressed, particularly #4 since it's the only one that isn't purely cosmetic. |
Illusion Temple mini-game event
Implements the full Illusion Temple event: a team-based PvP mini game where two teams (Allied Forces / Illusion Forces) fight over a sacred relic held by a randomly-spawning Stone Statue.
What's included
IllusionTempleState/IllusionTempleResult(missing field, wrong offset, and a padding gap that the original client's C struct expects) — new packets for event state, holy-relic notification, skill usage/points, and reward requests.Known issues