Skip to content

Add the Crywolf event - #1005

Open
apraxico wants to merge 25 commits into
MUnique:masterfrom
apraxico:feature/crywolf-event
Open

apraxico wants to merge 25 commits into
MUnique:masterfrom
apraxico:feature/crywolf-event

Conversation

@apraxico

@apraxico apraxico commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Part of #23.

Summary

This adds the Crywolf Fortress event. The players defend the statue of the holy wolf against the army of Balgass. Like Raklion, it runs on the existing map (34) as a continuous event, not as a mini game.

It follows the original game, based on the known server sources (IGCN, MuEmu, kessiler) and on what the current client (MuMain) expects. No server code was copied.

It contains the event itself, and the global benefits and penalties of its result on all game servers. The commits are split so that they can be reviewed one by one.

Run of the event

State Default duration What happens
None until the schedule Default schedule: wednesday and saturday at 20:30, in the time zone of the server.
Notify1 10 min Server wide notifications.
Notify2 5 min The war begins: the common monsters of the map are removed, its NPCs are hidden, the terrain changes and the client plays the intro.
Ready 150 s The army appears, but doesn't move. The elves can contract the altars.
Start 15 min The army advances. After 5 min, it attacks the statue; after 10 min, Balgass appears.
End 15 s Result, MvP ranks with experience, heroes, rewards.
EndCycle 10 s The army disappears, the common monsters return, the terrain and NPCs follow the result.

Altars:

  • An elf of level 260 or higher stands on an altar and sends the contract request. After 5 s, the contract is valid.
  • Each altar has 2 contracts per event; a failed attempt counts as well, like in the original. There is a cooldown of 10 s between them.
  • Moving, dying, leaving the map or disconnecting cancels the contract. A mounted elf can't contract an altar.
  • The client shows the state of each altar with an effect (21–24).
  • The statue's shield is the health of the elves of the contracted altars.

Army: the groups of the original game are spawn waves on the map.

  • The Dark Elf which leads a group marches along the waypoints of the original game to the goals of its group, and revives the dead members of its group one at a time.

  • The soldiers follow their leader.

  • The ballistas bombard fixed points of the fortress (BD 0C shows the arrow).

  • Balgass marches to the statue. He acts every second, and when his health is below 10,000, he tries to escape from his target, like in the original.

  • Skills: like the monster skill units of the original, the monsters use one of their skills instead of a normal attack with a chance of 75 %. The client shows them with packet 0x69, e.g. the ground smash of Balgass.

    Monster Skill Effects (chance per hit player)
    Balgass (349) 12, around him (6) push 50 %, stun 3 s 50 %, removes greater defense 50 %
    Balgass (349) 13, around him (6) push 50 %, stun 3 s 75 % (the original rolls 50 % twice)
    Dark Elf (340) 8, target stun 3 s 50 %
    Soram (341) 11, around it (6) push 50 %
    Balram (344) 9, target removes greater defense 50 %
    Death Spirit (345) 10, target mana −50 % 50 %, AG −50 % 50 %

    The skills are part of the configuration. When a Dark Elf revives a member of its group, the client shows it as its skill 6.

Result:

  • Victory: Balgass is killed.
  • Defeat: no altar is contracted anymore, or Balgass is still alive at the end of the battle.
  • MvP: the players get a score per killed monster, and the elves of the altars get a contractor score. The score gives a rank from D to S with experience, of which the players get 10 % after a defeat.
  • Rewards: after a victory, the contracted elves and the 5 heroes get a Jewel of Bless.

Occupation:

  • The result (peace or occupied) is kept until the next event. Like the MuCrywolf_DATA table of the original game, it's saved in its own table (CrywolfData) when the war starts and at the end of each battle, and loaded when the event starts up, so it survives a restart of the server.
  • If the server was stopped during the war, the war is reset at the start, and the result of the last battle stays, like in the original.
  • While the fortress isn't in peace, the server uses the terrain of the war or occupation, like the client does. They're the terrain files of the original server (Terrain35_WAR.att, Terrain35_OCCUPIED.att), stored as variants of the terrain of the map (see below).
  • The common NPCs are hidden with effect 27, and they can't be talked to.

