Skip to content

Add the Doppelganger (Double Goer) event - #973

Merged
sven-n merged 18 commits into
MUnique:masterfrom
apraxico:feature/doppelganger-event
Sep 24, 2026
Merged

sven-n merged 18 commits into
MUnique:masterfrom
apraxico:feature/doppelganger-event

Conversation

@apraxico

Copy link
Copy Markdown
Contributor

Implements the Doppelganger (Double Goer) event of season 6. Part of #23.

Depends on #972: the monsters walk along a path of areas, and without the fix of the ScopedGridNetwork they get stuck next to the magic circle.

How the event works

A party enters through the NPC Lugard (Elvenland/Devias) with a Mirror of Dimensions or a Doppelganger Free Ticket. It plays in its own instance of one of the maps 65–68. Herds of monsters spawn at the start of a path and walk towards a magic circle; when three of them reach it, the defense fails. Ice walkers appear on the path and have to be killed within a minute, otherwise the following monsters get stronger. Killed butchers leave reward chests, of which only one can be opened. When the time is over, the remaining players succeed and a final reward chest appears.

What it adds

File Purpose
ServerToClientPackets.xml (+ generated code, docs, tests) Packets 0xBF 0x0E–0x14 of the event, and the values 16–19 of the mini game countdown (0x92). The result packet is 12 bytes long, because the client structure isn't packed.
MiniGames/Doppelganger/DoppelgangerContext.cs The run of the event: play info every second, herds and additional spawns, ice walker mission, goals, chests, individual failure (death, leaving the map).
MiniGames/Doppelganger/DoppelgangerMonsterIntelligence.cs Monsters walk from area to area towards the magic circle and only attack players in their range, so they don't leave the path. Unlike BasicMonsterIntelligence, it keeps running without observers.
MiniGames/Doppelganger/DoppelgangerEventDefinition*.cs, DoppelgangerPath*.cs, DoppelgangerHerdSize.cs, DoppelgangerMonsterSpawn.cs, DoppelgangerMonsterScaling.cs The configurable description of the event.
MiniGames/Doppelganger/DoppelgangerFeaturePlugIn.cs Holds the definition as plug-in configuration, like the event definition of Kanturu.
MiniGames/Doppelganger/IDoppelgangerEventViewPlugIn.cs, DoppelgangerState.cs, DoppelgangerResult.cs View interface and enums of the game logic.
RemoteView/MiniGames/DoppelgangerEventViewPlugIn.cs Sends the event packets.
PlayerActions/MiniGames/EnterDoppelgangerAction.cs, MessageHandler/MiniGames/DoppelgangerEnterRequestHandlerPlugIn.cs Entrance with a random map per party and the two tickets.
PlugIns/DoppelgangerRewardChestPlugIn.cs Opens a reward chest when a player talks to it (the client handles the chests as NPCs).
PlugIns/SignOfDimensionsStackedPlugIn.cs Transforms five signs of dimensions into a mirror of dimensions.
Events/DoppelgangerInitializer.cs, Events/DoppelgangerMonsters.cs, Updates/AddDoppelgangerDataUpdatePlugIn.cs (UpdateVersion 115) Data of new databases and the update of existing ones: definitions, monsters 529–539, chests, items, Lugard, gates and safezone. Idempotent, with tests.

Modified shared code

File Change
MiniGameManager.cs Creates the DoppelgangerContext.
ShowMiniGameEnterResultViewPlugIn.cs, Extensions.cs Handle the doppelganger instead of throwing an ArgumentOutOfRangeException.
UpdateMiniGameStateViewViewPlugIn.cs Countdowns of the doppelganger.
OpenNpcWindowPlugIn.cs Lugard's window gets the byte with the minutes until the entrance opens, set to 0. It was left uninitialized before.
Gates.cs The entrance gates 329–332 are reduced to their walkable part. With a non-walkable position, the player was warped to the safezone right after entering.
NpcInitialization.cs, EventTicketItems.cs, Doppelgaenger1-4.cs, DataInitializationBase.cs Lugard's window, the tickets and the sign, the safezone of the event maps, and the seeding of the configuration.

Design decisions

  • One instance per party, no schedule, so no start plug-in and no change to EnterMiniGameAction.
  • The definitions don't define a ticket item, because the event accepts two tickets. The EnterDoppelgangerAction checks and consumes them.
  • Monster multipliers are added to each spawned instance (AggregateType.Multiplicate) before it's initialized, so the definitions aren't affected.
  • Configuration: a new database gets the default definition as plug-in configuration. Existing databases fall back to the default until an administrator configures it, like Kanturu. The defaults are simple values, meant to be tuned per server.

Known limitations

  • The final reward chest drops jewels and feathers; excellent items aren't part of its default drop groups.

