Conversation
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
left a comment
There was a problem hiding this comment.
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):
- 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. - The other game servers can get stuck in
War. If the event server stops during the war, orGameServerIdpoints to a server which doesn't exist,IsWarRunningis 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
CrywolfDatarow.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
|
|
||
| this._dataId = data.Id; | ||
| this._hasBattleResult = data.LastBattleEnd.HasValue; | ||
| this.Occupation = data.IsWarRunning && !this._isEventServer |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Fixed in 7cd2928.
- The plugin subscribes to
PlugInManager.PlugInDeactivatedand 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
ConditionalWeakTableof the attribute systems prevents adding them twice.
Tests: CrywolfPlugInTest.DeactivationUndoesTheMultipliersAsync and PlayersInTheGameGetTheMultiplierAsync.
| /// </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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| Player? contractor; | ||
| lock (altar) | ||
| { | ||
| contractor = altar.Contractor; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
The client is informed through the attribute change:
- Setting the value raises
AttributeValueChanged. Player.OnAttributeValueChangedcallsIUpdateStatsPlugIn.UpdateStatsPlugInthen sendsSendCurrentManaAndAbilityAsyncforCurrentManaandCurrentAbility.
It's the same path as the mana toll in Player.AttackByAsync. I added a comment in Decrease to make that visible.
| 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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Fixed in 7cd2928: when the id is unknown, SaveDataAsync loads the existing row first, and only creates one if there is none.
|
One more change request for this PR: please replace 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 Suggested approach:
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>
Done in 7cd2928, following your suggestion:
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 |
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
|
I merged master including #992 (e4bc6f4). The conflicts were only additions next to each other: Other changes:
All four test suites pass. 🤖 Generated with Claude Code |
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
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 0Cshows 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.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:
Occupation:
MuCrywolf_DATAtable 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.Terrain35_WAR.att,Terrain35_OCCUPIED.att), stored as variants of the terrain of the map (see below).One game server runs the event:
GameServerId(0 by default, the first server the data initialization creates). Only that game server runs the event.CrywolfDataevery 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/crywolftells the game master which server runs it.Benefits and penalties (all configurable, defaults of IGCN):
BaseEventTicketCrafting), like the recipes which the client marks for the bonus in itsMix.bmd(optionF: mixes 2, 8 and 37). Illusion Temple keeps the maximum rate of 80 % of IGCN.BD 09sends the bonus to the client, which shows it for these recipes.MaximumHealthfor 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.IMonsterItemDropPlugIn.ExperienceRateandMasterExperienceRate, 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:
/crywolf. It starts the event; during the battle, it lets Balgass appear first; otherwise it ends the current state.Packets
These are the
0xBDpackets.BD 00(info),BD 03(contract),BD 09(chaos rate).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 00is only sent to players on map 34. The client loads the terrain of the occupation for its current map, whatever map that is.End, otherwise the client shows no result.BD 03sends the key317 + indexof the altar. The client useskey - 317as index into its five altar states without checking the range.BD 03is 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 04is 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.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 newMaximumSuccessRate(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 inDevilSquareTicketCraftingabout the Crywolf bonus is resolved.AttackableNpcBase: calls the new plugin pointIMonsterItemDropPlugInfor each dropped item.GameMapDefinition.TerrainVariantsholds alternative terrains of a map (GameMapTerrainVariant: number, description, terrain data), andGameMapTerrain.LoadTerrainDatareplaces 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.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:
Everything can be changed or deactivated in the configuration of the plugin.
Modified clients
Deviations from the original
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.BD 04sends correct hours and minutes; the original computedminutes / 24.Data and update plugin
OnceAtWaveStartspawns on map 34, with the group number of the original as wave number.PassiveNpc, so that the event can show their state. Before, they wereOnceAtEventStartmonsters, which never appeared.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 migrationAddCrywolfData(only creates the table) and entries in theBackupService. The row is created at the start of the first war, so no data update is needed for it.AddGameMapTerrainVariants.IMonsterItemDropPlugIn: a new plugin point inAttackableNpcBase.DropItemAsync, which can cancel the drop of an item. Until now, there was no way to filter the drops of monsters.AddCrywolfEventUpdatePlugIn(UpdateVersion122) 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:CrywolfContextTest: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.Up to date with master (including #992); built with
-p:ci=true, and all four test suites pass.Codacy reports some
usingdirectives as unnecessary, but they're needed (System.ComponentModelforCancelEventArgs,System.Collections.ConcurrentforConcurrentDictionary); 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:
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