From 789f28223c5924c20feda110ead40916bbb53156 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 00:52:50 -0300 Subject: [PATCH 01/23] feat(crywolf): add the packets of the crywolf event Co-Authored-By: Claude Opus 5.5 --- .../Packets/C1-BD-00-CrywolfInfo_by-server.md | 44 + ...-02-CrywolfStatueAndAltarInfo_by-server.md | 24 + ...1-BD-03-CrywolfContractResult_by-server.md | 21 + .../C1-BD-04-CrywolfLeftTime_by-server.md | 20 + ...-BD-05-CrywolfBossMonsterInfo_by-server.md | 20 + .../C1-BD-07-CrywolfPersonalRank_by-server.md | 32 + .../C1-BD-08-CrywolfHeroList_by-server.md | 33 + ...9-CrywolfBenefitPlusChaosRate_by-server.md | 19 + ...0C-CrywolfRegionMonsterAttack_by-server.md | 23 + docs/Packets/ServerToClient.md | 9 + .../ServerToClient/ConnectionExtensions.cs | 254 ++++ .../ServerToClient/ServerToClientPackets.cs | 1068 +++++++++++++++++ .../ServerToClient/ServerToClientPackets.xml | 393 ++++++ .../ServerToClientPacketsRef.cs | 975 +++++++++++++++ 14 files changed, 2935 insertions(+) create mode 100644 docs/Packets/C1-BD-00-CrywolfInfo_by-server.md create mode 100644 docs/Packets/C1-BD-02-CrywolfStatueAndAltarInfo_by-server.md create mode 100644 docs/Packets/C1-BD-03-CrywolfContractResult_by-server.md create mode 100644 docs/Packets/C1-BD-04-CrywolfLeftTime_by-server.md create mode 100644 docs/Packets/C1-BD-05-CrywolfBossMonsterInfo_by-server.md create mode 100644 docs/Packets/C1-BD-07-CrywolfPersonalRank_by-server.md create mode 100644 docs/Packets/C1-BD-08-CrywolfHeroList_by-server.md create mode 100644 docs/Packets/C1-BD-09-CrywolfBenefitPlusChaosRate_by-server.md create mode 100644 docs/Packets/C1-BD-0C-CrywolfRegionMonsterAttack_by-server.md diff --git a/docs/Packets/C1-BD-00-CrywolfInfo_by-server.md b/docs/Packets/C1-BD-00-CrywolfInfo_by-server.md new file mode 100644 index 000000000..42d7206a2 --- /dev/null +++ b/docs/Packets/C1-BD-00-CrywolfInfo_by-server.md @@ -0,0 +1,44 @@ +# C1 BD 00 - CrywolfInfo (by server) + +## Is sent when + +The state of the crywolf event changed, or the player entered the crywolf map. It should only be sent to players on the crywolf map, because the client loads the terrain of the occupation state for its current map. + +## Causes the following actions on the client side + +The client shows the event window in the states Ready, Start and End, plays the intro in the state Notify2, shows the result in the state End, and loads the terrain and light of the occupation state. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | 6 | Packet header - length of the packet | +| 2 | 1 | Byte | 0xBD | Packet header - packet type identifier | +| 3 | 1 | Byte | 0x00 | Packet header - sub packet type identifier | +| 4 | 1 | OccupationState | | Occupation; The occupation state. It must be Peace or Occupied in the state End, otherwise the client shows no result. | +| 5 | 1 | CrywolfState | | State; The state of the event. | + +### OccupationState Enum + +The occupation state of the crywolf fortress. + +| Value | Name | Description | +|-------|------|-------------| +| 0 | Peace | The fortress has been defended. The benefits apply. | +| 1 | Occupied | The fortress has been occupied by Balgass. The penalties apply. | +| 2 | War | The fortress is under attack. | + +### CrywolfState Enum + +The state of the crywolf event. + +| Value | Name | Description | +|-------|------|-------------| +| 0 | None | The event is not running. | +| 1 | Notify1 | The first notification of the upcoming event. | +| 2 | Notify2 | The second notification. The client plays the intro of the event. | +| 3 | Ready | The altars can be contracted and the monsters appeared. | +| 4 | Start | The monsters attack. | +| 5 | End | The event ended. The client shows the result. | +| 6 | EndCycle | The event is finished. | \ No newline at end of file diff --git a/docs/Packets/C1-BD-02-CrywolfStatueAndAltarInfo_by-server.md b/docs/Packets/C1-BD-02-CrywolfStatueAndAltarInfo_by-server.md new file mode 100644 index 000000000..fc5f2f0f4 --- /dev/null +++ b/docs/Packets/C1-BD-02-CrywolfStatueAndAltarInfo_by-server.md @@ -0,0 +1,24 @@ +# C1 BD 02 - CrywolfStatueAndAltarInfo (by server) + +## Is sent when + +Every two seconds while the altars can be contracted and during the battle. + +## Causes the following actions on the client side + +The client shows the shield of the statue and the states of the altars. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | 16 | Packet header - length of the packet | +| 2 | 1 | Byte | 0xBD | Packet header - packet type identifier | +| 3 | 1 | Byte | 0x02 | Packet header - sub packet type identifier | +| 4 | 4 | IntegerLittleEndian | | StatueHealthPercent; The shield of the statue in percent, from 0 to 100. | +| 8 | 1 | Byte | | Altar1State; The state of the first altar (205). The high nibble is the altar state (1 = contracted), the low nibble the number of remaining contracts. | +| 9 | 1 | Byte | | Altar2State; The state of the second altar (206), like Altar1State. | +| 10 | 1 | Byte | | Altar3State; The state of the third altar (207), like Altar1State. | +| 11 | 1 | Byte | | Altar4State; The state of the fourth altar (208), like Altar1State. | +| 12 | 1 | Byte | | Altar5State; The state of the fifth altar (209), like Altar1State. | \ No newline at end of file diff --git a/docs/Packets/C1-BD-03-CrywolfContractResult_by-server.md b/docs/Packets/C1-BD-03-CrywolfContractResult_by-server.md new file mode 100644 index 000000000..ec5a66192 --- /dev/null +++ b/docs/Packets/C1-BD-03-CrywolfContractResult_by-server.md @@ -0,0 +1,21 @@ +# C1 BD 03 - CrywolfContractResult (by server) + +## Is sent when + +The player requested to contract an altar. + +## Causes the following actions on the client side + +On success, the client shows that the player is a guardian of the altar, and lets the character pray. Otherwise, it shows why the contract failed. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | 8 | Packet header - length of the packet | +| 2 | 1 | Byte | 0xBD | Packet header - packet type identifier | +| 3 | 1 | Byte | 0x03 | Packet header - sub packet type identifier | +| 4 | 1 | Boolean | | Success; A value indicating whether the contract was accepted. | +| 5 | 1 | Byte | | AltarState; The new state of the altar, like in the CrywolfStatueAndAltarInfo. | +| 6 | 2 | ShortBigEndian | | AltarKey; The key of the altar. The client uses the key minus 317 as index into its five altar states without checking the range, so it must be 317 plus the index of the altar (0 to 4). | \ No newline at end of file diff --git a/docs/Packets/C1-BD-04-CrywolfLeftTime_by-server.md b/docs/Packets/C1-BD-04-CrywolfLeftTime_by-server.md new file mode 100644 index 000000000..4355dc157 --- /dev/null +++ b/docs/Packets/C1-BD-04-CrywolfLeftTime_by-server.md @@ -0,0 +1,20 @@ +# C1 BD 04 - CrywolfLeftTime (by server) + +## Is sent when + +Every 20 seconds during the battle. + +## Causes the following actions on the client side + +The client shows the remaining minutes and counts down the seconds by itself. It expects this packet every 20 seconds: when the minute is the same as before, it assumes that 40 or 20 seconds of the minute are left. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | 6 | Packet header - length of the packet | +| 2 | 1 | Byte | 0xBD | Packet header - packet type identifier | +| 3 | 1 | Byte | 0x04 | Packet header - sub packet type identifier | +| 4 | 1 | Byte | | Hours; The remaining hours. The client doesn't show them. | +| 5 | 1 | Byte | | Minutes; The remaining minutes. | \ No newline at end of file diff --git a/docs/Packets/C1-BD-05-CrywolfBossMonsterInfo_by-server.md b/docs/Packets/C1-BD-05-CrywolfBossMonsterInfo_by-server.md new file mode 100644 index 000000000..fe5c04777 --- /dev/null +++ b/docs/Packets/C1-BD-05-CrywolfBossMonsterInfo_by-server.md @@ -0,0 +1,20 @@ +# C1 BD 05 - CrywolfBossMonsterInfo (by server) + +## Is sent when + +Every five seconds during the battle. + +## Causes the following actions on the client side + +The client shows the health of Balgass and the number of Dark Elves. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | 12 | Packet header - length of the packet | +| 2 | 1 | Byte | 0xBD | Packet header - packet type identifier | +| 3 | 1 | Byte | 0x05 | Packet header - sub packet type identifier | +| 4 | 4 | IntegerLittleEndian | | BalgassHealthPercent; The health of Balgass in percent, or -1, when Balgass is not alive. | +| 8 | 1 | Byte | | DarkElfCount; The number of alive Dark Elves. The client shows it as count of 12. | \ No newline at end of file diff --git a/docs/Packets/C1-BD-07-CrywolfPersonalRank_by-server.md b/docs/Packets/C1-BD-07-CrywolfPersonalRank_by-server.md new file mode 100644 index 000000000..d16994eb6 --- /dev/null +++ b/docs/Packets/C1-BD-07-CrywolfPersonalRank_by-server.md @@ -0,0 +1,32 @@ +# C1 BD 07 - CrywolfPersonalRank (by server) + +## Is sent when + +The crywolf event ended, after the CrywolfInfo with the state End. + +## Causes the following actions on the client side + +The client shows the rank and the experience of the player in the result. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | 12 | Packet header - length of the packet | +| 2 | 1 | Byte | 0xBD | Packet header - packet type identifier | +| 3 | 1 | Byte | 0x07 | Packet header - sub packet type identifier | +| 4 | 1 | CrywolfRank | | Rank; The rank of the player. | +| 8 | 4 | IntegerLittleEndian | | Experience; The rewarded experience. The field is aligned to 4 bytes, because the client structure is not packed. | + +### CrywolfRank Enum + +The rank of a player in the crywolf event. + +| Value | Name | Description | +|-------|------|-------------| +| 0 | D | The rank D. | +| 1 | C | The rank C. | +| 2 | B | The rank B. | +| 3 | A | The rank A. | +| 4 | S | The rank S. | \ No newline at end of file diff --git a/docs/Packets/C1-BD-08-CrywolfHeroList_by-server.md b/docs/Packets/C1-BD-08-CrywolfHeroList_by-server.md new file mode 100644 index 000000000..b7a9cd337 --- /dev/null +++ b/docs/Packets/C1-BD-08-CrywolfHeroList_by-server.md @@ -0,0 +1,33 @@ +# C1 BD 08 - CrywolfHeroList (by server) + +## Is sent when + +The crywolf event ended, after the CrywolfInfo with the state End. + +## Causes the following actions on the client side + +The client shows the heroes with the highest scores in the result. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | | Packet header - length of the packet | +| 2 | 1 | Byte | 0xBD | Packet header - packet type identifier | +| 3 | 1 | Byte | 0x08 | Packet header - sub packet type identifier | +| 4 | 1 | Byte | | HeroCount; The number of heroes, at most 5. | +| 5 | Hero.Length * HeroCount | Array of Hero | | Heroes; The heroes. | + +### Hero Structure + +Contains a hero of the crywolf event. + +Length: 20 Bytes + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | | Rank; The place of the hero, from 0 (first) to 4. The client uses it as array index, so it must not be greater than 4. | +| 1 | 10 | String | | Name; The name of the hero. | +| 12 | 4 | IntegerLittleEndian | | Score; The score of the hero. The field is aligned to 4 bytes, because the client structure is not packed. | +| 16 | 1 | Byte | | CharacterClass; The number of the character class. | \ No newline at end of file diff --git a/docs/Packets/C1-BD-09-CrywolfBenefitPlusChaosRate_by-server.md b/docs/Packets/C1-BD-09-CrywolfBenefitPlusChaosRate_by-server.md new file mode 100644 index 000000000..c17473f03 --- /dev/null +++ b/docs/Packets/C1-BD-09-CrywolfBenefitPlusChaosRate_by-server.md @@ -0,0 +1,19 @@ +# C1 BD 09 - CrywolfBenefitPlusChaosRate (by server) + +## Is sent when + +The player opened a crafting dialog and requested the chaos rate benefit of the crywolf event. + +## Causes the following actions on the client side + +The client shows the additional success rate of the crafting. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | 5 | Packet header - length of the packet | +| 2 | 1 | Byte | 0xBD | Packet header - packet type identifier | +| 3 | 1 | Byte | 0x09 | Packet header - sub packet type identifier | +| 4 | 1 | Byte | | PlusChaosRate; The additional success rate in percent. | \ No newline at end of file diff --git a/docs/Packets/C1-BD-0C-CrywolfRegionMonsterAttack_by-server.md b/docs/Packets/C1-BD-0C-CrywolfRegionMonsterAttack_by-server.md new file mode 100644 index 000000000..63e85e57c --- /dev/null +++ b/docs/Packets/C1-BD-0C-CrywolfRegionMonsterAttack_by-server.md @@ -0,0 +1,23 @@ +# C1 BD 0C - CrywolfRegionMonsterAttack (by server) + +## Is sent when + +A ballista of the crywolf event attacks an area. + +## Causes the following actions on the client side + +The client shows an arrow which hits the target point. The direction of the arrow depends on the x coordinate of the ballista. + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | 10 | Packet header - length of the packet | +| 2 | 1 | Byte | 0xBD | Packet header - packet type identifier | +| 3 | 1 | Byte | 0x0C | Packet header - sub packet type identifier | +| 4 | 2 | ShortBigEndian | | MonsterNumber; The number of the monster. The client ignores it. | +| 6 | 1 | Byte | | SourceX; The x coordinate of the ballista. | +| 7 | 1 | Byte | | SourceY; The y coordinate of the ballista. The client ignores it. | +| 8 | 1 | Byte | | TargetX; The x coordinate of the target point. | +| 9 | 1 | Byte | | TargetY; The y coordinate of the target point. | \ No newline at end of file diff --git a/docs/Packets/ServerToClient.md b/docs/Packets/ServerToClient.md index 433a61409..652adf30f 100644 --- a/docs/Packets/ServerToClient.md +++ b/docs/Packets/ServerToClient.md @@ -190,6 +190,15 @@ * [C1 B9 05 - CastleSiegeHuntingZoneEnterResponse (by server)](C1-B9-05-CastleSiegeHuntingZoneEnterResponse_by-server.md) * [C1 BA - SkillStageUpdate (by server)](C1-BA-SkillStageUpdate_by-server.md) * [C2 BB - CastleSiegeMiniMapNpcPositions (by server)](C2-BB-CastleSiegeMiniMapNpcPositions_by-server.md) + * [C1 BD 00 - CrywolfInfo (by server)](C1-BD-00-CrywolfInfo_by-server.md) + * [C1 BD 02 - CrywolfStatueAndAltarInfo (by server)](C1-BD-02-CrywolfStatueAndAltarInfo_by-server.md) + * [C1 BD 03 - CrywolfContractResult (by server)](C1-BD-03-CrywolfContractResult_by-server.md) + * [C1 BD 04 - CrywolfLeftTime (by server)](C1-BD-04-CrywolfLeftTime_by-server.md) + * [C1 BD 05 - CrywolfBossMonsterInfo (by server)](C1-BD-05-CrywolfBossMonsterInfo_by-server.md) + * [C1 BD 07 - CrywolfPersonalRank (by server)](C1-BD-07-CrywolfPersonalRank_by-server.md) + * [C1 BD 08 - CrywolfHeroList (by server)](C1-BD-08-CrywolfHeroList_by-server.md) + * [C1 BD 09 - CrywolfBenefitPlusChaosRate (by server)](C1-BD-09-CrywolfBenefitPlusChaosRate_by-server.md) + * [C1 BD 0C - CrywolfRegionMonsterAttack (by server)](C1-BD-0C-CrywolfRegionMonsterAttack_by-server.md) * [C1 BF 00 - IllusionTempleEnterResult (by server)](C1-BF-00-IllusionTempleEnterResult_by-server.md) * [C1 BF 01 - IllusionTempleState (by server)](C1-BF-01-IllusionTempleState_by-server.md) * [C1 BF 02 - IllusionTempleSkillUsageResult (by server)](C1-BF-02-IllusionTempleSkillUsageResult_by-server.md) diff --git a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs index c75de1161..3a89a4ec2 100644 --- a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs +++ b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs @@ -5332,6 +5332,260 @@ int WritePacket() await connection.SendAsync(WritePacket).ConfigureAwait(false); } + /// + /// Sends a to this connection. + /// + /// The connection. + /// The occupation state. It must be Peace or Occupied in the state End, otherwise the client shows no result. + /// The state of the event. + /// + /// Is sent by the server when: The state of the crywolf event changed, or the player entered the crywolf map. It should only be sent to players on the crywolf map, because the client loads the terrain of the occupation state for its current map. + /// Causes reaction on client side: The client shows the event window in the states Ready, Start and End, plays the intro in the state Notify2, shows the result in the state End, and loads the terrain and light of the occupation state. + /// + public static async ValueTask SendCrywolfInfoAsync(this IConnection? connection, CrywolfInfo.OccupationState @occupation, CrywolfInfo.CrywolfState @state) + { + if (connection is null) + { + return; + } + + int WritePacket() + { + var length = CrywolfInfoRef.Length; + var packet = new CrywolfInfoRef(connection.Output.GetSpan(length)[..length]); + packet.Occupation = @occupation; + packet.State = @state; + + return packet.Header.Length; + } + + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } + + /// + /// Sends a to this connection. + /// + /// The connection. + /// The shield of the statue in percent, from 0 to 100. + /// The state of the first altar (205). The high nibble is the altar state (1 = contracted), the low nibble the number of remaining contracts. + /// The state of the second altar (206), like Altar1State. + /// The state of the third altar (207), like Altar1State. + /// The state of the fourth altar (208), like Altar1State. + /// The state of the fifth altar (209), like Altar1State. + /// + /// Is sent by the server when: Every two seconds while the altars can be contracted and during the battle. + /// Causes reaction on client side: The client shows the shield of the statue and the states of the altars. + /// + public static async ValueTask SendCrywolfStatueAndAltarInfoAsync(this IConnection? connection, uint @statueHealthPercent, byte @altar1State, byte @altar2State, byte @altar3State, byte @altar4State, byte @altar5State) + { + if (connection is null) + { + return; + } + + int WritePacket() + { + var length = CrywolfStatueAndAltarInfoRef.Length; + var packet = new CrywolfStatueAndAltarInfoRef(connection.Output.GetSpan(length)[..length]); + packet.StatueHealthPercent = @statueHealthPercent; + packet.Altar1State = @altar1State; + packet.Altar2State = @altar2State; + packet.Altar3State = @altar3State; + packet.Altar4State = @altar4State; + packet.Altar5State = @altar5State; + + return packet.Header.Length; + } + + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } + + /// + /// Sends a to this connection. + /// + /// The connection. + /// A value indicating whether the contract was accepted. + /// The new state of the altar, like in the CrywolfStatueAndAltarInfo. + /// The key of the altar. The client uses the key minus 317 as index into its five altar states without checking the range, so it must be 317 plus the index of the altar (0 to 4). + /// + /// Is sent by the server when: The player requested to contract an altar. + /// Causes reaction on client side: On success, the client shows that the player is a guardian of the altar, and lets the character pray. Otherwise, it shows why the contract failed. + /// + public static async ValueTask SendCrywolfContractResultAsync(this IConnection? connection, bool @success, byte @altarState, ushort @altarKey) + { + if (connection is null) + { + return; + } + + int WritePacket() + { + var length = CrywolfContractResultRef.Length; + var packet = new CrywolfContractResultRef(connection.Output.GetSpan(length)[..length]); + packet.Success = @success; + packet.AltarState = @altarState; + packet.AltarKey = @altarKey; + + return packet.Header.Length; + } + + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } + + /// + /// Sends a to this connection. + /// + /// The connection. + /// The remaining hours. The client doesn't show them. + /// The remaining minutes. + /// + /// Is sent by the server when: Every 20 seconds during the battle. + /// Causes reaction on client side: The client shows the remaining minutes and counts down the seconds by itself. It expects this packet every 20 seconds: when the minute is the same as before, it assumes that 40 or 20 seconds of the minute are left. + /// + public static async ValueTask SendCrywolfLeftTimeAsync(this IConnection? connection, byte @hours, byte @minutes) + { + if (connection is null) + { + return; + } + + int WritePacket() + { + var length = CrywolfLeftTimeRef.Length; + var packet = new CrywolfLeftTimeRef(connection.Output.GetSpan(length)[..length]); + packet.Hours = @hours; + packet.Minutes = @minutes; + + return packet.Header.Length; + } + + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } + + /// + /// Sends a to this connection. + /// + /// The connection. + /// The health of Balgass in percent, or -1, when Balgass is not alive. + /// The number of alive Dark Elves. The client shows it as count of 12. + /// + /// Is sent by the server when: Every five seconds during the battle. + /// Causes reaction on client side: The client shows the health of Balgass and the number of Dark Elves. + /// + public static async ValueTask SendCrywolfBossMonsterInfoAsync(this IConnection? connection, uint @balgassHealthPercent, byte @darkElfCount) + { + if (connection is null) + { + return; + } + + int WritePacket() + { + var length = CrywolfBossMonsterInfoRef.Length; + var packet = new CrywolfBossMonsterInfoRef(connection.Output.GetSpan(length)[..length]); + packet.BalgassHealthPercent = @balgassHealthPercent; + packet.DarkElfCount = @darkElfCount; + + return packet.Header.Length; + } + + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } + + /// + /// Sends a to this connection. + /// + /// The connection. + /// The rank of the player. + /// The rewarded experience. The field is aligned to 4 bytes, because the client structure is not packed. + /// + /// Is sent by the server when: The crywolf event ended, after the CrywolfInfo with the state End. + /// Causes reaction on client side: The client shows the rank and the experience of the player in the result. + /// + public static async ValueTask SendCrywolfPersonalRankAsync(this IConnection? connection, CrywolfPersonalRank.CrywolfRank @rank, uint @experience) + { + if (connection is null) + { + return; + } + + int WritePacket() + { + var length = CrywolfPersonalRankRef.Length; + var packet = new CrywolfPersonalRankRef(connection.Output.GetSpan(length)[..length]); + packet.Rank = @rank; + packet.Experience = @experience; + + return packet.Header.Length; + } + + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } + + /// + /// Sends a to this connection. + /// + /// The connection. + /// The additional success rate in percent. + /// + /// Is sent by the server when: The player opened a crafting dialog and requested the chaos rate benefit of the crywolf event. + /// Causes reaction on client side: The client shows the additional success rate of the crafting. + /// + public static async ValueTask SendCrywolfBenefitPlusChaosRateAsync(this IConnection? connection, byte @plusChaosRate) + { + if (connection is null) + { + return; + } + + int WritePacket() + { + var length = CrywolfBenefitPlusChaosRateRef.Length; + var packet = new CrywolfBenefitPlusChaosRateRef(connection.Output.GetSpan(length)[..length]); + packet.PlusChaosRate = @plusChaosRate; + + return packet.Header.Length; + } + + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } + + /// + /// Sends a to this connection. + /// + /// The connection. + /// The number of the monster. The client ignores it. + /// The x coordinate of the ballista. + /// The y coordinate of the ballista. The client ignores it. + /// The x coordinate of the target point. + /// The y coordinate of the target point. + /// + /// Is sent by the server when: A ballista of the crywolf event attacks an area. + /// Causes reaction on client side: The client shows an arrow which hits the target point. The direction of the arrow depends on the x coordinate of the ballista. + /// + public static async ValueTask SendCrywolfRegionMonsterAttackAsync(this IConnection? connection, ushort @monsterNumber, byte @sourceX, byte @sourceY, byte @targetX, byte @targetY) + { + if (connection is null) + { + return; + } + + int WritePacket() + { + var length = CrywolfRegionMonsterAttackRef.Length; + var packet = new CrywolfRegionMonsterAttackRef(connection.Output.GetSpan(length)[..length]); + packet.MonsterNumber = @monsterNumber; + packet.SourceX = @sourceX; + packet.SourceY = @sourceY; + packet.TargetX = @targetX; + packet.TargetY = @targetY; + + return packet.Header.Length; + } + + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } + /// /// Sends a to this connection. /// diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs index 4eb75f484..bba744e0c 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs @@ -25714,6 +25714,1074 @@ public byte Stage } +/// +/// Is sent by the server when: The state of the crywolf event changed, or the player entered the crywolf map. It should only be sent to players on the crywolf map, because the client loads the terrain of the occupation state for its current map. +/// Causes reaction on client side: The client shows the event window in the states Ready, Start and End, plays the intro in the state Notify2, shows the result in the state End, and loads the terrain and light of the occupation state. +/// +public readonly struct CrywolfInfo +{ + /// + /// The occupation state of the crywolf fortress. + /// + public enum OccupationState + { + /// + /// The fortress has been defended. The benefits apply. + /// + Peace = 0, + + /// + /// The fortress has been occupied by Balgass. The penalties apply. + /// + Occupied = 1, + + /// + /// The fortress is under attack. + /// + War = 2, + } + + /// + /// The state of the crywolf event. + /// + public enum CrywolfState + { + /// + /// The event is not running. + /// + None = 0, + + /// + /// The first notification of the upcoming event. + /// + Notify1 = 1, + + /// + /// The second notification. The client plays the intro of the event. + /// + Notify2 = 2, + + /// + /// The altars can be contracted and the monsters appeared. + /// + Ready = 3, + + /// + /// The monsters attack. + /// + Start = 4, + + /// + /// The event ended. The client shows the result. + /// + End = 5, + + /// + /// The event is finished. + /// + EndCycle = 6, + } + + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfInfo(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfInfo(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x00; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 6; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCode Header => new (this._data); + + /// + /// Gets or sets the occupation state. It must be Peace or Occupied in the state End, otherwise the client shows no result. + /// + public CrywolfInfo.OccupationState Occupation + { + get => (OccupationState)this._data.Span[4]; + set => this._data.Span[4] = (byte)value; + } + + /// + /// Gets or sets the state of the event. + /// + public CrywolfInfo.CrywolfState State + { + get => (CrywolfState)this._data.Span[5]; + set => this._data.Span[5] = (byte)value; + } + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfInfo(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(CrywolfInfo packet) => packet._data; +} + + +/// +/// Is sent by the server when: Every two seconds while the altars can be contracted and during the battle. +/// Causes reaction on client side: The client shows the shield of the statue and the states of the altars. +/// +public readonly struct CrywolfStatueAndAltarInfo +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfStatueAndAltarInfo(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfStatueAndAltarInfo(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x02; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 16; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCode Header => new (this._data); + + /// + /// Gets or sets the shield of the statue in percent, from 0 to 100. + /// + public uint StatueHealthPercent + { + get => ReadUInt32LittleEndian(this._data.Span[4..]); + set => WriteUInt32LittleEndian(this._data.Span[4..], value); + } + + /// + /// Gets or sets the state of the first altar (205). The high nibble is the altar state (1 = contracted), the low nibble the number of remaining contracts. + /// + public byte Altar1State + { + get => this._data.Span[8]; + set => this._data.Span[8] = value; + } + + /// + /// Gets or sets the state of the second altar (206), like Altar1State. + /// + public byte Altar2State + { + get => this._data.Span[9]; + set => this._data.Span[9] = value; + } + + /// + /// Gets or sets the state of the third altar (207), like Altar1State. + /// + public byte Altar3State + { + get => this._data.Span[10]; + set => this._data.Span[10] = value; + } + + /// + /// Gets or sets the state of the fourth altar (208), like Altar1State. + /// + public byte Altar4State + { + get => this._data.Span[11]; + set => this._data.Span[11] = value; + } + + /// + /// Gets or sets the state of the fifth altar (209), like Altar1State. + /// + public byte Altar5State + { + get => this._data.Span[12]; + set => this._data.Span[12] = value; + } + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfStatueAndAltarInfo(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(CrywolfStatueAndAltarInfo packet) => packet._data; +} + + +/// +/// Is sent by the server when: The player requested to contract an altar. +/// Causes reaction on client side: On success, the client shows that the player is a guardian of the altar, and lets the character pray. Otherwise, it shows why the contract failed. +/// +public readonly struct CrywolfContractResult +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfContractResult(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfContractResult(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x03; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 8; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCode Header => new (this._data); + + /// + /// Gets or sets a value indicating whether the contract was accepted. + /// + public bool Success + { + get => this._data.Span[4..].GetBoolean(); + set => this._data.Span[4..].SetBoolean(value); + } + + /// + /// Gets or sets the new state of the altar, like in the CrywolfStatueAndAltarInfo. + /// + public byte AltarState + { + get => this._data.Span[5]; + set => this._data.Span[5] = value; + } + + /// + /// Gets or sets the key of the altar. The client uses the key minus 317 as index into its five altar states without checking the range, so it must be 317 plus the index of the altar (0 to 4). + /// + public ushort AltarKey + { + get => ReadUInt16BigEndian(this._data.Span[6..]); + set => WriteUInt16BigEndian(this._data.Span[6..], value); + } + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfContractResult(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(CrywolfContractResult packet) => packet._data; +} + + +/// +/// Is sent by the server when: Every 20 seconds during the battle. +/// Causes reaction on client side: The client shows the remaining minutes and counts down the seconds by itself. It expects this packet every 20 seconds: when the minute is the same as before, it assumes that 40 or 20 seconds of the minute are left. +/// +public readonly struct CrywolfLeftTime +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfLeftTime(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfLeftTime(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x04; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 6; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCode Header => new (this._data); + + /// + /// Gets or sets the remaining hours. The client doesn't show them. + /// + public byte Hours + { + get => this._data.Span[4]; + set => this._data.Span[4] = value; + } + + /// + /// Gets or sets the remaining minutes. + /// + public byte Minutes + { + get => this._data.Span[5]; + set => this._data.Span[5] = value; + } + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfLeftTime(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(CrywolfLeftTime packet) => packet._data; +} + + +/// +/// Is sent by the server when: Every five seconds during the battle. +/// Causes reaction on client side: The client shows the health of Balgass and the number of Dark Elves. +/// +public readonly struct CrywolfBossMonsterInfo +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfBossMonsterInfo(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfBossMonsterInfo(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x05; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 12; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCode Header => new (this._data); + + /// + /// Gets or sets the health of Balgass in percent, or -1, when Balgass is not alive. + /// + public uint BalgassHealthPercent + { + get => ReadUInt32LittleEndian(this._data.Span[4..]); + set => WriteUInt32LittleEndian(this._data.Span[4..], value); + } + + /// + /// Gets or sets the number of alive Dark Elves. The client shows it as count of 12. + /// + public byte DarkElfCount + { + get => this._data.Span[8]; + set => this._data.Span[8] = value; + } + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfBossMonsterInfo(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(CrywolfBossMonsterInfo packet) => packet._data; +} + + +/// +/// Is sent by the server when: The crywolf event ended, after the CrywolfInfo with the state End. +/// Causes reaction on client side: The client shows the rank and the experience of the player in the result. +/// +public readonly struct CrywolfPersonalRank +{ + /// + /// The rank of a player in the crywolf event. + /// + public enum CrywolfRank + { + /// + /// The rank D. + /// + D = 0, + + /// + /// The rank C. + /// + C = 1, + + /// + /// The rank B. + /// + B = 2, + + /// + /// The rank A. + /// + A = 3, + + /// + /// The rank S. + /// + S = 4, + } + + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfPersonalRank(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfPersonalRank(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x07; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 12; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCode Header => new (this._data); + + /// + /// Gets or sets the rank of the player. + /// + public CrywolfPersonalRank.CrywolfRank Rank + { + get => (CrywolfRank)this._data.Span[4]; + set => this._data.Span[4] = (byte)value; + } + + /// + /// Gets or sets the rewarded experience. The field is aligned to 4 bytes, because the client structure is not packed. + /// + public uint Experience + { + get => ReadUInt32LittleEndian(this._data.Span[8..]); + set => WriteUInt32LittleEndian(this._data.Span[8..], value); + } + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfPersonalRank(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(CrywolfPersonalRank packet) => packet._data; +} + + +/// +/// Is sent by the server when: The crywolf event ended, after the CrywolfInfo with the state End. +/// Causes reaction on client side: The client shows the heroes with the highest scores in the result. +/// +public readonly struct CrywolfHeroList +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfHeroList(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfHeroList(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)data.Length; + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x08; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCode Header => new (this._data); + + /// + /// Gets or sets the number of heroes, at most 5. + /// + public byte HeroCount + { + get => this._data.Span[4]; + set => this._data.Span[4] = value; + } + + /// + /// Gets the of the specified index. + /// + public Hero this[int index] => new (this._data.Slice(5 + index * Hero.Length)); + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfHeroList(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(CrywolfHeroList packet) => packet._data; + + /// + /// Calculates the size of the packet for the specified count of . + /// + /// The count of from which the size will be calculated. + + public static int GetRequiredSize(int heroesCount) => heroesCount * Hero.Length + 5; + + +/// +/// Contains a hero of the crywolf event.. +/// +public readonly struct Hero +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public Hero(Memory data) + { + this._data = data; + } + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 20; + + /// + /// Gets or sets the place of the hero, from 0 (first) to 4. The client uses it as array index, so it must not be greater than 4. + /// + public byte Rank + { + get => this._data.Span[0]; + set => this._data.Span[0] = value; + } + + /// + /// Gets or sets the name of the hero. + /// + public string Name + { + get => this._data.Span.ExtractString(1, 10, System.Text.Encoding.UTF8); + set => this._data.Slice(1, 10).Span.WriteString(value, System.Text.Encoding.UTF8); + } + + /// + /// Gets or sets the score of the hero. The field is aligned to 4 bytes, because the client structure is not packed. + /// + public uint Score + { + get => ReadUInt32LittleEndian(this._data.Span[12..]); + set => WriteUInt32LittleEndian(this._data.Span[12..], value); + } + + /// + /// Gets or sets the number of the character class. + /// + public byte CharacterClass + { + get => this._data.Span[16]; + set => this._data.Span[16] = value; + } +} +} + + +/// +/// Is sent by the server when: The player opened a crafting dialog and requested the chaos rate benefit of the crywolf event. +/// Causes reaction on client side: The client shows the additional success rate of the crafting. +/// +public readonly struct CrywolfBenefitPlusChaosRate +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfBenefitPlusChaosRate(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfBenefitPlusChaosRate(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x09; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 5; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCode Header => new (this._data); + + /// + /// Gets or sets the additional success rate in percent. + /// + public byte PlusChaosRate + { + get => this._data.Span[4]; + set => this._data.Span[4] = value; + } + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfBenefitPlusChaosRate(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(CrywolfBenefitPlusChaosRate packet) => packet._data; +} + + +/// +/// Is sent by the server when: A ballista of the crywolf event attacks an area. +/// Causes reaction on client side: The client shows an arrow which hits the target point. The direction of the arrow depends on the x coordinate of the ballista. +/// +public readonly struct CrywolfRegionMonsterAttack +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfRegionMonsterAttack(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfRegionMonsterAttack(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x0C; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 10; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCode Header => new (this._data); + + /// + /// Gets or sets the number of the monster. The client ignores it. + /// + public ushort MonsterNumber + { + get => ReadUInt16BigEndian(this._data.Span[4..]); + set => WriteUInt16BigEndian(this._data.Span[4..], value); + } + + /// + /// Gets or sets the x coordinate of the ballista. + /// + public byte SourceX + { + get => this._data.Span[6]; + set => this._data.Span[6] = value; + } + + /// + /// Gets or sets the y coordinate of the ballista. The client ignores it. + /// + public byte SourceY + { + get => this._data.Span[7]; + set => this._data.Span[7] = value; + } + + /// + /// Gets or sets the x coordinate of the target point. + /// + public byte TargetX + { + get => this._data.Span[8]; + set => this._data.Span[8] = value; + } + + /// + /// Gets or sets the y coordinate of the target point. + /// + public byte TargetY + { + get => this._data.Span[9]; + set => this._data.Span[9] = value; + } + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfRegionMonsterAttack(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(CrywolfRegionMonsterAttack packet) => packet._data; +} + + /// /// Is sent by the server when: The player requested to enter the illusion temple event. /// Causes reaction on client side: The client shows the result. diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml index c3361acfe..edadc1773 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml @@ -9405,6 +9405,399 @@ + + C1HeaderWithSubCode + BD + 00 + CrywolfInfo + 6 + ServerToClient + The state of the crywolf event changed, or the player entered the crywolf map. It should only be sent to players on the crywolf map, because the client loads the terrain of the occupation state for its current map. + The client shows the event window in the states Ready, Start and End, plays the intro in the state Notify2, shows the result in the state End, and loads the terrain and light of the occupation state. + + + 4 + Enum + OccupationState + Occupation + The occupation state. It must be Peace or Occupied in the state End, otherwise the client shows no result. + + + 5 + Enum + CrywolfState + State + The state of the event. + + + + + OccupationState + The occupation state of the crywolf fortress. + + + Peace + The fortress has been defended. The benefits apply. + 0 + + + Occupied + The fortress has been occupied by Balgass. The penalties apply. + 1 + + + War + The fortress is under attack. + 2 + + + + + CrywolfState + The state of the crywolf event. + + + None + The event is not running. + 0 + + + Notify1 + The first notification of the upcoming event. + 1 + + + Notify2 + The second notification. The client plays the intro of the event. + 2 + + + Ready + The altars can be contracted and the monsters appeared. + 3 + + + Start + The monsters attack. + 4 + + + End + The event ended. The client shows the result. + 5 + + + EndCycle + The event is finished. + 6 + + + + + + + C1HeaderWithSubCode + BD + 02 + CrywolfStatueAndAltarInfo + 16 + ServerToClient + Every two seconds while the altars can be contracted and during the battle. + The client shows the shield of the statue and the states of the altars. + + + 4 + IntegerLittleEndian + StatueHealthPercent + The shield of the statue in percent, from 0 to 100. + + + 8 + Byte + Altar1State + The state of the first altar (205). The high nibble is the altar state (1 = contracted), the low nibble the number of remaining contracts. + + + 9 + Byte + Altar2State + The state of the second altar (206), like Altar1State. + + + 10 + Byte + Altar3State + The state of the third altar (207), like Altar1State. + + + 11 + Byte + Altar4State + The state of the fourth altar (208), like Altar1State. + + + 12 + Byte + Altar5State + The state of the fifth altar (209), like Altar1State. + + + + + C1HeaderWithSubCode + BD + 03 + CrywolfContractResult + 8 + ServerToClient + The player requested to contract an altar. + On success, the client shows that the player is a guardian of the altar, and lets the character pray. Otherwise, it shows why the contract failed. + + + 4 + Boolean + Success + A value indicating whether the contract was accepted. + + + 5 + Byte + AltarState + The new state of the altar, like in the CrywolfStatueAndAltarInfo. + + + 6 + ShortBigEndian + AltarKey + The key of the altar. The client uses the key minus 317 as index into its five altar states without checking the range, so it must be 317 plus the index of the altar (0 to 4). + + + + + C1HeaderWithSubCode + BD + 04 + CrywolfLeftTime + 6 + ServerToClient + Every 20 seconds during the battle. + The client shows the remaining minutes and counts down the seconds by itself. It expects this packet every 20 seconds: when the minute is the same as before, it assumes that 40 or 20 seconds of the minute are left. + + + 4 + Byte + Hours + The remaining hours. The client doesn't show them. + + + 5 + Byte + Minutes + The remaining minutes. + + + + + C1HeaderWithSubCode + BD + 05 + CrywolfBossMonsterInfo + 12 + ServerToClient + Every five seconds during the battle. + The client shows the health of Balgass and the number of Dark Elves. + + + 4 + IntegerLittleEndian + BalgassHealthPercent + The health of Balgass in percent, or -1, when Balgass is not alive. + + + 8 + Byte + DarkElfCount + The number of alive Dark Elves. The client shows it as count of 12. + + + + + C1HeaderWithSubCode + BD + 07 + CrywolfPersonalRank + 12 + ServerToClient + The crywolf event ended, after the CrywolfInfo with the state End. + The client shows the rank and the experience of the player in the result. + + + 4 + Enum + CrywolfRank + Rank + The rank of the player. + + + 8 + IntegerLittleEndian + Experience + The rewarded experience. The field is aligned to 4 bytes, because the client structure is not packed. + + + + + CrywolfRank + The rank of a player in the crywolf event. + + + D + The rank D. + 0 + + + C + The rank C. + 1 + + + B + The rank B. + 2 + + + A + The rank A. + 3 + + + S + The rank S. + 4 + + + + + + + C1HeaderWithSubCode + BD + 08 + CrywolfHeroList + ServerToClient + The crywolf event ended, after the CrywolfInfo with the state End. + The client shows the heroes with the highest scores in the result. + + + 4 + Byte + HeroCount + The number of heroes, at most 5. + + + 5 + Structure[] + Hero + Heroes + The heroes. + HeroCount + + + + + Hero + Contains a hero of the crywolf event. + 20 + + + 0 + Byte + Rank + The place of the hero, from 0 (first) to 4. The client uses it as array index, so it must not be greater than 4. + + + 1 + String + Name + The name of the hero. + 10 + + + 12 + IntegerLittleEndian + Score + The score of the hero. The field is aligned to 4 bytes, because the client structure is not packed. + + + 16 + Byte + CharacterClass + The number of the character class. + + + + + + + C1HeaderWithSubCode + BD + 09 + CrywolfBenefitPlusChaosRate + 5 + ServerToClient + The player opened a crafting dialog and requested the chaos rate benefit of the crywolf event. + The client shows the additional success rate of the crafting. + + + 4 + Byte + PlusChaosRate + The additional success rate in percent. + + + + + C1HeaderWithSubCode + BD + 0C + CrywolfRegionMonsterAttack + 10 + ServerToClient + A ballista of the crywolf event attacks an area. + The client shows an arrow which hits the target point. The direction of the arrow depends on the x coordinate of the ballista. + + + 4 + ShortBigEndian + MonsterNumber + The number of the monster. The client ignores it. + + + 6 + Byte + SourceX + The x coordinate of the ballista. + + + 7 + Byte + SourceY + The y coordinate of the ballista. The client ignores it. + + + 8 + Byte + TargetX + The x coordinate of the target point. + + + 9 + Byte + TargetY + The y coordinate of the target point. + + + C1HeaderWithSubCode BF diff --git a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs index 7756e4137..1645303c2 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs @@ -24380,6 +24380,981 @@ public byte Stage } +/// +/// Is sent by the server when: The state of the crywolf event changed, or the player entered the crywolf map. It should only be sent to players on the crywolf map, because the client loads the terrain of the occupation state for its current map. +/// Causes reaction on client side: The client shows the event window in the states Ready, Start and End, plays the intro in the state Notify2, shows the result in the state End, and loads the terrain and light of the occupation state. +/// +public readonly ref struct CrywolfInfoRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfInfoRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfInfoRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x00; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 6; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCodeRef Header => new (this._data); + + /// + /// Gets or sets the occupation state. It must be Peace or Occupied in the state End, otherwise the client shows no result. + /// + public CrywolfInfo.OccupationState Occupation + { + get => (CrywolfInfo.OccupationState)this._data[4]; + set => this._data[4] = (byte)value; + } + + /// + /// Gets or sets the state of the event. + /// + public CrywolfInfo.CrywolfState State + { + get => (CrywolfInfo.CrywolfState)this._data[5]; + set => this._data[5] = (byte)value; + } + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfInfoRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(CrywolfInfoRef packet) => packet._data; +} + + +/// +/// Is sent by the server when: Every two seconds while the altars can be contracted and during the battle. +/// Causes reaction on client side: The client shows the shield of the statue and the states of the altars. +/// +public readonly ref struct CrywolfStatueAndAltarInfoRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfStatueAndAltarInfoRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfStatueAndAltarInfoRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x02; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 16; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCodeRef Header => new (this._data); + + /// + /// Gets or sets the shield of the statue in percent, from 0 to 100. + /// + public uint StatueHealthPercent + { + get => ReadUInt32LittleEndian(this._data[4..]); + set => WriteUInt32LittleEndian(this._data[4..], value); + } + + /// + /// Gets or sets the state of the first altar (205). The high nibble is the altar state (1 = contracted), the low nibble the number of remaining contracts. + /// + public byte Altar1State + { + get => this._data[8]; + set => this._data[8] = value; + } + + /// + /// Gets or sets the state of the second altar (206), like Altar1State. + /// + public byte Altar2State + { + get => this._data[9]; + set => this._data[9] = value; + } + + /// + /// Gets or sets the state of the third altar (207), like Altar1State. + /// + public byte Altar3State + { + get => this._data[10]; + set => this._data[10] = value; + } + + /// + /// Gets or sets the state of the fourth altar (208), like Altar1State. + /// + public byte Altar4State + { + get => this._data[11]; + set => this._data[11] = value; + } + + /// + /// Gets or sets the state of the fifth altar (209), like Altar1State. + /// + public byte Altar5State + { + get => this._data[12]; + set => this._data[12] = value; + } + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfStatueAndAltarInfoRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(CrywolfStatueAndAltarInfoRef packet) => packet._data; +} + + +/// +/// Is sent by the server when: The player requested to contract an altar. +/// Causes reaction on client side: On success, the client shows that the player is a guardian of the altar, and lets the character pray. Otherwise, it shows why the contract failed. +/// +public readonly ref struct CrywolfContractResultRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfContractResultRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfContractResultRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x03; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 8; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCodeRef Header => new (this._data); + + /// + /// Gets or sets a value indicating whether the contract was accepted. + /// + public bool Success + { + get => this._data[4..].GetBoolean(); + set => this._data[4..].SetBoolean(value); + } + + /// + /// Gets or sets the new state of the altar, like in the CrywolfStatueAndAltarInfo. + /// + public byte AltarState + { + get => this._data[5]; + set => this._data[5] = value; + } + + /// + /// Gets or sets the key of the altar. The client uses the key minus 317 as index into its five altar states without checking the range, so it must be 317 plus the index of the altar (0 to 4). + /// + public ushort AltarKey + { + get => ReadUInt16BigEndian(this._data[6..]); + set => WriteUInt16BigEndian(this._data[6..], value); + } + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfContractResultRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(CrywolfContractResultRef packet) => packet._data; +} + + +/// +/// Is sent by the server when: Every 20 seconds during the battle. +/// Causes reaction on client side: The client shows the remaining minutes and counts down the seconds by itself. It expects this packet every 20 seconds: when the minute is the same as before, it assumes that 40 or 20 seconds of the minute are left. +/// +public readonly ref struct CrywolfLeftTimeRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfLeftTimeRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfLeftTimeRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x04; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 6; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCodeRef Header => new (this._data); + + /// + /// Gets or sets the remaining hours. The client doesn't show them. + /// + public byte Hours + { + get => this._data[4]; + set => this._data[4] = value; + } + + /// + /// Gets or sets the remaining minutes. + /// + public byte Minutes + { + get => this._data[5]; + set => this._data[5] = value; + } + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfLeftTimeRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(CrywolfLeftTimeRef packet) => packet._data; +} + + +/// +/// Is sent by the server when: Every five seconds during the battle. +/// Causes reaction on client side: The client shows the health of Balgass and the number of Dark Elves. +/// +public readonly ref struct CrywolfBossMonsterInfoRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfBossMonsterInfoRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfBossMonsterInfoRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x05; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 12; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCodeRef Header => new (this._data); + + /// + /// Gets or sets the health of Balgass in percent, or -1, when Balgass is not alive. + /// + public uint BalgassHealthPercent + { + get => ReadUInt32LittleEndian(this._data[4..]); + set => WriteUInt32LittleEndian(this._data[4..], value); + } + + /// + /// Gets or sets the number of alive Dark Elves. The client shows it as count of 12. + /// + public byte DarkElfCount + { + get => this._data[8]; + set => this._data[8] = value; + } + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfBossMonsterInfoRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(CrywolfBossMonsterInfoRef packet) => packet._data; +} + + +/// +/// Is sent by the server when: The crywolf event ended, after the CrywolfInfo with the state End. +/// Causes reaction on client side: The client shows the rank and the experience of the player in the result. +/// +public readonly ref struct CrywolfPersonalRankRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfPersonalRankRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfPersonalRankRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x07; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 12; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCodeRef Header => new (this._data); + + /// + /// Gets or sets the rank of the player. + /// + public CrywolfPersonalRank.CrywolfRank Rank + { + get => (CrywolfPersonalRank.CrywolfRank)this._data[4]; + set => this._data[4] = (byte)value; + } + + /// + /// Gets or sets the rewarded experience. The field is aligned to 4 bytes, because the client structure is not packed. + /// + public uint Experience + { + get => ReadUInt32LittleEndian(this._data[8..]); + set => WriteUInt32LittleEndian(this._data[8..], value); + } + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfPersonalRankRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(CrywolfPersonalRankRef packet) => packet._data; +} + + +/// +/// Is sent by the server when: The crywolf event ended, after the CrywolfInfo with the state End. +/// Causes reaction on client side: The client shows the heroes with the highest scores in the result. +/// +public readonly ref struct CrywolfHeroListRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfHeroListRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfHeroListRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)data.Length; + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x08; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCodeRef Header => new (this._data); + + /// + /// Gets or sets the number of heroes, at most 5. + /// + public byte HeroCount + { + get => this._data[4]; + set => this._data[4] = value; + } + + /// + /// Gets the of the specified index. + /// + public HeroRef this[int index] => new (this._data[(5 + index * HeroRef.Length)..]); + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfHeroListRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(CrywolfHeroListRef packet) => packet._data; + + /// + /// Calculates the size of the packet for the specified count of . + /// + /// The count of from which the size will be calculated. + + public static int GetRequiredSize(int heroesCount) => heroesCount * HeroRef.Length + 5; + + +/// +/// Contains a hero of the crywolf event.. +/// +public readonly ref struct HeroRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public HeroRef(Span data) + { + this._data = data; + } + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 20; + + /// + /// Gets or sets the place of the hero, from 0 (first) to 4. The client uses it as array index, so it must not be greater than 4. + /// + public byte Rank + { + get => this._data[0]; + set => this._data[0] = value; + } + + /// + /// Gets or sets the name of the hero. + /// + public string Name + { + get => this._data.ExtractString(1, 10, System.Text.Encoding.UTF8); + set => this._data.Slice(1, 10).WriteString(value, System.Text.Encoding.UTF8); + } + + /// + /// Gets or sets the score of the hero. The field is aligned to 4 bytes, because the client structure is not packed. + /// + public uint Score + { + get => ReadUInt32LittleEndian(this._data[12..]); + set => WriteUInt32LittleEndian(this._data[12..], value); + } + + /// + /// Gets or sets the number of the character class. + /// + public byte CharacterClass + { + get => this._data[16]; + set => this._data[16] = value; + } +} +} + + +/// +/// Is sent by the server when: The player opened a crafting dialog and requested the chaos rate benefit of the crywolf event. +/// Causes reaction on client side: The client shows the additional success rate of the crafting. +/// +public readonly ref struct CrywolfBenefitPlusChaosRateRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfBenefitPlusChaosRateRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfBenefitPlusChaosRateRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x09; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 5; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCodeRef Header => new (this._data); + + /// + /// Gets or sets the additional success rate in percent. + /// + public byte PlusChaosRate + { + get => this._data[4]; + set => this._data[4] = value; + } + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfBenefitPlusChaosRateRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(CrywolfBenefitPlusChaosRateRef packet) => packet._data; +} + + +/// +/// Is sent by the server when: A ballista of the crywolf event attacks an area. +/// Causes reaction on client side: The client shows an arrow which hits the target point. The direction of the arrow depends on the x coordinate of the ballista. +/// +public readonly ref struct CrywolfRegionMonsterAttackRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public CrywolfRegionMonsterAttackRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private CrywolfRegionMonsterAttackRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + header.SubCode = SubCode; + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xBD; + + /// + /// Gets the operation sub-code of this data packet. + /// The is used as a grouping key. + /// + public static byte SubCode => 0x0C; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 10; + + /// + /// Gets the header of this packet. + /// + public C1HeaderWithSubCodeRef Header => new (this._data); + + /// + /// Gets or sets the number of the monster. The client ignores it. + /// + public ushort MonsterNumber + { + get => ReadUInt16BigEndian(this._data[4..]); + set => WriteUInt16BigEndian(this._data[4..], value); + } + + /// + /// Gets or sets the x coordinate of the ballista. + /// + public byte SourceX + { + get => this._data[6]; + set => this._data[6] = value; + } + + /// + /// Gets or sets the y coordinate of the ballista. The client ignores it. + /// + public byte SourceY + { + get => this._data[7]; + set => this._data[7] = value; + } + + /// + /// Gets or sets the x coordinate of the target point. + /// + public byte TargetX + { + get => this._data[8]; + set => this._data[8] = value; + } + + /// + /// Gets or sets the y coordinate of the target point. + /// + public byte TargetY + { + get => this._data[9]; + set => this._data[9] = value; + } + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator CrywolfRegionMonsterAttackRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(CrywolfRegionMonsterAttackRef packet) => packet._data; +} + + /// /// Is sent by the server when: The player requested to enter the illusion temple event. /// Causes reaction on client side: The client shows the result. From da29dec22383699041acae80533f3c8e85645f55 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:10:25 -0300 Subject: [PATCH 02/23] feat(crywolf): add the game logic of the crywolf event 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 --- src/GameLogic/Crywolf/CrywolfAltar.cs | 202 +++ src/GameLogic/Crywolf/CrywolfAltarState.cs | 31 + src/GameLogic/Crywolf/CrywolfArmyStage.cs | 26 + src/GameLogic/Crywolf/CrywolfBallista.cs | 40 + src/GameLogic/Crywolf/CrywolfContext.cs | 1136 +++++++++++++++++ .../Crywolf/CrywolfContractChange.cs | 26 + .../Crywolf/CrywolfContractResult.cs | 36 + src/GameLogic/Crywolf/CrywolfEffect.cs | 36 + src/GameLogic/Crywolf/CrywolfEffectDisplay.cs | 79 ++ .../Crywolf/CrywolfEventDefinition.cs | 328 +++++ src/GameLogic/Crywolf/CrywolfHero.cs | 13 + src/GameLogic/Crywolf/CrywolfMonsterGroup.cs | 49 + .../Crywolf/CrywolfMonsterGroupState.cs | 69 + .../Crywolf/CrywolfMonsterIntelligence.cs | 271 ++++ src/GameLogic/Crywolf/CrywolfMonsterRole.cs | 31 + src/GameLogic/Crywolf/CrywolfMonsterScore.cs | 27 + src/GameLogic/Crywolf/CrywolfMovePath.cs | 245 ++++ .../Crywolf/CrywolfOccupationState.cs | 26 + src/GameLogic/Crywolf/CrywolfPlugIn.cs | 120 ++ src/GameLogic/Crywolf/CrywolfState.cs | 46 + src/GameLogic/Crywolf/CrywolfTerrain.cs | 276 ++++ .../Crywolf/ICrywolfEventViewPlugIn.cs | 85 ++ .../PlayerLosesExperienceAfterDeathPlugIn.cs | 5 +- .../Properties/PlayerMessage.Designer.cs | 270 ++++ src/GameLogic/Properties/PlayerMessage.resx | 90 ++ .../Properties/PlugInResources.Designer.cs | 18 + src/GameLogic/Properties/PlugInResources.resx | 6 + ...olfChaosRateBenefitRequestHandlerPlugIn.cs | 41 + .../CrywolfContractRequestHandlerPlugIn.cs | 42 + .../MiniGames/CrywolfGroupHandlerPlugIn.cs | 42 + .../CrywolfInfoRequestHandlerPlugIn.cs | 41 + .../Properties/PlugInResources.Designer.cs | 90 ++ .../Properties/PlugInResources.resx | 30 + .../World/CrywolfEventViewPlugIn.cs | 141 ++ 34 files changed, 4012 insertions(+), 2 deletions(-) create mode 100644 src/GameLogic/Crywolf/CrywolfAltar.cs create mode 100644 src/GameLogic/Crywolf/CrywolfAltarState.cs create mode 100644 src/GameLogic/Crywolf/CrywolfArmyStage.cs create mode 100644 src/GameLogic/Crywolf/CrywolfBallista.cs create mode 100644 src/GameLogic/Crywolf/CrywolfContext.cs create mode 100644 src/GameLogic/Crywolf/CrywolfContractChange.cs create mode 100644 src/GameLogic/Crywolf/CrywolfContractResult.cs create mode 100644 src/GameLogic/Crywolf/CrywolfEffect.cs create mode 100644 src/GameLogic/Crywolf/CrywolfEffectDisplay.cs create mode 100644 src/GameLogic/Crywolf/CrywolfEventDefinition.cs create mode 100644 src/GameLogic/Crywolf/CrywolfHero.cs create mode 100644 src/GameLogic/Crywolf/CrywolfMonsterGroup.cs create mode 100644 src/GameLogic/Crywolf/CrywolfMonsterGroupState.cs create mode 100644 src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs create mode 100644 src/GameLogic/Crywolf/CrywolfMonsterRole.cs create mode 100644 src/GameLogic/Crywolf/CrywolfMonsterScore.cs create mode 100644 src/GameLogic/Crywolf/CrywolfMovePath.cs create mode 100644 src/GameLogic/Crywolf/CrywolfOccupationState.cs create mode 100644 src/GameLogic/Crywolf/CrywolfPlugIn.cs create mode 100644 src/GameLogic/Crywolf/CrywolfState.cs create mode 100644 src/GameLogic/Crywolf/CrywolfTerrain.cs create mode 100644 src/GameLogic/Crywolf/ICrywolfEventViewPlugIn.cs create mode 100644 src/GameServer/MessageHandler/MiniGames/CrywolfChaosRateBenefitRequestHandlerPlugIn.cs create mode 100644 src/GameServer/MessageHandler/MiniGames/CrywolfContractRequestHandlerPlugIn.cs create mode 100644 src/GameServer/MessageHandler/MiniGames/CrywolfGroupHandlerPlugIn.cs create mode 100644 src/GameServer/MessageHandler/MiniGames/CrywolfInfoRequestHandlerPlugIn.cs create mode 100644 src/GameServer/RemoteView/World/CrywolfEventViewPlugIn.cs diff --git a/src/GameLogic/Crywolf/CrywolfAltar.cs b/src/GameLogic/Crywolf/CrywolfAltar.cs new file mode 100644 index 000000000..914f76dba --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfAltar.cs @@ -0,0 +1,202 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.GameLogic.NPC; + +/// +/// An altar of the crywolf event, which an elf contracts to protect the statue of the holy wolf. +/// +/// +/// An elf has to stand at the altar and stay there until the contract is valid. When the elf moves, +/// dies or leaves, the contract is cancelled. Each altar can be contracted only a few times per event, +/// and the attempts count as well, like in the original game. +/// +public sealed class CrywolfAltar +{ + /// + /// Initializes a new instance of the class. + /// + /// The index of the altar, from 0 to 4. + /// The NPC of the altar. + public CrywolfAltar(int index, NonPlayerCharacter npc) + { + this.Index = index; + this.Npc = npc; + this.Display = new CrywolfEffectDisplay(npc); + } + + /// + /// Gets the index of the altar, from 0 to 4. + /// + public int Index { get; } + + /// + /// Gets the NPC of the altar. + /// + public NonPlayerCharacter Npc { get; } + + /// + /// Gets the state of the altar. + /// + public CrywolfAltarState State { get; private set; } + + /// + /// Gets the number of contracts of this event, including the attempts. + /// + public int ContractCount { get; private set; } + + /// + /// Gets the elf which contracted the altar or tries to contract it. + /// + public Player? Contractor { get; private set; } + + /// + /// Gets the display of the effect of the altar. + /// + internal CrywolfEffectDisplay Display { get; } + + private DateTime ValidFrom { get; set; } + + private DateTime LastChange { get; set; } = DateTime.MinValue; + + /// + /// Gets the state of the altar as it's expected by the client: the altar state in the high nibble, + /// and the number of remaining contracts in the low nibble. + /// + /// The number of contracts per altar. + /// The state for the client. + public byte GetClientState(int contractsPerAltar) + { + var remainingContracts = Math.Clamp(contractsPerAltar - this.ContractCount, 0, 0x0F); + return (byte)(((byte)this.State << 4) | remainingContracts); + } + + /// + /// Resets the altar for a new event. + /// + public void Reset() + { + this.State = CrywolfAltarState.Free; + this.ContractCount = 0; + this.Contractor = null; + this.LastChange = DateTime.MinValue; + this.UpdateEffect(true); + } + + /// + /// Hides the effect of the altar, e.g. when the event is finished. + /// + public void Hide() + { + this.UpdateEffect(false); + } + + /// + /// Tries to start a contract with an elf. + /// + /// The player of the elf. + /// The definition of the event. + /// The current time. + /// The result. + public CrywolfContractResult TryStartContract(Player player, CrywolfEventDefinition definition, DateTime now) + { + if (this.State != CrywolfAltarState.Free) + { + return CrywolfContractResult.NotAvailable; + } + + if (player.SelectedCharacter?.CharacterClass is not { } characterClass + || !definition.ContractCharacterClassNumbers.Contains(characterClass.Number) + || (player.Attributes?[Stats.Level] ?? 0) < definition.MinimumContractLevel) + { + return CrywolfContractResult.NotQualified; + } + + if (now - this.LastChange < definition.AltarCooldown) + { + return CrywolfContractResult.Cooldown; + } + + if (player.CurrentMap != this.Npc.CurrentMap || player.Position != this.Npc.Position) + { + return CrywolfContractResult.WrongPosition; + } + + // Like in the original game, the attempt already counts as contract. + this.ContractCount++; + this.State = CrywolfAltarState.Attempting; + this.Contractor = player; + this.ValidFrom = now + definition.ContractDelay; + this.LastChange = now; + this.UpdateEffect(true); + return CrywolfContractResult.Success; + } + + /// + /// Checks the contractor of the altar: the contract gets valid when the elf stayed at the altar long enough, + /// and it's cancelled when the elf moved, died or left. + /// + /// The definition of the event. + /// The current time. + /// The change of the contract. + public CrywolfContractChange Update(CrywolfEventDefinition definition, DateTime now) + { + if (this.Contractor is not { } contractor) + { + return CrywolfContractChange.None; + } + + if (!IsAtAltar(contractor, this.Npc)) + { + this.Cancel(definition, now); + return CrywolfContractChange.Cancelled; + } + + if (this.State == CrywolfAltarState.Attempting && now >= this.ValidFrom) + { + this.State = CrywolfAltarState.Contracted; + this.UpdateEffect(true); + return CrywolfContractChange.Validated; + } + + return CrywolfContractChange.None; + } + + /// + /// Cancels the contract of the altar. + /// + /// The definition of the event. + /// The current time. + public void Cancel(CrywolfEventDefinition definition, DateTime now) + { + this.Contractor = null; + this.LastChange = now; + this.State = this.ContractCount >= definition.ContractsPerAltar ? CrywolfAltarState.Exhausted : CrywolfAltarState.Free; + this.UpdateEffect(true); + } + + private static bool IsAtAltar(Player player, NonPlayerCharacter altar) + { + return player.IsAlive + && player.PlayerState.CurrentState == PlayerState.EnteredWorld + && player.CurrentMap == altar.CurrentMap + && player.Position == altar.Position; + } + + private void UpdateEffect(bool isVisible) + { + this.Display.Effect = !isVisible + ? null + : this.State switch + { + CrywolfAltarState.Free => CrywolfEffect.AltarEnabled, + CrywolfAltarState.Contracted => CrywolfEffect.AltarContracted, + CrywolfAltarState.Attempting => CrywolfEffect.AltarAttempt, + _ => CrywolfEffect.AltarDisabled, + }; + } +} diff --git a/src/GameLogic/Crywolf/CrywolfAltarState.cs b/src/GameLogic/Crywolf/CrywolfAltarState.cs new file mode 100644 index 000000000..c889b4f51 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfAltarState.cs @@ -0,0 +1,31 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// The state of an altar of the crywolf event, matching the values expected by the client. +/// +public enum CrywolfAltarState : byte +{ + /// + /// The altar can be contracted. + /// + Free = 0, + + /// + /// The altar is contracted by an elf, which protects the statue. + /// + Contracted = 1, + + /// + /// An elf is trying to contract the altar. + /// + Attempting = 2, + + /// + /// The altar can't be contracted anymore, because all of its contracts were used. + /// + Exhausted = 3, +} diff --git a/src/GameLogic/Crywolf/CrywolfArmyStage.cs b/src/GameLogic/Crywolf/CrywolfArmyStage.cs new file mode 100644 index 000000000..ff41a4623 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfArmyStage.cs @@ -0,0 +1,26 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// The stage of the army of Balgass during the crywolf event. +/// +public enum CrywolfArmyStage +{ + /// + /// The army waits for the battle and doesn't move or attack. + /// + Waiting, + + /// + /// The army advances towards the fortress. + /// + Advancing, + + /// + /// The army attacks the statue of the holy wolf. + /// + AttackingStatue, +} diff --git a/src/GameLogic/Crywolf/CrywolfBallista.cs b/src/GameLogic/Crywolf/CrywolfBallista.cs new file mode 100644 index 000000000..89dd222b3 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfBallista.cs @@ -0,0 +1,40 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// A ballista of the army of Balgass, which bombards a point of the fortress. +/// +/// +/// The ballista is identified by the coordinates of its monster spawn. +/// +public class CrywolfBallista +{ + /// + /// Gets or sets the x coordinate of the monster spawn of the ballista. + /// + public byte X { get; set; } + + /// + /// Gets or sets the y coordinate of the monster spawn of the ballista. + /// + public byte Y { get; set; } + + /// + /// Gets or sets the x coordinate of the point, which the ballista bombards. + /// + public byte TargetX { get; set; } + + /// + /// Gets or sets the y coordinate of the point, which the ballista bombards. + /// + public byte TargetY { get; set; } + + /// + public override string ToString() + { + return $"({this.X}, {this.Y}) -> ({this.TargetX}, {this.TargetY})"; + } +} diff --git a/src/GameLogic/Crywolf/CrywolfContext.cs b/src/GameLogic/Crywolf/CrywolfContext.cs new file mode 100644 index 000000000..561332850 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfContext.cs @@ -0,0 +1,1136 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +using System.Collections.Concurrent; +using System.Threading; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.GameLogic.NPC; +using MUnique.OpenMU.GameLogic.Properties; +using MUnique.OpenMU.GameLogic.Views; +using MUnique.OpenMU.Interfaces; +using MUnique.OpenMU.Pathfinding; + +/// +/// The context of the crywolf event of a game server. +/// +/// +/// Unlike the mini games, the event takes place on the (only) instance of the crywolf map. Its run is: +/// +/// The players are notified about the upcoming attack of the army of Balgass (). +/// The fortress prepares for the war: the common monsters and NPCs of the map disappear (). +/// The army appears and the elves contract the altars, which protect the statue (). +/// The army attacks. Later, Balgass appears. When he's killed, the fortress has been defended (). +/// The result is shown, and the players get experience depending on their score (). +/// The army disappears and the common monsters return. The heroes are shown (). +/// +/// The result of the event () is kept until the next event. +/// +public sealed class CrywolfContext : IEventStateProvider, IDisposable +{ + private static readonly TimeSpan AltarInfoInterval = TimeSpan.FromSeconds(2); + private static readonly TimeSpan BossMonsterInfoInterval = TimeSpan.FromSeconds(5); + private static readonly TimeSpan RemainingTimeInterval = TimeSpan.FromSeconds(20); + + private readonly GameContext _gameContext; + private readonly ILogger _logger; + private readonly List _altars = new(); + private readonly ConcurrentDictionary _hiddenNpcs = new(); + private readonly ConcurrentDictionary _groups = new(); + private readonly ConcurrentDictionary _eventMonsters = new(); + private readonly ConcurrentDictionary _removedMonsters = new(); + private readonly ConcurrentDictionary _scores = new(); + private readonly Func _saveDefinition; + + private CrywolfEventDefinition _definition; + private GameMap? _map; + private NonPlayerCharacter? _statue; + private Monster? _balgass; + private string? _balgassKillerName; + private DateTime _stateStart = DateTime.UtcNow; + private DateTime _lastNotification = DateTime.MinValue; + private DateTime _lastAltarInfo = DateTime.MinValue; + private DateTime _lastBossMonsterInfo = DateTime.MinValue; + private DateTime _lastRemainingTime = DateTime.MinValue; + private int _contractedAltarCount; + private bool _isBalgassAppearanceDone; + private bool _isStatueAttackStarted; + private bool _isStartForced; + private bool _isMissingConfigurationLogged; + private int _isBalgassDead; + + /// + /// Initializes a new instance of the class. + /// + /// The game context. + /// The definition of the event. + /// The function which saves the definition, when the occupation changed. + public CrywolfContext(GameContext gameContext, CrywolfEventDefinition definition, Func saveDefinition) + { + this._gameContext = gameContext; + this._definition = definition; + this._saveDefinition = saveDefinition; + this._logger = gameContext.LoggerFactory.CreateLogger(); + this.Occupation = definition.Occupation; + } + + /// + /// Gets the current state of the event. + /// + public CrywolfState State { get; private set; } = CrywolfState.None; + + /// + /// Gets the occupation state of the fortress. + /// + public CrywolfOccupationState Occupation { get; private set; } + + /// + /// Gets the stage of the army: 0, when it doesn't move, 1 when it advances, and 2 when it attacks the statue. + /// + public CrywolfArmyStage ArmyStage => this.State != CrywolfState.Start + ? CrywolfArmyStage.Waiting + : this._isStatueAttackStarted ? CrywolfArmyStage.AttackingStatue : CrywolfArmyStage.Advancing; + + /// + /// Gets the altars. + /// + public IReadOnlyList Altars => this._altars; + + /// + /// Gets the definition of the event. + /// + public CrywolfEventDefinition Definition => this._definition; + + /// + public bool IsEventRunning => this.State is >= CrywolfState.Notify2 and <= CrywolfState.End; + + /// + /// Gets a value indicating whether the battle is running. + /// + public bool IsBattleRunning => this.State == CrywolfState.Start; + + /// + /// Gets the remaining time of the current state. + /// + public TimeSpan RemainingTime + { + get + { + var remaining = this._definition.GetDuration(this.State) - (DateTime.UtcNow - this._stateStart); + return remaining > TimeSpan.Zero ? remaining : TimeSpan.Zero; + } + } + + /// + /// Gets the crywolf map. + /// + internal GameMap? Map => this._map; + + private bool IsBalgassDead => Volatile.Read(ref this._isBalgassDead) != 0; + + /// + public bool IsSpawnWaveActive(byte waveNumber) => false; + + /// + /// Initializes the context by registering at the map of the event. + /// + public async ValueTask InitializeAsync() + { + this._map = await this._gameContext.GetMapAsync((ushort)this._definition.MapNumber).ConfigureAwait(false); + if (this._map is null) + { + this._logger.LogWarning("The map {map} of the crywolf event wasn't found.", this._definition.MapNumber); + return; + } + + this._map.ObjectAdded += this.OnObjectAddedToMapAsync; + this.InitializeNpcs(); + await this.ApplyOccupationAsync().ConfigureAwait(false); + } + + /// + /// Executes one step of the event. It's called every second. + /// + public async ValueTask TickAsync() + { + if (this._map is null) + { + return; + } + + if (this._altars.Count == 0) + { + this.InitializeNpcs(); + } + + if (this._altars.Count != this._definition.AltarNumbers.Count || this._statue is null) + { + // Without the altars and the statue (e.g. when the data update wasn't applied yet), the event can't run. + if (!this._isMissingConfigurationLogged) + { + this._isMissingConfigurationLogged = true; + this._logger.LogWarning("The crywolf event doesn't run, because the statue and the altars aren't spawned on the map {map}.", this._definition.MapNumber); + } + + return; + } + + var elapsed = DateTime.UtcNow - this._stateStart; + switch (this.State) + { + case CrywolfState.None: + await this.CheckStartAsync().ConfigureAwait(false); + break; + case CrywolfState.Notify1: + await this.NotifyAsync(player => player.GetLocalizedMessage(nameof(PlayerMessage.CrywolfNotify1)), true).ConfigureAwait(false); + await this.ProceedAfterDurationAsync(elapsed, CrywolfState.Notify2).ConfigureAwait(false); + break; + case CrywolfState.Notify2: + await this.RemoveCommonMonstersAsync().ConfigureAwait(false); + await this.NotifyRemainingTimeAsync(nameof(PlayerMessage.CrywolfContractsStartInMinutes), nameof(PlayerMessage.CrywolfContractsStartInSeconds)).ConfigureAwait(false); + await this.ProceedAfterDurationAsync(elapsed, CrywolfState.Ready).ConfigureAwait(false); + break; + case CrywolfState.Ready: + await this.RemoveCommonMonstersAsync().ConfigureAwait(false); + await this.UpdateAltarsAsync().ConfigureAwait(false); + await this.NotifyRemainingTimeAsync(nameof(PlayerMessage.CrywolfAttackStartsInMinutes), nameof(PlayerMessage.CrywolfAttackStartsInSeconds)).ConfigureAwait(false); + await this.ProceedAfterDurationAsync(elapsed, CrywolfState.Start).ConfigureAwait(false); + break; + case CrywolfState.Start: + await this.RemoveCommonMonstersAsync().ConfigureAwait(false); + await this.UpdateBattleAsync(elapsed).ConfigureAwait(false); + break; + case CrywolfState.End: + await this.ProceedAfterDurationAsync(elapsed, CrywolfState.EndCycle).ConfigureAwait(false); + break; + case CrywolfState.EndCycle: + await this.ProceedAfterDurationAsync(elapsed, CrywolfState.None).ConfigureAwait(false); + break; + default: + // nothing to do + break; + } + + await this.UpdateEffectsAsync().ConfigureAwait(false); + } + + /// + /// Updates the definition of the event, e.g. after it has been changed in the admin panel. + /// + /// The definition. + public void UpdateDefinition(CrywolfEventDefinition definition) + { + this._definition = definition; + } + + /// + /// Forces the event to proceed: it starts when it's not running, otherwise the current state ends. + /// + public void SkipWaitingTime() + { + if (this.State == CrywolfState.None) + { + this._isStartForced = true; + } + else + { + this._stateStart = DateTime.MinValue; + } + } + + /// + /// Determines whether the player loses experience when it's killed on the specified map. + /// During the battle, it doesn't lose experience on the crywolf map. + /// + /// The map. + /// true, if the player keeps its experience; otherwise, false. + public bool KeepsExperienceOnDeath(GameMap? map) + { + return this.IsBattleRunning && map is not null && map == this._map; + } + + /// + /// Gets the additional success rate of the chaos machine crafting, which is shown to the player. + /// + /// The additional success rate in percent. + public byte GetChaosRateBenefit() + { + // The benefits are part of a separate change. + return 0; + } + + /// + /// Shows the current state of the event to a player, which entered the crywolf map. + /// + /// The player. + public async ValueTask ShowCurrentStateAsync(Player player) + { + if (player.CurrentMap != this._map) + { + // The client loads the terrain of the occupation state for its current map. + return; + } + + await player.InvokeViewPlugInAsync(p => p.ShowStateAsync(this.Occupation, this.State)).ConfigureAwait(false); + if (this.State is CrywolfState.Ready or CrywolfState.Start) + { + var (shield, altarStates) = this.GetStatueAndAltarInfo(); + await player.InvokeViewPlugInAsync(p => p.ShowStatueAndAltarsAsync(shield, altarStates)).ConfigureAwait(false); + } + + if (this.State == CrywolfState.Start) + { + var remaining = this.RemainingTime; + var (balgassHealth, darkElfCount) = this.GetBossMonsterInfo(); + await player.InvokeViewPlugInAsync(p => p.ShowRemainingTimeAsync(remaining)).ConfigureAwait(false); + await player.InvokeViewPlugInAsync(p => p.ShowBossMonsterInfoAsync(balgassHealth, darkElfCount)).ConfigureAwait(false); + } + } + + /// + /// Tries to contract an altar with the player. + /// + /// The player. + /// The id of the altar, as the client knows it. + public async ValueTask ContractAltarAsync(Player player, ushort altarId) + { + var altar = this._altars.FirstOrDefault(a => a.Npc.Id == altarId); + if (altar is null) + { + return; + } + + CrywolfContractResult result; + if (this.State is not (CrywolfState.Ready or CrywolfState.Start)) + { + // Unlike the original game, the altars can only be contracted while the event is running. + result = CrywolfContractResult.NotAvailable; + } + else + { + lock (altar) + { + result = altar.TryStartContract(player, this._definition, DateTime.UtcNow); + } + } + + var altarNumber = altar.Index + 1; + switch (result) + { + case CrywolfContractResult.Success: + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfContractAttemptInfo), (int)this._definition.ContractDelay.TotalSeconds).ConfigureAwait(false); + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfContractAttempt), player.Name, altarNumber).ConfigureAwait(false); + break; + case CrywolfContractResult.Cooldown: + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfAltarCooldown), altarNumber).ConfigureAwait(false); + break; + case CrywolfContractResult.WrongPosition: + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfContractWrongPosition), altarNumber).ConfigureAwait(false); + break; + default: + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfContractRequirement), this._definition.MinimumContractLevel).ConfigureAwait(false); + break; + } + + var altarState = altar.GetClientState(this._definition.ContractsPerAltar); + await player.InvokeViewPlugInAsync(p => p.ShowContractResultAsync(result == CrywolfContractResult.Success, altar.Index, altarState)).ConfigureAwait(false); + } + + /// + public void Dispose() + { + if (this._map is { } map) + { + map.ObjectAdded -= this.OnObjectAddedToMapAsync; + } + } + + /// + /// Gets the leader of a group, if it's alive. + /// + /// The wave number of the group. + /// The leader of the group. + internal Monster? GetLeader(byte waveNumber) + { + return this._groups.TryGetValue(waveNumber, out var group) && group.Leader is { IsAlive: true } leader ? leader : null; + } + + /// + /// Revives the dead members of a group, which is done by the leader of the group. + /// + /// The wave number of the group. + internal async ValueTask ReviveGroupMembersAsync(byte waveNumber) + { + if (!this._groups.TryGetValue(waveNumber, out var group) || this.State != CrywolfState.Start) + { + return; + } + + foreach (var spawnArea in group.GetDeadMembers()) + { + await this.SpawnMemberAsync(group, spawnArea).ConfigureAwait(false); + } + } + + /// + /// Lets a ballista bombard the area around its target point. + /// + /// The monster of the ballista. + /// The definition of the ballista. + internal async ValueTask BombardAsync(Monster ballista, CrywolfBallista definition) + { + if (this._map is not { } map) + { + return; + } + + var spread = this._definition.BallistaTargetSpread; + var target = new Point( + (byte)Math.Clamp(definition.TargetX + Rand.NextInt(-spread, spread + 1), 0, byte.MaxValue), + (byte)Math.Clamp(definition.TargetY + Rand.NextInt(-spread, spread + 1), 0, byte.MaxValue)); + var players = map.GetAttackablesInRange(target, this._definition.BallistaEffectRadius).OfType().ToList(); + foreach (var player in players) + { + await player.InvokeViewPlugInAsync(p => p.ShowBallistaAttackAsync(ballista, target)).ConfigureAwait(false); + } + + foreach (var player in players.Where(p => p.IsActive() && !p.IsAtSafezone() && p.IsInRange(target, this._definition.BallistaHitRadius))) + { + await player.AttackByAsync(ballista, null, false).ConfigureAwait(false); + } + } + + private static string GetRankName(int rank) + { + return rank switch + { + 4 => "S", + 3 => "A", + 2 => "B", + 1 => "C", + _ => "D", + }; + } + + private void InitializeNpcs() + { + if (this._map is not { } map) + { + return; + } + + var npcs = map.GetNpcsInRange(new Point(128, 128), byte.MaxValue); + this._statue = npcs.FirstOrDefault(npc => npc.Definition.Number == this._definition.StatueNumber); + this._altars.Clear(); + for (var i = 0; i < this._definition.AltarNumbers.Count; i++) + { + var number = this._definition.AltarNumbers[i]; + if (npcs.FirstOrDefault(npc => npc.Definition.Number == number) is { } npc) + { + this._altars.Add(new CrywolfAltar(i, npc)); + } + } + + foreach (var npc in npcs.Where(this.IsCommonNpc)) + { + this._hiddenNpcs.TryAdd(npc, new CrywolfEffectDisplay(npc)); + } + } + + private bool IsCommonNpc(NonPlayerCharacter npc) + { + return npc is not Monster + && npc.SpawnArea.SpawnTrigger == SpawnTrigger.Automatic + && npc != this._statue + && !this._definition.AltarNumbers.Contains(npc.Definition.Number) + && !this._definition.AlwaysVisibleNpcNumbers.Contains(npc.Definition.Number); + } + + private async ValueTask CheckStartAsync() + { + var isStartTime = this._definition.IsStartTime(TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, this._gameContext.ServerTimeZone)); + if (!isStartTime && !this._isStartForced) + { + return; + } + + this._isStartForced = false; + await this.ChangeStateAsync(CrywolfState.Notify1).ConfigureAwait(false); + } + + private async ValueTask ProceedAfterDurationAsync(TimeSpan elapsed, CrywolfState nextState) + { + if (elapsed >= this._definition.GetDuration(this.State)) + { + await this.ChangeStateAsync(nextState).ConfigureAwait(false); + } + } + + private async ValueTask ChangeStateAsync(CrywolfState state) + { + this.State = state; + this._stateStart = DateTime.UtcNow; + this._lastNotification = DateTime.MinValue; + this._logger.LogInformation("Crywolf state changed to {state}.", state); + switch (state) + { + case CrywolfState.Notify2: + await this.StartWarAsync().ConfigureAwait(false); + break; + case CrywolfState.Ready: + await this.PrepareBattleAsync().ConfigureAwait(false); + break; + case CrywolfState.Start: + await this.StartBattleAsync().ConfigureAwait(false); + break; + case CrywolfState.End: + await this.EndBattleAsync().ConfigureAwait(false); + break; + case CrywolfState.EndCycle: + await this.EndCycleAsync().ConfigureAwait(false); + break; + default: + await this.ShowStateToMapPlayersAsync().ConfigureAwait(false); + break; + } + } + + private async ValueTask StartWarAsync() + { + await this.ShowMessageToAllPlayersAsync(nameof(PlayerMessage.CrywolfNotify2)).ConfigureAwait(false); + this.Occupation = CrywolfOccupationState.War; + await this.ApplyOccupationAsync().ConfigureAwait(false); + await this.ShowStateToMapPlayersAsync().ConfigureAwait(false); + await this.RemoveCommonMonstersAsync().ConfigureAwait(false); + } + + private async ValueTask PrepareBattleAsync() + { + await this.ShowMessageToAllPlayersAsync(nameof(PlayerMessage.CrywolfReady)).ConfigureAwait(false); + await this.RemoveEventMonstersAsync().ConfigureAwait(false); + Volatile.Write(ref this._isBalgassDead, 0); + this._balgassKillerName = null; + this._isBalgassAppearanceDone = false; + this._isStatueAttackStarted = false; + this._contractedAltarCount = 0; + this._scores.Clear(); + foreach (var altar in this._altars) + { + lock (altar) + { + altar.Reset(); + } + } + + foreach (var group in this._definition.MonsterGroups) + { + await this.SpawnGroupAsync(group).ConfigureAwait(false); + } + + await this.ShowStateToMapPlayersAsync().ConfigureAwait(false); + } + + private async ValueTask StartBattleAsync() + { + await this.ShowMessageToAllPlayersAsync(nameof(PlayerMessage.CrywolfStart)).ConfigureAwait(false); + await this.ShowStateToMapPlayersAsync().ConfigureAwait(false); + this._lastAltarInfo = DateTime.MinValue; + this._lastBossMonsterInfo = DateTime.MinValue; + this._lastRemainingTime = DateTime.MinValue; + if (this.CountContractedAltars() == 0) + { + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfNoAltarContracted)).ConfigureAwait(false); + this.Occupation = CrywolfOccupationState.Occupied; + await this.ChangeStateAsync(CrywolfState.End).ConfigureAwait(false); + } + } + + private async ValueTask UpdateBattleAsync(TimeSpan elapsed) + { + await this.UpdateAltarsAsync().ConfigureAwait(false); + if (this.CountContractedAltars() == 0) + { + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfNoAltarContracted)).ConfigureAwait(false); + this.Occupation = CrywolfOccupationState.Occupied; + await this.ChangeStateAsync(CrywolfState.End).ConfigureAwait(false); + return; + } + + var now = DateTime.UtcNow; + if (now - this._lastBossMonsterInfo >= BossMonsterInfoInterval) + { + this._lastBossMonsterInfo = now; + var (balgassHealth, darkElfCount) = this.GetBossMonsterInfo(); + await this.ForEachMapPlayerAsync(player => player.InvokeViewPlugInAsync(p => p.ShowBossMonsterInfoAsync(balgassHealth, darkElfCount)).AsTask()).ConfigureAwait(false); + } + + if (now - this._lastRemainingTime >= RemainingTimeInterval) + { + // The client counts down the seconds by itself and expects this every 20 seconds. + this._lastRemainingTime = now; + var remaining = this.RemainingTime; + await this.ForEachMapPlayerAsync(player => player.InvokeViewPlugInAsync(p => p.ShowRemainingTimeAsync(remaining)).AsTask()).ConfigureAwait(false); + } + + if (!this._isStatueAttackStarted && elapsed >= this._definition.StatueAttackDelay) + { + this._isStatueAttackStarted = true; + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfStatueAttack)).ConfigureAwait(false); + } + + if (!this._isBalgassAppearanceDone && elapsed >= this._definition.BalgassAppearanceDelay) + { + this._isBalgassAppearanceDone = true; + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfBalgassAppeared)).ConfigureAwait(false); + await this.SpawnBalgassAsync().ConfigureAwait(false); + } + + if (this._balgass is not null && this.IsBalgassDead) + { + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfBalgassDefeated)).ConfigureAwait(false); + this.Occupation = CrywolfOccupationState.Peace; + await this.ChangeStateAsync(CrywolfState.End).ConfigureAwait(false); + return; + } + + if (elapsed >= this._definition.BattleDuration) + { + if (this._balgass is not null) + { + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfBalgassAlive)).ConfigureAwait(false); + this.Occupation = CrywolfOccupationState.Occupied; + } + else + { + // Balgass didn't appear (e.g. because of the configuration), and the altars were protected. + this.Occupation = CrywolfOccupationState.Peace; + } + + await this.ChangeStateAsync(CrywolfState.End).ConfigureAwait(false); + } + } + + private async ValueTask EndBattleAsync() + { + var isDefended = this.Occupation == CrywolfOccupationState.Peace; + this._logger.LogInformation("Crywolf battle ended, defended: {defended}.", isDefended); + await this.ShowMessageToAllPlayersAsync(isDefended ? nameof(PlayerMessage.CrywolfDefenseSucceeded) : nameof(PlayerMessage.CrywolfDefenseFailed)).ConfigureAwait(false); + + // The client only shows the result, when the occupation is final. + await this.ShowStateToMapPlayersAsync().ConfigureAwait(false); + + foreach (var altar in this._altars) + { + Player? contractor; + lock (altar) + { + contractor = altar.Contractor; + altar.Reset(); + altar.Hide(); + } + + if (contractor is null) + { + continue; + } + + this._scores.AddOrUpdate(contractor, this._definition.ContractorScore, (_, score) => score + this._definition.ContractorScore); + if (isDefended) + { + await this.DropRewardAsync(contractor).ConfigureAwait(false); + } + } + + await this.ForEachMapPlayerAsync(player => this.RewardExperienceAsync(player, isDefended).AsTask()).ConfigureAwait(false); + await this.SaveOccupationAsync().ConfigureAwait(false); + } + + private async ValueTask EndCycleAsync() + { + await this.RemoveEventMonstersAsync().ConfigureAwait(false); + await this.ApplyOccupationAsync().ConfigureAwait(false); + await this.RestoreCommonMonstersAsync().ConfigureAwait(false); + await this.ShowStateToMapPlayersAsync().ConfigureAwait(false); + + var isDefended = this.Occupation == CrywolfOccupationState.Peace; + var heroes = this._scores + .Where(entry => entry.Key.CurrentMap == this._map) + .OrderByDescending(entry => entry.Value) + .Take(Math.Min(this._definition.HeroCount, 5)) + .ToList(); + var heroList = heroes + .Select(entry => new CrywolfHero(entry.Key.Name, entry.Value, entry.Key.SelectedCharacter?.CharacterClass?.Number ?? 0)) + .ToList(); + await this.ForEachMapPlayerAsync(player => player.InvokeViewPlugInAsync(p => p.ShowHeroListAsync(heroList)).AsTask()).ConfigureAwait(false); + if (isDefended) + { + foreach (var (hero, _) in heroes) + { + await this.DropRewardAsync(hero).ConfigureAwait(false); + } + } + + this._scores.Clear(); + } + + private async ValueTask RewardExperienceAsync(Player player, bool isDefended) + { + var score = this._scores.GetValueOrDefault(player); + var rank = this._definition.GetRank(score); + var experience = this._definition.GetExperience(rank, isDefended); + if (experience > 0) + { + await player.AddExperienceAsync(experience, null).ConfigureAwait(false); + } + + await player.InvokeViewPlugInAsync(p => p.ShowPersonalRankAsync(rank, experience)).ConfigureAwait(false); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfRankResult), GetRankName(rank), experience).ConfigureAwait(false); + } + + private async ValueTask DropRewardAsync(Player player) + { + if (player.CurrentMap is not { } map + || this._gameContext.Configuration.Items.FirstOrDefault(item => item.Group == this._definition.RewardItemGroup && item.Number == this._definition.RewardItemNumber) is not { } definition) + { + return; + } + + var item = new TemporaryItem { Definition = definition, Durability = 1 }; + await map.AddAsync(new DroppedItem(item, player.Position, map, null, player.GetAsEnumerable())).ConfigureAwait(false); + } + + private async ValueTask UpdateAltarsAsync() + { + var now = DateTime.UtcNow; + foreach (var altar in this._altars) + { + Player? contractor; + CrywolfContractChange change; + lock (altar) + { + contractor = altar.Contractor; + change = altar.Update(this._definition, now); + } + + if (contractor is null) + { + continue; + } + + var altarNumber = altar.Index + 1; + if (change == CrywolfContractChange.Validated) + { + await contractor.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfContractValid), altarNumber).ConfigureAwait(false); + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfContractValidToOthers), contractor.Name, altarNumber).ConfigureAwait(false); + } + else if (change == CrywolfContractChange.Cancelled) + { + await contractor.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfContractCancelled), altarNumber).ConfigureAwait(false); + } + else + { + // nothing changed + } + } + + var contractedCount = this.CountContractedAltars(); + var previousCount = Interlocked.Exchange(ref this._contractedAltarCount, contractedCount); + if (contractedCount != previousCount) + { + var (shieldPercentage, _) = this.GetStatueAndAltarInfo(); + if (contractedCount == 0) + { + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfBarrierDisappeared)).ConfigureAwait(false); + } + else if (previousCount == 0) + { + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfBarrierCreated)).ConfigureAwait(false); + } + else + { + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfBarrierStatus), contractedCount, shieldPercentage).ConfigureAwait(false); + } + } + + if (now - this._lastAltarInfo >= AltarInfoInterval) + { + this._lastAltarInfo = now; + var (shield, altarStates) = this.GetStatueAndAltarInfo(); + await this.ForEachMapPlayerAsync(player => player.InvokeViewPlugInAsync(p => p.ShowStatueAndAltarsAsync(shield, altarStates)).AsTask()).ConfigureAwait(false); + } + } + + private int CountContractedAltars() + { + return this._altars.Count(altar => altar.State == CrywolfAltarState.Contracted); + } + + /// + /// Gets the shield of the statue in percent, which is the health of the elves of the contracted altars, and the states of the altars. + /// + private (int ShieldPercentage, IReadOnlyList AltarStates) GetStatueAndAltarInfo() + { + double health = 0; + double maximumHealth = 0; + foreach (var altar in this._altars) + { + if (altar.State == CrywolfAltarState.Contracted && altar.Contractor?.Attributes is { } attributes) + { + health += attributes[Stats.CurrentHealth]; + maximumHealth += attributes[Stats.MaximumHealth]; + } + } + + var percentage = maximumHealth > 0 ? (int)Math.Clamp(health * 100 / maximumHealth, 0, 100) : 0; + var altarStates = this._altars.Select(altar => altar.GetClientState(this._definition.ContractsPerAltar)).ToList(); + return (percentage, altarStates); + } + + private (int BalgassHealthPercentage, int DarkElfCount) GetBossMonsterInfo() + { + var balgassHealth = -1; + if (this._balgass is { IsAlive: true } balgass) + { + var maximumHealth = balgass.Attributes[Stats.MaximumHealth]; + balgassHealth = maximumHealth > 0 ? (int)Math.Clamp(balgass.Health * 100 / maximumHealth, 1, 100) : 1; + } + + var darkElfCount = this._groups.Values.Count(group => group.Leader is { IsAlive: true }); + return (balgassHealth, darkElfCount); + } + + private async ValueTask NotifyAsync(Func getMessage, bool global) + { + var now = DateTime.UtcNow; + if (now - this._lastNotification < this._definition.NotificationInterval) + { + return; + } + + this._lastNotification = now; + if (global) + { + await this.ShowMessageToAllPlayersAsync(getMessage).ConfigureAwait(false); + } + else + { + await this.ForEachMapPlayerAsync(player => player.InvokeViewPlugInAsync(p => p.ShowMessageAsync(getMessage(player), MessageType.GoldenCenter)).AsTask()).ConfigureAwait(false); + } + } + + private ValueTask NotifyRemainingTimeAsync(string minutesMessageKey, string secondsMessageKey) + { + var remaining = this.RemainingTime; + return this.NotifyAsync( + player => remaining >= TimeSpan.FromMinutes(1) + ? player.GetLocalizedMessage(minutesMessageKey, (int)Math.Ceiling(remaining.TotalMinutes)) + : player.GetLocalizedMessage(secondsMessageKey, (int)Math.Ceiling(remaining.TotalSeconds)), + true); + } + + private async ValueTask ApplyOccupationAsync() + { + if (this._map is not { } map) + { + return; + } + + // The client loads the terrain of the occupation by itself. The one of the war and the occupation + // don't differ for the server. + CrywolfTerrain.Apply(map.Terrain, this.Occupation == CrywolfOccupationState.Peace); + foreach (var display in this._hiddenNpcs.Values) + { + display.Effect = this.Occupation == CrywolfOccupationState.Peace ? null : CrywolfEffect.NpcHidden; + } + + await this.UpdateEffectsAsync().ConfigureAwait(false); + } + + private async ValueTask SaveOccupationAsync() + { + if (this._definition.Occupation == this.Occupation) + { + return; + } + + this._definition.Occupation = this.Occupation; + try + { + await this._saveDefinition(this._definition).ConfigureAwait(false); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Couldn't save the occupation state of the crywolf fortress."); + } + } + + private async ValueTask UpdateEffectsAsync() + { + foreach (var altar in this._altars) + { + await altar.Display.UpdateAsync().ConfigureAwait(false); + } + + foreach (var display in this._hiddenNpcs.Values) + { + await display.UpdateAsync().ConfigureAwait(false); + } + } + + private async ValueTask SpawnGroupAsync(CrywolfMonsterGroup definition) + { + var group = new CrywolfMonsterGroupState(definition, this.GetWaveSpawns(definition.WaveNumber).ToList()); + this._groups[definition.WaveNumber] = group; + foreach (var spawnArea in group.SpawnAreas) + { + await this.SpawnMemberAsync(group, spawnArea).ConfigureAwait(false); + } + } + + private async ValueTask SpawnMemberAsync(CrywolfMonsterGroupState group, MonsterSpawnArea spawnArea) + { + var monsterDefinition = spawnArea.MonsterDefinition!; + var ballista = this._definition.Ballistas.FirstOrDefault(b => b.X == spawnArea.X1 && b.Y == spawnArea.Y1); + var role = monsterDefinition.Number == this._definition.DarkElfNumber + ? CrywolfMonsterRole.Leader + : ballista is not null ? CrywolfMonsterRole.Ballista : CrywolfMonsterRole.Soldier; + var intelligence = new CrywolfMonsterIntelligence(this, role, group.Definition, ballista, this._logger); + if (await this.SpawnMonsterAsync(spawnArea, intelligence).ConfigureAwait(false) is { } monster) + { + group.SetMonster(spawnArea, monster, role == CrywolfMonsterRole.Leader); + } + } + + private async ValueTask SpawnBalgassAsync() + { + var spawnArea = this.GetWaveSpawns(this._definition.BalgassWaveNumber).FirstOrDefault(); + if (spawnArea is null) + { + this._logger.LogWarning("The spawn of Balgass isn't configured at the wave {wave} of the crywolf map.", this._definition.BalgassWaveNumber); + return; + } + + var intelligence = new CrywolfMonsterIntelligence(this, CrywolfMonsterRole.Balgass, null, null, this._logger); + if (await this.SpawnMonsterAsync(spawnArea, intelligence).ConfigureAwait(false) is { } balgass) + { + this._balgass = balgass; + } + } + + private IEnumerable GetWaveSpawns(byte waveNumber) + { + return this._map?.Definition.MonsterSpawns + .Where(spawn => spawn.MonsterDefinition is not null + && spawn.SpawnTrigger == SpawnTrigger.OnceAtWaveStart + && spawn.WaveNumber == waveNumber) + ?? []; + } + + private async ValueTask SpawnMonsterAsync(MonsterSpawnArea spawnArea, INpcIntelligence intelligence) + { + var map = this._map!; + var monster = new Monster(spawnArea, spawnArea.MonsterDefinition!, map, this._gameContext.DropGenerator, intelligence, this._gameContext.PlugInManager, this._gameContext.PathFinderPool, this); + try + { + monster.Initialize(); + await map.AddAsync(monster).ConfigureAwait(false); + monster.OnSpawn(); + this._eventMonsters.TryAdd(monster, 0); + monster.Died += this.OnEventMonsterDied; + return monster; + } + catch (Exception ex) + { + this._logger.LogError(ex, "Couldn't spawn {monster} of the crywolf event.", spawnArea.MonsterDefinition); + monster.Dispose(); + return null; + } + } + + private async ValueTask RemoveEventMonstersAsync() + { + this._balgass = null; + this._groups.Clear(); + foreach (var monster in this._eventMonsters.Keys) + { + monster.Died -= this.OnEventMonsterDied; + if (monster.IsAlive) + { + await monster.CurrentMap.RemoveAsync(monster).ConfigureAwait(false); + monster.Dispose(); + } + } + + this._eventMonsters.Clear(); + } + + /// + /// Removes the common monsters of the map during the war, like the original game does. + /// It's done in each step, because dead monsters respawn after some time. + /// + private async ValueTask RemoveCommonMonstersAsync() + { + if (this._map is not { } map) + { + return; + } + + var commonMonsters = map.GetNpcsInRange(new Point(128, 128), byte.MaxValue) + .OfType() + .Where(monster => monster.SpawnArea.SpawnTrigger == SpawnTrigger.Automatic && monster.IsAlive) + .ToList(); + foreach (var monster in commonMonsters) + { + await map.RemoveAsync(monster).ConfigureAwait(false); + this._removedMonsters.TryAdd(monster, 0); + } + } + + private async ValueTask RestoreCommonMonstersAsync() + { + if (this._map is not { } map) + { + return; + } + + foreach (var monster in this._removedMonsters.Keys) + { + try + { + monster.Initialize(); + await map.AddAsync(monster).ConfigureAwait(false); + monster.OnSpawn(); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Couldn't restore {monster} after the crywolf event.", monster); + } + } + + this._removedMonsters.Clear(); + } + + private void OnEventMonsterDied(object? sender, DeathInformation e) + { + if (sender is not Monster monster) + { + return; + } + + monster.Died -= this.OnEventMonsterDied; + this._eventMonsters.TryRemove(monster, out _); + if (monster == this._balgass) + { + this._balgassKillerName = e.KillerName; + Volatile.Write(ref this._isBalgassDead, 1); + } + + _ = this.OnEventMonsterDiedAsync(monster, e); + } + + private async Task OnEventMonsterDiedAsync(Monster monster, DeathInformation e) + { + try + { + if (this.State != CrywolfState.Start) + { + return; + } + + var killer = this._map?.GetObject(e.KillerId) as Player; + if (monster == this._balgass) + { + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfBalgassKilled), this._balgassKillerName ?? string.Empty).ConfigureAwait(false); + } + else if (monster.Definition.Number == this._definition.DarkElfNumber) + { + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfDarkElfKilled), monster.SpawnArea.WaveNumber, e.KillerName).ConfigureAwait(false); + } + else + { + // no message for the other monsters + } + + var score = this._definition.GetScore(monster.Definition.Number); + if (killer is not null && score > 0) + { + var newScore = this._scores.AddOrUpdate(killer, score, (_, current) => current + score); + await killer.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfCurrentScore), newScore).ConfigureAwait(false); + } + } + catch (Exception ex) + { + this._logger.LogError(ex, "Unexpected error when {monster} of the crywolf event died.", monster); + } + } + + private async ValueTask OnObjectAddedToMapAsync((GameMap Map, ILocateable Object) args) + { + if (args.Object is Player player) + { + await this.ShowCurrentStateAsync(player).ConfigureAwait(false); + } + } + + private ValueTask ShowStateToMapPlayersAsync() + { + var occupation = this.Occupation; + var state = this.State; + return this.ForEachMapPlayerAsync(player => player.InvokeViewPlugInAsync(p => p.ShowStateAsync(occupation, state)).AsTask()); + } + + /// + /// Shows a golden message to all players of the game server, like the original game does. + /// + private ValueTask ShowMessageToAllPlayersAsync(string messageKey) + { + return this.ShowMessageToAllPlayersAsync(player => player.GetLocalizedMessage(messageKey)); + } + + private async ValueTask ShowMessageToAllPlayersAsync(Func getMessage) + { + var players = await this._gameContext.GetPlayersAsync().ConfigureAwait(false); + foreach (var player in players) + { + try + { + var message = getMessage(player); + await player.InvokeViewPlugInAsync(p => p.ShowMessageAsync(message, MessageType.GoldenCenter)).ConfigureAwait(false); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Couldn't notify {player} about the crywolf event.", player); + } + } + } + + /// + /// Shows a golden message to the players of the crywolf map. + /// + private ValueTask ShowMessageToMapPlayersAsync(string messageKey, params object?[] arguments) + { + return this.ForEachMapPlayerAsync(player => + { + var message = player.GetLocalizedMessage(messageKey, arguments); + return player.InvokeViewPlugInAsync(p => p.ShowMessageAsync(message, MessageType.GoldenCenter)).AsTask(); + }); + } + + private async ValueTask ForEachMapPlayerAsync(Func action) + { + var players = await this._gameContext.GetPlayersAsync().ConfigureAwait(false); + foreach (var player in players.Where(player => player.CurrentMap is { } map && map == this._map)) + { + try + { + await action(player).ConfigureAwait(false); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Couldn't notify {player} about the crywolf event.", player); + } + } + } +} diff --git a/src/GameLogic/Crywolf/CrywolfContractChange.cs b/src/GameLogic/Crywolf/CrywolfContractChange.cs new file mode 100644 index 000000000..3abaaba90 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfContractChange.cs @@ -0,0 +1,26 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// The change of the contract of an altar of the crywolf event. +/// +public enum CrywolfContractChange +{ + /// + /// The contract didn't change. + /// + None, + + /// + /// The contract got valid. + /// + Validated, + + /// + /// The contract has been cancelled. + /// + Cancelled, +} diff --git a/src/GameLogic/Crywolf/CrywolfContractResult.cs b/src/GameLogic/Crywolf/CrywolfContractResult.cs new file mode 100644 index 000000000..27cc1b6bc --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfContractResult.cs @@ -0,0 +1,36 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// The result of a request to contract an altar of the crywolf event. +/// +public enum CrywolfContractResult +{ + /// + /// The elf is trying to contract the altar. The contract gets valid when the elf stays at the altar. + /// + Success, + + /// + /// The altar can't be contracted now, e.g. because it's contracted already or the event isn't in the right state. + /// + NotAvailable, + + /// + /// The character can't contract an altar, because of its class or level. + /// + NotQualified, + + /// + /// The altar can't be contracted yet, because its last contract ended a short time ago. + /// + Cooldown, + + /// + /// The character doesn't stand at the altar. + /// + WrongPosition, +} diff --git a/src/GameLogic/Crywolf/CrywolfEffect.cs b/src/GameLogic/Crywolf/CrywolfEffect.cs new file mode 100644 index 000000000..4bd37e623 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfEffect.cs @@ -0,0 +1,36 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// The effects of the crywolf event, which the client shows at objects, matching the values expected by the client. +/// +public enum CrywolfEffect : byte +{ + /// + /// The altar can be contracted. + /// + AltarEnabled = 21, + + /// + /// The altar can't be contracted anymore. + /// + AltarDisabled = 22, + + /// + /// The altar is contracted. + /// + AltarContracted = 23, + + /// + /// An elf is trying to contract the altar. + /// + AltarAttempt = 24, + + /// + /// The NPC is hidden, because the fortress isn't in peace. + /// + NpcHidden = 27, +} diff --git a/src/GameLogic/Crywolf/CrywolfEffectDisplay.cs b/src/GameLogic/Crywolf/CrywolfEffectDisplay.cs new file mode 100644 index 000000000..2d5975fc3 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfEffectDisplay.cs @@ -0,0 +1,79 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +using MUnique.OpenMU.GameLogic.NPC; + +/// +/// Shows an effect of the crywolf event at an NPC to the players which observe it. +/// +/// +/// The effects are sent separately, because the packet which adds the NPCs to the scope of a player +/// doesn't contain their effects. The effects which a player has seen are remembered, because the client +/// removes all effects of the event from an object, when it gets an effect which the object already has. +/// When the NPC gets out of the scope of the player, the client forgets its effects as well. +/// +internal sealed class CrywolfEffectDisplay +{ + private readonly Dictionary _shownEffects = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The NPC. + public CrywolfEffectDisplay(NonPlayerCharacter npc) + { + this.Npc = npc; + } + + /// + /// Gets the NPC. + /// + public NonPlayerCharacter Npc { get; } + + /// + /// Gets or sets the effect which should be shown, or null if no effect should be shown. + /// + public CrywolfEffect? Effect { get; set; } + + /// + /// Shows the current effect to the players which observe the NPC and haven't seen it yet. + /// + public async ValueTask UpdateAsync() + { + List observers; + using (await this.Npc.ObserverLock.ReaderLockAsync()) + { + observers = this.Npc.Observers.OfType().ToList(); + } + + foreach (var player in this._shownEffects.Keys.Except(observers).ToList()) + { + this._shownEffects.Remove(player); + } + + var effect = this.Effect; + foreach (var player in observers) + { + var isShown = this._shownEffects.TryGetValue(player, out var shownEffect); + if (isShown && shownEffect == effect) + { + continue; + } + + if (isShown) + { + await player.InvokeViewPlugInAsync(p => p.ShowEffectAsync(this.Npc, shownEffect, false)).ConfigureAwait(false); + this._shownEffects.Remove(player); + } + + if (effect is { } newEffect) + { + await player.InvokeViewPlugInAsync(p => p.ShowEffectAsync(this.Npc, newEffect, true)).ConfigureAwait(false); + this._shownEffects[player] = newEffect; + } + } + } +} diff --git a/src/GameLogic/Crywolf/CrywolfEventDefinition.cs b/src/GameLogic/Crywolf/CrywolfEventDefinition.cs new file mode 100644 index 000000000..9a73c2e53 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfEventDefinition.cs @@ -0,0 +1,328 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// Describes the run of the crywolf event. +/// +/// +/// It's configured at the , so it can be adapted in the admin panel. +/// The monsters of the event are defined by the monster spawns of the crywolf map with the trigger +/// and the wave numbers of the +/// and . The default values are the ones of the original game. +/// +public class CrywolfEventDefinition +{ + /// + /// Gets or sets the number of the crywolf map. + /// + public short MapNumber { get; set; } = 34; + + /// + /// Gets or sets the days of the week, on which the event starts. + /// + public IList StartDays { get; set; } = new List { DayOfWeek.Wednesday, DayOfWeek.Saturday }; + + /// + /// Gets or sets the times of the day (in the time zone of the server), at which the event starts with its first notification. + /// + public IList StartTimes { get; set; } = new List { new(20, 30) }; + + /// + /// Gets or sets the duration of the first notification of the upcoming attack. + /// + public TimeSpan Notify1Duration { get; set; } = TimeSpan.FromMinutes(10); + + /// + /// Gets or sets the duration of the second notification, in which the fortress prepares for the attack. + /// + public TimeSpan Notify2Duration { get; set; } = TimeSpan.FromMinutes(5); + + /// + /// Gets or sets the duration, in which the elves can contract the altars before the battle starts. + /// + public TimeSpan ReadyDuration { get; set; } = TimeSpan.FromSeconds(150); + + /// + /// Gets or sets the duration of the battle. + /// + public TimeSpan BattleDuration { get; set; } = TimeSpan.FromMinutes(15); + + /// + /// Gets or sets the duration, in which the result is shown. + /// + public TimeSpan EndDuration { get; set; } = TimeSpan.FromSeconds(15); + + /// + /// Gets or sets the duration, after which the event is finished and the common monsters return. + /// + public TimeSpan EndCycleDuration { get; set; } = TimeSpan.FromSeconds(10); + + /// + /// Gets or sets the interval, in which the notifications are repeated. + /// + public TimeSpan NotificationInterval { get; set; } = TimeSpan.FromSeconds(70); + + /// + /// Gets or sets the time after the start of the battle, at which the army starts to attack the statue. + /// + public TimeSpan StatueAttackDelay { get; set; } = TimeSpan.FromMinutes(5); + + /// + /// Gets or sets the time after the start of the battle, at which Balgass appears. + /// + public TimeSpan BalgassAppearanceDelay { get; set; } = TimeSpan.FromMinutes(10); + + /// + /// Gets or sets the number of the statue of the holy wolf. + /// + public short StatueNumber { get; set; } = 204; + + /// + /// Gets or sets the numbers of the altars, in the order which is expected by the client. + /// + public IList AltarNumbers { get; set; } = new List { 205, 206, 207, 208, 209 }; + + /// + /// Gets or sets the numbers of the character classes, which can contract an altar. + /// + public IList ContractCharacterClassNumbers { get; set; } = new List { 8, 10, 11 }; + + /// + /// Gets or sets the minimum level of a character to contract an altar. + /// + public int MinimumContractLevel { get; set; } = 260; + + /// + /// Gets or sets the time which an elf has to stay at the altar until the contract is valid. + /// + public TimeSpan ContractDelay { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// Gets or sets the number of contracts per altar and event. A failed attempt counts as contract as well. + /// + public int ContractsPerAltar { get; set; } = 2; + + /// + /// Gets or sets the time after a contract ended, in which the altar can't be contracted. + /// + public TimeSpan AltarCooldown { get; set; } = TimeSpan.FromSeconds(10); + + /// + /// Gets or sets the number of the Dark Elf, which leads a group of the army. + /// + public short DarkElfNumber { get; set; } = 340; + + /// + /// Gets or sets the groups of the army of Balgass. + /// + public IList MonsterGroups { get; set; } = new List + { + new() { WaveNumber = 1, AdvanceGoalX = 128, AdvanceGoalY = 45, AttackGoalX = 126, AttackGoalY = 35 }, + new() { WaveNumber = 2, AdvanceGoalX = 109, AdvanceGoalY = 42, AttackGoalX = 125, AttackGoalY = 27 }, + new() { WaveNumber = 3, AdvanceGoalX = 123, AdvanceGoalY = 84, AttackGoalX = 121, AttackGoalY = 19 }, + new() { WaveNumber = 4, AdvanceGoalX = 119, AdvanceGoalY = 87, AttackGoalX = 119, AttackGoalY = 87 }, + new() { WaveNumber = 6, AdvanceGoalX = 105, AdvanceGoalY = 32, AttackGoalX = 120, AttackGoalY = 38 }, + new() { WaveNumber = 7, AdvanceGoalX = 89, AdvanceGoalY = 32, AttackGoalX = 115, AttackGoalY = 32 }, + new() { WaveNumber = 8, AdvanceGoalX = 83, AdvanceGoalY = 29, AttackGoalX = 109, AttackGoalY = 40 }, + new() { WaveNumber = 9, AdvanceGoalX = 65, AdvanceGoalY = 34, AttackGoalX = 65, AttackGoalY = 34 }, + new() { WaveNumber = 10, AdvanceGoalX = 136, AdvanceGoalY = 38, AttackGoalX = 117, AttackGoalY = 27 }, + new() { WaveNumber = 11, AdvanceGoalX = 134, AdvanceGoalY = 19, AttackGoalX = 117, AttackGoalY = 37 }, + new() { WaveNumber = 12, AdvanceGoalX = 177, AdvanceGoalY = 37, AttackGoalX = 131, AttackGoalY = 31 }, + new() { WaveNumber = 13, AdvanceGoalX = 179, AdvanceGoalY = 33, AttackGoalX = 179, AttackGoalY = 33 }, + }; + + /// + /// Gets or sets the ballistas of the army, which bombard the fortress. + /// + public IList Ballistas { get; set; } = new List + { + new() { X = 122, Y = 90, TargetX = 121, TargetY = 52 }, + new() { X = 116, Y = 90, TargetX = 120, TargetY = 35 }, + new() { X = 62, Y = 37, TargetX = 101, TargetY = 31 }, + new() { X = 62, Y = 31, TargetX = 117, TargetY = 31 }, + new() { X = 183, Y = 36, TargetX = 141, TargetY = 32 }, + new() { X = 183, Y = 30, TargetX = 125, TargetY = 31 }, + }; + + /// + /// Gets or sets the maximum random offset of the bombarded point of a ballista. + /// + public int BallistaTargetSpread { get; set; } = 4; + + /// + /// Gets or sets the radius around the bombarded point, in which the players are hit. + /// + public int BallistaHitRadius { get; set; } = 6; + + /// + /// Gets or sets the radius around the bombarded point, in which the players see the arrow of the ballista. + /// + public int BallistaEffectRadius { get; set; } = 10; + + /// + /// Gets or sets the distance to the leader, above which the members of a group follow it. + /// + public int FollowLeaderDistance { get; set; } = 6; + + /// + /// Gets or sets the wave number of the monster spawn of Balgass. + /// + public byte BalgassWaveNumber { get; set; } = 5; + + /// + /// Gets or sets the x coordinate, to which Balgass marches. + /// + public byte BalgassGoalX { get; set; } = 121; + + /// + /// Gets or sets the y coordinate, to which Balgass marches. + /// + public byte BalgassGoalY { get; set; } = 36; + + /// + /// Gets or sets the scores which the players get for killing the monsters of the army. + /// + public IList MonsterScores { get; set; } = new List + { + new() { MonsterNumber = 340, Score = 3000 }, + new() { MonsterNumber = 341, Score = 700 }, + new() { MonsterNumber = 344, Score = 600 }, + new() { MonsterNumber = 345, Score = 600 }, + new() { MonsterNumber = 348, Score = 1000 }, + new() { MonsterNumber = 349, Score = 7000 }, + }; + + /// + /// Gets or sets the score, which an elf gets for having contracted an altar until the end of the battle. + /// + public int ContractorScore { get; set; } = 6000; + + /// + /// Gets or sets the minimum scores of the ranks D, C, B, A and S. + /// + public IList RankScores { get; set; } = new List { 0, 1001, 3001, 5001, 10001 }; + + /// + /// Gets or sets the experience of the ranks D, C, B, A and S. + /// + public IList RankExperience { get; set; } = new List { 0, 200_000, 800_000, 1_200_000, 1_800_000 }; + + /// + /// Gets or sets the percentage of the rank experience, which the players get when the fortress has been occupied. + /// + public int FailedExperiencePercentage { get; set; } = 10; + + /// + /// Gets or sets the number of heroes, which are shown in the result and rewarded. + /// + public int HeroCount { get; set; } = 5; + + /// + /// Gets or sets the group of the item, which is dropped for the contractors and heroes when the fortress has been defended. + /// + public byte RewardItemGroup { get; set; } = 14; + + /// + /// Gets or sets the number of the item, which is dropped for the contractors and heroes when the fortress has been defended. + /// + public short RewardItemNumber { get; set; } = 13; + + /// + /// Gets or sets the numbers of the NPCs of the crywolf map, which stay visible when the fortress is occupied. + /// + public IList AlwaysVisibleNpcNumbers { get; set; } = new List { 406, 407 }; + + /// + /// Gets or sets the occupation state of the fortress. It's the result of the last event, which is kept until the next one. + /// + /// + /// It's saved by the event, so that it survives a restart of the server. + /// + public CrywolfOccupationState Occupation { get; set; } = CrywolfOccupationState.Peace; + + /// + /// Determines whether the event starts at the specified local time of the server. + /// + /// The local time of the server. + /// true, if the event starts at the specified time; otherwise, false. + public bool IsStartTime(DateTime localTime) + { + var time = TimeOnly.FromDateTime(localTime); + var earlier = time.Add(TimeSpan.FromSeconds(-5)); + return this.StartDays.Contains(localTime.DayOfWeek) + && this.StartTimes.Any(startTime => startTime.IsBetween(earlier, time)); + } + + /// + /// Gets the duration of the specified state. + /// + /// The state. + /// The duration of the state, or if the state has no duration. + public TimeSpan GetDuration(CrywolfState state) + { + return state switch + { + CrywolfState.Notify1 => this.Notify1Duration, + CrywolfState.Notify2 => this.Notify2Duration, + CrywolfState.Ready => this.ReadyDuration, + CrywolfState.Start => this.BattleDuration, + CrywolfState.End => this.EndDuration, + CrywolfState.EndCycle => this.EndCycleDuration, + _ => TimeSpan.Zero, + }; + } + + /// + /// Gets the score which a player gets for killing a monster. + /// + /// The number of the monster. + /// The score. + public int GetScore(short monsterNumber) + { + return this.MonsterScores.FirstOrDefault(score => score.MonsterNumber == monsterNumber)?.Score ?? 0; + } + + /// + /// Gets the rank (0 = D to 4 = S) of a score. + /// + /// The score. + /// The rank. + public int GetRank(int score) + { + var rank = 0; + for (var i = 0; i < this.RankScores.Count; i++) + { + if (score >= this.RankScores[i]) + { + rank = i; + } + } + + return Math.Min(rank, 4); + } + + /// + /// Gets the experience which a player gets for its rank. + /// + /// The rank. + /// If set to true, the fortress has been defended. + /// The experience. + public int GetExperience(int rank, bool isDefended) + { + var experience = rank >= 0 && rank < this.RankExperience.Count ? this.RankExperience[rank] : 0; + return isDefended ? experience : (int)((long)experience * this.FailedExperiencePercentage / 100); + } + + /// + /// Gets the index (0 to 4) of an altar. + /// + /// The number of the altar. + /// The index of the altar, or -1 if it's no altar. + public int GetAltarIndex(short monsterNumber) + { + return this.AltarNumbers.IndexOf(monsterNumber); + } +} diff --git a/src/GameLogic/Crywolf/CrywolfHero.cs b/src/GameLogic/Crywolf/CrywolfHero.cs new file mode 100644 index 000000000..e6ea49135 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfHero.cs @@ -0,0 +1,13 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// A hero of the crywolf event, which is one of the players with the highest scores. +/// +/// The name of the character. +/// The score. +/// The number of the character class. +public record CrywolfHero(string Name, int Score, byte CharacterClassNumber); diff --git a/src/GameLogic/Crywolf/CrywolfMonsterGroup.cs b/src/GameLogic/Crywolf/CrywolfMonsterGroup.cs new file mode 100644 index 000000000..b476159ff --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfMonsterGroup.cs @@ -0,0 +1,49 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// A group of the army of Balgass, which is led by a Dark Elf. +/// +/// +/// The members of a group are defined by the monster spawns of the crywolf map with the trigger +/// and the of the group. +/// When the battle starts, the leader marches to the /. +/// When the army starts to attack the statue, it marches to the /. +/// The other members of the group follow their leader. +/// +public class CrywolfMonsterGroup +{ + /// + /// Gets or sets the wave number of the monster spawns of the group. + /// + public byte WaveNumber { get; set; } + + /// + /// Gets or sets the x coordinate, to which the leader marches when the battle starts. + /// + public byte AdvanceGoalX { get; set; } + + /// + /// Gets or sets the y coordinate, to which the leader marches when the battle starts. + /// + public byte AdvanceGoalY { get; set; } + + /// + /// Gets or sets the x coordinate, to which the leader marches when the army attacks the statue. + /// + public byte AttackGoalX { get; set; } + + /// + /// Gets or sets the y coordinate, to which the leader marches when the army attacks the statue. + /// + public byte AttackGoalY { get; set; } + + /// + public override string ToString() + { + return $"Group {this.WaveNumber}: ({this.AdvanceGoalX}, {this.AdvanceGoalY}), ({this.AttackGoalX}, {this.AttackGoalY})"; + } +} diff --git a/src/GameLogic/Crywolf/CrywolfMonsterGroupState.cs b/src/GameLogic/Crywolf/CrywolfMonsterGroupState.cs new file mode 100644 index 000000000..2d0ef7d27 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfMonsterGroupState.cs @@ -0,0 +1,69 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +using System.Collections.Concurrent; +using MUnique.OpenMU.GameLogic.NPC; + +/// +/// The state of a group of the army of Balgass during an event. +/// +internal sealed class CrywolfMonsterGroupState +{ + private readonly ConcurrentDictionary _monsters = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The definition of the group. + /// The spawn areas of the members of the group. + public CrywolfMonsterGroupState(CrywolfMonsterGroup definition, IReadOnlyList spawnAreas) + { + this.Definition = definition; + this.SpawnAreas = spawnAreas; + } + + /// + /// Gets the definition of the group. + /// + public CrywolfMonsterGroup Definition { get; } + + /// + /// Gets the spawn areas of the members of the group. + /// + public IReadOnlyList SpawnAreas { get; } + + /// + /// Gets the leader of the group. + /// + public Monster? Leader { get; private set; } + + /// + /// Sets the current monster of a spawn area. + /// + /// The spawn area. + /// The monster. + /// If set to true, the monster is the leader of the group. + public void SetMonster(MonsterSpawnArea spawnArea, Monster monster, bool isLeader) + { + this._monsters[spawnArea] = monster; + if (isLeader) + { + this.Leader = monster; + } + } + + /// + /// Gets the spawn areas of the dead members, except the leader. + /// + /// The spawn areas of the dead members. + public IEnumerable GetDeadMembers() + { + return this._monsters + .Where(entry => !entry.Value.IsAlive && entry.Value != this.Leader) + .Select(entry => entry.Key) + .ToList(); + } +} diff --git a/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs b/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs new file mode 100644 index 000000000..f84f719a2 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs @@ -0,0 +1,271 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +using System.Threading; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.GameLogic.NPC; +using MUnique.OpenMU.Pathfinding; + +/// +/// The intelligence of the monsters of the army of Balgass during the crywolf event. +/// +/// +/// The monsters don't move or attack until the battle starts. Then, like in the original game: +/// +/// The Dark Elf which leads a group marches to the goal of the group and revives the dead members of its group. +/// The soldiers follow their leader and attack the players around them. +/// The ballistas bombard a point of the fortress. +/// Balgass marches to the statue. +/// +/// The goals are too far away for the path finder, so the monsters walk along the waypoints of the map. +/// +public sealed class CrywolfMonsterIntelligence : INpcIntelligence, IDisposable +{ + /// + /// The distance to the goal, below which a monster walks directly to it. + /// + private const int DirectWalkDistance = 10; + + /// + /// The maximum distance of a waypoint, which a monster walks to. + /// + private const int WaypointDistance = 20; + + private readonly CrywolfContext _context; + private readonly CrywolfMonsterGroup? _group; + private readonly CrywolfBallista? _ballista; + private readonly ILogger _logger; + private Timer? _timer; + private Monster? _monster; + private IAttackable? _attacker; + private int _isRunning; + + /// + /// Initializes a new instance of the class. + /// + /// The context of the event. + /// The role of the monster. + /// The group of the monster. + /// The definition of the ballista, if the monster is a ballista. + /// The logger. + public CrywolfMonsterIntelligence(CrywolfContext context, CrywolfMonsterRole role, CrywolfMonsterGroup? group, CrywolfBallista? ballista, ILogger logger) + { + this._context = context; + this.Role = role; + this._group = group; + this._ballista = ballista; + this._logger = logger; + } + + /// + /// Gets the role of the monster. + /// + public CrywolfMonsterRole Role { get; } + + /// + public NonPlayerCharacter Npc + { + get => this.Monster; + set => this.Monster = (Monster)value; + } + + /// + /// Gets or sets the monster. + /// + public Monster Monster + { + get => this._monster ?? throw new InvalidOperationException("Instance is not initialized with a Monster yet"); + set => this._monster = value; + } + + /// + public bool CanWalkOnSafezone => false; + + /// + public void RegisterHit(IAttacker attacker) + { + if (attacker is IAttackable attackable) + { + this._attacker = attackable; + } + } + + /// + public void Start() + { + this._timer ??= new Timer(state => _ = this.SafeTickAsync(), null, this.Monster.Definition.AttackDelay, this.Monster.Definition.AttackDelay); + } + + /// + /// + /// The army keeps marching when no player observes it, so pausing is ignored. + /// + public void Pause() + { + // intentionally left blank. + } + + /// + public bool CanWalkOn(Point target) + { + return (this.Monster.CurrentMap.Terrain.AIgrid[target.X, target.Y] & 1) == 1; + } + + /// + public void Dispose() + { + this._timer?.Dispose(); + this._timer = null; + } + + /// + /// Gets the point, to which a monster walks next to reach its goal. + /// When the goal is far away, it's the waypoint near the monster, which is the closest to the goal. + /// + /// The position of the monster. + /// The goal. + /// The point to walk to. + internal static Point GetNextWalkTarget(Point position, Point goal) + { + var distanceToGoal = position.EuclideanDistanceTo(goal); + if (distanceToGoal <= DirectWalkDistance) + { + return goal; + } + + var waypoint = CrywolfMovePath.Waypoints + .Where(point => position.EuclideanDistanceTo(point) < WaypointDistance) + .MinBy(point => point.EuclideanDistanceTo(goal)); + return waypoint != default && waypoint.EuclideanDistanceTo(goal) < distanceToGoal ? waypoint : goal; + } + + /// + /// Executes one step of the intelligence. It's called periodically by a timer. + /// + internal async ValueTask TickAsync() + { + if (this._monster is not { IsAlive: true } monster + || this._context.ArmyStage == CrywolfArmyStage.Waiting + || monster.Attributes[Stats.IsStunned] > 0 + || monster.Attributes[Stats.IsAsleep] > 0) + { + return; + } + + if (this.Role == CrywolfMonsterRole.Ballista) + { + if (this._ballista is { } ballista) + { + await this._context.BombardAsync(monster, ballista).ConfigureAwait(false); + } + + return; + } + + if (this.Role == CrywolfMonsterRole.Leader && this._group is { } group) + { + await this._context.ReviveGroupMembersAsync(group.WaveNumber).ConfigureAwait(false); + } + + if (monster.IsWalking) + { + return; + } + + if (await this.SearchTargetAsync(monster).ConfigureAwait(false) is { } target) + { + await monster.AttackAsync(target).ConfigureAwait(false); + return; + } + + if (monster.Attributes[Stats.IsFrozen] > 0 || this.GetGoal(monster) is not { } goal || monster.Position == goal) + { + return; + } + + var walkTarget = GetNextWalkTarget(monster.Position, goal); + if (!await monster.WalkToAsync(walkTarget).ConfigureAwait(false) && walkTarget != goal) + { + // The waypoint can't be reached, e.g. because of the terrain. The goal might be reachable. + await monster.WalkToAsync(goal).ConfigureAwait(false); + } + } + + private static bool IsValidTarget(Monster monster, IAttackable target) + { + return target is Player { IsInvisible: false } + && target.IsActive() + && !target.IsAtSafezone() + && target.IsInRange(monster.Position, monster.Definition.AttackRange) + && monster.HasLineOfSightTo(target); + } + + private Point? GetGoal(Monster monster) + { + var definition = this._context.Definition; + switch (this.Role) + { + case CrywolfMonsterRole.Balgass: + return new Point(definition.BalgassGoalX, definition.BalgassGoalY); + case CrywolfMonsterRole.Leader when this._group is { } group: + return this._context.ArmyStage == CrywolfArmyStage.AttackingStatue + ? new Point(group.AttackGoalX, group.AttackGoalY) + : new Point(group.AdvanceGoalX, group.AdvanceGoalY); + case CrywolfMonsterRole.Soldier when this._group is { } group: + if (this._context.GetLeader(group.WaveNumber) is { } leader + && !leader.IsInRange(monster.Position, definition.FollowLeaderDistance)) + { + return leader.Position; + } + + return null; + default: + return null; + } + } + + private async ValueTask SearchTargetAsync(Monster monster) + { + if (this._attacker is { } attacker && IsValidTarget(monster, attacker)) + { + return attacker; + } + + this._attacker = null; + List candidates; + using (await monster.ObserverLock.ReaderLockAsync()) + { + candidates = monster.Observers.OfType().Where(candidate => IsValidTarget(monster, candidate)).ToList(); + } + + return candidates.MinBy(candidate => candidate.GetDistanceTo(monster)); + } + + private async Task SafeTickAsync() + { + if (Interlocked.Exchange(ref this._isRunning, 1) != 0) + { + return; + } + + try + { + await this.TickAsync().ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // expected during shutdown. + } + catch (Exception ex) + { + this._logger.LogError(ex, "Unexpected error in the crywolf intelligence of {monster}.", this._monster); + } + finally + { + Interlocked.Exchange(ref this._isRunning, 0); + } + } +} diff --git a/src/GameLogic/Crywolf/CrywolfMonsterRole.cs b/src/GameLogic/Crywolf/CrywolfMonsterRole.cs new file mode 100644 index 000000000..7435aa106 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfMonsterRole.cs @@ -0,0 +1,31 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// The role of a monster of the army of Balgass. +/// +public enum CrywolfMonsterRole +{ + /// + /// The Dark Elf which leads a group: it marches to the goals of the group and revives its members. + /// + Leader, + + /// + /// A soldier which follows the leader of its group. + /// + Soldier, + + /// + /// A ballista which bombards a point of the fortress. + /// + Ballista, + + /// + /// Balgass, which marches to the statue. + /// + Balgass, +} diff --git a/src/GameLogic/Crywolf/CrywolfMonsterScore.cs b/src/GameLogic/Crywolf/CrywolfMonsterScore.cs new file mode 100644 index 000000000..0d4ff05dd --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfMonsterScore.cs @@ -0,0 +1,27 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// The score which a player gets for killing a monster of the army of Balgass. +/// +public class CrywolfMonsterScore +{ + /// + /// Gets or sets the number of the monster. + /// + public short MonsterNumber { get; set; } + + /// + /// Gets or sets the score. + /// + public int Score { get; set; } + + /// + public override string ToString() + { + return $"{this.MonsterNumber}: {this.Score}"; + } +} diff --git a/src/GameLogic/Crywolf/CrywolfMovePath.cs b/src/GameLogic/Crywolf/CrywolfMovePath.cs new file mode 100644 index 000000000..743627032 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfMovePath.cs @@ -0,0 +1,245 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +using MUnique.OpenMU.Pathfinding; + +/// +/// The waypoints of the crywolf map, along which the monsters of the army of Balgass march to their goals. +/// +/// +/// The goals of the monsters are too far away for the path finder. So, like in the original game, +/// a monster walks to the waypoint near it, which is the closest to its goal, until it's near the goal. +/// +internal static class CrywolfMovePath +{ + /// + /// Gets the waypoints. + /// + public static IReadOnlyList Waypoints { get; } = + [ + new(43, 61), + new(47, 55), + new(54, 46), + new(63, 40), + new(70, 41), + new(79, 33), + new(79, 52), + new(87, 63), + new(86, 81), + new(82, 97), + new(77, 112), + new(70, 112), + new(80, 138), + new(88, 149), + new(99, 169), + new(108, 186), + new(112, 200), + new(115, 168), + new(120, 155), + new(123, 141), + new(123, 126), + new(120, 112), + new(116, 103), + new(111, 98), + new(116, 88), + new(114, 76), + new(102, 76), + new(136, 74), + new(152, 63), + new(125, 75), + new(145, 67), + new(162, 55), + new(170, 47), + new(173, 37), + new(183, 35), + new(192, 42), + new(199, 48), + new(207, 59), + new(205, 49), + new(212, 69), + new(215, 78), + new(216, 89), + new(227, 89), + new(235, 100), + new(235, 121), + new(222, 120), + new(210, 105), + new(189, 83), + new(175, 83), + new(203, 86), + new(164, 85), + new(154, 83), + new(142, 83), + new(213, 116), + new(207, 123), + new(204, 130), + new(202, 138), + new(197, 144), + new(185, 150), + new(176, 151), + new(166, 150), + new(164, 140), + new(162, 127), + new(158, 120), + new(153, 111), + new(148, 104), + new(144, 98), + new(138, 89), + new(125, 86), + new(158, 155), + new(153, 159), + new(148, 164), + new(145, 170), + new(144, 176), + new(143, 182), + new(142, 192), + new(135, 198), + new(123, 201), + new(156, 193), + new(164, 193), + new(176, 194), + new(184, 199), + new(196, 199), + new(205, 200), + new(217, 193), + new(224, 186), + new(232, 174), + new(235, 159), + new(235, 140), + new(229, 128), + new(235, 109), + new(160, 206), + new(145, 204), + new(153, 215), + new(144, 220), + new(143, 211), + new(169, 208), + new(170, 220), + new(161, 224), + new(111, 213), + new(99, 199), + new(86, 201), + new(81, 208), + new(73, 214), + new(63, 209), + new(54, 203), + new(48, 196), + new(46, 183), + new(47, 175), + new(52, 166), + new(56, 159), + new(62, 152), + new(68, 146), + new(60, 113), + new(52, 118), + new(46, 123), + new(39, 126), + new(31, 122), + new(26, 119), + new(20, 114), + new(22, 102), + new(25, 93), + new(30, 84), + new(34, 76), + new(39, 68), + new(16, 123), + new(15, 132), + new(15, 140), + new(24, 130), + new(15, 148), + new(19, 159), + new(30, 170), + new(38, 174), + new(71, 22), + new(65, 29), + new(94, 159), + new(77, 130), + new(74, 123), + new(85, 89), + new(94, 80), + new(86, 71), + new(140, 71), + new(103, 208), + new(207, 94), + new(176, 166), + new(173, 176), + new(168, 185), + new(75, 27), + new(68, 212), + new(97, 178), + new(93, 188), + new(91, 195), + new(115, 181), + new(65, 200), + new(170, 200), + new(29, 133), + new(120, 63), + new(120, 58), + new(120, 52), + new(120, 48), + new(120, 43), + new(120, 38), + new(113, 46), + new(110, 42), + new(106, 38), + new(84, 32), + new(88, 31), + new(94, 31), + new(99, 31), + new(103, 31), + new(157, 32), + new(152, 32), + new(147, 32), + new(141, 31), + new(136, 31), + new(130, 31), + new(126, 31), + new(124, 26), + new(121, 25), + new(114, 31), + new(109, 31), + new(116, 36), + new(130, 21), + new(126, 16), + new(136, 23), + new(119, 15), + new(111, 17), + new(129, 45), + new(139, 38), + new(128, 38), + new(106, 23), + new(113, 22), + new(117, 27), + new(125, 36), + new(81, 28), + new(75, 20), + new(73, 16), + new(73, 36), + new(81, 39), + new(77, 44), + new(67, 36), + new(178, 37), + new(168, 37), + new(162, 32), + new(165, 33), + new(187, 39), + new(176, 32), + new(170, 30), + new(120, 69), + new(157, 59), + new(119, 79), + new(119, 74), + new(78, 24), + new(83, 57), + new(109, 79), + new(116, 69), + new(164, 39), + new(160, 35), + new(168, 43), + new(135, 42), + new(124, 117), + ]; +} diff --git a/src/GameLogic/Crywolf/CrywolfOccupationState.cs b/src/GameLogic/Crywolf/CrywolfOccupationState.cs new file mode 100644 index 000000000..2c0fa9ec7 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfOccupationState.cs @@ -0,0 +1,26 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// The occupation state of the crywolf fortress, matching the values expected by the client. +/// +public enum CrywolfOccupationState : byte +{ + /// + /// The fortress has been defended. + /// + Peace = 0, + + /// + /// The fortress has been occupied by Balgass. The common NPCs of the map are hidden. + /// + Occupied = 1, + + /// + /// The fortress is under attack. + /// + War = 2, +} diff --git a/src/GameLogic/Crywolf/CrywolfPlugIn.cs b/src/GameLogic/Crywolf/CrywolfPlugIn.cs new file mode 100644 index 000000000..84d479d53 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfPlugIn.cs @@ -0,0 +1,120 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic.PlugIns; +using MUnique.OpenMU.PlugIns; + +/// +/// The plugin of the crywolf event, in which the players defend the crywolf fortress against the army of Balgass. +/// +/// +/// The event starts at the configured times. The result of the event, the occupation state of the fortress, +/// is kept until the next event and saved in the configuration of this plugin. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.CrywolfPlugIn_Name), Description = nameof(PlugInResources.CrywolfPlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("3E8B5C27-9D41-4A6F-B2C8-7F1D6E0A9B34")] +public sealed class CrywolfPlugIn : IFeaturePlugIn, IPeriodicTaskPlugIn, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration, IDisposable +{ + private readonly ConcurrentDictionary _contexts = new(); + private readonly ConcurrentDictionary _runningTicks = new(); + + /// + public CrywolfEventDefinition? Configuration { get; set; } + + /// + /// Gets the context of the crywolf event of the game context, if it's running. + /// + /// The game context. + /// The context of the crywolf event. + public static CrywolfContext? GetContext(IGameContext gameContext) + { + var plugIn = gameContext.FeaturePlugIns.GetPlugIn(); + return plugIn is not null && plugIn._contexts.TryGetValue(gameContext, out var context) ? context : null; + } + + /// + public async ValueTask ExecuteTaskAsync(GameContext gameContext) + { + if (this._runningTicks.GetOrAdd(gameContext, 0) != 0 + || !this._runningTicks.TryUpdate(gameContext, 1, 0)) + { + return; + } + + try + { + var definition = this.Configuration ??= new CrywolfEventDefinition(); + if (!this._contexts.TryGetValue(gameContext, out var context)) + { + context = new CrywolfContext(gameContext, definition, d => SaveConfigurationAsync(gameContext, d)); + await context.InitializeAsync().ConfigureAwait(false); + this._contexts[gameContext] = context; + } + else + { + context.UpdateDefinition(definition); + } + + await context.TickAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + gameContext.LoggerFactory.CreateLogger().LogError(ex, "Unexpected error in the crywolf event."); + } + finally + { + this._runningTicks[gameContext] = 0; + } + } + + /// + public void ForceStart() + { + foreach (var context in this._contexts.Values) + { + context.SkipWaitingTime(); + } + } + + /// + public object CreateDefaultConfig() + { + return new CrywolfEventDefinition(); + } + + /// + public void Dispose() + { + foreach (var context in this._contexts.Values) + { + context.Dispose(); + } + + this._contexts.Clear(); + } + + /// + /// Saves the configuration, so that the occupation state of the fortress survives a restart of the server. + /// + private static async ValueTask SaveConfigurationAsync(GameContext gameContext, CrywolfEventDefinition definition) + { + // The configuration is loaded through a new context, so that the change is tracked and saved. + using var context = gameContext.PersistenceContextProvider.CreateNewContext(); + var typeId = typeof(CrywolfPlugIn).GUID; + var gameConfiguration = (await context.GetAsync().ConfigureAwait(false)).FirstOrDefault(); + if (gameConfiguration?.PlugInConfigurations.FirstOrDefault(c => c.TypeId == typeId) is not { } configuration) + { + gameContext.LoggerFactory.CreateLogger().LogWarning("The configuration of the crywolf event wasn't found, so its occupation state isn't saved."); + return; + } + + configuration.SetConfiguration(definition, gameContext.PlugInManager.CustomConfigReferenceHandler); + await context.SaveChangesAsync().ConfigureAwait(false); + } +} diff --git a/src/GameLogic/Crywolf/CrywolfState.cs b/src/GameLogic/Crywolf/CrywolfState.cs new file mode 100644 index 000000000..c54bf8d87 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfState.cs @@ -0,0 +1,46 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// The state of the crywolf event, matching the values expected by the client. +/// +public enum CrywolfState : byte +{ + /// + /// The event is not running. + /// + None = 0, + + /// + /// The players are notified about the upcoming attack of the army of Balgass. + /// + Notify1 = 1, + + /// + /// The fortress prepares for the attack: the common monsters of the map disappear. The client plays the intro of the event. + /// + Notify2 = 2, + + /// + /// The army of Balgass appeared and the elves can contract the altars. + /// + Ready = 3, + + /// + /// The army of Balgass attacks. + /// + Start = 4, + + /// + /// The battle ended and the result is shown. + /// + End = 5, + + /// + /// The event is finished: the army disappears and the common monsters of the map return. + /// + EndCycle = 6, +} diff --git a/src/GameLogic/Crywolf/CrywolfTerrain.cs b/src/GameLogic/Crywolf/CrywolfTerrain.cs new file mode 100644 index 000000000..c2daed1cd --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfTerrain.cs @@ -0,0 +1,276 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +using MUnique.OpenMU.DataModel.Configuration; + +/// +/// The changes of the terrain of the crywolf map, while the fortress isn't in peace. +/// +/// +/// The game client loads a terrain file for each occupation state by itself. The terrain files of +/// the war and the occupation only differ from the one of the peace in these areas: the safezone +/// of the fortress is removed, and some areas are blocked. They were taken from the terrain files. +/// +internal static class CrywolfTerrain +{ + /// + /// Gets the areas, which aren't a safezone while the fortress isn't in peace. + /// + public static IReadOnlyList<(byte X1, byte Y1, byte X2, byte Y2)> SafezoneRemovedAreas { get; } = + [ + new(92, 0, 157, 5), + new(92, 6, 156, 13), + new(91, 11, 91, 43), + new(92, 14, 155, 15), + new(90, 16, 90, 39), + new(92, 16, 92, 46), + new(94, 16, 155, 28), + new(93, 18, 93, 48), + new(86, 28, 89, 30), + new(94, 29, 153, 45), + new(89, 31, 89, 38), + new(87, 35, 88, 38), + new(154, 35, 155, 36), + new(154, 37, 154, 44), + new(94, 46, 152, 46), + new(94, 47, 151, 47), + new(94, 48, 150, 48), + new(94, 49, 149, 49), + new(95, 50, 149, 50), + new(96, 51, 147, 52), + new(96, 53, 146, 53), + new(97, 54, 145, 54), + new(98, 55, 144, 55), + new(99, 56, 143, 56), + new(101, 57, 141, 57), + new(102, 58, 140, 58), + new(104, 59, 139, 59), + new(107, 60, 137, 60), + new(110, 61, 136, 61), + new(114, 62, 135, 62), + new(115, 63, 125, 64), + new(116, 65, 117, 65), + new(124, 65, 125, 65), + new(101, 252, 110, 253), + new(96, 253, 100, 253), + new(111, 253, 111, 253), + new(141, 253, 151, 253), + ]; + + /// + /// Gets the areas, which are blocked while the fortress isn't in peace. + /// + public static IReadOnlyList<(byte X1, byte Y1, byte X2, byte Y2)> BlockedAreas { get; } = + [ + new(92, 0, 157, 5), + new(92, 6, 101, 6), + new(108, 6, 132, 6), + new(146, 6, 156, 6), + new(92, 7, 99, 8), + new(110, 7, 130, 7), + new(148, 7, 156, 7), + new(112, 8, 129, 8), + new(150, 8, 156, 9), + new(92, 9, 98, 9), + new(113, 9, 119, 9), + new(122, 9, 127, 10), + new(92, 10, 97, 15), + new(114, 10, 119, 10), + new(151, 10, 156, 13), + new(115, 11, 117, 12), + new(124, 11, 126, 11), + new(91, 12, 91, 28), + new(124, 12, 125, 12), + new(148, 12, 150, 29), + new(103, 13, 105, 14), + new(115, 13, 116, 13), + new(125, 13, 125, 13), + new(135, 13, 141, 14), + new(147, 13, 147, 21), + new(146, 14, 146, 20), + new(151, 14, 155, 28), + new(135, 15, 140, 15), + new(144, 15, 145, 19), + new(90, 16, 90, 28), + new(92, 16, 92, 28), + new(94, 16, 97, 22), + new(143, 16, 143, 18), + new(123, 17, 123, 24), + new(142, 17, 142, 18), + new(93, 18, 93, 28), + new(118, 18, 118, 24), + new(145, 20, 145, 20), + new(124, 21, 124, 24), + new(115, 22, 117, 23), + new(125, 22, 127, 23), + new(94, 23, 95, 28), + new(111, 24, 112, 29), + new(116, 24, 117, 24), + new(125, 24, 126, 24), + new(145, 24, 147, 29), + new(113, 25, 113, 29), + new(117, 25, 117, 25), + new(129, 25, 130, 29), + new(114, 26, 114, 28), + new(128, 26, 128, 28), + new(96, 27, 100, 28), + new(131, 27, 131, 29), + new(142, 27, 144, 28), + new(87, 28, 89, 28), + new(107, 28, 110, 29), + new(119, 28, 122, 35), + new(106, 29, 106, 29), + new(118, 29, 118, 34), + new(123, 29, 123, 34), + new(132, 29, 135, 29), + new(151, 29, 153, 29), + new(117, 30, 117, 33), + new(124, 30, 124, 33), + new(106, 34, 112, 34), + new(129, 34, 135, 34), + new(145, 34, 153, 45), + new(87, 35, 100, 36), + new(110, 35, 113, 35), + new(128, 35, 132, 35), + new(143, 35, 144, 36), + new(154, 35, 155, 36), + new(111, 36, 113, 37), + new(128, 36, 130, 36), + new(91, 37, 98, 37), + new(129, 37, 130, 37), + new(144, 37, 144, 55), + new(154, 37, 154, 43), + new(91, 38, 97, 43), + new(111, 38, 112, 38), + new(130, 38, 130, 38), + new(115, 39, 118, 41), + new(124, 39, 126, 41), + new(98, 40, 98, 54), + new(114, 40, 114, 41), + new(123, 40, 123, 45), + new(127, 40, 127, 40), + new(99, 41, 99, 55), + new(100, 42, 100, 56), + new(116, 42, 118, 42), + new(124, 42, 125, 42), + new(101, 43, 101, 45), + new(118, 43, 118, 45), + new(124, 43, 124, 45), + new(92, 44, 97, 46), + new(142, 45, 143, 55), + new(199, 45, 199, 45), + new(131, 46, 132, 62), + new(145, 46, 152, 46), + new(93, 47, 97, 48), + new(130, 47, 130, 62), + new(133, 47, 133, 62), + new(145, 47, 151, 47), + new(129, 48, 129, 50), + new(134, 48, 136, 61), + new(141, 48, 141, 57), + new(145, 48, 150, 48), + new(94, 49, 97, 49), + new(140, 49, 140, 58), + new(145, 49, 149, 50), + new(95, 50, 97, 50), + new(137, 50, 138, 59), + new(96, 51, 97, 52), + new(101, 51, 104, 56), + new(139, 51, 139, 59), + new(145, 51, 147, 52), + new(105, 52, 105, 59), + new(116, 52, 117, 65), + new(124, 52, 125, 65), + new(97, 53, 97, 53), + new(106, 53, 110, 59), + new(114, 53, 115, 61), + new(145, 53, 146, 53), + new(145, 54, 145, 54), + new(111, 55, 111, 61), + new(126, 55, 129, 61), + new(112, 56, 112, 61), + new(142, 56, 142, 56), + new(102, 57, 104, 57), + new(113, 57, 113, 61), + new(103, 58, 104, 58), + new(104, 59, 104, 59), + new(107, 60, 110, 60), + new(137, 60, 137, 60), + new(110, 61, 110, 61), + new(126, 62, 126, 62), + new(134, 62, 135, 62), + new(104, 66, 104, 66), + new(38, 83, 38, 83), + new(37, 84, 37, 84), + new(36, 85, 36, 85), + new(35, 86, 35, 95), + new(34, 87, 34, 87), + new(34, 91, 34, 91), + new(34, 93, 34, 95), + new(152, 105, 152, 105), + new(151, 119, 152, 119), + new(153, 120, 153, 121), + new(228, 123, 231, 129), + new(232, 124, 232, 132), + new(227, 125, 227, 127), + new(233, 125, 233, 132), + new(234, 129, 234, 132), + new(229, 130, 231, 131), + new(157, 133, 157, 133), + new(158, 134, 158, 135), + new(159, 135, 159, 135), + new(110, 155, 110, 155), + new(229, 155, 229, 155), + new(90, 161, 90, 161), + new(91, 162, 91, 162), + new(141, 165, 141, 166), + new(140, 167, 140, 167), + new(137, 178, 137, 178), + new(136, 179, 136, 179), + new(205, 193, 205, 193), + new(202, 194, 202, 194), + new(227, 194, 227, 195), + new(193, 196, 193, 196), + new(162, 235, 163, 235), + new(110, 238, 112, 238), + new(116, 238, 117, 238), + new(112, 239, 112, 239), + new(116, 239, 116, 239), + new(101, 252, 110, 253), + new(96, 253, 100, 253), + new(111, 253, 111, 253), + new(141, 253, 151, 253), + ]; + + /// + /// Applies the terrain of the occupation state to the terrain of the map. + /// + /// The terrain of the crywolf map. + /// If set to true, the terrain of the peace is applied; otherwise, the one of the war and the occupation. + public static void Apply(GameMapTerrain terrain, bool isPeace) + { + foreach (var (x1, y1, x2, y2) in SafezoneRemovedAreas) + { + ForEach(x1, y1, x2, y2, (x, y) => terrain.ApplyTerrainAttribute(x, y, TerrainAttributeType.Safezone, isPeace)); + } + + foreach (var (x1, y1, x2, y2) in BlockedAreas) + { + ForEach(x1, y1, x2, y2, (x, y) => terrain.ApplyTerrainAttribute(x, y, TerrainAttributeType.Blocked, !isPeace)); + } + } + + private static void ForEach(byte x1, byte y1, byte x2, byte y2, Action action) + { + for (var x = x1; x <= x2; x++) + { + for (var y = y1; y <= y2; y++) + { + action(x, y); + } + } + } +} diff --git a/src/GameLogic/Crywolf/ICrywolfEventViewPlugIn.cs b/src/GameLogic/Crywolf/ICrywolfEventViewPlugIn.cs new file mode 100644 index 000000000..5907eda4b --- /dev/null +++ b/src/GameLogic/Crywolf/ICrywolfEventViewPlugIn.cs @@ -0,0 +1,85 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +using MUnique.OpenMU.GameLogic.NPC; +using MUnique.OpenMU.GameLogic.Views; +using MUnique.OpenMU.Pathfinding; + +/// +/// The view plugin interface of the crywolf event. +/// +public interface ICrywolfEventViewPlugIn : IViewPlugIn +{ + /// + /// Shows the state of the event and the occupation state of the fortress. + /// It must only be sent to players on the crywolf map, because the client loads the terrain of the occupation for its current map. + /// + /// The occupation state. + /// The state of the event. + ValueTask ShowStateAsync(CrywolfOccupationState occupation, CrywolfState state); + + /// + /// Shows the shield of the statue and the states of the altars. + /// + /// The shield of the statue in percent. + /// The states of the altars, like . + ValueTask ShowStatueAndAltarsAsync(int statueShieldPercentage, IReadOnlyList altarStates); + + /// + /// Shows the result of the request to contract an altar. + /// + /// If set to true, the contract has been accepted. + /// The index of the altar (0 to 4). + /// The state of the altar, like . + ValueTask ShowContractResultAsync(bool success, int altarIndex, byte altarState); + + /// + /// Shows the remaining time of the battle. + /// + /// The remaining time. + ValueTask ShowRemainingTimeAsync(TimeSpan remainingTime); + + /// + /// Shows the health of Balgass and the number of the Dark Elves. + /// + /// The health of Balgass in percent, or -1 if Balgass isn't alive. + /// The number of alive Dark Elves. + ValueTask ShowBossMonsterInfoAsync(int balgassHealthPercentage, int darkElfCount); + + /// + /// Shows the rank and the rewarded experience of the player. + /// + /// The rank, from 0 (D) to 4 (S). + /// The rewarded experience. + ValueTask ShowPersonalRankAsync(int rank, int experience); + + /// + /// Shows the heroes with the highest scores. + /// + /// The heroes, ordered by their place. + ValueTask ShowHeroListAsync(IReadOnlyList heroes); + + /// + /// Shows the additional success rate of the chaos machine crafting. + /// + /// The additional success rate in percent. + ValueTask ShowChaosRateBenefitAsync(byte rate); + + /// + /// Shows the arrow of a ballista, which hits the target point. + /// + /// The ballista. + /// The target point. + ValueTask ShowBallistaAttackAsync(NonPlayerCharacter ballista, Point target); + + /// + /// Shows or hides an effect of the event at an object, e.g. the state of an altar. + /// + /// The object. + /// The effect. + /// If set to true, the effect is shown; otherwise, it's hidden. + ValueTask ShowEffectAsync(NonPlayerCharacter npc, CrywolfEffect effect, bool isActive); +} diff --git a/src/GameLogic/PlugIns/PlayerLosesExperienceAfterDeathPlugIn.cs b/src/GameLogic/PlugIns/PlayerLosesExperienceAfterDeathPlugIn.cs index a678cbfcd..f44f0e237 100644 --- a/src/GameLogic/PlugIns/PlayerLosesExperienceAfterDeathPlugIn.cs +++ b/src/GameLogic/PlugIns/PlayerLosesExperienceAfterDeathPlugIn.cs @@ -7,6 +7,7 @@ namespace MUnique.OpenMU.GameLogic.PlugIns; using System.Runtime.InteropServices; using MUnique.OpenMU.AttributeSystem; using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.GameLogic.Crywolf; using MUnique.OpenMU.GameLogic.NPC; using MUnique.OpenMU.PlugIns; @@ -41,9 +42,9 @@ public async ValueTask AttackableGotKilledAsync(IAttackable killed, IAttacker? k return; } - const int MAP_INDEX_CRYWOLF_FIRSTZONE = 34; - if (player.CurrentMap?.Definition.Number == MAP_INDEX_CRYWOLF_FIRSTZONE) + if (CrywolfPlugIn.GetContext(player.GameContext) is { } crywolf && crywolf.KeepsExperienceOnDeath(player.CurrentMap)) { + // During the battle of the crywolf event, the players don't lose experience on its map. return; } diff --git a/src/GameLogic/Properties/PlayerMessage.Designer.cs b/src/GameLogic/Properties/PlayerMessage.Designer.cs index 0de918dc1..b0499bf9e 100644 --- a/src/GameLogic/Properties/PlayerMessage.Designer.cs +++ b/src/GameLogic/Properties/PlayerMessage.Designer.cs @@ -177,6 +177,276 @@ public static string BloodCastleCrystalStatusDestroyed { } } + /// + /// Looks up a localized string similar to The altar ({0}) of the holy wolf can not be contracted yet.. + /// + public static string CrywolfAltarCooldown { + get { + return ResourceManager.GetString("CrywolfAltarCooldown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The army of Balgass attacks in {0} minute(s).. + /// + public static string CrywolfAttackStartsInMinutes { + get { + return ResourceManager.GetString("CrywolfAttackStartsInMinutes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The army of Balgass attacks in {0} second(s).. + /// + public static string CrywolfAttackStartsInSeconds { + get { + return ResourceManager.GetString("CrywolfAttackStartsInSeconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Balgass stayed alive until the end of the battle.. + /// + public static string CrywolfBalgassAlive { + get { + return ResourceManager.GetString("CrywolfBalgassAlive", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Balgass is coming! Protect the statue of the holy wolf!. + /// + public static string CrywolfBalgassAppeared { + get { + return ResourceManager.GetString("CrywolfBalgassAppeared", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Balgass has been defeated with the help of the heroes!. + /// + public static string CrywolfBalgassDefeated { + get { + return ResourceManager.GetString("CrywolfBalgassDefeated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Balgass has been killed by {0}!. + /// + public static string CrywolfBalgassKilled { + get { + return ResourceManager.GetString("CrywolfBalgassKilled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The barrier of the statue of the holy wolf has been created.. + /// + public static string CrywolfBarrierCreated { + get { + return ResourceManager.GetString("CrywolfBarrierCreated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The barrier of the statue of the holy wolf has disappeared.. + /// + public static string CrywolfBarrierDisappeared { + get { + return ResourceManager.GetString("CrywolfBarrierDisappeared", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Muses protecting the statue of the holy wolf: {0}, barrier: {1}%.. + /// + public static string CrywolfBarrierStatus { + get { + return ResourceManager.GetString("CrywolfBarrierStatus", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is trying to contract the altar ({1})!. + /// + public static string CrywolfContractAttempt { + get { + return ResourceManager.GetString("CrywolfContractAttempt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The contract is valid in {0} seconds. Do not move until it is completed.. + /// + public static string CrywolfContractAttemptInfo { + get { + return ResourceManager.GetString("CrywolfContractAttemptInfo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The connection to the altar ({0}) has been cancelled.. + /// + public static string CrywolfContractCancelled { + get { + return ResourceManager.GetString("CrywolfContractCancelled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Contracting an altar requires a Muse of level {0} or higher.. + /// + public static string CrywolfContractRequirement { + get { + return ResourceManager.GetString("CrywolfContractRequirement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The contracts of the wolf altars begin in {0} minute(s).. + /// + public static string CrywolfContractsStartInMinutes { + get { + return ResourceManager.GetString("CrywolfContractsStartInMinutes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The contracts of the wolf altars begin in {0} second(s).. + /// + public static string CrywolfContractsStartInSeconds { + get { + return ResourceManager.GetString("CrywolfContractsStartInSeconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are connected to the altar ({0}) of the holy wolf.. + /// + public static string CrywolfContractValid { + get { + return ResourceManager.GetString("CrywolfContractValid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} connected the Muse to the altar ({1}) of the holy wolf.. + /// + public static string CrywolfContractValidToOthers { + get { + return ResourceManager.GetString("CrywolfContractValidToOthers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You have to stand on the altar ({0}) of the holy wolf to contract it.. + /// + public static string CrywolfContractWrongPosition { + get { + return ResourceManager.GetString("CrywolfContractWrongPosition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your current MvP score is {0}.. + /// + public static string CrywolfCurrentScore { + get { + return ResourceManager.GetString("CrywolfCurrentScore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Dark Elf leader ({0}) of the army of Balgass has been killed by {1}!. + /// + public static string CrywolfDarkElfKilled { + get { + return ResourceManager.GetString("CrywolfDarkElfKilled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Defending has failed! The statue of the holy wolf has been taken over by Balgass.. + /// + public static string CrywolfDefenseFailed { + get { + return ResourceManager.GetString("CrywolfDefenseFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Defending has been successful! The statue of the holy wolf has been protected.. + /// + public static string CrywolfDefenseSucceeded { + get { + return ResourceManager.GetString("CrywolfDefenseSucceeded", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Without the protection of the Muses, the statue of the holy wolf will be stolen.. + /// + public static string CrywolfNoAltarContracted { + get { + return ResourceManager.GetString("CrywolfNoAltarContracted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Balgass has joined forces with Kundun's army and is preparing to attack the Crywolf Fortress.. + /// + public static string CrywolfNotify1 { + get { + return ResourceManager.GetString("CrywolfNotify1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The army of Balgass is ready to attack the Crywolf Fortress.. + /// + public static string CrywolfNotify2 { + get { + return ResourceManager.GetString("CrywolfNotify2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your MvP rank is {0}. You received {1} experience points.. + /// + public static string CrywolfRankResult { + get { + return ResourceManager.GetString("CrywolfRankResult", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The army of Balgass is marching towards the Crywolf Fortress.. + /// + public static string CrywolfReady { + get { + return ResourceManager.GetString("CrywolfReady", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The army of Balgass attacks the Crywolf Fortress!. + /// + public static string CrywolfStart { + get { + return ResourceManager.GetString("CrywolfStart", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The army of Balgass starts attacking the statue of the holy wolf!. + /// + public static string CrywolfStatueAttack { + get { + return ResourceManager.GetString("CrywolfStatueAttack", resourceCulture); + } + } + /// /// Looks up a localized string similar to The Ice Walker appeared! Kill it within one minute, or the following monsters become stronger.. /// diff --git a/src/GameLogic/Properties/PlayerMessage.resx b/src/GameLogic/Properties/PlayerMessage.resx index 875ae9c49..9a5ad4bb0 100644 --- a/src/GameLogic/Properties/PlayerMessage.resx +++ b/src/GameLogic/Properties/PlayerMessage.resx @@ -846,4 +846,94 @@ {0} minute(s) + + Balgass has joined forces with Kundun's army and is preparing to attack the Crywolf Fortress. + + + The army of Balgass is ready to attack the Crywolf Fortress. + + + The army of Balgass is marching towards the Crywolf Fortress. + + + The army of Balgass attacks the Crywolf Fortress! + + + The contracts of the wolf altars begin in {0} minute(s). + + + The contracts of the wolf altars begin in {0} second(s). + + + The army of Balgass attacks in {0} minute(s). + + + The army of Balgass attacks in {0} second(s). + + + Without the protection of the Muses, the statue of the holy wolf will be stolen. + + + The army of Balgass starts attacking the statue of the holy wolf! + + + Balgass is coming! Protect the statue of the holy wolf! + + + Balgass has been defeated with the help of the heroes! + + + Balgass stayed alive until the end of the battle. + + + Defending has been successful! The statue of the holy wolf has been protected. + + + Defending has failed! The statue of the holy wolf has been taken over by Balgass. + + + Your MvP rank is {0}. You received {1} experience points. + + + Your current MvP score is {0}. + + + Balgass has been killed by {0}! + + + The Dark Elf leader ({0}) of the army of Balgass has been killed by {1}! + + + {0} is trying to contract the altar ({1})! + + + The contract is valid in {0} seconds. Do not move until it is completed. + + + You are connected to the altar ({0}) of the holy wolf. + + + {0} connected the Muse to the altar ({1}) of the holy wolf. + + + The connection to the altar ({0}) has been cancelled. + + + The altar ({0}) of the holy wolf can not be contracted yet. + + + You have to stand on the altar ({0}) of the holy wolf to contract it. + + + Contracting an altar requires a Muse of level {0} or higher. + + + The barrier of the statue of the holy wolf has been created. + + + The barrier of the statue of the holy wolf has disappeared. + + + Muses protecting the statue of the holy wolf: {0}, barrier: {1}%. + diff --git a/src/GameLogic/Properties/PlugInResources.Designer.cs b/src/GameLogic/Properties/PlugInResources.Designer.cs index 5fa889ecf..30ea5cb5d 100644 --- a/src/GameLogic/Properties/PlugInResources.Designer.cs +++ b/src/GameLogic/Properties/PlugInResources.Designer.cs @@ -591,6 +591,24 @@ public static string CreateMonsterChatCommand_Name { } } + /// + /// Looks up a localized string similar to The event in the crywolf fortress: the players defend the statue of the holy wolf against the army of Balgass. The result is kept until the next event.. + /// + public static string CrywolfPlugIn_Description { + get { + return ResourceManager.GetString("CrywolfPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Crywolf Event. + /// + public static string CrywolfPlugIn_Name { + get { + return ResourceManager.GetString("CrywolfPlugIn_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Handles the chat command '/disconnect <char>'. Disconnects a player from the game server.. /// diff --git a/src/GameLogic/Properties/PlugInResources.resx b/src/GameLogic/Properties/PlugInResources.resx index db520a2c1..82b60597e 100644 --- a/src/GameLogic/Properties/PlugInResources.resx +++ b/src/GameLogic/Properties/PlugInResources.resx @@ -1308,4 +1308,10 @@ The event in the hatchery of raklion: when all spider eggs are destroyed, Selupan appears. After the battle, the hatchery is closed for some time. + + Crywolf Event + + + The event in the crywolf fortress: the players defend the statue of the holy wolf against the army of Balgass. The result is kept until the next event. + diff --git a/src/GameServer/MessageHandler/MiniGames/CrywolfChaosRateBenefitRequestHandlerPlugIn.cs b/src/GameServer/MessageHandler/MiniGames/CrywolfChaosRateBenefitRequestHandlerPlugIn.cs new file mode 100644 index 000000000..31da621c9 --- /dev/null +++ b/src/GameServer/MessageHandler/MiniGames/CrywolfChaosRateBenefitRequestHandlerPlugIn.cs @@ -0,0 +1,41 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.MessageHandler.MiniGames; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Crywolf; +using MUnique.OpenMU.GameServer.Properties; +using MUnique.OpenMU.Network.Packets.ClientToServer; +using MUnique.OpenMU.PlugIns; + +/// +/// Handler for 0xBD/0x09 — CrywolfChaosRateBenefitRequest, which the client sends when it opens a crafting dialog. +/// The server responds with the additional success rate of the crywolf event. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.CrywolfChaosRateBenefitRequestHandlerPlugIn_Name), Description = nameof(PlugInResources.CrywolfChaosRateBenefitRequestHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("B4E7D2C9-8A15-4F6B-9D3E-7C2A1F5B9E61")] +[BelongsToGroup(CrywolfGroupHandlerPlugIn.GroupKey)] +internal class CrywolfChaosRateBenefitRequestHandlerPlugIn : ISubPacketHandlerPlugIn +{ + /// + public bool IsEncryptionExpected => false; + + /// + public byte Key => CrywolfChaosRateBenefitRequest.SubCode; + + /// + public async ValueTask HandlePacketAsync(Player player, Memory packet) + { + if (packet.Length < CrywolfChaosRateBenefitRequest.Length) + { + return; + } + + var rate = CrywolfPlugIn.GetContext(player.GameContext)?.GetChaosRateBenefit() ?? 0; + await player.InvokeViewPlugInAsync(p => p.ShowChaosRateBenefitAsync(rate)).ConfigureAwait(false); + } +} diff --git a/src/GameServer/MessageHandler/MiniGames/CrywolfContractRequestHandlerPlugIn.cs b/src/GameServer/MessageHandler/MiniGames/CrywolfContractRequestHandlerPlugIn.cs new file mode 100644 index 000000000..81cc4e916 --- /dev/null +++ b/src/GameServer/MessageHandler/MiniGames/CrywolfContractRequestHandlerPlugIn.cs @@ -0,0 +1,42 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.MessageHandler.MiniGames; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Crywolf; +using MUnique.OpenMU.GameServer.Properties; +using MUnique.OpenMU.Network.Packets.ClientToServer; +using MUnique.OpenMU.PlugIns; + +/// +/// Handler for 0xBD/0x03 — CrywolfContractRequest, which an elf sends to contract an altar of the crywolf event. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.CrywolfContractRequestHandlerPlugIn_Name), Description = nameof(PlugInResources.CrywolfContractRequestHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("6F1B9A28-4D7C-4E53-B8A2-3C9E5F1D7B48")] +[BelongsToGroup(CrywolfGroupHandlerPlugIn.GroupKey)] +internal class CrywolfContractRequestHandlerPlugIn : ISubPacketHandlerPlugIn +{ + /// + public bool IsEncryptionExpected => false; + + /// + public byte Key => CrywolfContractRequest.SubCode; + + /// + public async ValueTask HandlePacketAsync(Player player, Memory packet) + { + if (packet.Length < CrywolfContractRequest.Length + || CrywolfPlugIn.GetContext(player.GameContext) is not { } context) + { + return; + } + + // The field is named after the statue, but the client sends the id of the altar. + CrywolfContractRequest request = packet; + await context.ContractAltarAsync(player, request.StatueId).ConfigureAwait(false); + } +} diff --git a/src/GameServer/MessageHandler/MiniGames/CrywolfGroupHandlerPlugIn.cs b/src/GameServer/MessageHandler/MiniGames/CrywolfGroupHandlerPlugIn.cs new file mode 100644 index 000000000..91c35dc49 --- /dev/null +++ b/src/GameServer/MessageHandler/MiniGames/CrywolfGroupHandlerPlugIn.cs @@ -0,0 +1,42 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.MessageHandler.MiniGames; + +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using MUnique.OpenMU.GameServer.Properties; +using MUnique.OpenMU.Network.PlugIns; +using MUnique.OpenMU.PlugIns; + +/// +/// Group packet handler for the 0xBD (Crywolf) packet group. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.CrywolfGroupHandlerPlugIn_Name), Description = nameof(PlugInResources.CrywolfGroupHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("5D2C8E14-6A9B-4F37-8C1E-9B4A7D2F6E03")] +internal class CrywolfGroupHandlerPlugIn : GroupPacketHandlerPlugIn +{ + /// + /// The group key for the crywolf packet group. + /// + internal const byte GroupKey = (byte)PacketType.CrywolfGroup; + + /// + /// Initializes a new instance of the class. + /// + /// The client version provider. + /// The manager. + /// The logger factory. + public CrywolfGroupHandlerPlugIn(IClientVersionProvider clientVersionProvider, PlugInManager manager, ILoggerFactory loggerFactory) + : base(clientVersionProvider, manager, loggerFactory) + { + } + + /// + public override bool IsEncryptionExpected => false; + + /// + public override byte Key => GroupKey; +} diff --git a/src/GameServer/MessageHandler/MiniGames/CrywolfInfoRequestHandlerPlugIn.cs b/src/GameServer/MessageHandler/MiniGames/CrywolfInfoRequestHandlerPlugIn.cs new file mode 100644 index 000000000..a19cc8b8c --- /dev/null +++ b/src/GameServer/MessageHandler/MiniGames/CrywolfInfoRequestHandlerPlugIn.cs @@ -0,0 +1,41 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.MessageHandler.MiniGames; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Crywolf; +using MUnique.OpenMU.GameServer.Properties; +using MUnique.OpenMU.Network.Packets.ClientToServer; +using MUnique.OpenMU.PlugIns; + +/// +/// Handler for 0xBD/0x00 — CrywolfInfoRequest, which the client sends when it entered the crywolf map. +/// The server responds with the state of the crywolf event. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.CrywolfInfoRequestHandlerPlugIn_Name), Description = nameof(PlugInResources.CrywolfInfoRequestHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("A8C3F517-2B6D-4E90-9F4A-1D7E3C5B8A26")] +[BelongsToGroup(CrywolfGroupHandlerPlugIn.GroupKey)] +internal class CrywolfInfoRequestHandlerPlugIn : ISubPacketHandlerPlugIn +{ + /// + public bool IsEncryptionExpected => false; + + /// + public byte Key => CrywolfInfoRequest.SubCode; + + /// + public async ValueTask HandlePacketAsync(Player player, Memory packet) + { + if (packet.Length < CrywolfInfoRequest.Length + || CrywolfPlugIn.GetContext(player.GameContext) is not { } context) + { + return; + } + + await context.ShowCurrentStateAsync(player).ConfigureAwait(false); + } +} diff --git a/src/GameServer/Properties/PlugInResources.Designer.cs b/src/GameServer/Properties/PlugInResources.Designer.cs index 7353ef0e1..205644cea 100644 --- a/src/GameServer/Properties/PlugInResources.Designer.cs +++ b/src/GameServer/Properties/PlugInResources.Designer.cs @@ -2004,6 +2004,96 @@ public static string CraftingDialogCloseRequestHandlerPlugIn_Name { } } + /// + /// Looks up a localized string similar to Handles the request of the client for the additional success rate of the chaos machine.. + /// + public static string CrywolfChaosRateBenefitRequestHandlerPlugIn_Description { + get { + return ResourceManager.GetString("CrywolfChaosRateBenefitRequestHandlerPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Crywolf Chaos Rate Benefit Request Handler. + /// + public static string CrywolfChaosRateBenefitRequestHandlerPlugIn_Name { + get { + return ResourceManager.GetString("CrywolfChaosRateBenefitRequestHandlerPlugIn_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Handles the request of an elf to contract an altar of the crywolf event.. + /// + public static string CrywolfContractRequestHandlerPlugIn_Description { + get { + return ResourceManager.GetString("CrywolfContractRequestHandlerPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Crywolf Contract Request Handler. + /// + public static string CrywolfContractRequestHandlerPlugIn_Name { + get { + return ResourceManager.GetString("CrywolfContractRequestHandlerPlugIn_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sends the state of the crywolf event, its altars and its result to the client.. + /// + public static string CrywolfEventViewPlugIn_Description { + get { + return ResourceManager.GetString("CrywolfEventViewPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Crywolf Event View. + /// + public static string CrywolfEventViewPlugIn_Name { + get { + return ResourceManager.GetString("CrywolfEventViewPlugIn_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Handles the packets of the crywolf event.. + /// + public static string CrywolfGroupHandlerPlugIn_Description { + get { + return ResourceManager.GetString("CrywolfGroupHandlerPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Crywolf Group Handler. + /// + public static string CrywolfGroupHandlerPlugIn_Name { + get { + return ResourceManager.GetString("CrywolfGroupHandlerPlugIn_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Handles the request of the client for the state of the crywolf event.. + /// + public static string CrywolfInfoRequestHandlerPlugIn_Description { + get { + return ResourceManager.GetString("CrywolfInfoRequestHandlerPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Crywolf Info Request Handler. + /// + public static string CrywolfInfoRequestHandlerPlugIn_Name { + get { + return ResourceManager.GetString("CrywolfInfoRequestHandlerPlugIn_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to The default implementation of the ICurrentlyActiveQuestsPlugIn which is forwarding everything to the game client with specific data packets.. /// diff --git a/src/GameServer/Properties/PlugInResources.resx b/src/GameServer/Properties/PlugInResources.resx index e29c17fd5..895da6db9 100644 --- a/src/GameServer/Properties/PlugInResources.resx +++ b/src/GameServer/Properties/PlugInResources.resx @@ -2499,4 +2499,34 @@ Handles the request of the client for the state of the raklion event. + + Crywolf Event View + + + Sends the state of the crywolf event, its altars and its result to the client. + + + Crywolf Group Handler + + + Handles the packets of the crywolf event. + + + Crywolf Info Request Handler + + + Handles the request of the client for the state of the crywolf event. + + + Crywolf Contract Request Handler + + + Handles the request of an elf to contract an altar of the crywolf event. + + + Crywolf Chaos Rate Benefit Request Handler + + + Handles the request of the client for the additional success rate of the chaos machine. + diff --git a/src/GameServer/RemoteView/World/CrywolfEventViewPlugIn.cs b/src/GameServer/RemoteView/World/CrywolfEventViewPlugIn.cs new file mode 100644 index 000000000..f61fe02e6 --- /dev/null +++ b/src/GameServer/RemoteView/World/CrywolfEventViewPlugIn.cs @@ -0,0 +1,141 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.RemoteView.World; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Crywolf; +using MUnique.OpenMU.GameLogic.NPC; +using MUnique.OpenMU.GameLogic.Views; +using MUnique.OpenMU.Network; +using MUnique.OpenMU.Network.Packets.ServerToClient; +using MUnique.OpenMU.Pathfinding; +using MUnique.OpenMU.PlugIns; + +/// +/// The default implementation of the which +/// sends the crywolf event packets (0xBD) to the client. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.CrywolfEventViewPlugIn_Name), Description = nameof(PlugInResources.CrywolfEventViewPlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("7B2E9D41-5C63-4F8A-A1D7-2E9F4B6C8A15")] +public sealed class CrywolfEventViewPlugIn : ICrywolfEventViewPlugIn +{ + /// + /// The key of the first altar, which the client expects. It uses the key minus this value as index into its altar states. + /// + private const ushort FirstAltarKey = 317; + + private readonly RemotePlayer _player; + + /// + /// Initializes a new instance of the class. + /// + /// The player. + public CrywolfEventViewPlugIn(RemotePlayer player) => this._player = player; + + /// + public async ValueTask ShowStateAsync(CrywolfOccupationState occupation, CrywolfState state) + { + // The states of the packet have the same values as the ones of the game logic. + await this._player.Connection.SendCrywolfInfoAsync((CrywolfInfo.OccupationState)(byte)occupation, (CrywolfInfo.CrywolfState)(byte)state).ConfigureAwait(false); + } + + /// + public async ValueTask ShowStatueAndAltarsAsync(int statueShieldPercentage, IReadOnlyList altarStates) + { + byte GetAltarState(int index) => index < altarStates.Count ? altarStates[index] : (byte)0; + + await this._player.Connection.SendCrywolfStatueAndAltarInfoAsync( + (uint)Math.Clamp(statueShieldPercentage, 0, 100), + GetAltarState(0), + GetAltarState(1), + GetAltarState(2), + GetAltarState(3), + GetAltarState(4)).ConfigureAwait(false); + } + + /// + public async ValueTask ShowContractResultAsync(bool success, int altarIndex, byte altarState) + { + await this._player.Connection.SendCrywolfContractResultAsync(success, altarState, (ushort)(FirstAltarKey + Math.Clamp(altarIndex, 0, 4))).ConfigureAwait(false); + } + + /// + public async ValueTask ShowRemainingTimeAsync(TimeSpan remainingTime) + { + // The client counts down the seconds of the current minute by itself, so it gets the started minutes. + var remainingSeconds = Math.Max(0, (int)Math.Ceiling(remainingTime.TotalSeconds)); + var minutes = remainingSeconds > 0 ? (remainingSeconds - 1) / 60 : 0; + await this._player.Connection.SendCrywolfLeftTimeAsync((byte)Math.Min(minutes / 60, byte.MaxValue), (byte)(minutes % 60)).ConfigureAwait(false); + } + + /// + public async ValueTask ShowBossMonsterInfoAsync(int balgassHealthPercentage, int darkElfCount) + { + await this._player.Connection.SendCrywolfBossMonsterInfoAsync(unchecked((uint)balgassHealthPercentage), (byte)Math.Clamp(darkElfCount, 0, byte.MaxValue)).ConfigureAwait(false); + } + + /// + public async ValueTask ShowPersonalRankAsync(int rank, int experience) + { + // The client shows images of the ranks D to S, so it must not get other values. + await this._player.Connection.SendCrywolfPersonalRankAsync((CrywolfPersonalRank.CrywolfRank)Math.Clamp(rank, 0, 4), (uint)Math.Max(0, experience)).ConfigureAwait(false); + } + + /// + public async ValueTask ShowHeroListAsync(IReadOnlyList heroes) + { + var connection = this._player.Connection; + if (connection is null) + { + return; + } + + // The client uses the rank as array index of five entries. + var count = Math.Min(heroes.Count, 5); + + int Write() + { + var size = CrywolfHeroListRef.GetRequiredSize(count); + var packet = new CrywolfHeroListRef(connection.Output.GetSpan(size)[..size]) + { + HeroCount = (byte)count, + }; + + for (var i = 0; i < count; i++) + { + var hero = heroes[i]; + var entry = packet[i]; + entry.Rank = (byte)i; + entry.Name = hero.Name; + entry.Score = (uint)Math.Max(0, hero.Score); + entry.CharacterClass = hero.CharacterClassNumber; + } + + return size; + } + + await connection.SendAsync(Write).ConfigureAwait(false); + } + + /// + public async ValueTask ShowChaosRateBenefitAsync(byte rate) + { + await this._player.Connection.SendCrywolfBenefitPlusChaosRateAsync(rate).ConfigureAwait(false); + } + + /// + public async ValueTask ShowBallistaAttackAsync(NonPlayerCharacter ballista, Point target) + { + await this._player.Connection.SendCrywolfRegionMonsterAttackAsync((ushort)ballista.Definition.Number, ballista.Position.X, ballista.Position.Y, target.X, target.Y).ConfigureAwait(false); + } + + /// + public async ValueTask ShowEffectAsync(NonPlayerCharacter npc, CrywolfEffect effect, bool isActive) + { + await this._player.Connection.SendMagicEffectStatusAsync(isActive, npc.GetId(this._player), (byte)effect).ConfigureAwait(false); + } +} From ec2263ddab27142f542d5047402bccfff2d28dcf Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:13:25 -0300 Subject: [PATCH 03/23] feat(crywolf): add the data of the crywolf event 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 --- .../Updates/AddCrywolfEventUpdatePlugIn.cs | 58 ++++++ .../Initialization/Updates/UpdateVersion.cs | 5 + .../Events/CrywolfInitializer.cs | 178 ++++++++++++++++++ .../VersionSeasonSix/Events/CrywolfSpawn.cs | 15 ++ .../VersionSeasonSix/Events/CrywolfSpawns.cs | 98 ++++++++++ .../GameConfigurationInitializer.cs | 1 + .../VersionSeasonSix/Maps/CrywolfFortress.cs | 18 +- 7 files changed, 367 insertions(+), 6 deletions(-) create mode 100644 src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs create mode 100644 src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfInitializer.cs create mode 100644 src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfSpawn.cs create mode 100644 src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfSpawns.cs diff --git a/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs new file mode 100644 index 000000000..66b7e85b9 --- /dev/null +++ b/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs @@ -0,0 +1,58 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Updates; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events; +using MUnique.OpenMU.PlugIns; + +/// +/// Adds the data of the crywolf event: the monsters of the army of Balgass and their spawns on the crywolf map. +/// The statue and the altars of the fortress are always on the map now, as NPCs which can't be attacked. +/// +[PlugIn] +[Display(Name = PlugInName, Description = PlugInDescription)] +[Guid("9C4E1A73-2F58-4B6D-8E09-5A3D7C1F2B84")] +public class AddCrywolfEventUpdatePlugIn : UpdatePlugInBase +{ + /// + /// The plug-in name. + /// + internal const string PlugInName = "Add Crywolf event"; + + /// + /// The plug-in description. + /// + internal const string PlugInDescription = "This update adds the monsters of the crywolf event and their spawns, and lets the statue and the altars of the fortress always be on the map."; + + /// + public override string Name => PlugInName; + + /// + public override string Description => PlugInDescription; + + /// + public override UpdateVersion Version => UpdateVersion.AddCrywolfEvent; + + /// + public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id; + + /// + public override bool IsMandatory => true; + + /// + public override DateTime CreatedAt => new(2026, 09, 28, 0, 0, 0, DateTimeKind.Utc); + + /// + protected override ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) + { + var initializer = new CrywolfInitializer(context, gameConfiguration); + initializer.CreateMonsters(); + initializer.ConfigureStatueAndAltars(); + initializer.CreateSpawns(); + return ValueTask.CompletedTask; + } +} diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs index 0256b82f5..70ee61eb4 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -604,4 +604,9 @@ public enum UpdateVersion /// The version of the . /// AddSelupanFallSkill = 120, + + /// + /// The version of the . + /// + AddCrywolfEvent = 121, } diff --git a/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfInitializer.cs b/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfInitializer.cs new file mode 100644 index 000000000..d1dd1a3b5 --- /dev/null +++ b/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfInitializer.cs @@ -0,0 +1,178 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events; + +using MUnique.OpenMU.AttributeSystem; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps; + +/// +/// The initializer for the crywolf event: it creates the monsters of the army of Balgass and their spawns, +/// and lets the statue and the altars of the fortress always be on the map. +/// +/// +/// All methods only create or change what's missing, so they can be applied to existing data as well. +/// +internal class CrywolfInitializer : InitializerBase +{ + /// + /// The number of the statue of the holy wolf. + /// + internal const short StatueNumber = 204; + + /// + /// The number of the last altar. + /// + internal const short LastAltarNumber = 209; + + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// The game configuration. + public CrywolfInitializer(IContext context, GameConfiguration gameConfiguration) + : base(context, gameConfiguration) + { + } + + /// + public override void Initialize() + { + this.CreateMonsters(); + this.CreateSpawns(); + } + + /// + /// Creates the monsters of the army of Balgass, which don't exist yet. The values are the ones of the original game. + /// + internal void CreateMonsters() + { + this.CreateMonster(340, "Dark Elf", 135, 1_500_000, 800, 900, 900, 1500, 370, 6, 6, 10, 23); + this.CreateMonster(341, "Soram", 134, 100_000, 600, 700, 500, 1500, 397, 6, 4, 7, 25); + this.CreateMonster(344, "Balram", 134, 90_000, 600, 700, 500, 1500, 370, 6, 7, 7, 23); + this.CreateMonster(345, "Death Spirit", 134, 95_000, 600, 700, 500, 1500, 397, 6, 6, 7, 25); + this.CreateMonster(348, "Tanker", 133, 450_000, 800, 1000, 840, 1500, 370, 0, 6, 7, 23); + this.CreateMonster(349, "Balgass", 135, 500_000, 1000, 1500, 800, 2000, 370, 6, 6, 7, 23); + } + + /// + /// Lets the statue and the altars of existing data always be on the crywolf map, as NPCs which can't be attacked. + /// The event shows their state. + /// + internal void ConfigureStatueAndAltars() + { + foreach (var monster in this.GameConfiguration.Monsters.Where(IsStatueOrAltar)) + { + monster.ObjectKind = NpcObjectKind.PassiveNpc; + } + + if (this.GetMap() is not { } map) + { + return; + } + + foreach (var spawn in map.MonsterSpawns.Where(spawn => spawn.MonsterDefinition is { } definition && IsStatueOrAltar(definition))) + { + spawn.SpawnTrigger = SpawnTrigger.Automatic; + } + } + + /// + /// Creates the spawns of the army of Balgass on the crywolf map, which don't exist yet. + /// + internal void CreateSpawns() + { + if (this.GetMap() is not { } map) + { + return; + } + + foreach (var spawn in CrywolfSpawns.Spawns) + { + var id = GuidHelper.CreateGuid(map.Number, spawn.Number); + if (map.MonsterSpawns.Any(area => area.GetId() == id) + || this.GameConfiguration.Monsters.FirstOrDefault(monster => monster.Number == spawn.MonsterNumber) is not { } monsterDefinition) + { + continue; + } + + var area = this.Context.CreateNew(); + area.SetGuid(map.Number, spawn.Number); + area.GameMap = map; + area.MonsterDefinition = monsterDefinition; + area.Quantity = 1; + area.Direction = Direction.Undefined; + area.SpawnTrigger = SpawnTrigger.OnceAtWaveStart; + area.WaveNumber = spawn.WaveNumber; + area.X1 = spawn.X; + area.X2 = spawn.X; + area.Y1 = spawn.Y; + area.Y2 = spawn.Y; + map.MonsterSpawns.Add(area); + } + } + + private static bool IsStatueOrAltar(MonsterDefinition monster) + { + return monster.Number is >= StatueNumber and <= LastAltarNumber; + } + + private GameMapDefinition? GetMap() + { + return this.GameConfiguration.Maps.FirstOrDefault(map => map.Number == CrywolfFortress.Number); + } + + private void CreateMonster( + short number, + string designation, + byte level, + int health, + int minimumDamage, + int maximumDamage, + int defense, + int attackRate, + int defenseRate, + byte moveRange, + byte attackRange, + short viewRange, + byte resistance) + { + if (this.GameConfiguration.Monsters.Any(monster => monster.Number == number)) + { + return; + } + + var monster = this.Context.CreateNew(); + this.GameConfiguration.Monsters.Add(monster); + monster.Number = number; + monster.Designation = designation; + monster.MoveRange = moveRange; + monster.AttackRange = attackRange; + monster.ViewRange = viewRange; + monster.MoveDelay = TimeSpan.FromMilliseconds(700); + monster.AttackDelay = TimeSpan.FromMilliseconds(1700); + monster.RespawnDelay = TimeSpan.FromSeconds(10); + monster.Attribute = 2; + monster.NumberOfMaximumItemDrops = 1; + var attributes = new Dictionary + { + { Stats.Level, level }, + { Stats.MaximumHealth, health }, + { Stats.MinimumPhysBaseDmg, minimumDamage }, + { Stats.MaximumPhysBaseDmg, maximumDamage }, + { Stats.DefenseBase, defense }, + { Stats.AttackRatePvm, attackRate }, + { Stats.DefenseRatePvm, defenseRate }, + { Stats.PoisonResistance, resistance / 255f }, + { Stats.IceResistance, resistance / 255f }, + { Stats.LightningResistance, resistance / 255f }, + { Stats.FireResistance, resistance / 255f }, + }; + + monster.AddAttributes(attributes, this.Context, this.GameConfiguration); + monster.SetGuid(monster.Number); + } +} diff --git a/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfSpawn.cs b/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfSpawn.cs new file mode 100644 index 000000000..9d2381e21 --- /dev/null +++ b/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfSpawn.cs @@ -0,0 +1,15 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events; + +/// +/// A monster spawn of the army of Balgass on the crywolf map. +/// +/// The number of the spawn. +/// The number of the monster. +/// The x coordinate. +/// The y coordinate. +/// The wave number, which is the number of the group. +internal readonly record struct CrywolfSpawn(short Number, short MonsterNumber, byte X, byte Y, byte WaveNumber); diff --git a/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfSpawns.cs b/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfSpawns.cs new file mode 100644 index 000000000..15832d545 --- /dev/null +++ b/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfSpawns.cs @@ -0,0 +1,98 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Events; + +/// +/// The monster spawns of the army of Balgass on the crywolf map, which are spawned by the crywolf event. +/// +/// +/// The wave number is the number of the group of the original game. The group 5 is Balgass, +/// the first spawn of each other group is its leader, the Dark Elf. +/// +internal static class CrywolfSpawns +{ + /// + /// Gets the spawns. + /// + public static IReadOnlyList Spawns { get; } = + [ + new(1020, 340, 110, 77, 1), + new(1021, 341, 108, 73, 1), + new(1022, 341, 110, 73, 1), + new(1023, 341, 112, 73, 1), + new(1024, 341, 109, 75, 1), + new(1025, 341, 111, 75, 1), + new(1040, 340, 125, 77, 2), + new(1041, 341, 125, 73, 2), + new(1042, 341, 123, 73, 2), + new(1043, 341, 127, 73, 2), + new(1044, 341, 124, 75, 2), + new(1045, 341, 126, 75, 2), + new(1060, 340, 119, 83, 3), + new(1061, 341, 119, 77, 3), + new(1062, 341, 117, 77, 3), + new(1063, 341, 121, 77, 3), + new(1064, 344, 118, 79, 3), + new(1065, 344, 120, 79, 3), + new(1066, 345, 116, 81, 3), + new(1067, 345, 119, 81, 3), + new(1068, 345, 122, 81, 3), + new(1080, 340, 119, 87, 4), + new(1081, 344, 119, 90, 4), + new(1082, 348, 122, 90, 4), + new(1083, 348, 116, 90, 4), + new(1100, 349, 110, 79, 5), + new(1120, 340, 77, 28, 6), + new(1121, 341, 81, 25, 6), + new(1122, 341, 81, 27, 6), + new(1123, 341, 81, 29, 6), + new(1124, 341, 79, 26, 6), + new(1125, 341, 79, 28, 6), + new(1140, 340, 77, 42, 7), + new(1141, 341, 79, 38, 7), + new(1142, 341, 81, 37, 7), + new(1143, 341, 81, 39, 7), + new(1144, 341, 81, 41, 7), + new(1145, 341, 79, 40, 7), + new(1160, 340, 70, 34, 8), + new(1161, 341, 77, 31, 8), + new(1162, 341, 77, 33, 8), + new(1163, 341, 77, 35, 8), + new(1164, 344, 75, 32, 8), + new(1165, 344, 75, 34, 8), + new(1166, 345, 73, 33, 8), + new(1167, 345, 73, 29, 8), + new(1168, 345, 73, 37, 8), + new(1180, 340, 65, 34, 9), + new(1181, 344, 62, 34, 9), + new(1182, 348, 62, 37, 9), + new(1183, 348, 62, 31, 9), + new(1200, 340, 167, 27, 10), + new(1201, 341, 163, 29, 10), + new(1202, 341, 163, 27, 10), + new(1203, 341, 163, 25, 10), + new(1204, 341, 165, 26, 10), + new(1205, 341, 165, 28, 10), + new(1220, 340, 167, 39, 11), + new(1221, 341, 165, 38, 11), + new(1222, 341, 163, 37, 11), + new(1223, 341, 163, 39, 11), + new(1224, 341, 165, 40, 11), + new(1225, 341, 163, 41, 11), + new(1240, 340, 174, 33, 12), + new(1241, 341, 167, 35, 12), + new(1242, 341, 167, 33, 12), + new(1243, 341, 167, 31, 12), + new(1244, 344, 169, 32, 12), + new(1245, 344, 169, 34, 12), + new(1246, 345, 171, 33, 12), + new(1247, 345, 171, 30, 12), + new(1248, 345, 171, 36, 12), + new(1260, 340, 179, 33, 13), + new(1261, 344, 183, 33, 13), + new(1262, 348, 183, 36, 13), + new(1263, 348, 183, 30, 13), + ]; +} diff --git a/src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs b/src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs index 97a3998fc..5cf7da298 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs @@ -92,6 +92,7 @@ public override void Initialize() new CastleSiegeInitializer(this.Context, this.GameConfiguration).Initialize(); new KanturuInitializer(this.Context, this.GameConfiguration).Initialize(); new DoppelgangerInitializer(this.Context, this.GameConfiguration).Initialize(); + new CrywolfInitializer(this.Context, this.GameConfiguration).Initialize(); // After all items exist. new ItemRules(this.Context, this.GameConfiguration).Initialize(); diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/CrywolfFortress.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/CrywolfFortress.cs index 873d52470..2c28fb3f1 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Maps/CrywolfFortress.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/CrywolfFortress.cs @@ -51,12 +51,12 @@ protected override IEnumerable CreateNpcSpawns() yield return this.CreateMonsterSpawn(7, this.NpcDictionary[240], 113, 056, Direction.SouthWest); // Baz The Vault Keeper yield return this.CreateMonsterSpawn(8, this.NpcDictionary[224], 118, 011, Direction.SouthEast); // Guardsman - yield return this.CreateMonsterSpawn(9, this.NpcDictionary[204], 121, 031, Direction.South, SpawnTrigger.OnceAtEventStart); // Wolf Status - yield return this.CreateMonsterSpawn(10, this.NpcDictionary[205], 125, 027, Direction.South, SpawnTrigger.OnceAtEventStart); // Wolf Altar1 - yield return this.CreateMonsterSpawn(11, this.NpcDictionary[206], 126, 035, Direction.South, SpawnTrigger.OnceAtEventStart); // Wolf Altar2 - yield return this.CreateMonsterSpawn(12, this.NpcDictionary[207], 120, 038, Direction.South, SpawnTrigger.OnceAtEventStart); // Wolf Altar3 - yield return this.CreateMonsterSpawn(13, this.NpcDictionary[208], 115, 035, Direction.South, SpawnTrigger.OnceAtEventStart); // Wolf Altar4 - yield return this.CreateMonsterSpawn(14, this.NpcDictionary[209], 117, 027, Direction.South, SpawnTrigger.OnceAtEventStart); // Wolf Altar5 + yield return this.CreateMonsterSpawn(9, this.NpcDictionary[204], 121, 031, Direction.South); // Wolf Status + yield return this.CreateMonsterSpawn(10, this.NpcDictionary[205], 125, 027, Direction.South); // Wolf Altar1 + yield return this.CreateMonsterSpawn(11, this.NpcDictionary[206], 126, 035, Direction.South); // Wolf Altar2 + yield return this.CreateMonsterSpawn(12, this.NpcDictionary[207], 120, 038, Direction.South); // Wolf Altar3 + yield return this.CreateMonsterSpawn(13, this.NpcDictionary[208], 115, 035, Direction.South); // Wolf Altar4 + yield return this.CreateMonsterSpawn(14, this.NpcDictionary[209], 117, 027, Direction.South); // Wolf Altar5 } /// @@ -252,6 +252,7 @@ protected override void CreateMonsters() this.GameConfiguration.Monsters.Add(monster); monster.Number = 204; monster.Designation = "Wolf Status"; + monster.ObjectKind = NpcObjectKind.PassiveNpc; var attributes = new Dictionary { { Stats.Level, 2 }, @@ -272,6 +273,7 @@ protected override void CreateMonsters() this.GameConfiguration.Monsters.Add(monster); monster.Number = 205; monster.Designation = "Wolf Altar1"; + monster.ObjectKind = NpcObjectKind.PassiveNpc; var attributes = new Dictionary { { Stats.Level, 2 }, @@ -292,6 +294,7 @@ protected override void CreateMonsters() this.GameConfiguration.Monsters.Add(monster); monster.Number = 206; monster.Designation = "Wolf Altar2"; + monster.ObjectKind = NpcObjectKind.PassiveNpc; var attributes = new Dictionary { { Stats.Level, 2 }, @@ -312,6 +315,7 @@ protected override void CreateMonsters() this.GameConfiguration.Monsters.Add(monster); monster.Number = 207; monster.Designation = "Wolf Altar3"; + monster.ObjectKind = NpcObjectKind.PassiveNpc; var attributes = new Dictionary { { Stats.Level, 2 }, @@ -332,6 +336,7 @@ protected override void CreateMonsters() this.GameConfiguration.Monsters.Add(monster); monster.Number = 208; monster.Designation = "Wolf Altar4"; + monster.ObjectKind = NpcObjectKind.PassiveNpc; var attributes = new Dictionary { { Stats.Level, 2 }, @@ -352,6 +357,7 @@ protected override void CreateMonsters() this.GameConfiguration.Monsters.Add(monster); monster.Number = 209; monster.Designation = "Wolf Altar5"; + monster.ObjectKind = NpcObjectKind.PassiveNpc; var attributes = new Dictionary { { Stats.Level, 2 }, From 0fb7b590f300b5a78c960fb166a9411b79a4464c Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:18:37 -0300 Subject: [PATCH 04/23] test(crywolf): cover the definition, the altars, the terrain, the packets 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 --- ...1-BD-03-CrywolfContractResult_by-server.md | 2 +- .../World/CrywolfEventViewPlugIn.cs | 2 +- .../ServerToClient/ConnectionExtensions.cs | 6 +- .../ServerToClient/ServerToClientPackets.cs | 8 +- .../ServerToClient/ServerToClientPackets.xml | 6 +- .../ServerToClientPacketsRef.cs | 8 +- .../CrywolfDataTest.cs | 105 +++++++++++++ .../MUnique.OpenMU.Tests/CrywolfAltarTest.cs | 141 ++++++++++++++++++ .../CrywolfEventDefinitionTest.cs | 94 ++++++++++++ .../CrywolfRemoteViewTests.cs | 76 ++++++++++ 10 files changed, 432 insertions(+), 16 deletions(-) create mode 100644 tests/MUnique.OpenMU.Persistence.Initialization.Tests/CrywolfDataTest.cs create mode 100644 tests/MUnique.OpenMU.Tests/CrywolfAltarTest.cs create mode 100644 tests/MUnique.OpenMU.Tests/CrywolfEventDefinitionTest.cs create mode 100644 tests/MUnique.OpenMU.Tests/CrywolfRemoteViewTests.cs diff --git a/docs/Packets/C1-BD-03-CrywolfContractResult_by-server.md b/docs/Packets/C1-BD-03-CrywolfContractResult_by-server.md index ec5a66192..4b270c628 100644 --- a/docs/Packets/C1-BD-03-CrywolfContractResult_by-server.md +++ b/docs/Packets/C1-BD-03-CrywolfContractResult_by-server.md @@ -16,6 +16,6 @@ On success, the client shows that the player is a guardian of the altar, and let | 1 | 1 | Byte | 8 | Packet header - length of the packet | | 2 | 1 | Byte | 0xBD | Packet header - packet type identifier | | 3 | 1 | Byte | 0x03 | Packet header - sub packet type identifier | -| 4 | 1 | Boolean | | Success; A value indicating whether the contract was accepted. | +| 4 | 1 | Byte | | Result; The result of the contract: 1 when it was accepted, 0 otherwise. The client compares the whole byte, so it's no boolean field. | | 5 | 1 | Byte | | AltarState; The new state of the altar, like in the CrywolfStatueAndAltarInfo. | | 6 | 2 | ShortBigEndian | | AltarKey; The key of the altar. The client uses the key minus 317 as index into its five altar states without checking the range, so it must be 317 plus the index of the altar (0 to 4). | \ No newline at end of file diff --git a/src/GameServer/RemoteView/World/CrywolfEventViewPlugIn.cs b/src/GameServer/RemoteView/World/CrywolfEventViewPlugIn.cs index f61fe02e6..5883fa125 100644 --- a/src/GameServer/RemoteView/World/CrywolfEventViewPlugIn.cs +++ b/src/GameServer/RemoteView/World/CrywolfEventViewPlugIn.cs @@ -60,7 +60,7 @@ await this._player.Connection.SendCrywolfStatueAndAltarInfoAsync( /// public async ValueTask ShowContractResultAsync(bool success, int altarIndex, byte altarState) { - await this._player.Connection.SendCrywolfContractResultAsync(success, altarState, (ushort)(FirstAltarKey + Math.Clamp(altarIndex, 0, 4))).ConfigureAwait(false); + await this._player.Connection.SendCrywolfContractResultAsync(success ? (byte)1 : (byte)0, altarState, (ushort)(FirstAltarKey + Math.Clamp(altarIndex, 0, 4))).ConfigureAwait(false); } /// diff --git a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs index 3a89a4ec2..329cfc1d7 100644 --- a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs +++ b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs @@ -5404,14 +5404,14 @@ int WritePacket() /// Sends a to this connection. /// /// The connection. - /// A value indicating whether the contract was accepted. + /// The result of the contract: 1 when it was accepted, 0 otherwise. The client compares the whole byte, so it's no boolean field. /// The new state of the altar, like in the CrywolfStatueAndAltarInfo. /// The key of the altar. The client uses the key minus 317 as index into its five altar states without checking the range, so it must be 317 plus the index of the altar (0 to 4). /// /// Is sent by the server when: The player requested to contract an altar. /// Causes reaction on client side: On success, the client shows that the player is a guardian of the altar, and lets the character pray. Otherwise, it shows why the contract failed. /// - public static async ValueTask SendCrywolfContractResultAsync(this IConnection? connection, bool @success, byte @altarState, ushort @altarKey) + public static async ValueTask SendCrywolfContractResultAsync(this IConnection? connection, byte @result, byte @altarState, ushort @altarKey) { if (connection is null) { @@ -5422,7 +5422,7 @@ int WritePacket() { var length = CrywolfContractResultRef.Length; var packet = new CrywolfContractResultRef(connection.Output.GetSpan(length)[..length]); - packet.Success = @success; + packet.Result = @result; packet.AltarState = @altarState; packet.AltarKey = @altarKey; diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs index bba744e0c..970411120 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs @@ -26064,12 +26064,12 @@ private CrywolfContractResult(Memory data, bool initialize) public C1HeaderWithSubCode Header => new (this._data); /// - /// Gets or sets a value indicating whether the contract was accepted. + /// Gets or sets the result of the contract: 1 when it was accepted, 0 otherwise. The client compares the whole byte, so it's no boolean field. /// - public bool Success + public byte Result { - get => this._data.Span[4..].GetBoolean(); - set => this._data.Span[4..].SetBoolean(value); + get => this._data.Span[4]; + set => this._data.Span[4] = value; } /// diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml index edadc1773..3d252483b 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml @@ -9555,9 +9555,9 @@ 4 - Boolean - Success - A value indicating whether the contract was accepted. + Byte + Result + The result of the contract: 1 when it was accepted, 0 otherwise. The client compares the whole byte, so it's no boolean field. 5 diff --git a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs index 1645303c2..6eca8038b 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs @@ -24668,12 +24668,12 @@ private CrywolfContractResultRef(Span data, bool initialize) public C1HeaderWithSubCodeRef Header => new (this._data); /// - /// Gets or sets a value indicating whether the contract was accepted. + /// Gets or sets the result of the contract: 1 when it was accepted, 0 otherwise. The client compares the whole byte, so it's no boolean field. /// - public bool Success + public byte Result { - get => this._data[4..].GetBoolean(); - set => this._data[4..].SetBoolean(value); + get => this._data[4]; + set => this._data[4] = value; } /// diff --git a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/CrywolfDataTest.cs b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/CrywolfDataTest.cs new file mode 100644 index 000000000..56fc8cfff --- /dev/null +++ b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/CrywolfDataTest.cs @@ -0,0 +1,105 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Tests; + +using Microsoft.Extensions.Logging.Abstractions; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.GameLogic.Crywolf; +using MUnique.OpenMU.Persistence.Initialization.Updates; +using MUnique.OpenMU.Persistence.InMemory; + +/// +/// Tests the data of the crywolf event. +/// +[TestFixture] +internal class CrywolfDataTest +{ + private const short CrywolfNumber = 34; + + /// + /// Tests that a new database contains the monsters of the army and their spawns, + /// and that the statue and the altars are always on the map. + /// + [Test] + public async Task NewDatabaseContainsEventDataAsync() + { + var contextProvider = new InMemoryPersistenceContextProvider(); + var gameConfiguration = await CreateConfigurationAsync(contextProvider).ConfigureAwait(false); + + AssertEventData(gameConfiguration); + } + + /// + /// Tests that the update adds the event data to an existing database, and that applying it twice doesn't change anything. + /// + [Test] + public async Task UpdateAddsEventDataAsync() + { + var contextProvider = new InMemoryPersistenceContextProvider(); + var gameConfiguration = await CreateConfigurationAsync(contextProvider).ConfigureAwait(false); + var map = GetMap(gameConfiguration); + foreach (var spawn in map.MonsterSpawns.Where(spawn => spawn.SpawnTrigger == SpawnTrigger.OnceAtWaveStart).ToList()) + { + map.MonsterSpawns.Remove(spawn); + } + + foreach (var spawn in map.MonsterSpawns.Where(spawn => spawn.MonsterDefinition!.Number is >= 204 and <= 209)) + { + spawn.SpawnTrigger = SpawnTrigger.OnceAtEventStart; + spawn.MonsterDefinition!.ObjectKind = NpcObjectKind.Monster; + } + + var update = new AddCrywolfEventUpdatePlugIn(); + await update.ApplyUpdateAsync(contextProvider.CreateNewContext(), gameConfiguration).ConfigureAwait(false); + await update.ApplyUpdateAsync(contextProvider.CreateNewContext(), gameConfiguration).ConfigureAwait(false); + + AssertEventData(gameConfiguration); + } + + private static async Task CreateConfigurationAsync(InMemoryPersistenceContextProvider contextProvider) + { + var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory()); + await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false); + using var context = contextProvider.CreateNewContext(); + return (await context.GetAsync().ConfigureAwait(false)).First(); + } + + private static GameMapDefinition GetMap(GameConfiguration gameConfiguration) + { + return gameConfiguration.Maps.Single(map => map.Number == CrywolfNumber); + } + + private static void AssertEventData(GameConfiguration gameConfiguration) + { + var definition = new CrywolfEventDefinition(); + var map = GetMap(gameConfiguration); + foreach (var number in new short[] { 340, 341, 344, 345, 348, 349 }) + { + Assert.That(gameConfiguration.Monsters.Where(monster => monster.Number == number).ToList(), Has.Count.EqualTo(1), $"Monster {number}"); + } + + var eventSpawns = map.MonsterSpawns.Where(spawn => spawn.SpawnTrigger == SpawnTrigger.OnceAtWaveStart).ToList(); + Assert.That(eventSpawns, Has.Count.EqualTo(76)); + + foreach (var group in definition.MonsterGroups) + { + var groupSpawns = eventSpawns.Where(spawn => spawn.WaveNumber == group.WaveNumber).ToList(); + Assert.That(groupSpawns.Count(spawn => spawn.MonsterDefinition!.Number == definition.DarkElfNumber), Is.EqualTo(1), $"The leader of the group {group.WaveNumber}"); + } + + var balgass = eventSpawns.Single(spawn => spawn.WaveNumber == definition.BalgassWaveNumber); + Assert.That(balgass.MonsterDefinition!.Number, Is.EqualTo(349)); + + foreach (var ballista in definition.Ballistas) + { + Assert.That(eventSpawns.Count(spawn => spawn.X1 == ballista.X && spawn.Y1 == ballista.Y && spawn.MonsterDefinition!.Number == 348), Is.EqualTo(1), $"The ballista at {ballista}"); + } + + var statueAndAltars = map.MonsterSpawns.Where(spawn => spawn.MonsterDefinition!.Number is >= 204 and <= 209).ToList(); + Assert.That(statueAndAltars, Has.Count.EqualTo(6)); + Assert.That(statueAndAltars.Select(spawn => spawn.SpawnTrigger), Is.All.EqualTo(SpawnTrigger.Automatic)); + Assert.That(statueAndAltars.Select(spawn => spawn.MonsterDefinition!.ObjectKind), Is.All.EqualTo(NpcObjectKind.PassiveNpc)); + } +} diff --git a/tests/MUnique.OpenMU.Tests/CrywolfAltarTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfAltarTest.cs new file mode 100644 index 000000000..07614e04c --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/CrywolfAltarTest.cs @@ -0,0 +1,141 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Crywolf; +using MUnique.OpenMU.GameLogic.NPC; +using MUnique.OpenMU.Pathfinding; +using MonsterDefinition = MUnique.OpenMU.Persistence.BasicModel.MonsterDefinition; + +/// +/// Tests the and the terrain of the crywolf event. +/// +[TestFixture] +public class CrywolfAltarTest +{ + private static readonly Point AltarPosition = new(125, 27); + + private readonly CrywolfEventDefinition _definition = new() + { + // The test player has the class number 0 and the level 0. + ContractCharacterClassNumbers = new List { 0 }, + MinimumContractLevel = 0, + }; + + /// + /// Tests that the contract gets valid when the elf stays at the altar, + /// and that the attempt already counts as contract. + /// + [Test] + public async Task ContractGetsValidAsync() + { + var (altar, player) = await this.CreateAltarAsync(AltarPosition).ConfigureAwait(false); + var now = DateTime.UtcNow; + + Assert.That(altar.TryStartContract(player, this._definition, now), Is.EqualTo(CrywolfContractResult.Success)); + Assert.That(altar.State, Is.EqualTo(CrywolfAltarState.Attempting)); + Assert.That(altar.GetClientState(this._definition.ContractsPerAltar), Is.EqualTo(0x21), "Attempting, one remaining contract."); + + Assert.That(altar.Update(this._definition, now.AddSeconds(1)), Is.EqualTo(CrywolfContractChange.None)); + Assert.That(altar.Update(this._definition, now + this._definition.ContractDelay), Is.EqualTo(CrywolfContractChange.Validated)); + Assert.That(altar.State, Is.EqualTo(CrywolfAltarState.Contracted)); + Assert.That(altar.GetClientState(this._definition.ContractsPerAltar), Is.EqualTo(0x11)); + Assert.That(altar.Display.Effect, Is.EqualTo(CrywolfEffect.AltarContracted)); + } + + /// + /// Tests that the contract is cancelled when the elf moves, and that the altar is exhausted after its contracts. + /// + [Test] + public async Task ContractIsCancelledWhenMovingAsync() + { + var (altar, player) = await this.CreateAltarAsync(AltarPosition).ConfigureAwait(false); + var now = DateTime.UtcNow; + + altar.TryStartContract(player, this._definition, now); + player.Position = new Point(126, 27); + Assert.That(altar.Update(this._definition, now.AddSeconds(1)), Is.EqualTo(CrywolfContractChange.Cancelled)); + Assert.That(altar.State, Is.EqualTo(CrywolfAltarState.Free)); + Assert.That(altar.Contractor, Is.Null); + + player.Position = AltarPosition; + Assert.That(altar.TryStartContract(player, this._definition, now.AddSeconds(2)), Is.EqualTo(CrywolfContractResult.Cooldown)); + + var later = now + this._definition.AltarCooldown + TimeSpan.FromSeconds(2); + Assert.That(altar.TryStartContract(player, this._definition, later), Is.EqualTo(CrywolfContractResult.Success)); + altar.Cancel(this._definition, later); + Assert.That(altar.State, Is.EqualTo(CrywolfAltarState.Exhausted), "Both contracts are used."); + Assert.That(altar.Display.Effect, Is.EqualTo(CrywolfEffect.AltarDisabled)); + Assert.That(altar.TryStartContract(player, this._definition, later + this._definition.AltarCooldown), Is.EqualTo(CrywolfContractResult.NotAvailable)); + + altar.Reset(); + Assert.That(altar.State, Is.EqualTo(CrywolfAltarState.Free)); + Assert.That(altar.GetClientState(this._definition.ContractsPerAltar), Is.EqualTo(0x02)); + } + + /// + /// Tests that only qualified characters which stand at the altar can contract it. + /// + [Test] + public async Task ContractRequirementsAsync() + { + var (altar, player) = await this.CreateAltarAsync(new Point(126, 28)).ConfigureAwait(false); + var now = DateTime.UtcNow; + + Assert.That(altar.TryStartContract(player, this._definition, now), Is.EqualTo(CrywolfContractResult.WrongPosition)); + Assert.That(altar.TryStartContract(player, new CrywolfEventDefinition(), now), Is.EqualTo(CrywolfContractResult.NotQualified), "The player is no elf."); + Assert.That(altar.ContractCount, Is.Zero); + } + + /// + /// Tests that the safezone of the fortress is removed while it's not in peace, and restored afterward. + /// + [Test] + public void TerrainOfTheOccupation() + { + var terrainData = new byte[(256 * 256) + 3]; + Array.Fill(terrainData, (byte)TerrainAttributeType.Safezone, 3, terrainData.Length - 3); + var terrain = new GameMapTerrain(terrainData); + var (areaX, areaY, _, _) = CrywolfTerrain.SafezoneRemovedAreas[0]; + var (blockedX, blockedY, _, _) = CrywolfTerrain.BlockedAreas[0]; + + CrywolfTerrain.Apply(terrain, false); + Assert.That(terrain.SafezoneMap[areaX, areaY], Is.False); + Assert.That(terrain.WalkMap[blockedX, blockedY], Is.False); + + CrywolfTerrain.Apply(terrain, true); + Assert.That(terrain.SafezoneMap[areaX, areaY], Is.True); + Assert.That(terrain.WalkMap[blockedX, blockedY], Is.True); + } + + private async ValueTask<(CrywolfAltar Altar, Player Player)> CreateAltarAsync(Point playerPosition) + { + var gameContext = GameContextTestHelper.CreateGameContext(); + var map = await gameContext.GetMapAsync(0).ConfigureAwait(false); + var altarDefinition = new MonsterDefinition { Id = Guid.NewGuid(), Number = 205, ObjectKind = NpcObjectKind.PassiveNpc }; + var spawnArea = new MonsterSpawnArea + { + MonsterDefinition = altarDefinition, + GameMap = map!.Definition, + X1 = AltarPosition.X, + X2 = AltarPosition.X, + Y1 = AltarPosition.Y, + Y2 = AltarPosition.Y, + Quantity = 1, + }; + var npc = new NonPlayerCharacter(spawnArea, altarDefinition, map); + npc.Initialize(); + await map.AddAsync(npc).ConfigureAwait(false); + + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + await player.PlayerState.TryAdvanceToAsync(PlayerState.EnteredWorld).ConfigureAwait(false); + player.IsAlive = true; + player.Position = playerPosition; + await player.CurrentMap!.AddAsync(player).ConfigureAwait(false); + return (new CrywolfAltar(0, npc), player); + } +} diff --git a/tests/MUnique.OpenMU.Tests/CrywolfEventDefinitionTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfEventDefinitionTest.cs new file mode 100644 index 000000000..6c2bbd232 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/CrywolfEventDefinitionTest.cs @@ -0,0 +1,94 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// Tests the . +/// +[TestFixture] +public class CrywolfEventDefinitionTest +{ + /// + /// Tests that the rank of a player is determined by its score, like in the original game. + /// + /// The score. + /// The expected rank, from 0 (D) to 4 (S). + [TestCase(0, 0)] + [TestCase(1000, 0)] + [TestCase(1001, 1)] + [TestCase(3001, 2)] + [TestCase(5000, 2)] + [TestCase(5001, 3)] + [TestCase(10001, 4)] + [TestCase(99999, 4)] + public void RankByScore(int score, int expectedRank) + { + Assert.That(new CrywolfEventDefinition().GetRank(score), Is.EqualTo(expectedRank)); + } + + /// + /// Tests that the players get only a part of the experience when the fortress has been occupied. + /// + [Test] + public void ExperienceByRankAndResult() + { + var definition = new CrywolfEventDefinition(); + + Assert.That(definition.GetExperience(0, true), Is.Zero); + Assert.That(definition.GetExperience(4, true), Is.EqualTo(1_800_000)); + Assert.That(definition.GetExperience(4, false), Is.EqualTo(180_000)); + Assert.That(definition.GetExperience(2, false), Is.EqualTo(80_000)); + } + + /// + /// Tests that the event only starts on the configured days and times. + /// + [Test] + public void StartTimes() + { + var definition = new CrywolfEventDefinition(); + + // 2026-09-30 is a wednesday, 2026-09-29 a tuesday. + Assert.That(definition.IsStartTime(new DateTime(2026, 09, 30, 20, 30, 2)), Is.True); + Assert.That(definition.IsStartTime(new DateTime(2026, 09, 30, 20, 31, 0)), Is.False); + Assert.That(definition.IsStartTime(new DateTime(2026, 09, 29, 20, 30, 2)), Is.False); + } + + /// + /// Tests the scores of the monsters and the durations of the states. + /// + [Test] + public void ScoresAndDurations() + { + var definition = new CrywolfEventDefinition(); + + Assert.That(definition.GetScore(349), Is.EqualTo(7000)); + Assert.That(definition.GetScore(340), Is.EqualTo(3000)); + Assert.That(definition.GetScore(310), Is.Zero, "The common monsters of the map don't count."); + Assert.That(definition.GetDuration(CrywolfState.Start), Is.EqualTo(TimeSpan.FromMinutes(15))); + Assert.That(definition.GetDuration(CrywolfState.None), Is.EqualTo(TimeSpan.Zero)); + Assert.That(definition.GetAltarIndex(207), Is.EqualTo(2)); + Assert.That(definition.GetAltarIndex(204), Is.EqualTo(-1)); + } + + /// + /// Tests that a monster walks along the waypoints to a goal which is far away, and directly to a goal which is near. + /// + [Test] + public void WalkTargetsAlongWaypoints() + { + var goal = new Pathfinding.Point(121, 36); + + Assert.That(CrywolfMonsterIntelligence.GetNextWalkTarget(new Pathfinding.Point(125, 40), goal), Is.EqualTo(goal)); + + var start = new Pathfinding.Point(110, 79); + var waypoint = CrywolfMonsterIntelligence.GetNextWalkTarget(start, goal); + Assert.That(waypoint, Is.Not.EqualTo(goal)); + Assert.That(waypoint.EuclideanDistanceTo(start), Is.LessThan(20)); + Assert.That(waypoint.EuclideanDistanceTo(goal), Is.LessThan(start.EuclideanDistanceTo(goal))); + } +} diff --git a/tests/MUnique.OpenMU.Tests/CrywolfRemoteViewTests.cs b/tests/MUnique.OpenMU.Tests/CrywolfRemoteViewTests.cs new file mode 100644 index 000000000..53402ebf4 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/CrywolfRemoteViewTests.cs @@ -0,0 +1,76 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using MUnique.OpenMU.GameLogic.Crywolf; +using MUnique.OpenMU.GameServer.RemoteView.World; +using MUnique.OpenMU.Network.Packets.ServerToClient; + +/// +/// Tests the packets of the , which have to match the (not packed) structures of the client. +/// +[TestFixture] +public class CrywolfRemoteViewTests +{ + /// + /// Tests the packets of the state, the altars, the contract, the time and the boss monsters. + /// + [Test] + public async Task EventPacketsAsync() + { + var (player, output) = CastleSiegeRemoteViewTestHelper.CreatePlayer(); + var view = new CrywolfEventViewPlugIn(player); + + await view.ShowStateAsync(CrywolfOccupationState.War, CrywolfState.Ready).ConfigureAwait(false); + await view.ShowStatueAndAltarsAsync(75, [0x12, 0x21, 0x02, 0x30, 0x01]).ConfigureAwait(false); + await view.ShowContractResultAsync(true, 2, 0x21).ConfigureAwait(false); + await view.ShowRemainingTimeAsync(TimeSpan.FromSeconds(880)).ConfigureAwait(false); + await view.ShowBossMonsterInfoAsync(-1, 12).ConfigureAwait(false); + + var data = output.ToArray(); + Assert.That(data, Has.Length.EqualTo(6 + 16 + 8 + 6 + 12)); + + Assert.That(data[..6], Is.EqualTo(new byte[] { 0xC1, 6, 0xBD, 0x00, 2, 3 })); + // The padding at the end isn't checked, because the client ignores it. + Assert.That(data[6..19], Is.EqualTo(new byte[] { 0xC1, 16, 0xBD, 0x02, 75, 0, 0, 0, 0x12, 0x21, 0x02, 0x30, 0x01 })); + + // The client uses the key minus 317 as index of the altar. + Assert.That(data[22..30], Is.EqualTo(new byte[] { 0xC1, 8, 0xBD, 0x03, 1, 0x21, 0x01, 0x3F })); + + // 880 seconds are 14 started minutes; the client counts down the seconds by itself. + Assert.That(data[30..36], Is.EqualTo(new byte[] { 0xC1, 6, 0xBD, 0x04, 0, 14 })); + Assert.That(data[36..45], Is.EqualTo(new byte[] { 0xC1, 12, 0xBD, 0x05, 0xFF, 0xFF, 0xFF, 0xFF, 12 })); + } + + /// + /// Tests the packets of the result: the personal rank and the heroes. + /// + [Test] + public async Task ResultPacketsAsync() + { + var (player, output) = CastleSiegeRemoteViewTestHelper.CreatePlayer(); + var view = new CrywolfEventViewPlugIn(player); + + await view.ShowPersonalRankAsync(4, 1_800_000).ConfigureAwait(false); + await view.ShowHeroListAsync([new CrywolfHero("Hero", 10500, 8), new CrywolfHero("Second", 3000, 4)]).ConfigureAwait(false); + + var data = output.ToArray(); + Assert.That(data, Has.Length.EqualTo(12 + 5 + (2 * 20))); + + var rank = (CrywolfPersonalRank)data.AsMemory(0, 12); + Assert.That(data[..4], Is.EqualTo(new byte[] { 0xC1, 12, 0xBD, 0x07 })); + Assert.That(rank.Rank, Is.EqualTo(CrywolfPersonalRank.CrywolfRank.S)); + Assert.That(rank.Experience, Is.EqualTo(1_800_000)); + + var heroes = data.AsSpan(12); + Assert.That(heroes[..5].ToArray(), Is.EqualTo(new byte[] { 0xC1, 45, 0xBD, 0x08, 2 })); + var first = heroes[5..25]; + Assert.That(first[0], Is.EqualTo(0), "The rank is the index of the hero."); + Assert.That(System.Text.Encoding.UTF8.GetString(first[1..5]), Is.EqualTo("Hero")); + Assert.That(BitConverter.ToInt32(first[12..16]), Is.EqualTo(10500)); + Assert.That(first[16], Is.EqualTo(8)); + Assert.That(heroes[25], Is.EqualTo(1), "The second hero has the rank 1."); + } +} From fdc07a32142b1899bcfdadbeed6558b2180bee33 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:26:42 -0300 Subject: [PATCH 05/23] test(crywolf): add the generated tests of the crywolf packets Co-Authored-By: Claude Opus 5.5 --- .../ServerToClientPacketTests.cs | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) diff --git a/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs b/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs index 77fbbbc55..f024828db 100644 --- a/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs +++ b/tests/MUnique.OpenMU.Network.Packets.Tests/ServerToClientPacketTests.cs @@ -5376,6 +5376,224 @@ public void SkillStageUpdate_PacketSizeValidation() "Field 'Stage' exceeds packet boundary"); } + /// + /// Tests the packet size calculation for CrywolfInfo. + /// + [Test] + public void CrywolfInfo_PacketSizeValidation() + { + // Fixed-length packet validation + const int expectedLength = 6; + var actualLength = CrywolfInfoRef.Length; + + Assert.That(actualLength, Is.EqualTo(expectedLength), + "Packet length mismatch: declared length does not match calculated size"); + + // Validate field 'Occupation' boundary + Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'Occupation' exceeds packet boundary"); + + // Validate field 'State' boundary + Assert.That(5 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'State' exceeds packet boundary"); + } + + /// + /// Tests the packet size calculation for CrywolfStatueAndAltarInfo. + /// + [Test] + public void CrywolfStatueAndAltarInfo_PacketSizeValidation() + { + // Fixed-length packet validation + const int expectedLength = 16; + var actualLength = CrywolfStatueAndAltarInfoRef.Length; + + Assert.That(actualLength, Is.EqualTo(expectedLength), + "Packet length mismatch: declared length does not match calculated size"); + + // Validate field 'StatueHealthPercent' boundary + Assert.That(4 + 4, Is.LessThanOrEqualTo(expectedLength), + "Field 'StatueHealthPercent' exceeds packet boundary"); + + // Validate field 'Altar1State' boundary + Assert.That(8 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'Altar1State' exceeds packet boundary"); + + // Validate field 'Altar2State' boundary + Assert.That(9 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'Altar2State' exceeds packet boundary"); + + // Validate field 'Altar3State' boundary + Assert.That(10 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'Altar3State' exceeds packet boundary"); + + // Validate field 'Altar4State' boundary + Assert.That(11 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'Altar4State' exceeds packet boundary"); + + // Validate field 'Altar5State' boundary + Assert.That(12 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'Altar5State' exceeds packet boundary"); + } + + /// + /// Tests the packet size calculation for CrywolfContractResult. + /// + [Test] + public void CrywolfContractResult_PacketSizeValidation() + { + // Fixed-length packet validation + const int expectedLength = 8; + var actualLength = CrywolfContractResultRef.Length; + + Assert.That(actualLength, Is.EqualTo(expectedLength), + "Packet length mismatch: declared length does not match calculated size"); + + // Validate field 'Result' boundary + Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'Result' exceeds packet boundary"); + + // Validate field 'AltarState' boundary + Assert.That(5 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'AltarState' exceeds packet boundary"); + + // Validate field 'AltarKey' boundary + Assert.That(6 + 2, Is.LessThanOrEqualTo(expectedLength), + "Field 'AltarKey' exceeds packet boundary"); + } + + /// + /// Tests the packet size calculation for CrywolfLeftTime. + /// + [Test] + public void CrywolfLeftTime_PacketSizeValidation() + { + // Fixed-length packet validation + const int expectedLength = 6; + var actualLength = CrywolfLeftTimeRef.Length; + + Assert.That(actualLength, Is.EqualTo(expectedLength), + "Packet length mismatch: declared length does not match calculated size"); + + // Validate field 'Hours' boundary + Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'Hours' exceeds packet boundary"); + + // Validate field 'Minutes' boundary + Assert.That(5 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'Minutes' exceeds packet boundary"); + } + + /// + /// Tests the packet size calculation for CrywolfBossMonsterInfo. + /// + [Test] + public void CrywolfBossMonsterInfo_PacketSizeValidation() + { + // Fixed-length packet validation + const int expectedLength = 12; + var actualLength = CrywolfBossMonsterInfoRef.Length; + + Assert.That(actualLength, Is.EqualTo(expectedLength), + "Packet length mismatch: declared length does not match calculated size"); + + // Validate field 'BalgassHealthPercent' boundary + Assert.That(4 + 4, Is.LessThanOrEqualTo(expectedLength), + "Field 'BalgassHealthPercent' exceeds packet boundary"); + + // Validate field 'DarkElfCount' boundary + Assert.That(8 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'DarkElfCount' exceeds packet boundary"); + } + + /// + /// Tests the packet size calculation for CrywolfPersonalRank. + /// + [Test] + public void CrywolfPersonalRank_PacketSizeValidation() + { + // Fixed-length packet validation + const int expectedLength = 12; + var actualLength = CrywolfPersonalRankRef.Length; + + Assert.That(actualLength, Is.EqualTo(expectedLength), + "Packet length mismatch: declared length does not match calculated size"); + + // Validate field 'Rank' boundary + Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'Rank' exceeds packet boundary"); + + // Validate field 'Experience' boundary + Assert.That(8 + 4, Is.LessThanOrEqualTo(expectedLength), + "Field 'Experience' exceeds packet boundary"); + } + + /// + /// Tests the packet size calculation for CrywolfHeroList. + /// + [Test] + public void CrywolfHeroList_PacketSizeValidation() + { + // Basic packet validation + // Validate header type and field boundaries + + // Field 'HeroCount' starts at index 4 with size 1 + Assert.That(4, Is.GreaterThanOrEqualTo(0), + "Field 'HeroCount' has invalid negative index"); + } + + /// + /// Tests the packet size calculation for CrywolfBenefitPlusChaosRate. + /// + [Test] + public void CrywolfBenefitPlusChaosRate_PacketSizeValidation() + { + // Fixed-length packet validation + const int expectedLength = 5; + var actualLength = CrywolfBenefitPlusChaosRateRef.Length; + + Assert.That(actualLength, Is.EqualTo(expectedLength), + "Packet length mismatch: declared length does not match calculated size"); + + // Validate field 'PlusChaosRate' boundary + Assert.That(4 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'PlusChaosRate' exceeds packet boundary"); + } + + /// + /// Tests the packet size calculation for CrywolfRegionMonsterAttack. + /// + [Test] + public void CrywolfRegionMonsterAttack_PacketSizeValidation() + { + // Fixed-length packet validation + const int expectedLength = 10; + var actualLength = CrywolfRegionMonsterAttackRef.Length; + + Assert.That(actualLength, Is.EqualTo(expectedLength), + "Packet length mismatch: declared length does not match calculated size"); + + // Validate field 'MonsterNumber' boundary + Assert.That(4 + 2, Is.LessThanOrEqualTo(expectedLength), + "Field 'MonsterNumber' exceeds packet boundary"); + + // Validate field 'SourceX' boundary + Assert.That(6 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'SourceX' exceeds packet boundary"); + + // Validate field 'SourceY' boundary + Assert.That(7 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'SourceY' exceeds packet boundary"); + + // Validate field 'TargetX' boundary + Assert.That(8 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'TargetX' exceeds packet boundary"); + + // Validate field 'TargetY' boundary + Assert.That(9 + 1, Is.LessThanOrEqualTo(expectedLength), + "Field 'TargetY' exceeds packet boundary"); + } + /// /// Tests the packet size calculation for IllusionTempleEnterResult. /// From 2669b556ea7294c2a6fffa457c51719cd62e5a8d Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:28:10 -0300 Subject: [PATCH 06/23] feat(crywolf): add a chat command for game masters to let the event proceed 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 --- src/GameLogic/Crywolf/CrywolfContext.cs | 6 ++- .../ChatCommands/CrywolfChatCommandPlugIn.cs | 41 +++++++++++++++++++ .../Properties/PlayerMessage.Designer.cs | 18 ++++++++ src/GameLogic/Properties/PlayerMessage.resx | 6 +++ .../Properties/PlugInResources.Designer.cs | 18 ++++++++ src/GameLogic/Properties/PlugInResources.resx | 6 +++ 6 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/GameLogic/PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs diff --git a/src/GameLogic/Crywolf/CrywolfContext.cs b/src/GameLogic/Crywolf/CrywolfContext.cs index 561332850..a37095b41 100644 --- a/src/GameLogic/Crywolf/CrywolfContext.cs +++ b/src/GameLogic/Crywolf/CrywolfContext.cs @@ -441,7 +441,7 @@ private void InitializeNpcs() private bool IsCommonNpc(NonPlayerCharacter npc) { - return npc is not Monster + return npc.Definition.ObjectKind != NpcObjectKind.Monster && npc.SpawnArea.SpawnTrigger == SpawnTrigger.Automatic && npc != this._statue && !this._definition.AltarNumbers.Contains(npc.Definition.Number) @@ -979,7 +979,9 @@ private async ValueTask RemoveCommonMonstersAsync() var commonMonsters = map.GetNpcsInRange(new Point(128, 128), byte.MaxValue) .OfType() - .Where(monster => monster.SpawnArea.SpawnTrigger == SpawnTrigger.Automatic && monster.IsAlive) + .Where(monster => monster.Definition.ObjectKind == NpcObjectKind.Monster + && monster.SpawnArea.SpawnTrigger == SpawnTrigger.Automatic + && monster.IsAlive) .ToList(); foreach (var monster in commonMonsters) { diff --git a/src/GameLogic/PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs b/src/GameLogic/PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs new file mode 100644 index 000000000..a2e6bf2c2 --- /dev/null +++ b/src/GameLogic/PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs @@ -0,0 +1,41 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic.Crywolf; +using MUnique.OpenMU.PlugIns; + +/// +/// A chat command for game masters, which lets the crywolf event proceed: it starts the event +/// when it's not running, otherwise the current state ends. +/// +[Guid("2A7D4F95-8C31-4E6B-9A05-6F2E1B8D3C47")] +[PlugIn] +[Display(Name = nameof(PlugInResources.CrywolfChatCommandPlugIn_Name), Description = nameof(PlugInResources.CrywolfChatCommandPlugIn_Description), ResourceType = typeof(PlugInResources))] +[ChatCommandHelp(Command, CharacterStatus.GameMaster)] +public class CrywolfChatCommandPlugIn : IChatCommandPlugIn +{ + private const string Command = "/crywolf"; + + /// + public string Key => Command; + + /// + public CharacterStatus MinCharacterStatusRequirement => CharacterStatus.GameMaster; + + /// + public async ValueTask HandleCommandAsync(Player player, string command) + { + if (CrywolfPlugIn.GetContext(player.GameContext) is not { } context) + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfNotActive)).ConfigureAwait(false); + return; + } + + context.SkipWaitingTime(); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfProceeds), context.State).ConfigureAwait(false); + } +} diff --git a/src/GameLogic/Properties/PlayerMessage.Designer.cs b/src/GameLogic/Properties/PlayerMessage.Designer.cs index b0499bf9e..38437300d 100644 --- a/src/GameLogic/Properties/PlayerMessage.Designer.cs +++ b/src/GameLogic/Properties/PlayerMessage.Designer.cs @@ -393,6 +393,15 @@ public static string CrywolfNoAltarContracted { } } + /// + /// Looks up a localized string similar to The crywolf event is not active.. + /// + public static string CrywolfNotActive { + get { + return ResourceManager.GetString("CrywolfNotActive", resourceCulture); + } + } + /// /// Looks up a localized string similar to Balgass has joined forces with Kundun's army and is preparing to attack the Crywolf Fortress.. /// @@ -411,6 +420,15 @@ public static string CrywolfNotify2 { } } + /// + /// Looks up a localized string similar to The crywolf event proceeds from the state {0}.. + /// + public static string CrywolfProceeds { + get { + return ResourceManager.GetString("CrywolfProceeds", resourceCulture); + } + } + /// /// Looks up a localized string similar to Your MvP rank is {0}. You received {1} experience points.. /// diff --git a/src/GameLogic/Properties/PlayerMessage.resx b/src/GameLogic/Properties/PlayerMessage.resx index 9a5ad4bb0..4a6cbfe1c 100644 --- a/src/GameLogic/Properties/PlayerMessage.resx +++ b/src/GameLogic/Properties/PlayerMessage.resx @@ -936,4 +936,10 @@ Muses protecting the statue of the holy wolf: {0}, barrier: {1}%. + + The crywolf event is not active. + + + The crywolf event proceeds from the state {0}. + diff --git a/src/GameLogic/Properties/PlugInResources.Designer.cs b/src/GameLogic/Properties/PlugInResources.Designer.cs index 30ea5cb5d..62ab0d6ab 100644 --- a/src/GameLogic/Properties/PlugInResources.Designer.cs +++ b/src/GameLogic/Properties/PlugInResources.Designer.cs @@ -591,6 +591,24 @@ public static string CreateMonsterChatCommand_Name { } } + /// + /// Looks up a localized string similar to Lets the crywolf event proceed: it starts the event when it is not running, otherwise the current state ends.. + /// + public static string CrywolfChatCommandPlugIn_Description { + get { + return ResourceManager.GetString("CrywolfChatCommandPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Chat command: /crywolf. + /// + public static string CrywolfChatCommandPlugIn_Name { + get { + return ResourceManager.GetString("CrywolfChatCommandPlugIn_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to The event in the crywolf fortress: the players defend the statue of the holy wolf against the army of Balgass. The result is kept until the next event.. /// diff --git a/src/GameLogic/Properties/PlugInResources.resx b/src/GameLogic/Properties/PlugInResources.resx index 82b60597e..836e92c08 100644 --- a/src/GameLogic/Properties/PlugInResources.resx +++ b/src/GameLogic/Properties/PlugInResources.resx @@ -1314,4 +1314,10 @@ The event in the crywolf fortress: the players defend the statue of the holy wolf against the army of Balgass. The result is kept until the next event. + + Chat command: /crywolf + + + Lets the crywolf event proceed: it starts the event when it is not running, otherwise the current state ends. + From 791d187c2a685cf9f75875263af2ee996d50c240 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:50:30 -0300 Subject: [PATCH 07/23] feat(crywolf): add the warp entry of the crywolf fortress The client expects the fortress at the index 26 of the warp list, like the original game. Co-Authored-By: Claude Opus 5.5 --- .../Updates/AddCrywolfEventUpdatePlugIn.cs | 6 +++-- .../Events/CrywolfInitializer.cs | 25 +++++++++++++++++++ .../Initialization/VersionSeasonSix/Gates.cs | 1 + .../CrywolfDataTest.cs | 6 +++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs index 66b7e85b9..53e83cb82 100644 --- a/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs +++ b/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs @@ -11,7 +11,8 @@ namespace MUnique.OpenMU.Persistence.Initialization.Updates; /// /// Adds the data of the crywolf event: the monsters of the army of Balgass and their spawns on the crywolf map. -/// The statue and the altars of the fortress are always on the map now, as NPCs which can't be attacked. +/// The statue and the altars of the fortress are always on the map now, as NPCs which can't be attacked, +/// and the fortress can be warped to. /// [PlugIn] [Display(Name = PlugInName, Description = PlugInDescription)] @@ -26,7 +27,7 @@ public class AddCrywolfEventUpdatePlugIn : UpdatePlugInBase /// /// The plug-in description. /// - internal const string PlugInDescription = "This update adds the monsters of the crywolf event and their spawns, and lets the statue and the altars of the fortress always be on the map."; + internal const string PlugInDescription = "This update adds the monsters of the crywolf event and their spawns, lets the statue and the altars of the fortress always be on the map, and adds the warp entry of the fortress."; /// public override string Name => PlugInName; @@ -53,6 +54,7 @@ protected override ValueTask ApplyAsync(IContext context, GameConfiguration game initializer.CreateMonsters(); initializer.ConfigureStatueAndAltars(); initializer.CreateSpawns(); + initializer.CreateWarpEntry(); return ValueTask.CompletedTask; } } diff --git a/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfInitializer.cs b/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfInitializer.cs index d1dd1a3b5..a02f14574 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfInitializer.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfInitializer.cs @@ -28,6 +28,11 @@ internal class CrywolfInitializer : InitializerBase /// internal const short LastAltarNumber = 209; + /// + /// The index of the warp entry of the crywolf fortress, which is expected by the client. + /// + internal const ushort WarpIndex = 26; + /// /// Initializes a new instance of the class. /// @@ -115,6 +120,26 @@ internal void CreateSpawns() } } + /// + /// Adds the warp entry of the crywolf fortress to existing data, if it doesn't exist yet. + /// + internal void CreateWarpEntry() + { + if (this.GameConfiguration.WarpList.Any(warp => warp.Index == WarpIndex) + || this.GetMap()?.ExitGates.FirstOrDefault(gate => gate is { IsSpawnGate: true, X1: 229, Y1: 37 }) is not { } gate) + { + return; + } + + var warpInfo = this.Context.CreateNew(); + warpInfo.Index = WarpIndex; + warpInfo.Name = "Crywolf"; + warpInfo.Costs = 10000; + warpInfo.LevelRequirement = 190; + warpInfo.Gate = gate; + this.GameConfiguration.WarpList.Add(warpInfo); + } + private static bool IsStatueOrAltar(MonsterDefinition monster) { return monster.Number is >= StatueNumber and <= LastAltarNumber; diff --git a/src/Persistence/Initialization/VersionSeasonSix/Gates.cs b/src/Persistence/Initialization/VersionSeasonSix/Gates.cs index 79bf92d64..18c6936d0 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Gates.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Gates.cs @@ -68,6 +68,7 @@ private void CreateWarpEntries(IDictionary gates) this.GameConfiguration.WarpList.Add(this.CreateWarpInfo(22, "Tarkan2", 8500, 140, gates[77])); this.GameConfiguration.WarpList.Add(this.CreateWarpInfo(23, "Icarus", 10000, 170, gates[63])); this.GameConfiguration.WarpList.Add(this.CreateWarpInfo(25, "Aida1", 8500, 150, gates[119])); + this.GameConfiguration.WarpList.Add(this.CreateWarpInfo(26, "Crywolf", 10000, 190, gates[118])); this.GameConfiguration.WarpList.Add(this.CreateWarpInfo(27, "Aida2", 8500, 150, gates[140])); this.GameConfiguration.WarpList.Add(this.CreateWarpInfo(28, "KanturuRuins1", 9000, 160, gates[138])); this.GameConfiguration.WarpList.Add(this.CreateWarpInfo(29, "KanturuRuins2", 9000, 160, gates[141])); diff --git a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/CrywolfDataTest.cs b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/CrywolfDataTest.cs index 56fc8cfff..ae936798f 100644 --- a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/CrywolfDataTest.cs +++ b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/CrywolfDataTest.cs @@ -51,6 +51,8 @@ public async Task UpdateAddsEventDataAsync() spawn.MonsterDefinition!.ObjectKind = NpcObjectKind.Monster; } + gameConfiguration.WarpList.Remove(gameConfiguration.WarpList.Single(warp => warp.Index == 26)); + var update = new AddCrywolfEventUpdatePlugIn(); await update.ApplyUpdateAsync(contextProvider.CreateNewContext(), gameConfiguration).ConfigureAwait(false); await update.ApplyUpdateAsync(contextProvider.CreateNewContext(), gameConfiguration).ConfigureAwait(false); @@ -101,5 +103,9 @@ private static void AssertEventData(GameConfiguration gameConfiguration) Assert.That(statueAndAltars, Has.Count.EqualTo(6)); Assert.That(statueAndAltars.Select(spawn => spawn.SpawnTrigger), Is.All.EqualTo(SpawnTrigger.Automatic)); Assert.That(statueAndAltars.Select(spawn => spawn.MonsterDefinition!.ObjectKind), Is.All.EqualTo(NpcObjectKind.PassiveNpc)); + + var warp = gameConfiguration.WarpList.Single(warp => warp.Index == 26); + Assert.That(warp.Gate?.Map, Is.SameAs(map)); + Assert.That(warp.LevelRequirement, Is.EqualTo(190)); } } From 7ae58846314399586f51618ae7f59085e2896fdd Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 02:30:23 -0300 Subject: [PATCH 08/23] feat(crywolf): let Balgass appear by the chat command during the battle 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 --- src/GameLogic/Crywolf/CrywolfContext.cs | 8 +++++++- .../PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs | 2 +- src/GameLogic/Properties/PlugInResources.Designer.cs | 2 +- src/GameLogic/Properties/PlugInResources.resx | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/GameLogic/Crywolf/CrywolfContext.cs b/src/GameLogic/Crywolf/CrywolfContext.cs index a37095b41..6fdf597a4 100644 --- a/src/GameLogic/Crywolf/CrywolfContext.cs +++ b/src/GameLogic/Crywolf/CrywolfContext.cs @@ -226,7 +226,8 @@ public void UpdateDefinition(CrywolfEventDefinition definition) } /// - /// Forces the event to proceed: it starts when it's not running, otherwise the current state ends. + /// Forces the event to proceed: it starts when it's not running, and during the battle Balgass appears + /// if he didn't yet. Otherwise, the current state ends. /// public void SkipWaitingTime() { @@ -234,6 +235,11 @@ public void SkipWaitingTime() { this._isStartForced = true; } + else if (this.State == CrywolfState.Start && !this._isBalgassAppearanceDone) + { + // The battle continues at the appearance of Balgass, so that the battle against him can be tested. + this._stateStart = DateTime.UtcNow - this._definition.BalgassAppearanceDelay; + } else { this._stateStart = DateTime.MinValue; diff --git a/src/GameLogic/PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs b/src/GameLogic/PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs index a2e6bf2c2..a350fc6e1 100644 --- a/src/GameLogic/PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs +++ b/src/GameLogic/PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs @@ -10,7 +10,7 @@ namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; /// /// A chat command for game masters, which lets the crywolf event proceed: it starts the event -/// when it's not running, otherwise the current state ends. +/// when it's not running, and during the battle Balgass appears if he didn't yet. Otherwise, the current state ends. /// [Guid("2A7D4F95-8C31-4E6B-9A05-6F2E1B8D3C47")] [PlugIn] diff --git a/src/GameLogic/Properties/PlugInResources.Designer.cs b/src/GameLogic/Properties/PlugInResources.Designer.cs index 62ab0d6ab..783f64373 100644 --- a/src/GameLogic/Properties/PlugInResources.Designer.cs +++ b/src/GameLogic/Properties/PlugInResources.Designer.cs @@ -592,7 +592,7 @@ public static string CreateMonsterChatCommand_Name { } /// - /// Looks up a localized string similar to Lets the crywolf event proceed: it starts the event when it is not running, otherwise the current state ends.. + /// Looks up a localized string similar to Lets the crywolf event proceed: it starts the event when it is not running, and during the battle Balgass appears if he did not yet. Otherwise, the current state ends.. /// public static string CrywolfChatCommandPlugIn_Description { get { diff --git a/src/GameLogic/Properties/PlugInResources.resx b/src/GameLogic/Properties/PlugInResources.resx index 836e92c08..20d8bb3d3 100644 --- a/src/GameLogic/Properties/PlugInResources.resx +++ b/src/GameLogic/Properties/PlugInResources.resx @@ -1318,6 +1318,6 @@ Chat command: /crywolf - Lets the crywolf event proceed: it starts the event when it is not running, otherwise the current state ends. + Lets the crywolf event proceed: it starts the event when it is not running, and during the battle Balgass appears if he did not yet. Otherwise, the current state ends. From 663705d5889ad468c5ef8fd554f805dca6aece42 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:14:51 -0300 Subject: [PATCH 09/23] fix(crywolf): show the heroes at the end of the battle 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 --- src/GameLogic/Crywolf/CrywolfContext.cs | 32 +++++++++++++++---------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/GameLogic/Crywolf/CrywolfContext.cs b/src/GameLogic/Crywolf/CrywolfContext.cs index 6fdf597a4..c6fef7f1f 100644 --- a/src/GameLogic/Crywolf/CrywolfContext.cs +++ b/src/GameLogic/Crywolf/CrywolfContext.cs @@ -42,6 +42,7 @@ public sealed class CrywolfContext : IEventStateProvider, IDisposable private readonly ConcurrentDictionary _eventMonsters = new(); private readonly ConcurrentDictionary _removedMonsters = new(); private readonly ConcurrentDictionary _scores = new(); + private readonly List _heroes = new(); private readonly Func _saveDefinition; private CrywolfEventDefinition _definition; @@ -650,34 +651,41 @@ private async ValueTask EndBattleAsync() } await this.ForEachMapPlayerAsync(player => this.RewardExperienceAsync(player, isDefended).AsTask()).ConfigureAwait(false); - await this.SaveOccupationAsync().ConfigureAwait(false); - } - - private async ValueTask EndCycleAsync() - { - await this.RemoveEventMonstersAsync().ConfigureAwait(false); - await this.ApplyOccupationAsync().ConfigureAwait(false); - await this.RestoreCommonMonstersAsync().ConfigureAwait(false); - await this.ShowStateToMapPlayersAsync().ConfigureAwait(false); - var isDefended = this.Occupation == CrywolfOccupationState.Peace; + // Unlike the original game, the heroes are shown already at the end of the battle. The client shows + // them in the result dialog, which it opens after an animation of some frames, so with a higher frame + // rate than the original client, the heroes wouldn't arrive in time at the end of the event. var heroes = this._scores .Where(entry => entry.Key.CurrentMap == this._map) .OrderByDescending(entry => entry.Value) .Take(Math.Min(this._definition.HeroCount, 5)) .ToList(); + this._heroes.Clear(); + this._heroes.AddRange(heroes.Select(entry => entry.Key)); var heroList = heroes .Select(entry => new CrywolfHero(entry.Key.Name, entry.Value, entry.Key.SelectedCharacter?.CharacterClass?.Number ?? 0)) .ToList(); await this.ForEachMapPlayerAsync(player => player.InvokeViewPlugInAsync(p => p.ShowHeroListAsync(heroList)).AsTask()).ConfigureAwait(false); - if (isDefended) + await this.SaveOccupationAsync().ConfigureAwait(false); + } + + private async ValueTask EndCycleAsync() + { + await this.RemoveEventMonstersAsync().ConfigureAwait(false); + await this.ApplyOccupationAsync().ConfigureAwait(false); + await this.RestoreCommonMonstersAsync().ConfigureAwait(false); + await this.ShowStateToMapPlayersAsync().ConfigureAwait(false); + + if (this.Occupation == CrywolfOccupationState.Peace) { - foreach (var (hero, _) in heroes) + // Like in the original game, the heroes get their reward at the end of the event. + foreach (var hero in this._heroes) { await this.DropRewardAsync(hero).ConfigureAwait(false); } } + this._heroes.Clear(); this._scores.Clear(); } From 7c7555f2e7eca171e844ce094acf105055669299 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:38:46 -0300 Subject: [PATCH 10/23] fix(crywolf): address the findings of the review - 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 --- src/GameLogic/Crywolf/CrywolfAltar.cs | 18 ++ src/GameLogic/Crywolf/CrywolfContext.cs | 142 ++++++++++---- .../Crywolf/CrywolfContractResult.cs | 10 + .../Crywolf/CrywolfEventDefinition.cs | 5 + .../Crywolf/CrywolfMonsterGroupState.cs | 5 + src/GameLogic/PlayerActions/TalkNpcAction.cs | 6 + .../Properties/PlayerMessage.Designer.cs | 18 ++ src/GameLogic/Properties/PlayerMessage.resx | 6 + .../CrywolfContextTest.cs | 177 ++++++++++++++++++ 9 files changed, 346 insertions(+), 41 deletions(-) create mode 100644 tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs diff --git a/src/GameLogic/Crywolf/CrywolfAltar.cs b/src/GameLogic/Crywolf/CrywolfAltar.cs index 914f76dba..2b4f01f44 100644 --- a/src/GameLogic/Crywolf/CrywolfAltar.cs +++ b/src/GameLogic/Crywolf/CrywolfAltar.cs @@ -104,6 +104,11 @@ public void Hide() /// The result. public CrywolfContractResult TryStartContract(Player player, CrywolfEventDefinition definition, DateTime now) { + if (this.Contractor == player) + { + return CrywolfContractResult.AlreadyContracting; + } + if (this.State != CrywolfAltarState.Free) { return CrywolfContractResult.NotAvailable; @@ -116,6 +121,11 @@ public CrywolfContractResult TryStartContract(Player player, CrywolfEventDefinit return CrywolfContractResult.NotQualified; } + if (IsMounted(player)) + { + return CrywolfContractResult.Mounted; + } + if (now - this.LastChange < definition.AltarCooldown) { return CrywolfContractResult.Cooldown; @@ -179,6 +189,14 @@ public void Cancel(CrywolfEventDefinition definition, DateTime now) this.UpdateEffect(true); } + /// + /// Determines whether the player rides a mount (Uniria, Dinorant or Fenrir), like the client checks it. + /// + private static bool IsMounted(Player player) + { + return player.Inventory?.GetItem(InventoryConstants.PetSlot)?.Definition is { Group: 13, Number: 2 or 3 or 37 }; + } + private static bool IsAtAltar(Player player, NonPlayerCharacter altar) { return player.IsAlive diff --git a/src/GameLogic/Crywolf/CrywolfContext.cs b/src/GameLogic/Crywolf/CrywolfContext.cs index c6fef7f1f..128d61dcb 100644 --- a/src/GameLogic/Crywolf/CrywolfContext.cs +++ b/src/GameLogic/Crywolf/CrywolfContext.cs @@ -32,11 +32,9 @@ public sealed class CrywolfContext : IEventStateProvider, IDisposable { private static readonly TimeSpan AltarInfoInterval = TimeSpan.FromSeconds(2); private static readonly TimeSpan BossMonsterInfoInterval = TimeSpan.FromSeconds(5); - private static readonly TimeSpan RemainingTimeInterval = TimeSpan.FromSeconds(20); private readonly GameContext _gameContext; private readonly ILogger _logger; - private readonly List _altars = new(); private readonly ConcurrentDictionary _hiddenNpcs = new(); private readonly ConcurrentDictionary _groups = new(); private readonly ConcurrentDictionary _eventMonsters = new(); @@ -54,11 +52,13 @@ public sealed class CrywolfContext : IEventStateProvider, IDisposable private DateTime _lastNotification = DateTime.MinValue; private DateTime _lastAltarInfo = DateTime.MinValue; private DateTime _lastBossMonsterInfo = DateTime.MinValue; - private DateTime _lastRemainingTime = DateTime.MinValue; + private int _lastRemainingTimeStep = -1; private int _contractedAltarCount; private bool _isBalgassAppearanceDone; private bool _isStatueAttackStarted; private bool _isStartForced; + private int _isSkipRequested; + private IReadOnlyList _altars = []; private bool _isMissingConfigurationLogged; private int _isBalgassDead; @@ -178,6 +178,11 @@ public async ValueTask TickAsync() return; } + if (Interlocked.Exchange(ref this._isSkipRequested, 0) != 0) + { + this.Skip(); + } + var elapsed = DateTime.UtcNow - this._stateStart; switch (this.State) { @@ -227,24 +232,24 @@ public void UpdateDefinition(CrywolfEventDefinition definition) } /// - /// Forces the event to proceed: it starts when it's not running, and during the battle Balgass appears - /// if he didn't yet. Otherwise, the current state ends. + /// Forces the event to proceed with the next step: it starts when it's not running, and during the battle + /// Balgass appears if he didn't yet. Otherwise, the current state ends. /// public void SkipWaitingTime() { - if (this.State == CrywolfState.None) - { - this._isStartForced = true; - } - else if (this.State == CrywolfState.Start && !this._isBalgassAppearanceDone) - { - // The battle continues at the appearance of Balgass, so that the battle against him can be tested. - this._stateStart = DateTime.UtcNow - this._definition.BalgassAppearanceDelay; - } - else - { - this._stateStart = DateTime.MinValue; - } + // It's applied by the next step, so that it doesn't interfere with a running step. + Interlocked.Exchange(ref this._isSkipRequested, 1); + } + + /// + /// Determines whether the NPC is hidden, because the fortress isn't in peace. + /// Players can't talk to hidden NPCs. + /// + /// The NPC. + /// true, if the NPC is hidden; otherwise, false. + public bool IsNpcHidden(NonPlayerCharacter npc) + { + return this._hiddenNpcs.TryGetValue(npc, out var display) && display.Effect == CrywolfEffect.NpcHidden; } /// @@ -289,9 +294,8 @@ public async ValueTask ShowCurrentStateAsync(Player player) if (this.State == CrywolfState.Start) { - var remaining = this.RemainingTime; + // The remaining time isn't sent here, because the client expects it every 20 seconds, and would count the seconds wrong. var (balgassHealth, darkElfCount) = this.GetBossMonsterInfo(); - await player.InvokeViewPlugInAsync(p => p.ShowRemainingTimeAsync(remaining)).ConfigureAwait(false); await player.InvokeViewPlugInAsync(p => p.ShowBossMonsterInfoAsync(balgassHealth, darkElfCount)).ConfigureAwait(false); } } @@ -326,6 +330,9 @@ public async ValueTask ContractAltarAsync(Player player, ushort altarId) var altarNumber = altar.Index + 1; switch (result) { + case CrywolfContractResult.AlreadyContracting: + // The client sent the request again, e.g. by a double click. Nothing changed. + break; case CrywolfContractResult.Success: await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfContractAttemptInfo), (int)this._definition.ContractDelay.TotalSeconds).ConfigureAwait(false); await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfContractAttempt), player.Name, altarNumber).ConfigureAwait(false); @@ -336,13 +343,28 @@ public async ValueTask ContractAltarAsync(Player player, ushort altarId) case CrywolfContractResult.WrongPosition: await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfContractWrongPosition), altarNumber).ConfigureAwait(false); break; + case CrywolfContractResult.Mounted: + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfContractMounted)).ConfigureAwait(false); + break; + case CrywolfContractResult.NotAvailable when this.State is not (CrywolfState.Ready or CrywolfState.Start): + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfNotActive)).ConfigureAwait(false); + break; + case CrywolfContractResult.NotAvailable: + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfAltarNotAvailable), altarNumber).ConfigureAwait(false); + break; default: await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfContractRequirement), this._definition.MinimumContractLevel).ConfigureAwait(false); break; } - var altarState = altar.GetClientState(this._definition.ContractsPerAltar); - await player.InvokeViewPlugInAsync(p => p.ShowContractResultAsync(result == CrywolfContractResult.Success, altar.Index, altarState)).ConfigureAwait(false); + byte altarState; + lock (altar) + { + altarState = altar.GetClientState(this._definition.ContractsPerAltar); + } + + var isSuccess = result is CrywolfContractResult.Success or CrywolfContractResult.AlreadyContracting; + await player.InvokeViewPlugInAsync(p => p.ShowContractResultAsync(isSuccess, altar.Index, altarState)).ConfigureAwait(false); } /// @@ -375,10 +397,16 @@ internal async ValueTask ReviveGroupMembersAsync(byte waveNumber) return; } - foreach (var spawnArea in group.GetDeadMembers()) + // The leader revives one of the dead members at a time. + var now = DateTime.UtcNow; + if (now - group.LastRevive < this._definition.MemberReviveInterval + || group.GetDeadMembers().FirstOrDefault() is not { } spawnArea) { - await this.SpawnMemberAsync(group, spawnArea).ConfigureAwait(false); + return; } + + group.LastRevive = now; + await this.SpawnMemberAsync(group, spawnArea).ConfigureAwait(false); } /// @@ -421,6 +449,26 @@ private static string GetRankName(int rank) }; } + /// + /// Lets the event proceed, like requested by . + /// + private void Skip() + { + if (this.State == CrywolfState.None) + { + this._isStartForced = true; + } + else if (this.State == CrywolfState.Start && !this._isBalgassAppearanceDone) + { + // The battle continues at the appearance of Balgass, so that the battle against him can be tested. + this._stateStart = DateTime.UtcNow - this._definition.BalgassAppearanceDelay; + } + else + { + this._stateStart = DateTime.MinValue; + } + } + private void InitializeNpcs() { if (this._map is not { } map) @@ -430,16 +478,19 @@ private void InitializeNpcs() var npcs = map.GetNpcsInRange(new Point(128, 128), byte.MaxValue); this._statue = npcs.FirstOrDefault(npc => npc.Definition.Number == this._definition.StatueNumber); - this._altars.Clear(); + var altars = new List(); for (var i = 0; i < this._definition.AltarNumbers.Count; i++) { var number = this._definition.AltarNumbers[i]; if (npcs.FirstOrDefault(npc => npc.Definition.Number == number) is { } npc) { - this._altars.Add(new CrywolfAltar(i, npc)); + altars.Add(new CrywolfAltar(i, npc)); } } + // The list is replaced as a whole, because it's read by the requests of the players. + this._altars = altars; + foreach (var npc in npcs.Where(this.IsCommonNpc)) { this._hiddenNpcs.TryAdd(npc, new CrywolfEffectDisplay(npc)); @@ -545,7 +596,7 @@ private async ValueTask StartBattleAsync() await this.ShowStateToMapPlayersAsync().ConfigureAwait(false); this._lastAltarInfo = DateTime.MinValue; this._lastBossMonsterInfo = DateTime.MinValue; - this._lastRemainingTime = DateTime.MinValue; + this._lastRemainingTimeStep = -1; if (this.CountContractedAltars() == 0) { await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfNoAltarContracted)).ConfigureAwait(false); @@ -557,6 +608,15 @@ private async ValueTask StartBattleAsync() private async ValueTask UpdateBattleAsync(TimeSpan elapsed) { await this.UpdateAltarsAsync().ConfigureAwait(false); + if (this._balgass is not null && this.IsBalgassDead) + { + // Balgass is checked first, so that the battle is won when the last contract ended at the same time. + await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfBalgassDefeated)).ConfigureAwait(false); + this.Occupation = CrywolfOccupationState.Peace; + await this.ChangeStateAsync(CrywolfState.End).ConfigureAwait(false); + return; + } + if (this.CountContractedAltars() == 0) { await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfNoAltarContracted)).ConfigureAwait(false); @@ -573,11 +633,12 @@ private async ValueTask UpdateBattleAsync(TimeSpan elapsed) await this.ForEachMapPlayerAsync(player => player.InvokeViewPlugInAsync(p => p.ShowBossMonsterInfoAsync(balgassHealth, darkElfCount)).AsTask()).ConfigureAwait(false); } - if (now - this._lastRemainingTime >= RemainingTimeInterval) + // The client counts down the seconds of a minute by itself, and expects the remaining time at each 20 seconds of it. + var remaining = this.RemainingTime; + var remainingTimeStep = (int)Math.Ceiling(remaining.TotalSeconds / 20); + if (remainingTimeStep != this._lastRemainingTimeStep) { - // The client counts down the seconds by itself and expects this every 20 seconds. - this._lastRemainingTime = now; - var remaining = this.RemainingTime; + this._lastRemainingTimeStep = remainingTimeStep; await this.ForEachMapPlayerAsync(player => player.InvokeViewPlugInAsync(p => p.ShowRemainingTimeAsync(remaining)).AsTask()).ConfigureAwait(false); } @@ -594,14 +655,6 @@ private async ValueTask UpdateBattleAsync(TimeSpan elapsed) await this.SpawnBalgassAsync().ConfigureAwait(false); } - if (this._balgass is not null && this.IsBalgassDead) - { - await this.ShowMessageToMapPlayersAsync(nameof(PlayerMessage.CrywolfBalgassDefeated)).ConfigureAwait(false); - this.Occupation = CrywolfOccupationState.Peace; - await this.ChangeStateAsync(CrywolfState.End).ConfigureAwait(false); - return; - } - if (elapsed >= this._definition.BattleDuration) { if (this._balgass is not null) @@ -788,9 +841,17 @@ private int CountContractedAltars() { double health = 0; double maximumHealth = 0; + var altarStates = new List(); foreach (var altar in this._altars) { - if (altar.State == CrywolfAltarState.Contracted && altar.Contractor?.Attributes is { } attributes) + Player? contractor; + lock (altar) + { + contractor = altar.State == CrywolfAltarState.Contracted ? altar.Contractor : null; + altarStates.Add(altar.GetClientState(this._definition.ContractsPerAltar)); + } + + if (contractor?.Attributes is { } attributes) { health += attributes[Stats.CurrentHealth]; maximumHealth += attributes[Stats.MaximumHealth]; @@ -798,7 +859,6 @@ private int CountContractedAltars() } var percentage = maximumHealth > 0 ? (int)Math.Clamp(health * 100 / maximumHealth, 0, 100) : 0; - var altarStates = this._altars.Select(altar => altar.GetClientState(this._definition.ContractsPerAltar)).ToList(); return (percentage, altarStates); } diff --git a/src/GameLogic/Crywolf/CrywolfContractResult.cs b/src/GameLogic/Crywolf/CrywolfContractResult.cs index 27cc1b6bc..0c7e7980b 100644 --- a/src/GameLogic/Crywolf/CrywolfContractResult.cs +++ b/src/GameLogic/Crywolf/CrywolfContractResult.cs @@ -33,4 +33,14 @@ public enum CrywolfContractResult /// The character doesn't stand at the altar. /// WrongPosition, + + /// + /// The character rides a mount, which isn't allowed for a contract. + /// + Mounted, + + /// + /// The character is already contracting the altar, e.g. because the client sent the request twice. + /// + AlreadyContracting, } diff --git a/src/GameLogic/Crywolf/CrywolfEventDefinition.cs b/src/GameLogic/Crywolf/CrywolfEventDefinition.cs index 9a73c2e53..ee1e1e1cb 100644 --- a/src/GameLogic/Crywolf/CrywolfEventDefinition.cs +++ b/src/GameLogic/Crywolf/CrywolfEventDefinition.cs @@ -162,6 +162,11 @@ public class CrywolfEventDefinition /// public int BallistaEffectRadius { get; set; } = 10; + /// + /// Gets or sets the interval, in which the leader of a group revives one of its dead members. + /// + public TimeSpan MemberReviveInterval { get; set; } = TimeSpan.FromSeconds(5); + /// /// Gets or sets the distance to the leader, above which the members of a group follow it. /// diff --git a/src/GameLogic/Crywolf/CrywolfMonsterGroupState.cs b/src/GameLogic/Crywolf/CrywolfMonsterGroupState.cs index 2d0ef7d27..530c238c4 100644 --- a/src/GameLogic/Crywolf/CrywolfMonsterGroupState.cs +++ b/src/GameLogic/Crywolf/CrywolfMonsterGroupState.cs @@ -40,6 +40,11 @@ public CrywolfMonsterGroupState(CrywolfMonsterGroup definition, IReadOnlyList public Monster? Leader { get; private set; } + /// + /// Gets or sets the time, when the leader revived a member of the group the last time. + /// + public DateTime LastRevive { get; set; } = DateTime.MinValue; + /// /// Sets the current monster of a spawn area. /// diff --git a/src/GameLogic/PlayerActions/TalkNpcAction.cs b/src/GameLogic/PlayerActions/TalkNpcAction.cs index 05009e24f..7c31099cd 100644 --- a/src/GameLogic/PlayerActions/TalkNpcAction.cs +++ b/src/GameLogic/PlayerActions/TalkNpcAction.cs @@ -4,6 +4,7 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions; +using MUnique.OpenMU.GameLogic.Crywolf; using MUnique.OpenMU.GameLogic.MiniGames; using MUnique.OpenMU.GameLogic.NPC; using MUnique.OpenMU.GameLogic.PlayerActions.Quests; @@ -28,6 +29,11 @@ public class TalkNpcAction public async ValueTask TalkToNpcAsync(Player player, NonPlayerCharacter npc) { var npcStats = npc.Definition; + if (CrywolfPlugIn.GetContext(player.GameContext) is { } crywolf && crywolf.IsNpcHidden(npc)) + { + // The NPCs of the crywolf fortress are hidden while it isn't in peace, so they can't be used. + return; + } if (this.AdvancePlayerState(npc)) { diff --git a/src/GameLogic/Properties/PlayerMessage.Designer.cs b/src/GameLogic/Properties/PlayerMessage.Designer.cs index 38437300d..018980364 100644 --- a/src/GameLogic/Properties/PlayerMessage.Designer.cs +++ b/src/GameLogic/Properties/PlayerMessage.Designer.cs @@ -186,6 +186,15 @@ public static string CrywolfAltarCooldown { } } + /// + /// Looks up a localized string similar to The altar ({0}) of the holy wolf can not be contracted now.. + /// + public static string CrywolfAltarNotAvailable { + get { + return ResourceManager.GetString("CrywolfAltarNotAvailable", resourceCulture); + } + } + /// /// Looks up a localized string similar to The army of Balgass attacks in {0} minute(s).. /// @@ -294,6 +303,15 @@ public static string CrywolfContractCancelled { } } + /// + /// Looks up a localized string similar to An altar can not be contracted on a mount.. + /// + public static string CrywolfContractMounted { + get { + return ResourceManager.GetString("CrywolfContractMounted", resourceCulture); + } + } + /// /// Looks up a localized string similar to Contracting an altar requires a Muse of level {0} or higher.. /// diff --git a/src/GameLogic/Properties/PlayerMessage.resx b/src/GameLogic/Properties/PlayerMessage.resx index 4a6cbfe1c..356247337 100644 --- a/src/GameLogic/Properties/PlayerMessage.resx +++ b/src/GameLogic/Properties/PlayerMessage.resx @@ -942,4 +942,10 @@ The crywolf event proceeds from the state {0}. + + The altar ({0}) of the holy wolf can not be contracted now. + + + An altar can not be contracted on a mount. + diff --git a/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs new file mode 100644 index 000000000..6ed137e74 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs @@ -0,0 +1,177 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.GameLogic.Crywolf; +using MUnique.OpenMU.GameLogic.NPC; +using MUnique.OpenMU.Pathfinding; +using MonsterAttribute = MUnique.OpenMU.Persistence.BasicModel.MonsterAttribute; +using MonsterDefinition = MUnique.OpenMU.Persistence.BasicModel.MonsterDefinition; + +/// +/// Tests the run of the . +/// +[TestFixture] +public class CrywolfContextTest +{ + private static readonly Point AltarPosition = new(125, 27); + + /// + /// Tests that the battle is lost, when no altar is contracted when it starts. + /// + [Test] + public async Task BattleIsLostWithoutContractedAltarAsync() + { + var (context, _, saved) = await CreateContextAsync().ConfigureAwait(false); + + await ProceedToAsync(context, CrywolfState.Start).ConfigureAwait(false); + + Assert.That(context.State, Is.EqualTo(CrywolfState.End)); + Assert.That(context.Occupation, Is.EqualTo(CrywolfOccupationState.Occupied)); + Assert.That(saved, Has.Count.EqualTo(1), "The occupation is saved."); + Assert.That(saved[0], Is.EqualTo(CrywolfOccupationState.Occupied)); + } + + /// + /// Tests that an altar can't be contracted while the event isn't running. + /// + [Test] + public async Task AltarCantBeContractedOutsideTheEventAsync() + { + var (context, player, _) = await CreateContextAsync().ConfigureAwait(false); + var altar = context.Altars[0]; + + await context.ContractAltarAsync(player, altar.Npc.Id).ConfigureAwait(false); + + Assert.That(altar.State, Is.EqualTo(CrywolfAltarState.Free)); + Assert.That(altar.ContractCount, Is.Zero); + } + + /// + /// Tests that a contracted altar keeps the battle running, and that a repeated request doesn't change the contract. + /// + [Test] + public async Task ContractedAltarKeepsTheBattleRunningAsync() + { + var (context, player, _) = await CreateContextAsync().ConfigureAwait(false); + var altar = context.Altars[0]; + + await ProceedToAsync(context, CrywolfState.Ready).ConfigureAwait(false); + await context.ContractAltarAsync(player, altar.Npc.Id).ConfigureAwait(false); + await context.ContractAltarAsync(player, altar.Npc.Id).ConfigureAwait(false); + Assert.That(altar.ContractCount, Is.EqualTo(1), "The repeated request doesn't count as contract."); + + await context.TickAsync().ConfigureAwait(false); + Assert.That(altar.State, Is.EqualTo(CrywolfAltarState.Contracted)); + + await ProceedToAsync(context, CrywolfState.Start).ConfigureAwait(false); + await context.TickAsync().ConfigureAwait(false); + Assert.That(context.State, Is.EqualTo(CrywolfState.Start)); + Assert.That(context.ArmyStage, Is.EqualTo(CrywolfArmyStage.Advancing)); + } + + /// + /// Tests that the game master command lets Balgass appear during the battle, instead of ending it. + /// + [Test] + public async Task SkipDuringTheBattleLetsBalgassAppearAsync() + { + var (context, player, _) = await CreateContextAsync().ConfigureAwait(false); + var altar = context.Altars[0]; + + await ProceedToAsync(context, CrywolfState.Ready).ConfigureAwait(false); + await context.ContractAltarAsync(player, altar.Npc.Id).ConfigureAwait(false); + await context.TickAsync().ConfigureAwait(false); + await ProceedToAsync(context, CrywolfState.Start).ConfigureAwait(false); + + context.SkipWaitingTime(); + await context.TickAsync().ConfigureAwait(false); + + Assert.That(context.State, Is.EqualTo(CrywolfState.Start)); + Assert.That(context.ArmyStage, Is.EqualTo(CrywolfArmyStage.AttackingStatue)); + var balgass = context.Map!.GetNpcsInRange(new Point(110, 79), 3).OfType().FirstOrDefault(monster => monster.Definition.Number == 349); + Assert.That(balgass, Is.Not.Null); + } + + private static async ValueTask ProceedToAsync(CrywolfContext context, CrywolfState state) + { + for (var i = 0; i < 10 && context.State != state; i++) + { + context.SkipWaitingTime(); + await context.TickAsync().ConfigureAwait(false); + } + } + + private static async ValueTask<(CrywolfContext Context, Player Player, List Saved)> CreateContextAsync() + { + var gameContext = (GameContext)GameContextTestHelper.CreateGameContext(); + var map = (await gameContext.GetMapAsync(0).ConfigureAwait(false))!; + var definition = new CrywolfEventDefinition + { + MapNumber = 0, + ContractCharacterClassNumbers = new List { 0 }, + MinimumContractLevel = 0, + ContractDelay = TimeSpan.Zero, + MonsterGroups = new List(), + }; + + await AddNpcAsync(map, definition.StatueNumber, new Point(121, 31)).ConfigureAwait(false); + for (var i = 0; i < definition.AltarNumbers.Count; i++) + { + await AddNpcAsync(map, definition.AltarNumbers[i], new Point((byte)(AltarPosition.X - (i * 2)), AltarPosition.Y)).ConfigureAwait(false); + } + + var balgass = new MonsterDefinition { Id = Guid.NewGuid(), Number = 349, ObjectKind = NpcObjectKind.Monster, AttackDelay = TimeSpan.FromHours(1) }; + balgass.Attributes.Add(new MonsterAttribute { AttributeDefinition = Stats.MaximumHealth, Value = 100 }); + map.Definition.MonsterSpawns.Add(new Persistence.BasicModel.MonsterSpawnArea + { + MonsterDefinition = balgass, + GameMap = map.Definition, + X1 = 110, + X2 = 110, + Y1 = 79, + Y2 = 79, + Quantity = 1, + SpawnTrigger = SpawnTrigger.OnceAtWaveStart, + WaveNumber = definition.BalgassWaveNumber, + }); + + var saved = new List(); + var context = new CrywolfContext(gameContext, definition, d => + { + saved.Add(d.Occupation); + return ValueTask.CompletedTask; + }); + await context.InitializeAsync().ConfigureAwait(false); + + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + await player.PlayerState.TryAdvanceToAsync(PlayerState.EnteredWorld).ConfigureAwait(false); + player.IsAlive = true; + player.Position = AltarPosition; + await player.CurrentMap!.AddAsync(player).ConfigureAwait(false); + return (context, player, saved); + } + + private static async ValueTask AddNpcAsync(GameMap map, short number, Point position) + { + var npcDefinition = new MonsterDefinition { Id = Guid.NewGuid(), Number = number, ObjectKind = NpcObjectKind.PassiveNpc }; + var spawnArea = new MonsterSpawnArea + { + MonsterDefinition = npcDefinition, + GameMap = map.Definition, + X1 = position.X, + X2 = position.X, + Y1 = position.Y, + Y2 = position.Y, + Quantity = 1, + }; + var npc = new NonPlayerCharacter(spawnArea, npcDefinition, map); + npc.Initialize(); + await map.AddAsync(npc).ConfigureAwait(false); + } +} From 1558d2e77ebff0d9c1cb6308e5b2d61392665ce9 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 19:13:31 -0300 Subject: [PATCH 11/23] Save the crywolf occupation state in its own table 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 --- src/DataModel/Entities/CrywolfData.cs | 29 + src/GameLogic/Crywolf/CrywolfContext.cs | 40 +- .../Crywolf/CrywolfEventDefinition.cs | 8 - src/GameLogic/Crywolf/CrywolfPlugIn.cs | 23 +- src/Persistence/BackupService.cs | 7 + .../BasicModel/CrywolfData.Generated.cs | 46 + .../EntityFramework/EntityDataContext.cs | 5 + .../20260928215953_AddCrywolfData.Designer.cs | 5961 +++++++++++++++++ .../20260928215953_AddCrywolfData.cs | 37 + .../EntityDataContextModelSnapshot.cs | 17 + .../Model/CrywolfData.Generated.cs | 47 + .../Model/ExtendedTypeContext.Generated.cs | 1 + .../Model/ExtendedTypes.Custom.cs | 9 + .../Model/MapsterConfigurator.Generated.cs | 3 + .../CrywolfContextTest.cs | 43 +- 15 files changed, 6222 insertions(+), 54 deletions(-) create mode 100644 src/DataModel/Entities/CrywolfData.cs create mode 100644 src/Persistence/BasicModel/CrywolfData.Generated.cs create mode 100644 src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.Designer.cs create mode 100644 src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.cs create mode 100644 src/Persistence/EntityFramework/Model/CrywolfData.Generated.cs diff --git a/src/DataModel/Entities/CrywolfData.cs b/src/DataModel/Entities/CrywolfData.cs new file mode 100644 index 000000000..e7fea8d38 --- /dev/null +++ b/src/DataModel/Entities/CrywolfData.cs @@ -0,0 +1,29 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.DataModel.Entities; + +/// +/// Persistent state of the crywolf event, stored as a single row, like the original game does it. +/// It keeps the result of the last event until the next one. +/// +[AggregateRoot] +public class CrywolfData +{ + /// + /// Gets or sets the unique identifier of this record. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets a value indicating whether the crywolf fortress is occupied by Balgass, + /// because the last event failed. Otherwise, it's in peace. + /// + public bool IsOccupied { get; set; } + + /// + /// Gets or sets the time (UTC) when the last battle ended, or if there was no battle yet. + /// + public DateTime? LastBattleEnd { get; set; } +} diff --git a/src/GameLogic/Crywolf/CrywolfContext.cs b/src/GameLogic/Crywolf/CrywolfContext.cs index 128d61dcb..27d912091 100644 --- a/src/GameLogic/Crywolf/CrywolfContext.cs +++ b/src/GameLogic/Crywolf/CrywolfContext.cs @@ -26,7 +26,8 @@ namespace MUnique.OpenMU.GameLogic.Crywolf; /// The result is shown, and the players get experience depending on their score (). /// The army disappears and the common monsters return. The heroes are shown (). /// -/// The result of the event () is kept until the next event. +/// The result of the event () is kept until the next event. Like in the original game, +/// it's saved in the database (), so that it survives a restart of the server. /// public sealed class CrywolfContext : IEventStateProvider, IDisposable { @@ -41,7 +42,6 @@ public sealed class CrywolfContext : IEventStateProvider, IDisposable private readonly ConcurrentDictionary _removedMonsters = new(); private readonly ConcurrentDictionary _scores = new(); private readonly List _heroes = new(); - private readonly Func _saveDefinition; private CrywolfEventDefinition _definition; private GameMap? _map; @@ -61,20 +61,18 @@ public sealed class CrywolfContext : IEventStateProvider, IDisposable private IReadOnlyList _altars = []; private bool _isMissingConfigurationLogged; private int _isBalgassDead; + private Guid? _dataId; /// /// Initializes a new instance of the class. /// /// The game context. /// The definition of the event. - /// The function which saves the definition, when the occupation changed. - public CrywolfContext(GameContext gameContext, CrywolfEventDefinition definition, Func saveDefinition) + public CrywolfContext(GameContext gameContext, CrywolfEventDefinition definition) { this._gameContext = gameContext; this._definition = definition; - this._saveDefinition = saveDefinition; this._logger = gameContext.LoggerFactory.CreateLogger(); - this.Occupation = definition.Occupation; } /// @@ -85,7 +83,7 @@ public CrywolfContext(GameContext gameContext, CrywolfEventDefinition definition /// /// Gets the occupation state of the fortress. /// - public CrywolfOccupationState Occupation { get; private set; } + public CrywolfOccupationState Occupation { get; private set; } = CrywolfOccupationState.Peace; /// /// Gets the stage of the army: 0, when it doesn't move, 1 when it advances, and 2 when it attacks the statue. @@ -146,6 +144,7 @@ public async ValueTask InitializeAsync() return; } + await this.LoadOccupationAsync().ConfigureAwait(false); this._map.ObjectAdded += this.OnObjectAddedToMapAsync; this.InitializeNpcs(); await this.ApplyOccupationAsync().ConfigureAwait(false); @@ -922,17 +921,34 @@ private async ValueTask ApplyOccupationAsync() await this.UpdateEffectsAsync().ConfigureAwait(false); } - private async ValueTask SaveOccupationAsync() + private async ValueTask LoadOccupationAsync() { - if (this._definition.Occupation == this.Occupation) + try { - return; + using var context = this._gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CrywolfData), false, this._gameContext.Configuration); + if ((await context.GetAsync().ConfigureAwait(false)).FirstOrDefault() is { } data) + { + this._dataId = data.Id; + this.Occupation = data.IsOccupied ? CrywolfOccupationState.Occupied : CrywolfOccupationState.Peace; + } } + catch (Exception ex) + { + this._logger.LogError(ex, "Couldn't load the occupation state of the crywolf fortress."); + } + } - this._definition.Occupation = this.Occupation; + private async ValueTask SaveOccupationAsync() + { try { - await this._saveDefinition(this._definition).ConfigureAwait(false); + using var context = this._gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CrywolfData), false, this._gameContext.Configuration); + var data = this._dataId is { } id ? await context.GetByIdAsync(id).ConfigureAwait(false) : null; + data ??= context.CreateNew(); + data.IsOccupied = this.Occupation != CrywolfOccupationState.Peace; + data.LastBattleEnd = DateTime.UtcNow; + await context.SaveChangesAsync().ConfigureAwait(false); + this._dataId = data.Id; } catch (Exception ex) { diff --git a/src/GameLogic/Crywolf/CrywolfEventDefinition.cs b/src/GameLogic/Crywolf/CrywolfEventDefinition.cs index ee1e1e1cb..c1344eefb 100644 --- a/src/GameLogic/Crywolf/CrywolfEventDefinition.cs +++ b/src/GameLogic/Crywolf/CrywolfEventDefinition.cs @@ -240,14 +240,6 @@ public class CrywolfEventDefinition /// public IList AlwaysVisibleNpcNumbers { get; set; } = new List { 406, 407 }; - /// - /// Gets or sets the occupation state of the fortress. It's the result of the last event, which is kept until the next one. - /// - /// - /// It's saved by the event, so that it survives a restart of the server. - /// - public CrywolfOccupationState Occupation { get; set; } = CrywolfOccupationState.Peace; - /// /// Determines whether the event starts at the specified local time of the server. /// diff --git a/src/GameLogic/Crywolf/CrywolfPlugIn.cs b/src/GameLogic/Crywolf/CrywolfPlugIn.cs index 84d479d53..eb01b8ab8 100644 --- a/src/GameLogic/Crywolf/CrywolfPlugIn.cs +++ b/src/GameLogic/Crywolf/CrywolfPlugIn.cs @@ -14,7 +14,7 @@ namespace MUnique.OpenMU.GameLogic.Crywolf; /// /// /// The event starts at the configured times. The result of the event, the occupation state of the fortress, -/// is kept until the next event and saved in the configuration of this plugin. +/// is kept until the next event and saved in the database. /// [PlugIn] [Display(Name = nameof(PlugInResources.CrywolfPlugIn_Name), Description = nameof(PlugInResources.CrywolfPlugIn_Description), ResourceType = typeof(PlugInResources))] @@ -52,7 +52,7 @@ public async ValueTask ExecuteTaskAsync(GameContext gameContext) var definition = this.Configuration ??= new CrywolfEventDefinition(); if (!this._contexts.TryGetValue(gameContext, out var context)) { - context = new CrywolfContext(gameContext, definition, d => SaveConfigurationAsync(gameContext, d)); + context = new CrywolfContext(gameContext, definition); await context.InitializeAsync().ConfigureAwait(false); this._contexts[gameContext] = context; } @@ -98,23 +98,4 @@ public void Dispose() this._contexts.Clear(); } - - /// - /// Saves the configuration, so that the occupation state of the fortress survives a restart of the server. - /// - private static async ValueTask SaveConfigurationAsync(GameContext gameContext, CrywolfEventDefinition definition) - { - // The configuration is loaded through a new context, so that the change is tracked and saved. - using var context = gameContext.PersistenceContextProvider.CreateNewContext(); - var typeId = typeof(CrywolfPlugIn).GUID; - var gameConfiguration = (await context.GetAsync().ConfigureAwait(false)).FirstOrDefault(); - if (gameConfiguration?.PlugInConfigurations.FirstOrDefault(c => c.TypeId == typeId) is not { } configuration) - { - gameContext.LoggerFactory.CreateLogger().LogWarning("The configuration of the crywolf event wasn't found, so its occupation state isn't saved."); - return; - } - - configuration.SetConfiguration(definition, gameContext.PlugInManager.CustomConfigReferenceHandler); - await context.SaveChangesAsync().ConfigureAwait(false); - } } diff --git a/src/Persistence/BackupService.cs b/src/Persistence/BackupService.cs index ec7f2c18c..af5ba91b0 100644 --- a/src/Persistence/BackupService.cs +++ b/src/Persistence/BackupService.cs @@ -37,6 +37,7 @@ private static readonly (string Prefix, Type BasicModelType)[] EntryTypeInfos = ("ConfigurationUpdateState_", typeof(BasicModel.ConfigurationUpdateState)), ("Account_", typeof(BasicModel.Account)), ("CastleSiegeData_", typeof(BasicModel.CastleSiegeData)), + ("CrywolfData_", typeof(BasicModel.CrywolfData)), ]; private readonly IPersistenceContextProvider _contextProvider; @@ -90,6 +91,7 @@ public async Task CreateBackupAsync(Stream outputStream, BackupOptions options, } await ExportAsync(archive, "CastleSiegeData_", context, sharedHandler, cancellationToken).ConfigureAwait(false); + await ExportAsync(archive, "CrywolfData_", context, sharedHandler, cancellationToken).ConfigureAwait(false); } /// @@ -282,6 +284,11 @@ private static async Task ExportAsync( return deserializer.Deserialize(ms, referenceHandler); } + if (basicModelType == typeof(BasicModel.CrywolfData)) + { + return deserializer.Deserialize(ms, referenceHandler); + } + if (basicModelType == typeof(AdminUser)) { return deserializer.Deserialize(ms, referenceHandler); diff --git a/src/Persistence/BasicModel/CrywolfData.Generated.cs b/src/Persistence/BasicModel/CrywolfData.Generated.cs new file mode 100644 index 000000000..c4fd8f6f4 --- /dev/null +++ b/src/Persistence/BasicModel/CrywolfData.Generated.cs @@ -0,0 +1,46 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +//------------------------------------------------------------------------------ +// +// This source code was auto-generated by a roslyn code generator. +// +//------------------------------------------------------------------------------ + +// ReSharper disable All + +namespace MUnique.OpenMU.Persistence.BasicModel; + +using MUnique.OpenMU.Persistence.Json; + +/// +/// A plain implementation of . +/// +public partial class CrywolfData : MUnique.OpenMU.DataModel.Entities.CrywolfData, IIdentifiable, IConvertibleTo +{ + + + + + /// + public override bool Equals(object obj) + { + var baseObject = obj as IIdentifiable; + if (baseObject != null) + { + return baseObject.Id == this.Id; + } + + return base.Equals(obj); + } + + /// + public override int GetHashCode() + { + return this.Id.GetHashCode(); + } + + /// + public CrywolfData Convert() => this; +} diff --git a/src/Persistence/EntityFramework/EntityDataContext.cs b/src/Persistence/EntityFramework/EntityDataContext.cs index dd05e4ef7..499285459 100644 --- a/src/Persistence/EntityFramework/EntityDataContext.cs +++ b/src/Persistence/EntityFramework/EntityDataContext.cs @@ -26,6 +26,11 @@ public class EntityDataContext : ExtendedTypeContext /// internal DbSet CastleSiegeData => this.Set(); + /// + /// Gets the persistent state of the crywolf event. + /// + internal DbSet CrywolfData => this.Set(); + /// /// Gets the Castle Siege guild registrations. /// diff --git a/src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.Designer.cs b/src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.Designer.cs new file mode 100644 index 000000000..4d72d8ec8 --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.Designer.cs @@ -0,0 +1,5961 @@ +// +using System; +using MUnique.OpenMU.Persistence.EntityFramework; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations +{ + [DbContext(typeof(EntityDataContext))] + [Migration("20260928215953_AddCrywolfData")] + partial class AddCrywolfData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatBanUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("EMail") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsBot") + .HasColumnType("boolean"); + + b.Property("IsNetworkObservationActive") + .HasColumnType("boolean"); + + b.Property("IsTemplate") + .HasColumnType("boolean"); + + b.Property("IsVaultExtended") + .HasColumnType("boolean"); + + b.Property("LanguageIsoCode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasDefaultValue("en"); + + b.Property("LoginName") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegistrationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("TimeZone") + .HasColumnType("smallint"); + + b.Property("VaultId") + .HasColumnType("uuid"); + + b.Property("VaultPassword") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("LoginName") + .IsUnique(); + + b.HasIndex("VaultId") + .IsUnique(); + + b.ToTable("Account", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AccountCharacterClass", b => + { + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.HasKey("AccountId", "CharacterClassId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("AccountCharacterClass", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("FullAncientSetEquipped") + .HasColumnType("boolean"); + + b.Property("Pose") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("AppearanceData", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AreaSkillSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DelayBetweenHits") + .HasColumnType("interval"); + + b.Property("DelayPerOneDistance") + .HasColumnType("interval"); + + b.Property("EffectRange") + .HasColumnType("integer"); + + b.Property("FrustumDistance") + .HasColumnType("real"); + + b.Property("FrustumEndWidth") + .HasColumnType("real"); + + b.Property("FrustumStartWidth") + .HasColumnType("real"); + + b.Property("HitChancePerDistanceMultiplier") + .HasColumnType("real"); + + b.Property("MaximumNumberOfHitsPerAttack") + .HasColumnType("integer"); + + b.Property("MaximumNumberOfHitsPerTarget") + .HasColumnType("integer"); + + b.Property("MinimumNumberOfHitsPerAttack") + .HasColumnType("integer"); + + b.Property("MinimumNumberOfHitsPerTarget") + .HasColumnType("integer"); + + b.Property("ProjectileCount") + .HasColumnType("integer"); + + b.Property("TargetAreaDiameter") + .HasColumnType("real"); + + b.Property("UseDeferredHits") + .HasColumnType("boolean"); + + b.Property("UseFrustumFilter") + .HasColumnType("boolean"); + + b.Property("UseTargetAreaFilter") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("AreaSkillSettings", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Designation") + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MaximumValue") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("AttributeDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRelationship", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("InputAttributeId") + .HasColumnType("uuid"); + + b.Property("InputOperand") + .HasColumnType("real"); + + b.Property("InputOperator") + .HasColumnType("integer"); + + b.Property("OperandAttributeId") + .HasColumnType("uuid"); + + b.Property("PowerUpDefinitionValueId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterClassId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("InputAttributeId"); + + b.HasIndex("OperandAttributeId"); + + b.HasIndex("PowerUpDefinitionValueId"); + + b.HasIndex("SkillId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("AttributeRelationship", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeId") + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("MinimumValue") + .HasColumnType("integer"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("SkillId1") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AttributeId"); + + b.HasIndex("GameMapDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("SkillId"); + + b.HasIndex("SkillId1"); + + b.ToTable("AttributeRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroundId") + .HasColumnType("uuid"); + + b.Property("LeftGoalId") + .HasColumnType("uuid"); + + b.Property("LeftTeamSpawnPointX") + .HasColumnType("smallint"); + + b.Property("LeftTeamSpawnPointY") + .HasColumnType("smallint"); + + b.Property("RightGoalId") + .HasColumnType("uuid"); + + b.Property("RightTeamSpawnPointX") + .HasColumnType("smallint"); + + b.Property("RightTeamSpawnPointY") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GroundId") + .IsUnique(); + + b.HasIndex("LeftGoalId") + .IsUnique(); + + b.HasIndex("RightGoalId") + .IsUnique(); + + b.ToTable("BattleZoneDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MagicEffectDefinitionId") + .HasColumnType("uuid"); + + b.Property("MaximumLevel") + .HasColumnType("integer"); + + b.Property("MinimumLevel") + .HasColumnType("integer"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MagicEffectDefinitionId") + .IsUnique(); + + b.HasIndex("MonsterDefinitionId"); + + b.ToTable("Buff", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttackRespawnAreaId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("CrownHoldTimeSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30); + + b.Property("DefenseRespawnAreaId") + .HasColumnType("uuid"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("GateBuyPrice") + .HasColumnType("integer"); + + b.Property("GateRepairCostPerHealthPoint") + .HasColumnType("integer"); + + b.Property("GuildScoreCastleSiege") + .HasColumnType("integer"); + + b.Property("GuildScoreCastleSiegeMembers") + .HasColumnType("integer"); + + b.Property("LandOfTrialsMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("MaxAttackingGuilds") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(3); + + b.Property("ParticipantRewardMinSeconds") + .HasColumnType("integer"); + + b.Property("RegisterMinLevel") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(200); + + b.Property("RegisterMinMembers") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(20); + + b.Property("RepairCostPerUpgradeLevel") + .HasColumnType("integer"); + + b.Property("RewardItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("SignOfLordItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("SignOfLordItemLevel") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)3); + + b.Property("StatueBuyPrice") + .HasColumnType("integer"); + + b.Property("StatueRepairCostPerHealthPoint") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AttackRespawnAreaId") + .IsUnique(); + + b.HasIndex("CastleSiegeMapDefinitionId"); + + b.HasIndex("DefenseRespawnAreaId") + .IsUnique(); + + b.HasIndex("LandOfTrialsMapDefinitionId"); + + b.HasIndex("RewardItemDefinitionId"); + + b.HasIndex("SignOfLordItemDefinitionId"); + + b.ToTable("CastleSiegeConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsHuntZoneEnabled") + .HasColumnType("boolean"); + + b.Property("IsOccupied") + .HasColumnType("boolean"); + + b.Property("OwnerGuildId") + .HasColumnType("uuid"); + + b.Property("TaxChaos") + .HasColumnType("smallint"); + + b.Property("TaxHunt") + .HasColumnType("integer"); + + b.Property("TaxStore") + .HasColumnType("smallint"); + + b.Property("TributeMoney") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OwnerGuildId"); + + b.ToTable("CastleSiegeData", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeDataId") + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("GuildName") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("IsAllianceMaster") + .HasColumnType("boolean"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("Side") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeDataId"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("CastleSiegeGuild", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuildRegistration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("GuildName") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("Marks") + .HasColumnType("integer"); + + b.Property("RegistrationOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("CastleSiegeGuildRegistration", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("DefaultSide") + .HasColumnType("smallint"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("smallint"); + + b.Property("IsPersistedToDatabase") + .HasColumnType("boolean"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("SpawnX") + .HasColumnType("smallint"); + + b.Property("SpawnY") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.HasIndex("MonsterDefinitionId", "InstanceId"); + + b.ToTable("CastleSiegeNpcDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeDataId") + .HasColumnType("uuid"); + + b.Property("CurrentHp") + .HasColumnType("integer"); + + b.Property("DefenseLevel") + .HasColumnType("smallint"); + + b.Property("InstanceId") + .HasColumnType("smallint"); + + b.Property("LifeLevel") + .HasColumnType("smallint"); + + b.Property("MonsterNumber") + .HasColumnType("smallint"); + + b.Property("RegenLevel") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeDataId"); + + b.HasIndex("MonsterNumber", "InstanceId") + .IsUnique(); + + b.ToTable("CastleSiegeNpcState", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegePendingReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("CastleSiegePendingReward", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeStateScheduleEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("Hour") + .HasColumnType("smallint"); + + b.Property("Minute") + .HasColumnType("smallint"); + + b.Property("State") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.ToTable("CastleSiegeStateScheduleEntry", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeUpgradeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId1") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId2") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId3") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId4") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("RequiredJewelOfGuardianCount") + .HasColumnType("integer"); + + b.Property("RequiredZen") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.HasIndex("CastleSiegeConfigurationId1"); + + b.HasIndex("CastleSiegeConfigurationId2"); + + b.HasIndex("CastleSiegeConfigurationId3"); + + b.HasIndex("CastleSiegeConfigurationId4"); + + b.ToTable("CastleSiegeUpgradeDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId1") + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.HasIndex("CastleSiegeConfigurationId1"); + + b.ToTable("CastleSiegeZoneDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("CharacterSlot") + .HasColumnType("smallint"); + + b.Property("CharacterStatus") + .HasColumnType("integer"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentMapId") + .HasColumnType("uuid"); + + b.Property("Experience") + .HasColumnType("bigint"); + + b.Property("InventoryExtensions") + .HasColumnType("integer"); + + b.Property("InventoryId") + .HasColumnType("uuid"); + + b.Property("IsStoreOpened") + .HasColumnType("boolean"); + + b.Property("KeyConfiguration") + .HasColumnType("bytea"); + + b.Property("LevelUpPoints") + .HasColumnType("integer"); + + b.Property("MasterExperience") + .HasColumnType("bigint"); + + b.Property("MasterLevelUpPoints") + .HasColumnType("integer"); + + b.Property("MuHelperConfiguration") + .HasColumnType("bytea"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("PlayerKillCount") + .HasColumnType("integer"); + + b.Property("Pose") + .HasColumnType("smallint"); + + b.Property("PositionX") + .HasColumnType("smallint"); + + b.Property("PositionY") + .HasColumnType("smallint"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("StateRemainingSeconds") + .HasColumnType("integer"); + + b.Property("StoreName") + .HasColumnType("text"); + + b.Property("UsedFruitPoints") + .HasColumnType("integer"); + + b.Property("UsedNegFruitPoints") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CharacterClassId"); + + b.HasIndex("CurrentMapId"); + + b.HasIndex("InventoryId") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Character", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanGetCreated") + .HasColumnType("boolean"); + + b.Property("ComboDefinitionId") + .HasColumnType("uuid"); + + b.Property("CreationAllowedFlag") + .HasColumnType("smallint"); + + b.Property("FruitCalculation") + .HasColumnType("integer"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("HomeMapId") + .HasColumnType("uuid"); + + b.Property("IsMasterClass") + .HasColumnType("boolean"); + + b.Property("LevelRequirementByCreation") + .HasColumnType("smallint"); + + b.Property("LevelWarpRequirementReductionPercent") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextGenerationClassId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ComboDefinitionId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("HomeMapId"); + + b.HasIndex("NextGenerationClassId"); + + b.ToTable("CharacterClass", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterDropItemGroup", b => + { + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.HasKey("CharacterId", "DropItemGroupId"); + + b.HasIndex("DropItemGroupId"); + + b.ToTable("CharacterDropItemGroup", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActiveQuestId") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("ClientActionPerformed") + .HasColumnType("boolean"); + + b.Property("Group") + .HasColumnType("smallint"); + + b.Property("LastFinishedQuestId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ActiveQuestId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("LastFinishedQuestId"); + + b.ToTable("CharacterQuestState", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientCleanUpInterval") + .HasColumnType("interval"); + + b.Property("ClientTimeout") + .HasColumnType("interval"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaximumConnections") + .HasColumnType("integer"); + + b.Property("RoomCleanUpInterval") + .HasColumnType("interval"); + + b.Property("ServerId") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("ChatServerDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatServerDefinitionId") + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("NetworkPort") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ChatServerDefinitionId"); + + b.HasIndex("ClientId"); + + b.ToTable("ChatServerEndpoint", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CombinationBonusRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemOptionCombinationBonusId") + .HasColumnType("uuid"); + + b.Property("MinimumCount") + .HasColumnType("integer"); + + b.Property("OptionTypeId") + .HasColumnType("uuid"); + + b.Property("SubOptionType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemOptionCombinationBonusId"); + + b.HasIndex("OptionTypeId"); + + b.ToTable("CombinationBonusRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConfigurationUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstalledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ConfigurationUpdate", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConfigurationUpdateState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrentInstalledVersion") + .HasColumnType("integer"); + + b.Property("InitializationKey") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ConfigurationUpdateState", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConnectServerDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CheckMaxConnectionsPerAddress") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientListenerPort") + .HasColumnType("integer"); + + b.Property("CurrentPatchVersion") + .HasColumnType("bytea"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DisconnectOnUnknownPacket") + .HasColumnType("boolean"); + + b.Property("ListenerBacklog") + .HasColumnType("integer"); + + b.Property("MaxConnections") + .HasColumnType("integer"); + + b.Property("MaxConnectionsPerAddress") + .HasColumnType("integer"); + + b.Property("MaxFtpRequests") + .HasColumnType("integer"); + + b.Property("MaxIpRequests") + .HasColumnType("integer"); + + b.Property("MaxServerListRequests") + .HasColumnType("integer"); + + b.Property("MaximumReceiveSize") + .HasColumnType("smallint"); + + b.Property("PatchAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("smallint"); + + b.Property("Timeout") + .HasColumnType("interval"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ConnectServerDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConstValueAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("CharacterClassId"); + + b.HasIndex("DefinitionId"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ConstValueAttribute", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CrywolfData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsOccupied") + .HasColumnType("boolean"); + + b.Property("LastBattleEnd") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("CrywolfData", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Chance") + .HasColumnType("double precision"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("ItemLevel") + .HasColumnType("smallint"); + + b.Property("ItemType") + .HasColumnType("integer"); + + b.Property("MaximumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MinimumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MonsterId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MonsterId"); + + b.ToTable("DropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroupItemDefinition", b => + { + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("DropItemGroupId", "ItemDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("DropItemGroupItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DuelConfigurationId") + .HasColumnType("uuid"); + + b.Property("FirstPlayerGateId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("smallint"); + + b.Property("SecondPlayerGateId") + .HasColumnType("uuid"); + + b.Property("SpectatorsGateId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DuelConfigurationId"); + + b.HasIndex("FirstPlayerGateId"); + + b.HasIndex("SecondPlayerGateId"); + + b.HasIndex("SpectatorsGateId"); + + b.ToTable("DuelArea", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntranceFee") + .HasColumnType("integer"); + + b.Property("ExitId") + .HasColumnType("uuid"); + + b.Property("MaximumScore") + .HasColumnType("integer"); + + b.Property("MaximumSpectatorsPerDuelRoom") + .HasColumnType("integer"); + + b.Property("MinimumCharacterLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExitId"); + + b.ToTable("DuelConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.EnterGate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("LevelRequirement") + .HasColumnType("smallint"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("TargetGateId") + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GameMapDefinitionId"); + + b.HasIndex("TargetGateId"); + + b.ToTable("EnterGate", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("IsSpawnGate") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("MapId"); + + b.ToTable("ExitGate", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Friend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Accepted") + .HasColumnType("boolean"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("FriendId") + .HasColumnType("uuid"); + + b.Property("RequestOpen") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasAlternateKey("CharacterId", "FriendId"); + + b.ToTable("Friend", "friend"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Episode") + .HasColumnType("smallint"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("Season") + .HasColumnType("smallint"); + + b.Property("Serial") + .HasColumnType("bytea"); + + b.Property("Version") + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.ToTable("GameClientDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AreaSkillHitsPlayer") + .HasColumnType("boolean"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("CharacterNameRegex") + .HasColumnType("text"); + + b.Property("ClampMoneyOnPickup") + .HasColumnType("boolean"); + + b.Property("DamagePerOneItemDurability") + .HasColumnType("double precision"); + + b.Property("DamagePerOnePetDurability") + .HasColumnType("double precision"); + + b.Property("DuelConfigurationId") + .HasColumnType("uuid"); + + b.Property("ExcellentItemDropLevelDelta") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)25); + + b.Property("ExperienceFormula") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("if(level == 0, 0, if(level < 256, 10 * (level + 8) * (level - 1) * (level - 1), (10 * (level + 8) * (level - 1) * (level - 1)) + (1000 * (level - 247) * (level - 256) * (level - 256))))"); + + b.Property("ExperienceRate") + .HasColumnType("real"); + + b.Property("HitsPerOneItemDurability") + .HasColumnType("double precision"); + + b.Property("InfoRange") + .HasColumnType("smallint"); + + b.Property("ItemDropDuration") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 1, 0, 0)); + + b.Property("LetterSendPrice") + .HasColumnType("integer"); + + b.Property("MasterExperienceFormula") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("(505 * level * level * level) + (35278500 * level) + (228045 * level * level)"); + + b.Property("MasterExperienceRate") + .HasColumnType("real"); + + b.Property("MaximumCharactersPerAccount") + .HasColumnType("smallint"); + + b.Property("MaximumInventoryMoney") + .HasColumnType("integer"); + + b.Property("MaximumItemOptionLevelDrop") + .HasColumnType("smallint"); + + b.Property("MaximumLetters") + .HasColumnType("integer"); + + b.Property("MaximumLevel") + .HasColumnType("smallint"); + + b.Property("MaximumMasterLevel") + .HasColumnType("smallint"); + + b.Property("MaximumPartySize") + .HasColumnType("smallint"); + + b.Property("MaximumPasswordLength") + .HasColumnType("integer"); + + b.Property("MaximumVaultMoney") + .HasColumnType("integer"); + + b.Property("MinimumMonsterLevelForMasterExperience") + .HasColumnType("smallint"); + + b.Property("PreventExperienceOverflow") + .HasColumnType("boolean"); + + b.Property("RecoveryInterval") + .HasColumnType("integer"); + + b.Property("ShouldDropMoney") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId") + .IsUnique(); + + b.HasIndex("DuelConfigurationId") + .IsUnique(); + + b.ToTable("GameConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BattleZoneId") + .HasColumnType("uuid"); + + b.Property("Discriminator") + .HasColumnType("integer"); + + b.Property("ExpMultiplier") + .HasColumnType("double precision"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("SafezoneMapId") + .HasColumnType("uuid"); + + b.Property("TerrainData") + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("BattleZoneId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("SafezoneMapId"); + + b.ToTable("GameMapDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinitionDropItemGroup", b => + { + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.HasKey("GameMapDefinitionId", "DropItemGroupId"); + + b.HasIndex("DropItemGroupId"); + + b.ToTable("GameMapDefinitionDropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MaximumPlayers") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("GameServerConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfigurationGameMapDefinition", b => + { + b.Property("GameServerConfigurationId") + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("GameServerConfigurationId", "GameMapDefinitionId"); + + b.HasIndex("GameMapDefinitionId"); + + b.ToTable("GameServerConfigurationGameMapDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExperienceRate") + .HasColumnType("real"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("PvpEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ServerConfigurationId") + .HasColumnType("uuid"); + + b.Property("ServerID") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("ServerConfigurationId"); + + b.ToTable("GameServerDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AlternativePublishedPort") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("GameServerDefinitionId") + .HasColumnType("uuid"); + + b.Property("NetworkPort") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("GameServerDefinitionId"); + + b.ToTable("GameServerEndpoint", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllianceGuildId") + .HasColumnType("uuid"); + + b.Property("HostilityId") + .HasColumnType("uuid"); + + b.Property("Logo") + .HasColumnType("bytea"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("Notice") + .HasColumnType("text"); + + b.Property("Score") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AllianceGuildId"); + + b.HasIndex("HostilityId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Guild", "guild"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GuildMember", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.ToTable("GuildMember", "guild"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemOptionDefinitionId") + .HasColumnType("uuid"); + + b.Property("LevelType") + .HasColumnType("integer"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OptionTypeId") + .HasColumnType("uuid"); + + b.Property("PowerUpDefinitionId") + .HasColumnType("uuid"); + + b.Property("SubOptionType") + .HasColumnType("integer"); + + b.Property("Weight") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ItemOptionDefinitionId"); + + b.HasIndex("OptionTypeId"); + + b.HasIndex("PowerUpDefinitionId") + .IsUnique(); + + b.ToTable("IncreasableItemOption", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("Durability") + .HasColumnType("double precision"); + + b.Property("HasSkill") + .HasColumnType("boolean"); + + b.Property("ItemSlot") + .HasColumnType("smallint"); + + b.Property("ItemStorageId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("PetExperience") + .HasColumnType("integer"); + + b.Property("SocketCount") + .HasColumnType("integer"); + + b.Property("StorePrice") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DefinitionId"); + + b.HasIndex("ItemStorageId"); + + b.ToTable("Item", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppearanceDataId") + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemSlot") + .HasColumnType("smallint"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AppearanceDataId"); + + b.HasIndex("DefinitionId"); + + b.ToTable("ItemAppearance", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearanceItemOptionType", b => + { + b.Property("ItemAppearanceId") + .HasColumnType("uuid"); + + b.Property("ItemOptionTypeId") + .HasColumnType("uuid"); + + b.HasKey("ItemAppearanceId", "ItemOptionTypeId"); + + b.HasIndex("ItemOptionTypeId"); + + b.ToTable("ItemAppearanceItemOptionType", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemBasePowerUpDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("BaseValue") + .HasColumnType("real"); + + b.Property("BonusPerLevelTableId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BonusPerLevelTableId"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("ItemBasePowerUpDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCrafting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemCraftingHandlerClassName") + .IsRequired() + .HasColumnType("text"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("SimpleCraftingSettingsId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MonsterDefinitionId"); + + b.HasIndex("SimpleCraftingSettingsId") + .IsUnique(); + + b.ToTable("ItemCrafting", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddPercentage") + .HasColumnType("smallint"); + + b.Property("FailResult") + .HasColumnType("integer"); + + b.Property("MaximumAmount") + .HasColumnType("smallint"); + + b.Property("MaximumItemLevel") + .HasColumnType("smallint"); + + b.Property("MinimumAmount") + .HasColumnType("smallint"); + + b.Property("MinimumItemLevel") + .HasColumnType("smallint"); + + b.Property("NpcPriceDivisor") + .HasColumnType("integer"); + + b.Property("Reference") + .HasColumnType("smallint"); + + b.Property("SimpleCraftingSettingsId") + .HasColumnType("uuid"); + + b.Property("SuccessResult") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SimpleCraftingSettingsId"); + + b.ToTable("ItemCraftingRequiredItem", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemDefinition", b => + { + b.Property("ItemCraftingRequiredItemId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("ItemCraftingRequiredItemId", "ItemDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("ItemCraftingRequiredItemItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemOptionType", b => + { + b.Property("ItemCraftingRequiredItemId") + .HasColumnType("uuid"); + + b.Property("ItemOptionTypeId") + .HasColumnType("uuid"); + + b.HasKey("ItemCraftingRequiredItemId", "ItemOptionTypeId"); + + b.HasIndex("ItemOptionTypeId"); + + b.ToTable("ItemCraftingRequiredItemItemOptionType", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingResultItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddLevel") + .HasColumnType("smallint"); + + b.Property("Durability") + .HasColumnType("smallint"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("RandomMaximumLevel") + .HasColumnType("smallint"); + + b.Property("RandomMinimumLevel") + .HasColumnType("smallint"); + + b.Property("Reference") + .HasColumnType("smallint"); + + b.Property("SimpleCraftingSettingsId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("SimpleCraftingSettingsId"); + + b.ToTable("ItemCraftingResultItem", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConsumeEffectId") + .HasColumnType("uuid"); + + b.Property("DropLevel") + .HasColumnType("smallint"); + + b.Property("DropsFromMonsters") + .HasColumnType("boolean"); + + b.Property("Durability") + .HasColumnType("smallint"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Group") + .HasColumnType("smallint"); + + b.Property("Height") + .HasColumnType("smallint"); + + b.Property("IsAmmunition") + .HasColumnType("boolean"); + + b.Property("IsBoundToCharacter") + .HasColumnType("boolean"); + + b.Property("IsDroppable") + .HasColumnType("boolean"); + + b.Property("IsPersonalStoreSellable") + .HasColumnType("boolean"); + + b.Property("IsQuestItem") + .HasColumnType("boolean"); + + b.Property("IsRepairable") + .HasColumnType("boolean"); + + b.Property("IsSellableToNpc") + .HasColumnType("boolean"); + + b.Property("IsStorable") + .HasColumnType("boolean"); + + b.Property("IsTradable") + .HasColumnType("boolean"); + + b.Property("ItemSlotId") + .HasColumnType("uuid"); + + b.Property("MaximumDropLevel") + .HasColumnType("smallint"); + + b.Property("MaximumItemLevel") + .HasColumnType("smallint"); + + b.Property("MaximumSockets") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("PetExperienceFormula") + .HasColumnType("text"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("StorageLimitPerCharacter") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("integer"); + + b.Property("Width") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ConsumeEffectId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("ItemSlotId"); + + b.HasIndex("SkillId"); + + b.ToTable("ItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionCharacterClass", b => + { + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.HasKey("ItemDefinitionId", "CharacterClassId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("ItemDefinitionCharacterClass", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemOptionDefinition", b => + { + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemOptionDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("ItemDefinitionId", "ItemOptionDefinitionId"); + + b.HasIndex("ItemOptionDefinitionId"); + + b.ToTable("ItemDefinitionItemOptionDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemSetGroup", b => + { + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemSetGroupId") + .HasColumnType("uuid"); + + b.HasKey("ItemDefinitionId", "ItemSetGroupId"); + + b.HasIndex("ItemSetGroupId"); + + b.ToTable("ItemDefinitionItemSetGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Chance") + .HasColumnType("double precision"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DropEffect") + .HasColumnType("integer"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemLevel") + .HasColumnType("smallint"); + + b.Property("ItemType") + .HasColumnType("integer"); + + b.Property("MaximumLevel") + .HasColumnType("smallint"); + + b.Property("MaximumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MinimumLevel") + .HasColumnType("smallint"); + + b.Property("MinimumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MoneyAmount") + .HasColumnType("integer"); + + b.Property("MonsterId") + .HasColumnType("uuid"); + + b.Property("RequiredCharacterLevel") + .HasColumnType("smallint"); + + b.Property("SourceItemLevel") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("MonsterId"); + + b.ToTable("ItemDropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroupItemDefinition", b => + { + b.Property("ItemDropItemGroupId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("ItemDropItemGroupId", "ItemDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("ItemDropItemGroupItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemItemOfItemSet", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ItemOfItemSetId") + .HasColumnType("uuid"); + + b.HasKey("ItemId", "ItemOfItemSetId"); + + b.HasIndex("ItemOfItemSetId"); + + b.ToTable("ItemItemOfItemSet", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemLevelBonusTable", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOfItemSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AncientSetDiscriminator") + .HasColumnType("integer"); + + b.Property("BonusOptionId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemSetGroupId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BonusOptionId"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("ItemSetGroupId"); + + b.ToTable("ItemOfItemSet", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OptionTypeId") + .HasColumnType("uuid"); + + b.Property("PowerUpDefinitionId") + .HasColumnType("uuid"); + + b.Property("SubOptionType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OptionTypeId"); + + b.HasIndex("PowerUpDefinitionId") + .IsUnique(); + + b.ToTable("ItemOption", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppliesMultipleTimes") + .HasColumnType("boolean"); + + b.Property("BonusId") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BonusId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemOptionCombinationBonus", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddChance") + .HasColumnType("real"); + + b.Property("AddsRandomly") + .HasColumnType("boolean"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MaximumOptionsPerItem") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemOptionDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ItemOptionId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.HasIndex("ItemOptionId"); + + b.ToTable("ItemOptionLink", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionOfLevel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IncreasableItemOptionId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("PowerUpDefinitionId") + .HasColumnType("uuid"); + + b.Property("RequiredItemLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IncreasableItemOptionId"); + + b.HasIndex("PowerUpDefinitionId") + .IsUnique(); + + b.ToTable("ItemOptionOfLevel", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemOptionType", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AlwaysApplies") + .HasColumnType("boolean"); + + b.Property("CountDistinct") + .HasColumnType("boolean"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MinimumItemCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OptionsId") + .HasColumnType("uuid"); + + b.Property("SetLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("OptionsId"); + + b.ToTable("ItemSetGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSlotType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("RawItemSlots") + .HasColumnType("text") + .HasColumnName("ItemSlots") + .HasJsonPropertyName("itemSlots"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemSlotType", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Money") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ItemStorage", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.JewelMix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MixedJewelId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("SingleJewelId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MixedJewelId"); + + b.HasIndex("SingleJewelId"); + + b.ToTable("JewelMix", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterBody", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Animation") + .HasColumnType("smallint"); + + b.Property("HeaderId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rotation") + .HasColumnType("smallint"); + + b.Property("SenderAppearanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("HeaderId"); + + b.HasIndex("SenderAppearanceId") + .IsUnique(); + + b.ToTable("LetterBody", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterHeader", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("LetterDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadFlag") + .HasColumnType("boolean"); + + b.Property("ReceiverId") + .HasColumnType("uuid"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ReceiverId"); + + b.ToTable("LetterHeader", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LevelBonus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdditionalValue") + .HasColumnType("real"); + + b.Property("ItemLevelBonusTableId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemLevelBonusTableId"); + + b.ToTable("LevelBonus", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChanceId") + .HasColumnType("uuid"); + + b.Property("ChancePvpId") + .HasColumnType("uuid"); + + b.Property("DurationDependsOnTargetLevel") + .HasColumnType("boolean"); + + b.Property("DurationId") + .HasColumnType("uuid"); + + b.Property("DurationPvpId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("InformObservers") + .HasColumnType("boolean"); + + b.Property("MonsterTargetLevelDivisor") + .HasColumnType("real"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("PlayerTargetLevelDivisor") + .HasColumnType("real"); + + b.Property("SendDuration") + .HasColumnType("boolean"); + + b.Property("StopByDeath") + .HasColumnType("boolean"); + + b.Property("SubType") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ChanceId") + .IsUnique(); + + b.HasIndex("ChancePvpId") + .IsUnique(); + + b.HasIndex("DurationId") + .IsUnique(); + + b.HasIndex("DurationPvpId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("MagicEffectDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Aggregation") + .HasColumnType("integer"); + + b.Property("DisplayValueFormula") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtendsDuration") + .HasColumnType("boolean"); + + b.Property("MaximumLevel") + .HasColumnType("smallint"); + + b.Property("MinimumLevel") + .HasColumnType("smallint"); + + b.Property("Rank") + .HasColumnType("smallint"); + + b.Property("ReplacedSkillId") + .HasColumnType("uuid"); + + b.Property("RootId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.Property("ValueFormula") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ReplacedSkillId"); + + b.HasIndex("RootId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("MasterSkillDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinitionSkill", b => + { + b.Property("MasterSkillDefinitionId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("MasterSkillDefinitionId", "SkillId"); + + b.HasIndex("SkillId"); + + b.ToTable("MasterSkillDefinitionSkill", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("MasterSkillRoot", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("MiniGameDefinitionId") + .HasColumnType("uuid"); + + b.Property("MinimumTargetLevel") + .HasColumnType("smallint"); + + b.Property("MultiplyKillsByPlayers") + .HasColumnType("boolean"); + + b.Property("NumberOfKills") + .HasColumnType("smallint"); + + b.Property("SpawnAreaId") + .HasColumnType("uuid"); + + b.Property("Target") + .HasColumnType("integer"); + + b.Property("TargetDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MiniGameDefinitionId"); + + b.HasIndex("SpawnAreaId") + .IsUnique(); + + b.HasIndex("TargetDefinitionId"); + + b.ToTable("MiniGameChangeEvent", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowParty") + .HasColumnType("boolean"); + + b.Property("ArePlayerKillersAllowedToEnter") + .HasColumnType("boolean"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EnterDuration") + .HasColumnType("interval"); + + b.Property("EntranceFee") + .HasColumnType("integer"); + + b.Property("EntranceId") + .HasColumnType("uuid"); + + b.Property("ExitDuration") + .HasColumnType("interval"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("GameDuration") + .HasColumnType("interval"); + + b.Property("GameLevel") + .HasColumnType("smallint"); + + b.Property("MapCreationPolicy") + .HasColumnType("integer"); + + b.Property("MaximumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MaximumPlayerCount") + .HasColumnType("integer"); + + b.Property("MaximumSpecialCharacterLevel") + .HasColumnType("integer"); + + b.Property("MinimumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MinimumSpecialCharacterLevel") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequiresMasterClass") + .HasColumnType("boolean"); + + b.Property("SaveRankingStatistics") + .HasColumnType("boolean"); + + b.Property("TicketItemId") + .HasColumnType("uuid"); + + b.Property("TicketItemLevel") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EntranceId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("TicketItemId"); + + b.ToTable("MiniGameDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameRankingEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("GameInstanceId") + .HasColumnType("uuid"); + + b.Property("MiniGameId") + .HasColumnType("uuid"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("MiniGameId"); + + b.ToTable("MiniGameRankingEntry", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemRewardId") + .HasColumnType("uuid"); + + b.Property("MiniGameDefinitionId") + .HasColumnType("uuid"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("RequiredKillId") + .HasColumnType("uuid"); + + b.Property("RequiredSuccess") + .HasColumnType("integer"); + + b.Property("RewardAmount") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemRewardId"); + + b.HasIndex("MiniGameDefinitionId"); + + b.HasIndex("RequiredKillId"); + + b.ToTable("MiniGameReward", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameSpawnWave", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("interval"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("MiniGameDefinitionId") + .HasColumnType("uuid"); + + b.Property("StartTime") + .HasColumnType("interval"); + + b.Property("WaveNumber") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("MiniGameDefinitionId"); + + b.ToTable("MiniGameSpawnWave", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameTerrainChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EndX") + .HasColumnType("smallint"); + + b.Property("EndY") + .HasColumnType("smallint"); + + b.Property("IsClientUpdateRequired") + .HasColumnType("boolean"); + + b.Property("MiniGameChangeEventId") + .HasColumnType("uuid"); + + b.Property("SetTerrainAttribute") + .HasColumnType("boolean"); + + b.Property("StartX") + .HasColumnType("smallint"); + + b.Property("StartY") + .HasColumnType("smallint"); + + b.Property("TerrainAttribute") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("MiniGameChangeEventId"); + + b.ToTable("MiniGameTerrainChange", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeDefinitionId") + .HasColumnType("uuid"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("AttributeDefinitionId"); + + b.HasIndex("MonsterDefinitionId"); + + b.ToTable("MonsterAttribute", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttackDelay") + .HasColumnType("interval"); + + b.Property("AttackRange") + .HasColumnType("smallint"); + + b.Property("AttackSkillId") + .HasColumnType("uuid"); + + b.Property("Attribute") + .HasColumnType("smallint"); + + b.Property("Designation") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("IntelligenceTypeName") + .HasColumnType("text"); + + b.Property("MerchantStoreId") + .HasColumnType("uuid"); + + b.Property("MoveDelay") + .HasColumnType("interval"); + + b.Property("MoveRange") + .HasColumnType("smallint"); + + b.Property("NpcWindow") + .HasColumnType("integer"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("NumberOfMaximumItemDrops") + .HasColumnType("integer"); + + b.Property("ObjectKind") + .HasColumnType("integer"); + + b.Property("RespawnDelay") + .HasColumnType("interval"); + + b.Property("ViewRange") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AttackSkillId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MerchantStoreId") + .IsUnique(); + + b.ToTable("MonsterDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinitionDropItemGroup", b => + { + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.HasKey("MonsterDefinitionId", "DropItemGroupId"); + + b.HasIndex("DropItemGroupId"); + + b.ToTable("MonsterDefinitionDropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterSpawnArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("GameMapId") + .HasColumnType("uuid"); + + b.Property("MaximumHealthOverride") + .HasColumnType("integer"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("smallint"); + + b.Property("SpawnTrigger") + .HasColumnType("integer"); + + b.Property("WaveNumber") + .HasColumnType("smallint"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GameMapId"); + + b.HasIndex("MonsterDefinitionId"); + + b.ToTable("MonsterSpawnArea", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PlugInConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CustomConfiguration") + .HasColumnType("text"); + + b.Property("CustomPlugInSource") + .HasColumnType("text"); + + b.Property("ExternalAssemblyName") + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("TypeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("PlugInConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BoostId") + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("MagicEffectDefinitionId") + .HasColumnType("uuid"); + + b.Property("MagicEffectDefinitionId1") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BoostId") + .IsUnique(); + + b.HasIndex("GameMapDefinitionId"); + + b.HasIndex("MagicEffectDefinitionId"); + + b.HasIndex("MagicEffectDefinitionId1"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("PowerUpDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("MaximumValue") + .HasColumnType("real"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.ToTable("PowerUpDefinitionValue", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Group") + .HasColumnType("smallint"); + + b.Property("MaximumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MinimumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("QualifiedCharacterId") + .HasColumnType("uuid"); + + b.Property("QuestGiverId") + .HasColumnType("uuid"); + + b.Property("RefuseNumber") + .HasColumnType("smallint"); + + b.Property("Repeatable") + .HasColumnType("boolean"); + + b.Property("RequiredStartMoney") + .HasColumnType("integer"); + + b.Property("RequiresClientAction") + .HasColumnType("boolean"); + + b.Property("StartingNumber") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("MonsterDefinitionId"); + + b.HasIndex("QualifiedCharacterId"); + + b.HasIndex("QuestGiverId"); + + b.ToTable("QuestDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestItemRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("MinimumNumber") + .HasColumnType("integer"); + + b.Property("QuestDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DropItemGroupId"); + + b.HasIndex("ItemId"); + + b.HasIndex("QuestDefinitionId"); + + b.ToTable("QuestItemRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MinimumNumber") + .HasColumnType("integer"); + + b.Property("MonsterId") + .HasColumnType("uuid"); + + b.Property("QuestDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MonsterId"); + + b.HasIndex("QuestDefinitionId"); + + b.ToTable("QuestMonsterKillRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirementState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterQuestStateId") + .HasColumnType("uuid"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("RequirementId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterQuestStateId"); + + b.HasIndex("RequirementId"); + + b.ToTable("QuestMonsterKillRequirementState", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeRewardId") + .HasColumnType("uuid"); + + b.Property("ItemRewardId") + .HasColumnType("uuid"); + + b.Property("QuestDefinitionId") + .HasColumnType("uuid"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("SkillRewardId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AttributeRewardId"); + + b.HasIndex("ItemRewardId") + .IsUnique(); + + b.HasIndex("QuestDefinitionId"); + + b.HasIndex("SkillRewardId"); + + b.ToTable("QuestReward", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Rectangle", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MaximumSuccessPercent") + .HasColumnType("smallint"); + + b.Property("Money") + .HasColumnType("integer"); + + b.Property("MoneyPerFinalSuccessPercentage") + .HasColumnType("integer"); + + b.Property("MultipleAllowed") + .HasColumnType("boolean"); + + b.Property("NpcPriceDivisor") + .HasColumnType("integer"); + + b.Property("ResultItemExcellentOptionChance") + .HasColumnType("smallint"); + + b.Property("ResultItemLuckOptionChance") + .HasColumnType("smallint"); + + b.Property("ResultItemMaxExcOptionCount") + .HasColumnType("smallint"); + + b.Property("ResultItemSelect") + .HasColumnType("integer"); + + b.Property("ResultItemSkillChance") + .HasColumnType("smallint"); + + b.Property("SuccessPercent") + .HasColumnType("smallint"); + + b.Property("SuccessPercentageAdditionForAncientItem") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForExcellentItem") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForGuardianItem") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForLuck") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForSocketItem") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SimpleCraftingSettings", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AreaSkillSettingsId") + .HasColumnType("uuid"); + + b.Property("AttackDamage") + .HasColumnType("integer"); + + b.Property("DamageType") + .HasColumnType("integer"); + + b.Property("ElementalModifierTargetId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("ImplicitTargetRange") + .HasColumnType("smallint"); + + b.Property("MagicEffectDefId") + .HasColumnType("uuid"); + + b.Property("MasterDefinitionId") + .HasColumnType("uuid"); + + b.Property("MovesTarget") + .HasColumnType("boolean"); + + b.Property("MovesToTarget") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("NumberOfHitsPerAttack") + .HasColumnType("smallint"); + + b.Property("Range") + .HasColumnType("smallint"); + + b.Property("SkillType") + .HasColumnType("integer"); + + b.Property("SkipElementalModifier") + .HasColumnType("boolean"); + + b.Property("Target") + .HasColumnType("integer"); + + b.Property("TargetRestriction") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AreaSkillSettingsId") + .IsUnique(); + + b.HasIndex("ElementalModifierTargetId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MagicEffectDefId"); + + b.HasIndex("MasterDefinitionId") + .IsUnique(); + + b.ToTable("Skill", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillCharacterClass", b => + { + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.HasKey("SkillId", "CharacterClassId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("SkillCharacterClass", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MaximumCompletionTime") + .HasColumnType("interval"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SkillComboDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsFinalStep") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("SkillComboDefinitionId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SkillComboDefinitionId"); + + b.HasIndex("SkillId"); + + b.ToTable("SkillComboStep", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("SkillId"); + + b.ToTable("SkillEntry", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("DefinitionId"); + + b.ToTable("StatAttribute", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttributeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeId") + .HasColumnType("uuid"); + + b.Property("BaseValue") + .HasColumnType("real"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("IncreasableByPlayer") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AttributeId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("StatAttributeDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SystemConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AutoStart") + .HasColumnType("boolean"); + + b.Property("AutoUpdateSchema") + .HasColumnType("boolean"); + + b.Property("IpResolver") + .HasColumnType("integer"); + + b.Property("IpResolverParameter") + .HasColumnType("text"); + + b.Property("NetworkAnalyzerLiveBufferSize") + .HasColumnType("integer"); + + b.Property("NetworkObservationArchivePath") + .HasColumnType("text"); + + b.Property("NetworkObservationMaxSessionSizeMb") + .HasColumnType("integer"); + + b.Property("NetworkObservationMaxTotalSizeMb") + .HasColumnType("integer"); + + b.Property("NetworkObservationRetentionDays") + .HasColumnType("integer"); + + b.Property("ReadConsoleInput") + .HasColumnType("boolean"); + + b.Property("TimeZoneId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SystemConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.WarpInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Costs") + .HasColumnType("integer"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("GateId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("LevelRequirement") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("GateId"); + + b.ToTable("WarpInfo", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawVault") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", "VaultId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawVault"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AccountCharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", "Account") + .WithMany("JoinedUnlockedCharacterClasses") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("CharacterClass"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawCharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId"); + + b.Navigation("RawCharacterClass"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawAttributes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRelationship", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", null) + .WithMany("RawAttributeCombinations") + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawGlobalAttributeCombinations") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawInputAttribute") + .WithMany() + .HasForeignKey("InputAttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawOperandAttribute") + .WithMany() + .HasForeignKey("OperandAttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", null) + .WithMany("RawRelatedValues") + .HasForeignKey("PowerUpDefinitionValueId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", null) + .WithMany("RawAttributeRelationships") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawInputAttribute"); + + b.Navigation("RawOperandAttribute"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttribute") + .WithMany() + .HasForeignKey("AttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawMapRequirements") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany("RawRequirements") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", null) + .WithMany("RawConsumeRequirements") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", null) + .WithMany("RawRequirements") + .HasForeignKey("SkillId1") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", "RawGround") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "GroundId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", "RawLeftGoal") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "LeftGoalId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", "RawRightGoal") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "RightGoalId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawGround"); + + b.Navigation("RawLeftGoal"); + + b.Navigation("RawRightGoal"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "RawMagicEffectDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", "MagicEffectDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawBuffs") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawMagicEffectDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", "RawAttackRespawnArea") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", "AttackRespawnAreaId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawCastleSiegeMapDefinition") + .WithMany() + .HasForeignKey("CastleSiegeMapDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", "RawDefenseRespawnArea") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", "DefenseRespawnAreaId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawLandOfTrialsMapDefinition") + .WithMany() + .HasForeignKey("LandOfTrialsMapDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawRewardItemDefinition") + .WithMany() + .HasForeignKey("RewardItemDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawSignOfLordItemDefinition") + .WithMany() + .HasForeignKey("SignOfLordItemDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("RawAttackRespawnArea"); + + b.Navigation("RawCastleSiegeMapDefinition"); + + b.Navigation("RawDefenseRespawnArea"); + + b.Navigation("RawLandOfTrialsMapDefinition"); + + b.Navigation("RawRewardItemDefinition"); + + b.Navigation("RawSignOfLordItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("OwnerGuildId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuild", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", null) + .WithMany("RawGuilds") + .HasForeignKey("CastleSiegeDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuildRegistration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawNpcDefinitions") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonsterDefinition") + .WithMany() + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("RawMonsterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcState", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", null) + .WithMany("RawNpcStates") + .HasForeignKey("CastleSiegeDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegePendingReward", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeStateScheduleEntry", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStateSchedule") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeUpgradeDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawGateDefenseUpgrades") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawGateLifeUpgrades") + .HasForeignKey("CastleSiegeConfigurationId1") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~1"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStatueDefenseUpgrades") + .HasForeignKey("CastleSiegeConfigurationId2") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~2"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStatueLifeUpgrades") + .HasForeignKey("CastleSiegeConfigurationId3") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~3"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStatueRegenUpgrades") + .HasForeignKey("CastleSiegeConfigurationId4") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~4"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawAttackMachineZones") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawDefenseMachineZones") + .HasForeignKey("CastleSiegeConfigurationId1") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeZoneDefinition_CastleSiegeConfiguration_CastleS~1"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany("RawCharacters") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawCharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawCurrentMap") + .WithMany() + .HasForeignKey("CurrentMapId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawInventory") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "InventoryId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawCharacterClass"); + + b.Navigation("RawCurrentMap"); + + b.Navigation("RawInventory"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", "RawComboDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "ComboDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawCharacterClasses") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawHomeMap") + .WithMany() + .HasForeignKey("HomeMapId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawNextGenerationClass") + .WithMany() + .HasForeignKey("NextGenerationClassId"); + + b.Navigation("RawComboDefinition"); + + b.Navigation("RawHomeMap"); + + b.Navigation("RawNextGenerationClass"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "Character") + .WithMany("JoinedDropItemGroups") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + + b.Navigation("DropItemGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", "RawActiveQuest") + .WithMany() + .HasForeignKey("ActiveQuestId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany("RawQuestStates") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", "RawLastFinishedQuest") + .WithMany() + .HasForeignKey("LastFinishedQuestId"); + + b.Navigation("RawActiveQuest"); + + b.Navigation("RawLastFinishedQuest"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerEndpoint", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", null) + .WithMany("RawEndpoints") + .HasForeignKey("ChatServerDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", "RawClient") + .WithMany() + .HasForeignKey("ClientId"); + + b.Navigation("RawClient"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CombinationBonusRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", null) + .WithMany("RawRequirements") + .HasForeignKey("ItemOptionCombinationBonusId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "RawOptionType") + .WithMany() + .HasForeignKey("OptionTypeId"); + + b.Navigation("RawOptionType"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConnectServerDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", "RawClient") + .WithMany() + .HasForeignKey("ClientId"); + + b.Navigation("RawClient"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConstValueAttribute", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany("RawBaseAttributeValues") + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "GameConfiguration") + .WithMany("RawGlobalBaseAttributeValues") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("CharacterClass"); + + b.Navigation("GameConfiguration"); + + b.Navigation("RawDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawDropItemGroups") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonster") + .WithMany() + .HasForeignKey("MonsterId"); + + b.Navigation("RawMonster"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroupItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany("JoinedPossibleItems") + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DropItemGroup"); + + b.Navigation("ItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelArea", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", null) + .WithMany("RawDuelAreas") + .HasForeignKey("DuelConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawFirstPlayerGate") + .WithMany() + .HasForeignKey("FirstPlayerGateId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawSecondPlayerGate") + .WithMany() + .HasForeignKey("SecondPlayerGateId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawSpectatorsGate") + .WithMany() + .HasForeignKey("SpectatorsGateId"); + + b.Navigation("RawFirstPlayerGate"); + + b.Navigation("RawSecondPlayerGate"); + + b.Navigation("RawSpectatorsGate"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawExit") + .WithMany() + .HasForeignKey("ExitId"); + + b.Navigation("RawExit"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.EnterGate", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawEnterGates") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawTargetGate") + .WithMany() + .HasForeignKey("TargetGateId"); + + b.Navigation("RawTargetGate"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawMap") + .WithMany("RawExitGates") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawMap"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", "RawCastleSiegeConfiguration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", "RawDuelConfiguration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "DuelConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawCastleSiegeConfiguration"); + + b.Navigation("RawDuelConfiguration"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "RawBattleZone") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "BattleZoneId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMaps") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawSafezoneMap") + .WithMany() + .HasForeignKey("SafezoneMapId"); + + b.Navigation("RawBattleZone"); + + b.Navigation("RawSafezoneMap"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinitionDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "GameMapDefinition") + .WithMany("JoinedDropItemGroups") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DropItemGroup"); + + b.Navigation("GameMapDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfigurationGameMapDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "GameMapDefinition") + .WithMany() + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", "GameServerConfiguration") + .WithMany("JoinedMaps") + .HasForeignKey("GameServerConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GameMapDefinition"); + + b.Navigation("GameServerConfiguration"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "RawGameConfiguration") + .WithMany() + .HasForeignKey("GameConfigurationId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", "RawServerConfiguration") + .WithMany() + .HasForeignKey("ServerConfigurationId"); + + b.Navigation("RawGameConfiguration"); + + b.Navigation("RawServerConfiguration"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerEndpoint", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", "RawClient") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", null) + .WithMany("RawEndpoints") + .HasForeignKey("GameServerDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawClient"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", "RawAllianceGuild") + .WithMany() + .HasForeignKey("AllianceGuildId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", "RawHostility") + .WithMany() + .HasForeignKey("HostilityId"); + + b.Navigation("RawAllianceGuild"); + + b.Navigation("RawHostility"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GuildMember", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany("RawMembers") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "Character") + .WithMany() + .HasForeignKey("Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", null) + .WithMany("RawPossibleOptions") + .HasForeignKey("ItemOptionDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "RawOptionType") + .WithMany() + .HasForeignKey("OptionTypeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawPowerUpDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", "PowerUpDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawOptionType"); + + b.Navigation("RawPowerUpDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawItemStorage") + .WithMany("RawItems") + .HasForeignKey("ItemStorageId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawDefinition"); + + b.Navigation("RawItemStorage"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", null) + .WithMany("RawEquippedItems") + .HasForeignKey("AppearanceDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.Navigation("RawDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearanceItemOptionType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", "ItemAppearance") + .WithMany("JoinedVisibleOptions") + .HasForeignKey("ItemAppearanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "ItemOptionType") + .WithMany() + .HasForeignKey("ItemOptionTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemAppearance"); + + b.Navigation("ItemOptionType"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemBasePowerUpDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", "RawBonusPerLevelTable") + .WithMany() + .HasForeignKey("BonusPerLevelTableId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany("RawBasePowerUpAttributes") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawBonusPerLevelTable"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCrafting", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawItemCraftings") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", "RawSimpleCraftingSettings") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCrafting", "SimpleCraftingSettingsId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawSimpleCraftingSettings"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", null) + .WithMany("RawRequiredItems") + .HasForeignKey("SimpleCraftingSettingsId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", "ItemCraftingRequiredItem") + .WithMany("JoinedPossibleItems") + .HasForeignKey("ItemCraftingRequiredItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemCraftingRequiredItem"); + + b.Navigation("ItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemOptionType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", "ItemCraftingRequiredItem") + .WithMany("JoinedRequiredItemOptions") + .HasForeignKey("ItemCraftingRequiredItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "ItemOptionType") + .WithMany() + .HasForeignKey("ItemOptionTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemCraftingRequiredItem"); + + b.Navigation("ItemOptionType"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingResultItem", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", null) + .WithMany("RawResultItems") + .HasForeignKey("SimpleCraftingSettingsId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "RawConsumeEffect") + .WithMany() + .HasForeignKey("ConsumeEffectId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItems") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSlotType", "RawItemSlot") + .WithMany() + .HasForeignKey("ItemSlotId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkill") + .WithMany() + .HasForeignKey("SkillId"); + + b.Navigation("RawConsumeEffect"); + + b.Navigation("RawItemSlot"); + + b.Navigation("RawSkill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionCharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany("JoinedQualifiedCharacters") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CharacterClass"); + + b.Navigation("ItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemOptionDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany("JoinedPossibleItemOptions") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", "ItemOptionDefinition") + .WithMany() + .HasForeignKey("ItemOptionDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemDefinition"); + + b.Navigation("ItemOptionDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemSetGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany("JoinedPossibleItemSetGroups") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", "ItemSetGroup") + .WithMany() + .HasForeignKey("ItemSetGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemDefinition"); + + b.Navigation("ItemSetGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany("RawDropItems") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonster") + .WithMany() + .HasForeignKey("MonsterId"); + + b.Navigation("RawMonster"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroupItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", "ItemDropItemGroup") + .WithMany("JoinedPossibleItems") + .HasForeignKey("ItemDropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemDefinition"); + + b.Navigation("ItemDropItemGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemItemOfItemSet", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", "Item") + .WithMany("JoinedItemSetGroups") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOfItemSet", "ItemOfItemSet") + .WithMany() + .HasForeignKey("ItemOfItemSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemOfItemSet"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemLevelBonusTables") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOfItemSet", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", "RawBonusOption") + .WithMany() + .HasForeignKey("BonusOptionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", "RawItemSetGroup") + .WithMany("RawItems") + .HasForeignKey("ItemSetGroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawBonusOption"); + + b.Navigation("RawItemDefinition"); + + b.Navigation("RawItemSetGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOption", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "RawOptionType") + .WithMany() + .HasForeignKey("OptionTypeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawPowerUpDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOption", "PowerUpDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawOptionType"); + + b.Navigation("RawPowerUpDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawBonus") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", "BonusId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemOptionCombinationBonuses") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawBonus"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemOptions") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionLink", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", null) + .WithMany("RawItemOptions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", "RawItemOption") + .WithMany() + .HasForeignKey("ItemOptionId"); + + b.Navigation("RawItemOption"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionOfLevel", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", null) + .WithMany("RawLevelDependentOptions") + .HasForeignKey("IncreasableItemOptionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawPowerUpDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionOfLevel", "PowerUpDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawPowerUpDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemOptionTypes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemSetGroups") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", "RawOptions") + .WithMany() + .HasForeignKey("OptionsId"); + + b.Navigation("RawOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSlotType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemSlotTypes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.JewelMix", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawJewelMixes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawMixedJewel") + .WithMany() + .HasForeignKey("MixedJewelId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawSingleJewel") + .WithMany() + .HasForeignKey("SingleJewelId"); + + b.Navigation("RawMixedJewel"); + + b.Navigation("RawSingleJewel"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterBody", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterHeader", "RawHeader") + .WithMany() + .HasForeignKey("HeaderId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", "RawSenderAppearance") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterBody", "SenderAppearanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawHeader"); + + b.Navigation("RawSenderAppearance"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterHeader", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "Receiver") + .WithMany("RawLetters") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Receiver"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LevelBonus", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", null) + .WithMany("RawBonusPerLevel") + .HasForeignKey("ItemLevelBonusTableId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawChance") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "ChanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawChancePvp") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "ChancePvpId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawDuration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "DurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawDurationPvp") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "DurationPvpId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMagicEffects") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawChance"); + + b.Navigation("RawChancePvp"); + + b.Navigation("RawDuration"); + + b.Navigation("RawDurationPvp"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawReplacedSkill") + .WithMany() + .HasForeignKey("ReplacedSkillId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillRoot", "RawRoot") + .WithMany() + .HasForeignKey("RootId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawReplacedSkill"); + + b.Navigation("RawRoot"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinitionSkill", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", "MasterSkillDefinition") + .WithMany("JoinedRequiredMasterSkills") + .HasForeignKey("MasterSkillDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "Skill") + .WithMany() + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MasterSkillDefinition"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillRoot", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMasterSkillRoots") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", null) + .WithMany("RawChangeEvents") + .HasForeignKey("MiniGameDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterSpawnArea", "RawSpawnArea") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", "SpawnAreaId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawTargetDefinition") + .WithMany() + .HasForeignKey("TargetDefinitionId"); + + b.Navigation("RawSpawnArea"); + + b.Navigation("RawTargetDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawEntrance") + .WithMany() + .HasForeignKey("EntranceId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMiniGameDefinitions") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawTicketItem") + .WithMany() + .HasForeignKey("TicketItemId"); + + b.Navigation("RawEntrance"); + + b.Navigation("RawTicketItem"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameRankingEntry", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "RawCharacter") + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", "RawMiniGame") + .WithMany() + .HasForeignKey("MiniGameId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawCharacter"); + + b.Navigation("RawMiniGame"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameReward", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "RawItemReward") + .WithMany() + .HasForeignKey("ItemRewardId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", null) + .WithMany("RawRewards") + .HasForeignKey("MiniGameDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawRequiredKill") + .WithMany() + .HasForeignKey("RequiredKillId"); + + b.Navigation("RawItemReward"); + + b.Navigation("RawRequiredKill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameSpawnWave", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", null) + .WithMany("RawSpawnWaves") + .HasForeignKey("MiniGameDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameTerrainChange", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", null) + .WithMany("RawTerrainChanges") + .HasForeignKey("MiniGameChangeEventId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterAttribute", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttributeDefinition") + .WithMany() + .HasForeignKey("AttributeDefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawAttributes") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttributeDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawAttackSkill") + .WithMany() + .HasForeignKey("AttackSkillId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMonsters") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawMerchantStore") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "MerchantStoreId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttackSkill"); + + b.Navigation("RawMerchantStore"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinitionDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "MonsterDefinition") + .WithMany("JoinedDropItemGroups") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DropItemGroup"); + + b.Navigation("MonsterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterSpawnArea", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawGameMap") + .WithMany("RawMonsterSpawns") + .HasForeignKey("GameMapId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonsterDefinition") + .WithMany() + .HasForeignKey("MonsterDefinitionId"); + + b.Navigation("RawGameMap"); + + b.Navigation("RawMonsterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PlugInConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawPlugInConfigurations") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawBoost") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "BoostId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawCharacterPowerUpDefinitions") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", null) + .WithMany("RawPowerUpDefinitions") + .HasForeignKey("MagicEffectDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", null) + .WithMany("RawPowerUpDefinitionsPvp") + .HasForeignKey("MagicEffectDefinitionId1") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_PowerUpDefinition_MagicEffectDefinition_MagicEffectDefinit~1"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawBoost"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawQuests") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawQualifiedCharacter") + .WithMany() + .HasForeignKey("QualifiedCharacterId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawQuestGiver") + .WithMany() + .HasForeignKey("QuestGiverId"); + + b.Navigation("RawQualifiedCharacter"); + + b.Navigation("RawQuestGiver"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestItemRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "RawDropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItem") + .WithMany() + .HasForeignKey("ItemId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", null) + .WithMany("RawRequiredItems") + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawDropItemGroup"); + + b.Navigation("RawItem"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonster") + .WithMany() + .HasForeignKey("MonsterId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", null) + .WithMany("RawRequiredMonsterKills") + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawMonster"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirementState", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", null) + .WithMany("RawRequirementStates") + .HasForeignKey("CharacterQuestStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirement", "RawRequirement") + .WithMany() + .HasForeignKey("RequirementId"); + + b.Navigation("RawRequirement"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestReward", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttributeReward") + .WithMany() + .HasForeignKey("AttributeRewardId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", "RawItemReward") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestReward", "ItemRewardId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", null) + .WithMany("RawRewards") + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkillReward") + .WithMany() + .HasForeignKey("SkillRewardId"); + + b.Navigation("RawAttributeReward"); + + b.Navigation("RawItemReward"); + + b.Navigation("RawSkillReward"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AreaSkillSettings", "RawAreaSkillSettings") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "AreaSkillSettingsId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawElementalModifierTarget") + .WithMany() + .HasForeignKey("ElementalModifierTargetId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawSkills") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "RawMagicEffectDef") + .WithMany() + .HasForeignKey("MagicEffectDefId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", "RawMasterDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "MasterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAreaSkillSettings"); + + b.Navigation("RawElementalModifierTarget"); + + b.Navigation("RawMagicEffectDef"); + + b.Navigation("RawMasterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillCharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "Skill") + .WithMany("JoinedQualifiedCharacters") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CharacterClass"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboStep", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", null) + .WithMany("RawSteps") + .HasForeignKey("SkillComboDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkill") + .WithMany() + .HasForeignKey("SkillId"); + + b.Navigation("RawSkill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillEntry", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany("RawLearnedSkills") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkill") + .WithMany() + .HasForeignKey("SkillId"); + + b.Navigation("RawSkill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttribute", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany("RawAttributes") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany("RawAttributes") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.Navigation("RawDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttributeDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttribute") + .WithMany() + .HasForeignKey("AttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", null) + .WithMany("RawStatAttributes") + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.WarpInfo", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawWarpList") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawGate") + .WithMany() + .HasForeignKey("GateId"); + + b.Navigation("RawGate"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b => + { + b.Navigation("JoinedUnlockedCharacterClasses"); + + b.Navigation("RawAttributes"); + + b.Navigation("RawCharacters"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => + { + b.Navigation("RawEquippedItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b => + { + b.Navigation("RawAttackMachineZones"); + + b.Navigation("RawDefenseMachineZones"); + + b.Navigation("RawGateDefenseUpgrades"); + + b.Navigation("RawGateLifeUpgrades"); + + b.Navigation("RawNpcDefinitions"); + + b.Navigation("RawStateSchedule"); + + b.Navigation("RawStatueDefenseUpgrades"); + + b.Navigation("RawStatueLifeUpgrades"); + + b.Navigation("RawStatueRegenUpgrades"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b => + { + b.Navigation("RawGuilds"); + + b.Navigation("RawNpcStates"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b => + { + b.Navigation("JoinedDropItemGroups"); + + b.Navigation("RawAttributes"); + + b.Navigation("RawLearnedSkills"); + + b.Navigation("RawLetters"); + + b.Navigation("RawQuestStates"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b => + { + b.Navigation("RawAttributeCombinations"); + + b.Navigation("RawBaseAttributeValues"); + + b.Navigation("RawStatAttributes"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b => + { + b.Navigation("RawRequirementStates"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", b => + { + b.Navigation("RawEndpoints"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => + { + b.Navigation("JoinedPossibleItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b => + { + b.Navigation("RawDuelAreas"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b => + { + b.Navigation("RawAttributes"); + + b.Navigation("RawCharacterClasses"); + + b.Navigation("RawDropItemGroups"); + + b.Navigation("RawGlobalAttributeCombinations"); + + b.Navigation("RawGlobalBaseAttributeValues"); + + b.Navigation("RawItemLevelBonusTables"); + + b.Navigation("RawItemOptionCombinationBonuses"); + + b.Navigation("RawItemOptionTypes"); + + b.Navigation("RawItemOptions"); + + b.Navigation("RawItemSetGroups"); + + b.Navigation("RawItemSlotTypes"); + + b.Navigation("RawItems"); + + b.Navigation("RawJewelMixes"); + + b.Navigation("RawMagicEffects"); + + b.Navigation("RawMaps"); + + b.Navigation("RawMasterSkillRoots"); + + b.Navigation("RawMiniGameDefinitions"); + + b.Navigation("RawMonsters"); + + b.Navigation("RawPlugInConfigurations"); + + b.Navigation("RawSkills"); + + b.Navigation("RawWarpList"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b => + { + b.Navigation("JoinedDropItemGroups"); + + b.Navigation("RawCharacterPowerUpDefinitions"); + + b.Navigation("RawEnterGates"); + + b.Navigation("RawExitGates"); + + b.Navigation("RawMapRequirements"); + + b.Navigation("RawMonsterSpawns"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", b => + { + b.Navigation("JoinedMaps"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", b => + { + b.Navigation("RawEndpoints"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", b => + { + b.Navigation("RawMembers"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", b => + { + b.Navigation("RawLevelDependentOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", b => + { + b.Navigation("JoinedItemSetGroups"); + + b.Navigation("RawItemOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => + { + b.Navigation("JoinedVisibleOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", b => + { + b.Navigation("JoinedPossibleItems"); + + b.Navigation("JoinedRequiredItemOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", b => + { + b.Navigation("JoinedPossibleItemOptions"); + + b.Navigation("JoinedPossibleItemSetGroups"); + + b.Navigation("JoinedQualifiedCharacters"); + + b.Navigation("RawBasePowerUpAttributes"); + + b.Navigation("RawDropItems"); + + b.Navigation("RawRequirements"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", b => + { + b.Navigation("JoinedPossibleItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", b => + { + b.Navigation("RawBonusPerLevel"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", b => + { + b.Navigation("RawRequirements"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", b => + { + b.Navigation("RawPossibleOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", b => + { + b.Navigation("RawItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", b => + { + b.Navigation("RawItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", b => + { + b.Navigation("RawPowerUpDefinitions"); + + b.Navigation("RawPowerUpDefinitionsPvp"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", b => + { + b.Navigation("JoinedRequiredMasterSkills"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", b => + { + b.Navigation("RawTerrainChanges"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", b => + { + b.Navigation("RawChangeEvents"); + + b.Navigation("RawRewards"); + + b.Navigation("RawSpawnWaves"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", b => + { + b.Navigation("JoinedDropItemGroups"); + + b.Navigation("RawAttributes"); + + b.Navigation("RawBuffs"); + + b.Navigation("RawItemCraftings"); + + b.Navigation("RawQuests"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", b => + { + b.Navigation("RawRelatedValues"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", b => + { + b.Navigation("RawRequiredItems"); + + b.Navigation("RawRequiredMonsterKills"); + + b.Navigation("RawRewards"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", b => + { + b.Navigation("RawRequiredItems"); + + b.Navigation("RawResultItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", b => + { + b.Navigation("JoinedQualifiedCharacters"); + + b.Navigation("RawAttributeRelationships"); + + b.Navigation("RawConsumeRequirements"); + + b.Navigation("RawRequirements"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", b => + { + b.Navigation("RawSteps"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.cs b/src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.cs new file mode 100644 index 000000000..36b371d98 --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations +{ + /// + public partial class AddCrywolfData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CrywolfData", + schema: "data", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + IsOccupied = table.Column(type: "boolean", nullable: false), + LastBattleEnd = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CrywolfData", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CrywolfData", + schema: "data"); + } + } +} diff --git a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs index da079b142..a798c4b25 100644 --- a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs +++ b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs @@ -1205,6 +1205,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ConstValueAttribute", "config"); }); + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CrywolfData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsOccupied") + .HasColumnType("boolean"); + + b.Property("LastBattleEnd") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("CrywolfData", "data"); + }); + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => { b.Property("Id") diff --git a/src/Persistence/EntityFramework/Model/CrywolfData.Generated.cs b/src/Persistence/EntityFramework/Model/CrywolfData.Generated.cs new file mode 100644 index 000000000..0c8501605 --- /dev/null +++ b/src/Persistence/EntityFramework/Model/CrywolfData.Generated.cs @@ -0,0 +1,47 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +//------------------------------------------------------------------------------ +// +// This source code was auto-generated by a roslyn code generator. +// +//------------------------------------------------------------------------------ + +// ReSharper disable All + +namespace MUnique.OpenMU.Persistence.EntityFramework.Model; + +using System.ComponentModel.DataAnnotations.Schema; +using MUnique.OpenMU.Persistence; + +/// +/// The Entity Framework Core implementation of . +/// +[Table(nameof(CrywolfData), Schema = SchemaNames.AccountData)] +internal partial class CrywolfData : MUnique.OpenMU.DataModel.Entities.CrywolfData, IIdentifiable +{ + + + + + /// + public override bool Equals(object obj) + { + var baseObject = obj as IIdentifiable; + if (baseObject != null) + { + return baseObject.Id == this.Id; + } + + return base.Equals(obj); + } + + /// + public override int GetHashCode() + { + return this.Id.GetHashCode(); + } + + +} diff --git a/src/Persistence/EntityFramework/Model/ExtendedTypeContext.Generated.cs b/src/Persistence/EntityFramework/Model/ExtendedTypeContext.Generated.cs index d2c8a85a7..d379e5b4d 100644 --- a/src/Persistence/EntityFramework/Model/ExtendedTypeContext.Generated.cs +++ b/src/Persistence/EntityFramework/Model/ExtendedTypeContext.Generated.cs @@ -34,6 +34,7 @@ protected override void OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuild modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); + modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); diff --git a/src/Persistence/EntityFramework/Model/ExtendedTypes.Custom.cs b/src/Persistence/EntityFramework/Model/ExtendedTypes.Custom.cs index 77fedfa98..aafcefd27 100644 --- a/src/Persistence/EntityFramework/Model/ExtendedTypes.Custom.cs +++ b/src/Persistence/EntityFramework/Model/ExtendedTypes.Custom.cs @@ -328,3 +328,12 @@ public BasicModel.CastleSiegeData Convert() return this.Adapt(); } } + +internal partial class CrywolfData : IConvertibleTo +{ + public BasicModel.CrywolfData Convert() + { + MapsterConfigurator.EnsureConfigured(); + return this.Adapt(); + } +} diff --git a/src/Persistence/EntityFramework/Model/MapsterConfigurator.Generated.cs b/src/Persistence/EntityFramework/Model/MapsterConfigurator.Generated.cs index b1c4c2c9d..e8c512f39 100644 --- a/src/Persistence/EntityFramework/Model/MapsterConfigurator.Generated.cs +++ b/src/Persistence/EntityFramework/Model/MapsterConfigurator.Generated.cs @@ -78,6 +78,9 @@ public static void EnsureConfigured() Mapster.TypeAdapterConfig.GlobalSettings.NewConfig() .Include(); + Mapster.TypeAdapterConfig.GlobalSettings.NewConfig() + .Include(); + Mapster.TypeAdapterConfig.GlobalSettings.NewConfig() .Include(); diff --git a/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs index 6ed137e74..7b203175b 100644 --- a/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs +++ b/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs @@ -5,6 +5,7 @@ namespace MUnique.OpenMU.Tests; using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.GameLogic.Crywolf; @@ -22,19 +23,40 @@ public class CrywolfContextTest private static readonly Point AltarPosition = new(125, 27); /// - /// Tests that the battle is lost, when no altar is contracted when it starts. + /// Tests that the battle is lost, when no altar is contracted when it starts, and that the occupation is saved in the database. /// [Test] public async Task BattleIsLostWithoutContractedAltarAsync() { - var (context, _, saved) = await CreateContextAsync().ConfigureAwait(false); + var (context, _, gameContext) = await CreateContextAsync().ConfigureAwait(false); await ProceedToAsync(context, CrywolfState.Start).ConfigureAwait(false); Assert.That(context.State, Is.EqualTo(CrywolfState.End)); Assert.That(context.Occupation, Is.EqualTo(CrywolfOccupationState.Occupied)); - Assert.That(saved, Has.Count.EqualTo(1), "The occupation is saved."); - Assert.That(saved[0], Is.EqualTo(CrywolfOccupationState.Occupied)); + using var persistenceContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CrywolfData), false, gameContext.Configuration); + var data = (await persistenceContext.GetAsync().ConfigureAwait(false)).ToList(); + Assert.That(data, Has.Count.EqualTo(1), "The occupation is saved."); + Assert.That(data[0].IsOccupied, Is.True); + Assert.That(data[0].LastBattleEnd, Is.Not.Null); + } + + /// + /// Tests that the saved occupation is loaded, so that it survives a restart of the server. + /// + [Test] + public async Task SavedOccupationIsLoadedAsync() + { + var gameContext = (GameContext)GameContextTestHelper.CreateGameContext(); + using (var persistenceContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CrywolfData), false, gameContext.Configuration)) + { + persistenceContext.CreateNew().IsOccupied = true; + await persistenceContext.SaveChangesAsync().ConfigureAwait(false); + } + + var (context, _, _) = await CreateContextAsync(gameContext).ConfigureAwait(false); + + Assert.That(context.Occupation, Is.EqualTo(CrywolfOccupationState.Occupied)); } /// @@ -107,9 +129,9 @@ private static async ValueTask ProceedToAsync(CrywolfContext context, CrywolfSta } } - private static async ValueTask<(CrywolfContext Context, Player Player, List Saved)> CreateContextAsync() + private static async ValueTask<(CrywolfContext Context, Player Player, GameContext GameContext)> CreateContextAsync(GameContext? gameContext = null) { - var gameContext = (GameContext)GameContextTestHelper.CreateGameContext(); + gameContext ??= (GameContext)GameContextTestHelper.CreateGameContext(); var map = (await gameContext.GetMapAsync(0).ConfigureAwait(false))!; var definition = new CrywolfEventDefinition { @@ -141,12 +163,7 @@ private static async ValueTask ProceedToAsync(CrywolfContext context, CrywolfSta WaveNumber = definition.BalgassWaveNumber, }); - var saved = new List(); - var context = new CrywolfContext(gameContext, definition, d => - { - saved.Add(d.Occupation); - return ValueTask.CompletedTask; - }); + var context = new CrywolfContext(gameContext, definition); await context.InitializeAsync().ConfigureAwait(false); var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); @@ -154,7 +171,7 @@ private static async ValueTask ProceedToAsync(CrywolfContext context, CrywolfSta player.IsAlive = true; player.Position = AltarPosition; await player.CurrentMap!.AddAsync(player).ConfigureAwait(false); - return (context, player, saved); + return (context, player, gameContext); } private static async ValueTask AddNpcAsync(GameMap map, short number, Point position) From a58c0b395bad981b1c179301cc98c9574c437adf Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 19:50:55 -0300 Subject: [PATCH 12/23] Run the crywolf event on one game server and add its benefits and penalties 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 --- src/DataModel/Entities/CrywolfData.cs | 10 + src/GameLogic/Crywolf/CrywolfContext.cs | 179 ++++++++++++++++-- .../Crywolf/CrywolfEventDefinition.cs | 43 +++++ src/GameLogic/Crywolf/CrywolfPlugIn.cs | 166 +++++++++++++++- src/GameLogic/NPC/AttackableNpcBase.cs | 12 ++ .../Craftings/BaseEventTicketCrafting.cs | 9 +- .../Craftings/BloodCastleTicketCrafting.cs | 3 + .../Craftings/DevilSquareTicketCrafting.cs | 2 +- .../Craftings/IllusionTempleTicketCrafting.cs | 3 + .../ChatCommands/CrywolfChatCommandPlugIn.cs | 6 + .../PlugIns/IMonsterItemDropPlugIn.cs | 27 +++ .../Properties/PlayerMessage.Designer.cs | 18 ++ src/GameLogic/Properties/PlayerMessage.resx | 6 + ...20260928223416_AddCrywolfData.Designer.cs} | 5 +- ...ta.cs => 20260928223416_AddCrywolfData.cs} | 1 + .../EntityDataContextModelSnapshot.cs | 3 + .../CrywolfContextTest.cs | 125 +++++++++++- .../MUnique.OpenMU.Tests/CrywolfPlugInTest.cs | 92 +++++++++ 18 files changed, 682 insertions(+), 28 deletions(-) create mode 100644 src/GameLogic/PlugIns/IMonsterItemDropPlugIn.cs rename src/Persistence/EntityFramework/Migrations/{20260928215953_AddCrywolfData.Designer.cs => 20260928223416_AddCrywolfData.Designer.cs} (99%) rename src/Persistence/EntityFramework/Migrations/{20260928215953_AddCrywolfData.cs => 20260928223416_AddCrywolfData.cs} (93%) create mode 100644 tests/MUnique.OpenMU.Tests/CrywolfPlugInTest.cs diff --git a/src/DataModel/Entities/CrywolfData.cs b/src/DataModel/Entities/CrywolfData.cs index e7fea8d38..118b4317e 100644 --- a/src/DataModel/Entities/CrywolfData.cs +++ b/src/DataModel/Entities/CrywolfData.cs @@ -8,6 +8,10 @@ namespace MUnique.OpenMU.DataModel.Entities; /// Persistent state of the crywolf event, stored as a single row, like the original game does it. /// It keeps the result of the last event until the next one. /// +/// +/// It's written by the game server which runs the event, and read by the other game servers, +/// so that the occupation state and its benefits and penalties apply on all of them. +/// [AggregateRoot] public class CrywolfData { @@ -22,6 +26,12 @@ public class CrywolfData /// public bool IsOccupied { get; set; } + /// + /// Gets or sets a value indicating whether the war is running, from the preparation of the fortress + /// until the end of the battle. Meanwhile, neither the benefits nor the penalties apply. + /// + public bool IsWarRunning { get; set; } + /// /// Gets or sets the time (UTC) when the last battle ended, or if there was no battle yet. /// diff --git a/src/GameLogic/Crywolf/CrywolfContext.cs b/src/GameLogic/Crywolf/CrywolfContext.cs index 27d912091..ea278b6ff 100644 --- a/src/GameLogic/Crywolf/CrywolfContext.cs +++ b/src/GameLogic/Crywolf/CrywolfContext.cs @@ -28,11 +28,15 @@ namespace MUnique.OpenMU.GameLogic.Crywolf; /// /// The result of the event () is kept until the next event. Like in the original game, /// it's saved in the database (), so that it survives a restart of the server. +/// Only one game server runs the event (), like the castle siege +/// server of the original game. The other game servers take over its occupation state from the database, so that +/// the benefits and penalties are the same on all of them. /// public sealed class CrywolfContext : IEventStateProvider, IDisposable { private static readonly TimeSpan AltarInfoInterval = TimeSpan.FromSeconds(2); private static readonly TimeSpan BossMonsterInfoInterval = TimeSpan.FromSeconds(5); + private static readonly TimeSpan DataSynchronizationInterval = TimeSpan.FromSeconds(5); private readonly GameContext _gameContext; private readonly ILogger _logger; @@ -42,6 +46,7 @@ public sealed class CrywolfContext : IEventStateProvider, IDisposable private readonly ConcurrentDictionary _removedMonsters = new(); private readonly ConcurrentDictionary _scores = new(); private readonly List _heroes = new(); + private readonly byte? _serverId; private CrywolfEventDefinition _definition; private GameMap? _map; @@ -62,6 +67,10 @@ public sealed class CrywolfContext : IEventStateProvider, IDisposable private bool _isMissingConfigurationLogged; private int _isBalgassDead; private Guid? _dataId; + private bool _hasBattleResult; + private bool _isEventServer = true; + private bool _isEventServerLogged; + private DateTime _lastDataSynchronization = DateTime.MinValue; /// /// Initializes a new instance of the class. @@ -69,9 +78,21 @@ public sealed class CrywolfContext : IEventStateProvider, IDisposable /// The game context. /// The definition of the event. public CrywolfContext(GameContext gameContext, CrywolfEventDefinition definition) + : this(gameContext, definition, (gameContext as IGameServerContext)?.Id) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The game context. + /// The definition of the event. + /// The id of the game server, or if the game context isn't one of a game server. + internal CrywolfContext(GameContext gameContext, CrywolfEventDefinition definition, byte? serverId) { this._gameContext = gameContext; this._definition = definition; + this._serverId = serverId; this._logger = gameContext.LoggerFactory.CreateLogger(); } @@ -102,6 +123,32 @@ public CrywolfContext(GameContext gameContext, CrywolfEventDefinition definition /// public CrywolfEventDefinition Definition => this._definition; + /// + /// Gets a value indicating whether this game server runs the event. Otherwise, it takes over the + /// occupation state of the game server which runs it. + /// + public bool IsEventServer => this._isEventServer; + + /// + /// Gets a value indicating whether the benefits apply, because the fortress is in peace after it has been defended. + /// + public bool AreBenefitsApplied => this._definition.IsBenefitActive && this._hasBattleResult && this.Occupation == CrywolfOccupationState.Peace; + + /// + /// Gets a value indicating whether the penalties apply, because the fortress is occupied. + /// + public bool ArePenaltiesApplied => this._definition.IsPenaltyActive && this.Occupation == CrywolfOccupationState.Occupied; + + /// + /// Gets the multiplier of the maximum health of the monsters. + /// + public float MonsterHealthMultiplier => this.AreBenefitsApplied ? this._definition.MonsterHealthBenefitPercentage / 100f : 1f; + + /// + /// Gets the multiplier of the experience of killed monsters. + /// + public float ExperienceMultiplier => this.ArePenaltiesApplied ? this._definition.ExperiencePenaltyPercentage / 100f : 1f; + /// public bool IsEventRunning => this.State is >= CrywolfState.Notify2 and <= CrywolfState.End; @@ -133,18 +180,32 @@ public TimeSpan RemainingTime public bool IsSpawnWaveActive(byte waveNumber) => false; /// - /// Initializes the context by registering at the map of the event. + /// Initializes the context by loading the persistent state and registering at the map of the event. /// public async ValueTask InitializeAsync() { + this.UpdateEventServer(); + if (await this.LoadDataAsync().ConfigureAwait(false) && this._isEventServer) + { + // The server was stopped during the war. Like in the original game, the event starts + // again at its next start time, and the result of the last battle stays. + await this.SaveDataAsync(false).ConfigureAwait(false); + } + + this._lastDataSynchronization = DateTime.UtcNow; + + // The occupation state applies to game servers without the crywolf map, too. this._map = await this._gameContext.GetMapAsync((ushort)this._definition.MapNumber).ConfigureAwait(false); if (this._map is null) { - this._logger.LogWarning("The map {map} of the crywolf event wasn't found.", this._definition.MapNumber); + if (this._isEventServer) + { + this._logger.LogWarning("The map {map} of the crywolf event wasn't found.", this._definition.MapNumber); + } + return; } - await this.LoadOccupationAsync().ConfigureAwait(false); this._map.ObjectAdded += this.OnObjectAddedToMapAsync; this.InitializeNpcs(); await this.ApplyOccupationAsync().ConfigureAwait(false); @@ -155,16 +216,28 @@ public async ValueTask InitializeAsync() /// public async ValueTask TickAsync() { - if (this._map is null) + if (this.State == CrywolfState.None) { - return; + this.UpdateEventServer(); } - if (this._altars.Count == 0) + if (this._map is not null && this._altars.Count == 0) { this.InitializeNpcs(); } + if (!this._isEventServer) + { + Interlocked.Exchange(ref this._isSkipRequested, 0); + await this.SynchronizeWithEventServerAsync().ConfigureAwait(false); + return; + } + + if (this._map is null) + { + return; + } + if (this._altars.Count != this._definition.AltarNumbers.Count || this._statue is null) { // Without the altars and the statue (e.g. when the data update wasn't applied yet), the event can't run. @@ -268,8 +341,7 @@ public bool KeepsExperienceOnDeath(GameMap? map) /// The additional success rate in percent. public byte GetChaosRateBenefit() { - // The benefits are part of a separate change. - return 0; + return this.AreBenefitsApplied ? this._definition.ChaosRateBenefit : (byte)0; } /// @@ -285,6 +357,11 @@ public async ValueTask ShowCurrentStateAsync(Player player) } await player.InvokeViewPlugInAsync(p => p.ShowStateAsync(this.Occupation, this.State)).ConfigureAwait(false); + if (!this._isEventServer && this.Occupation == CrywolfOccupationState.War) + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfBattleOnOtherServer)).ConfigureAwait(false); + } + if (this.State is CrywolfState.Ready or CrywolfState.Start) { var (shield, altarStates) = this.GetStatueAndAltarInfo(); @@ -558,6 +635,7 @@ private async ValueTask StartWarAsync() { await this.ShowMessageToAllPlayersAsync(nameof(PlayerMessage.CrywolfNotify2)).ConfigureAwait(false); this.Occupation = CrywolfOccupationState.War; + await this.SaveDataAsync(false).ConfigureAwait(false); await this.ApplyOccupationAsync().ConfigureAwait(false); await this.ShowStateToMapPlayersAsync().ConfigureAwait(false); await this.RemoveCommonMonstersAsync().ConfigureAwait(false); @@ -718,7 +796,7 @@ private async ValueTask EndBattleAsync() .Select(entry => new CrywolfHero(entry.Key.Name, entry.Value, entry.Key.SelectedCharacter?.CharacterClass?.Number ?? 0)) .ToList(); await this.ForEachMapPlayerAsync(player => player.InvokeViewPlugInAsync(p => p.ShowHeroListAsync(heroList)).AsTask()).ConfigureAwait(false); - await this.SaveOccupationAsync().ConfigureAwait(false); + await this.SaveDataAsync(true).ConfigureAwait(false); } private async ValueTask EndCycleAsync() @@ -921,34 +999,103 @@ private async ValueTask ApplyOccupationAsync() await this.UpdateEffectsAsync().ConfigureAwait(false); } - private async ValueTask LoadOccupationAsync() + private void UpdateEventServer() + { + var isEventServer = this._serverId is not { } serverId || serverId == this._definition.GameServerId; + if (isEventServer == this._isEventServer && this._isEventServerLogged) + { + return; + } + + this._isEventServer = isEventServer; + this._isEventServerLogged = true; + if (isEventServer) + { + this._logger.LogInformation("This game server runs the crywolf event."); + } + else + { + this._logger.LogInformation("The crywolf event runs on the game server {serverId}. This game server takes over its occupation state.", this._definition.GameServerId); + } + } + + private async ValueTask SynchronizeWithEventServerAsync() + { + var now = DateTime.UtcNow; + if (now - this._lastDataSynchronization < DataSynchronizationInterval) + { + return; + } + + this._lastDataSynchronization = now; + var previousOccupation = this.Occupation; + await this.LoadDataAsync().ConfigureAwait(false); + if (this.Occupation == previousOccupation) + { + return; + } + + await this.ApplyOccupationAsync().ConfigureAwait(false); + await this.ShowStateToMapPlayersAsync().ConfigureAwait(false); + if (this.Occupation == CrywolfOccupationState.War) + { + await this.ShowMessageToAllPlayersAsync(nameof(PlayerMessage.CrywolfBattleOnOtherServer)).ConfigureAwait(false); + } + } + + /// + /// Loads the persistent state of the event. + /// + /// true, if the war is running according to the persistent state; otherwise, false. + private async ValueTask LoadDataAsync() { try { using var context = this._gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CrywolfData), false, this._gameContext.Configuration); - if ((await context.GetAsync().ConfigureAwait(false)).FirstOrDefault() is { } data) + if ((await context.GetAsync().ConfigureAwait(false)).FirstOrDefault() is not { } data) { - this._dataId = data.Id; - this.Occupation = data.IsOccupied ? CrywolfOccupationState.Occupied : CrywolfOccupationState.Peace; + return false; } + + this._dataId = data.Id; + this._hasBattleResult = data.LastBattleEnd.HasValue; + this.Occupation = data.IsWarRunning && !this._isEventServer + ? CrywolfOccupationState.War + : data.IsOccupied ? CrywolfOccupationState.Occupied : CrywolfOccupationState.Peace; + return data.IsWarRunning; } catch (Exception ex) { this._logger.LogError(ex, "Couldn't load the occupation state of the crywolf fortress."); + return false; } } - private async ValueTask SaveOccupationAsync() + /// + /// Saves the persistent state of the event, so that it survives a restart and the other game servers take it over. + /// + /// If set to true, the battle just ended. + private async ValueTask SaveDataAsync(bool isBattleEnd) { try { using var context = this._gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CrywolfData), false, this._gameContext.Configuration); var data = this._dataId is { } id ? await context.GetByIdAsync(id).ConfigureAwait(false) : null; data ??= context.CreateNew(); - data.IsOccupied = this.Occupation != CrywolfOccupationState.Peace; - data.LastBattleEnd = DateTime.UtcNow; + data.IsWarRunning = this.Occupation == CrywolfOccupationState.War; + if (!data.IsWarRunning) + { + data.IsOccupied = this.Occupation == CrywolfOccupationState.Occupied; + } + + if (isBattleEnd) + { + data.LastBattleEnd = DateTime.UtcNow; + } + await context.SaveChangesAsync().ConfigureAwait(false); this._dataId = data.Id; + this._hasBattleResult |= isBattleEnd; } catch (Exception ex) { diff --git a/src/GameLogic/Crywolf/CrywolfEventDefinition.cs b/src/GameLogic/Crywolf/CrywolfEventDefinition.cs index c1344eefb..0a525e138 100644 --- a/src/GameLogic/Crywolf/CrywolfEventDefinition.cs +++ b/src/GameLogic/Crywolf/CrywolfEventDefinition.cs @@ -20,6 +20,16 @@ public class CrywolfEventDefinition /// public short MapNumber { get; set; } = 34; + /// + /// Gets or sets the id of the game server, which runs the event. + /// + /// + /// Like the castle siege server of the original game, only one game server runs the event. The other + /// game servers take over its result from the database, so that the occupation state and its benefits + /// and penalties are the same on all of them. On their crywolf map, the battle doesn't take place. + /// + public byte GameServerId { get; set; } + /// /// Gets or sets the days of the week, on which the event starts. /// @@ -240,6 +250,39 @@ public class CrywolfEventDefinition /// public IList AlwaysVisibleNpcNumbers { get; set; } = new List { 406, 407 }; + /// + /// Gets or sets a value indicating whether the benefits apply on all game servers, while the fortress is + /// in peace after it has been defended: and . + /// + public bool IsBenefitActive { get; set; } = true; + + /// + /// Gets or sets the additional success rate in percent of the chaos machine mixes of the tickets + /// of Devil Square, Blood Castle and Illusion Temple, while the benefits apply. + /// + public byte ChaosRateBenefit { get; set; } = 5; + + /// + /// Gets or sets the maximum health of the monsters in percent of their normal maximum health, while the benefits apply. + /// + public int MonsterHealthBenefitPercentage { get; set; } = 90; + + /// + /// Gets or sets a value indicating whether the penalties apply on all game servers, while the fortress is + /// occupied: and . + /// + public bool IsPenaltyActive { get; set; } + + /// + /// Gets or sets the chance in percent, with which a jewel which is dropped by a monster still drops, while the penalties apply. + /// + public int JewelDropPenaltyPercentage { get; set; } = 70; + + /// + /// Gets or sets the experience of killed monsters in percent of the normal experience, while the penalties apply. + /// + public int ExperiencePenaltyPercentage { get; set; } = 100; + /// /// Determines whether the event starts at the specified local time of the server. /// diff --git a/src/GameLogic/Crywolf/CrywolfPlugIn.cs b/src/GameLogic/Crywolf/CrywolfPlugIn.cs index eb01b8ab8..1200eac93 100644 --- a/src/GameLogic/Crywolf/CrywolfPlugIn.cs +++ b/src/GameLogic/Crywolf/CrywolfPlugIn.cs @@ -5,8 +5,14 @@ namespace MUnique.OpenMU.GameLogic.Crywolf; using System.Collections.Concurrent; +using System.ComponentModel; using System.Runtime.InteropServices; +using System.Threading; +using MUnique.OpenMU.AttributeSystem; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.GameLogic.NPC; using MUnique.OpenMU.GameLogic.PlugIns; +using MUnique.OpenMU.Pathfinding; using MUnique.OpenMU.PlugIns; /// @@ -14,16 +20,47 @@ namespace MUnique.OpenMU.GameLogic.Crywolf; /// /// /// The event starts at the configured times. The result of the event, the occupation state of the fortress, -/// is kept until the next event and saved in the database. +/// is kept until the next event and saved in the database. It results in benefits or penalties on all game servers: +/// +/// While the fortress is in peace after it has been defended, the maximum health of the monsters is reduced, +/// and the chaos machine mixes of the event tickets get an additional success rate. +/// While the fortress is occupied, the jewels drop less often, and the experience of killed monsters is reduced. +/// /// [PlugIn] [Display(Name = nameof(PlugInResources.CrywolfPlugIn_Name), Description = nameof(PlugInResources.CrywolfPlugIn_Description), ResourceType = typeof(PlugInResources))] [Guid("3E8B5C27-9D41-4A6F-B2C8-7F1D6E0A9B34")] -public sealed class CrywolfPlugIn : IFeaturePlugIn, IPeriodicTaskPlugIn, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration, IDisposable +public sealed class CrywolfPlugIn : IFeaturePlugIn, IPeriodicTaskPlugIn, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration, + IObjectAddedToMapPlugIn, IObjectRemovedFromMapPlugIn, IPlayerStateChangedPlugIn, IMonsterItemDropPlugIn, IDisposable { + /// + /// The jewels which drop less often while the penalties apply, like in the original game. + /// + private static readonly HashSet<(byte Group, short Number)> PenaltyJewels = new() + { + (14, 13), // Jewel of Bless + (14, 14), // Jewel of Soul + (14, 16), // Jewel of Life + (14, 22), // Jewel of Creation + (12, 15), // Jewel of Chaos + (14, 31), // Jewel of Guardian + }; + private readonly ConcurrentDictionary _contexts = new(); private readonly ConcurrentDictionary _runningTicks = new(); + /// + /// The multiplier of the maximum health of the monsters. Like the occupation state, it's the same for all game servers. + /// + private readonly SimpleElement _monsterHealthMultiplier = new(1.0f, AggregateType.Multiplicate); + + /// + /// The multiplier of the and of the players. + /// + private readonly SimpleElement _experienceMultiplier = new(1.0f, AggregateType.Multiplicate); + + private int _isUpdatingMultipliers; + /// public CrywolfEventDefinition? Configuration { get; set; } @@ -62,6 +99,7 @@ public async ValueTask ExecuteTaskAsync(GameContext gameContext) } await context.TickAsync().ConfigureAwait(false); + await this.UpdateMultipliersAsync().ConfigureAwait(false); } catch (Exception ex) { @@ -88,6 +126,61 @@ public object CreateDefaultConfig() return new CrywolfEventDefinition(); } + /// + public ValueTask ObjectAddedToMapAsync(GameMap map, ILocateable addedObject) + { + if (addedObject is Monster monster && IsAffectedByHealthBenefit(monster)) + { + monster.Attributes.AddElement(this._monsterHealthMultiplier, Stats.MaximumHealth); + + // The monster got its health before it was added to the map. When it respawns, it stays on the map and keeps the multiplier. + monster.Health = (int)monster.Attributes[Stats.MaximumHealth]; + } + + return ValueTask.CompletedTask; + } + + /// + public ValueTask ObjectRemovedFromMapAsync(GameMap map, ILocateable removedObject) + { + if (removedObject is Monster monster && IsAffectedByHealthBenefit(monster)) + { + monster.Attributes.RemoveElement(this._monsterHealthMultiplier, Stats.MaximumHealth); + } + + return ValueTask.CompletedTask; + } + + /// + public ValueTask PlayerStateChangedAsync(Player player, State previousState, State currentState) + { + if (currentState.IsDisconnectedOrFinished()) + { + player.Attributes?.RemoveElement(this._experienceMultiplier, Stats.ExperienceRate); + player.Attributes?.RemoveElement(this._experienceMultiplier, Stats.MasterExperienceRate); + } + else if (previousState == PlayerState.CharacterSelection && currentState == PlayerState.EnteredWorld) + { + player.Attributes?.AddElement(this._experienceMultiplier, Stats.ExperienceRate); + player.Attributes?.AddElement(this._experienceMultiplier, Stats.MasterExperienceRate); + } + + return ValueTask.CompletedTask; + } + + /// + public void ItemDropping(AttackableNpcBase monster, Player killer, Item item, CancelEventArgs eventArgs) + { + if (item.Definition is { } definition + && PenaltyJewels.Contains((definition.Group, definition.Number)) + && this._contexts.TryGetValue(killer.GameContext, out var context) + && context.ArePenaltiesApplied + && Rand.NextInt(0, 100) >= context.Definition.JewelDropPenaltyPercentage) + { + eventArgs.Cancel = true; + } + } + /// public void Dispose() { @@ -98,4 +191,73 @@ public void Dispose() this._contexts.Clear(); } + + private static bool IsAffectedByHealthBenefit(Monster monster) + { + // Like in the original game, it applies to all monsters, but not to the summons of players. + // Monsters with a fixed maximum health (e.g. the gates of blood castle) keep it. + return monster.Definition.ObjectKind == NpcObjectKind.Monster + && monster.SummonedBy is null + && monster.SpawnArea.MaximumHealthOverride is null; + } + + private async ValueTask UpdateMultipliersAsync() + { + if (Interlocked.Exchange(ref this._isUpdatingMultipliers, 1) != 0) + { + return; + } + + try + { + // The occupation state is the same on all game servers, but the one which runs the event knows its changes first. + var context = this._contexts.Values.FirstOrDefault(c => c.IsEventServer) ?? this._contexts.Values.FirstOrDefault(); + if (context is null) + { + return; + } + + var experienceMultiplier = context.ExperienceMultiplier; + if (Math.Abs(this._experienceMultiplier.Value - experienceMultiplier) > float.Epsilon) + { + this._experienceMultiplier.Value = experienceMultiplier; + } + + var healthMultiplier = context.MonsterHealthMultiplier; + var previousHealthMultiplier = this._monsterHealthMultiplier.Value; + if (Math.Abs(previousHealthMultiplier - healthMultiplier) > float.Epsilon) + { + this._monsterHealthMultiplier.Value = healthMultiplier; + if (healthMultiplier < previousHealthMultiplier) + { + await this.LimitMonsterHealthAsync().ConfigureAwait(false); + } + } + } + finally + { + Volatile.Write(ref this._isUpdatingMultipliers, 0); + } + } + + /// + /// Limits the health of the living monsters to their reduced maximum health. + /// + private async ValueTask LimitMonsterHealthAsync() + { + foreach (var gameContext in this._contexts.Keys) + { + foreach (var map in await gameContext.GetMapsAsync().ConfigureAwait(false)) + { + foreach (var monster in map.GetNpcsInRange(new Point(128, 128), byte.MaxValue).OfType()) + { + var maximumHealth = (int)monster.Attributes[Stats.MaximumHealth]; + if (monster.Health > maximumHealth) + { + monster.Health = maximumHealth; + } + } + } + } + } } diff --git a/src/GameLogic/NPC/AttackableNpcBase.cs b/src/GameLogic/NPC/AttackableNpcBase.cs index 960f69362..35c10c032 100644 --- a/src/GameLogic/NPC/AttackableNpcBase.cs +++ b/src/GameLogic/NPC/AttackableNpcBase.cs @@ -4,6 +4,7 @@ namespace MUnique.OpenMU.GameLogic.NPC; +using System.ComponentModel; using System.Diagnostics; using System.Threading; using MUnique.OpenMU.AttributeSystem; @@ -471,8 +472,19 @@ private async ValueTask DropItemAsync(IReadOnlyList experienceS } var firstItem = !droppedMoney.HasValue; + var plugInPoint = killer.GameContext.PlugInManager.GetPlugInPoint(); foreach (var item in generatedItems) { + if (plugInPoint is not null) + { + var eventArgs = new CancelEventArgs(); + plugInPoint.ItemDropping(this, killer, item, eventArgs); + if (eventArgs.Cancel) + { + continue; + } + } + Point dropCoordinates; if (firstItem) { diff --git a/src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs b/src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs index 33f58934c..79687f41b 100644 --- a/src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs +++ b/src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs @@ -4,6 +4,7 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings; +using MUnique.OpenMU.GameLogic.Crywolf; using MUnique.OpenMU.GameLogic.PlayerActions.Items; using MUnique.OpenMU.GameLogic.Views.NPC; @@ -36,6 +37,11 @@ protected BaseEventTicketCrafting(string resultItemName, string requiredEventIte /// protected virtual CraftingResult IncorrectMixItemsResult => CraftingResult.IncorrectMixItems; + /// + /// Gets the maximum success rate, which also limits the additional success rate of a defended crywolf fortress. + /// + protected virtual byte MaximumSuccessRate => 100; + /// public override CraftingResult? TryGetRequiredItems(Player player, out IList itemLinks, out byte successRate) { @@ -75,7 +81,8 @@ protected BaseEventTicketCrafting(string resultItemName, string requiredEventIte MinimumAmount = 1, })); - successRate = this.GetSuccessRate(item1.Level); + var crywolfBenefit = CrywolfPlugIn.GetContext(player.GameContext)?.GetChaosRateBenefit() ?? 0; + successRate = (byte)Math.Min(this.GetSuccessRate(item1.Level) + crywolfBenefit, this.MaximumSuccessRate); return default; } diff --git a/src/GameLogic/PlayerActions/Craftings/BloodCastleTicketCrafting.cs b/src/GameLogic/PlayerActions/Craftings/BloodCastleTicketCrafting.cs index 47d99018e..355d80041 100644 --- a/src/GameLogic/PlayerActions/Craftings/BloodCastleTicketCrafting.cs +++ b/src/GameLogic/PlayerActions/Craftings/BloodCastleTicketCrafting.cs @@ -22,6 +22,9 @@ public BloodCastleTicketCrafting() /// protected override CraftingResult IncorrectMixItemsResult => CraftingResult.IncorrectBloodCastleItems; + /// + protected override byte MaximumSuccessRate => 80; + /// protected override int GetPrice(int eventLevel) { diff --git a/src/GameLogic/PlayerActions/Craftings/DevilSquareTicketCrafting.cs b/src/GameLogic/PlayerActions/Craftings/DevilSquareTicketCrafting.cs index fe606a3ce..754c39fa2 100644 --- a/src/GameLogic/PlayerActions/Craftings/DevilSquareTicketCrafting.cs +++ b/src/GameLogic/PlayerActions/Craftings/DevilSquareTicketCrafting.cs @@ -35,6 +35,6 @@ protected override int GetPrice(int eventLevel) /// protected override byte GetSuccessRate(int eventLevel) { - return (byte)(eventLevel < 5 ? 80 : 70); // Future to-do: There is a +10% increase if Crywolf event is beaten + return (byte)(eventLevel < 5 ? 80 : 70); } } \ No newline at end of file diff --git a/src/GameLogic/PlayerActions/Craftings/IllusionTempleTicketCrafting.cs b/src/GameLogic/PlayerActions/Craftings/IllusionTempleTicketCrafting.cs index 0451c4f33..368e6b809 100644 --- a/src/GameLogic/PlayerActions/Craftings/IllusionTempleTicketCrafting.cs +++ b/src/GameLogic/PlayerActions/Craftings/IllusionTempleTicketCrafting.cs @@ -19,6 +19,9 @@ public IllusionTempleTicketCrafting() { } + /// + protected override byte MaximumSuccessRate => 80; + /// protected override int GetPrice(int eventLevel) { diff --git a/src/GameLogic/PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs b/src/GameLogic/PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs index a350fc6e1..75355bb99 100644 --- a/src/GameLogic/PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs +++ b/src/GameLogic/PlugIns/ChatCommands/CrywolfChatCommandPlugIn.cs @@ -35,6 +35,12 @@ public async ValueTask HandleCommandAsync(Player player, string command) return; } + if (!context.IsEventServer) + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfRunsOnOtherServer), context.Definition.GameServerId).ConfigureAwait(false); + return; + } + context.SkipWaitingTime(); await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CrywolfProceeds), context.State).ConfigureAwait(false); } diff --git a/src/GameLogic/PlugIns/IMonsterItemDropPlugIn.cs b/src/GameLogic/PlugIns/IMonsterItemDropPlugIn.cs new file mode 100644 index 000000000..db85bd389 --- /dev/null +++ b/src/GameLogic/PlugIns/IMonsterItemDropPlugIn.cs @@ -0,0 +1,27 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlugIns; + +using System.ComponentModel; +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic.NPC; +using MUnique.OpenMU.PlugIns; + +/// +/// A plugin interface which is called when a killed monster is about to drop an item. +/// +[Guid("2E90D8A9-96BD-41FF-82B9-7A9D97CE817E")] +[PlugInPoint("Monster item dropping", "Plugins which are called when a killed monster is about to drop an item. They can cancel the drop.")] +public interface IMonsterItemDropPlugIn +{ + /// + /// Is called when a killed monster is about to drop an item. + /// + /// The monster. + /// The player which killed the monster. + /// The item which is about to drop. + /// The instance containing the event data. Allows to cancel the drop. + void ItemDropping(AttackableNpcBase monster, Player killer, Item item, CancelEventArgs eventArgs); +} diff --git a/src/GameLogic/Properties/PlayerMessage.Designer.cs b/src/GameLogic/Properties/PlayerMessage.Designer.cs index 018980364..6e34af641 100644 --- a/src/GameLogic/Properties/PlayerMessage.Designer.cs +++ b/src/GameLogic/Properties/PlayerMessage.Designer.cs @@ -411,6 +411,24 @@ public static string CrywolfNoAltarContracted { } } + /// + /// Looks up a localized string similar to The battle of the Crywolf Fortress takes place on another server.. + /// + public static string CrywolfBattleOnOtherServer { + get { + return ResourceManager.GetString("CrywolfBattleOnOtherServer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The crywolf event runs on the game server {0}.. + /// + public static string CrywolfRunsOnOtherServer { + get { + return ResourceManager.GetString("CrywolfRunsOnOtherServer", resourceCulture); + } + } + /// /// Looks up a localized string similar to The crywolf event is not active.. /// diff --git a/src/GameLogic/Properties/PlayerMessage.resx b/src/GameLogic/Properties/PlayerMessage.resx index 356247337..850c93cc9 100644 --- a/src/GameLogic/Properties/PlayerMessage.resx +++ b/src/GameLogic/Properties/PlayerMessage.resx @@ -936,6 +936,12 @@ Muses protecting the statue of the holy wolf: {0}, barrier: {1}%. + + The battle of the Crywolf Fortress takes place on another server. + + + The crywolf event runs on the game server {0}. + The crywolf event is not active. diff --git a/src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.Designer.cs b/src/Persistence/EntityFramework/Migrations/20260928223416_AddCrywolfData.Designer.cs similarity index 99% rename from src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.Designer.cs rename to src/Persistence/EntityFramework/Migrations/20260928223416_AddCrywolfData.Designer.cs index 4d72d8ec8..a4874dbdd 100644 --- a/src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.Designer.cs +++ b/src/Persistence/EntityFramework/Migrations/20260928223416_AddCrywolfData.Designer.cs @@ -12,7 +12,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations { [DbContext(typeof(EntityDataContext))] - [Migration("20260928215953_AddCrywolfData")] + [Migration("20260928223416_AddCrywolfData")] partial class AddCrywolfData { /// @@ -1217,6 +1217,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("IsOccupied") .HasColumnType("boolean"); + b.Property("IsWarRunning") + .HasColumnType("boolean"); + b.Property("LastBattleEnd") .HasColumnType("timestamp with time zone"); diff --git a/src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.cs b/src/Persistence/EntityFramework/Migrations/20260928223416_AddCrywolfData.cs similarity index 93% rename from src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.cs rename to src/Persistence/EntityFramework/Migrations/20260928223416_AddCrywolfData.cs index 36b371d98..bf785464e 100644 --- a/src/Persistence/EntityFramework/Migrations/20260928215953_AddCrywolfData.cs +++ b/src/Persistence/EntityFramework/Migrations/20260928223416_AddCrywolfData.cs @@ -18,6 +18,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "uuid", nullable: false), IsOccupied = table.Column(type: "boolean", nullable: false), + IsWarRunning = table.Column(type: "boolean", nullable: false), LastBattleEnd = table.Column(type: "timestamp with time zone", nullable: true) }, constraints: table => diff --git a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs index a798c4b25..ce52f74e4 100644 --- a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs +++ b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs @@ -1214,6 +1214,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsOccupied") .HasColumnType("boolean"); + b.Property("IsWarRunning") + .HasColumnType("boolean"); + b.Property("LastBattleEnd") .HasColumnType("timestamp with time zone"); diff --git a/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs index 7b203175b..33acdc061 100644 --- a/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs +++ b/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs @@ -38,9 +38,25 @@ public async Task BattleIsLostWithoutContractedAltarAsync() var data = (await persistenceContext.GetAsync().ConfigureAwait(false)).ToList(); Assert.That(data, Has.Count.EqualTo(1), "The occupation is saved."); Assert.That(data[0].IsOccupied, Is.True); + Assert.That(data[0].IsWarRunning, Is.False); Assert.That(data[0].LastBattleEnd, Is.Not.Null); } + /// + /// Tests that the start of the war is saved, so that the other game servers take it over. + /// + [Test] + public async Task WarIsSavedForTheOtherServersAsync() + { + var (context, _, gameContext) = await CreateContextAsync().ConfigureAwait(false); + + await ProceedToAsync(context, CrywolfState.Notify2).ConfigureAwait(false); + + var data = await LoadDataAsync(gameContext).ConfigureAwait(false); + Assert.That(data?.IsWarRunning, Is.True); + Assert.That(data?.IsOccupied, Is.False); + } + /// /// Tests that the saved occupation is loaded, so that it survives a restart of the server. /// @@ -48,17 +64,95 @@ public async Task BattleIsLostWithoutContractedAltarAsync() public async Task SavedOccupationIsLoadedAsync() { var gameContext = (GameContext)GameContextTestHelper.CreateGameContext(); - using (var persistenceContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CrywolfData), false, gameContext.Configuration)) - { - persistenceContext.CreateNew().IsOccupied = true; - await persistenceContext.SaveChangesAsync().ConfigureAwait(false); - } + await SaveDataAsync(gameContext, data => data.IsOccupied = true).ConfigureAwait(false); var (context, _, _) = await CreateContextAsync(gameContext).ConfigureAwait(false); Assert.That(context.Occupation, Is.EqualTo(CrywolfOccupationState.Occupied)); } + /// + /// Tests that a war, which was interrupted by a restart of the server, is reset, and the result of the last battle stays. + /// + [Test] + public async Task InterruptedWarIsResetAsync() + { + var gameContext = (GameContext)GameContextTestHelper.CreateGameContext(); + await SaveDataAsync(gameContext, data => data.IsWarRunning = true).ConfigureAwait(false); + + var (context, _, _) = await CreateContextAsync(gameContext).ConfigureAwait(false); + + Assert.That(context.State, Is.EqualTo(CrywolfState.None)); + Assert.That(context.Occupation, Is.EqualTo(CrywolfOccupationState.Peace)); + var data = await LoadDataAsync(gameContext).ConfigureAwait(false); + Assert.That(data?.IsWarRunning, Is.False); + } + + /// + /// Tests that a game server, which doesn't run the event, takes over the war of the game server which runs it, + /// and doesn't start the event by itself. + /// + [Test] + public async Task OtherServerTakesOverTheWarAsync() + { + var gameContext = (GameContext)GameContextTestHelper.CreateGameContext(); + await SaveDataAsync(gameContext, data => data.IsWarRunning = true).ConfigureAwait(false); + + var (context, _, _) = await CreateContextAsync(gameContext, serverId: 1).ConfigureAwait(false); + context.SkipWaitingTime(); + await context.TickAsync().ConfigureAwait(false); + + Assert.That(context.IsEventServer, Is.False); + Assert.That(context.Occupation, Is.EqualTo(CrywolfOccupationState.War)); + Assert.That(context.State, Is.EqualTo(CrywolfState.None)); + Assert.That(context.AreBenefitsApplied, Is.False); + Assert.That(context.ArePenaltiesApplied, Is.False); + } + + /// + /// Tests that the benefits apply after the fortress has been defended, but not before the first battle. + /// + [Test] + public async Task BenefitsApplyAfterTheFortressHasBeenDefendedAsync() + { + var (withoutBattle, _, _) = await CreateContextAsync().ConfigureAwait(false); + Assert.That(withoutBattle.GetChaosRateBenefit(), Is.Zero); + Assert.That(withoutBattle.MonsterHealthMultiplier, Is.EqualTo(1f)); + + var gameContext = (GameContext)GameContextTestHelper.CreateGameContext(); + await SaveDataAsync(gameContext, data => data.LastBattleEnd = DateTime.UtcNow).ConfigureAwait(false); + var (context, _, _) = await CreateContextAsync(gameContext).ConfigureAwait(false); + + Assert.That(context.GetChaosRateBenefit(), Is.EqualTo(5)); + Assert.That(context.MonsterHealthMultiplier, Is.EqualTo(0.9f)); + Assert.That(context.ExperienceMultiplier, Is.EqualTo(1f)); + } + + /// + /// Tests that the penalties apply while the fortress is occupied, when they're activated. + /// + [Test] + public async Task PenaltiesApplyWhileOccupiedAsync() + { + var gameContext = (GameContext)GameContextTestHelper.CreateGameContext(); + await SaveDataAsync(gameContext, data => + { + data.IsOccupied = true; + data.LastBattleEnd = DateTime.UtcNow; + }).ConfigureAwait(false); + + var (context, _, _) = await CreateContextAsync(gameContext, configure: d => + { + d.IsPenaltyActive = true; + d.ExperiencePenaltyPercentage = 80; + }).ConfigureAwait(false); + + Assert.That(context.ArePenaltiesApplied, Is.True); + Assert.That(context.ExperienceMultiplier, Is.EqualTo(0.8f)); + Assert.That(context.GetChaosRateBenefit(), Is.Zero); + Assert.That(context.MonsterHealthMultiplier, Is.EqualTo(1f)); + } + /// /// Tests that an altar can't be contracted while the event isn't running. /// @@ -129,7 +223,23 @@ private static async ValueTask ProceedToAsync(CrywolfContext context, CrywolfSta } } - private static async ValueTask<(CrywolfContext Context, Player Player, GameContext GameContext)> CreateContextAsync(GameContext? gameContext = null) + private static async ValueTask SaveDataAsync(GameContext gameContext, Action change) + { + using var persistenceContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CrywolfData), false, gameContext.Configuration); + change(persistenceContext.CreateNew()); + await persistenceContext.SaveChangesAsync().ConfigureAwait(false); + } + + private static async ValueTask LoadDataAsync(GameContext gameContext) + { + using var persistenceContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CrywolfData), false, gameContext.Configuration); + return (await persistenceContext.GetAsync().ConfigureAwait(false)).SingleOrDefault(); + } + + private static async ValueTask<(CrywolfContext Context, Player Player, GameContext GameContext)> CreateContextAsync( + GameContext? gameContext = null, + byte? serverId = null, + Action? configure = null) { gameContext ??= (GameContext)GameContextTestHelper.CreateGameContext(); var map = (await gameContext.GetMapAsync(0).ConfigureAwait(false))!; @@ -141,6 +251,7 @@ private static async ValueTask ProceedToAsync(CrywolfContext context, CrywolfSta ContractDelay = TimeSpan.Zero, MonsterGroups = new List(), }; + configure?.Invoke(definition); await AddNpcAsync(map, definition.StatueNumber, new Point(121, 31)).ConfigureAwait(false); for (var i = 0; i < definition.AltarNumbers.Count; i++) @@ -163,7 +274,7 @@ private static async ValueTask ProceedToAsync(CrywolfContext context, CrywolfSta WaveNumber = definition.BalgassWaveNumber, }); - var context = new CrywolfContext(gameContext, definition); + var context = new CrywolfContext(gameContext, definition, serverId); await context.InitializeAsync().ConfigureAwait(false); var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); diff --git a/tests/MUnique.OpenMU.Tests/CrywolfPlugInTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfPlugInTest.cs new file mode 100644 index 000000000..7b7af2f67 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/CrywolfPlugInTest.cs @@ -0,0 +1,92 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using System.ComponentModel; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.GameLogic.Crywolf; +using ItemDefinition = MUnique.OpenMU.Persistence.BasicModel.ItemDefinition; + +/// +/// Tests the benefits and penalties of the , which apply on all game servers. +/// +[TestFixture] +public class CrywolfPlugInTest +{ + /// + /// Tests that jewels drop less often while the fortress is occupied, and that other items aren't affected. + /// + [Test] + public async Task JewelsDropLessOftenWhileOccupiedAsync() + { + var (plugIn, gameContext) = await CreatePlugInAsync(isOccupied: true, d => d.JewelDropPenaltyPercentage = 0).ConfigureAwait(false); + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + + Assert.That(IsDropCancelled(plugIn, player, 14, 13), Is.True, "Jewel of Bless"); + Assert.That(IsDropCancelled(plugIn, player, 12, 15), Is.True, "Jewel of Chaos"); + Assert.That(IsDropCancelled(plugIn, player, 14, 15), Is.False, "Zen isn't a jewel"); + } + + /// + /// Tests that jewels drop normally while the fortress is in peace. + /// + [Test] + public async Task JewelsDropNormallyInPeaceAsync() + { + var (plugIn, gameContext) = await CreatePlugInAsync(isOccupied: false, d => d.JewelDropPenaltyPercentage = 0).ConfigureAwait(false); + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + + Assert.That(IsDropCancelled(plugIn, player, 14, 13), Is.False); + } + + /// + /// Tests that the experience rate of the players is reduced while the fortress is occupied. + /// + [Test] + public async Task ExperienceIsReducedWhileOccupiedAsync() + { + var (plugIn, gameContext) = await CreatePlugInAsync(isOccupied: true, d => d.ExperiencePenaltyPercentage = 50, execute: false).ConfigureAwait(false); + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + await plugIn.PlayerStateChangedAsync(player, PlayerState.CharacterSelection, PlayerState.EnteredWorld).ConfigureAwait(false); + var normalRate = player.Attributes![Stats.ExperienceRate]; + + await plugIn.ExecuteTaskAsync(gameContext).ConfigureAwait(false); + + Assert.That(player.Attributes[Stats.ExperienceRate], Is.EqualTo(normalRate * 0.5f)); + } + + private static bool IsDropCancelled(CrywolfPlugIn plugIn, Player killer, byte group, short number) + { + var item = new MUnique.OpenMU.Persistence.BasicModel.Item { Definition = new ItemDefinition { Group = group, Number = number } }; + var eventArgs = new CancelEventArgs(); + plugIn.ItemDropping(null!, killer, item, eventArgs); + return eventArgs.Cancel; + } + + private static async ValueTask<(CrywolfPlugIn PlugIn, GameContext GameContext)> CreatePlugInAsync(bool isOccupied, Action configure, bool execute = true) + { + var gameContext = (GameContext)GameContextTestHelper.CreateGameContext(); + using (var persistenceContext = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CrywolfData), false, gameContext.Configuration)) + { + var data = persistenceContext.CreateNew(); + data.IsOccupied = isOccupied; + data.LastBattleEnd = DateTime.UtcNow; + await persistenceContext.SaveChangesAsync().ConfigureAwait(false); + } + + // The map of the event doesn't exist in the test configuration. The occupation state applies anyway. + var definition = new CrywolfEventDefinition { MapNumber = 999, IsPenaltyActive = true }; + configure(definition); + var plugIn = new CrywolfPlugIn { Configuration = definition }; + if (execute) + { + await plugIn.ExecuteTaskAsync(gameContext).ConfigureAwait(false); + } + + return (plugIn, gameContext); + } +} From cbf4777a499ad93afa228dbff295f9818a9a932a Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 20:54:24 -0300 Subject: [PATCH 13/23] Log which crywolf benefits and penalties apply Co-Authored-By: Claude Opus 5.5 --- src/GameLogic/Crywolf/CrywolfPlugIn.cs | 39 ++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/GameLogic/Crywolf/CrywolfPlugIn.cs b/src/GameLogic/Crywolf/CrywolfPlugIn.cs index 1200eac93..66e2b5010 100644 --- a/src/GameLogic/Crywolf/CrywolfPlugIn.cs +++ b/src/GameLogic/Crywolf/CrywolfPlugIn.cs @@ -61,6 +61,11 @@ public sealed class CrywolfPlugIn : IFeaturePlugIn, IPeriodicTaskPlugIn, ISuppor private int _isUpdatingMultipliers; + /// + /// The benefits and penalties which apply, so that their changes are logged. + /// + private (bool Benefits, bool Penalties)? _appliedEffects; + /// public CrywolfEventDefinition? Configuration { get; set; } @@ -99,7 +104,7 @@ public async ValueTask ExecuteTaskAsync(GameContext gameContext) } await context.TickAsync().ConfigureAwait(false); - await this.UpdateMultipliersAsync().ConfigureAwait(false); + await this.UpdateMultipliersAsync(gameContext).ConfigureAwait(false); } catch (Exception ex) { @@ -201,7 +206,30 @@ private static bool IsAffectedByHealthBenefit(Monster monster) && monster.SpawnArea.MaximumHealthOverride is null; } - private async ValueTask UpdateMultipliersAsync() + private static void LogAppliedEffects(ILogger logger, CrywolfContext context) + { + var definition = context.Definition; + if (context.AreBenefitsApplied) + { + logger.LogInformation( + "The crywolf fortress is in peace. The benefits apply on all game servers: chaos mix success rate +{chaosRate} %, monster health {monsterHealth} %.", + definition.ChaosRateBenefit, + definition.MonsterHealthBenefitPercentage); + } + else if (context.ArePenaltiesApplied) + { + logger.LogInformation( + "The crywolf fortress is occupied. The penalties apply on all game servers: jewel drop chance {jewelDrop} %, experience {experience} %.", + definition.JewelDropPenaltyPercentage, + definition.ExperiencePenaltyPercentage); + } + else + { + logger.LogInformation("Neither the benefits nor the penalties of the crywolf fortress apply. Occupation: {occupation}.", context.Occupation); + } + } + + private async ValueTask UpdateMultipliersAsync(GameContext gameContext) { if (Interlocked.Exchange(ref this._isUpdatingMultipliers, 1) != 0) { @@ -217,6 +245,13 @@ private async ValueTask UpdateMultipliersAsync() return; } + var appliedEffects = (context.AreBenefitsApplied, context.ArePenaltiesApplied); + if (this._appliedEffects != appliedEffects) + { + this._appliedEffects = appliedEffects; + LogAppliedEffects(gameContext.LoggerFactory.CreateLogger(), context); + } + var experienceMultiplier = context.ExperienceMultiplier; if (Math.Abs(this._experienceMultiplier.Value - experienceMultiplier) > float.Epsilon) { From 92158c523f55f60c76d2eac4fd6d20ae451addf2 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:27:48 -0300 Subject: [PATCH 14/23] Give the crywolf chaos mix bonus to the blood castle ticket, like the client shows it Co-Authored-By: Claude Opus 5.5 --- .../PlayerActions/Craftings/BloodCastleTicketCrafting.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/GameLogic/PlayerActions/Craftings/BloodCastleTicketCrafting.cs b/src/GameLogic/PlayerActions/Craftings/BloodCastleTicketCrafting.cs index 355d80041..47d99018e 100644 --- a/src/GameLogic/PlayerActions/Craftings/BloodCastleTicketCrafting.cs +++ b/src/GameLogic/PlayerActions/Craftings/BloodCastleTicketCrafting.cs @@ -22,9 +22,6 @@ public BloodCastleTicketCrafting() /// protected override CraftingResult IncorrectMixItemsResult => CraftingResult.IncorrectBloodCastleItems; - /// - protected override byte MaximumSuccessRate => 80; - /// protected override int GetPrice(int eventLevel) { From be1c385b1573b9a1f17e5dd27e14e23ae658001a Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:43:21 -0300 Subject: [PATCH 15/23] Add the skills of Balgass and his army 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 --- src/GameLogic/Crywolf/CrywolfContext.cs | 13 +- .../Crywolf/CrywolfEventDefinition.cs | 54 +++++ .../Crywolf/CrywolfMonsterIntelligence.cs | 184 +++++++++++++++++- src/GameLogic/Crywolf/CrywolfMonsterSkill.cs | 74 +++++++ .../Crywolf/ICrywolfEventViewPlugIn.cs | 8 + .../World/CrywolfEventViewPlugIn.cs | 6 + .../CrywolfEventDefinitionTest.cs | 42 ++++ 7 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 src/GameLogic/Crywolf/CrywolfMonsterSkill.cs diff --git a/src/GameLogic/Crywolf/CrywolfContext.cs b/src/GameLogic/Crywolf/CrywolfContext.cs index ea278b6ff..9622dbb83 100644 --- a/src/GameLogic/Crywolf/CrywolfContext.cs +++ b/src/GameLogic/Crywolf/CrywolfContext.cs @@ -482,7 +482,13 @@ internal async ValueTask ReviveGroupMembersAsync(byte waveNumber) } group.LastRevive = now; - await this.SpawnMemberAsync(group, spawnArea).ConfigureAwait(false); + if (await this.SpawnMemberAsync(group, spawnArea).ConfigureAwait(false) is { } member + && group.Leader is { IsAlive: true } leader) + { + // Like in the original game, the client shows the revival as a skill of the leader. + var skillNumber = this._definition.ReviveSkillNumber; + await leader.ForEachWorldObserverAsync(p => p.ShowMonsterSkillAsync(leader, member, skillNumber), true).ConfigureAwait(false); + } } /// @@ -1126,7 +1132,7 @@ private async ValueTask SpawnGroupAsync(CrywolfMonsterGroup definition) } } - private async ValueTask SpawnMemberAsync(CrywolfMonsterGroupState group, MonsterSpawnArea spawnArea) + private async ValueTask SpawnMemberAsync(CrywolfMonsterGroupState group, MonsterSpawnArea spawnArea) { var monsterDefinition = spawnArea.MonsterDefinition!; var ballista = this._definition.Ballistas.FirstOrDefault(b => b.X == spawnArea.X1 && b.Y == spawnArea.Y1); @@ -1137,7 +1143,10 @@ private async ValueTask SpawnMemberAsync(CrywolfMonsterGroupState group, Monster if (await this.SpawnMonsterAsync(spawnArea, intelligence).ConfigureAwait(false) is { } monster) { group.SetMonster(spawnArea, monster, role == CrywolfMonsterRole.Leader); + return monster; } + + return null; } private async ValueTask SpawnBalgassAsync() diff --git a/src/GameLogic/Crywolf/CrywolfEventDefinition.cs b/src/GameLogic/Crywolf/CrywolfEventDefinition.cs index 0a525e138..3c1f5cd3b 100644 --- a/src/GameLogic/Crywolf/CrywolfEventDefinition.cs +++ b/src/GameLogic/Crywolf/CrywolfEventDefinition.cs @@ -197,6 +197,60 @@ public class CrywolfEventDefinition /// public byte BalgassGoalY { get; set; } = 36; + /// + /// Gets or sets the interval, in which Balgass acts. Like in the original game, it's shorter than his attack delay. + /// + public TimeSpan BalgassActionInterval { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// Gets or sets the health, below which Balgass tries to escape from his target. When it's 0, he doesn't escape. + /// + public int BalgassEscapeHealth { get; set; } = 10_000; + + /// + /// Gets or sets the chance in percent, with which Balgass escapes instead of attacking, when his health is below . + /// + public int BalgassEscapeChance { get; set; } = 70; + + /// + /// Gets or sets the distance, which Balgass walks away from his target when he escapes. + /// + public int BalgassEscapeDistance { get; set; } = 5; + + /// + /// Gets or sets the chance in percent, with which a monster of the army uses one of its instead of a normal attack. + /// + public int SkillChance { get; set; } = 75; + + /// + /// Gets or sets the number of the skill, which the client shows when a Dark Elf revives a member of its group. + /// + public short ReviveSkillNumber { get; set; } = 6; + + /// + /// Gets or sets the skills of the monsters of the army, like the monster skill units of the original game. + /// + public IList MonsterSkills { get; set; } = new List + { + // Balgass: two skills around him, which push and stun the players, and remove the greater defense of the elves. + new() { MonsterNumber = 349, SkillNumber = 12, Radius = 6, PushChance = 50, StunChance = 50, RemovedMagicEffectNumber = 2, RemoveEffectChance = 50 }, + + // The original game rolls the stun of this skill twice with 50 %. + new() { MonsterNumber = 349, SkillNumber = 13, Radius = 6, PushChance = 50, StunChance = 75 }, + + // Dark Elf: stuns its target. + new() { MonsterNumber = 340, SkillNumber = 8, StunChance = 50 }, + + // Balram: removes the greater defense of its target. + new() { MonsterNumber = 344, SkillNumber = 9, RemovedMagicEffectNumber = 2, RemoveEffectChance = 50 }, + + // Death Spirit: decreases the mana and the ability of its target. + new() { MonsterNumber = 345, SkillNumber = 10, ManaDecreaseChance = 50, AbilityDecreaseChance = 50 }, + + // Soram: pushes the players around it. + new() { MonsterNumber = 341, SkillNumber = 11, Radius = 6, PushChance = 50 }, + }; + /// /// Gets or sets the scores which the players get for killing the monsters of the army. /// diff --git a/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs b/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs index f84f719a2..ad16265e7 100644 --- a/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs +++ b/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs @@ -5,6 +5,7 @@ namespace MUnique.OpenMU.GameLogic.Crywolf; using System.Threading; +using MUnique.OpenMU.AttributeSystem; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.GameLogic.NPC; using MUnique.OpenMU.Pathfinding; @@ -18,8 +19,9 @@ namespace MUnique.OpenMU.GameLogic.Crywolf; /// The Dark Elf which leads a group marches to the goal of the group and revives the dead members of its group. /// The soldiers follow their leader and attack the players around them. /// The ballistas bombard a point of the fortress. -/// Balgass marches to the statue. +/// Balgass marches to the statue. When his health is low, he tries to escape from his target. /// +/// The monsters use their skills () instead of a normal attack with a chance. /// The goals are too far away for the path finder, so the monsters walk along the waypoints of the map. /// public sealed class CrywolfMonsterIntelligence : INpcIntelligence, IDisposable @@ -34,6 +36,11 @@ public sealed class CrywolfMonsterIntelligence : INpcIntelligence, IDisposable /// private const int WaypointDistance = 20; + /// + /// The number of the magic effect of a stun. + /// + private const short StunnedMagicEffectNumber = 61; + private readonly CrywolfContext _context; private readonly CrywolfMonsterGroup? _group; private readonly CrywolfBallista? _ballista; @@ -41,6 +48,7 @@ public sealed class CrywolfMonsterIntelligence : INpcIntelligence, IDisposable private Timer? _timer; private Monster? _monster; private IAttackable? _attacker; + private IReadOnlyList? _skills; private int _isRunning; /// @@ -96,7 +104,10 @@ public void RegisterHit(IAttacker attacker) /// public void Start() { - this._timer ??= new Timer(state => _ = this.SafeTickAsync(), null, this.Monster.Definition.AttackDelay, this.Monster.Definition.AttackDelay); + var interval = this.Role == CrywolfMonsterRole.Balgass && this._context.Definition.BalgassActionInterval > TimeSpan.Zero + ? this._context.Definition.BalgassActionInterval + : this.Monster.Definition.AttackDelay; + this._timer ??= new Timer(state => _ = this.SafeTickAsync(), null, interval, interval); } /// @@ -177,7 +188,7 @@ internal async ValueTask TickAsync() if (await this.SearchTargetAsync(monster).ConfigureAwait(false) is { } target) { - await monster.AttackAsync(target).ConfigureAwait(false); + await this.AttackAsync(monster, target).ConfigureAwait(false); return; } @@ -194,6 +205,89 @@ internal async ValueTask TickAsync() } } + /// + /// Pushes the player away from the monster, as far as the terrain allows it. + /// + /// The monster. + /// The player. + /// The distance. + internal static async ValueTask PushAwayAsync(Monster monster, Player player, int distance) + { + if (distance <= 0 + || player.Attributes is not { } attributes + || attributes[Stats.IsStunned] > 0 + || attributes[Stats.IsFrozen] > 0) + { + return; + } + + var target = GetPointAwayFrom(monster.CurrentMap.Terrain, monster.Position, player.Position, distance, point => true); + if (target != player.Position) + { + await player.MoveAsync(target).ConfigureAwait(false); + } + } + + /// + /// Gets the point, which is up to the specified distance away from the origin, in the direction from the origin to the start. + /// It stops before tiles which can't be walked on or are in a safezone. + /// + /// The terrain. + /// The origin, from which the point is away. + /// The start point. + /// The maximum distance. + /// A function which determines, if a point can be walked on in addition to the terrain. + /// The point. + internal static Point GetPointAwayFrom(GameMapTerrain terrain, Point origin, Point start, int distance, Func canWalkOn) + { + var direction = origin.GetDirectionTo(start); + if (direction == Direction.Undefined) + { + direction = (Direction)Rand.NextInt(1, 9); + } + + var target = start; + for (var i = 0; i < distance; i++) + { + var next = target.CalculateTargetPoint(direction); + if (!terrain.WalkMap[next.X, next.Y] || terrain.SafezoneMap[next.X, next.Y] || !canWalkOn(next)) + { + break; + } + + target = next; + } + + return target; + } + + private static bool IsChance(int percentage) + { + return percentage > 0 && Rand.NextInt(0, 100) < percentage; + } + + private static async ValueTask StunAsync(Player player, TimeSpan duration) + { + if (player.Attributes is not { } attributes + || player.GameContext.Configuration.MagicEffects.FirstOrDefault(m => m.Number == StunnedMagicEffectNumber) is not { } definition + || definition.PowerUpDefinitions.FirstOrDefault(p => p.TargetAttribute == Stats.IsStunned) is not { } powerUpDefinition) + { + return; + } + + var powerUp = attributes.CreateElement(powerUpDefinition); + var effect = new MagicEffect(duration, definition, [new MagicEffect.ElementWithTarget(powerUp, Stats.IsStunned)]); + await player.MagicEffectList.AddEffectAsync(effect).ConfigureAwait(false); + } + + private static void Decrease(Player player, AttributeDefinition attribute, int percentage) + { + if (player.Attributes is { } attributes) + { + attributes[attribute] = attributes[attribute] * (100 - Math.Clamp(percentage, 0, 100)) / 100f; + } + } + private static bool IsValidTarget(Monster monster, IAttackable target) { return target is Player { IsInvisible: false } @@ -203,6 +297,90 @@ private static bool IsValidTarget(Monster monster, IAttackable target) && monster.HasLineOfSightTo(target); } + private async ValueTask AttackAsync(Monster monster, IAttackable target) + { + if (this.Role == CrywolfMonsterRole.Balgass && await this.TryEscapeAsync(monster, target).ConfigureAwait(false)) + { + return; + } + + var definition = this._context.Definition; + this._skills ??= definition.MonsterSkills.Where(skill => skill.MonsterNumber == monster.Definition.Number).ToList(); + if (this._skills.Count == 0 || !IsChance(definition.SkillChance)) + { + await monster.AttackAsync(target).ConfigureAwait(false); + return; + } + + await this.UseSkillAsync(monster, target, this._skills[Rand.NextInt(0, this._skills.Count)]).ConfigureAwait(false); + } + + /// + /// Lets Balgass try to escape from his target, when his health is low, like in the original game. + /// + private async ValueTask TryEscapeAsync(Monster monster, IAttackable target) + { + var definition = this._context.Definition; + if (definition.BalgassEscapeHealth <= 0 + || monster.Health >= definition.BalgassEscapeHealth + || !IsChance(definition.BalgassEscapeChance) + || monster.Attributes[Stats.IsFrozen] > 0) + { + return false; + } + + var escapeTarget = GetPointAwayFrom(monster.CurrentMap.Terrain, target.Position, monster.Position, definition.BalgassEscapeDistance, this.CanWalkOn); + return escapeTarget != monster.Position && await monster.WalkToAsync(escapeTarget).ConfigureAwait(false); + } + + private async ValueTask UseSkillAsync(Monster monster, IAttackable target, CrywolfMonsterSkill skill) + { + await monster.ForEachWorldObserverAsync(p => p.ShowMonsterSkillAsync(monster, target, skill.SkillNumber), true).ConfigureAwait(false); + + IReadOnlyList players = skill.Radius > 0 + ? monster.CurrentMap.GetAttackablesInRange(monster.Position, skill.Radius) + .OfType() + .Where(player => player.IsActive() && !player.IsAtSafezone()) + .ToList() + : target is Player targetPlayer ? [targetPlayer] : []; + foreach (var player in players) + { + await player.AttackByAsync(monster, null, false).ConfigureAwait(false); + if (!player.IsAlive) + { + continue; + } + + // The push comes first, because a stunned player isn't pushed. + if (IsChance(skill.PushChance)) + { + await PushAwayAsync(monster, player, skill.PushDistance).ConfigureAwait(false); + } + + if (IsChance(skill.StunChance)) + { + await StunAsync(player, skill.StunDuration).ConfigureAwait(false); + } + + if (skill.RemovedMagicEffectNumber > 0 + && IsChance(skill.RemoveEffectChance) + && player.MagicEffectList.TryGetEffect(skill.RemovedMagicEffectNumber, out var effect)) + { + await effect.DisposeAsync().ConfigureAwait(false); + } + + if (IsChance(skill.ManaDecreaseChance)) + { + Decrease(player, Stats.CurrentMana, skill.DecreasePercentage); + } + + if (IsChance(skill.AbilityDecreaseChance)) + { + Decrease(player, Stats.CurrentAbility, skill.DecreasePercentage); + } + } + } + private Point? GetGoal(Monster monster) { var definition = this._context.Definition; diff --git a/src/GameLogic/Crywolf/CrywolfMonsterSkill.cs b/src/GameLogic/Crywolf/CrywolfMonsterSkill.cs new file mode 100644 index 000000000..c90582ee7 --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfMonsterSkill.cs @@ -0,0 +1,74 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +/// +/// A skill of a monster of the army of Balgass, like the monster skill units of the original game. +/// +/// +/// Each effect is rolled separately for each hit player. +/// +public class CrywolfMonsterSkill +{ + /// + /// Gets or sets the number of the monster. + /// + public short MonsterNumber { get; set; } + + /// + /// Gets or sets the number of the skill, which the client uses to show its animation. + /// + public short SkillNumber { get; set; } + + /// + /// Gets or sets the radius around the monster, in which the skill hits the players. When it's 0, it only hits the target. + /// + public int Radius { get; set; } + + /// + /// Gets or sets the chance in percent to push the hit players away from the monster. + /// + public int PushChance { get; set; } + + /// + /// Gets or sets the distance, by which the hit players are pushed away. + /// + public int PushDistance { get; set; } = 3; + + /// + /// Gets or sets the chance in percent to stun the hit players. + /// + public int StunChance { get; set; } + + /// + /// Gets or sets the duration of the stun. + /// + public TimeSpan StunDuration { get; set; } = TimeSpan.FromSeconds(3); + + /// + /// Gets or sets the number of the magic effect, which is removed from the hit players, e.g. the greater defense of the elves. + /// + public short RemovedMagicEffectNumber { get; set; } + + /// + /// Gets or sets the chance in percent to remove the . + /// + public int RemoveEffectChance { get; set; } + + /// + /// Gets or sets the chance in percent to decrease the mana of the hit players by . + /// + public int ManaDecreaseChance { get; set; } + + /// + /// Gets or sets the chance in percent to decrease the ability (AG) of the hit players by . + /// + public int AbilityDecreaseChance { get; set; } + + /// + /// Gets or sets the percentage, by which the mana or ability is decreased. + /// + public int DecreasePercentage { get; set; } = 50; +} diff --git a/src/GameLogic/Crywolf/ICrywolfEventViewPlugIn.cs b/src/GameLogic/Crywolf/ICrywolfEventViewPlugIn.cs index 5907eda4b..d5d29e1d0 100644 --- a/src/GameLogic/Crywolf/ICrywolfEventViewPlugIn.cs +++ b/src/GameLogic/Crywolf/ICrywolfEventViewPlugIn.cs @@ -75,6 +75,14 @@ public interface ICrywolfEventViewPlugIn : IViewPlugIn /// The target point. ValueTask ShowBallistaAttackAsync(NonPlayerCharacter ballista, Point target); + /// + /// Shows the animation of a skill of a monster of the army. + /// + /// The monster. + /// The target of the skill. + /// The number of the skill. + ValueTask ShowMonsterSkillAsync(IAttacker monster, IIdentifiable target, short skillNumber); + /// /// Shows or hides an effect of the event at an object, e.g. the state of an altar. /// diff --git a/src/GameServer/RemoteView/World/CrywolfEventViewPlugIn.cs b/src/GameServer/RemoteView/World/CrywolfEventViewPlugIn.cs index 5883fa125..f34a42e5f 100644 --- a/src/GameServer/RemoteView/World/CrywolfEventViewPlugIn.cs +++ b/src/GameServer/RemoteView/World/CrywolfEventViewPlugIn.cs @@ -133,6 +133,12 @@ public async ValueTask ShowBallistaAttackAsync(NonPlayerCharacter ballista, Poin await this._player.Connection.SendCrywolfRegionMonsterAttackAsync((ushort)ballista.Definition.Number, ballista.Position.X, ballista.Position.Y, target.X, target.Y).ConfigureAwait(false); } + /// + public async ValueTask ShowMonsterSkillAsync(IAttacker monster, IIdentifiable target, short skillNumber) + { + await this._player.Connection.SendMonsterSkillAnimationAsync((ushort)skillNumber, monster.GetId(this._player), target.GetId(this._player)).ConfigureAwait(false); + } + /// public async ValueTask ShowEffectAsync(NonPlayerCharacter npc, CrywolfEffect effect, bool isActive) { diff --git a/tests/MUnique.OpenMU.Tests/CrywolfEventDefinitionTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfEventDefinitionTest.cs index 6c2bbd232..065ae76f2 100644 --- a/tests/MUnique.OpenMU.Tests/CrywolfEventDefinitionTest.cs +++ b/tests/MUnique.OpenMU.Tests/CrywolfEventDefinitionTest.cs @@ -4,7 +4,9 @@ namespace MUnique.OpenMU.Tests; +using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.Crywolf; +using MUnique.OpenMU.Pathfinding; /// /// Tests the . @@ -91,4 +93,44 @@ public void WalkTargetsAlongWaypoints() Assert.That(waypoint.EuclideanDistanceTo(start), Is.LessThan(20)); Assert.That(waypoint.EuclideanDistanceTo(goal), Is.LessThan(start.EuclideanDistanceTo(goal))); } + + /// + /// Tests that Balgass and the army have the skills of the original game by default. + /// + [Test] + public void DefaultMonsterSkills() + { + var skills = new CrywolfEventDefinition().MonsterSkills; + + var balgassSkills = skills.Where(skill => skill.MonsterNumber == 349).ToList(); + Assert.That(balgassSkills.Select(skill => skill.SkillNumber), Is.EquivalentTo(new short[] { 12, 13 })); + Assert.That(balgassSkills.Select(skill => skill.Radius), Is.All.EqualTo(6)); + Assert.That(skills.Select(skill => skill.MonsterNumber).Distinct(), Is.EquivalentTo(new short[] { 340, 341, 344, 345, 349 })); + } + + /// + /// Tests that a pushed or escaping object moves away from the origin, but not onto blocked tiles or into a safezone. + /// + [Test] + public void PointAwayFromOriginStopsAtObstacles() + { + var terrain = new GameMapTerrain((byte[]?)null); + var origin = new Point(100, 100); + var start = new Point(101, 100); + var direction = origin.GetDirectionTo(start); + var first = start.CalculateTargetPoint(direction); + var second = first.CalculateTargetPoint(direction); + var third = second.CalculateTargetPoint(direction); + terrain.WalkMap[first.X, first.Y] = true; + terrain.WalkMap[second.X, second.Y] = true; + terrain.WalkMap[third.X, third.Y] = true; + + Assert.That(CrywolfMonsterIntelligence.GetPointAwayFrom(terrain, origin, start, 3, _ => true), Is.EqualTo(third)); + + terrain.WalkMap[third.X, third.Y] = false; + Assert.That(CrywolfMonsterIntelligence.GetPointAwayFrom(terrain, origin, start, 3, _ => true), Is.EqualTo(second)); + + terrain.SafezoneMap[first.X, first.Y] = true; + Assert.That(CrywolfMonsterIntelligence.GetPointAwayFrom(terrain, origin, start, 3, _ => true), Is.EqualTo(start)); + } } From 37062fb5a711766a01ad53855c47c0a1c9a24a5b Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:13:41 -0300 Subject: [PATCH 16/23] Warn when the stun effect for the crywolf monster skills is missing Co-Authored-By: Claude Opus 5.5 --- src/GameLogic/Crywolf/CrywolfContext.cs | 10 ++++++++++ src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs | 10 +++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/GameLogic/Crywolf/CrywolfContext.cs b/src/GameLogic/Crywolf/CrywolfContext.cs index 9622dbb83..f1b602b6c 100644 --- a/src/GameLogic/Crywolf/CrywolfContext.cs +++ b/src/GameLogic/Crywolf/CrywolfContext.cs @@ -206,6 +206,16 @@ public async ValueTask InitializeAsync() return; } + if (this._isEventServer + && this._definition.MonsterSkills.Any(skill => skill.StunChance > 0) + && this._gameContext.Configuration.MagicEffects.All(effect => effect.Number != CrywolfMonsterIntelligence.StunnedMagicEffectNumber)) + { + this._logger.LogWarning( + "The magic effect {effect} of the stun is missing in the configuration, so the monsters of the crywolf event can't stun. It's added by the data update {update}.", + CrywolfMonsterIntelligence.StunnedMagicEffectNumber, + "Fix summoner curse skills"); + } + this._map.ObjectAdded += this.OnObjectAddedToMapAsync; this.InitializeNpcs(); await this.ApplyOccupationAsync().ConfigureAwait(false); diff --git a/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs b/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs index ad16265e7..02a2a98c7 100644 --- a/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs +++ b/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs @@ -26,6 +26,11 @@ namespace MUnique.OpenMU.GameLogic.Crywolf; /// public sealed class CrywolfMonsterIntelligence : INpcIntelligence, IDisposable { + /// + /// The number of the magic effect of a stun. + /// + internal const short StunnedMagicEffectNumber = 61; + /// /// The distance to the goal, below which a monster walks directly to it. /// @@ -36,11 +41,6 @@ public sealed class CrywolfMonsterIntelligence : INpcIntelligence, IDisposable /// private const int WaypointDistance = 20; - /// - /// The number of the magic effect of a stun. - /// - private const short StunnedMagicEffectNumber = 61; - private readonly CrywolfContext _context; private readonly CrywolfMonsterGroup? _group; private readonly CrywolfBallista? _ballista; From 0e1640157930d263507e015d0e5c109806035aa3 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Tue, 29 Sep 2026 02:33:14 -0300 Subject: [PATCH 17/23] Share the push of Selupan with the monsters of the crywolf event 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 --- .../Crywolf/CrywolfMonsterIntelligence.cs | 60 +-------------- src/GameLogic/NPC/MonsterPushExtensions.cs | 75 +++++++++++++++++++ src/GameLogic/Raklion/SelupanIntelligence.cs | 48 +----------- .../CrywolfEventDefinitionTest.cs | 7 +- .../SelupanIntelligenceTest.cs | 4 +- 5 files changed, 85 insertions(+), 109 deletions(-) create mode 100644 src/GameLogic/NPC/MonsterPushExtensions.cs diff --git a/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs b/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs index 02a2a98c7..a65957b1d 100644 --- a/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs +++ b/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs @@ -205,62 +205,6 @@ internal async ValueTask TickAsync() } } - /// - /// Pushes the player away from the monster, as far as the terrain allows it. - /// - /// The monster. - /// The player. - /// The distance. - internal static async ValueTask PushAwayAsync(Monster monster, Player player, int distance) - { - if (distance <= 0 - || player.Attributes is not { } attributes - || attributes[Stats.IsStunned] > 0 - || attributes[Stats.IsFrozen] > 0) - { - return; - } - - var target = GetPointAwayFrom(monster.CurrentMap.Terrain, monster.Position, player.Position, distance, point => true); - if (target != player.Position) - { - await player.MoveAsync(target).ConfigureAwait(false); - } - } - - /// - /// Gets the point, which is up to the specified distance away from the origin, in the direction from the origin to the start. - /// It stops before tiles which can't be walked on or are in a safezone. - /// - /// The terrain. - /// The origin, from which the point is away. - /// The start point. - /// The maximum distance. - /// A function which determines, if a point can be walked on in addition to the terrain. - /// The point. - internal static Point GetPointAwayFrom(GameMapTerrain terrain, Point origin, Point start, int distance, Func canWalkOn) - { - var direction = origin.GetDirectionTo(start); - if (direction == Direction.Undefined) - { - direction = (Direction)Rand.NextInt(1, 9); - } - - var target = start; - for (var i = 0; i < distance; i++) - { - var next = target.CalculateTargetPoint(direction); - if (!terrain.WalkMap[next.X, next.Y] || terrain.SafezoneMap[next.X, next.Y] || !canWalkOn(next)) - { - break; - } - - target = next; - } - - return target; - } - private static bool IsChance(int percentage) { return percentage > 0 && Rand.NextInt(0, 100) < percentage; @@ -329,7 +273,7 @@ private async ValueTask TryEscapeAsync(Monster monster, IAttackable target return false; } - var escapeTarget = GetPointAwayFrom(monster.CurrentMap.Terrain, target.Position, monster.Position, definition.BalgassEscapeDistance, this.CanWalkOn); + var escapeTarget = monster.CurrentMap.Terrain.GetPointAwayFrom(target.Position, monster.Position, definition.BalgassEscapeDistance, this.CanWalkOn); return escapeTarget != monster.Position && await monster.WalkToAsync(escapeTarget).ConfigureAwait(false); } @@ -354,7 +298,7 @@ private async ValueTask UseSkillAsync(Monster monster, IAttackable target, Crywo // The push comes first, because a stunned player isn't pushed. if (IsChance(skill.PushChance)) { - await PushAwayAsync(monster, player, skill.PushDistance).ConfigureAwait(false); + await monster.PushAwayAsync(player, skill.PushDistance).ConfigureAwait(false); } if (IsChance(skill.StunChance)) diff --git a/src/GameLogic/NPC/MonsterPushExtensions.cs b/src/GameLogic/NPC/MonsterPushExtensions.cs new file mode 100644 index 000000000..845859ba1 --- /dev/null +++ b/src/GameLogic/NPC/MonsterPushExtensions.cs @@ -0,0 +1,75 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.NPC; + +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.Pathfinding; + +/// +/// Extensions to push players away from monsters, like the knock-back of the monster skills of the original game. +/// +internal static class MonsterPushExtensions +{ + /// + /// Pushes a player away from the monster, field by field, until the distance is reached or + /// the next field isn't walkable or part of a safezone. + /// Stunned and frozen players aren't pushed. + /// + /// The monster. + /// The player which is pushed. + /// The maximum number of fields the player is pushed. + public static async ValueTask PushAwayAsync(this Monster monster, Player player, int distance) + { + if (distance <= 0 + || !player.IsAlive + || player.Attributes is not { } attributes + || attributes[Stats.IsStunned] > 0 + || attributes[Stats.IsFrozen] > 0) + { + return; + } + + var target = monster.CurrentMap.Terrain.GetPointAwayFrom(monster.Position, player.Position, distance); + if (target != player.Position) + { + await player.MoveAsync(target).ConfigureAwait(false); + } + } + + /// + /// Gets the point which is up to the specified distance away from the origin, in the direction from the origin to the start. + /// It stops before fields which aren't walkable or part of a safezone. + /// + /// The terrain. + /// The origin, from which the point is away. + /// The start point. + /// The maximum number of fields. + /// An optional additional condition for the fields, e.g. the walkable fields of a monster. + /// The point. + public static Point GetPointAwayFrom(this GameMapTerrain terrain, Point origin, Point start, int distance, Func? canWalkOn = null) + { + var direction = origin.GetDirectionTo(start); + if (direction == Direction.Undefined) + { + direction = (Direction)Rand.NextInt(1, 9); + } + + var target = start; + for (var i = 0; i < distance; i++) + { + var next = target.CalculateTargetPoint(direction); + if (!terrain.WalkMap[next.X, next.Y] + || terrain.SafezoneMap[next.X, next.Y] + || canWalkOn?.Invoke(next) == false) + { + break; + } + + target = next; + } + + return target; + } +} diff --git a/src/GameLogic/Raklion/SelupanIntelligence.cs b/src/GameLogic/Raklion/SelupanIntelligence.cs index 45c597705..d21f03055 100644 --- a/src/GameLogic/Raklion/SelupanIntelligence.cs +++ b/src/GameLogic/Raklion/SelupanIntelligence.cs @@ -117,50 +117,6 @@ public void Dispose() this._timer = null; } - /// - /// Pushes a player away from Selupan, field by field, until the distance is reached or - /// the next field isn't walkable or part of a safezone. - /// Stunned and frozen players aren't pushed. - /// - /// The monster of Selupan. - /// The player which is pushed. - /// The maximum number of fields the player is pushed. - internal static async ValueTask PushAwayAsync(Monster monster, Player player, int distance) - { - if (distance <= 0 - || !player.IsAlive - || player.Attributes is not { } attributes - || attributes[Stats.IsStunned] > 0 - || attributes[Stats.IsFrozen] > 0) - { - return; - } - - var direction = monster.Position.GetDirectionTo(player.Position); - if (direction == Direction.Undefined) - { - direction = (Direction)Rand.NextInt(1, 9); - } - - var terrain = monster.CurrentMap.Terrain; - var target = player.Position; - for (var i = 0; i < distance; i++) - { - var next = target.CalculateTargetPoint(direction); - if (!terrain.WalkMap[next.X, next.Y] || terrain.SafezoneMap[next.X, next.Y]) - { - break; - } - - target = next; - } - - if (target != player.Position) - { - await player.MoveAsync(target).ConfigureAwait(false); - } - } - /// /// Executes one step of the intelligence. It's called periodically by a timer. /// @@ -178,7 +134,7 @@ internal async ValueTask TickAsync() foreach (var player in this.GetFallTargets(monster)) { await player.AttackByAsync(monster, this.GetSkillEntry(SelupanSkill.Fall, player), false).ConfigureAwait(false); - await PushAwayAsync(monster, player, this.GetPushDistance(player, this._definition.FallPushDistance)).ConfigureAwait(false); + await monster.PushAwayAsync(player, this.GetPushDistance(player, this._definition.FallPushDistance)).ConfigureAwait(false); } await this.UpdatePatternAsync(monster, 1).ConfigureAwait(false); @@ -375,7 +331,7 @@ private async ValueTask ExecuteSkillAsync(Monster monster, IAttackable target, S await this.ShowSkillAsync(monster, target, skill).ConfigureAwait(false); foreach (var hitPlayer in await this.AttackAreaAsync(monster, target, skill).ConfigureAwait(false)) { - await PushAwayAsync(monster, hitPlayer, this.GetPushDistance(hitPlayer, this._definition.IceStrikePushDistance)).ConfigureAwait(false); + await monster.PushAwayAsync(hitPlayer, this.GetPushDistance(hitPlayer, this._definition.IceStrikePushDistance)).ConfigureAwait(false); } break; diff --git a/tests/MUnique.OpenMU.Tests/CrywolfEventDefinitionTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfEventDefinitionTest.cs index 065ae76f2..b4296bfd5 100644 --- a/tests/MUnique.OpenMU.Tests/CrywolfEventDefinitionTest.cs +++ b/tests/MUnique.OpenMU.Tests/CrywolfEventDefinitionTest.cs @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.Tests; using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.Crywolf; +using MUnique.OpenMU.GameLogic.NPC; using MUnique.OpenMU.Pathfinding; /// @@ -125,12 +126,12 @@ public void PointAwayFromOriginStopsAtObstacles() terrain.WalkMap[second.X, second.Y] = true; terrain.WalkMap[third.X, third.Y] = true; - Assert.That(CrywolfMonsterIntelligence.GetPointAwayFrom(terrain, origin, start, 3, _ => true), Is.EqualTo(third)); + Assert.That(terrain.GetPointAwayFrom(origin, start, 3), Is.EqualTo(third)); terrain.WalkMap[third.X, third.Y] = false; - Assert.That(CrywolfMonsterIntelligence.GetPointAwayFrom(terrain, origin, start, 3, _ => true), Is.EqualTo(second)); + Assert.That(terrain.GetPointAwayFrom(origin, start, 3), Is.EqualTo(second)); terrain.SafezoneMap[first.X, first.Y] = true; - Assert.That(CrywolfMonsterIntelligence.GetPointAwayFrom(terrain, origin, start, 3, _ => true), Is.EqualTo(start)); + Assert.That(terrain.GetPointAwayFrom(origin, start, 3), Is.EqualTo(start)); } } diff --git a/tests/MUnique.OpenMU.Tests/SelupanIntelligenceTest.cs b/tests/MUnique.OpenMU.Tests/SelupanIntelligenceTest.cs index 1bdc32cb8..b864474bc 100644 --- a/tests/MUnique.OpenMU.Tests/SelupanIntelligenceTest.cs +++ b/tests/MUnique.OpenMU.Tests/SelupanIntelligenceTest.cs @@ -57,7 +57,7 @@ public async Task FallPushesPlayerAwayFromSelupanAsync() var (monster, _) = await CreateSelupanAsync(gameContext, new RaklionEventDefinition()).ConfigureAwait(false); var player = await CreatePlayerAsync(gameContext, new Point(102, 100)).ConfigureAwait(false); - await SelupanIntelligence.PushAwayAsync(monster, player, 4).ConfigureAwait(false); + await monster.PushAwayAsync(player, 4).ConfigureAwait(false); Assert.That(player.Position, Is.EqualTo(new Point(106, 100))); } @@ -73,7 +73,7 @@ public async Task PushStopsAtUnwalkableFieldAsync() var player = await CreatePlayerAsync(gameContext, new Point(102, 100)).ConfigureAwait(false); monster.CurrentMap.Terrain.WalkMap[104, 100] = false; - await SelupanIntelligence.PushAwayAsync(monster, player, 4).ConfigureAwait(false); + await monster.PushAwayAsync(player, 4).ConfigureAwait(false); Assert.That(player.Position, Is.EqualTo(new Point(103, 100))); } From 8e397340bd2754185df2796d5e04727479b2725e Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Tue, 29 Sep 2026 03:04:01 -0300 Subject: [PATCH 18/23] Keep the crywolf state of all game servers up to date - 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 --- src/GameLogic/Crywolf/CrywolfContext.cs | 26 ++++++++++++++++--- src/GameLogic/Crywolf/CrywolfPlugIn.cs | 12 ++++++++- .../CrywolfContextTest.cs | 23 ++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/GameLogic/Crywolf/CrywolfContext.cs b/src/GameLogic/Crywolf/CrywolfContext.cs index f1b602b6c..b337dbfb0 100644 --- a/src/GameLogic/Crywolf/CrywolfContext.cs +++ b/src/GameLogic/Crywolf/CrywolfContext.cs @@ -226,9 +226,17 @@ public async ValueTask InitializeAsync() /// public async ValueTask TickAsync() { - if (this.State == CrywolfState.None) + if (this.State == CrywolfState.None && this.UpdateEventServer()) { - this.UpdateEventServer(); + // This game server took over the event, e.g. because the configuration changed. A war of the + // previous game server is reset, like after a restart. + if (await this.LoadDataAsync().ConfigureAwait(false)) + { + await this.SaveDataAsync(false).ConfigureAwait(false); + } + + await this.ApplyOccupationAsync().ConfigureAwait(false); + await this.ShowStateToMapPlayersAsync().ConfigureAwait(false); } if (this._map is not null && this._altars.Count == 0) @@ -240,6 +248,9 @@ public async ValueTask TickAsync() { Interlocked.Exchange(ref this._isSkipRequested, 0); await this.SynchronizeWithEventServerAsync().ConfigureAwait(false); + + // The hidden NPCs have to be shown as hidden to the players which see them for the first time. + await this.UpdateEffectsAsync().ConfigureAwait(false); return; } @@ -1015,14 +1026,19 @@ private async ValueTask ApplyOccupationAsync() await this.UpdateEffectsAsync().ConfigureAwait(false); } - private void UpdateEventServer() + /// + /// Updates whether this game server runs the event, according to the definition. + /// + /// true, if this game server took over the event from another game server; otherwise, false. + private bool UpdateEventServer() { var isEventServer = this._serverId is not { } serverId || serverId == this._definition.GameServerId; if (isEventServer == this._isEventServer && this._isEventServerLogged) { - return; + return false; } + var isTakeOver = isEventServer && !this._isEventServer && this._isEventServerLogged; this._isEventServer = isEventServer; this._isEventServerLogged = true; if (isEventServer) @@ -1033,6 +1049,8 @@ private void UpdateEventServer() { this._logger.LogInformation("The crywolf event runs on the game server {serverId}. This game server takes over its occupation state.", this._definition.GameServerId); } + + return isTakeOver; } private async ValueTask SynchronizeWithEventServerAsync() diff --git a/src/GameLogic/Crywolf/CrywolfPlugIn.cs b/src/GameLogic/Crywolf/CrywolfPlugIn.cs index 66e2b5010..97b774d00 100644 --- a/src/GameLogic/Crywolf/CrywolfPlugIn.cs +++ b/src/GameLogic/Crywolf/CrywolfPlugIn.cs @@ -95,7 +95,17 @@ public async ValueTask ExecuteTaskAsync(GameContext gameContext) if (!this._contexts.TryGetValue(gameContext, out var context)) { context = new CrywolfContext(gameContext, definition); - await context.InitializeAsync().ConfigureAwait(false); + try + { + await context.InitializeAsync().ConfigureAwait(false); + } + catch + { + // It's created again at the next tick, so the handlers of this one must not stay registered. + context.Dispose(); + throw; + } + this._contexts[gameContext] = context; } else diff --git a/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs index 33acdc061..de1ba0ded 100644 --- a/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs +++ b/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs @@ -109,6 +109,29 @@ public async Task OtherServerTakesOverTheWarAsync() Assert.That(context.ArePenaltiesApplied, Is.False); } + /// + /// Tests that a game server, which takes over the event, e.g. after a change of the configuration, + /// resets the war of the previous game server instead of staying in it. + /// + [Test] + public async Task TakeOverResetsTheWarOfTheOtherServerAsync() + { + var gameContext = (GameContext)GameContextTestHelper.CreateGameContext(); + await SaveDataAsync(gameContext, data => data.IsWarRunning = true).ConfigureAwait(false); + var (context, _, _) = await CreateContextAsync(gameContext, serverId: 1).ConfigureAwait(false); + Assert.That(context.Occupation, Is.EqualTo(CrywolfOccupationState.War)); + + var definition = context.Definition; + definition.GameServerId = 1; + context.UpdateDefinition(definition); + await context.TickAsync().ConfigureAwait(false); + + Assert.That(context.IsEventServer, Is.True); + Assert.That(context.Occupation, Is.EqualTo(CrywolfOccupationState.Peace)); + var data = await LoadDataAsync(gameContext).ConfigureAwait(false); + Assert.That(data?.IsWarRunning, Is.False); + } + /// /// Tests that the benefits apply after the fortress has been defended, but not before the first battle. /// From 314b052981fd7e0d15757fa1c7b59cb435fcd206 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Tue, 29 Sep 2026 03:16:54 -0300 Subject: [PATCH 19/23] Address the static code analysis findings of the crywolf event Co-Authored-By: Claude Opus 5.5 --- src/GameLogic/Crywolf/CrywolfContext.cs | 14 +++++--------- src/GameLogic/Crywolf/CrywolfPlugIn.cs | 4 +++- .../Craftings/BaseEventTicketCrafting.cs | 2 +- .../CrywolfChaosRateBenefitRequestHandlerPlugIn.cs | 2 +- tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs | 6 +++--- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/GameLogic/Crywolf/CrywolfContext.cs b/src/GameLogic/Crywolf/CrywolfContext.cs index b337dbfb0..e5aea8459 100644 --- a/src/GameLogic/Crywolf/CrywolfContext.cs +++ b/src/GameLogic/Crywolf/CrywolfContext.cs @@ -149,6 +149,11 @@ internal CrywolfContext(GameContext gameContext, CrywolfEventDefinition definiti /// public float ExperienceMultiplier => this.ArePenaltiesApplied ? this._definition.ExperiencePenaltyPercentage / 100f : 1f; + /// + /// Gets the additional success rate in percent of the chaos machine mixes of the event tickets, which is also shown to the player. + /// + public byte ChaosRateBenefit => this.AreBenefitsApplied ? this._definition.ChaosRateBenefit : (byte)0; + /// public bool IsEventRunning => this.State is >= CrywolfState.Notify2 and <= CrywolfState.End; @@ -356,15 +361,6 @@ public bool KeepsExperienceOnDeath(GameMap? map) return this.IsBattleRunning && map is not null && map == this._map; } - /// - /// Gets the additional success rate of the chaos machine crafting, which is shown to the player. - /// - /// The additional success rate in percent. - public byte GetChaosRateBenefit() - { - return this.AreBenefitsApplied ? this._definition.ChaosRateBenefit : (byte)0; - } - /// /// Shows the current state of the event to a player, which entered the crywolf map. /// diff --git a/src/GameLogic/Crywolf/CrywolfPlugIn.cs b/src/GameLogic/Crywolf/CrywolfPlugIn.cs index 97b774d00..91376f904 100644 --- a/src/GameLogic/Crywolf/CrywolfPlugIn.cs +++ b/src/GameLogic/Crywolf/CrywolfPlugIn.cs @@ -173,8 +173,10 @@ public ValueTask PlayerStateChangedAsync(Player player, State previousState, Sta { player.Attributes?.RemoveElement(this._experienceMultiplier, Stats.ExperienceRate); player.Attributes?.RemoveElement(this._experienceMultiplier, Stats.MasterExperienceRate); + return ValueTask.CompletedTask; } - else if (previousState == PlayerState.CharacterSelection && currentState == PlayerState.EnteredWorld) + + if (previousState == PlayerState.CharacterSelection && currentState == PlayerState.EnteredWorld) { player.Attributes?.AddElement(this._experienceMultiplier, Stats.ExperienceRate); player.Attributes?.AddElement(this._experienceMultiplier, Stats.MasterExperienceRate); diff --git a/src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs b/src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs index 79687f41b..d9eb76448 100644 --- a/src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs +++ b/src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs @@ -81,7 +81,7 @@ protected BaseEventTicketCrafting(string resultItemName, string requiredEventIte MinimumAmount = 1, })); - var crywolfBenefit = CrywolfPlugIn.GetContext(player.GameContext)?.GetChaosRateBenefit() ?? 0; + var crywolfBenefit = CrywolfPlugIn.GetContext(player.GameContext)?.ChaosRateBenefit ?? 0; successRate = (byte)Math.Min(this.GetSuccessRate(item1.Level) + crywolfBenefit, this.MaximumSuccessRate); return default; } diff --git a/src/GameServer/MessageHandler/MiniGames/CrywolfChaosRateBenefitRequestHandlerPlugIn.cs b/src/GameServer/MessageHandler/MiniGames/CrywolfChaosRateBenefitRequestHandlerPlugIn.cs index 31da621c9..dec0fdeea 100644 --- a/src/GameServer/MessageHandler/MiniGames/CrywolfChaosRateBenefitRequestHandlerPlugIn.cs +++ b/src/GameServer/MessageHandler/MiniGames/CrywolfChaosRateBenefitRequestHandlerPlugIn.cs @@ -35,7 +35,7 @@ public async ValueTask HandlePacketAsync(Player player, Memory packet) return; } - var rate = CrywolfPlugIn.GetContext(player.GameContext)?.GetChaosRateBenefit() ?? 0; + var rate = CrywolfPlugIn.GetContext(player.GameContext)?.ChaosRateBenefit ?? 0; await player.InvokeViewPlugInAsync(p => p.ShowChaosRateBenefitAsync(rate)).ConfigureAwait(false); } } diff --git a/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs index de1ba0ded..a99b7212a 100644 --- a/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs +++ b/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs @@ -139,14 +139,14 @@ public async Task TakeOverResetsTheWarOfTheOtherServerAsync() public async Task BenefitsApplyAfterTheFortressHasBeenDefendedAsync() { var (withoutBattle, _, _) = await CreateContextAsync().ConfigureAwait(false); - Assert.That(withoutBattle.GetChaosRateBenefit(), Is.Zero); + Assert.That(withoutBattle.ChaosRateBenefit, Is.Zero); Assert.That(withoutBattle.MonsterHealthMultiplier, Is.EqualTo(1f)); var gameContext = (GameContext)GameContextTestHelper.CreateGameContext(); await SaveDataAsync(gameContext, data => data.LastBattleEnd = DateTime.UtcNow).ConfigureAwait(false); var (context, _, _) = await CreateContextAsync(gameContext).ConfigureAwait(false); - Assert.That(context.GetChaosRateBenefit(), Is.EqualTo(5)); + Assert.That(context.ChaosRateBenefit, Is.EqualTo(5)); Assert.That(context.MonsterHealthMultiplier, Is.EqualTo(0.9f)); Assert.That(context.ExperienceMultiplier, Is.EqualTo(1f)); } @@ -172,7 +172,7 @@ await SaveDataAsync(gameContext, data => Assert.That(context.ArePenaltiesApplied, Is.True); Assert.That(context.ExperienceMultiplier, Is.EqualTo(0.8f)); - Assert.That(context.GetChaosRateBenefit(), Is.Zero); + Assert.That(context.ChaosRateBenefit, Is.Zero); Assert.That(context.MonsterHealthMultiplier, Is.EqualTo(1f)); } From 7cd2928682777f9156caa90d7413d94b1a8c1a7f Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:04:01 -0300 Subject: [PATCH 20/23] Address the review of the crywolf event - 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 --- .../Configuration/GameMapDefinition.cs | 7 + .../Configuration/GameMapTerrainVariant.cs | 39 + src/DataModel/Entities/CrywolfData.cs | 9 + src/GameLogic/Crywolf/CrywolfContext.cs | 42 +- .../Crywolf/CrywolfMonsterIntelligence.cs | 1 + src/GameLogic/Crywolf/CrywolfPlugIn.cs | 113 +- src/GameLogic/Crywolf/CrywolfTerrain.cs | 276 - src/GameLogic/GameMapTerrain.cs | 38 +- .../BasicModel/GameMapDefinition.Generated.cs | 21 + .../GameMapTerrainVariant.Generated.cs | 63 + ...20260929174745_AddCrywolfData.Designer.cs} | 5 +- ...ta.cs => 20260929174745_AddCrywolfData.cs} | 1 + ...4938_AddGameMapTerrainVariants.Designer.cs | 6003 +++++++++++++++++ ...0260929174938_AddGameMapTerrainVariants.cs | 52 + .../EntityDataContextModelSnapshot.cs | 39 + .../Model/ExtendedTypeContext.Generated.cs | 2 + .../Model/GameMapDefinition.Generated.cs | 9 + .../Model/GameMapTerrainVariant.Generated.cs | 65 + .../Model/MapsterConfigurator.Generated.cs | 3 + .../Initialization/TerrainUpdateHelper.cs | 18 + .../Updates/AddCrywolfEventUpdatePlugIn.cs | 5 +- .../Events/CrywolfInitializer.cs | 28 + .../CrywolfDataTest.cs | 21 + .../MUnique.OpenMU.Tests/CrywolfAltarTest.cs | 21 - .../CrywolfContextTest.cs | 40 + .../MUnique.OpenMU.Tests/CrywolfPlugInTest.cs | 37 + .../GameMapTerrainTests.cs | 32 +- 27 files changed, 6662 insertions(+), 328 deletions(-) create mode 100644 src/DataModel/Configuration/GameMapTerrainVariant.cs delete mode 100644 src/GameLogic/Crywolf/CrywolfTerrain.cs create mode 100644 src/Persistence/BasicModel/GameMapTerrainVariant.Generated.cs rename src/Persistence/EntityFramework/Migrations/{20260928223416_AddCrywolfData.Designer.cs => 20260929174745_AddCrywolfData.Designer.cs} (99%) rename src/Persistence/EntityFramework/Migrations/{20260928223416_AddCrywolfData.cs => 20260929174745_AddCrywolfData.cs} (92%) create mode 100644 src/Persistence/EntityFramework/Migrations/20260929174938_AddGameMapTerrainVariants.Designer.cs create mode 100644 src/Persistence/EntityFramework/Migrations/20260929174938_AddGameMapTerrainVariants.cs create mode 100644 src/Persistence/EntityFramework/Model/GameMapTerrainVariant.Generated.cs diff --git a/src/DataModel/Configuration/GameMapDefinition.cs b/src/DataModel/Configuration/GameMapDefinition.cs index fec41ba2f..33baac309 100644 --- a/src/DataModel/Configuration/GameMapDefinition.cs +++ b/src/DataModel/Configuration/GameMapDefinition.cs @@ -44,6 +44,13 @@ public partial class GameMapDefinition /// public byte[]? TerrainData { get; set; } + /// + /// Gets or sets the variants of the terrain, which replace the in certain states + /// of the map, e.g. during an event. + /// + [MemberOfAggregate] + public virtual ICollection TerrainVariants { get; protected set; } = null!; + /// /// Gets or sets the defined monster spawn areas. /// diff --git a/src/DataModel/Configuration/GameMapTerrainVariant.cs b/src/DataModel/Configuration/GameMapTerrainVariant.cs new file mode 100644 index 000000000..a8e2ca1f0 --- /dev/null +++ b/src/DataModel/Configuration/GameMapTerrainVariant.cs @@ -0,0 +1,39 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.DataModel.Configuration; + +using MUnique.OpenMU.Annotations; + +/// +/// Defines an alternative terrain of a map, which replaces its +/// in a certain state, e.g. while the crywolf fortress is occupied. +/// +[Cloneable] +public partial class GameMapTerrainVariant +{ + /// + /// Gets or sets the number, which identifies the variant on its map, e.g. the state in which it applies. + /// + public short Number { get; set; } + + /// + /// Gets or sets the description. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets the terrain data. + /// + /// + /// Content of the *.att file in the original server. + /// + public byte[]? TerrainData { get; set; } + + /// + public override string ToString() + { + return $"{this.Number}: {this.Description}"; + } +} diff --git a/src/DataModel/Entities/CrywolfData.cs b/src/DataModel/Entities/CrywolfData.cs index 118b4317e..6493e8096 100644 --- a/src/DataModel/Entities/CrywolfData.cs +++ b/src/DataModel/Entities/CrywolfData.cs @@ -32,6 +32,15 @@ public class CrywolfData /// public bool IsWarRunning { get; set; } + /// + /// Gets or sets the time (UTC) when the war started, or if it isn't running. + /// + /// + /// The other game servers consider the war as ended after the longest possible duration of the event, + /// in case the game server which runs it stopped during the war. + /// + public DateTime? WarStart { get; set; } + /// /// Gets or sets the time (UTC) when the last battle ended, or if there was no battle yet. /// diff --git a/src/GameLogic/Crywolf/CrywolfContext.cs b/src/GameLogic/Crywolf/CrywolfContext.cs index e5aea8459..4f9bc1ab3 100644 --- a/src/GameLogic/Crywolf/CrywolfContext.cs +++ b/src/GameLogic/Crywolf/CrywolfContext.cs @@ -786,7 +786,8 @@ private async ValueTask EndBattleAsync() Player? contractor; lock (altar) { - contractor = altar.Contractor; + // An elf which is still attempting the contract doesn't get the score and the reward. + contractor = altar.State == CrywolfAltarState.Contracted ? altar.Contractor : null; altar.Reset(); altar.Hide(); } @@ -1011,9 +1012,18 @@ private async ValueTask ApplyOccupationAsync() return; } - // The client loads the terrain of the occupation by itself. The one of the war and the occupation - // don't differ for the server. - CrywolfTerrain.Apply(map.Terrain, this.Occupation == CrywolfOccupationState.Peace); + // Like the client, which loads the terrain of the occupation state by itself, the server uses the terrain + // variant of the map with the number of the occupation state. In peace, it's the normal terrain. + // The crywolf map has no other changes of the terrain at runtime, so it's just replaced. + var terrainVariant = this.Occupation == CrywolfOccupationState.Peace + ? null + : map.Definition.TerrainVariants.FirstOrDefault(variant => variant.Number == (short)this.Occupation); + if (terrainVariant is null && this.Occupation != CrywolfOccupationState.Peace) + { + this._logger.LogWarning("The terrain of the crywolf map for the occupation state {occupation} is missing.", this.Occupation); + } + + map.Terrain.LoadTerrainData(terrainVariant?.TerrainData ?? map.Definition.TerrainData); foreach (var display in this._hiddenNpcs.Values) { display.Effect = this.Occupation == CrywolfOccupationState.Peace ? null : CrywolfEffect.NpcHidden; @@ -1089,7 +1099,7 @@ private async ValueTask LoadDataAsync() this._dataId = data.Id; this._hasBattleResult = data.LastBattleEnd.HasValue; - this.Occupation = data.IsWarRunning && !this._isEventServer + this.Occupation = data.IsWarRunning && !this._isEventServer && !this.IsWarOverdue(data) ? CrywolfOccupationState.War : data.IsOccupied ? CrywolfOccupationState.Occupied : CrywolfOccupationState.Peace; return data.IsWarRunning; @@ -1101,6 +1111,19 @@ private async ValueTask LoadDataAsync() } } + /// + /// Determines whether the war should have ended already, because the longest possible war is over. + /// It happens when the game server which runs the event stopped during the war, or doesn't exist. + /// + /// The persistent state of the event. + /// true, if the war should have ended already; otherwise, false. + private bool IsWarOverdue(CrywolfData data) + { + var definition = this._definition; + var longestWar = definition.Notify2Duration + definition.ReadyDuration + definition.BattleDuration + definition.EndDuration; + return data.WarStart is { } warStart && DateTime.UtcNow - warStart > longestWar; + } + /// /// Saves the persistent state of the event, so that it survives a restart and the other game servers take it over. /// @@ -1110,9 +1133,13 @@ private async ValueTask SaveDataAsync(bool isBattleEnd) try { using var context = this._gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(CrywolfData), false, this._gameContext.Configuration); - var data = this._dataId is { } id ? await context.GetByIdAsync(id).ConfigureAwait(false) : null; + var data = this._dataId is { } id + ? await context.GetByIdAsync(id).ConfigureAwait(false) + : (await context.GetAsync().ConfigureAwait(false)).FirstOrDefault(); data ??= context.CreateNew(); + var isWarStarting = this.Occupation == CrywolfOccupationState.War && !data.IsWarRunning; data.IsWarRunning = this.Occupation == CrywolfOccupationState.War; + data.WarStart = data.IsWarRunning ? (isWarStarting ? DateTime.UtcNow : data.WarStart) : null; if (!data.IsWarRunning) { data.IsOccupied = this.Occupation == CrywolfOccupationState.Occupied; @@ -1207,6 +1234,9 @@ private IEnumerable GetWaveSpawns(byte waveNumber) monster.Initialize(); await map.AddAsync(monster).ConfigureAwait(false); monster.OnSpawn(); + + // The monster only starts its intelligence when the first player observes it, but the army acts without observers. + intelligence.Start(); this._eventMonsters.TryAdd(monster, 0); monster.Died += this.OnEventMonsterDied; return monster; diff --git a/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs b/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs index a65957b1d..d56cc05e4 100644 --- a/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs +++ b/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs @@ -226,6 +226,7 @@ private static async ValueTask StunAsync(Player player, TimeSpan duration) private static void Decrease(Player player, AttributeDefinition attribute, int percentage) { + // The client gets the new value by the change of the attribute (see Player.OnAttributeValueChanged). if (player.Attributes is { } attributes) { attributes[attribute] = attributes[attribute] * (100 - Math.Clamp(percentage, 0, 100)) / 100f; diff --git a/src/GameLogic/Crywolf/CrywolfPlugIn.cs b/src/GameLogic/Crywolf/CrywolfPlugIn.cs index 91376f904..051567c11 100644 --- a/src/GameLogic/Crywolf/CrywolfPlugIn.cs +++ b/src/GameLogic/Crywolf/CrywolfPlugIn.cs @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.GameLogic.Crywolf; using System.Collections.Concurrent; using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using MUnique.OpenMU.AttributeSystem; @@ -59,7 +60,18 @@ public sealed class CrywolfPlugIn : IFeaturePlugIn, IPeriodicTaskPlugIn, ISuppor /// private readonly SimpleElement _experienceMultiplier = new(1.0f, AggregateType.Multiplicate); + /// + /// The attribute systems of the monsters and players, to which the multipliers are attached. + /// + private readonly ConditionalWeakTable _attachedAttributeSystems = new(); + + /// + /// The game contexts, in which the multipliers were attached to the existing monsters and players since the plugin has been activated. + /// + private readonly ConcurrentDictionary _attachedGameContexts = new(); + private int _isUpdatingMultipliers; + private int _isSubscribedToDeactivation; /// /// The benefits and penalties which apply, so that their changes are logged. @@ -114,6 +126,13 @@ public async ValueTask ExecuteTaskAsync(GameContext gameContext) } await context.TickAsync().ConfigureAwait(false); + this.SubscribeToDeactivation(gameContext.PlugInManager); + if (this._attachedGameContexts.TryAdd(gameContext, 0)) + { + // The plugin was just activated, so the monsters and players which exist already didn't get the multipliers. + await this.AttachMultipliersAsync(gameContext).ConfigureAwait(false); + } + await this.UpdateMultipliersAsync(gameContext).ConfigureAwait(false); } catch (Exception ex) @@ -144,10 +163,8 @@ public object CreateDefaultConfig() /// public ValueTask ObjectAddedToMapAsync(GameMap map, ILocateable addedObject) { - if (addedObject is Monster monster && IsAffectedByHealthBenefit(monster)) + if (addedObject is Monster monster && this.TryAttachHealthMultiplier(monster)) { - monster.Attributes.AddElement(this._monsterHealthMultiplier, Stats.MaximumHealth); - // The monster got its health before it was added to the map. When it respawns, it stays on the map and keeps the multiplier. monster.Health = (int)monster.Attributes[Stats.MaximumHealth]; } @@ -158,7 +175,7 @@ public ValueTask ObjectAddedToMapAsync(GameMap map, ILocateable addedObject) /// public ValueTask ObjectRemovedFromMapAsync(GameMap map, ILocateable removedObject) { - if (removedObject is Monster monster && IsAffectedByHealthBenefit(monster)) + if (removedObject is Monster monster && this._attachedAttributeSystems.Remove(monster.Attributes)) { monster.Attributes.RemoveElement(this._monsterHealthMultiplier, Stats.MaximumHealth); } @@ -171,15 +188,18 @@ public ValueTask PlayerStateChangedAsync(Player player, State previousState, Sta { if (currentState.IsDisconnectedOrFinished()) { - player.Attributes?.RemoveElement(this._experienceMultiplier, Stats.ExperienceRate); - player.Attributes?.RemoveElement(this._experienceMultiplier, Stats.MasterExperienceRate); + if (player.Attributes is { } attributes && this._attachedAttributeSystems.Remove(attributes)) + { + attributes.RemoveElement(this._experienceMultiplier, Stats.ExperienceRate); + attributes.RemoveElement(this._experienceMultiplier, Stats.MasterExperienceRate); + } + return ValueTask.CompletedTask; } if (previousState == PlayerState.CharacterSelection && currentState == PlayerState.EnteredWorld) { - player.Attributes?.AddElement(this._experienceMultiplier, Stats.ExperienceRate); - player.Attributes?.AddElement(this._experienceMultiplier, Stats.MasterExperienceRate); + this.TryAttachExperienceMultiplier(player); } return ValueTask.CompletedTask; @@ -201,6 +221,11 @@ public void ItemDropping(AttackableNpcBase monster, Player killer, Item item, Ca /// public void Dispose() { + if (this._contexts.Keys.FirstOrDefault() is { } gameContext) + { + gameContext.PlugInManager.PlugInDeactivated -= this.OnPlugInDeactivated; + } + foreach (var context in this._contexts.Values) { context.Dispose(); @@ -241,6 +266,78 @@ private static void LogAppliedEffects(ILogger logger, CrywolfContext context) } } + private bool TryAttachHealthMultiplier(Monster monster) + { + if (!IsAffectedByHealthBenefit(monster) || this._attachedAttributeSystems.TryGetValue(monster.Attributes, out _)) + { + return false; + } + + this._attachedAttributeSystems.AddOrUpdate(monster.Attributes, this); + monster.Attributes.AddElement(this._monsterHealthMultiplier, Stats.MaximumHealth); + return true; + } + + private void TryAttachExperienceMultiplier(Player player) + { + if (player.Attributes is not { } attributes || this._attachedAttributeSystems.TryGetValue(attributes, out _)) + { + return; + } + + this._attachedAttributeSystems.AddOrUpdate(attributes, this); + attributes.AddElement(this._experienceMultiplier, Stats.ExperienceRate); + attributes.AddElement(this._experienceMultiplier, Stats.MasterExperienceRate); + } + + /// + /// Attaches the multipliers to the monsters and players of the game context, which exist already. + /// + private async ValueTask AttachMultipliersAsync(IGameContext gameContext) + { + foreach (var player in await gameContext.GetPlayersAsync().ConfigureAwait(false)) + { + if (player.PlayerState.CurrentState == PlayerState.EnteredWorld) + { + this.TryAttachExperienceMultiplier(player); + } + } + + foreach (var map in await gameContext.GetMapsAsync().ConfigureAwait(false)) + { + foreach (var monster in map.GetNpcsInRange(new Point(128, 128), byte.MaxValue).OfType()) + { + this.TryAttachHealthMultiplier(monster); + } + } + } + + /// + /// Subscribes to the deactivation of this plugin, so that its effects can be undone. + /// + private void SubscribeToDeactivation(PlugInManager plugInManager) + { + if (Interlocked.Exchange(ref this._isSubscribedToDeactivation, 1) == 0) + { + plugInManager.PlugInDeactivated += this.OnPlugInDeactivated; + } + } + + private void OnPlugInDeactivated(object? sender, PlugInEventArgs e) + { + if (e.PlugInType != this.GetType()) + { + return; + } + + // The multipliers stay attached, but without an effect. When the plugin is activated again, they're + // attached to the monsters and players which came in the meantime, and get their values again. + this._monsterHealthMultiplier.Value = 1; + this._experienceMultiplier.Value = 1; + this._appliedEffects = null; + this._attachedGameContexts.Clear(); + } + private async ValueTask UpdateMultipliersAsync(GameContext gameContext) { if (Interlocked.Exchange(ref this._isUpdatingMultipliers, 1) != 0) diff --git a/src/GameLogic/Crywolf/CrywolfTerrain.cs b/src/GameLogic/Crywolf/CrywolfTerrain.cs deleted file mode 100644 index c2daed1cd..000000000 --- a/src/GameLogic/Crywolf/CrywolfTerrain.cs +++ /dev/null @@ -1,276 +0,0 @@ -// -// Licensed under the MIT License. See LICENSE file in the project root for full license information. -// - -namespace MUnique.OpenMU.GameLogic.Crywolf; - -using MUnique.OpenMU.DataModel.Configuration; - -/// -/// The changes of the terrain of the crywolf map, while the fortress isn't in peace. -/// -/// -/// The game client loads a terrain file for each occupation state by itself. The terrain files of -/// the war and the occupation only differ from the one of the peace in these areas: the safezone -/// of the fortress is removed, and some areas are blocked. They were taken from the terrain files. -/// -internal static class CrywolfTerrain -{ - /// - /// Gets the areas, which aren't a safezone while the fortress isn't in peace. - /// - public static IReadOnlyList<(byte X1, byte Y1, byte X2, byte Y2)> SafezoneRemovedAreas { get; } = - [ - new(92, 0, 157, 5), - new(92, 6, 156, 13), - new(91, 11, 91, 43), - new(92, 14, 155, 15), - new(90, 16, 90, 39), - new(92, 16, 92, 46), - new(94, 16, 155, 28), - new(93, 18, 93, 48), - new(86, 28, 89, 30), - new(94, 29, 153, 45), - new(89, 31, 89, 38), - new(87, 35, 88, 38), - new(154, 35, 155, 36), - new(154, 37, 154, 44), - new(94, 46, 152, 46), - new(94, 47, 151, 47), - new(94, 48, 150, 48), - new(94, 49, 149, 49), - new(95, 50, 149, 50), - new(96, 51, 147, 52), - new(96, 53, 146, 53), - new(97, 54, 145, 54), - new(98, 55, 144, 55), - new(99, 56, 143, 56), - new(101, 57, 141, 57), - new(102, 58, 140, 58), - new(104, 59, 139, 59), - new(107, 60, 137, 60), - new(110, 61, 136, 61), - new(114, 62, 135, 62), - new(115, 63, 125, 64), - new(116, 65, 117, 65), - new(124, 65, 125, 65), - new(101, 252, 110, 253), - new(96, 253, 100, 253), - new(111, 253, 111, 253), - new(141, 253, 151, 253), - ]; - - /// - /// Gets the areas, which are blocked while the fortress isn't in peace. - /// - public static IReadOnlyList<(byte X1, byte Y1, byte X2, byte Y2)> BlockedAreas { get; } = - [ - new(92, 0, 157, 5), - new(92, 6, 101, 6), - new(108, 6, 132, 6), - new(146, 6, 156, 6), - new(92, 7, 99, 8), - new(110, 7, 130, 7), - new(148, 7, 156, 7), - new(112, 8, 129, 8), - new(150, 8, 156, 9), - new(92, 9, 98, 9), - new(113, 9, 119, 9), - new(122, 9, 127, 10), - new(92, 10, 97, 15), - new(114, 10, 119, 10), - new(151, 10, 156, 13), - new(115, 11, 117, 12), - new(124, 11, 126, 11), - new(91, 12, 91, 28), - new(124, 12, 125, 12), - new(148, 12, 150, 29), - new(103, 13, 105, 14), - new(115, 13, 116, 13), - new(125, 13, 125, 13), - new(135, 13, 141, 14), - new(147, 13, 147, 21), - new(146, 14, 146, 20), - new(151, 14, 155, 28), - new(135, 15, 140, 15), - new(144, 15, 145, 19), - new(90, 16, 90, 28), - new(92, 16, 92, 28), - new(94, 16, 97, 22), - new(143, 16, 143, 18), - new(123, 17, 123, 24), - new(142, 17, 142, 18), - new(93, 18, 93, 28), - new(118, 18, 118, 24), - new(145, 20, 145, 20), - new(124, 21, 124, 24), - new(115, 22, 117, 23), - new(125, 22, 127, 23), - new(94, 23, 95, 28), - new(111, 24, 112, 29), - new(116, 24, 117, 24), - new(125, 24, 126, 24), - new(145, 24, 147, 29), - new(113, 25, 113, 29), - new(117, 25, 117, 25), - new(129, 25, 130, 29), - new(114, 26, 114, 28), - new(128, 26, 128, 28), - new(96, 27, 100, 28), - new(131, 27, 131, 29), - new(142, 27, 144, 28), - new(87, 28, 89, 28), - new(107, 28, 110, 29), - new(119, 28, 122, 35), - new(106, 29, 106, 29), - new(118, 29, 118, 34), - new(123, 29, 123, 34), - new(132, 29, 135, 29), - new(151, 29, 153, 29), - new(117, 30, 117, 33), - new(124, 30, 124, 33), - new(106, 34, 112, 34), - new(129, 34, 135, 34), - new(145, 34, 153, 45), - new(87, 35, 100, 36), - new(110, 35, 113, 35), - new(128, 35, 132, 35), - new(143, 35, 144, 36), - new(154, 35, 155, 36), - new(111, 36, 113, 37), - new(128, 36, 130, 36), - new(91, 37, 98, 37), - new(129, 37, 130, 37), - new(144, 37, 144, 55), - new(154, 37, 154, 43), - new(91, 38, 97, 43), - new(111, 38, 112, 38), - new(130, 38, 130, 38), - new(115, 39, 118, 41), - new(124, 39, 126, 41), - new(98, 40, 98, 54), - new(114, 40, 114, 41), - new(123, 40, 123, 45), - new(127, 40, 127, 40), - new(99, 41, 99, 55), - new(100, 42, 100, 56), - new(116, 42, 118, 42), - new(124, 42, 125, 42), - new(101, 43, 101, 45), - new(118, 43, 118, 45), - new(124, 43, 124, 45), - new(92, 44, 97, 46), - new(142, 45, 143, 55), - new(199, 45, 199, 45), - new(131, 46, 132, 62), - new(145, 46, 152, 46), - new(93, 47, 97, 48), - new(130, 47, 130, 62), - new(133, 47, 133, 62), - new(145, 47, 151, 47), - new(129, 48, 129, 50), - new(134, 48, 136, 61), - new(141, 48, 141, 57), - new(145, 48, 150, 48), - new(94, 49, 97, 49), - new(140, 49, 140, 58), - new(145, 49, 149, 50), - new(95, 50, 97, 50), - new(137, 50, 138, 59), - new(96, 51, 97, 52), - new(101, 51, 104, 56), - new(139, 51, 139, 59), - new(145, 51, 147, 52), - new(105, 52, 105, 59), - new(116, 52, 117, 65), - new(124, 52, 125, 65), - new(97, 53, 97, 53), - new(106, 53, 110, 59), - new(114, 53, 115, 61), - new(145, 53, 146, 53), - new(145, 54, 145, 54), - new(111, 55, 111, 61), - new(126, 55, 129, 61), - new(112, 56, 112, 61), - new(142, 56, 142, 56), - new(102, 57, 104, 57), - new(113, 57, 113, 61), - new(103, 58, 104, 58), - new(104, 59, 104, 59), - new(107, 60, 110, 60), - new(137, 60, 137, 60), - new(110, 61, 110, 61), - new(126, 62, 126, 62), - new(134, 62, 135, 62), - new(104, 66, 104, 66), - new(38, 83, 38, 83), - new(37, 84, 37, 84), - new(36, 85, 36, 85), - new(35, 86, 35, 95), - new(34, 87, 34, 87), - new(34, 91, 34, 91), - new(34, 93, 34, 95), - new(152, 105, 152, 105), - new(151, 119, 152, 119), - new(153, 120, 153, 121), - new(228, 123, 231, 129), - new(232, 124, 232, 132), - new(227, 125, 227, 127), - new(233, 125, 233, 132), - new(234, 129, 234, 132), - new(229, 130, 231, 131), - new(157, 133, 157, 133), - new(158, 134, 158, 135), - new(159, 135, 159, 135), - new(110, 155, 110, 155), - new(229, 155, 229, 155), - new(90, 161, 90, 161), - new(91, 162, 91, 162), - new(141, 165, 141, 166), - new(140, 167, 140, 167), - new(137, 178, 137, 178), - new(136, 179, 136, 179), - new(205, 193, 205, 193), - new(202, 194, 202, 194), - new(227, 194, 227, 195), - new(193, 196, 193, 196), - new(162, 235, 163, 235), - new(110, 238, 112, 238), - new(116, 238, 117, 238), - new(112, 239, 112, 239), - new(116, 239, 116, 239), - new(101, 252, 110, 253), - new(96, 253, 100, 253), - new(111, 253, 111, 253), - new(141, 253, 151, 253), - ]; - - /// - /// Applies the terrain of the occupation state to the terrain of the map. - /// - /// The terrain of the crywolf map. - /// If set to true, the terrain of the peace is applied; otherwise, the one of the war and the occupation. - public static void Apply(GameMapTerrain terrain, bool isPeace) - { - foreach (var (x1, y1, x2, y2) in SafezoneRemovedAreas) - { - ForEach(x1, y1, x2, y2, (x, y) => terrain.ApplyTerrainAttribute(x, y, TerrainAttributeType.Safezone, isPeace)); - } - - foreach (var (x1, y1, x2, y2) in BlockedAreas) - { - ForEach(x1, y1, x2, y2, (x, y) => terrain.ApplyTerrainAttribute(x, y, TerrainAttributeType.Blocked, !isPeace)); - } - } - - private static void ForEach(byte x1, byte y1, byte x2, byte y2, Action action) - { - for (var x = x1; x <= x2; x++) - { - for (var y = y1; y <= y2; y++) - { - action(x, y); - } - } - } -} diff --git a/src/GameLogic/GameMapTerrain.cs b/src/GameLogic/GameMapTerrain.cs index 8cc43cd75..e8cb0adbb 100644 --- a/src/GameLogic/GameMapTerrain.cs +++ b/src/GameLogic/GameMapTerrain.cs @@ -38,11 +38,11 @@ public class GameMapTerrain /// /// Pre-computed array of walkable, non-safezone points. - /// Built once during construction for O(1) random spawn lookups. + /// Built when the terrain data is loaded, for O(1) random spawn lookups. /// - private readonly Point[] _spawnPoints; + private Point[] _spawnPoints; - private readonly Point? _anyWalkableCoordinate; + private Point? _anyWalkableCoordinate; /// /// Initializes a new instance of the class. @@ -59,16 +59,8 @@ public GameMapTerrain(GameMapDefinition definition) /// The terrain data. public GameMapTerrain(byte[]? terrainData) { - if (terrainData is { }) - { - this.ReadTerrainData(terrainData.AsSpan(3)); - } - else - { - this.ReadTerrainData(DefaultTerrain); - } - - this._spawnPoints = this.BuildSpawnPoints(out this._anyWalkableCoordinate); + this._spawnPoints = []; + this.LoadTerrainData(terrainData); } /// @@ -344,6 +336,26 @@ public void ApplyTerrainAttribute(byte x, byte y, TerrainAttributeType attribute this.UpdateAiGridValue(x, y); } + /// + /// Loads the terrain data, e.g. another terrain of the map which applies in a certain state. + /// The grids are filled in place, so that references to them stay valid. + /// + /// The terrain data, which is the content of the *.att file of the original server. + /// + /// All changes which were made on the previous terrain at runtime (e.g. by , like the gates of the castle siege) are discarded. + /// For maps with such changes, they have to be applied again after loading. + /// + public void LoadTerrainData(byte[]? terrainData) + { + // The tiles which aren't part of the data get the same state as in a new terrain. + Array.Clear(this.AttributeMap); + Array.Clear(this.WalkMap); + Array.Clear(this.SafezoneMap); + Array.Clear(this.AIgrid); + this.ReadTerrainData(terrainData is { } ? terrainData.AsSpan(3) : DefaultTerrain); + this._spawnPoints = this.BuildSpawnPoints(out this._anyWalkableCoordinate); + } + /// /// Updates the ai grid value at the specified coordinate. /// diff --git a/src/Persistence/BasicModel/GameMapDefinition.Generated.cs b/src/Persistence/BasicModel/GameMapDefinition.Generated.cs index 5aedac6a2..c2bf34e60 100644 --- a/src/Persistence/BasicModel/GameMapDefinition.Generated.cs +++ b/src/Persistence/BasicModel/GameMapDefinition.Generated.cs @@ -25,6 +25,27 @@ public partial class GameMapDefinition : MUnique.OpenMU.DataModel.Configuration. /// public Guid Id { get; set; } + /// + /// Gets the raw collection of . + /// + [System.Text.Json.Serialization.JsonPropertyName("terrainVariants")] + public ICollection RawTerrainVariants { get; } = new List(); + + /// + [System.Text.Json.Serialization.JsonIgnore] + public override ICollection TerrainVariants + { + get => base.TerrainVariants ??= new CollectionAdapter(this.RawTerrainVariants); + protected set + { + this.TerrainVariants.Clear(); + foreach (var item in value) + { + this.TerrainVariants.Add(item); + } + } + } + /// /// Gets the raw collection of . /// diff --git a/src/Persistence/BasicModel/GameMapTerrainVariant.Generated.cs b/src/Persistence/BasicModel/GameMapTerrainVariant.Generated.cs new file mode 100644 index 000000000..256c531a8 --- /dev/null +++ b/src/Persistence/BasicModel/GameMapTerrainVariant.Generated.cs @@ -0,0 +1,63 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +//------------------------------------------------------------------------------ +// +// This source code was auto-generated by a roslyn code generator. +// +//------------------------------------------------------------------------------ + +// ReSharper disable All + +namespace MUnique.OpenMU.Persistence.BasicModel; + +using MUnique.OpenMU.Persistence.Json; + +/// +/// A plain implementation of . +/// +public partial class GameMapTerrainVariant : MUnique.OpenMU.DataModel.Configuration.GameMapTerrainVariant, IIdentifiable, IConvertibleTo +{ + + /// + /// Gets or sets the identifier of this instance. + /// + public Guid Id { get; set; } + + /// + public override MUnique.OpenMU.DataModel.Configuration.GameMapTerrainVariant Clone(MUnique.OpenMU.DataModel.Configuration.GameConfiguration gameConfiguration) + { + var clone = new GameMapTerrainVariant(); + clone.AssignValuesOf(this, gameConfiguration); + return clone; + } + + /// + public override void AssignValuesOf(MUnique.OpenMU.DataModel.Configuration.GameMapTerrainVariant other, MUnique.OpenMU.DataModel.Configuration.GameConfiguration gameConfiguration) + { + base.AssignValuesOf(other, gameConfiguration); + this.Id = other.GetId(); + } + + /// + public override bool Equals(object obj) + { + var baseObject = obj as IIdentifiable; + if (baseObject != null) + { + return baseObject.Id == this.Id; + } + + return base.Equals(obj); + } + + /// + public override int GetHashCode() + { + return this.Id.GetHashCode(); + } + + /// + public GameMapTerrainVariant Convert() => this; +} diff --git a/src/Persistence/EntityFramework/Migrations/20260928223416_AddCrywolfData.Designer.cs b/src/Persistence/EntityFramework/Migrations/20260929174745_AddCrywolfData.Designer.cs similarity index 99% rename from src/Persistence/EntityFramework/Migrations/20260928223416_AddCrywolfData.Designer.cs rename to src/Persistence/EntityFramework/Migrations/20260929174745_AddCrywolfData.Designer.cs index a4874dbdd..5847fcdd0 100644 --- a/src/Persistence/EntityFramework/Migrations/20260928223416_AddCrywolfData.Designer.cs +++ b/src/Persistence/EntityFramework/Migrations/20260929174745_AddCrywolfData.Designer.cs @@ -12,7 +12,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations { [DbContext(typeof(EntityDataContext))] - [Migration("20260928223416_AddCrywolfData")] + [Migration("20260929174745_AddCrywolfData")] partial class AddCrywolfData { /// @@ -1223,6 +1223,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("LastBattleEnd") .HasColumnType("timestamp with time zone"); + b.Property("WarStart") + .HasColumnType("timestamp with time zone"); + b.HasKey("Id"); b.ToTable("CrywolfData", "data"); diff --git a/src/Persistence/EntityFramework/Migrations/20260928223416_AddCrywolfData.cs b/src/Persistence/EntityFramework/Migrations/20260929174745_AddCrywolfData.cs similarity index 92% rename from src/Persistence/EntityFramework/Migrations/20260928223416_AddCrywolfData.cs rename to src/Persistence/EntityFramework/Migrations/20260929174745_AddCrywolfData.cs index bf785464e..ba669bd6d 100644 --- a/src/Persistence/EntityFramework/Migrations/20260928223416_AddCrywolfData.cs +++ b/src/Persistence/EntityFramework/Migrations/20260929174745_AddCrywolfData.cs @@ -19,6 +19,7 @@ protected override void Up(MigrationBuilder migrationBuilder) Id = table.Column(type: "uuid", nullable: false), IsOccupied = table.Column(type: "boolean", nullable: false), IsWarRunning = table.Column(type: "boolean", nullable: false), + WarStart = table.Column(type: "timestamp with time zone", nullable: true), LastBattleEnd = table.Column(type: "timestamp with time zone", nullable: true) }, constraints: table => diff --git a/src/Persistence/EntityFramework/Migrations/20260929174938_AddGameMapTerrainVariants.Designer.cs b/src/Persistence/EntityFramework/Migrations/20260929174938_AddGameMapTerrainVariants.Designer.cs new file mode 100644 index 000000000..2dabc8dfc --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20260929174938_AddGameMapTerrainVariants.Designer.cs @@ -0,0 +1,6003 @@ +// +using System; +using MUnique.OpenMU.Persistence.EntityFramework; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations +{ + [DbContext(typeof(EntityDataContext))] + [Migration("20260929174938_AddGameMapTerrainVariants")] + partial class AddGameMapTerrainVariants + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatBanUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("EMail") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsBot") + .HasColumnType("boolean"); + + b.Property("IsNetworkObservationActive") + .HasColumnType("boolean"); + + b.Property("IsTemplate") + .HasColumnType("boolean"); + + b.Property("IsVaultExtended") + .HasColumnType("boolean"); + + b.Property("LanguageIsoCode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasDefaultValue("en"); + + b.Property("LoginName") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegistrationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("TimeZone") + .HasColumnType("smallint"); + + b.Property("VaultId") + .HasColumnType("uuid"); + + b.Property("VaultPassword") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("LoginName") + .IsUnique(); + + b.HasIndex("VaultId") + .IsUnique(); + + b.ToTable("Account", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AccountCharacterClass", b => + { + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.HasKey("AccountId", "CharacterClassId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("AccountCharacterClass", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("FullAncientSetEquipped") + .HasColumnType("boolean"); + + b.Property("Pose") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("AppearanceData", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AreaSkillSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DelayBetweenHits") + .HasColumnType("interval"); + + b.Property("DelayPerOneDistance") + .HasColumnType("interval"); + + b.Property("EffectRange") + .HasColumnType("integer"); + + b.Property("FrustumDistance") + .HasColumnType("real"); + + b.Property("FrustumEndWidth") + .HasColumnType("real"); + + b.Property("FrustumStartWidth") + .HasColumnType("real"); + + b.Property("HitChancePerDistanceMultiplier") + .HasColumnType("real"); + + b.Property("MaximumNumberOfHitsPerAttack") + .HasColumnType("integer"); + + b.Property("MaximumNumberOfHitsPerTarget") + .HasColumnType("integer"); + + b.Property("MinimumNumberOfHitsPerAttack") + .HasColumnType("integer"); + + b.Property("MinimumNumberOfHitsPerTarget") + .HasColumnType("integer"); + + b.Property("ProjectileCount") + .HasColumnType("integer"); + + b.Property("TargetAreaDiameter") + .HasColumnType("real"); + + b.Property("UseDeferredHits") + .HasColumnType("boolean"); + + b.Property("UseFrustumFilter") + .HasColumnType("boolean"); + + b.Property("UseTargetAreaFilter") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("AreaSkillSettings", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Designation") + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MaximumValue") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("AttributeDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRelationship", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("InputAttributeId") + .HasColumnType("uuid"); + + b.Property("InputOperand") + .HasColumnType("real"); + + b.Property("InputOperator") + .HasColumnType("integer"); + + b.Property("OperandAttributeId") + .HasColumnType("uuid"); + + b.Property("PowerUpDefinitionValueId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterClassId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("InputAttributeId"); + + b.HasIndex("OperandAttributeId"); + + b.HasIndex("PowerUpDefinitionValueId"); + + b.HasIndex("SkillId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("AttributeRelationship", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeId") + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("MinimumValue") + .HasColumnType("integer"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("SkillId1") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AttributeId"); + + b.HasIndex("GameMapDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("SkillId"); + + b.HasIndex("SkillId1"); + + b.ToTable("AttributeRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroundId") + .HasColumnType("uuid"); + + b.Property("LeftGoalId") + .HasColumnType("uuid"); + + b.Property("LeftTeamSpawnPointX") + .HasColumnType("smallint"); + + b.Property("LeftTeamSpawnPointY") + .HasColumnType("smallint"); + + b.Property("RightGoalId") + .HasColumnType("uuid"); + + b.Property("RightTeamSpawnPointX") + .HasColumnType("smallint"); + + b.Property("RightTeamSpawnPointY") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GroundId") + .IsUnique(); + + b.HasIndex("LeftGoalId") + .IsUnique(); + + b.HasIndex("RightGoalId") + .IsUnique(); + + b.ToTable("BattleZoneDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MagicEffectDefinitionId") + .HasColumnType("uuid"); + + b.Property("MaximumLevel") + .HasColumnType("integer"); + + b.Property("MinimumLevel") + .HasColumnType("integer"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MagicEffectDefinitionId") + .IsUnique(); + + b.HasIndex("MonsterDefinitionId"); + + b.ToTable("Buff", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttackRespawnAreaId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("CrownHoldTimeSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30); + + b.Property("DefenseRespawnAreaId") + .HasColumnType("uuid"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("GateBuyPrice") + .HasColumnType("integer"); + + b.Property("GateRepairCostPerHealthPoint") + .HasColumnType("integer"); + + b.Property("GuildScoreCastleSiege") + .HasColumnType("integer"); + + b.Property("GuildScoreCastleSiegeMembers") + .HasColumnType("integer"); + + b.Property("LandOfTrialsMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("MaxAttackingGuilds") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(3); + + b.Property("ParticipantRewardMinSeconds") + .HasColumnType("integer"); + + b.Property("RegisterMinLevel") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(200); + + b.Property("RegisterMinMembers") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(20); + + b.Property("RepairCostPerUpgradeLevel") + .HasColumnType("integer"); + + b.Property("RewardItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("SignOfLordItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("SignOfLordItemLevel") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)3); + + b.Property("StatueBuyPrice") + .HasColumnType("integer"); + + b.Property("StatueRepairCostPerHealthPoint") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AttackRespawnAreaId") + .IsUnique(); + + b.HasIndex("CastleSiegeMapDefinitionId"); + + b.HasIndex("DefenseRespawnAreaId") + .IsUnique(); + + b.HasIndex("LandOfTrialsMapDefinitionId"); + + b.HasIndex("RewardItemDefinitionId"); + + b.HasIndex("SignOfLordItemDefinitionId"); + + b.ToTable("CastleSiegeConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsHuntZoneEnabled") + .HasColumnType("boolean"); + + b.Property("IsOccupied") + .HasColumnType("boolean"); + + b.Property("OwnerGuildId") + .HasColumnType("uuid"); + + b.Property("TaxChaos") + .HasColumnType("smallint"); + + b.Property("TaxHunt") + .HasColumnType("integer"); + + b.Property("TaxStore") + .HasColumnType("smallint"); + + b.Property("TributeMoney") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OwnerGuildId"); + + b.ToTable("CastleSiegeData", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeDataId") + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("GuildName") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("IsAllianceMaster") + .HasColumnType("boolean"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("Side") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeDataId"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("CastleSiegeGuild", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuildRegistration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("GuildName") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("Marks") + .HasColumnType("integer"); + + b.Property("RegistrationOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("CastleSiegeGuildRegistration", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("DefaultSide") + .HasColumnType("smallint"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("smallint"); + + b.Property("IsPersistedToDatabase") + .HasColumnType("boolean"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("SpawnX") + .HasColumnType("smallint"); + + b.Property("SpawnY") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.HasIndex("MonsterDefinitionId", "InstanceId"); + + b.ToTable("CastleSiegeNpcDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeDataId") + .HasColumnType("uuid"); + + b.Property("CurrentHp") + .HasColumnType("integer"); + + b.Property("DefenseLevel") + .HasColumnType("smallint"); + + b.Property("InstanceId") + .HasColumnType("smallint"); + + b.Property("LifeLevel") + .HasColumnType("smallint"); + + b.Property("MonsterNumber") + .HasColumnType("smallint"); + + b.Property("RegenLevel") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeDataId"); + + b.HasIndex("MonsterNumber", "InstanceId") + .IsUnique(); + + b.ToTable("CastleSiegeNpcState", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegePendingReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("CastleSiegePendingReward", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeStateScheduleEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("Hour") + .HasColumnType("smallint"); + + b.Property("Minute") + .HasColumnType("smallint"); + + b.Property("State") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.ToTable("CastleSiegeStateScheduleEntry", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeUpgradeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId1") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId2") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId3") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId4") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("RequiredJewelOfGuardianCount") + .HasColumnType("integer"); + + b.Property("RequiredZen") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.HasIndex("CastleSiegeConfigurationId1"); + + b.HasIndex("CastleSiegeConfigurationId2"); + + b.HasIndex("CastleSiegeConfigurationId3"); + + b.HasIndex("CastleSiegeConfigurationId4"); + + b.ToTable("CastleSiegeUpgradeDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId1") + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.HasIndex("CastleSiegeConfigurationId1"); + + b.ToTable("CastleSiegeZoneDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("CharacterSlot") + .HasColumnType("smallint"); + + b.Property("CharacterStatus") + .HasColumnType("integer"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentMapId") + .HasColumnType("uuid"); + + b.Property("Experience") + .HasColumnType("bigint"); + + b.Property("InventoryExtensions") + .HasColumnType("integer"); + + b.Property("InventoryId") + .HasColumnType("uuid"); + + b.Property("IsStoreOpened") + .HasColumnType("boolean"); + + b.Property("KeyConfiguration") + .HasColumnType("bytea"); + + b.Property("LevelUpPoints") + .HasColumnType("integer"); + + b.Property("MasterExperience") + .HasColumnType("bigint"); + + b.Property("MasterLevelUpPoints") + .HasColumnType("integer"); + + b.Property("MuHelperConfiguration") + .HasColumnType("bytea"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("PlayerKillCount") + .HasColumnType("integer"); + + b.Property("Pose") + .HasColumnType("smallint"); + + b.Property("PositionX") + .HasColumnType("smallint"); + + b.Property("PositionY") + .HasColumnType("smallint"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("StateRemainingSeconds") + .HasColumnType("integer"); + + b.Property("StoreName") + .HasColumnType("text"); + + b.Property("UsedFruitPoints") + .HasColumnType("integer"); + + b.Property("UsedNegFruitPoints") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CharacterClassId"); + + b.HasIndex("CurrentMapId"); + + b.HasIndex("InventoryId") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Character", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanGetCreated") + .HasColumnType("boolean"); + + b.Property("ComboDefinitionId") + .HasColumnType("uuid"); + + b.Property("CreationAllowedFlag") + .HasColumnType("smallint"); + + b.Property("FruitCalculation") + .HasColumnType("integer"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("HomeMapId") + .HasColumnType("uuid"); + + b.Property("IsMasterClass") + .HasColumnType("boolean"); + + b.Property("LevelRequirementByCreation") + .HasColumnType("smallint"); + + b.Property("LevelWarpRequirementReductionPercent") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextGenerationClassId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ComboDefinitionId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("HomeMapId"); + + b.HasIndex("NextGenerationClassId"); + + b.ToTable("CharacterClass", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterDropItemGroup", b => + { + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.HasKey("CharacterId", "DropItemGroupId"); + + b.HasIndex("DropItemGroupId"); + + b.ToTable("CharacterDropItemGroup", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActiveQuestId") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("ClientActionPerformed") + .HasColumnType("boolean"); + + b.Property("Group") + .HasColumnType("smallint"); + + b.Property("LastFinishedQuestId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ActiveQuestId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("LastFinishedQuestId"); + + b.ToTable("CharacterQuestState", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientCleanUpInterval") + .HasColumnType("interval"); + + b.Property("ClientTimeout") + .HasColumnType("interval"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaximumConnections") + .HasColumnType("integer"); + + b.Property("RoomCleanUpInterval") + .HasColumnType("interval"); + + b.Property("ServerId") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("ChatServerDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatServerDefinitionId") + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("NetworkPort") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ChatServerDefinitionId"); + + b.HasIndex("ClientId"); + + b.ToTable("ChatServerEndpoint", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CombinationBonusRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemOptionCombinationBonusId") + .HasColumnType("uuid"); + + b.Property("MinimumCount") + .HasColumnType("integer"); + + b.Property("OptionTypeId") + .HasColumnType("uuid"); + + b.Property("SubOptionType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemOptionCombinationBonusId"); + + b.HasIndex("OptionTypeId"); + + b.ToTable("CombinationBonusRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConfigurationUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstalledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ConfigurationUpdate", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConfigurationUpdateState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrentInstalledVersion") + .HasColumnType("integer"); + + b.Property("InitializationKey") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ConfigurationUpdateState", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConnectServerDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CheckMaxConnectionsPerAddress") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientListenerPort") + .HasColumnType("integer"); + + b.Property("CurrentPatchVersion") + .HasColumnType("bytea"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DisconnectOnUnknownPacket") + .HasColumnType("boolean"); + + b.Property("ListenerBacklog") + .HasColumnType("integer"); + + b.Property("MaxConnections") + .HasColumnType("integer"); + + b.Property("MaxConnectionsPerAddress") + .HasColumnType("integer"); + + b.Property("MaxFtpRequests") + .HasColumnType("integer"); + + b.Property("MaxIpRequests") + .HasColumnType("integer"); + + b.Property("MaxServerListRequests") + .HasColumnType("integer"); + + b.Property("MaximumReceiveSize") + .HasColumnType("smallint"); + + b.Property("PatchAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("smallint"); + + b.Property("Timeout") + .HasColumnType("interval"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ConnectServerDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConstValueAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("CharacterClassId"); + + b.HasIndex("DefinitionId"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ConstValueAttribute", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CrywolfData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsOccupied") + .HasColumnType("boolean"); + + b.Property("IsWarRunning") + .HasColumnType("boolean"); + + b.Property("LastBattleEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("WarStart") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("CrywolfData", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Chance") + .HasColumnType("double precision"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("ItemLevel") + .HasColumnType("smallint"); + + b.Property("ItemType") + .HasColumnType("integer"); + + b.Property("MaximumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MinimumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MonsterId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MonsterId"); + + b.ToTable("DropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroupItemDefinition", b => + { + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("DropItemGroupId", "ItemDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("DropItemGroupItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DuelConfigurationId") + .HasColumnType("uuid"); + + b.Property("FirstPlayerGateId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("smallint"); + + b.Property("SecondPlayerGateId") + .HasColumnType("uuid"); + + b.Property("SpectatorsGateId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DuelConfigurationId"); + + b.HasIndex("FirstPlayerGateId"); + + b.HasIndex("SecondPlayerGateId"); + + b.HasIndex("SpectatorsGateId"); + + b.ToTable("DuelArea", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntranceFee") + .HasColumnType("integer"); + + b.Property("ExitId") + .HasColumnType("uuid"); + + b.Property("MaximumScore") + .HasColumnType("integer"); + + b.Property("MaximumSpectatorsPerDuelRoom") + .HasColumnType("integer"); + + b.Property("MinimumCharacterLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExitId"); + + b.ToTable("DuelConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.EnterGate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("LevelRequirement") + .HasColumnType("smallint"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("TargetGateId") + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GameMapDefinitionId"); + + b.HasIndex("TargetGateId"); + + b.ToTable("EnterGate", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("IsSpawnGate") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("MapId"); + + b.ToTable("ExitGate", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Friend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Accepted") + .HasColumnType("boolean"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("FriendId") + .HasColumnType("uuid"); + + b.Property("RequestOpen") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasAlternateKey("CharacterId", "FriendId"); + + b.ToTable("Friend", "friend"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Episode") + .HasColumnType("smallint"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("Season") + .HasColumnType("smallint"); + + b.Property("Serial") + .HasColumnType("bytea"); + + b.Property("Version") + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.ToTable("GameClientDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AreaSkillHitsPlayer") + .HasColumnType("boolean"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("CharacterNameRegex") + .HasColumnType("text"); + + b.Property("ClampMoneyOnPickup") + .HasColumnType("boolean"); + + b.Property("DamagePerOneItemDurability") + .HasColumnType("double precision"); + + b.Property("DamagePerOnePetDurability") + .HasColumnType("double precision"); + + b.Property("DuelConfigurationId") + .HasColumnType("uuid"); + + b.Property("ExcellentItemDropLevelDelta") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)25); + + b.Property("ExperienceFormula") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("if(level == 0, 0, if(level < 256, 10 * (level + 8) * (level - 1) * (level - 1), (10 * (level + 8) * (level - 1) * (level - 1)) + (1000 * (level - 247) * (level - 256) * (level - 256))))"); + + b.Property("ExperienceRate") + .HasColumnType("real"); + + b.Property("HitsPerOneItemDurability") + .HasColumnType("double precision"); + + b.Property("InfoRange") + .HasColumnType("smallint"); + + b.Property("ItemDropDuration") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 1, 0, 0)); + + b.Property("LetterSendPrice") + .HasColumnType("integer"); + + b.Property("MasterExperienceFormula") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("(505 * level * level * level) + (35278500 * level) + (228045 * level * level)"); + + b.Property("MasterExperienceRate") + .HasColumnType("real"); + + b.Property("MaximumCharactersPerAccount") + .HasColumnType("smallint"); + + b.Property("MaximumInventoryMoney") + .HasColumnType("integer"); + + b.Property("MaximumItemOptionLevelDrop") + .HasColumnType("smallint"); + + b.Property("MaximumLetters") + .HasColumnType("integer"); + + b.Property("MaximumLevel") + .HasColumnType("smallint"); + + b.Property("MaximumMasterLevel") + .HasColumnType("smallint"); + + b.Property("MaximumPartySize") + .HasColumnType("smallint"); + + b.Property("MaximumPasswordLength") + .HasColumnType("integer"); + + b.Property("MaximumVaultMoney") + .HasColumnType("integer"); + + b.Property("MinimumMonsterLevelForMasterExperience") + .HasColumnType("smallint"); + + b.Property("PreventExperienceOverflow") + .HasColumnType("boolean"); + + b.Property("RecoveryInterval") + .HasColumnType("integer"); + + b.Property("ShouldDropMoney") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId") + .IsUnique(); + + b.HasIndex("DuelConfigurationId") + .IsUnique(); + + b.ToTable("GameConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BattleZoneId") + .HasColumnType("uuid"); + + b.Property("Discriminator") + .HasColumnType("integer"); + + b.Property("ExpMultiplier") + .HasColumnType("double precision"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("SafezoneMapId") + .HasColumnType("uuid"); + + b.Property("TerrainData") + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("BattleZoneId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("SafezoneMapId"); + + b.ToTable("GameMapDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinitionDropItemGroup", b => + { + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.HasKey("GameMapDefinitionId", "DropItemGroupId"); + + b.HasIndex("DropItemGroupId"); + + b.ToTable("GameMapDefinitionDropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapTerrainVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("TerrainData") + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("GameMapDefinitionId"); + + b.ToTable("GameMapTerrainVariant", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MaximumPlayers") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("GameServerConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfigurationGameMapDefinition", b => + { + b.Property("GameServerConfigurationId") + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("GameServerConfigurationId", "GameMapDefinitionId"); + + b.HasIndex("GameMapDefinitionId"); + + b.ToTable("GameServerConfigurationGameMapDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExperienceRate") + .HasColumnType("real"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("PvpEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ServerConfigurationId") + .HasColumnType("uuid"); + + b.Property("ServerID") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("ServerConfigurationId"); + + b.ToTable("GameServerDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AlternativePublishedPort") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("GameServerDefinitionId") + .HasColumnType("uuid"); + + b.Property("NetworkPort") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("GameServerDefinitionId"); + + b.ToTable("GameServerEndpoint", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllianceGuildId") + .HasColumnType("uuid"); + + b.Property("HostilityId") + .HasColumnType("uuid"); + + b.Property("Logo") + .HasColumnType("bytea"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("Notice") + .HasColumnType("text"); + + b.Property("Score") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AllianceGuildId"); + + b.HasIndex("HostilityId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Guild", "guild"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GuildMember", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.ToTable("GuildMember", "guild"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemOptionDefinitionId") + .HasColumnType("uuid"); + + b.Property("LevelType") + .HasColumnType("integer"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OptionTypeId") + .HasColumnType("uuid"); + + b.Property("PowerUpDefinitionId") + .HasColumnType("uuid"); + + b.Property("SubOptionType") + .HasColumnType("integer"); + + b.Property("Weight") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ItemOptionDefinitionId"); + + b.HasIndex("OptionTypeId"); + + b.HasIndex("PowerUpDefinitionId") + .IsUnique(); + + b.ToTable("IncreasableItemOption", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("Durability") + .HasColumnType("double precision"); + + b.Property("HasSkill") + .HasColumnType("boolean"); + + b.Property("ItemSlot") + .HasColumnType("smallint"); + + b.Property("ItemStorageId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("PetExperience") + .HasColumnType("integer"); + + b.Property("SocketCount") + .HasColumnType("integer"); + + b.Property("StorePrice") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DefinitionId"); + + b.HasIndex("ItemStorageId"); + + b.ToTable("Item", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppearanceDataId") + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemSlot") + .HasColumnType("smallint"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AppearanceDataId"); + + b.HasIndex("DefinitionId"); + + b.ToTable("ItemAppearance", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearanceItemOptionType", b => + { + b.Property("ItemAppearanceId") + .HasColumnType("uuid"); + + b.Property("ItemOptionTypeId") + .HasColumnType("uuid"); + + b.HasKey("ItemAppearanceId", "ItemOptionTypeId"); + + b.HasIndex("ItemOptionTypeId"); + + b.ToTable("ItemAppearanceItemOptionType", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemBasePowerUpDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("BaseValue") + .HasColumnType("real"); + + b.Property("BonusPerLevelTableId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BonusPerLevelTableId"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("ItemBasePowerUpDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCrafting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemCraftingHandlerClassName") + .IsRequired() + .HasColumnType("text"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("SimpleCraftingSettingsId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MonsterDefinitionId"); + + b.HasIndex("SimpleCraftingSettingsId") + .IsUnique(); + + b.ToTable("ItemCrafting", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddPercentage") + .HasColumnType("smallint"); + + b.Property("FailResult") + .HasColumnType("integer"); + + b.Property("MaximumAmount") + .HasColumnType("smallint"); + + b.Property("MaximumItemLevel") + .HasColumnType("smallint"); + + b.Property("MinimumAmount") + .HasColumnType("smallint"); + + b.Property("MinimumItemLevel") + .HasColumnType("smallint"); + + b.Property("NpcPriceDivisor") + .HasColumnType("integer"); + + b.Property("Reference") + .HasColumnType("smallint"); + + b.Property("SimpleCraftingSettingsId") + .HasColumnType("uuid"); + + b.Property("SuccessResult") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SimpleCraftingSettingsId"); + + b.ToTable("ItemCraftingRequiredItem", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemDefinition", b => + { + b.Property("ItemCraftingRequiredItemId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("ItemCraftingRequiredItemId", "ItemDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("ItemCraftingRequiredItemItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemOptionType", b => + { + b.Property("ItemCraftingRequiredItemId") + .HasColumnType("uuid"); + + b.Property("ItemOptionTypeId") + .HasColumnType("uuid"); + + b.HasKey("ItemCraftingRequiredItemId", "ItemOptionTypeId"); + + b.HasIndex("ItemOptionTypeId"); + + b.ToTable("ItemCraftingRequiredItemItemOptionType", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingResultItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddLevel") + .HasColumnType("smallint"); + + b.Property("Durability") + .HasColumnType("smallint"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("RandomMaximumLevel") + .HasColumnType("smallint"); + + b.Property("RandomMinimumLevel") + .HasColumnType("smallint"); + + b.Property("Reference") + .HasColumnType("smallint"); + + b.Property("SimpleCraftingSettingsId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("SimpleCraftingSettingsId"); + + b.ToTable("ItemCraftingResultItem", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConsumeEffectId") + .HasColumnType("uuid"); + + b.Property("DropLevel") + .HasColumnType("smallint"); + + b.Property("DropsFromMonsters") + .HasColumnType("boolean"); + + b.Property("Durability") + .HasColumnType("smallint"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Group") + .HasColumnType("smallint"); + + b.Property("Height") + .HasColumnType("smallint"); + + b.Property("IsAmmunition") + .HasColumnType("boolean"); + + b.Property("IsBoundToCharacter") + .HasColumnType("boolean"); + + b.Property("IsDroppable") + .HasColumnType("boolean"); + + b.Property("IsPersonalStoreSellable") + .HasColumnType("boolean"); + + b.Property("IsQuestItem") + .HasColumnType("boolean"); + + b.Property("IsRepairable") + .HasColumnType("boolean"); + + b.Property("IsSellableToNpc") + .HasColumnType("boolean"); + + b.Property("IsStorable") + .HasColumnType("boolean"); + + b.Property("IsTradable") + .HasColumnType("boolean"); + + b.Property("ItemSlotId") + .HasColumnType("uuid"); + + b.Property("MaximumDropLevel") + .HasColumnType("smallint"); + + b.Property("MaximumItemLevel") + .HasColumnType("smallint"); + + b.Property("MaximumSockets") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("PetExperienceFormula") + .HasColumnType("text"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("StorageLimitPerCharacter") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("integer"); + + b.Property("Width") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ConsumeEffectId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("ItemSlotId"); + + b.HasIndex("SkillId"); + + b.ToTable("ItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionCharacterClass", b => + { + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.HasKey("ItemDefinitionId", "CharacterClassId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("ItemDefinitionCharacterClass", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemOptionDefinition", b => + { + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemOptionDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("ItemDefinitionId", "ItemOptionDefinitionId"); + + b.HasIndex("ItemOptionDefinitionId"); + + b.ToTable("ItemDefinitionItemOptionDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemSetGroup", b => + { + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemSetGroupId") + .HasColumnType("uuid"); + + b.HasKey("ItemDefinitionId", "ItemSetGroupId"); + + b.HasIndex("ItemSetGroupId"); + + b.ToTable("ItemDefinitionItemSetGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Chance") + .HasColumnType("double precision"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DropEffect") + .HasColumnType("integer"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemLevel") + .HasColumnType("smallint"); + + b.Property("ItemType") + .HasColumnType("integer"); + + b.Property("MaximumLevel") + .HasColumnType("smallint"); + + b.Property("MaximumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MinimumLevel") + .HasColumnType("smallint"); + + b.Property("MinimumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MoneyAmount") + .HasColumnType("integer"); + + b.Property("MonsterId") + .HasColumnType("uuid"); + + b.Property("RequiredCharacterLevel") + .HasColumnType("smallint"); + + b.Property("SourceItemLevel") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("MonsterId"); + + b.ToTable("ItemDropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroupItemDefinition", b => + { + b.Property("ItemDropItemGroupId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("ItemDropItemGroupId", "ItemDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("ItemDropItemGroupItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemItemOfItemSet", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ItemOfItemSetId") + .HasColumnType("uuid"); + + b.HasKey("ItemId", "ItemOfItemSetId"); + + b.HasIndex("ItemOfItemSetId"); + + b.ToTable("ItemItemOfItemSet", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemLevelBonusTable", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOfItemSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AncientSetDiscriminator") + .HasColumnType("integer"); + + b.Property("BonusOptionId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemSetGroupId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BonusOptionId"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("ItemSetGroupId"); + + b.ToTable("ItemOfItemSet", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OptionTypeId") + .HasColumnType("uuid"); + + b.Property("PowerUpDefinitionId") + .HasColumnType("uuid"); + + b.Property("SubOptionType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OptionTypeId"); + + b.HasIndex("PowerUpDefinitionId") + .IsUnique(); + + b.ToTable("ItemOption", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppliesMultipleTimes") + .HasColumnType("boolean"); + + b.Property("BonusId") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BonusId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemOptionCombinationBonus", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddChance") + .HasColumnType("real"); + + b.Property("AddsRandomly") + .HasColumnType("boolean"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MaximumOptionsPerItem") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemOptionDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ItemOptionId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.HasIndex("ItemOptionId"); + + b.ToTable("ItemOptionLink", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionOfLevel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IncreasableItemOptionId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("PowerUpDefinitionId") + .HasColumnType("uuid"); + + b.Property("RequiredItemLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IncreasableItemOptionId"); + + b.HasIndex("PowerUpDefinitionId") + .IsUnique(); + + b.ToTable("ItemOptionOfLevel", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemOptionType", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AlwaysApplies") + .HasColumnType("boolean"); + + b.Property("CountDistinct") + .HasColumnType("boolean"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MinimumItemCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OptionsId") + .HasColumnType("uuid"); + + b.Property("SetLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("OptionsId"); + + b.ToTable("ItemSetGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSlotType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("RawItemSlots") + .HasColumnType("text") + .HasColumnName("ItemSlots") + .HasJsonPropertyName("itemSlots"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemSlotType", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Money") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ItemStorage", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.JewelMix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MixedJewelId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("SingleJewelId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MixedJewelId"); + + b.HasIndex("SingleJewelId"); + + b.ToTable("JewelMix", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterBody", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Animation") + .HasColumnType("smallint"); + + b.Property("HeaderId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rotation") + .HasColumnType("smallint"); + + b.Property("SenderAppearanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("HeaderId"); + + b.HasIndex("SenderAppearanceId") + .IsUnique(); + + b.ToTable("LetterBody", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterHeader", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("LetterDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadFlag") + .HasColumnType("boolean"); + + b.Property("ReceiverId") + .HasColumnType("uuid"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ReceiverId"); + + b.ToTable("LetterHeader", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LevelBonus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdditionalValue") + .HasColumnType("real"); + + b.Property("ItemLevelBonusTableId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemLevelBonusTableId"); + + b.ToTable("LevelBonus", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChanceId") + .HasColumnType("uuid"); + + b.Property("ChancePvpId") + .HasColumnType("uuid"); + + b.Property("DurationDependsOnTargetLevel") + .HasColumnType("boolean"); + + b.Property("DurationId") + .HasColumnType("uuid"); + + b.Property("DurationPvpId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("InformObservers") + .HasColumnType("boolean"); + + b.Property("MonsterTargetLevelDivisor") + .HasColumnType("real"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("PlayerTargetLevelDivisor") + .HasColumnType("real"); + + b.Property("SendDuration") + .HasColumnType("boolean"); + + b.Property("StopByDeath") + .HasColumnType("boolean"); + + b.Property("SubType") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ChanceId") + .IsUnique(); + + b.HasIndex("ChancePvpId") + .IsUnique(); + + b.HasIndex("DurationId") + .IsUnique(); + + b.HasIndex("DurationPvpId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("MagicEffectDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Aggregation") + .HasColumnType("integer"); + + b.Property("DisplayValueFormula") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtendsDuration") + .HasColumnType("boolean"); + + b.Property("MaximumLevel") + .HasColumnType("smallint"); + + b.Property("MinimumLevel") + .HasColumnType("smallint"); + + b.Property("Rank") + .HasColumnType("smallint"); + + b.Property("ReplacedSkillId") + .HasColumnType("uuid"); + + b.Property("RootId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.Property("ValueFormula") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ReplacedSkillId"); + + b.HasIndex("RootId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("MasterSkillDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinitionSkill", b => + { + b.Property("MasterSkillDefinitionId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("MasterSkillDefinitionId", "SkillId"); + + b.HasIndex("SkillId"); + + b.ToTable("MasterSkillDefinitionSkill", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("MasterSkillRoot", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("MiniGameDefinitionId") + .HasColumnType("uuid"); + + b.Property("MinimumTargetLevel") + .HasColumnType("smallint"); + + b.Property("MultiplyKillsByPlayers") + .HasColumnType("boolean"); + + b.Property("NumberOfKills") + .HasColumnType("smallint"); + + b.Property("SpawnAreaId") + .HasColumnType("uuid"); + + b.Property("Target") + .HasColumnType("integer"); + + b.Property("TargetDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MiniGameDefinitionId"); + + b.HasIndex("SpawnAreaId") + .IsUnique(); + + b.HasIndex("TargetDefinitionId"); + + b.ToTable("MiniGameChangeEvent", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowParty") + .HasColumnType("boolean"); + + b.Property("ArePlayerKillersAllowedToEnter") + .HasColumnType("boolean"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EnterDuration") + .HasColumnType("interval"); + + b.Property("EntranceFee") + .HasColumnType("integer"); + + b.Property("EntranceId") + .HasColumnType("uuid"); + + b.Property("ExitDuration") + .HasColumnType("interval"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("GameDuration") + .HasColumnType("interval"); + + b.Property("GameLevel") + .HasColumnType("smallint"); + + b.Property("MapCreationPolicy") + .HasColumnType("integer"); + + b.Property("MaximumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MaximumPlayerCount") + .HasColumnType("integer"); + + b.Property("MaximumSpecialCharacterLevel") + .HasColumnType("integer"); + + b.Property("MinimumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MinimumSpecialCharacterLevel") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequiresMasterClass") + .HasColumnType("boolean"); + + b.Property("SaveRankingStatistics") + .HasColumnType("boolean"); + + b.Property("TicketItemId") + .HasColumnType("uuid"); + + b.Property("TicketItemLevel") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EntranceId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("TicketItemId"); + + b.ToTable("MiniGameDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameRankingEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("GameInstanceId") + .HasColumnType("uuid"); + + b.Property("MiniGameId") + .HasColumnType("uuid"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("MiniGameId"); + + b.ToTable("MiniGameRankingEntry", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemRewardId") + .HasColumnType("uuid"); + + b.Property("MiniGameDefinitionId") + .HasColumnType("uuid"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("RequiredKillId") + .HasColumnType("uuid"); + + b.Property("RequiredSuccess") + .HasColumnType("integer"); + + b.Property("RewardAmount") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemRewardId"); + + b.HasIndex("MiniGameDefinitionId"); + + b.HasIndex("RequiredKillId"); + + b.ToTable("MiniGameReward", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameSpawnWave", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("interval"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("MiniGameDefinitionId") + .HasColumnType("uuid"); + + b.Property("StartTime") + .HasColumnType("interval"); + + b.Property("WaveNumber") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("MiniGameDefinitionId"); + + b.ToTable("MiniGameSpawnWave", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameTerrainChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EndX") + .HasColumnType("smallint"); + + b.Property("EndY") + .HasColumnType("smallint"); + + b.Property("IsClientUpdateRequired") + .HasColumnType("boolean"); + + b.Property("MiniGameChangeEventId") + .HasColumnType("uuid"); + + b.Property("SetTerrainAttribute") + .HasColumnType("boolean"); + + b.Property("StartX") + .HasColumnType("smallint"); + + b.Property("StartY") + .HasColumnType("smallint"); + + b.Property("TerrainAttribute") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("MiniGameChangeEventId"); + + b.ToTable("MiniGameTerrainChange", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeDefinitionId") + .HasColumnType("uuid"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("AttributeDefinitionId"); + + b.HasIndex("MonsterDefinitionId"); + + b.ToTable("MonsterAttribute", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttackDelay") + .HasColumnType("interval"); + + b.Property("AttackRange") + .HasColumnType("smallint"); + + b.Property("AttackSkillId") + .HasColumnType("uuid"); + + b.Property("Attribute") + .HasColumnType("smallint"); + + b.Property("Designation") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("IntelligenceTypeName") + .HasColumnType("text"); + + b.Property("MerchantStoreId") + .HasColumnType("uuid"); + + b.Property("MoveDelay") + .HasColumnType("interval"); + + b.Property("MoveRange") + .HasColumnType("smallint"); + + b.Property("NpcWindow") + .HasColumnType("integer"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("NumberOfMaximumItemDrops") + .HasColumnType("integer"); + + b.Property("ObjectKind") + .HasColumnType("integer"); + + b.Property("RespawnDelay") + .HasColumnType("interval"); + + b.Property("ViewRange") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AttackSkillId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MerchantStoreId") + .IsUnique(); + + b.ToTable("MonsterDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinitionDropItemGroup", b => + { + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.HasKey("MonsterDefinitionId", "DropItemGroupId"); + + b.HasIndex("DropItemGroupId"); + + b.ToTable("MonsterDefinitionDropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterSpawnArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("GameMapId") + .HasColumnType("uuid"); + + b.Property("MaximumHealthOverride") + .HasColumnType("integer"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("smallint"); + + b.Property("SpawnTrigger") + .HasColumnType("integer"); + + b.Property("WaveNumber") + .HasColumnType("smallint"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GameMapId"); + + b.HasIndex("MonsterDefinitionId"); + + b.ToTable("MonsterSpawnArea", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PlugInConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CustomConfiguration") + .HasColumnType("text"); + + b.Property("CustomPlugInSource") + .HasColumnType("text"); + + b.Property("ExternalAssemblyName") + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("TypeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("PlugInConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BoostId") + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("MagicEffectDefinitionId") + .HasColumnType("uuid"); + + b.Property("MagicEffectDefinitionId1") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BoostId") + .IsUnique(); + + b.HasIndex("GameMapDefinitionId"); + + b.HasIndex("MagicEffectDefinitionId"); + + b.HasIndex("MagicEffectDefinitionId1"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("PowerUpDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("MaximumValue") + .HasColumnType("real"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.ToTable("PowerUpDefinitionValue", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Group") + .HasColumnType("smallint"); + + b.Property("MaximumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MinimumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("QualifiedCharacterId") + .HasColumnType("uuid"); + + b.Property("QuestGiverId") + .HasColumnType("uuid"); + + b.Property("RefuseNumber") + .HasColumnType("smallint"); + + b.Property("Repeatable") + .HasColumnType("boolean"); + + b.Property("RequiredStartMoney") + .HasColumnType("integer"); + + b.Property("RequiresClientAction") + .HasColumnType("boolean"); + + b.Property("StartingNumber") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("MonsterDefinitionId"); + + b.HasIndex("QualifiedCharacterId"); + + b.HasIndex("QuestGiverId"); + + b.ToTable("QuestDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestItemRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("MinimumNumber") + .HasColumnType("integer"); + + b.Property("QuestDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DropItemGroupId"); + + b.HasIndex("ItemId"); + + b.HasIndex("QuestDefinitionId"); + + b.ToTable("QuestItemRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MinimumNumber") + .HasColumnType("integer"); + + b.Property("MonsterId") + .HasColumnType("uuid"); + + b.Property("QuestDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MonsterId"); + + b.HasIndex("QuestDefinitionId"); + + b.ToTable("QuestMonsterKillRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirementState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterQuestStateId") + .HasColumnType("uuid"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("RequirementId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterQuestStateId"); + + b.HasIndex("RequirementId"); + + b.ToTable("QuestMonsterKillRequirementState", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeRewardId") + .HasColumnType("uuid"); + + b.Property("ItemRewardId") + .HasColumnType("uuid"); + + b.Property("QuestDefinitionId") + .HasColumnType("uuid"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("SkillRewardId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AttributeRewardId"); + + b.HasIndex("ItemRewardId") + .IsUnique(); + + b.HasIndex("QuestDefinitionId"); + + b.HasIndex("SkillRewardId"); + + b.ToTable("QuestReward", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Rectangle", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MaximumSuccessPercent") + .HasColumnType("smallint"); + + b.Property("Money") + .HasColumnType("integer"); + + b.Property("MoneyPerFinalSuccessPercentage") + .HasColumnType("integer"); + + b.Property("MultipleAllowed") + .HasColumnType("boolean"); + + b.Property("NpcPriceDivisor") + .HasColumnType("integer"); + + b.Property("ResultItemExcellentOptionChance") + .HasColumnType("smallint"); + + b.Property("ResultItemLuckOptionChance") + .HasColumnType("smallint"); + + b.Property("ResultItemMaxExcOptionCount") + .HasColumnType("smallint"); + + b.Property("ResultItemSelect") + .HasColumnType("integer"); + + b.Property("ResultItemSkillChance") + .HasColumnType("smallint"); + + b.Property("SuccessPercent") + .HasColumnType("smallint"); + + b.Property("SuccessPercentageAdditionForAncientItem") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForExcellentItem") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForGuardianItem") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForLuck") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForSocketItem") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SimpleCraftingSettings", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AreaSkillSettingsId") + .HasColumnType("uuid"); + + b.Property("AttackDamage") + .HasColumnType("integer"); + + b.Property("DamageType") + .HasColumnType("integer"); + + b.Property("ElementalModifierTargetId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("ImplicitTargetRange") + .HasColumnType("smallint"); + + b.Property("MagicEffectDefId") + .HasColumnType("uuid"); + + b.Property("MasterDefinitionId") + .HasColumnType("uuid"); + + b.Property("MovesTarget") + .HasColumnType("boolean"); + + b.Property("MovesToTarget") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("NumberOfHitsPerAttack") + .HasColumnType("smallint"); + + b.Property("Range") + .HasColumnType("smallint"); + + b.Property("SkillType") + .HasColumnType("integer"); + + b.Property("SkipElementalModifier") + .HasColumnType("boolean"); + + b.Property("Target") + .HasColumnType("integer"); + + b.Property("TargetRestriction") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AreaSkillSettingsId") + .IsUnique(); + + b.HasIndex("ElementalModifierTargetId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MagicEffectDefId"); + + b.HasIndex("MasterDefinitionId") + .IsUnique(); + + b.ToTable("Skill", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillCharacterClass", b => + { + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.HasKey("SkillId", "CharacterClassId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("SkillCharacterClass", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MaximumCompletionTime") + .HasColumnType("interval"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SkillComboDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsFinalStep") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("SkillComboDefinitionId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SkillComboDefinitionId"); + + b.HasIndex("SkillId"); + + b.ToTable("SkillComboStep", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("SkillId"); + + b.ToTable("SkillEntry", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("DefinitionId"); + + b.ToTable("StatAttribute", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttributeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeId") + .HasColumnType("uuid"); + + b.Property("BaseValue") + .HasColumnType("real"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("IncreasableByPlayer") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AttributeId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("StatAttributeDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SystemConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AutoStart") + .HasColumnType("boolean"); + + b.Property("AutoUpdateSchema") + .HasColumnType("boolean"); + + b.Property("IpResolver") + .HasColumnType("integer"); + + b.Property("IpResolverParameter") + .HasColumnType("text"); + + b.Property("NetworkAnalyzerLiveBufferSize") + .HasColumnType("integer"); + + b.Property("NetworkObservationArchivePath") + .HasColumnType("text"); + + b.Property("NetworkObservationMaxSessionSizeMb") + .HasColumnType("integer"); + + b.Property("NetworkObservationMaxTotalSizeMb") + .HasColumnType("integer"); + + b.Property("NetworkObservationRetentionDays") + .HasColumnType("integer"); + + b.Property("ReadConsoleInput") + .HasColumnType("boolean"); + + b.Property("TimeZoneId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SystemConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.WarpInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Costs") + .HasColumnType("integer"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("GateId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("LevelRequirement") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("GateId"); + + b.ToTable("WarpInfo", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawVault") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", "VaultId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawVault"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AccountCharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", "Account") + .WithMany("JoinedUnlockedCharacterClasses") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("CharacterClass"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawCharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId"); + + b.Navigation("RawCharacterClass"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawAttributes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRelationship", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", null) + .WithMany("RawAttributeCombinations") + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawGlobalAttributeCombinations") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawInputAttribute") + .WithMany() + .HasForeignKey("InputAttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawOperandAttribute") + .WithMany() + .HasForeignKey("OperandAttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", null) + .WithMany("RawRelatedValues") + .HasForeignKey("PowerUpDefinitionValueId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", null) + .WithMany("RawAttributeRelationships") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawInputAttribute"); + + b.Navigation("RawOperandAttribute"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttribute") + .WithMany() + .HasForeignKey("AttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawMapRequirements") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany("RawRequirements") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", null) + .WithMany("RawConsumeRequirements") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", null) + .WithMany("RawRequirements") + .HasForeignKey("SkillId1") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", "RawGround") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "GroundId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", "RawLeftGoal") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "LeftGoalId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", "RawRightGoal") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "RightGoalId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawGround"); + + b.Navigation("RawLeftGoal"); + + b.Navigation("RawRightGoal"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "RawMagicEffectDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", "MagicEffectDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawBuffs") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawMagicEffectDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", "RawAttackRespawnArea") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", "AttackRespawnAreaId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawCastleSiegeMapDefinition") + .WithMany() + .HasForeignKey("CastleSiegeMapDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", "RawDefenseRespawnArea") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", "DefenseRespawnAreaId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawLandOfTrialsMapDefinition") + .WithMany() + .HasForeignKey("LandOfTrialsMapDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawRewardItemDefinition") + .WithMany() + .HasForeignKey("RewardItemDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawSignOfLordItemDefinition") + .WithMany() + .HasForeignKey("SignOfLordItemDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("RawAttackRespawnArea"); + + b.Navigation("RawCastleSiegeMapDefinition"); + + b.Navigation("RawDefenseRespawnArea"); + + b.Navigation("RawLandOfTrialsMapDefinition"); + + b.Navigation("RawRewardItemDefinition"); + + b.Navigation("RawSignOfLordItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("OwnerGuildId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuild", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", null) + .WithMany("RawGuilds") + .HasForeignKey("CastleSiegeDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuildRegistration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawNpcDefinitions") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonsterDefinition") + .WithMany() + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("RawMonsterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcState", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", null) + .WithMany("RawNpcStates") + .HasForeignKey("CastleSiegeDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegePendingReward", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeStateScheduleEntry", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStateSchedule") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeUpgradeDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawGateDefenseUpgrades") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawGateLifeUpgrades") + .HasForeignKey("CastleSiegeConfigurationId1") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~1"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStatueDefenseUpgrades") + .HasForeignKey("CastleSiegeConfigurationId2") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~2"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStatueLifeUpgrades") + .HasForeignKey("CastleSiegeConfigurationId3") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~3"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStatueRegenUpgrades") + .HasForeignKey("CastleSiegeConfigurationId4") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~4"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawAttackMachineZones") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawDefenseMachineZones") + .HasForeignKey("CastleSiegeConfigurationId1") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeZoneDefinition_CastleSiegeConfiguration_CastleS~1"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany("RawCharacters") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawCharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawCurrentMap") + .WithMany() + .HasForeignKey("CurrentMapId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawInventory") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "InventoryId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawCharacterClass"); + + b.Navigation("RawCurrentMap"); + + b.Navigation("RawInventory"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", "RawComboDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "ComboDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawCharacterClasses") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawHomeMap") + .WithMany() + .HasForeignKey("HomeMapId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawNextGenerationClass") + .WithMany() + .HasForeignKey("NextGenerationClassId"); + + b.Navigation("RawComboDefinition"); + + b.Navigation("RawHomeMap"); + + b.Navigation("RawNextGenerationClass"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "Character") + .WithMany("JoinedDropItemGroups") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + + b.Navigation("DropItemGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", "RawActiveQuest") + .WithMany() + .HasForeignKey("ActiveQuestId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany("RawQuestStates") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", "RawLastFinishedQuest") + .WithMany() + .HasForeignKey("LastFinishedQuestId"); + + b.Navigation("RawActiveQuest"); + + b.Navigation("RawLastFinishedQuest"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerEndpoint", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", null) + .WithMany("RawEndpoints") + .HasForeignKey("ChatServerDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", "RawClient") + .WithMany() + .HasForeignKey("ClientId"); + + b.Navigation("RawClient"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CombinationBonusRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", null) + .WithMany("RawRequirements") + .HasForeignKey("ItemOptionCombinationBonusId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "RawOptionType") + .WithMany() + .HasForeignKey("OptionTypeId"); + + b.Navigation("RawOptionType"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConnectServerDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", "RawClient") + .WithMany() + .HasForeignKey("ClientId"); + + b.Navigation("RawClient"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConstValueAttribute", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany("RawBaseAttributeValues") + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "GameConfiguration") + .WithMany("RawGlobalBaseAttributeValues") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("CharacterClass"); + + b.Navigation("GameConfiguration"); + + b.Navigation("RawDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawDropItemGroups") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonster") + .WithMany() + .HasForeignKey("MonsterId"); + + b.Navigation("RawMonster"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroupItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany("JoinedPossibleItems") + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DropItemGroup"); + + b.Navigation("ItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelArea", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", null) + .WithMany("RawDuelAreas") + .HasForeignKey("DuelConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawFirstPlayerGate") + .WithMany() + .HasForeignKey("FirstPlayerGateId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawSecondPlayerGate") + .WithMany() + .HasForeignKey("SecondPlayerGateId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawSpectatorsGate") + .WithMany() + .HasForeignKey("SpectatorsGateId"); + + b.Navigation("RawFirstPlayerGate"); + + b.Navigation("RawSecondPlayerGate"); + + b.Navigation("RawSpectatorsGate"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawExit") + .WithMany() + .HasForeignKey("ExitId"); + + b.Navigation("RawExit"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.EnterGate", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawEnterGates") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawTargetGate") + .WithMany() + .HasForeignKey("TargetGateId"); + + b.Navigation("RawTargetGate"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawMap") + .WithMany("RawExitGates") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawMap"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", "RawCastleSiegeConfiguration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", "RawDuelConfiguration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "DuelConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawCastleSiegeConfiguration"); + + b.Navigation("RawDuelConfiguration"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "RawBattleZone") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "BattleZoneId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMaps") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawSafezoneMap") + .WithMany() + .HasForeignKey("SafezoneMapId"); + + b.Navigation("RawBattleZone"); + + b.Navigation("RawSafezoneMap"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinitionDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "GameMapDefinition") + .WithMany("JoinedDropItemGroups") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DropItemGroup"); + + b.Navigation("GameMapDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapTerrainVariant", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawTerrainVariants") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfigurationGameMapDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "GameMapDefinition") + .WithMany() + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", "GameServerConfiguration") + .WithMany("JoinedMaps") + .HasForeignKey("GameServerConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GameMapDefinition"); + + b.Navigation("GameServerConfiguration"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "RawGameConfiguration") + .WithMany() + .HasForeignKey("GameConfigurationId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", "RawServerConfiguration") + .WithMany() + .HasForeignKey("ServerConfigurationId"); + + b.Navigation("RawGameConfiguration"); + + b.Navigation("RawServerConfiguration"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerEndpoint", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", "RawClient") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", null) + .WithMany("RawEndpoints") + .HasForeignKey("GameServerDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawClient"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", "RawAllianceGuild") + .WithMany() + .HasForeignKey("AllianceGuildId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", "RawHostility") + .WithMany() + .HasForeignKey("HostilityId"); + + b.Navigation("RawAllianceGuild"); + + b.Navigation("RawHostility"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GuildMember", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany("RawMembers") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "Character") + .WithMany() + .HasForeignKey("Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", null) + .WithMany("RawPossibleOptions") + .HasForeignKey("ItemOptionDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "RawOptionType") + .WithMany() + .HasForeignKey("OptionTypeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawPowerUpDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", "PowerUpDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawOptionType"); + + b.Navigation("RawPowerUpDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawItemStorage") + .WithMany("RawItems") + .HasForeignKey("ItemStorageId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawDefinition"); + + b.Navigation("RawItemStorage"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", null) + .WithMany("RawEquippedItems") + .HasForeignKey("AppearanceDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.Navigation("RawDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearanceItemOptionType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", "ItemAppearance") + .WithMany("JoinedVisibleOptions") + .HasForeignKey("ItemAppearanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "ItemOptionType") + .WithMany() + .HasForeignKey("ItemOptionTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemAppearance"); + + b.Navigation("ItemOptionType"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemBasePowerUpDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", "RawBonusPerLevelTable") + .WithMany() + .HasForeignKey("BonusPerLevelTableId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany("RawBasePowerUpAttributes") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawBonusPerLevelTable"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCrafting", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawItemCraftings") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", "RawSimpleCraftingSettings") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCrafting", "SimpleCraftingSettingsId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawSimpleCraftingSettings"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", null) + .WithMany("RawRequiredItems") + .HasForeignKey("SimpleCraftingSettingsId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", "ItemCraftingRequiredItem") + .WithMany("JoinedPossibleItems") + .HasForeignKey("ItemCraftingRequiredItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemCraftingRequiredItem"); + + b.Navigation("ItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemOptionType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", "ItemCraftingRequiredItem") + .WithMany("JoinedRequiredItemOptions") + .HasForeignKey("ItemCraftingRequiredItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "ItemOptionType") + .WithMany() + .HasForeignKey("ItemOptionTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemCraftingRequiredItem"); + + b.Navigation("ItemOptionType"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingResultItem", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", null) + .WithMany("RawResultItems") + .HasForeignKey("SimpleCraftingSettingsId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "RawConsumeEffect") + .WithMany() + .HasForeignKey("ConsumeEffectId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItems") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSlotType", "RawItemSlot") + .WithMany() + .HasForeignKey("ItemSlotId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkill") + .WithMany() + .HasForeignKey("SkillId"); + + b.Navigation("RawConsumeEffect"); + + b.Navigation("RawItemSlot"); + + b.Navigation("RawSkill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionCharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany("JoinedQualifiedCharacters") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CharacterClass"); + + b.Navigation("ItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemOptionDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany("JoinedPossibleItemOptions") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", "ItemOptionDefinition") + .WithMany() + .HasForeignKey("ItemOptionDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemDefinition"); + + b.Navigation("ItemOptionDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemSetGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany("JoinedPossibleItemSetGroups") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", "ItemSetGroup") + .WithMany() + .HasForeignKey("ItemSetGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemDefinition"); + + b.Navigation("ItemSetGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany("RawDropItems") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonster") + .WithMany() + .HasForeignKey("MonsterId"); + + b.Navigation("RawMonster"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroupItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", "ItemDropItemGroup") + .WithMany("JoinedPossibleItems") + .HasForeignKey("ItemDropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemDefinition"); + + b.Navigation("ItemDropItemGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemItemOfItemSet", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", "Item") + .WithMany("JoinedItemSetGroups") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOfItemSet", "ItemOfItemSet") + .WithMany() + .HasForeignKey("ItemOfItemSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemOfItemSet"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemLevelBonusTables") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOfItemSet", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", "RawBonusOption") + .WithMany() + .HasForeignKey("BonusOptionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", "RawItemSetGroup") + .WithMany("RawItems") + .HasForeignKey("ItemSetGroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawBonusOption"); + + b.Navigation("RawItemDefinition"); + + b.Navigation("RawItemSetGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOption", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "RawOptionType") + .WithMany() + .HasForeignKey("OptionTypeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawPowerUpDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOption", "PowerUpDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawOptionType"); + + b.Navigation("RawPowerUpDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawBonus") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", "BonusId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemOptionCombinationBonuses") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawBonus"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemOptions") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionLink", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", null) + .WithMany("RawItemOptions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", "RawItemOption") + .WithMany() + .HasForeignKey("ItemOptionId"); + + b.Navigation("RawItemOption"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionOfLevel", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", null) + .WithMany("RawLevelDependentOptions") + .HasForeignKey("IncreasableItemOptionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawPowerUpDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionOfLevel", "PowerUpDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawPowerUpDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemOptionTypes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemSetGroups") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", "RawOptions") + .WithMany() + .HasForeignKey("OptionsId"); + + b.Navigation("RawOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSlotType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemSlotTypes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.JewelMix", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawJewelMixes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawMixedJewel") + .WithMany() + .HasForeignKey("MixedJewelId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawSingleJewel") + .WithMany() + .HasForeignKey("SingleJewelId"); + + b.Navigation("RawMixedJewel"); + + b.Navigation("RawSingleJewel"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterBody", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterHeader", "RawHeader") + .WithMany() + .HasForeignKey("HeaderId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", "RawSenderAppearance") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterBody", "SenderAppearanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawHeader"); + + b.Navigation("RawSenderAppearance"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterHeader", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "Receiver") + .WithMany("RawLetters") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Receiver"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LevelBonus", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", null) + .WithMany("RawBonusPerLevel") + .HasForeignKey("ItemLevelBonusTableId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawChance") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "ChanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawChancePvp") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "ChancePvpId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawDuration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "DurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawDurationPvp") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "DurationPvpId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMagicEffects") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawChance"); + + b.Navigation("RawChancePvp"); + + b.Navigation("RawDuration"); + + b.Navigation("RawDurationPvp"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawReplacedSkill") + .WithMany() + .HasForeignKey("ReplacedSkillId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillRoot", "RawRoot") + .WithMany() + .HasForeignKey("RootId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawReplacedSkill"); + + b.Navigation("RawRoot"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinitionSkill", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", "MasterSkillDefinition") + .WithMany("JoinedRequiredMasterSkills") + .HasForeignKey("MasterSkillDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "Skill") + .WithMany() + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MasterSkillDefinition"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillRoot", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMasterSkillRoots") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", null) + .WithMany("RawChangeEvents") + .HasForeignKey("MiniGameDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterSpawnArea", "RawSpawnArea") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", "SpawnAreaId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawTargetDefinition") + .WithMany() + .HasForeignKey("TargetDefinitionId"); + + b.Navigation("RawSpawnArea"); + + b.Navigation("RawTargetDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawEntrance") + .WithMany() + .HasForeignKey("EntranceId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMiniGameDefinitions") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawTicketItem") + .WithMany() + .HasForeignKey("TicketItemId"); + + b.Navigation("RawEntrance"); + + b.Navigation("RawTicketItem"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameRankingEntry", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "RawCharacter") + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", "RawMiniGame") + .WithMany() + .HasForeignKey("MiniGameId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawCharacter"); + + b.Navigation("RawMiniGame"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameReward", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "RawItemReward") + .WithMany() + .HasForeignKey("ItemRewardId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", null) + .WithMany("RawRewards") + .HasForeignKey("MiniGameDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawRequiredKill") + .WithMany() + .HasForeignKey("RequiredKillId"); + + b.Navigation("RawItemReward"); + + b.Navigation("RawRequiredKill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameSpawnWave", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", null) + .WithMany("RawSpawnWaves") + .HasForeignKey("MiniGameDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameTerrainChange", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", null) + .WithMany("RawTerrainChanges") + .HasForeignKey("MiniGameChangeEventId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterAttribute", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttributeDefinition") + .WithMany() + .HasForeignKey("AttributeDefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawAttributes") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttributeDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawAttackSkill") + .WithMany() + .HasForeignKey("AttackSkillId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMonsters") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawMerchantStore") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "MerchantStoreId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttackSkill"); + + b.Navigation("RawMerchantStore"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinitionDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "MonsterDefinition") + .WithMany("JoinedDropItemGroups") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DropItemGroup"); + + b.Navigation("MonsterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterSpawnArea", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawGameMap") + .WithMany("RawMonsterSpawns") + .HasForeignKey("GameMapId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonsterDefinition") + .WithMany() + .HasForeignKey("MonsterDefinitionId"); + + b.Navigation("RawGameMap"); + + b.Navigation("RawMonsterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PlugInConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawPlugInConfigurations") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawBoost") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "BoostId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawCharacterPowerUpDefinitions") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", null) + .WithMany("RawPowerUpDefinitions") + .HasForeignKey("MagicEffectDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", null) + .WithMany("RawPowerUpDefinitionsPvp") + .HasForeignKey("MagicEffectDefinitionId1") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_PowerUpDefinition_MagicEffectDefinition_MagicEffectDefinit~1"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawBoost"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawQuests") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawQualifiedCharacter") + .WithMany() + .HasForeignKey("QualifiedCharacterId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawQuestGiver") + .WithMany() + .HasForeignKey("QuestGiverId"); + + b.Navigation("RawQualifiedCharacter"); + + b.Navigation("RawQuestGiver"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestItemRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "RawDropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItem") + .WithMany() + .HasForeignKey("ItemId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", null) + .WithMany("RawRequiredItems") + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawDropItemGroup"); + + b.Navigation("RawItem"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonster") + .WithMany() + .HasForeignKey("MonsterId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", null) + .WithMany("RawRequiredMonsterKills") + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawMonster"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirementState", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", null) + .WithMany("RawRequirementStates") + .HasForeignKey("CharacterQuestStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirement", "RawRequirement") + .WithMany() + .HasForeignKey("RequirementId"); + + b.Navigation("RawRequirement"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestReward", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttributeReward") + .WithMany() + .HasForeignKey("AttributeRewardId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", "RawItemReward") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestReward", "ItemRewardId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", null) + .WithMany("RawRewards") + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkillReward") + .WithMany() + .HasForeignKey("SkillRewardId"); + + b.Navigation("RawAttributeReward"); + + b.Navigation("RawItemReward"); + + b.Navigation("RawSkillReward"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AreaSkillSettings", "RawAreaSkillSettings") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "AreaSkillSettingsId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawElementalModifierTarget") + .WithMany() + .HasForeignKey("ElementalModifierTargetId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawSkills") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "RawMagicEffectDef") + .WithMany() + .HasForeignKey("MagicEffectDefId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", "RawMasterDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "MasterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAreaSkillSettings"); + + b.Navigation("RawElementalModifierTarget"); + + b.Navigation("RawMagicEffectDef"); + + b.Navigation("RawMasterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillCharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "Skill") + .WithMany("JoinedQualifiedCharacters") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CharacterClass"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboStep", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", null) + .WithMany("RawSteps") + .HasForeignKey("SkillComboDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkill") + .WithMany() + .HasForeignKey("SkillId"); + + b.Navigation("RawSkill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillEntry", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany("RawLearnedSkills") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkill") + .WithMany() + .HasForeignKey("SkillId"); + + b.Navigation("RawSkill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttribute", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany("RawAttributes") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany("RawAttributes") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.Navigation("RawDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttributeDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttribute") + .WithMany() + .HasForeignKey("AttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", null) + .WithMany("RawStatAttributes") + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.WarpInfo", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawWarpList") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawGate") + .WithMany() + .HasForeignKey("GateId"); + + b.Navigation("RawGate"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b => + { + b.Navigation("JoinedUnlockedCharacterClasses"); + + b.Navigation("RawAttributes"); + + b.Navigation("RawCharacters"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => + { + b.Navigation("RawEquippedItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b => + { + b.Navigation("RawAttackMachineZones"); + + b.Navigation("RawDefenseMachineZones"); + + b.Navigation("RawGateDefenseUpgrades"); + + b.Navigation("RawGateLifeUpgrades"); + + b.Navigation("RawNpcDefinitions"); + + b.Navigation("RawStateSchedule"); + + b.Navigation("RawStatueDefenseUpgrades"); + + b.Navigation("RawStatueLifeUpgrades"); + + b.Navigation("RawStatueRegenUpgrades"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b => + { + b.Navigation("RawGuilds"); + + b.Navigation("RawNpcStates"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b => + { + b.Navigation("JoinedDropItemGroups"); + + b.Navigation("RawAttributes"); + + b.Navigation("RawLearnedSkills"); + + b.Navigation("RawLetters"); + + b.Navigation("RawQuestStates"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b => + { + b.Navigation("RawAttributeCombinations"); + + b.Navigation("RawBaseAttributeValues"); + + b.Navigation("RawStatAttributes"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b => + { + b.Navigation("RawRequirementStates"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", b => + { + b.Navigation("RawEndpoints"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => + { + b.Navigation("JoinedPossibleItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b => + { + b.Navigation("RawDuelAreas"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b => + { + b.Navigation("RawAttributes"); + + b.Navigation("RawCharacterClasses"); + + b.Navigation("RawDropItemGroups"); + + b.Navigation("RawGlobalAttributeCombinations"); + + b.Navigation("RawGlobalBaseAttributeValues"); + + b.Navigation("RawItemLevelBonusTables"); + + b.Navigation("RawItemOptionCombinationBonuses"); + + b.Navigation("RawItemOptionTypes"); + + b.Navigation("RawItemOptions"); + + b.Navigation("RawItemSetGroups"); + + b.Navigation("RawItemSlotTypes"); + + b.Navigation("RawItems"); + + b.Navigation("RawJewelMixes"); + + b.Navigation("RawMagicEffects"); + + b.Navigation("RawMaps"); + + b.Navigation("RawMasterSkillRoots"); + + b.Navigation("RawMiniGameDefinitions"); + + b.Navigation("RawMonsters"); + + b.Navigation("RawPlugInConfigurations"); + + b.Navigation("RawSkills"); + + b.Navigation("RawWarpList"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b => + { + b.Navigation("JoinedDropItemGroups"); + + b.Navigation("RawCharacterPowerUpDefinitions"); + + b.Navigation("RawEnterGates"); + + b.Navigation("RawExitGates"); + + b.Navigation("RawMapRequirements"); + + b.Navigation("RawMonsterSpawns"); + + b.Navigation("RawTerrainVariants"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", b => + { + b.Navigation("JoinedMaps"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", b => + { + b.Navigation("RawEndpoints"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", b => + { + b.Navigation("RawMembers"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", b => + { + b.Navigation("RawLevelDependentOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", b => + { + b.Navigation("JoinedItemSetGroups"); + + b.Navigation("RawItemOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => + { + b.Navigation("JoinedVisibleOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", b => + { + b.Navigation("JoinedPossibleItems"); + + b.Navigation("JoinedRequiredItemOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", b => + { + b.Navigation("JoinedPossibleItemOptions"); + + b.Navigation("JoinedPossibleItemSetGroups"); + + b.Navigation("JoinedQualifiedCharacters"); + + b.Navigation("RawBasePowerUpAttributes"); + + b.Navigation("RawDropItems"); + + b.Navigation("RawRequirements"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", b => + { + b.Navigation("JoinedPossibleItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", b => + { + b.Navigation("RawBonusPerLevel"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", b => + { + b.Navigation("RawRequirements"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", b => + { + b.Navigation("RawPossibleOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", b => + { + b.Navigation("RawItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", b => + { + b.Navigation("RawItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", b => + { + b.Navigation("RawPowerUpDefinitions"); + + b.Navigation("RawPowerUpDefinitionsPvp"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", b => + { + b.Navigation("JoinedRequiredMasterSkills"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", b => + { + b.Navigation("RawTerrainChanges"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", b => + { + b.Navigation("RawChangeEvents"); + + b.Navigation("RawRewards"); + + b.Navigation("RawSpawnWaves"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", b => + { + b.Navigation("JoinedDropItemGroups"); + + b.Navigation("RawAttributes"); + + b.Navigation("RawBuffs"); + + b.Navigation("RawItemCraftings"); + + b.Navigation("RawQuests"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", b => + { + b.Navigation("RawRelatedValues"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", b => + { + b.Navigation("RawRequiredItems"); + + b.Navigation("RawRequiredMonsterKills"); + + b.Navigation("RawRewards"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", b => + { + b.Navigation("RawRequiredItems"); + + b.Navigation("RawResultItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", b => + { + b.Navigation("JoinedQualifiedCharacters"); + + b.Navigation("RawAttributeRelationships"); + + b.Navigation("RawConsumeRequirements"); + + b.Navigation("RawRequirements"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", b => + { + b.Navigation("RawSteps"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/EntityFramework/Migrations/20260929174938_AddGameMapTerrainVariants.cs b/src/Persistence/EntityFramework/Migrations/20260929174938_AddGameMapTerrainVariants.cs new file mode 100644 index 000000000..95814adbb --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20260929174938_AddGameMapTerrainVariants.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations +{ + /// + public partial class AddGameMapTerrainVariants : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "GameMapTerrainVariant", + schema: "config", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + GameMapDefinitionId = table.Column(type: "uuid", nullable: true), + Number = table.Column(type: "smallint", nullable: false), + Description = table.Column(type: "text", nullable: false), + TerrainData = table.Column(type: "bytea", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_GameMapTerrainVariant", x => x.Id); + table.ForeignKey( + name: "FK_GameMapTerrainVariant_GameMapDefinition_GameMapDefinitionId", + column: x => x.GameMapDefinitionId, + principalSchema: "config", + principalTable: "GameMapDefinition", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_GameMapTerrainVariant_GameMapDefinitionId", + schema: "config", + table: "GameMapTerrainVariant", + column: "GameMapDefinitionId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GameMapTerrainVariant", + schema: "config"); + } + } +} diff --git a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs index ce52f74e4..072755d81 100644 --- a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs +++ b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs @@ -1220,6 +1220,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LastBattleEnd") .HasColumnType("timestamp with time zone"); + b.Property("WarStart") + .HasColumnType("timestamp with time zone"); + b.HasKey("Id"); b.ToTable("CrywolfData", "data"); @@ -1640,6 +1643,32 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("GameMapDefinitionDropItemGroup", "config"); }); + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapTerrainVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("TerrainData") + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("GameMapDefinitionId"); + + b.ToTable("GameMapTerrainVariant", "config"); + }); + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", b => { b.Property("Id") @@ -4578,6 +4607,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("GameMapDefinition"); }); + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapTerrainVariant", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawTerrainVariants") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfigurationGameMapDefinition", b => { b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "GameMapDefinition") @@ -5794,6 +5831,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("RawMapRequirements"); b.Navigation("RawMonsterSpawns"); + + b.Navigation("RawTerrainVariants"); }); modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", b => diff --git a/src/Persistence/EntityFramework/Model/ExtendedTypeContext.Generated.cs b/src/Persistence/EntityFramework/Model/ExtendedTypeContext.Generated.cs index d379e5b4d..e2d64cf42 100644 --- a/src/Persistence/EntityFramework/Model/ExtendedTypeContext.Generated.cs +++ b/src/Persistence/EntityFramework/Model/ExtendedTypeContext.Generated.cs @@ -66,6 +66,7 @@ protected override void OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuild modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); + modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); @@ -184,6 +185,7 @@ protected override void OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuild modelBuilder.Entity().HasMany(entity => entity.RawPlugInConfigurations).WithOne().OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity().HasMany(entity => entity.RawMiniGameDefinitions).WithOne().OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity().HasOne(entity => entity.RawCastleSiegeConfiguration).WithOne().OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity().HasMany(entity => entity.RawTerrainVariants).WithOne().OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity().HasMany(entity => entity.RawMonsterSpawns).WithOne().OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity().HasMany(entity => entity.RawEnterGates).WithOne().OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity().HasOne(entity => entity.RawBattleZone).WithOne().OnDelete(DeleteBehavior.Cascade); diff --git a/src/Persistence/EntityFramework/Model/GameMapDefinition.Generated.cs b/src/Persistence/EntityFramework/Model/GameMapDefinition.Generated.cs index 7026da39a..690ae19d2 100644 --- a/src/Persistence/EntityFramework/Model/GameMapDefinition.Generated.cs +++ b/src/Persistence/EntityFramework/Model/GameMapDefinition.Generated.cs @@ -33,6 +33,15 @@ public GameMapDefinition() /// public Guid Id { get; set; } + /// + /// Gets the raw collection of . + /// + public ICollection RawTerrainVariants { get; } = new EntityFramework.List(); + + /// + [NotMapped] + public override ICollection TerrainVariants => base.TerrainVariants ??= new CollectionAdapter(this.RawTerrainVariants); + /// /// Gets the raw collection of . /// diff --git a/src/Persistence/EntityFramework/Model/GameMapTerrainVariant.Generated.cs b/src/Persistence/EntityFramework/Model/GameMapTerrainVariant.Generated.cs new file mode 100644 index 000000000..9b9bda624 --- /dev/null +++ b/src/Persistence/EntityFramework/Model/GameMapTerrainVariant.Generated.cs @@ -0,0 +1,65 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +//------------------------------------------------------------------------------ +// +// This source code was auto-generated by a roslyn code generator. +// +//------------------------------------------------------------------------------ + +// ReSharper disable All + +namespace MUnique.OpenMU.Persistence.EntityFramework.Model; + +using System.ComponentModel.DataAnnotations.Schema; +using MUnique.OpenMU.Persistence; + +/// +/// The Entity Framework Core implementation of . +/// +[Table(nameof(GameMapTerrainVariant), Schema = SchemaNames.Configuration)] +internal partial class GameMapTerrainVariant : MUnique.OpenMU.DataModel.Configuration.GameMapTerrainVariant, IIdentifiable +{ + + + /// + /// Gets or sets the identifier of this instance. + /// + public Guid Id { get; set; } + + /// + public override MUnique.OpenMU.DataModel.Configuration.GameMapTerrainVariant Clone(MUnique.OpenMU.DataModel.Configuration.GameConfiguration gameConfiguration) + { + var clone = new GameMapTerrainVariant(); + clone.AssignValuesOf(this, gameConfiguration); + return clone; + } + + /// + public override void AssignValuesOf(MUnique.OpenMU.DataModel.Configuration.GameMapTerrainVariant other, MUnique.OpenMU.DataModel.Configuration.GameConfiguration gameConfiguration) + { + base.AssignValuesOf(other, gameConfiguration); + this.Id = other.GetId(); + } + + /// + public override bool Equals(object obj) + { + var baseObject = obj as IIdentifiable; + if (baseObject != null) + { + return baseObject.Id == this.Id; + } + + return base.Equals(obj); + } + + /// + public override int GetHashCode() + { + return this.Id.GetHashCode(); + } + + +} diff --git a/src/Persistence/EntityFramework/Model/MapsterConfigurator.Generated.cs b/src/Persistence/EntityFramework/Model/MapsterConfigurator.Generated.cs index e8c512f39..614c2c363 100644 --- a/src/Persistence/EntityFramework/Model/MapsterConfigurator.Generated.cs +++ b/src/Persistence/EntityFramework/Model/MapsterConfigurator.Generated.cs @@ -177,6 +177,9 @@ public static void EnsureConfigured() Mapster.TypeAdapterConfig.GlobalSettings.NewConfig() .Include(); + Mapster.TypeAdapterConfig.GlobalSettings.NewConfig() + .Include(); + Mapster.TypeAdapterConfig.GlobalSettings.NewConfig() .Include(); diff --git a/src/Persistence/Initialization/TerrainUpdateHelper.cs b/src/Persistence/Initialization/TerrainUpdateHelper.cs index a2c44f396..60a44a05d 100644 --- a/src/Persistence/Initialization/TerrainUpdateHelper.cs +++ b/src/Persistence/Initialization/TerrainUpdateHelper.cs @@ -41,6 +41,24 @@ public static void UpdateTerrainFromResources(this GameMapDefinition gameMapDefi } } + /// + /// Reads a terrain file from the embedded resources. + /// + /// The name of the file, e.g. Terrain35_WAR.att. + /// The terrain data, or if the resource doesn't exist. + public static byte[]? ReadTerrainResource(string fileName) + { + var assembly = Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.Resources.{fileName}"); + if (stream is null) + { + return null; + } + + using var reader = new BinaryReader(stream); + return reader.ReadBytes(3 * ushort.MaxValue); + } + private static string GetTerrainFileName(this GameMapDefinition gameMapDefinition, string terrainVersionPrefix = "") { var assembly = Assembly.GetExecutingAssembly(); diff --git a/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs index 53e83cb82..b7eb1bd67 100644 --- a/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs +++ b/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs @@ -12,7 +12,7 @@ namespace MUnique.OpenMU.Persistence.Initialization.Updates; /// /// Adds the data of the crywolf event: the monsters of the army of Balgass and their spawns on the crywolf map. /// The statue and the altars of the fortress are always on the map now, as NPCs which can't be attacked, -/// and the fortress can be warped to. +/// the fortress can be warped to, and the map has the terrains of the occupation states. /// [PlugIn] [Display(Name = PlugInName, Description = PlugInDescription)] @@ -27,7 +27,7 @@ public class AddCrywolfEventUpdatePlugIn : UpdatePlugInBase /// /// The plug-in description. /// - internal const string PlugInDescription = "This update adds the monsters of the crywolf event and their spawns, lets the statue and the altars of the fortress always be on the map, and adds the warp entry of the fortress."; + internal const string PlugInDescription = "This update adds the monsters of the crywolf event and their spawns, lets the statue and the altars of the fortress always be on the map, adds the warp entry of the fortress and the terrains of the occupation states."; /// public override string Name => PlugInName; @@ -55,6 +55,7 @@ protected override ValueTask ApplyAsync(IContext context, GameConfiguration game initializer.ConfigureStatueAndAltars(); initializer.CreateSpawns(); initializer.CreateWarpEntry(); + initializer.CreateTerrainVariants(); return ValueTask.CompletedTask; } } diff --git a/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfInitializer.cs b/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfInitializer.cs index a02f14574..35a916695 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfInitializer.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Events/CrywolfInitializer.cs @@ -48,6 +48,7 @@ public override void Initialize() { this.CreateMonsters(); this.CreateSpawns(); + this.CreateTerrainVariants(); } /// @@ -120,6 +121,33 @@ internal void CreateSpawns() } } + /// + /// Adds the terrains of the crywolf map while it's occupied and during the war, if they don't exist yet. + /// Like the terrain files of the client, their numbers are the ones of the occupation states (1 and 2). + /// + internal void CreateTerrainVariants() + { + if (this.GetMap() is not { } map) + { + return; + } + + foreach (var (number, description, fileName) in new (short, string, string)[] { (1, "Occupied", "Terrain35_OCCUPIED.att"), (2, "War", "Terrain35_WAR.att") }) + { + if (map.TerrainVariants.Any(variant => variant.Number == number) + || TerrainUpdateHelper.ReadTerrainResource(fileName) is not { } terrainData) + { + continue; + } + + var variant = this.Context.CreateNew(); + variant.Number = number; + variant.Description = description; + variant.TerrainData = terrainData; + map.TerrainVariants.Add(variant); + } + } + /// /// Adds the warp entry of the crywolf fortress to existing data, if it doesn't exist yet. /// diff --git a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/CrywolfDataTest.cs b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/CrywolfDataTest.cs index ae936798f..6dcff1faa 100644 --- a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/CrywolfDataTest.cs +++ b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/CrywolfDataTest.cs @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.Persistence.Initialization.Tests; using Microsoft.Extensions.Logging.Abstractions; using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.Crywolf; using MUnique.OpenMU.Persistence.Initialization.Updates; using MUnique.OpenMU.Persistence.InMemory; @@ -52,6 +53,7 @@ public async Task UpdateAddsEventDataAsync() } gameConfiguration.WarpList.Remove(gameConfiguration.WarpList.Single(warp => warp.Index == 26)); + map.TerrainVariants.Clear(); var update = new AddCrywolfEventUpdatePlugIn(); await update.ApplyUpdateAsync(contextProvider.CreateNewContext(), gameConfiguration).ConfigureAwait(false); @@ -107,5 +109,24 @@ private static void AssertEventData(GameConfiguration gameConfiguration) var warp = gameConfiguration.WarpList.Single(warp => warp.Index == 26); Assert.That(warp.Gate?.Map, Is.SameAs(map)); Assert.That(warp.LevelRequirement, Is.EqualTo(190)); + + Assert.That(map.TerrainVariants.Select(variant => variant.Number), Is.EquivalentTo(new short[] { 1, 2 })); + AssertTerrainSwitch(map); + } + + /// + /// Asserts that switching to the terrain of an occupation state and back results in exactly the terrain of the respective file. + /// + private static void AssertTerrainSwitch(GameMapDefinition map) + { + var terrain = new GameMapTerrain(map.TerrainData); + foreach (var variant in map.TerrainVariants) + { + Assert.That(variant.TerrainData, Is.Not.Null.And.Not.EqualTo(map.TerrainData), $"Terrain variant {variant}"); + terrain.LoadTerrainData(variant.TerrainData); + Assert.That(terrain.AttributeMap, Is.EqualTo(new GameMapTerrain(variant.TerrainData).AttributeMap), $"Terrain variant {variant}"); + terrain.LoadTerrainData(map.TerrainData); + Assert.That(terrain.AttributeMap, Is.EqualTo(new GameMapTerrain(map.TerrainData).AttributeMap), $"Normal terrain after {variant}"); + } } } diff --git a/tests/MUnique.OpenMU.Tests/CrywolfAltarTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfAltarTest.cs index 07614e04c..4ba817b2a 100644 --- a/tests/MUnique.OpenMU.Tests/CrywolfAltarTest.cs +++ b/tests/MUnique.OpenMU.Tests/CrywolfAltarTest.cs @@ -91,27 +91,6 @@ public async Task ContractRequirementsAsync() Assert.That(altar.ContractCount, Is.Zero); } - /// - /// Tests that the safezone of the fortress is removed while it's not in peace, and restored afterward. - /// - [Test] - public void TerrainOfTheOccupation() - { - var terrainData = new byte[(256 * 256) + 3]; - Array.Fill(terrainData, (byte)TerrainAttributeType.Safezone, 3, terrainData.Length - 3); - var terrain = new GameMapTerrain(terrainData); - var (areaX, areaY, _, _) = CrywolfTerrain.SafezoneRemovedAreas[0]; - var (blockedX, blockedY, _, _) = CrywolfTerrain.BlockedAreas[0]; - - CrywolfTerrain.Apply(terrain, false); - Assert.That(terrain.SafezoneMap[areaX, areaY], Is.False); - Assert.That(terrain.WalkMap[blockedX, blockedY], Is.False); - - CrywolfTerrain.Apply(terrain, true); - Assert.That(terrain.SafezoneMap[areaX, areaY], Is.True); - Assert.That(terrain.WalkMap[blockedX, blockedY], Is.True); - } - private async ValueTask<(CrywolfAltar Altar, Player Player)> CreateAltarAsync(Point playerPosition) { var gameContext = GameContextTestHelper.CreateGameContext(); diff --git a/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs index a99b7212a..392e927d5 100644 --- a/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs +++ b/tests/MUnique.OpenMU.Tests/CrywolfContextTest.cs @@ -132,6 +132,46 @@ public async Task TakeOverResetsTheWarOfTheOtherServerAsync() Assert.That(data?.IsWarRunning, Is.False); } + /// + /// Tests that the other game servers don't stay in the war, when the game server which runs the event + /// stopped during the war, and that they fall back to the result of the last battle. + /// + [Test] + public async Task OverdueWarEndsOnTheOtherServersAsync() + { + var gameContext = (GameContext)GameContextTestHelper.CreateGameContext(); + await SaveDataAsync(gameContext, data => + { + data.IsWarRunning = true; + data.WarStart = DateTime.UtcNow - TimeSpan.FromHours(2); + data.IsOccupied = true; + data.LastBattleEnd = DateTime.UtcNow - TimeSpan.FromDays(3); + }).ConfigureAwait(false); + + var (context, _, _) = await CreateContextAsync(gameContext, serverId: 1).ConfigureAwait(false); + + Assert.That(context.Occupation, Is.EqualTo(CrywolfOccupationState.Occupied)); + } + + /// + /// Tests that the terrain of the map is replaced by its variant of the occupation state during the war, + /// and that the normal terrain is loaded again when the fortress is in peace. + /// + [Test] + public async Task WarUsesTheTerrainOfTheWarAsync() + { + var (context, _, _) = await CreateContextAsync().ConfigureAwait(false); + var map = context.Map!; + var blocked = new Point(100, 100); + var warTerrain = new byte[(256 * 256) + 3]; + warTerrain[3 + blocked.X + (blocked.Y << 8)] = (byte)TerrainAttributeType.Blocked; + map.Definition.TerrainVariants.Add(new Persistence.BasicModel.GameMapTerrainVariant { Number = (short)CrywolfOccupationState.War, TerrainData = warTerrain }); + Assert.That(map.Terrain.WalkMap[blocked.X, blocked.Y], Is.True, "precondition"); + + await ProceedToAsync(context, CrywolfState.Notify2).ConfigureAwait(false); + Assert.That(map.Terrain.WalkMap[blocked.X, blocked.Y], Is.False); + } + /// /// Tests that the benefits apply after the fortress has been defended, but not before the first battle. /// diff --git a/tests/MUnique.OpenMU.Tests/CrywolfPlugInTest.cs b/tests/MUnique.OpenMU.Tests/CrywolfPlugInTest.cs index 7b7af2f67..8e749d7cb 100644 --- a/tests/MUnique.OpenMU.Tests/CrywolfPlugInTest.cs +++ b/tests/MUnique.OpenMU.Tests/CrywolfPlugInTest.cs @@ -9,6 +9,7 @@ namespace MUnique.OpenMU.Tests; using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.GameLogic.Crywolf; +using MUnique.OpenMU.GameLogic.PlugIns; using ItemDefinition = MUnique.OpenMU.Persistence.BasicModel.ItemDefinition; /// @@ -59,6 +60,42 @@ public async Task ExperienceIsReducedWhileOccupiedAsync() Assert.That(player.Attributes[Stats.ExperienceRate], Is.EqualTo(normalRate * 0.5f)); } + /// + /// Tests that deactivating the plugin undoes its multipliers. + /// + [Test] + public async Task DeactivationUndoesTheMultipliersAsync() + { + var (plugIn, gameContext) = await CreatePlugInAsync(isOccupied: true, d => d.ExperiencePenaltyPercentage = 50, execute: false).ConfigureAwait(false); + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + await plugIn.PlayerStateChangedAsync(player, PlayerState.CharacterSelection, PlayerState.EnteredWorld).ConfigureAwait(false); + var normalRate = player.Attributes![Stats.ExperienceRate]; + await plugIn.ExecuteTaskAsync(gameContext).ConfigureAwait(false); + Assert.That(player.Attributes[Stats.ExperienceRate], Is.EqualTo(normalRate * 0.5f), "precondition"); + + gameContext.PlugInManager.RegisterPlugIn(); + gameContext.PlugInManager.DeactivatePlugIn(); + + Assert.That(player.Attributes[Stats.ExperienceRate], Is.EqualTo(normalRate)); + } + + /// + /// Tests that players, which are already in the game when the plugin starts, get the multiplier, too. + /// + [Test] + public async Task PlayersInTheGameGetTheMultiplierAsync() + { + var (plugIn, gameContext) = await CreatePlugInAsync(isOccupied: true, d => d.ExperiencePenaltyPercentage = 50, execute: false).ConfigureAwait(false); + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + await player.PlayerState.TryAdvanceToAsync(PlayerState.EnteredWorld).ConfigureAwait(false); + await gameContext.AddPlayerAsync(player).ConfigureAwait(false); + var normalRate = player.Attributes![Stats.ExperienceRate]; + + await plugIn.ExecuteTaskAsync(gameContext).ConfigureAwait(false); + + Assert.That(player.Attributes[Stats.ExperienceRate], Is.EqualTo(normalRate * 0.5f)); + } + private static bool IsDropCancelled(CrywolfPlugIn plugIn, Player killer, byte group, short number) { var item = new MUnique.OpenMU.Persistence.BasicModel.Item { Definition = new ItemDefinition { Group = group, Number = number } }; diff --git a/tests/MUnique.OpenMU.Tests/GameMapTerrainTests.cs b/tests/MUnique.OpenMU.Tests/GameMapTerrainTests.cs index 905af4897..23d216384 100644 --- a/tests/MUnique.OpenMU.Tests/GameMapTerrainTests.cs +++ b/tests/MUnique.OpenMU.Tests/GameMapTerrainTests.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -65,6 +65,36 @@ public void AnyWalkableCoordinateIsNullWhenNothingIsWalkable() /// The coordinate to mark as safezone, if any. /// The coordinate to mark as walkable but outside the safezone, if any. /// The terrain data, including its three byte header. + /// + /// Tests that loading other terrain data replaces the whole terrain, e.g. to switch to the terrain of an event state + /// and back, and that the grids stay the same instances, which are referenced e.g. by the path finder. + /// + [Test] + public void LoadTerrainDataReplacesTheWholeTerrain() + { + var peace = CreateTerrainData(safezoneAt: (10, 10), walkableAt: (5, 5)); + var war = CreateTerrainData(safezoneAt: null, walkableAt: (6, 6)); + var terrain = new GameMapTerrain(peace); + var aiGrid = terrain.AIgrid; + + terrain.LoadTerrainData(war); + AssertSameTerrain(terrain, new GameMapTerrain(war)); + Assert.That(terrain.AIgrid, Is.SameAs(aiGrid)); + Assert.That(terrain.AnyWalkableCoordinate, Is.EqualTo(new Pathfinding.Point(6, 6))); + + terrain.LoadTerrainData(peace); + AssertSameTerrain(terrain, new GameMapTerrain(peace)); + Assert.That(terrain.AnyWalkableCoordinate, Is.EqualTo(new Pathfinding.Point(10, 10))); + } + + private static void AssertSameTerrain(GameMapTerrain actual, GameMapTerrain expected) + { + Assert.That(actual.AttributeMap, Is.EqualTo(expected.AttributeMap)); + Assert.That(actual.WalkMap, Is.EqualTo(expected.WalkMap)); + Assert.That(actual.SafezoneMap, Is.EqualTo(expected.SafezoneMap)); + Assert.That(actual.AIgrid, Is.EqualTo(expected.AIgrid)); + } + private static byte[] CreateTerrainData((byte X, byte Y)? safezoneAt, (byte X, byte Y)? walkableAt) { var data = new byte[ushort.MaxValue + 3]; From 372bcf6e08b4cd244228f14bc696d95dc62e0e4b Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:46:54 -0300 Subject: [PATCH 21/23] fix(crywolf): renumber the update to 122 The imperial guardian event (#992) takes 121. CreatedAt is the actual date of the renumbering, like CODING_RULES.md asks. Co-Authored-By: Claude Opus 5.5 --- .../Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs | 2 +- src/Persistence/Initialization/Updates/UpdateVersion.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs index b7eb1bd67..a4991f81e 100644 --- a/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs +++ b/src/Persistence/Initialization/Updates/AddCrywolfEventUpdatePlugIn.cs @@ -45,7 +45,7 @@ public class AddCrywolfEventUpdatePlugIn : UpdatePlugInBase public override bool IsMandatory => true; /// - public override DateTime CreatedAt => new(2026, 09, 28, 0, 0, 0, DateTimeKind.Utc); + public override DateTime CreatedAt => new(2026, 09, 29, 0, 0, 0, DateTimeKind.Utc); /// protected override ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs index c2e02a1b3..577d6ab63 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -613,5 +613,5 @@ public enum UpdateVersion /// /// The version of the . /// - AddCrywolfEvent = 121, + AddCrywolfEvent = 122, } From 26310b46a7bc5f8094588c525703e897cc8e71c9 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:46:54 -0300 Subject: [PATCH 22/23] refactor(crywolf): configure the jewels of the penalty 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 --- .../Crywolf/CrywolfEventDefinition.cs | 13 +++++++ .../Crywolf/CrywolfItemIdentifier.cs | 39 +++++++++++++++++++ .../Crywolf/CrywolfMonsterIntelligence.cs | 4 ++ src/GameLogic/Crywolf/CrywolfPlugIn.cs | 15 +------ 4 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 src/GameLogic/Crywolf/CrywolfItemIdentifier.cs diff --git a/src/GameLogic/Crywolf/CrywolfEventDefinition.cs b/src/GameLogic/Crywolf/CrywolfEventDefinition.cs index 3c1f5cd3b..8e55fe908 100644 --- a/src/GameLogic/Crywolf/CrywolfEventDefinition.cs +++ b/src/GameLogic/Crywolf/CrywolfEventDefinition.cs @@ -332,6 +332,19 @@ public class CrywolfEventDefinition /// public int JewelDropPenaltyPercentage { get; set; } = 70; + /// + /// Gets or sets the jewels which drop less often while the penalties apply. The defaults are the ones of the original game. + /// + public IList PenaltyJewels { get; set; } = new List + { + new() { Group = 14, Number = 13 }, // Jewel of Bless + new() { Group = 14, Number = 14 }, // Jewel of Soul + new() { Group = 14, Number = 16 }, // Jewel of Life + new() { Group = 14, Number = 22 }, // Jewel of Creation + new() { Group = 12, Number = 15 }, // Jewel of Chaos + new() { Group = 14, Number = 31 }, // Jewel of Guardian + }; + /// /// Gets or sets the experience of killed monsters in percent of the normal experience, while the penalties apply. /// diff --git a/src/GameLogic/Crywolf/CrywolfItemIdentifier.cs b/src/GameLogic/Crywolf/CrywolfItemIdentifier.cs new file mode 100644 index 000000000..48f2b813f --- /dev/null +++ b/src/GameLogic/Crywolf/CrywolfItemIdentifier.cs @@ -0,0 +1,39 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Crywolf; + +using MUnique.OpenMU.DataModel.Configuration.Items; + +/// +/// Identifies an item definition by its group and number, e.g. a jewel which drops less often while the crywolf fortress is occupied. +/// +public class CrywolfItemIdentifier +{ + /// + /// Gets or sets the group of the item. + /// + public byte Group { get; set; } + + /// + /// Gets or sets the number of the item. + /// + public short Number { get; set; } + + /// + /// Determines whether the item identifier matches the specified item definition. + /// + /// The item definition. + /// true, if it matches; otherwise, false. + public bool Matches(ItemDefinition definition) + { + return definition.Group == this.Group && definition.Number == this.Number; + } + + /// + public override string ToString() + { + return $"{this.Group},{this.Number}"; + } +} diff --git a/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs b/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs index d56cc05e4..d9f094beb 100644 --- a/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs +++ b/src/GameLogic/Crywolf/CrywolfMonsterIntelligence.cs @@ -29,6 +29,10 @@ public sealed class CrywolfMonsterIntelligence : INpcIntelligence, IDisposable /// /// The number of the magic effect of a stun. /// + /// + /// It isn't part of the configuration, because the client shows the stun by this number. The other stuns of + /// the game logic use the same number (e.g. ). + /// internal const short StunnedMagicEffectNumber = 61; /// diff --git a/src/GameLogic/Crywolf/CrywolfPlugIn.cs b/src/GameLogic/Crywolf/CrywolfPlugIn.cs index 051567c11..fa1645a83 100644 --- a/src/GameLogic/Crywolf/CrywolfPlugIn.cs +++ b/src/GameLogic/Crywolf/CrywolfPlugIn.cs @@ -34,19 +34,6 @@ namespace MUnique.OpenMU.GameLogic.Crywolf; public sealed class CrywolfPlugIn : IFeaturePlugIn, IPeriodicTaskPlugIn, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration, IObjectAddedToMapPlugIn, IObjectRemovedFromMapPlugIn, IPlayerStateChangedPlugIn, IMonsterItemDropPlugIn, IDisposable { - /// - /// The jewels which drop less often while the penalties apply, like in the original game. - /// - private static readonly HashSet<(byte Group, short Number)> PenaltyJewels = new() - { - (14, 13), // Jewel of Bless - (14, 14), // Jewel of Soul - (14, 16), // Jewel of Life - (14, 22), // Jewel of Creation - (12, 15), // Jewel of Chaos - (14, 31), // Jewel of Guardian - }; - private readonly ConcurrentDictionary _contexts = new(); private readonly ConcurrentDictionary _runningTicks = new(); @@ -209,9 +196,9 @@ public ValueTask PlayerStateChangedAsync(Player player, State previousState, Sta public void ItemDropping(AttackableNpcBase monster, Player killer, Item item, CancelEventArgs eventArgs) { if (item.Definition is { } definition - && PenaltyJewels.Contains((definition.Group, definition.Number)) && this._contexts.TryGetValue(killer.GameContext, out var context) && context.ArePenaltiesApplied + && context.Definition.PenaltyJewels.Any(jewel => jewel.Matches(definition)) && Rand.NextInt(0, 100) >= context.Definition.JewelDropPenaltyPercentage) { eventArgs.Cancel = true; From f3f288347be9dbe2cec550b23827af8e97024bc6 Mon Sep 17 00:00:00 2001 From: apraxico <80994201+apraxico@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:46:54 -0300 Subject: [PATCH 23/23] docs(crywolf): mark the crywolf packet handlers as done Co-Authored-By: Claude Opus 5.5 --- docs/Progress.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Progress.md b/docs/Progress.md index 8b06a2175..025f299a8 100644 --- a/docs/Progress.md +++ b/docs/Progress.md @@ -83,7 +83,7 @@ complexity and effort). Complexity 0 means we wont implement it. | *LoggedIn* | 0xB8 | 100% | 1 | | | RequestGuildMarkCastleOwner | 0xB9 | 0% | 1 | | | *JewelMix* | 0xBC | 100% | 4 | | -| CrywolfGroup | 0xBD | 0% | 10 | | +| *CrywolfGroup* | 0xBD | 100% | 10 | | | GuildAssignStatus | 0xBE | 0% | 1 | | | FriendListRequest | 0xC0 | 0% | 0 | Not needed, friend list is sent automatically | | *FriendAdd* | 0xC1 | 100% | 2 | |