Verification

  • Clean build from a fresh worktree with the packet generator; the generated files are identical to the committed ones.
  • dotnet build -p:ci=true: no new warnings in the touched files. The only errors are the existing ones of Web/AdminPanel (LogFiles.razor) with SDK 10.0.103, which also occur on master.
  • Tests: MUnique.OpenMU.Tests 1019 passed, Persistence.Initialization.Tests 25 passed, Network.Packets.Tests 602 passed, PlugIns.Tests 41 passed.
  • Tested in game with the MuMain client: entrance, herds, goals and failure, ice walkers, chests, success and final chest, tickets.

🤖 Generated with Claude Code

apraxico and others added 18 commits September 23, 2026 21:47
Defines the doppelganger event packets as expected by the season 6 client:
enter result, monster position, state, ice walker state, play info (timer
and party positions), result and monster goal count.

The client structures are not packed, so the reward experience of the
result packet is aligned at offset 8 and the packet is 12 bytes long.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds IDoppelgangerEventViewPlugIn with its default implementation, which
sends the doppelganger packets (0xBF 0x0F-0x14). The play info packet has a
variable length, so it's written directly like the chain lightning packet.

The enter result of the doppelganger is now handled by
ShowMiniGameEnterResultViewPlugIn, instead of throwing an
ArgumentOutOfRangeException.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DoppelgangerContext runs the event on top of the mini game lifecycle:
- shows the event frame when the game starts and sends the remaining time
  and the path positions of the players every second,
- counts the monsters which reached the magic circle and ends the event when
  too many of them reached it,
- fails the event for a player which dies or leaves the map,
- sends the result to the remaining players when the game ends.

The path of each map consists of 23 areas, taken from the original season 6
server data. The countdowns of the mini game (0x92) got the values of the
doppelganger, and the MiniGameManager creates the new context.

The monster waves and the movement along the path follow in a later step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Lugard opens the doppelganger entrance window. The byte after the window
  (minutes until the entrance opens) is sent as 0, instead of leaving it
  uninitialized.
- The DoppelgangerEnterRequest (0xBF 0x0E) is handled. The first player of a
  party enters a random event map, the other members follow into the same
  instance. The Mirror of Dimensions (14,111) is consumed as ticket.
- Four mini game definitions, one per event map, with one instance per party.
- Players who die inside the event maps respawn at Elvenland.
- An update plug-in adds all of this to existing season 6 databases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Adds the monsters 529-539 with the base values of the original season 6
  server data. The class clones use the skills of their class.
- Herds spawn every 3 seconds at the start of the path. Their size grows
  with the game time and the number of players. Additional butchers and
  dark lord clones appear after 1, 4 and 7 minutes.
- DoppelgangerMonsterIntelligence lets the monsters walk from area to area
  towards the magic circle. They only attack players in their attack range,
  so they don't leave the path, and they keep walking when no player
  observes them. When a monster reaches the magic circle, it's removed and
  counted as goal.
- The ice walkers appear at a random position of the path after almost six
  minutes. They disappear when they aren't killed within a minute. The
  client shows their position and the countdown.
- The client shows the position of the most advanced monster.
- The update plug-in also adds the monsters to existing databases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The entrance gates of the event maps contained non-walkable coordinates,
up to 39% of the area on the fourth map. The position within a gate is
chosen randomly without checking the terrain, and a player on a
non-walkable coordinate gets warped to the safezone map. So it happened
that a player got warped out of the event right after entering, which
already consumed the ticket.

The gates are reduced to their walkable part, and the update plug-in
adjusts them in existing databases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- When a butcher dies, three interim reward chests appear around it. Only
  one of them can be opened by attacking it, the others disappear then.
  Unopened chests disappear after a minute.
- An opened interim chest contains either larvae (one for each player which
  started the event) or items. The drop generator of the event suppresses
  the item drop, when larvae came out of it.
- When the players successfully defended the magic circle, the final reward
  chest appears next to a remaining player.
- The chests (541, 542) are now destructibles with their own drop groups:
  jewels, and with a small chance a Loch's Feather or Crest of Monarch,
  similar to the original item bags. The update plug-in configures them in
  existing databases.
- Monsters which don't walk along the path can't reach the magic circle,
  so larvae next to it aren't counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client handles the reward chests (541, 542) as NPCs, so clicking on them
sends a talk request instead of an attack, and the server answered that
talking to them is not implemented.

The DoppelgangerRewardChestPlugIn now opens a chest when a player talks to
it. The client shows the opening as the death of the chest, then the items
drop or the larvae appear, and the chest is removed. Because the content is
decided when opening the chest, the special drop generator isn't required
anymore. The chests got a high health, so that attacks don't destroy them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…yers

The monsters get stronger depending on the highest player level (including
the master level) and the number of players which started the game, like in
the original event. The multipliers for the level, health, damage and defense
are taken from the original season 6 server data, in ranges of ten player
levels up to 800.

