Skip to content

Illusion Temple Mini Game - Event - #893

Open
bulgarashi wants to merge 38 commits into
MUnique:masterfrom
bulgarashi:feature/illusion-temple-mini-game
Open

Illusion Temple Mini Game - Event#893
bulgarashi wants to merge 38 commits into
MUnique:masterfrom
bulgarashi:feature/illusion-temple-mini-game

Conversation

@bulgarashi

Copy link
Copy Markdown
Contributor

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

  • Core game loop: statue → relic pickup → carry → deliver to own team's storage → score. Relic drops on death or on leaving the event and can be picked up by anyone. A team needs at least 2 points, and more than the opponent, to be declared the winner (a 1:0 finish is a draw, matching the original event).
  • Teams & state: players are split into two teams on start, each gets a live per-player state update (own team's positions, current relic carrier, remaining time) and an end-of-game result screen.
  • Rewards: experience is granted automatically to the winning team; other reward types (e.g. item drops) are only granted once the player explicitly claims them by closing the result dialog, matching the original "click Close to be compensated" flow.
  • Skill points & special skills: killing an enemy player (+1) or a roaming arena monster (+2) builds a skill-point pool (start 10, cap 90) that fuels four event-only skills (210 Order of Protection, 211 Restraint, 212 Tracking, 213 Weaken), each costing 10 points.
  • Admin-configurable minimum player count (EF migration included) instead of hardcoded per event.
  • Corrected packet layouts for 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.
  • Scheduling: starts every 2 hours (matching the official Webzen schedule), plus a GM chat command to force a start for testing.
  • Map data fixes: corrected statue/guardian/relic-box spawn positions on all six temple maps (verified against a working Season 6 Episode 3 server), added the 32 roaming arena-monster spawns (temple 6 has none), and fixed each temple's safezone map (previously defaulted to itself, so a player warped to "safezone" was sent right back into the arena).
  • Update plugin so existing databases get all of the above without a full reinstall (including a fix for a ticket-item number swap).
  • Test coverage for entrance timing, minimum-player handling, team assignment (even/odd counts), the statue/relic loop, scoring, and the reward flow.

Known issues

  • On the end-of-game result screen, the displayed character class is wrong when tested with a 1.04d client, because that client's class IDs don't match Webzen's numbering. Needs a client-version-aware class ID mapping in the result packet.

Zylkien and others added 17 commits August 7, 2026 21:33
Select and persist defending and attacking alliances using registration scores and tie order.

Synchronize join sides for players in the siege map, track battle participation, deliver configured online or pending rewards, award guild scores, and add the B4/B5 guild-list handlers with persistence and regression tests.

# Conflicts:
#	src/GameLogic/CastleSiege/CastleSiegeContext.cs
#	src/GameLogic/CastleSiege/CastleSiegePlugIn.cs
#	src/GameServer/Properties/PlugInResources.Designer.cs
Remove the redundant concurrent collections import and simplify the guild-selection test array declarations. This resolves the reported Codacy findings without changing runtime behavior.

# Conflicts:
#	src/GameLogic/CastleSiege/CastleSiegeContext.cs
Handle disconnected players safely when sending Castle Siege join-side notifications. Remove the unused Life Stone cleanup placeholder from the guild-selection phase and propagate Dapr runtime guild-resolution failures instead of treating them as missing guilds. Add regression coverage for notifications after disconnect.

# Conflicts:
#	src/GameLogic/CastleSiege/CastleSiegePlugIn.cs
Track exact participant time across delayed ticks, map transitions, and battle completion.

Persist cleared final guild selections when the game-server context is unavailable and centralize guild ordering for persistence and packet responses.

Integrate NPC map tracking and lifecycle synchronization with map-scoped join-side assignment and authorization.

Add regression coverage for elapsed participation, stale persisted guild lists, deterministic ordering, and defender gate operation.
Preserve Castle Siege join-side and participation state across transient player states and reconnects. Resolve battle winners and offline alliances correctly, deliver pending rewards atomically, batch guild score updates, and replace broad persistence queries with targeted lookups.

Synchronize runtime NPC and magic-effect access while keeping player map events independent of the state-machine lock. Add regression coverage for participant timing, reconnect overlap, reward delivery, and winning-alliance scoring.
- use a JSON-safe Dapr request model for guild score updates
- synchronize join-side effects only for tracked siege players
- preserve participation tracking when players leave the siege map
- prevent rewarding the previous castle owner before Crown capture
- notify clients about delivered rewards and batch pending reward writes
- discard permanently invalid pending rewards with a warning
- retain the in-memory alliance lookup fast path with persistence fallback
- prevent incomplete alliance data from consuming an attacking side
- add regression coverage for selection, tracking, and reward edge cases
Initialize and restore the defending alliance as the current battle owner so guild score rewards have a valid winner until a Crown capture replaces it.

Track fully loaded alliances and query persistence only for incomplete runtime caches, preserving cache-only repeated alliance lookups.

Add regression coverage for defender victories, restart restoration, alliance persistence fallback, and hot-path cache reuse.
…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>
Adds a documentation website which brings the existing guides together and
closes the biggest gap: the admin panel had no usage documentation at all.

Content:
* Getting Started - requirements/ports, docker, from source, connecting a
  game client, test accounts
* Deployment - the three variants, HTTPS, and the start parameters and
  environment variables of the startup project
* Admin Panel - one page per screen (setup, configuration updates, servers,
  accounts, online accounts, game configuration, plugins, chat commands,
  map editor, live map, logs and monitoring, users) plus a page with
  task-oriented how-tos
* Development - architecture, solution structure, the plugin system and
  contributing
* Reference - ports and a pointer to the generated packet documentation

Setup:
* Docusaurus 3 in docs-website, docs-only mode, offline search
  (@easyops-cn/docusaurus-search-local), light/dark theme
* The build fails on broken links, anchors and image references
* A GitHub Actions workflow builds the site for pull requests which touch it
* The deployment URL is configurable via DOCS_URL/DOCS_BASE_URL, so a
  Cloudflare Pages project or a fork doesn't need a code change
* README.md and QuickStart.md link to the site

The favicon is a placeholder, and screenshots for the admin panel pages are
still to be added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LidmXPYjGN64P1RgCiY7Ri

sven-n commented Aug 23, 2026

Copy link
Copy Markdown
Member

Review

Substantial, 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 action_required, not a failure). But there are several real bugs, one of which makes a headline feature of the PR a no-op.