One game server runs the event:

  • In the original game, only the castle siege server runs the event, and the other servers get its state through the DataServer. OpenMU has no castle siege server, and by default every game server hosts every map. So without a limit, each game server would run its own event, and all of them would save into the same table.
  • The configuration has a GameServerId (0 by default, the first server the data initialization creates). Only that game server runs the event.
  • The other game servers read CrywolfData every 5 s, which also holds whether and since when the war is running. If the game server which runs the event stops during the war, they consider it as ended after its longest possible duration. They show the occupation on their crywolf map (terrain, hidden NPCs, BD 00), and the benefits and penalties are the same everywhere. Their players get a message that the battle takes place on another server, and /crywolf tells the game master which server runs it.
  • I kept the setting in the configuration of the plugin, so it doesn't overlap with the shared maps of Cross-Server Shared Event Maps - Epic #818. When they exist, the event could follow the host of map 34 instead.
  • This also works when the game servers run in separate processes, because it only uses the database.

Benefits and penalties (all configurable, defaults of IGCN):

When Default Where
Chaos mix bonus peace, after the fortress has been defended +5 %, active The tickets of Devil Square, Blood Castle and Illusion Temple (BaseEventTicketCrafting), like the recipes which the client marks for the bonus in its Mix.bmd (option F: mixes 2, 8 and 37). Illusion Temple keeps the maximum rate of 80 % of IGCN. BD 09 sends the bonus to the client, which shows it for these recipes.
Monster health peace, after the fortress has been defended 90 %, active A multiplier of MaximumHealth for all monsters, except the summons and the monsters with a fixed health. When it decreases, the health of the living monsters is limited to it.
Jewel drops occupied 70 %, inactive Bless, Soul, Life, Creation, Chaos and Guardian (configurable), through the new plugin point IMonsterItemDropPlugIn.
Experience occupied 100 %, inactive A multiplier of ExperienceRate and MasterExperienceRate, like the happy hour, so that it covers the party experience, too.

During the war, neither the benefits nor the penalties apply, like in the original. The server logs which of them apply, whenever that changes.

Other:

  • Game masters can let the event proceed with /crywolf. It starts the event; during the battle, it lets Balgass appear first; otherwise it ends the current state.
  • The fortress got its warp entry (index 26), which the client expects.

Packets

These are the 0xBD packets.

  • Client to server: BD 00 (info), BD 03 (contract), BD 09 (chaos rate).
  • Server to client: BD 00, 02, 03, 04, 05, 07, 08, 09, 0C.

The client structures aren't packed, so the sizes include their padding.

Some details are required by the client:

  • BD 00 is only sent to players on map 34. The client loads the terrain of the occupation for its current map, whatever map that is.
  • The final occupation is set before End, otherwise the client shows no result.
  • BD 03 sends the key 317 + index of the altar. The client uses key - 317 as index into its five altar states without checking the range.
  • The result of BD 03 is a byte. A boolean field only sets its bit, so the other bits keep the content of the reused output buffer, but the client compares the whole byte.
  • BD 04 is sent at each 20 seconds of the remaining time. The client counts down the seconds by itself and assumes 60/40/20 s for three packets with the same minute.
  • An effect which an object already has is never sent again, because the client then removes all effects of the event from the object.