When the players fail to kill the ice walkers in time, the following herds
and additional monsters get twice the health, damage and defense.

The multipliers are added to the attributes of each monster instance before
it's initialized, so they don't affect the monster definitions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like the original server, the players get a notice when the ice walkers
appear, when all of them were killed and when they escaped, so that the
players know why the following monsters got stronger. The messages are
localizable resources of the PlayerMessage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…irrors

- The event accepts a Mirror of Dimensions (14,111) and a Doppelganger Free
  Ticket (13,125). MiniGameDefinition supports only one ticket item, so the
  definitions don't define one anymore, and the EnterDoppelgangerAction checks
  and consumes the ticket after a successful entrance. A free ticket loses one
  durability per use.
- The Sign of Dimensions (14,110) drops from monsters of level 32 and above
  with a chance of 0.1%. It stacks up to five pieces, which transform into a
  Mirror of Dimensions, like the Symbol of Kundun transforms into a Lost Map.
- The update plug-in adds the new items, the drop group to all maps and
  removes the ticket item from the definitions of existing databases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The plug-ins of the reward chests and the sign of dimensions use localized
  names and descriptions of the PlugInResources, like their siblings.
- The separator lines of the new PlayerMessage properties match the format of
  the resource code generator.
- An opened chest is removed after awaiting its opening animation, instead of
  a fire-and-forget task without cancellation.
- Removed a redundant assignment of the monster to its intelligence, which
  the constructor of the monster already does.
- The shown goal count doesn't exceed its maximum, when several monsters
  reach the magic circle at the same time.
- The default multipliers of the monsters end at a player level of 600. The
  entries above repeated the ones of 410 to 600, so monsters of higher level
  players got weaker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The run of the event is described by the DoppelgangerEventDefinition, which
is now the configuration of the DoppelgangerFeaturePlugIn, like the event
definition of Kanturu. It can therefore be adapted in the admin panel, e.g.
the monsters of the herds and their multipliers, the paths and the chests.

- The monsters are referenced by their definition instead of their number.
- The path areas have settable properties, so they can be edited.
- A new database gets the default configuration; for existing databases the
  context falls back to the default definition, until an administrator
  configures it.
- The default multipliers are simple values which grow linearly with the
  player level, meant to be adjusted by the server administrator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The packets of the event view, including the clamped path positions and
  the aligned reward experience, the enter result, the countdowns, the
  entrance window of Lugard and the handling of truncated enter requests.
- The path progress of the monster intelligence: the position only increases
  with the next areas, a walking monster reaches the magic circle once, and a
  stationary monster next to it doesn't.
- The ticket search: the mirror is preferred, a given slot is used and used
  up tickets are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comments describe the values as defaults which can be adjusted, and the
description of the reward experience of the result packet starts with what
it is, so the generated documentation reads well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The method which opens the entrance window of the doppelganger event is
placed after the static members (SA1204).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The timer callback of the monster intelligence awaits a Task instead of an
  async void method (S3168).
- The chain of conditions when opening a chest ends with an else (S126).
- The default monster multipliers are part of the event definition itself,
  instead of a partial class with only static members (S1118).
- The center of a path area is calculated by the intelligence, which is its
  only user, instead of a method which looks like a property (S4049).
- The maximum path position is a static property of the view interface
  instead of a constant (S2339).
- The magic circle callback is invoked explicitly (S4487).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parameter of the timer callback was named like a discard, so the task
was assigned to the parameter instead of being discarded (S1226, S1854).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@apraxico

Copy link
Copy Markdown
Contributor Author

The Codacy findings are addressed, except S1128 in DoppelgangerContext.cs, which looks like a false positive: removing using System.Collections.Concurrent; breaks the build (CS0246, ConcurrentDictionary<,> isn't covered by a global using).

The Azure build fails within 0 seconds, like on master since the merge of #910 and on #970, so it seems to be an issue of the build infrastructure.

@sven-n sven-n left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed and verified against current master (incl. #945, #971, #972): merges cleanly, builds with -p:ci=true, and all tests pass (OpenMU.Tests 1020, Persistence.Initialization.Tests 25, Network.Packets.Tests 602, PlugIns.Tests 41).

The context, intelligence, entrance, and data update all look solid. The concurrency handling (chest groups, goal count, per-player results) is careful, and the update plugin is idempotent.

One minor, non-blocking note: EnterDoppelgangerAction.TryEnterAsync checks player.CurrentMiniGame is DoppelgangerContext after the enter attempt. If a (modified) client sends the enter request while already inside an event, the enter fails but a ticket is still consumed. That only hurts the sender, but it could be tightened by checking before the attempt.

The Azure build failure is the known infrastructure issue (it also fails in 0s on other PRs).


Generated by Claude Code

@sven-n
sven-n merged commit 1e50df3 into MUnique:master Sep 24, 2026
0 of 2 checks passed
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.

2 participants