Blocking

1. UpdateVersion.IllusionTempleData = 100 collides with AddCastleSiegeData = 100src/Persistence/Initialization/Updates/UpdateVersion.cs:530

IConfigurationUpdatePlugIn documents the version as "must be unique over all DataInitializationKeys", and it's used two ways, both of which break:

  • Key => (int)Version (UpdatePlugInBase.cs:16) keys the strategy provider. StrategyPlugInProvider.ActivatePlugIn (StrategyPlugInProvider.cs:79-86) keeps the first registration and logs "will not be effective" for the second, and AvailableStrategies returns only the effective ones — so either the castle siege update or the illusion temple update disappears from the update list entirely.
  • DetermineAvailableUpdatesAsync filters with !installedUpdates.Contains(up.Version) (DataUpdateService.cs:59). Any existing DB that installed update 100 already considers this update installed.

So "existing databases get all of the above without a full reinstall" doesn't actually happen. Should be 104. The XML doc is also missing its opening <summary> tag.

2. Re-entrant warp in OnObjectRemovedFromMapAsyncIllusionTempleContext.cs:700-732

GameMap.RemoveAsync awaits ObjectRemoved inline (GameMap.cs:177-180), and Player.WarpToAsync calls TryRemoveFromCurrentMapAsyncmap.RemoveAsync before PlaceAtGateAsync (Player.cs:880-889). So the handler's WarpToAsync(devias) runs nested inside whatever warp removed the player:

MovePlayersToSafezoneAsync → WarpToSafezoneAsync → RemoveAsync
   └─ OnObjectRemovedFromMapAsync → WarpToAsync(Devias) → PlaceAtGate(Devias), MapChange
   ← returns
PlaceAtGate(safezone gate), MapChange     ← player now on two maps, two MapChange packets

This fires on the normal end-of-game path, on ClaimRewardAsync, and on any other warp off the temple map. It also silently overrides the per-temple SafezoneMapNumber this PR just fixed. Suggestion: limit the handler to state cleanup (relic drop, _teams/_skillPoints removal, party kick) and leave the warping to ClaimRewardAsync / MovePlayersToSafezoneAsync.

3. Every failed entry is reported to the client as successRemoteView/MiniGames/Extensions.cs:84-87

public static byte ToIllusionTempleEnterResult(this EnterResult enterResult)
{
    return 0;
}

The parameter is ignored and the remark itself says failures should be 1. ShowMiniGameEnterResultViewPlugIn sends this for every refusal.

4. Item rewards are silently lost for most winnersIllusionTempleContext.cs:844-869

ClaimRewardAsync defers non-experience rewards to DoesRewardApply, which for WinnerOrInWinningParty requires Winner.Party == player.Party. But each departing winner goes through KickMySelfAsyncParty.ExitPartyAsync, which disposes the party once fewer than 2 members remain (Party.cs:417-436), nulling everyone's Party. After that only the single arbitrary player returned by Winner (a FirstOrDefault over a ConcurrentDictionary, :229) still qualifies. On a 2v2 the second winner gets nothing; on larger teams most of them get nothing. Experience is unaffected, since it's granted eagerly in GameEndedAsync.