Changes outside of the event

  • PlayerLosesExperienceAfterDeathPlugIn: on map 34, players keep their experience when they die only during the battle, like in the original. Before, they never lost experience on this map. When the plugin is inactive, they lose it there like on other maps.
  • TalkNpcAction: the hidden NPCs of the fortress can't be talked to while it isn't in peace. The client hides them, and this also covers a modified client which would use their shops or the vault.
  • BaseEventTicketCrafting: adds the chaos mix bonus, limited by the new MaximumSuccessRate (80 % for Illusion Temple, like in IGCN). IGCN also limits the Blood Castle mix to 80 %, which leaves no bonus for it, but the client shows the bonus for it, so it's not limited here. Without the benefit, the rates don't change. The to-do comment in DevilSquareTicketCrafting about the Crywolf bonus is resolved.
  • AttackableNpcBase: calls the new plugin point IMonsterItemDropPlugIn for each dropped item.
  • Terrain variants: GameMapDefinition.TerrainVariants holds alternative terrains of a map (GameMapTerrainVariant: number, description, terrain data), and GameMapTerrain.LoadTerrainData replaces the whole terrain in place, so that references to its grids stay valid. It discards changes which were made at runtime on top of the terrain, which the crywolf map doesn't have.
  • MonsterPushExtensions: the push of Selupan (Raklion: fall and push of Selupan, skill multipliers, and the closed hatchery gate #994) and the one of the crywolf monsters were the same, so I moved it into extension methods of the monster, which both use. The behavior of Selupan doesn't change, and its tests use the extension now.
  • Stun effect: the skills use the stun effect (61) of the configuration. If it's missing, e.g. in a database which didn't get the update "Fix summoner curse skills" yet, the server logs a warning and the monsters don't stun.
  • Data: the statue and the altars became passive NPCs which are always on the map, and the fortress got its warp entry (see below).
  • The plugin is active by default, like the raklion event. So after an update, the event runs at its schedule. It can be deactivated or configured in the admin panel.

Effects on existing servers after the update

The update plugin is mandatory, like the other event data, and the plugin is active by default. So after the update:

  • the event runs on the game server 0 at its schedule (wednesday and saturday at 20:30, time zone of the server);
  • during the war, the common monsters of map 34 are removed and its NPCs are hidden;
  • the statue and the altars are always on the map, and warp entry 26 exists;
  • after the first defended battle, the benefits apply on all game servers: 90 % monster health and +5 % for the ticket mixes. Before a battle took place, nothing changes;
  • the penalties are inactive by default.

Everything can be changed or deactivated in the configuration of the plugin.

Modified clients

  • Contract requests are only accepted while the event runs, from a qualified elf (class and level), which stands on the altar and isn't mounted.
  • Invalid ids: an id which isn't an altar is ignored.
  • Repeated requests from the same elf don't change the contract.
  • One altar per elf: an elf can only be the contractor of the altar it stands on, so it can't hold two altars.
  • Packet lengths: all requests check their length.

Deviations from the original

  1. The altars can only be contracted while the event runs (Ready/Start). The original handler didn't check the state.
  2. The heroes (BD 08) are sent at the end of the battle, not at the end of the event. The client shows them in the result dialog, which it opens after an animation of about 400 frames. With the higher frame rate of the current client, they would arrive too late and the dialog stays empty. The reward of the heroes is still dropped at the end of the event.
  3. The benefits only apply after the fortress has been defended once. In the original, a new database is in peace, so the benefits applied right away. Here, existing servers don't get weaker monsters just by the update.
  4. On map 34, players keep their experience when they die only during the battle, like in the original. Before, they never lost experience on this map.
  5. The Dark Elves revive one member every 5 s (configurable), which the original did by its skill 6. They don't heal their group (skill 7), because the meaning of its value isn't clear from the sources.
  6. Not included:
    • the knock-back of the players at the start of the battle;
    • the knock-back immunity of the contracted elves;
    • the protection effects 66–70 of the statue, which the client doesn't show.
  7. Group 1 of the army is included, like in MuEmu. That makes 12 Dark Elves, which matches the "/12" of the client.
  8. If Balgass doesn't appear (by configuration) and altars stay contracted until the end, the battle is won.
  9. BD 04 sends correct hours and minutes; the original computed minutes / 24.
  10. The altar numbers in the messages are 1 to 5, and there are separate messages for the cooldown, a wrong position and a mount. The original showed a generic "level 260" message.

Data and update plugin

  • New monsters: 340 Dark Elf, 341 Soram, 344 Balram, 345 Death Spirit, 348 Tanker (ballista, doesn't move) and 349 Balgass, with the stats of the original.
  • Spawns: 76 OnceAtWaveStart spawns on map 34, with the group number of the original as wave number.
  • Statue (204) and altars (205–209): they are always on the map now, as PassiveNpc, so that the event can show their state. Before, they were OnceAtEventStart monsters, which never appeared.
  • Warp entry 26: Crywolf, 10,000 zen, level 190.
  • CrywolfData: a new aggregate root with the occupation state, whether and since when the war is running and the end of the last battle, with the migration AddCrywolfData (only creates the table) and entries in the BackupService. The row is created at the start of the first war, so no data update is needed for it.
  • Terrain variants of map 34: 1 (occupied) and 2 (war), from the embedded terrain files, with the migration AddGameMapTerrainVariants.
  • IMonsterItemDropPlugIn: a new plugin point in AttackableNpcBase.DropItemAsync, which can cancel the drop of an item. Until now, there was no way to filter the drops of monsters.
  • AddCrywolfEventUpdatePlugIn (UpdateVersion 122) applies the same to existing databases, and applying it twice changes nothing. 121 is the one of the imperial guardian event (Add the Imperial Guardian event (Gaion's event) #992).

Tests

  • CrywolfEventDefinitionTest: ranks, experience, schedule, scores, waypoints.
  • CrywolfAltarTest:
    • valid contract;
    • cancellation;
    • cooldown and exhausted altar;
    • requirements.
  • CrywolfContextTest:
    • defeat without a contracted altar;
    • contract rejected outside the event;
    • a contracted altar keeps the battle running and a repeated request changes nothing;
    • the GM command lets Balgass appear;
    • the occupation is saved in the database and loaded again;
    • the start of the war is saved, and an interrupted war is reset;
    • another game server takes over the war and doesn't start the event;
    • a game server which takes over the event resets the war of the previous one;
    • the other game servers end an overdue war;
    • the war uses the terrain of the war.
    • the benefits after a victory, and the penalties while occupied.
  • CrywolfPlugInTest: the jewel drop and experience penalties, the deactivation of the plugin and players which are already in the game.
  • GameMapTerrainTests: loading other terrain data replaces the whole terrain and keeps the grids.
  • CrywolfEventDefinitionTest: the default skills, and the point of a push or an escape, which stops at obstacles and safezones.
  • SelupanIntelligenceTest: unchanged, now through the shared push.
  • CrywolfRemoteViewTests: the exact bytes of the packets.
  • CrywolfDataTest: a new database, and the update applied twice, including the terrain variants: switching the terrain of map 34 to each variant and back results in exactly the terrain of the respective file.
  • The generated packet tests.

Up to date with master (including #992); built with -p:ci=true, and all four test suites pass.

Codacy reports some using directives as unnecessary, but they're needed (System.ComponentModel for CancelEventArgs, System.Collections.Concurrent for ConcurrentDictionary); the build fails without them. The Azure pipeline currently fails for all pull requests.

Tested in game with the Season 6 client (MuMain) and three game servers, in several complete events with defeats and victories:

  • intro;
  • contracts;
  • army, ballistas and the skills of the monsters (push, stun, the ground smash of Balgass);
  • Balgass;
  • result, ranks and heroes;
  • occupation, terrain and NPCs;
  • the benefits and the other game servers taking over the state.

The intro of the client got stuck at Balgass and played too fast at high frame rates. That's fixed in MuMain in sven-n/MuMain#655.

🤖 Generated with Claude Code

apraxico and others added 19 commits September 29, 2026 02:29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The players defend the statue of the holy wolf in the crywolf fortress
against the army of Balgass, like in the original game: the elves contract
the altars, the Dark Elves lead their groups along the waypoints of the
map, the ballistas bombard the fortress, and Balgass appears later. The
result is kept until the next event and saved in the configuration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The monsters of the army of Balgass and their spawns are added to the
crywolf map, with the groups of the original game as wave numbers. The
statue and the altars are always on the map now, as NPCs which can't be
attacked, so that the event can show their state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kets and the data

The result of the contract is a byte now instead of a boolean. A boolean
field only sets its bit, so the other bits keep the content of the reused
output buffer, but the client compares the whole byte.

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

Like the command of the original game, /crywolf starts the event, or ends
its current state. The guards of the fortress are hidden with the other
NPCs now, instead of being removed like the common monsters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client expects the fortress at the index 26 of the warp list, like the original game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
During the battle, /crywolf lets Balgass appear if he didn't yet, so that
the battle against him can be tested. Otherwise, the current state ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client shows the heroes in the result dialog, which it opens after an
animation of some frames. With the higher frame rate of the current client,
the heroes didn't arrive in time at the end of the event, like in the
original game, so the dialog stayed empty. Their reward is still dropped
at the end of the event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The death of Balgass is checked before the contracts, so that the battle
  is won when the last contract ends at the same time.
- The chat command is applied by the next step of the event instead of
  changing its state from another thread, and the altars are replaced as a
  whole list.
- The remaining time is sent at each 20 seconds of it, like the client
  expects, instead of 20 seconds after the last one, which added up, and
  it isn't sent twice when entering the map anymore.
- A repeated contract request of the same elf doesn't cancel it, a mounted
  elf can't contract an altar, and the messages explain why an altar can't
  be contracted.
- The leader of a group revives one member at a time.
- The hidden NPCs of an occupied fortress can't be talked to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like the original game (MuCrywolf_DATA), the occupation state of the
fortress is stored in the database (CrywolfData), instead of the
configuration of the plugin. It's loaded when the event initializes and
saved at the end of each battle, so it survives a restart of the server.

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

Like the castle siege server of the original game, only one game server
runs the event (CrywolfEventDefinition.GameServerId, 0 by default). The
other game servers take over its occupation state from the database
(CrywolfData, which now also holds whether the war is running), so that
the benefits and penalties are the same on all of them.

Benefits, while the fortress is in peace after it has been defended
(active by default, like IGCN):
- the chaos machine mixes of the tickets of Devil Square, Blood Castle
  and Illusion Temple get an additional success rate (5 %), with the
  maximum rates of the original game;
- the maximum health of the monsters is reduced (90 %).

Penalties, while the fortress is occupied (inactive by default):
- jewels drop less often (70 %), through the new plugin point
  IMonsterItemDropPlugIn;
- the experience of killed monsters is reduced (100 %).

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like the monster skill units of the original game, the monsters of the
army use one of their skills instead of a normal attack with a chance
of 75 %. The client shows them with packet 0x69, e.g. the ground smash
of Balgass. The skills push and stun the players, remove the greater
defense of the elves or decrease their mana and ability.

Balgass acts every second, and tries to escape from his target when his
health is below 10,000. The revival of a group member is shown as the
skill of its Dark Elf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The push of Selupan and the one of the crywolf monsters were the same.
They are extension methods of the monster now, and the escape of Balgass
uses the same calculation of the point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The game servers which don't run the event show the hidden NPCs as
  hidden to players which see them for the first time, too.
- A game server which takes over the event, e.g. after a change of the
  configuration, resets the war of the previous one like after a
  restart, instead of staying in it.
- A context whose initialization failed doesn't keep its handlers.

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

@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.

Thanks for this, it's a big and well documented piece of work. The structure follows the Raklion event, and the deviations from the original are clearly explained. I built the branch locally with -p:ci=true, and all 1154 tests of MUnique.OpenMU.Tests pass (I didn't run the other test projects).

Two things need to change before merging (see the inline comments):

  1. The army monsters don't act until a player observes them. The crywolf intelligence is only started by Monster.OnFirstObserverAdded, so groups, ballistas and Balgass which nobody has seen yet don't march, bombard or revive.
  2. The other game servers can get stuck in War. If the event server stops during the war, or GameServerId points to a server which doesn't exist, IsWarRunning is never reset. The safezone stays removed and the NPCs stay hidden on all other servers.

Smaller points (inline):

  • deactivating the plugin doesn't reset the multipliers, and players already online when it's activated don't get the experience multiplier;
  • please confirm that the terrain rectangles are an exact difference between the terrain files, because the peace terrain is applied at startup, too;
  • elves whose contract is still being attempted get the contractor score and the reward;
  • the mana and AG decrease isn't sent to the client;
  • a failed load can lead to a second CrywolfData row.

Behaviour changes worth noting for the release notes: on map 34, players now lose experience outside of the battle (and always, when the plugin is inactive). The event is also active by default after the mandatory update, and the benefits apply on all servers after the first defended battle.


Generated by Claude Code

{
monster.Initialize();
await map.AddAsync(monster).ConfigureAwait(false);
monster.OnSpawn();

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.

Army monsters don't act until a player observes them. Nothing here starts the CrywolfMonsterIntelligence. Monster only calls INpcIntelligence.Start() from OnFirstObserverAdded, and OnSpawn() just sets _isReadyToWalk. So a group, ballista or Balgass that no player has come near yet never starts its timer: it doesn't march, bombard or revive members. That contradicts the remark on CrywolfMonsterIntelligence.Pause() ("The army keeps marching when no player observes it").

Please start the intelligence explicitly after OnSpawn(), e.g. intelligence.Start() (it's already idempotent through ??=).


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7cd2928. SpawnMonsterAsync starts the intelligence right after OnSpawn(), so groups, ballistas and Balgass act without observers. Start() is idempotent, so the later call from OnFirstObserverAdded doesn't create a second timer.

Comment thread src/GameLogic/Crywolf/CrywolfContext.cs Outdated

this._dataId = data.Id;
this._hasBattleResult = data.LastBattleEnd.HasValue;
this.Occupation = data.IsWarRunning && !this._isEventServer

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.

The other game servers can get stuck in War. They only rely on IsWarRunning. If the event server stops during the war and isn't restarted, or GameServerId points to a server which doesn't exist (anymore), the flag is never reset. All other game servers then stay in War indefinitely:

  • the safezone of the fortress stays removed and some areas stay blocked;
  • the NPCs stay hidden and can't be talked to;
  • neither benefits nor penalties apply.

Could you store when the war started (e.g. WarStart in CrywolfData) and treat the war as ended after the longest possible event duration, falling back to IsOccupied?


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7cd2928. CrywolfData.WarStart is set when the war starts and cleared when it ends. The other game servers consider the war as ended once its longest possible duration (Notify2 + Ready + battle + End) has passed, and fall back to IsOccupied. Test: CrywolfContextTest.OverdueWarEndsOnTheOtherServersAsync.

/// <summary>
/// The multiplier of the maximum health of the monsters. Like the occupation state, it's the same for all game servers.
/// </summary>
private readonly SimpleElement _monsterHealthMultiplier = new(1.0f, AggregateType.Multiplicate);

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.

Deactivating the plugin doesn't undo its effects. When the plugin is deactivated at runtime, UpdateMultipliersAsync doesn't run anymore, but the elements stay attached with their last value:

  • monsters which respawn in place keep e.g. 90 % health;
  • players keep the experience multiplier until they relog.

The other direction has a gap, too: players who are already in the game when the plugin is activated never get the experience multiplier (see PlayerStateChangedAsync). Please reset the values to 1 when the plugin is deactivated, or attach the elements in a way which handles activation and deactivation.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7cd2928.

  • The plugin subscribes to PlugInManager.PlugInDeactivated and resets both multipliers to 1 there. The elements stay attached, but without an effect.
  • When it's activated again, the first tick of each game context attaches the multipliers to the players and monsters which exist already. A ConditionalWeakTable of the attribute systems prevents adding them twice.

Tests: CrywolfPlugInTest.DeactivationUndoesTheMultipliersAsync and PlayersInTheGameGetTheMultiplierAsync.

Comment thread src/GameLogic/Crywolf/CrywolfTerrain.cs Outdated
/// </summary>
/// <param name="terrain">The terrain of the crywolf map.</param>
/// <param name="isPeace">If set to <c>true</c>, the terrain of the peace is applied; otherwise, the one of the war and the occupation.</param>
public static void Apply(GameMapTerrain terrain, bool isPeace)

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.

This also runs with isPeace: true at startup. It sets the safezone bit and clears the blocked bit for every cell of the rectangles. That's only harmless if the rectangles are the exact difference between the peace and war/occupied terrain files. Some of them look suspicious, e.g. (92, 0, 157, 5) at the map edge. If any cell in them was blocked, or not a safezone, in the peace terrain, it gets changed at startup.

Can you confirm that they were computed cell by cell? A test which applies the peace terrain to the original terrain of map 34 and compares the result would settle it.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They were computed cell by cell from Terrain35.att against Terrain35_WAR.att and Terrain35_OCCUPIED.att; both differ from the peace terrain in exactly the same cells. But you're right that it's hard to verify. So in 7cd2928 I replaced them with terrain variants, as you suggested below, and CrywolfTerrain.cs is removed. CrywolfDataTest now switches the real terrain of map 34 to each variant and back, and compares the result with the respective file.

Comment thread src/GameLogic/Crywolf/CrywolfContext.cs Outdated
Player? contractor;
lock (altar)
{
contractor = altar.Contractor;

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.

altar.Contractor is also set while the altar is still Attempting. So an elf whose contract isn't valid yet gets the ContractorScore here, and after a victory the Jewel of Bless as well. Is that intended? Otherwise, please check altar.State == CrywolfAltarState.Contracted.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not intended, thanks. Fixed in 7cd2928: only an altar in CrywolfAltarState.Contracted gives its elf the contractor score and the reward.


if (IsChance(skill.ManaDecreaseChance))
{
Decrease(player, Stats.CurrentMana, skill.DecreasePercentage);

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.

Minor: Decrease sets CurrentMana / CurrentAbility directly, but the client isn't told. It shows the old value until the next regeneration update. Please send the update to the client, like the other places which change mana or AG.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The client is informed through the attribute change:

  • Setting the value raises AttributeValueChanged.
  • Player.OnAttributeValueChanged calls IUpdateStatsPlugIn.
  • UpdateStatsPlugIn then sends SendCurrentManaAndAbilityAsync for CurrentMana and CurrentAbility.

It's the same path as the mana toll in Player.AttackByAsync. I added a comment in Decrease to make that visible.

Comment thread src/GameLogic/Crywolf/CrywolfContext.cs Outdated
try
{
using var context = this._gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CrywolfData), false, this._gameContext.Configuration);
var data = this._dataId is { } id ? await context.GetByIdAsync<CrywolfData>(id).ConfigureAwait(false) : null;

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.

Minor: if LoadDataAsync failed (e.g. a temporary database error), _dataId is still null here and a second CrywolfData row is created. After that, FirstOrDefault() in LoadDataAsync picks one of the rows unpredictably. Loading the existing row here when _dataId is null would avoid it.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7cd2928: when the id is unknown, SaveDataAsync loads the existing row first, and only creates one if there is none.

sven-n commented Sep 29, 2026

Copy link
Copy Markdown
Member

One more change request for this PR: please replace CrywolfTerrain with terrain states of the map, i.e. the terrain files of the client for each state, instead of hand-derived rectangles.

The client loads its own terrain file for each occupation state. Keeping the exact differences in about 250 lines of rectangles is hard to verify (see my comment on CrywolfTerrain.Apply). Also, toggling bits on the current terrain can't restore the original value of a cell.

Suggested approach:

  1. Data model: add a collection of alternative terrains to GameMapDefinition, next to TerrainData, e.g. TerrainVariants. Each entry has a number or key and its own byte[] TerrainData. The data initialization and AddCrywolfEventUpdatePlugIn add the war and occupied terrain files of map 34 (plus the migration).
  2. Game logic: add something like GameMapTerrain.LoadTerrainData(byte[]? data), which reads the data into the existing AttributeMap, SafezoneMap and AIgrid arrays again. They're get-only and filled in place, so references to them (e.g. the AIgrid passed to the path finder) stay valid. Switching back to peace reloads the base TerrainData of the definition.
  3. Crywolf: ApplyOccupationAsync selects the variant for the occupation state, and CrywolfTerrain.cs can be removed. The client switches its terrain by itself on BD 00, so no additional packet is needed.
  4. Test: switching to war/occupied and back to peace results in exactly the terrain of the respective file.

One thing to keep in mind: reloading the terrain discards changes made at runtime on top of it (like the castle siege gates or mini game change events). The crywolf map has none, so a plain swap is fine here. A short remark in the code would help, so that nobody uses it for such maps without re-applying those changes.


Generated by Claude Code

- Terrain variants: the terrains of the occupation states are stored as
  variants of the terrain of the map (GameMapDefinition.TerrainVariants,
  migration AddGameMapTerrainVariants), taken from the terrain files of
  the original server. GameMapTerrain.LoadTerrainData replaces the whole
  terrain in place, so the event switches to the exact terrain of each
  state and back. CrywolfTerrain with its rectangles is removed.
- The intelligence of the army is started when it spawns, so that it
  acts without observers.
- CrywolfData stores the start of the war. The other game servers
  consider the war as ended after its longest possible duration, e.g.
  when the game server which runs it stopped during the war.
- Deactivating the plugin resets the multipliers. When it's activated,
  the monsters and players which exist already get them, too.
- Only a valid contract gives the contractor score and the reward.
- Saving doesn't create a second row after a failed load.

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

Copy link
Copy Markdown
Contributor Author

please replace CrywolfTerrain with terrain states of the map, i.e. the terrain files of the client for each state, instead of hand-derived rectangles.

Done in 7cd2928, following your suggestion:

  1. Data model: GameMapDefinition.TerrainVariants is a collection of GameMapTerrainVariant entries, each with Number, Description and TerrainData.
    • It comes with the migration AddGameMapTerrainVariants.
    • The data initialization and AddCrywolfEventUpdatePlugIn add Terrain35_OCCUPIED.att as variant 1 and Terrain35_WAR.att as variant 2. These files of the original server were already embedded resources.
    • The numbers are the occupation states, like the client selects its terrain.
  2. Game logic: GameMapTerrain.LoadTerrainData(byte[]?) clears and refills the existing grids in place and rebuilds the spawn points. The references to the grids, like the AIgrid of the path finder, stay valid. Its doc comment notes that changes made at runtime, like the castle siege gates, are discarded and have to be applied again.
  3. Crywolf: ApplyOccupationAsync loads the variant of the occupation state, or the normal terrain in peace, and logs a warning if a variant is missing. CrywolfTerrain.cs is removed.
  4. Tests:
    • GameMapTerrainTests.LoadTerrainDataReplacesTheWholeTerrain checks the switch and back, and that the grid instances stay the same.
    • CrywolfDataTest switches the real terrain of map 34 to each variant and back, and compares it with the respective file. It does this for a new database and after the update.
    • CrywolfContextTest.WarUsesTheTerrainOfTheWarAsync checks the switch during the event.

The other points of the review are answered inline. The behavior changes for the release notes are in the description: the section "Effects on existing servers after the update", and the experience loss on map 34, now also when the plugin is inactive.

🤖 Generated with Claude Code

apraxico and others added 5 commits September 29, 2026 17:40
The imperial guardian event (MUnique#992) takes 121. CreatedAt is the actual
date of the renumbering, like CODING_RULES.md asks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The jewels which drop less often while the fortress is occupied are part
of the configuration of the event now, instead of a hard-coded set. The
stun effect number stays a constant, documented why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	src/GameLogic/Properties/PlayerMessage.resx
#	src/GameLogic/Properties/PlugInResources.resx
#	src/GameServer/Properties/PlugInResources.resx
#	src/Persistence/Initialization/Updates/UpdateVersion.cs
#	src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs
@apraxico

Copy link
Copy Markdown
Contributor Author

I merged master including #992 (e4bc6f4). The conflicts were only additions next to each other: UpdateVersion, the initializer, and the resource files.

Other changes:

  • AddCrywolfEvent is 122 now, with the actual date as CreatedAt (372bcf6).
  • Following the new CODING_RULES.md:
    • The jewels of the penalty are part of the configuration of the event instead of a hard-coded set (26310b4). The stun effect number stays an internal const, with a remark why, like the other stuns.
    • Progress.md marks the crywolf packet handlers as done (f3f2883).

All four test suites pass.

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
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