5. The 20s preparation delay runs inside OnGameStartAsync:584-593

The tests drive OnGameStartAsync directly (IllusionTempleContextTest.cs:376-379), so ~15 tests each block for the full PreparationDuration, adding roughly five minutes to the suite. Make it a protected virtual TimeSpan the tests can shorten, or move the preparation phase out of the start hook.

Correctness

6. TeleportToStartCoordinatesAsync doesn't do what its comment says:919-933. cordinatesAlliedForces is a local copy of a readonly field, so += new Point(1, 0) is discarded every iteration and every member of a team is moved to the same tile. Either spread by index or drop the += and the // every player on differend point comment (also a typo). The two fields don't follow the _camelCase convention used elsewhere in the file, and duplicate the literals in GetSpawnGate.

7. player.Party = null;:548. The only other assignments to Party are inside Party itself; this bypasses KickMySelfAsync and leaves the player in the old party's _partyMembers array. EnterMiniGameAction already kicks non-party mini-game entrants properly (EnterMiniGameAction.cs:125-128), so the line is redundant at best.

8. Special skills hit your own team. UseRestraintAsync (:465) and UseWeakenAsync (:508) only exclude target == player — no team check, so you can freeze a team mate for 15s. UseSkillAsync also doesn't check IsEventRunning or that the caster is alive.

9. Reported experience is wrong for ExperiencePerRemainingSeconds:899. _grantedExperience sums RewardAmount, but GiveRewardAsync grants seconds * RewardAmount (MiniGameContext.cs:573-580); AddExperienceAsync also applies server rates. The seeded reward is plain Experience, so this only bites custom configs — but the score board will lie.

10. Relic race and item leak:272-304. _relicCarrier is checked and set with awaits in between (same in OnPlayerPickedUpItemAsync), so two players talking to the same statue can both walk away with a relic. Separately, the Item is created via CreateNew<Item>() before the inventory-space check, so a full inventory leaves an orphan entity in the persistence context.

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. IllusionTempleEventStatus.Ended and WaitingRoom are never sent — the client's event UI is never told the match is over. IIllusionTempleSkillEndedViewPlugin has a remote-view implementation but no caller, so the special-skill effects never notify the client when they expire.

13. Hardcoded Devias exit gate duplicated in ClaimRewardAsync (:860-868) and OnObjectRemovedFromMapAsync (:711-719), rather than the map's configured safezone — which is what the SafezoneMapNumber map fixes in this same PR exist for.

Consistency / style

  • EnterMiniGameAction.ShowRefusalAsync: hardcoded English strings where the codebase (and this PR) uses localized PlayerMessage resources; several needless $""; "Killers can`t enter!" uses a backtick. These messages now fire for all mini games, which is a behavior change beyond the PR's stated scope.
  • Copy-paste docs: StartIllusionTempleEventChatCommandPlugIn — "handles the startcc command"; IllusionTempleGameServerState — "for a chaos castle event"; MiniGameContext.GetSpawnGate — "Gets spown gate" with empty <param>/<returns>.
  • StartIllusionTempleEventChatCommandPlugIn.HandleCommandAsync is async with no await (CS1998) and gives the GM no feedback when the plugin isn't registered.
  • IllusionTempleStartPlugin uses literal [Display] strings while its siblings use PlugInResources; the two new sub-packet handlers lack [Display] entirely.
  • File/type name mismatches: IllusionTempleEnterHandlerPlugin.cs…PlugIn (its copyright header names a file that doesn't exist), IllusionTempleUserCountViewPlugin.cs…PlugIn, and IllusionTempleStartPlugin breaks the PlugIn suffix convention.
  • TalkNpcAction: cases 383/384 are byte-identical (collapse them), case bodies aren't indented, and both carry the same misleading comment. TalkToNpcTeamStorageAsync's body is indented one level too deep.
  • Member ordering: IllusionTempleScore.MinimumWinningScore sits between properties; SorcererSpiritPositions is declared after a method in all six map files.
  • IllusionTempleTeam.cs lives in MiniGames/ but declares namespace MUnique.OpenMU.GameLogic.
  • Unrelated whitespace noise in MiniGameContext.cs:37,91 (trailing whitespace + a stray blank line) and an extra blank line in Player.cs:1550.
  • GameContextTestHelper now sets MaximumLevel = 400 for every test using the helper — a global change tucked into a feature PR.
  • Group == 14 && Number == 64 is repeated three times in the context; extract constants.
  • The arena-monster stat table and map spawn coordinates are duplicated verbatim between NpcInitialization.CreateIllusionSorcererSpirits and IllusionTempleDataUpdatePlugIn. Worth a shared source given how easily these drift.
  • FixTicketItemNumbers changes Number but not the deterministic SetGuid(group, number) id — deliberate (changing it would break owned-item FKs), but it means an updated DB's "Illusion Sorcerer Covenant" keeps the id of (13, 50) while a fresh install gets (13, 51). That divergence should at least be documented, since later updates that resolve items by Guid will hit it.
  • Tests reach into private/protected members by reflection (pragmatic, given the constructor starts a real-time countdown, but brittle), and FinishesWhenTooFewPlayersRemainAsync asserts only PlayerCount < 2 — the precondition it just created, not that the event finished.

Questions

  • The old Cursed Statue / Captured Stone Statue spawns (NPCs 658–668) are deleted from all six maps. Fresh installs are fine, but existing DBs only get the replacement via the update plugin — which currently can't run (see Bug at player appearance - weird wing is shown #1). Intended?
  • The known issue about wrong class IDs on 1.04d clients is fine to defer, but IllusionTempleResult has no client-version handling at all right now, so it will be wrong for every client that doesn't use Webzen's numbering.

Generated by Claude Code

sven-n and others added 12 commits August 23, 2026 23:05
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>
The site duplicated what it was built from, so each topic now has exactly one
home:

* The website owns what a user, operator or new contributor reads: getting
  started, deployment, the admin panel, server features, architecture, the
  plugin system and contributing.
* The repository keeps the documentation which is derived from or tightly bound
  to the code: the generated packet descriptions, the game mechanics in docs/
  and the implementation notes in the projects' Readme files.

Moved to the website:

* docs/Bots.md becomes the new "Server features" section, cross-linked with the
  plugins, servers, logs and game configuration pages of the admin panel
* src/Web/AdminPanel/Readme.md and src/Web/Map/Readme.md - the latter was an
  outdated copy of the former; their "ideas for the future" are now the planned
  sections of the admin panel overview and the live map page

Replaced by a short pointer to the page which now holds the content:
QuickStart.md, the four deploy readmes, src/Startup/Readme.md,
src/PlugIns/Readme.md, docs/Bots.md, src/Web/AdminPanel/Readme.md and
src/Web/Map/Readme.md.

README.md keeps the project introduction and gains a documentation index; its
duplicated sections (used technologies, gameplay differences, deployment,
contribution rules) now live on the website and in CONTRIBUTING.md.
docs/Readme.md becomes the index of the code-bound documentation and explains
the split.

All relative links were verified to resolve, and the site build - which fails on
broken links - passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LidmXPYjGN64P1RgCiY7Ri
The packet analysis of the network analyzer is useful outside of the
WinForms tool, e.g. to show the traffic of a connected player in the
admin panel. Its platform neutral part is therefore extracted into a
net10.0 library, so it can be referenced by the servers and the web
projects.

* MUnique.OpenMU.Network.Analyzer (src/Network/Analyzer) is now a
  library which contains Packet, PacketAnalyzer, ICapturedConnection,
  SavedConnection and the mucap save/load extensions. It also carries
  the packet definition xml files, so they are copied to the output of
  every referencing project.
* MUnique.OpenMU.Network.Analyzer.WinForms
  (src/Network/Analyzer.WinForms) is the tool itself, with the forms,
  the proxy (LiveConnection, LiveConnectionListener) and the program
  entry point. Its assembly is renamed accordingly, because the library
  assembly keeps the previous name and lands in the same output folder.
* MUnique.OpenMU.Network.csproj removes the new folder from its default
  item globs, like it already does for Analyzer and Packets. Without
  that, the WinForms resx files would be compiled into the network
  library, which fails because they contain non-string resources.

There is no functional change: all files are moved as they are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pb82LmoaUVdZtBtQs7xrtA
The plugin implementation is a developer topic, so it doesn't belong on the
documentation website, which is aimed at users and server operators.

src/PlugIns/Readme.md is restored to its previous state (byte-identical to
master) and is the single source again; the website's development/plugins.md is
removed and the places which linked to it now link to the readme:

* the Development section of the sidebar and the architecture page
* the PlugIns row of the solution structure
* "Writing your own plugin" on the admin panel's Plugins page, which keeps
  covering what operators do with plugins: activate, deactivate, configure
* CONTRIBUTING.md and the docs index

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LidmXPYjGN64P1RgCiY7Ri
Resolves the conflict in docs/Bots.md, whose content moved to the documentation
website in this branch while master reworked the bot party behaviour: the file
keeps its pointer and the changes from master were applied to the page which
now holds the content.

Also follows the network analyzer split from master, which turned the analyzer
into a library plus a WinForms tool, in the solution structure page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LidmXPYjGN64P1RgCiY7Ri
…tup-a82apj

docs: add Docusaurus documentation site in docs-website
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>
…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>
bulgarashi and others added 9 commits August 24, 2026 21:51
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>
…ashi/OpenMU into feature/illusion-temple-mini-game
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants