diff --git a/Maple2.Database/Model/CharacterConfig.cs b/Maple2.Database/Model/CharacterConfig.cs index ae6094115..ac63edde6 100644 --- a/Maple2.Database/Model/CharacterConfig.cs +++ b/Maple2.Database/Model/CharacterConfig.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Maple2.Database.Extensions; +using Maple2.Database.Extensions; using Maple2.Model.Enum; using Maple2.Model.Game; using Microsoft.EntityFrameworkCore; @@ -23,8 +19,7 @@ internal class CharacterConfig { public IList? FavoriteStickers { get; set; } public IList? FavoriteDesigners { get; set; } public IDictionary? Lapenshards { get; set; } - public long DeathTick { get; set; } - public int DeathCount { get; set; } + public int InstantRevivalCount { get; set; } public IDictionary? GatheringCounts { get; set; } public IDictionary? GuideRecords { get; set; } public int ExplorationProgress { get; set; } diff --git a/Maple2.Database/Storage/Game/GameStorage.ServerInfo.cs b/Maple2.Database/Storage/Game/GameStorage.ServerInfo.cs index 3ca5cbe4c..05bb62099 100644 --- a/Maple2.Database/Storage/Game/GameStorage.ServerInfo.cs +++ b/Maple2.Database/Storage/Game/GameStorage.ServerInfo.cs @@ -32,6 +32,7 @@ public void DailyReset() { Context.Database.ExecuteSqlRaw("UPDATE `account` SET `PrestigeLevelsGained` = DEFAULT"); Context.Database.ExecuteSqlRaw("UPDATE `account` SET `PremiumRewardsClaimed` = DEFAULT"); Context.Database.ExecuteSqlRaw("UPDATE `character-config` SET `GatheringCounts` = DEFAULT"); + Context.Database.ExecuteSqlRaw("UPDATE `character-config` SET `InstantRevivalCount` = 0"); Context.Database.ExecuteSqlRaw("UPDATE `nurturing` SET `PlayedBy` = '[]'"); Context.Database.ExecuteSqlRaw("UPDATE `home` SET `DecorationRewardTimestamp` = 0"); // TODO: Death counter diff --git a/Maple2.Database/Storage/Game/GameStorage.User.cs b/Maple2.Database/Storage/Game/GameStorage.User.cs index 2650c1338..ca4828ea2 100644 --- a/Maple2.Database/Storage/Game/GameStorage.User.cs +++ b/Maple2.Database/Storage/Game/GameStorage.User.cs @@ -310,11 +310,11 @@ public bool SaveCharacter(Character character) { } public (IList? KeyBinds, IList? HotBars, List?, List?, List? FavoriteStickers, List? FavoriteDesigners, - IDictionary? Lapenshards, long DeathTick, int DeathCount, int ExplorationProgress, IDictionary?, + IDictionary? Lapenshards, int InstantRevivalCount, int ExplorationProgress, IDictionary?, IDictionary?, SkillPoint? SkillPoint, IDictionary? GatheringCounts, IDictionary? GuideRecords, SkillBook?) LoadCharacterConfig(long characterId) { CharacterConfig? config = Context.CharacterConfig.Find(characterId); if (config == null) { - return (null, null, null, null, null, null, null, 0, 0, 0, null, null, null, null, null, null); + return (null, null, null, null, null, null, null, 0, 0, null, null, null, null, null, null); } SkillBook? skillBook = config.SkillBook == null ? null : new SkillBook { @@ -344,8 +344,7 @@ public bool SaveCharacter(Character character) { config.FavoriteStickers?.Select(stickers => stickers).ToList(), config.FavoriteDesigners?.Select(designer => designer).ToList(), config.Lapenshards, - config.DeathTick, - config.DeathCount, + config.InstantRevivalCount, config.ExplorationProgress, config.StatPoints, config.StatAllocation, @@ -365,9 +364,7 @@ public bool SaveCharacterConfig( IList favoriteStickers, IList favoriteDesigners, IDictionary lapenshards, - IList skillCooldowns, - long deathTick, - int deathCount, + int instantRevivalCount, int explorationProgress, StatAttributes.PointAllocation allocation, StatAttributes.PointSources statSources, @@ -389,8 +386,7 @@ public bool SaveCharacterConfig( config.FavoriteStickers = favoriteStickers; config.FavoriteDesigners = favoriteDesigners; config.Lapenshards = lapenshards; - config.DeathTick = deathTick; - config.DeathCount = deathCount; + config.InstantRevivalCount = instantRevivalCount; config.ExplorationProgress = explorationProgress; config.StatAllocation = allocation.Attributes.ToDictionary( attribute => attribute, diff --git a/Maple2.File.Ingest/Maple2.File.Ingest.csproj b/Maple2.File.Ingest/Maple2.File.Ingest.csproj index 6d3d7fb3a..40f8c88f0 100644 --- a/Maple2.File.Ingest/Maple2.File.Ingest.csproj +++ b/Maple2.File.Ingest/Maple2.File.Ingest.csproj @@ -19,7 +19,7 @@ - + diff --git a/Maple2.File.Ingest/Mapper/MapMapper.cs b/Maple2.File.Ingest/Mapper/MapMapper.cs index f2b85bda1..476975e15 100644 --- a/Maple2.File.Ingest/Mapper/MapMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapMapper.cs @@ -52,7 +52,7 @@ protected override IEnumerable Map() { TutorialType: data.property.tutorialType, RevivalReturnId: data.property.revivalreturnid, EnterReturnId: data.property.enterreturnid, - AutoRevivalType: data.property.autoRevivalType, + AutoRevivalType: (AutoReviveType) data.property.autoRevivalType, AutoRevivalTime: data.property.autoRevivalTime, InfiniteMeretRevival: data.property.infinityMeratRevival, NoRevivalHere: data.property.doNotRevivalHere, diff --git a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs index 5a3b84f27..57f9c11f9 100644 --- a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs +++ b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs @@ -148,6 +148,7 @@ private Dictionary> MergeNpcScript Meso: ParseToIntKeyValuePair(scriptCondition.meso), Level: ParseToIntKeyValuePair(scriptCondition.level), AchieveCompleted: ParseToIntKeyValuePair(scriptCondition.achieve_complete), + DeathPenalty: scriptCondition.panelty == 1, InGuild: scriptCondition.guild )); } @@ -210,7 +211,8 @@ private Dictionary> MergeQuestScri Meso: ParseToIntKeyValuePair(scriptCondition.meso), Level: ParseToIntKeyValuePair(scriptCondition.level), AchieveCompleted: ParseToIntKeyValuePair(scriptCondition.achieve_complete), - InGuild: scriptCondition.guild + InGuild: scriptCondition.guild, + DeathPenalty: scriptCondition.panelty == 1 )); } results.Add(questId, scriptConditions); @@ -391,7 +393,8 @@ private JobConditionTable ParseJobCondition() { ChangeToJobCode: (JobCode) jobCondition.jobCode, MapId: jobCondition.map, MoveMapId: jobCondition.moveFieldID, - MovePortalId: jobCondition.movePortalID + MovePortalId: jobCondition.movePortalID, + DeathPenalty: jobCondition.panelty )); } diff --git a/Maple2.File.Ingest/MapperExtensions.cs b/Maple2.File.Ingest/MapperExtensions.cs index c85875d2c..292d1aabe 100644 --- a/Maple2.File.Ingest/MapperExtensions.cs +++ b/Maple2.File.Ingest/MapperExtensions.cs @@ -327,6 +327,7 @@ public static BeginCondition Convert(this Maple2.File.Parser.Xml.Skill.BeginCond CooldownTime: beginCondition.cooldownTime, OnlyShadowWorld: beginCondition.onlyShadowWorld || beginCondition.isShadowWorld, OnlyFlyableMap: beginCondition.onlyFlyableMap, + AllowDead: beginCondition.allowDeadState, Weapon: beginCondition.weapon.Select(weapon => new BeginConditionWeapon( new ItemType(1, (byte) weapon.lh), new ItemType(1, (byte) weapon.rh))).ToArray(), diff --git a/Maple2.Model/Enum/Death.cs b/Maple2.Model/Enum/Death.cs new file mode 100644 index 000000000..2f4eee71e --- /dev/null +++ b/Maple2.Model/Enum/Death.cs @@ -0,0 +1,7 @@ +namespace Maple2.Model.Enum; + +public enum DeathState : short { + Alive = 0, + FirstDeath = 1, + Metal = 2, +} diff --git a/Maple2.Model/Enum/Map.cs b/Maple2.Model/Enum/Map.cs index 7f316a4d0..92cf78235 100644 --- a/Maple2.Model/Enum/Map.cs +++ b/Maple2.Model/Enum/Map.cs @@ -47,3 +47,9 @@ public enum Continent { Kritias = 105, ShadowWorld = 202, } + +public enum AutoReviveType { + None = 0, + Trigger = 1, + Countdown = 2, // Only map 65000003 - Treasure Island. Uses autoRevivalTime +} diff --git a/Maple2.Model/Game/User/Character.cs b/Maple2.Model/Game/User/Character.cs index 6080c8b34..a99d2201d 100644 --- a/Maple2.Model/Game/User/Character.cs +++ b/Maple2.Model/Game/User/Character.cs @@ -37,7 +37,6 @@ public class Character { public long StorageCooldown; public long DoctorCooldown; - public int ReviveMapId; public int ReturnMapId; public Vector3 ReturnPosition; public string Picture = string.Empty; @@ -50,5 +49,8 @@ public class Character { public AchievementInfo AchievementInfo; public MarriageInfo MarriageInfo; public readonly Dictionary DungeonEnterLimits = []; + public short DeathCount; + public long DeathTick; + public DeathState DeathState; public long PremiumTime; } diff --git a/Maple2.Model/Game/User/IPlayerInfo.cs b/Maple2.Model/Game/User/IPlayerInfo.cs index 127fea04f..2d0a871bf 100644 --- a/Maple2.Model/Game/User/IPlayerInfo.cs +++ b/Maple2.Model/Game/User/IPlayerInfo.cs @@ -18,6 +18,7 @@ public interface IPlayerInfo { // Health public long CurrentHp { get; set; } public long TotalHp { get; set; } + public DeathState DeathState { get; set; } // Location public int MapId { get; set; } public short Channel { get; set; } diff --git a/Maple2.Model/Game/User/PlayerInfo.cs b/Maple2.Model/Game/User/PlayerInfo.cs index 308a8c39c..323b4240e 100644 --- a/Maple2.Model/Game/User/PlayerInfo.cs +++ b/Maple2.Model/Game/User/PlayerInfo.cs @@ -62,9 +62,9 @@ public void WriteTo(IByteWriter writer) { writer.WriteShort(Channel); writer.WriteInt((int) Job.Code()); writer.Write(Job); - writer.WriteInt((int) CurrentHp); writer.WriteInt((int) TotalHp); - writer.WriteShort(); + writer.WriteInt((int) CurrentHp); + writer.Write(DeathState); writer.WriteLong(); writer.WriteLong(); // Home Storage Access Time writer.WriteLong(); // Home Doctor Access Time @@ -123,6 +123,7 @@ public class CharacterInfo { // Health public long CurrentHp { get; set; } public long TotalHp { get; set; } + public DeathState DeathState { get; set; } // Location public int MapId { get; set; } public short Channel { get; set; } diff --git a/Maple2.Model/Metadata/BeginCondition.cs b/Maple2.Model/Metadata/BeginCondition.cs index a8315ab89..e6a212a62 100644 --- a/Maple2.Model/Metadata/BeginCondition.cs +++ b/Maple2.Model/Metadata/BeginCondition.cs @@ -13,6 +13,7 @@ public record BeginCondition( float CooldownTime, bool OnlyShadowWorld, bool OnlyFlyableMap, + bool AllowDead, IReadOnlyDictionary Stat, BeginConditionWeapon[]? Weapon, BeginConditionTarget? Target, diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index 1526623a3..a85542bd7 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -928,6 +928,8 @@ public static class Constant { public const int NextStateTriggerDefaultTick = 100; public const int UserRevivalPaneltyTick = 3600000; public const int UserRevivalPaneltyMinLevel = 10; + public const int maxDeadCount = 3; + public const byte hitPerDeadCount = 5; public const int FishFightingProp = 3000; public const float NpcLastSightRadius = 1800; diff --git a/Maple2.Model/Metadata/MapMetadata.cs b/Maple2.Model/Metadata/MapMetadata.cs index 5e93779ae..a99d3123b 100644 --- a/Maple2.Model/Metadata/MapMetadata.cs +++ b/Maple2.Model/Metadata/MapMetadata.cs @@ -49,7 +49,7 @@ public record MapMetadataProperty( int TutorialType, int RevivalReturnId, int EnterReturnId, - int AutoRevivalType, + AutoReviveType AutoRevivalType, int AutoRevivalTime, bool InfiniteMeretRevival, bool NoRevivalHere, diff --git a/Maple2.Model/Metadata/ServerTable/JobConditionTable.cs b/Maple2.Model/Metadata/ServerTable/JobConditionTable.cs index 370d6db86..60ece39d9 100644 --- a/Maple2.Model/Metadata/ServerTable/JobConditionTable.cs +++ b/Maple2.Model/Metadata/ServerTable/JobConditionTable.cs @@ -26,4 +26,5 @@ public record JobConditionMetadata( JobCode ChangeToJobCode, int MapId, int MoveMapId, + bool DeathPenalty, int MovePortalId); diff --git a/Maple2.Model/Metadata/ServerTable/ScriptConditionTable.cs b/Maple2.Model/Metadata/ServerTable/ScriptConditionTable.cs index 77b53d6e3..d02288de5 100644 --- a/Maple2.Model/Metadata/ServerTable/ScriptConditionTable.cs +++ b/Maple2.Model/Metadata/ServerTable/ScriptConditionTable.cs @@ -21,6 +21,7 @@ public record ScriptConditionMetadata( KeyValuePair Meso, KeyValuePair Level, KeyValuePair AchieveCompleted, + bool DeathPenalty, bool InGuild) { public record MaidData( diff --git a/Maple2.Server.Core/Sync/PlayerInfoUpdateEvent.cs b/Maple2.Server.Core/Sync/PlayerInfoUpdateEvent.cs index cfeb6f814..25b53f0c2 100644 --- a/Maple2.Server.Core/Sync/PlayerInfoUpdateEvent.cs +++ b/Maple2.Server.Core/Sync/PlayerInfoUpdateEvent.cs @@ -19,12 +19,13 @@ public enum UpdateField { PremiumTime = 512, Clubs = 1024, Dungeon = 2048, + Death = 4096, // Presets Buddy = Profile | Job | Level | Map | Channel | Home | Trophy, Guild = Profile | Job | Level | GearScore | Map | Channel | Home | Trophy | Clubs, Club = Profile | Job | Level | GearScore | Map | Channel | Home | Trophy | Clubs, - Party = Profile | Job | Level | GearScore | Health | Map | Channel | Home | Clubs | Dungeon, + Party = Profile | Job | Level | GearScore | Health | Map | Channel | Home | Clubs | Dungeon | Death, GroupChat = Profile | Job | Level | Map | Channel | Home | Clubs, Marriage = Profile, All = int.MaxValue, @@ -77,6 +78,9 @@ public PlayerInfoUpdateEvent(IPlayerInfo player, PlayerUpdateRequest request) { Type |= UpdateField.Health; } } + if (request.HasDeathState && player.DeathState != (DeathState) request.DeathState) { + Type |= UpdateField.Death; + } if (request.Home != null) { if (player.PlotMapId != request.Home.MapId) { Type |= UpdateField.Home; @@ -106,13 +110,13 @@ public PlayerInfoUpdateEvent(IPlayerInfo player, PlayerUpdateRequest request) { } } - if (request.Clubs != null) { + if (request.Clubs.Count > 0) { if (player.ClubIds != request.Clubs.Select(club => club.Id).ToList()) { Type |= UpdateField.Clubs; } } - if (request.DungeonEnterLimits != null) { + if (request.DungeonEnterLimits.Count > 0) { if (player.DungeonEnterLimits != request.DungeonEnterLimits.Select(dungeon => new KeyValuePair(dungeon.DungeonId, (DungeonEnterLimit) dungeon.Limit)).ToDictionary()) { Type |= UpdateField.Dungeon; } diff --git a/Maple2.Server.Core/Sync/PlayerInfoUpdateExtensions.cs b/Maple2.Server.Core/Sync/PlayerInfoUpdateExtensions.cs index 221a374e2..89853ebe9 100644 --- a/Maple2.Server.Core/Sync/PlayerInfoUpdateExtensions.cs +++ b/Maple2.Server.Core/Sync/PlayerInfoUpdateExtensions.cs @@ -48,6 +48,9 @@ public static void Update(this IPlayerInfo info, PlayerInfoUpdateEvent update) { info.CurrentHp = update.Request.Health.CurrentHp; info.TotalHp = update.Request.Health.TotalHp; } + if (update.Type.HasFlag(UpdateField.Death) && update.Request.HasDeathState) { + info.DeathState = (DeathState) update.Request.DeathState; + } if (update.Type.HasFlag(UpdateField.Home) && update.Request.Home != null) { info.HomeName = update.Request.Home.Name; info.PlotMapId = update.Request.Home.MapId; @@ -102,6 +105,9 @@ public static void Update(this IPlayerInfo self, UpdateField type, IPlayerInfo o self.CurrentHp = other.CurrentHp; self.TotalHp = other.TotalHp; } + if (type.HasFlag(UpdateField.Death)) { + self.DeathState = other.DeathState; + } if (type.HasFlag(UpdateField.Home)) { self.HomeName = other.HomeName; self.PlotMapId = other.PlotMapId; @@ -156,6 +162,9 @@ public static void SetFields(this PlayerUpdateRequest request, UpdateField type, TotalHp = info.TotalHp, }; } + if (type.HasFlag(UpdateField.Death)) { + request.DeathState = (int) info.DeathState; + } if (type.HasFlag(UpdateField.Home)) { request.Home = new HomeUpdate { Name = info.HomeName, diff --git a/Maple2.Server.Core/proto/sync.proto b/Maple2.Server.Core/proto/sync.proto index a581d2caf..9a10fba47 100644 --- a/Maple2.Server.Core/proto/sync.proto +++ b/Maple2.Server.Core/proto/sync.proto @@ -65,9 +65,10 @@ message PlayerUpdateRequest { optional TrophyUpdate trophy = 19; repeated ClubUpdate clubs = 20; repeated DungeonEnterLimitUpdate dungeon_enter_limits = 21; + optional int32 death_state = 22; // Batch updates in a queue rather than immediately forwarding. - bool async = 22; + bool async = 23; } message PlayerUpdateResponse { @@ -100,4 +101,5 @@ message PlayerInfoResponse { TrophyUpdate trophy = 19; repeated ClubUpdate clubs = 20; repeated DungeonEnterLimitUpdate dungeon_enter_limits = 21; + int32 death_state = 22; } diff --git a/Maple2.Server.Core/proto/world/world.proto b/Maple2.Server.Core/proto/world/world.proto index 98938f254..f12e29fc1 100644 --- a/Maple2.Server.Core/proto/world/world.proto +++ b/Maple2.Server.Core/proto/world/world.proto @@ -528,12 +528,13 @@ message PlayerConfigRequest { message Save { repeated BuffInfo buffs = 1; repeated SkillCooldownInfo skill_cooldowns = 2; + DeathInfo death_info = 3; } message Get { } int64 requester_id = 1; - oneof buff { + oneof player_config { Save save = 2; Get get = 3; } @@ -542,6 +543,7 @@ message PlayerConfigRequest { message PlayerConfigResponse { repeated BuffInfo buffs = 1; repeated SkillCooldownInfo skill_cooldowns = 2; + DeathInfo death_info = 3; } message BuffInfo { @@ -561,3 +563,9 @@ message SkillCooldownInfo { int64 stop_time = 5; int32 charges = 6; } + +message DeathInfo { + int32 count = 1; + int32 ms_remaining = 2; + int64 stop_time = 3; +} diff --git a/Maple2.Server.Game/Commands/KillCommand.cs b/Maple2.Server.Game/Commands/KillCommand.cs index aa4d4b097..23d4b62ff 100644 --- a/Maple2.Server.Game/Commands/KillCommand.cs +++ b/Maple2.Server.Game/Commands/KillCommand.cs @@ -14,11 +14,12 @@ namespace Maple2.Server.Game.Commands; public class KillCommand : Command { private const string NAME = "kill"; - private const string DESCRIPTION = "Kill NPCs and/or Mobs."; + private const string DESCRIPTION = "Kill NPCs/Mobs/Players"; public KillCommand(GameSession session) : base(NAME, DESCRIPTION) { AddCommand(new KillAllNpcCommand(session)); AddCommand(new KillNearNpcCommand(session)); + AddCommand(new KillPlayerCommand(session)); } private class KillAllNpcCommand : Command { @@ -104,6 +105,41 @@ private void Handle(InvocationContext ctx, int distance) { } } + private class KillPlayerCommand : Command { + private readonly GameSession session; + + public KillPlayerCommand(GameSession session) : base("player", "Kill specific player in field.") { + this.session = session; + + var name = new Argument("name", "Name of player."); + + AddArgument(name); + this.SetHandler(Handle, name); + + } + + private void Handle(InvocationContext ctx, string name) { + if (string.IsNullOrEmpty(name)) { + ctx.Console.Out.WriteLine("Name cannot be empty."); + return; + } + FieldPlayer? player = session.Field.GetPlayers().Values + .FirstOrDefault(player => string.Equals(player.Value.Character.Name, name, StringComparison.OrdinalIgnoreCase)); + + if (player is null) { + ctx.Console.Out.WriteLine($"Player {name} not found."); + return; + } + + if (player.DeathState != DeathState.Alive) { + ctx.Console.Out.WriteLine($"Player {name} is already dead."); + return; + } + + player.ConsumeHp((int) player.Stats.Values[BasicAttribute.Health].Current); + } + } + private static void Kill(GameSession session, FieldNpc npc, SkillMetadata skill) { var damageRecord = new DamageRecord(skill, skill.Data.Motions[0].Attacks[0]) { CasterId = session.Player.ObjectId, diff --git a/Maple2.Server.Game/Manager/Config/BuffManager.cs b/Maple2.Server.Game/Manager/Config/BuffManager.cs index d10612112..7065bce43 100644 --- a/Maple2.Server.Game/Manager/Config/BuffManager.cs +++ b/Maple2.Server.Game/Manager/Config/BuffManager.cs @@ -413,7 +413,9 @@ public void OnDeath() { foreach (Buff buff in Buffs.Values) { if (!buff.Metadata.Property.KeepOnDeath) { Remove(buff.Id); + continue; } + buff.UpdateEnabled(); } } @@ -430,4 +432,10 @@ private bool CheckImmunity(int newBuffId, BuffCategory category) { public List GetSaveCacheBuffs() { return Buffs.Values.Where(buff => !buff.Metadata.Property.RemoveOnLogout).ToList(); } + + public void UpdateEnabled() { + foreach (Buff buff in Buffs.Values) { + buff.UpdateEnabled(); + } + } } diff --git a/Maple2.Server.Game/Manager/Config/ConfigManager.cs b/Maple2.Server.Game/Manager/Config/ConfigManager.cs index c0256f6e4..15586552b 100644 --- a/Maple2.Server.Game/Manager/Config/ConfigManager.cs +++ b/Maple2.Server.Game/Manager/Config/ConfigManager.cs @@ -6,7 +6,6 @@ using Maple2.Model.Metadata; using Maple2.Server.Core.Packets; using Maple2.Server.Game.Manager.Items; -using Maple2.Server.Game.Model; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; using Maple2.Server.World.Service; @@ -27,8 +26,15 @@ public class ConfigManager { private readonly IList favoriteDesigners; private readonly IDictionary lapenshards; private readonly IDictionary skillCooldowns; - private long deathPenaltyTick; - public int DeathCount; + public long DeathPenaltyEndTick { + get => session.Player.Value.Character.DeathTick; + private set => session.Player.Value.Character.DeathTick = value; + } + public short DeathCount { + get => session.Player.Value.Character.DeathCount; + private set => session.Player.Value.Character.DeathCount = value; + } + public int InstantReviveCount; private readonly StatAttributes statAttributes; private readonly SkillPoint skillPoints; public readonly IDictionary GatheringCounts; @@ -53,8 +59,7 @@ public ConfigManager(GameStorage.Request db, GameSession session) { IList? FavoriteStickers, IList? FavoriteDesigners, IDictionary? Lapenshards, - long deathPenaltyTick, - int DeathCounter, + int InstantReviveCount, int ExplorationProgress, IDictionary? StatPoints, IDictionary? Allocation, @@ -86,8 +91,6 @@ public ConfigManager(GameStorage.Request db, GameSession session) { GatheringCounts = load.GatheringCounts ?? new Dictionary(); GuideRecords = load.GuideRecords ?? new Dictionary(); skillPoints = load.SkillPoint ?? new SkillPoint(); - deathPenaltyTick = load.deathPenaltyTick; - DeathCount = load.DeathCounter; ExplorationProgress = load.ExplorationProgress; statAttributes = new StatAttributes(); @@ -302,11 +305,57 @@ public void RemoveFavoriteDesigner(long designer) { favoriteDesigners.Remove(designer); } - public void UpdateDeathPenalty(int tick) { - deathPenaltyTick = tick; - DeathCount = tick > 0 ? DeathCount++ : 0; + public void LoadRevival() { + if (session.Player.Field.FieldTick > DeathPenaltyEndTick) { + DeathPenaltyEndTick = 0; + DeathCount = 0; + } + session.Send(RevivalPacket.RevivalCount(InstantReviveCount)); + session.Send(RevivalPacket.UpdatePenalty(session.Player.ObjectId, (int) DeathPenaltyEndTick, DeathCount)); + } + + /// + /// Updates the death penalty for the player + /// + /// The tick when the penalty ends, or 0 to reset + public void UpdateDeathPenalty(long endTick) { + // Skip penalty for low level players + if (session.Player.Value.Character.Level < Constant.UserRevivalPaneltyMinLevel) { + return; + } + + // Reset penalty if endTick is 0 + if (endTick == 0) { + DeathCount = 0; + DeathPenaltyEndTick = 0; + } + // Otherwise update penalty + else { + // Reset count if previous penalty expired + if (session.Field.FieldTick > DeathPenaltyEndTick) { + DeathCount = 0; + } + DeathCount++; + DeathPenaltyEndTick = endTick; + } + + // Send update to client + session.Send(RevivalPacket.UpdatePenalty(session.Player.ObjectId, (int) DeathPenaltyEndTick, DeathCount)); + } + + public void SetDeathPenalty(DeathInfo deathInfo, long currentTick) { + DeathPenaltyEndTick = currentTick + deathInfo.MsRemaining; + DeathCount = (short) deathInfo.Count; + } + + public void AddInstantReviveCount(int count = 1) { + if (count < 0) { + InstantReviveCount = 0; + } else { + InstantReviveCount += count; + } - session.Send(RevivalPacket.Confirm(session.Player, (int) deathPenaltyTick, DeathCount)); + session.Send(RevivalPacket.RevivalCount(InstantReviveCount)); } #region KeyBind @@ -524,9 +573,7 @@ public void Save(GameStorage.Request db) { favoriteStickers, favoriteDesigners, lapenshards, - skillCooldowns.Values.ToList(), - deathPenaltyTick, - DeathCount, + InstantReviveCount, ExplorationProgress, statAttributes.Allocation, statAttributes.Sources, diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index e509cf8fd..dd018e009 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -83,17 +83,12 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId // Use SpawnPoint if needed. if (fieldPlayer.Position == default) { - FieldPlayerSpawnPoint? spawn = fieldPlayerSpawnPoints.Values.FirstOrDefault(spawn => spawn.Enable); - if (spawn != null) { + if (TryGetPlayerSpawn(-1, out FieldPlayerSpawnPoint? spawn)) { fieldPlayer.Position = spawn.Position + new Vector3(0, 0, 25); fieldPlayer.Rotation = spawn.Rotation; } } - if (Metadata.Property.RevivalReturnId != 0) { - player.Character.ReviveMapId = Metadata.Property.RevivalReturnId; - } - if (FieldInstance.Type == InstanceType.none || FieldInstance.SaveField) { if (Metadata.Property.EnterReturnId != 0) { player.Character.ReturnMapId = Metadata.Property.EnterReturnId; @@ -780,6 +775,8 @@ public void OnAddPlayer(FieldPlayer added) { } } Broadcast(FieldPacket.AddPlayer(added.Session), added.Session); + added.Flag = PlayerObjectFlag.All; + Broadcast(ProxyObjectPacket.AddPlayer(added), added.Session); foreach (FieldItem fieldItem in fieldItems.Values) { added.Session.Send(FieldPacket.DropItem(fieldItem)); diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs index 7113b953b..d7097bc00 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs @@ -389,6 +389,12 @@ public bool TryGetPortal(int portalId, [NotNullWhen(true)] out FieldPortal? port } public bool TryGetPlayerSpawn(int id, [NotNullWhen(true)] out FieldPlayerSpawnPoint? playerSpawnPoint) { + // Get random spawn point if id is -1 + if (id < 0) { + List enabledSpawns = fieldPlayerSpawnPoints.Values.Where(spawn => spawn.Enable).ToList(); + playerSpawnPoint = enabledSpawns.Count > 0 ? enabledSpawns[Random.Shared.Next(enabledSpawns.Count)] : null; + return playerSpawnPoint != null; + } return fieldPlayerSpawnPoints.TryGetValue(id, out playerSpawnPoint); } diff --git a/Maple2.Server.Game/Manager/PartyManager.cs b/Maple2.Server.Game/Manager/PartyManager.cs index 86efc745d..b50f08624 100644 --- a/Maple2.Server.Game/Manager/PartyManager.cs +++ b/Maple2.Server.Game/Manager/PartyManager.cs @@ -362,7 +362,7 @@ private bool SyncUpdate(CancellationToken cancel, long id, UpdateField type, IPl member.Info.Update(type, info); - if (type == UpdateField.Health || type == UpdateField.Level) { + if (type is UpdateField.Health or UpdateField.Death) { session.Send(PartyPacket.UpdateStats(member)); } else { session.Send(PartyPacket.Update(member)); diff --git a/Maple2.Server.Game/Manager/StatsManager.cs b/Maple2.Server.Game/Manager/StatsManager.cs index 194359926..355052f36 100644 --- a/Maple2.Server.Game/Manager/StatsManager.cs +++ b/Maple2.Server.Game/Manager/StatsManager.cs @@ -122,6 +122,16 @@ public void Refresh() { StatConversion(player); Actor.Field.Broadcast(StatsPacket.Init(player)); Actor.Field.Broadcast(StatsPacket.Update(player), player.Session); + + player.Session.PlayerInfo.SendUpdate(new PlayerUpdateRequest { + AccountId = player.Session.AccountId, + CharacterId = player.Session.CharacterId, + Health = new HealthUpdate { + CurrentHp = Values[BasicAttribute.Health].Current, + TotalHp = Values[BasicAttribute.Health].Total, + }, + Async = true, + }); } public void ResetActor(IActor actor) { diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs index 47d19b4ab..ecb7e6fae 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs @@ -1,6 +1,8 @@ using System.Numerics; using Maple2.Model.Enum; +using Maple2.Model.Error; using Maple2.Model.Game; +using Maple2.Model.Metadata; using Maple2.Server.Game.Manager; using Maple2.Server.Game.Manager.Config; using Maple2.Server.Game.Packets; @@ -17,7 +19,34 @@ public class FieldPlayer : Actor { public override StatsManager Stats => Session.Stats; public override BuffManager Buffs => Session.Buffs; public override IPrism Shape => new Prism(new Circle(new Vector2(Position.X, Position.Y), 10), Position.Z, 100); - public ActorState State { get; set; } + private ActorState state; + public ActorState State { + get => state; + set { + if (state == value) return; + state = value; + Flag |= PlayerObjectFlag.State; + stateSyncTimeTracking = 0; // Reset time tracking on state change + } + } + private bool isDead; + public override bool IsDead { + get => isDead; + protected set { + if (value == isDead) return; + isDead = value; + Flag |= PlayerObjectFlag.Dead; + if (!isDead) { + DeathState = DeathState.Alive; + } else { + if (Session.Field.Metadata.Property.OnlyDarkTomb) { + DeathState = DeathState.Metal; + } else { + DeathState = Session.Config.DeathCount == 0 ? DeathState.FirstDeath : DeathState.Metal; + } + } + } + } public ActorSubState SubState { get; set; } public PlayerObjectFlag Flag { get; set; } private long flagTick; @@ -33,6 +62,22 @@ public class FieldPlayer : Actor { private long stateSyncTimeTracking { get; set; } private long stateSyncTrackingTick { get; set; } + public Tombstone? Tombstone { get; set; } + public DeathState DeathState { + get => Value.Character.DeathState; + set { + if (value != Value.Character.DeathState) { + Session.PlayerInfo.SendUpdate(new PlayerUpdateRequest { + AccountId = Session.AccountId, + CharacterId = Session.CharacterId, + DeathState = (int) value, + Async = true, + }); + } + Value.Character.DeathState = value; + } + } + #region DebugFlags private bool debugAi = false; public bool DebugSkills = false; @@ -45,7 +90,6 @@ public class FieldPlayer : Actor { public FieldPlayer(GameSession session, Player player) : base(session.Field!, player.ObjectId, player, GetPlayerModel(player.Character.Gender)) { Session = session; - regenStats = new Dictionary>(); lastRegenTime = new Dictionary(); @@ -97,9 +141,7 @@ public bool DebugAi { } public override void Update(long tickCount) { - if (InBattle && tickCount - battleTick > 2000) { - InBattle = false; - } + base.Update(tickCount); if (Flag != PlayerObjectFlag.None && tickCount > flagTick) { Field.Broadcast(ProxyObjectPacket.UpdatePlayer(this, Flag)); @@ -107,6 +149,20 @@ public override void Update(long tickCount) { flagTick = (long) (tickCount + TimeSpan.FromSeconds(2).TotalMilliseconds); } + if (IsDead) { + if (Tombstone == null) { + return; + } + if (Tombstone.HitsRemaining == 0) { + Revive(); + } + return; + } + + if (InBattle && tickCount - battleTick > 2000) { + InBattle = false; + } + // Loops through each registered regen stat and applies regen var statsToRemove = new List(); foreach (BasicAttribute attribute in regenStats.Keys) { @@ -124,7 +180,10 @@ public override void Update(long tickCount) { if (tickCount - regenTime > interval.Total) { lastRegenTime[attribute] = tickCount; - Stats.Values[attribute].Add(regen.Total); + if (attribute == BasicAttribute.Health) { + RecoverHp((int) regen.Total); + continue; + } Session.Send(StatsPacket.Update(this, attribute)); } } @@ -133,8 +192,6 @@ public override void Update(long tickCount) { } Session.GameEvent.Update(tickCount); - - base.Update(tickCount); } public void OnStateSync(StateSync stateSync) { @@ -149,10 +206,6 @@ public void OnStateSync(StateSync stateSync) { Position = stateSync.Position; Rotation = new Vector3(0, 0, stateSync.Rotation / 10f); - if (State != stateSync.State) { - Flag |= PlayerObjectFlag.State; - stateSyncTimeTracking = 0; // Reset time tracking on state change - } State = stateSync.State; SubState = stateSync.SubState; @@ -258,7 +311,64 @@ bool UpdateStateSyncTimeTracking() { } protected override void OnDeath() { - Flag |= PlayerObjectFlag.Dead; // TODO: Need to also send this flag upon revival + Field.Broadcast(SetCraftModePacket.Stop(ObjectId)); + + Session.HeldCube = null; + InBattle = false; + Tombstone = new Tombstone(this, Session.Config.DeathCount + 1); + bool darkTomb = Session.Field.Metadata.Property.OnlyDarkTomb || Session.Config.DeathCount > 0; + Field.Broadcast(DeadUserPacket.Dead(ObjectId, darkTomb)); + + Buffs.OnDeath(); + } + + /// + /// Revives the player from death state. + /// + /// If true, performs an instant revival without moving to spawn point. + public bool Revive(bool instant = false) { + // Check if player can be revived + if (!IsDead || Tombstone == null) { + return false; + } + + // Restore health and update state + RecoverHp((int) Stats.Values[BasicAttribute.Health].Total); + IsDead = false; + + // Apply death penalty if field requires it + if (Field.Metadata.Property.DeathPenalty) { + Session.Config.UpdateDeathPenalty(Field.FieldTick + Constant.UserRevivalPaneltyTick); + } + + // Update revival condition + Session.ConditionUpdate(ConditionType.revival); + + // Mark tombstone as cleared + Tombstone.HitsRemaining = 0; + + // Handle revival return to different map + if (!instant && Field.Metadata.Property.RevivalReturnId != 0 && Field.Metadata.Property.RevivalReturnId != Field.Metadata.Id) { + Session.Send(Session.PrepareField(Field.Metadata.Property.RevivalReturnId) + ? FieldEnterPacket.Request(Session.Player) + : FieldEnterPacket.Error(MigrationError.s_move_err_default)); + return true; + } + + // Send revival packets + Session.Send(StatsPacket.Init(this)); + if (instant) { + Session.Send(RevivalPacket.RevivalCount(Session.Config.InstantReviveCount)); + } + Field.Broadcast(RevivalPacket.Revive(ObjectId)); + + // Move to spawn point if not instant revival + if (!instant && Field.TryGetPlayerSpawn(-1, out FieldPlayerSpawnPoint? point)) { + MoveToPosition(point.Position, point.Rotation); + } + + Buffs.UpdateEnabled(); + return true; } /// @@ -275,6 +385,16 @@ public void RecoverHp(int amount) { stat.Add(amount); Session.Send(StatsPacket.Update(this, BasicAttribute.Health)); } + + Session.PlayerInfo.SendUpdate(new PlayerUpdateRequest { + AccountId = Session.AccountId, + CharacterId = Session.CharacterId, + Health = new HealthUpdate { + CurrentHp = Stats.Values[BasicAttribute.Health].Current, + TotalHp = Stats.Values[BasicAttribute.Health].Total, + }, + Async = true, + }); } /// @@ -288,10 +408,21 @@ public void ConsumeHp(int amount) { Stat stat = Stats.Values[BasicAttribute.Health]; stat.Add(-amount); + Session.Send(StatsPacket.Update(this, BasicAttribute.Health)); - if (!regenStats.ContainsKey(BasicAttribute.Health)) { + if (stat.Current > 0 && !regenStats.ContainsKey(BasicAttribute.Health)) { regenStats.Add(BasicAttribute.Health, new Tuple(BasicAttribute.HpRegen, BasicAttribute.HpRegenInterval)); } + + Session.PlayerInfo.SendUpdate(new PlayerUpdateRequest { + AccountId = Session.AccountId, + CharacterId = Session.CharacterId, + Health = new HealthUpdate { + CurrentHp = Stats.Values[BasicAttribute.Health].Current, + TotalHp = Stats.Values[BasicAttribute.Health].Total, + }, + Async = true, + }); } /// diff --git a/Maple2.Server.Game/Model/Field/Buff.cs b/Maple2.Server.Game/Model/Field/Buff.cs index 5ef05dbab..35c064859 100644 --- a/Maple2.Server.Game/Model/Field/Buff.cs +++ b/Maple2.Server.Game/Model/Field/Buff.cs @@ -103,7 +103,7 @@ public virtual void Update(long tickCount) { Proc(); } - private bool UpdateEnabled(bool notifyField = true) { + public bool UpdateEnabled(bool notifyField = true) { bool enabled = Metadata.Condition.Check(Caster, Owner, Owner); if (Enabled != enabled) { Enabled = enabled; diff --git a/Maple2.Server.Game/Model/Field/Tombstone.cs b/Maple2.Server.Game/Model/Field/Tombstone.cs new file mode 100644 index 000000000..4eb54f9b7 --- /dev/null +++ b/Maple2.Server.Game/Model/Field/Tombstone.cs @@ -0,0 +1,39 @@ +using Maple2.Model.Metadata; +using Maple2.PacketLib.Tools; +using Maple2.Server.Game.Packets; +using Maple2.Tools; + +namespace Maple2.Server.Game.Model; + +public class Tombstone : IByteSerializable { + public readonly FieldPlayer Owner; + public int ObjectId => Owner.ObjectId; + private byte hitsRemaining; + public byte HitsRemaining { + get => hitsRemaining; + set { + if (hitsRemaining == 0) { + return; + } + hitsRemaining = Math.Clamp(value, (byte) 0, TotalHitCount); + + Owner.Field.Broadcast(RevivalPacket.Tombstone(this)); + } + } + public byte TotalHitCount { get; } + public int Unknown1 { get; } = 1; + public bool Unknown2 { get; } + + public Tombstone(FieldPlayer owner, int totalDeaths) { + Owner = owner; + TotalHitCount = (byte) Math.Min(totalDeaths * Constant.hitPerDeadCount, Constant.hitPerDeadCount * Constant.maxDeadCount); + hitsRemaining = TotalHitCount; + } + public void WriteTo(IByteWriter writer) { + writer.WriteInt(ObjectId); + writer.WriteByte(HitsRemaining); + writer.WriteByte(TotalHitCount); + writer.WriteInt(Unknown1); // 1 if hit by a user? + writer.WriteBool(Unknown2); // true if revived by pet + } +} diff --git a/Maple2.Server.Game/PacketHandlers/HomeDoctorHandler.cs b/Maple2.Server.Game/PacketHandlers/HomeDoctorHandler.cs index 69aa7a1de..e16d77432 100644 --- a/Maple2.Server.Game/PacketHandlers/HomeDoctorHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/HomeDoctorHandler.cs @@ -1,9 +1,9 @@ -using Maple2.Model.Metadata; +using Maple2.Model.Enum; +using Maple2.Model.Metadata; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Core.PacketHandlers; using Maple2.Server.Core.Packets; -using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; namespace Maple2.Server.Game.PacketHandlers; @@ -11,23 +11,27 @@ namespace Maple2.Server.Game.PacketHandlers; public class HomeDoctorHandler : PacketHandler { public override RecvOp OpCode => RecvOp.RequestHomeDoctor; - private const int DOCTOR_COST_MESO = 10000; - public override void Handle(GameSession session, IByteReader packet) { long time = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (session.Player.Value.Character.DoctorCooldown + Constant.HomeDoctorCallCooldown > time) { return; } - // TODO: Error here? and how much should this cost. - if (session.Currency.Meso < DOCTOR_COST_MESO) { + if (session.Config.DeathPenaltyEndTick < session.Field.FieldTick) { + return; + } + + int cost = session.Field.Lua.CalcResolvePenaltyPrice((ushort) session.Player.Value.Character.Level, session.Config.DeathCount, 0); + if (session.Currency.Meso < cost) { return; } session.Player.Value.Character.DoctorCooldown = time; - session.Currency.Meso -= DOCTOR_COST_MESO; - session.Send(RevivalPacket.Confirm(session.Player)); + session.Currency.Meso -= cost; + session.Config.UpdateDeathPenalty(0); session.Send(HomeDoctor(time)); + session.ConditionUpdate(ConditionType.home_doctor); + session.ConditionUpdate(ConditionType.resolve_panelty); } private static ByteWriter HomeDoctor(long time) { diff --git a/Maple2.Server.Game/PacketHandlers/ResolveDeathPenaltyHandler.cs b/Maple2.Server.Game/PacketHandlers/ResolveDeathPenaltyHandler.cs new file mode 100644 index 000000000..3205377ef --- /dev/null +++ b/Maple2.Server.Game/PacketHandlers/ResolveDeathPenaltyHandler.cs @@ -0,0 +1,33 @@ +using Maple2.Model.Enum; +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.PacketHandlers; +using Maple2.Server.Game.Model; +using Maple2.Server.Game.Session; + +namespace Maple2.Server.Game.PacketHandlers; + +public class ResolveDeathPenaltyHandler : PacketHandler { + public override RecvOp OpCode => RecvOp.ResolvePenalty; + + public override void Handle(GameSession session, IByteReader packet) { + int npcObjectId = packet.ReadInt(); + if (!session.Field.Npcs.TryGetValue(npcObjectId, out FieldNpc? npc) || npc.Value.Metadata.Basic.Kind != 81) { // 81 = Doctor + return; + } + + if (session.Config.DeathPenaltyEndTick < session.Field.FieldTick) { + return; + } + + int mode = session.Field.Metadata.Property.Continent == Continent.ShadowWorld ? 1 : 0; + int cost = session.Field.Lua.CalcResolvePenaltyPrice((ushort) session.Player.Value.Character.Level, session.Config.DeathCount, mode); + if (session.Currency.Meso < cost) { + return; + } + + session.Currency.Meso -= cost; + session.Config.UpdateDeathPenalty(0); + session.ConditionUpdate(ConditionType.resolve_panelty); + } +} diff --git a/Maple2.Server.Game/PacketHandlers/RevivalHandler.cs b/Maple2.Server.Game/PacketHandlers/RevivalHandler.cs index c569f2cd7..e79964b68 100644 --- a/Maple2.Server.Game/PacketHandlers/RevivalHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/RevivalHandler.cs @@ -1,7 +1,9 @@ -using Maple2.Model.Metadata; +using Maple2.Model.Enum; +using Maple2.Model.Game; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Core.PacketHandlers; +using Maple2.Server.Core.Packets; using Maple2.Server.Game.Session; namespace Maple2.Server.Game.PacketHandlers; @@ -26,13 +28,52 @@ public override void Handle(GameSession session, IByteReader packet) { } } + /// + /// Handles normal revival (from tombstone hits or manual revival) + /// private void HandleSafeRevive(GameSession session) { - session.Config.UpdateDeathPenalty((int) (Environment.TickCount64 + TimeSpan.FromMilliseconds(Constant.UserRevivalPaneltyTick).TotalMilliseconds)); - // send invincible buff? - // send player to a spawn point + if (session.Field.Metadata.Property.NoRevivalHere || session.Field.Metadata.Property.RevivalReturnId == 0) { + return; + } + // Revive player - this will handle moving to spawn point + if (session.Player.Revive()) { + session.Player.Tombstone = null; + } } + /// + /// Handles instant revival (using mesos or voucher) + /// private void HandleInstantRevive(GameSession session, IByteReader packet) { + if (session.Field.Metadata.Property.NoRevivalHere) { + return; + } + bool useVoucher = packet.ReadBool(); + + if (useVoucher) { + if (!session.Item.Inventory.Consume([new IngredientInfo(ItemTag.FreeReviveCoupon, 1)])) { + // Send error packet? + return; + } + session.Send(NoticePacket.Notice(NoticePacket.Flags.Alert | NoticePacket.Flags.Message, StringCode.s_revival_use_coupon)); + } else { + int totalMaxCount = session.Field.Lua.GetMesoRevivalDailyMaxCount(); + if (session.Config.InstantReviveCount >= totalMaxCount) { + return; + } + + int cost = session.Field.Lua.CalcRevivalPrice((ushort) session.Player.Value.Character.Level); + if (session.Currency.Meso < cost) { + // Client sends error + return; + } + session.Currency.Meso -= cost; + } + + session.Config.InstantReviveCount++; + if (session.Player.Revive(true)) { + session.Player.Tombstone = null; + } } } diff --git a/Maple2.Server.Game/PacketHandlers/TombstoneHandler.cs b/Maple2.Server.Game/PacketHandlers/TombstoneHandler.cs new file mode 100644 index 000000000..ce089170e --- /dev/null +++ b/Maple2.Server.Game/PacketHandlers/TombstoneHandler.cs @@ -0,0 +1,24 @@ +using Maple2.Model.Enum; +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.PacketHandlers; +using Maple2.Server.Game.Model; +using Maple2.Server.Game.Session; + +namespace Maple2.Server.Game.PacketHandlers; + +public class TombstoneHandler : PacketHandler { + public override RecvOp OpCode => RecvOp.Tombstone; + + public override void Handle(GameSession session, IByteReader packet) { + int objectId = packet.ReadInt(); + int hits = packet.ReadInt(); + + if (!session.Field.TryGetPlayer(objectId, out FieldPlayer? player) || player.Tombstone == null) { + return; + } + + player.Tombstone.HitsRemaining -= (byte) hits; + session.ConditionUpdate(ConditionType.hit_tombstone); + } +} diff --git a/Maple2.Server.Game/Packets/DeadUserPacket.cs b/Maple2.Server.Game/Packets/DeadUserPacket.cs new file mode 100644 index 000000000..497d4f10a --- /dev/null +++ b/Maple2.Server.Game/Packets/DeadUserPacket.cs @@ -0,0 +1,15 @@ +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.Packets; + +namespace Maple2.Server.Game.Packets; + +public static class DeadUserPacket { + public static ByteWriter Dead(int objectId, bool darkTombstone) { + var pWriter = Packet.Of(SendOp.DeadUser); + pWriter.WriteInt(objectId); + pWriter.WriteBool(darkTombstone); + + return pWriter; + } +} diff --git a/Maple2.Server.Game/Packets/FieldPacket.cs b/Maple2.Server.Game/Packets/FieldPacket.cs index bee6530fd..5d252b70a 100644 --- a/Maple2.Server.Game/Packets/FieldPacket.cs +++ b/Maple2.Server.Game/Packets/FieldPacket.cs @@ -298,9 +298,9 @@ private static void WriteCharacter(this IByteWriter writer, GameSession session) writer.WriteShort(character.Channel); writer.WriteInt((int) character.Job.Code()); writer.Write(character.Job); - writer.WriteInt((int) session.Stats.Values[BasicAttribute.Health].Current); writer.WriteInt((int) session.Stats.Values[BasicAttribute.Health].Total); - writer.WriteShort(); + writer.WriteInt((int) session.Stats.Values[BasicAttribute.Health].Current); + writer.WriteShort(character.DeathCount); writer.WriteLong(); writer.WriteLong(character.StorageCooldown); writer.WriteLong(character.DoctorCooldown); diff --git a/Maple2.Server.Game/Packets/PartyPacket.cs b/Maple2.Server.Game/Packets/PartyPacket.cs index bb7c457ba..4db8b93c0 100644 --- a/Maple2.Server.Game/Packets/PartyPacket.cs +++ b/Maple2.Server.Game/Packets/PartyPacket.cs @@ -6,7 +6,7 @@ using Maple2.Server.Core.Packets; using Maple2.Tools.Extensions; using Maple2.Model.Game.Party; -using Maple2.Server.Game.Manager.Field; +// ReSharper disable RedundantTypeArgumentsOfMethod namespace Maple2.Server.Game.Packets; @@ -192,9 +192,9 @@ public static ByteWriter UpdateStats(PartyMember member) { pWriter.WriteLong(member.CharacterId); pWriter.WriteLong(member.AccountId); - pWriter.WriteInt((int) member.Info.CurrentHp); pWriter.WriteInt((int) member.Info.TotalHp); - pWriter.WriteShort(member.Info.Level); + pWriter.WriteInt((int) member.Info.CurrentHp); + pWriter.Write(member.Info.DeathState); return pWriter; } @@ -204,6 +204,7 @@ public static ByteWriter PartyNotice(PartyMessage message, string systemSoundKey pWriter.Write(Command.DungeonNotice); pWriter.WriteUnicodeString(message.ToString()); + // ReSharper disable once CommentTypo pWriter.WriteUnicodeString(systemSoundKey); // "Field_Enterance_Reset_Dungeon" pWriter.WriteUnicodeString(); @@ -243,6 +244,7 @@ public static ByteWriter LoadPartySearchListing(Party party) { public static ByteWriter PartySearch(byte type, bool searching) { var pWriter = Packet.Of(SendOp.Party); pWriter.Write(Command.PartySearch); + // ReSharper disable once CommentTypo /* if type == 1: dungeon_message(1) # s_enum_dungeon_group_normal diff --git a/Maple2.Server.Game/Packets/RevivalPacket.cs b/Maple2.Server.Game/Packets/RevivalPacket.cs index f7f69d03f..b7f01a697 100644 --- a/Maple2.Server.Game/Packets/RevivalPacket.cs +++ b/Maple2.Server.Game/Packets/RevivalPacket.cs @@ -1,25 +1,40 @@ -using Maple2.Model.Game; -using Maple2.PacketLib.Tools; +using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Core.Packets; using Maple2.Server.Game.Model; +using Maple2.Tools.Extensions; namespace Maple2.Server.Game.Packets; public static class RevivalPacket { - public static ByteWriter Confirm(IActor player, int ticks = 0, int counter = 0) { + public static ByteWriter UpdatePenalty(int objectId, int endTick, int count) { var pWriter = Packet.Of(SendOp.RevivalConfirm); - pWriter.WriteInt(player.ObjectId); - pWriter.WriteInt(ticks); // 0 in house - pWriter.WriteInt(counter); // (some counter); 0 in house + pWriter.WriteInt(objectId); + pWriter.WriteInt(endTick); + pWriter.WriteInt(count); return pWriter; } - public static ByteWriter Count(int count) { + public static ByteWriter RevivalCount(int count) { var pWriter = Packet.Of(SendOp.RevivalCount); pWriter.WriteInt(count); return pWriter; } + + public static ByteWriter Revive(int objectId) { + var pWriter = Packet.Of(SendOp.Revival); + pWriter.WriteInt(objectId); + pWriter.WriteByte(); + + return pWriter; + } + + public static ByteWriter Tombstone(Tombstone tombstone) { + var pWriter = Packet.Of(SendOp.Tombstone); + pWriter.WriteClass(tombstone); + + return pWriter; + } } diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index a4414cff7..b0d85c9e0 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -255,6 +255,7 @@ public bool EnterServer(long accountId, Guid machineId, MigrateInResponse migrat long currentTick = Environment.TickCount64; Buffs.SetCacheBuffs(configResponse.Buffs, currentTick); Config.SetCacheSkillCooldowns(configResponse.SkillCooldowns, currentTick); + Config.SetDeathPenalty(configResponse.DeathInfo, currentTick); } catch (RpcException ex) { Logger.Warning(ex, "Failed to load cache player config"); @@ -425,6 +426,7 @@ public bool EnterField() { Send(StatsPacket.Init(Player)); Field.Broadcast(StatsPacket.Update(Player), Player.Session); + //TODO: Save current hp/sp/ep in memory. This will help determine if player is dead upon login. var pWriter = Packet.Of(SendOp.UserState); pWriter.WriteInt(Player.ObjectId); pWriter.Write(ActorState.Fall); @@ -441,8 +443,7 @@ public bool EnterField() { Send(CubePacket.ReturnMap(Player.Value.Character.ReturnMapId)); Config.LoadLapenshard(); - Send(RevivalPacket.Count(0)); // TODO: Consumed daily revivals? - Send(RevivalPacket.Confirm(Player)); + Config.LoadRevival(); Config.LoadStatAttributes(); Config.LoadSkillPoints(); Player.Buffs.LoadFieldBuffs(); @@ -601,8 +602,7 @@ public void DailyReset() { Config.GatheringCounts.Clear(); Send(UserEnvPacket.GatheringCounts(Config.GatheringCounts)); // Death Counter - Config.DeathCount = 0; - Send(RevivalPacket.Count(0)); + Config.AddInstantReviveCount(-1); // Premium Rewards Claimed Player.Value.Account.PremiumRewardsClaimed.Clear(); Send(PremiumCubPacket.LoadItems(Player.Value.Account.PremiumRewardsClaimed)); @@ -786,6 +786,11 @@ void SaveCacheConfig() { Charges = cooldown.Charges, }), }, + DeathInfo = new DeathInfo { + Count = Config.DeathCount, + MsRemaining = (int) (Config.DeathPenaltyEndTick - fieldTick), + StopTime = stopTime, + }, }, RequesterId = CharacterId, }); diff --git a/Maple2.Server.Game/Util/ConditionUtil.cs b/Maple2.Server.Game/Util/ConditionUtil.cs index 25e5226d2..a8f30f672 100644 --- a/Maple2.Server.Game/Util/ConditionUtil.cs +++ b/Maple2.Server.Game/Util/ConditionUtil.cs @@ -159,6 +159,10 @@ private static bool CheckCode(this ConditionMetadata.Parameters code, GameSessio case ConditionType.wedding_propose_declined: case ConditionType.wedding_hall_cancel: case ConditionType.item_gear_score: + case ConditionType.revival: + case ConditionType.home_doctor: + case ConditionType.resolve_panelty: + case ConditionType.hit_tombstone: return true; } return false; @@ -288,6 +292,10 @@ private static bool CheckTarget(this ConditionMetadata.Parameters target, GameSe case ConditionType.continent: case ConditionType.explore: case ConditionType.item_gear_score: + case ConditionType.revival: + case ConditionType.home_doctor: + case ConditionType.resolve_panelty: + case ConditionType.hit_tombstone: return true; } return false; diff --git a/Maple2.Server.Game/Util/NpcTalkUtil.cs b/Maple2.Server.Game/Util/NpcTalkUtil.cs index 1518ee91c..403c4a0ac 100644 --- a/Maple2.Server.Game/Util/NpcTalkUtil.cs +++ b/Maple2.Server.Game/Util/NpcTalkUtil.cs @@ -131,6 +131,10 @@ private static bool MeetsJobCondition(GameSession session, JobConditionMetadata return false; } + if (jobCondition.DeathPenalty && session.Config.DeathPenaltyEndTick < session.Field.FieldTick) { + return false; + } + return true; } @@ -213,6 +217,10 @@ public static bool ConditionCheck(this ScriptConditionMetadata scriptCondition, return false; } + if (scriptCondition.DeathPenalty && session.Config.DeathPenaltyEndTick < session.Field.FieldTick) { + return false; + } + return true; } diff --git a/Maple2.Server.Game/Util/SkillUtils.cs b/Maple2.Server.Game/Util/SkillUtils.cs index 0b1a4e880..6f9a36697 100644 --- a/Maple2.Server.Game/Util/SkillUtils.cs +++ b/Maple2.Server.Game/Util/SkillUtils.cs @@ -72,6 +72,9 @@ public static bool Check(this BeginCondition condition, IActor caster, IActor ow if (condition is not { Probability: 1 } && condition.Probability < Random.Shared.NextDouble()) { return false; } + if (!condition.AllowDead && caster.IsDead) { + return false; + } if (condition.OnlyShadowWorld && caster.Field.Metadata.Property.Type != MapType.Shadow) { return false; } diff --git a/Maple2.Server.World/Containers/PlayerConfigLookUp.cs b/Maple2.Server.World/Containers/PlayerConfigLookUp.cs index 730ef3e19..7f3789361 100644 --- a/Maple2.Server.World/Containers/PlayerConfigLookUp.cs +++ b/Maple2.Server.World/Containers/PlayerConfigLookUp.cs @@ -11,14 +11,16 @@ public class PlayerConfigLookUp { private readonly ConcurrentDictionary> buffs; private readonly ConcurrentDictionary> skillCooldowns; + private readonly ConcurrentDictionary deaths; public PlayerConfigLookUp(SkillMetadataStorage skillMetadataStorage) { this.skillMetadataStorage = skillMetadataStorage; buffs = []; skillCooldowns = []; + deaths = []; } - public void Save(List saveBuffs, List skillCooldownInfos, long characterId) { + public void Save(List saveBuffs, List skillCooldownInfos, DeathInfo death, long characterId) { // Save buffs if (!buffs.TryGetValue(characterId, out ConcurrentDictionary? list)) { buffs.TryAdd(characterId, new ConcurrentDictionary()); @@ -50,13 +52,21 @@ public void Save(List saveBuffs, List skillCooldown skillList.TryAdd(skillCooldown.SkillId, skillCooldown); } + + // Save death + if (!deaths.TryGetValue(characterId, out DeathInfo? _)) { + deaths.TryAdd(characterId, death); + } else { + deaths[characterId] = death; + } } - public (List, List) Retrieve(long characterId) { + public (List Buffs, List SkillCooldowns, DeathInfo Death) Retrieve(long characterId) { long currentTime = DateTime.Now.ToEpochSeconds(); List buffs = RetrieveBuffs(characterId, currentTime); List skillCooldowns = RetrieveSkillCooldowns(characterId, currentTime); - return (buffs, skillCooldowns); + DeathInfo death = RetrieveDeath(characterId); + return (buffs, skillCooldowns, death); } private List RetrieveBuffs(long characterId, long currentTime) { @@ -115,4 +125,20 @@ private List RetrieveSkillCooldowns(long characterId, long cu return list.Values.ToList(); } + private DeathInfo RetrieveDeath(long characterId) { + if (!deaths.TryGetValue(characterId, out DeathInfo? death)) { + return new DeathInfo(); + } + + long msSurpassed = (long) ((DateTime.Now.ToEpochSeconds() - death.StopTime) * TimeSpan.FromSeconds(1).TotalMilliseconds); + if (msSurpassed > death.MsRemaining) { + deaths.Remove(characterId, out _); + return new DeathInfo(); + } + + death.MsRemaining -= (int) msSurpassed; + + return death; + } + } diff --git a/Maple2.Server.World/Migrations/20250406064924_DeathCount.Designer.cs b/Maple2.Server.World/Migrations/20250406064924_DeathCount.Designer.cs new file mode 100644 index 000000000..22aee7a90 --- /dev/null +++ b/Maple2.Server.World/Migrations/20250406064924_DeathCount.Designer.cs @@ -0,0 +1,1941 @@ +// +using System; +using Maple2.Database.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Maple2.Server.World.Migrations +{ + [DbContext(typeof(Ms2Context))] + [Migration("20250406064924_DeathCount")] + partial class DeathCount + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Maple2.Database.Model.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ActiveGoldPass") + .HasColumnType("tinyint(1)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("json"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("MachineId") + .HasColumnType("binary(16)"); + + b.Property("MarketLimits") + .IsRequired() + .HasColumnType("json"); + + b.Property("MaxCharacters") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(4); + + b.Property("Online") + .HasColumnType("tinyint(1)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("PremiumRewardsClaimed") + .IsRequired() + .HasColumnType("json"); + + b.Property("PremiumTime") + .HasColumnType("bigint"); + + b.Property("PrestigeCurrentExp") + .HasColumnType("bigint"); + + b.Property("PrestigeExp") + .HasColumnType("bigint"); + + b.Property("PrestigeLevel") + .HasColumnType("int"); + + b.Property("PrestigeLevelsGained") + .HasColumnType("int"); + + b.Property("PrestigeMissions") + .IsRequired() + .HasColumnType("json"); + + b.Property("PrestigeRewardsClaimed") + .IsRequired() + .HasColumnType("json"); + + b.Property("SurvivalExp") + .HasColumnType("bigint"); + + b.Property("SurvivalGoldLevelRewardClaimed") + .HasColumnType("int"); + + b.Property("SurvivalLevel") + .HasColumnType("int"); + + b.Property("SurvivalSilverLevelRewardClaimed") + .HasColumnType("int"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("account", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Achievement", b => + { + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CompletedCount") + .HasColumnType("int"); + + b.Property("Counter") + .HasColumnType("bigint"); + + b.Property("CurrentGrade") + .HasColumnType("int"); + + b.Property("Favorite") + .HasColumnType("tinyint(1)"); + + b.Property("Grades") + .IsRequired() + .HasColumnType("json"); + + b.Property("RewardGrade") + .HasColumnType("int"); + + b.HasKey("OwnerId", "Id"); + + b.ToTable("achievement", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.BannerSlot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ActivateTime") + .HasColumnType("datetime(6)"); + + b.Property("BannerId") + .HasColumnType("bigint"); + + b.Property("Template") + .HasColumnType("json"); + + b.HasKey("Id"); + + b.HasIndex("BannerId"); + + b.ToTable("ugc-banner-slot", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.BlackMarketListing", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Deposit") + .HasColumnType("bigint"); + + b.Property("ExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("ItemUid") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("black-market-listing", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Buddy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("BuddyId") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("BuddyId"); + + b.HasIndex("OwnerId"); + + b.ToTable("buddy", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Channel") + .HasColumnType("smallint"); + + b.Property("Cooldown") + .IsRequired() + .HasColumnType("json"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("json"); + + b.Property("DeleteTime") + .HasColumnType("datetime(6)"); + + b.Property("Experience") + .IsRequired() + .HasColumnType("json"); + + b.Property("Gender") + .HasColumnType("tinyint unsigned"); + + b.Property("Job") + .HasColumnType("int"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("Level") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((short)1); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("Mastery") + .IsRequired() + .HasColumnType("json"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Profile") + .IsRequired() + .HasColumnType("json"); + + b.Property("ReturnChannel") + .HasColumnType("smallint"); + + b.Property("ReturnMapId") + .HasColumnType("int"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("character", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.CharacterConfig", b => + { + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("ExplorationProgress") + .HasColumnType("int"); + + b.Property("FavoriteDesigners") + .HasColumnType("json"); + + b.Property("FavoriteStickers") + .HasColumnType("json"); + + b.Property("GatheringCounts") + .HasColumnType("json"); + + b.Property("GuideRecords") + .HasColumnType("json"); + + b.Property("HotBars") + .HasColumnType("json"); + + b.Property("InstantRevivalCount") + .HasColumnType("int"); + + b.Property("KeyBinds") + .HasColumnType("json"); + + b.Property("Lapenshards") + .HasColumnType("json"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("SkillMacros") + .HasColumnType("json"); + + b.Property("SkillPoint") + .HasColumnType("json"); + + b.Property("StatAllocation") + .HasColumnType("json"); + + b.Property("StatPoints") + .HasColumnType("json"); + + b.Property("Wardrobes") + .HasColumnType("json"); + + b.HasKey("CharacterId"); + + b.ToTable("character-config", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.CharacterUnlock", b => + { + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CollectedItems") + .IsRequired() + .HasColumnType("json"); + + b.Property("DungeonRankRewards") + .IsRequired() + .HasColumnType("json"); + + b.Property("Emotes") + .IsRequired() + .HasColumnType("json"); + + b.Property("Expand") + .IsRequired() + .HasColumnType("json"); + + b.Property("FishAlbum") + .IsRequired() + .HasColumnType("json"); + + b.Property("HairSlotExpand") + .HasColumnType("smallint"); + + b.Property("InteractedObjects") + .IsRequired() + .HasColumnType("json"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("Maps") + .IsRequired() + .HasColumnType("json"); + + b.Property("MasteryRewardsClaimed") + .IsRequired() + .HasColumnType("json"); + + b.Property("Pets") + .IsRequired() + .HasColumnType("json"); + + b.Property("StickerSets") + .IsRequired() + .HasColumnType("json"); + + b.Property("Taxis") + .IsRequired() + .HasColumnType("json"); + + b.Property("Titles") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("CharacterId"); + + b.ToTable("character-unlock", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Club", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("BuffId") + .HasColumnType("int"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("LeaderId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("NameChangeCooldown") + .HasColumnType("datetime(6)"); + + b.Property("State") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("LeaderId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("club", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.ClubMember", b => + { + b.Property("ClubId") + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.HasKey("ClubId", "CharacterId"); + + b.HasIndex("CharacterId"); + + b.ToTable("club-member", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.DungeonRecord", b => + { + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("DungeonId") + .HasColumnType("int"); + + b.Property("ClearTime") + .HasColumnType("datetime(6)"); + + b.Property("CooldownTime") + .HasColumnType("datetime(6)"); + + b.Property("CurrentClears") + .HasColumnType("tinyint unsigned"); + + b.Property("CurrentRecord") + .HasColumnType("smallint"); + + b.Property("CurrentSubClears") + .HasColumnType("tinyint unsigned"); + + b.Property("DailyResetTime") + .HasColumnType("datetime(6)"); + + b.Property("ExtraCurrentClears") + .HasColumnType("tinyint unsigned"); + + b.Property("ExtraCurrentSubClears") + .HasColumnType("tinyint unsigned"); + + b.Property("Flag") + .HasColumnType("tinyint unsigned"); + + b.Property("LifetimeRecord") + .HasColumnType("smallint"); + + b.Property("TotalClears") + .HasColumnType("int"); + + b.Property("UnionCooldownTime") + .HasColumnType("datetime(6)"); + + b.HasKey("OwnerId", "DungeonId"); + + b.ToTable("dungeon-record", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.GameEventUserValue", b => + { + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("EventId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("ExpirationTime") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("CharacterId", "EventId", "Type"); + + b.ToTable("game-event-user-value", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Guild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Buffs") + .IsRequired() + .HasColumnType("json"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Emblem") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Experience") + .HasColumnType("int"); + + b.Property("Focus") + .HasColumnType("int"); + + b.Property("Funds") + .HasColumnType("int"); + + b.Property("HouseRank") + .HasColumnType("int"); + + b.Property("HouseTheme") + .HasColumnType("int"); + + b.Property("LeaderId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Notice") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Npcs") + .IsRequired() + .HasColumnType("json"); + + b.Property("Posters") + .IsRequired() + .HasColumnType("json"); + + b.Property("Ranks") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("Id"); + + b.HasIndex("LeaderId") + .IsUnique(); + + b.ToTable("guild", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.GuildApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApplicantId") + .HasColumnType("bigint"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("GuildId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("GuildId"); + + b.ToTable("guild-application", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.GuildMember", b => + { + b.Property("GuildId") + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CheckinTime") + .HasColumnType("datetime(6)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("DailyDonationCount") + .HasColumnType("int"); + + b.Property("DonationTime") + .HasColumnType("datetime(6)"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Rank") + .HasColumnType("tinyint unsigned"); + + b.Property("TotalContribution") + .HasColumnType("int"); + + b.Property("WeeklyContribution") + .HasColumnType("int"); + + b.HasKey("GuildId", "CharacterId"); + + b.HasIndex("CharacterId") + .IsUnique(); + + b.ToTable("guild-member", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Home", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("ArchitectScore") + .HasColumnType("int"); + + b.Property("Area") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint unsigned") + .HasDefaultValue((byte)4); + + b.Property("Background") + .HasColumnType("tinyint unsigned"); + + b.Property("Blueprints") + .IsRequired() + .HasColumnType("json"); + + b.Property("Camera") + .HasColumnType("tinyint unsigned"); + + b.Property("CurrentArchitectScore") + .HasColumnType("int"); + + b.Property("DecorationExp") + .HasColumnType("bigint"); + + b.Property("DecorationLevel") + .HasColumnType("bigint"); + + b.Property("DecorationRewardTimestamp") + .HasColumnType("bigint"); + + b.Property("Height") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint unsigned") + .HasDefaultValue((byte)3); + + b.Property("InteriorRewardsClaimed") + .IsRequired() + .HasColumnType("json"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("Layouts") + .IsRequired() + .HasColumnType("json"); + + b.Property("Lighting") + .HasColumnType("tinyint unsigned"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Passcode") + .HasColumnType("longtext"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("AccountId"); + + b.ToTable("home", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.HomeLayout", b => + { + b.Property("Uid") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Area") + .HasColumnType("tinyint unsigned"); + + b.Property("Background") + .HasColumnType("tinyint unsigned"); + + b.Property("Camera") + .HasColumnType("tinyint unsigned"); + + b.Property("Height") + .HasColumnType("tinyint unsigned"); + + b.Property("Id") + .HasColumnType("int"); + + b.Property("Lighting") + .HasColumnType("tinyint unsigned"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Uid"); + + b.ToTable("home-layout", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.HomeLayoutCube", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("HomeLayoutId") + .HasColumnType("bigint"); + + b.Property("Interact") + .HasColumnType("json"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Rotation") + .HasColumnType("float"); + + b.Property("Template") + .HasColumnType("json"); + + b.Property("X") + .HasColumnType("tinyint"); + + b.Property("Y") + .HasColumnType("tinyint"); + + b.Property("Z") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("HomeLayoutId"); + + b.ToTable("home-layout-cube", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Amount") + .HasColumnType("int"); + + b.Property("Appearance") + .IsRequired() + .HasColumnType("json"); + + b.Property("Binding") + .HasColumnType("json"); + + b.Property("CoupleInfo") + .HasColumnType("json"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Enchant") + .HasColumnType("json"); + + b.Property("ExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("GachaDismantleId") + .HasColumnType("int"); + + b.Property("GlamorForges") + .HasColumnType("smallint"); + + b.Property("Group") + .HasColumnType("tinyint unsigned"); + + b.Property("IsLocked") + .HasColumnType("tinyint(1)"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("LimitBreak") + .HasColumnType("json"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("RemainUses") + .HasColumnType("int"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("Socket") + .HasColumnType("json"); + + b.Property("Stats") + .HasColumnType("json"); + + b.Property("SubType") + .HasColumnType("json"); + + b.Property("TimeChangedOption") + .HasColumnType("int"); + + b.Property("Transfer") + .HasColumnType("json"); + + b.Property("UnlockTime") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("item", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.ItemStorage", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Expand") + .HasColumnType("smallint"); + + b.Property("Meso") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("item-storage", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Mail", b => + { + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ContentArgs") + .IsRequired() + .HasColumnType("json"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("json"); + + b.Property("ExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("ReadTime") + .HasColumnType("datetime(6)"); + + b.Property("SendTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("SenderId") + .HasColumnType("bigint"); + + b.Property("SenderName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TitleArgs") + .IsRequired() + .HasColumnType("json"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WeddingInvite") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ReceiverId", "Id"); + + b.ToTable("mail", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Marriage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("ExpHistory") + .IsRequired() + .HasColumnType("json"); + + b.Property("Partner1Id") + .HasColumnType("bigint"); + + b.Property("Partner1Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Partner2Id") + .HasColumnType("bigint"); + + b.Property("Partner2Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Profile") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("Partner1Id"); + + b.HasIndex("Partner2Id"); + + b.ToTable("marriage", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Medal", b => + { + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("int"); + + b.Property("ExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.HasKey("OwnerId", "Id"); + + b.ToTable("medal", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.MesoListing", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("ExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CharacterId"); + + b.ToTable("meso-market", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Nurturing", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("InteractId") + .HasColumnType("int"); + + b.Property("ClaimedGiftForStage") + .HasColumnType("smallint"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Exp") + .HasColumnType("bigint"); + + b.Property("LastFeedTime") + .HasColumnType("datetime(6)"); + + b.Property("PlayedBy") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("AccountId", "InteractId"); + + b.ToTable("nurturing", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.PetConfig", b => + { + b.Property("ItemUid") + .HasColumnType("bigint"); + + b.Property("LootConfig") + .IsRequired() + .HasColumnType("json"); + + b.Property("PotionConfigs") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("ItemUid"); + + b.ToTable("pet-config", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.PlayerReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Category") + .HasColumnType("tinyint unsigned"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CreateTime") + .HasColumnType("datetime(6)"); + + b.Property("PlayerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReportInfo") + .IsRequired() + .HasColumnType("json"); + + b.Property("ReporterCharacterId") + .HasColumnType("bigint"); + + b.Property("ReporterName") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("player-reports", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Quest", b => + { + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("int"); + + b.Property("CompletionCount") + .HasColumnType("int"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("json"); + + b.Property("EndTime") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("bigint"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("Track") + .HasColumnType("tinyint(1)"); + + b.HasKey("OwnerId", "Id"); + + b.ToTable("quest", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.ServerInfo", b => + { + b.Property("Key") + .HasColumnType("varchar(255)"); + + b.Property("LastModified") + .HasColumnType("datetime(6)"); + + b.HasKey("Key"); + + b.ToTable("server-info", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Shop.CharacterShopData", b => + { + b.Property("ShopId") + .HasColumnType("int"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Interval") + .HasColumnType("tinyint unsigned"); + + b.Property("RestockCount") + .HasColumnType("int"); + + b.Property("RestockTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ShopId", "OwnerId"); + + b.ToTable("character-shop-data", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Shop.CharacterShopItemData", b => + { + b.Property("ShopItemId") + .HasColumnType("int"); + + b.Property("ShopId") + .HasColumnType("int"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Item") + .IsRequired() + .HasColumnType("json"); + + b.Property("StockPurchased") + .HasColumnType("int"); + + b.HasKey("ShopItemId", "ShopId", "OwnerId"); + + b.ToTable("character-shop-item-data", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.SkillTab", b => + { + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Skills") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("CharacterId", "Id"); + + b.HasIndex("CharacterId"); + + b.ToTable("skill-tab", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.SoldMeretMarketItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("MarketId") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("SoldTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("meret-market-sold", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.SoldMesoListing", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("ListedTime") + .HasColumnType("datetime(6)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("SoldTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("meso-market-sold", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.SoldUgcMarketItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("Profit") + .HasColumnType("bigint"); + + b.Property("SoldTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("ugc-market-item-sold", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.SystemBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BeginTime") + .HasColumnType("datetime(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("Function") + .HasColumnType("int"); + + b.Property("FunctionParameter") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Language") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("system-banner", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.UgcMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApartmentNumber") + .HasColumnType("int"); + + b.Property("ExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("Indoor") + .HasColumnType("tinyint(1)"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("MapId"); + + b.HasIndex("OwnerId"); + + b.ToTable("ugcmap", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.UgcMapCube", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Interact") + .HasColumnType("json"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Rotation") + .HasColumnType("float"); + + b.Property("Template") + .HasColumnType("json"); + + b.Property("UgcMapId") + .HasColumnType("bigint"); + + b.Property("X") + .HasColumnType("tinyint"); + + b.Property("Y") + .HasColumnType("tinyint"); + + b.Property("Z") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("UgcMapId"); + + b.ToTable("ugcmap-cube", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.UgcMarketItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Blueprint") + .IsRequired() + .HasColumnType("json"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("ListingEndTime") + .HasColumnType("datetime(6)"); + + b.Property("Look") + .IsRequired() + .HasColumnType("json"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("PromotionEndTime") + .HasColumnType("datetime(6)"); + + b.Property("SalesCount") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("TabId") + .HasColumnType("int"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("Id"); + + b.ToTable("ugc-market-item", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.UgcResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Path") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("ugcresource", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.WeddingHall", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CeremonyTime") + .HasColumnType("datetime(6)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("GuestList") + .IsRequired() + .HasColumnType("json"); + + b.Property("MarriageId") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("PackageHallId") + .HasColumnType("int"); + + b.Property("PackageId") + .HasColumnType("int"); + + b.Property("Public") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("MarriageId") + .IsUnique(); + + b.ToTable("wedding-hall", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Buddy", b => + { + b.HasOne("Maple2.Database.Model.Character", "BuddyCharacter") + .WithMany() + .HasForeignKey("BuddyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BuddyCharacter"); + }); + + modelBuilder.Entity("Maple2.Database.Model.Character", b => + { + b.HasOne("Maple2.Database.Model.Account", null) + .WithMany("Characters") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.CharacterConfig", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.CharacterConfig", "CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Maple2.Database.Model.SkillBook", "SkillBook", b1 => + { + b1.Property("CharacterConfigCharacterId") + .HasColumnType("bigint"); + + b1.Property("ActiveSkillTabId") + .HasColumnType("bigint"); + + b1.Property("MaxSkillTabs") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + + b1.HasKey("CharacterConfigCharacterId"); + + b1.HasIndex("ActiveSkillTabId") + .IsUnique(); + + b1.ToTable("character-config"); + + b1.HasOne("Maple2.Database.Model.SkillTab", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.CharacterConfig.SkillBook#Maple2.Database.Model.SkillBook", "ActiveSkillTabId") + .HasPrincipalKey("Maple2.Database.Model.SkillTab", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("CharacterConfigCharacterId"); + }); + + b.Navigation("SkillBook"); + }); + + modelBuilder.Entity("Maple2.Database.Model.CharacterUnlock", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.CharacterUnlock", "CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.Club", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("LeaderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.ClubMember", b => + { + b.HasOne("Maple2.Database.Model.Character", "Character") + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Maple2.Database.Model.Club", null) + .WithMany("Members") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Maple2.Database.Model.DungeonRecord", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.GameEventUserValue", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.Guild", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.Guild", "LeaderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.GuildApplication", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Maple2.Database.Model.Guild", null) + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.GuildMember", b => + { + b.HasOne("Maple2.Database.Model.Character", "Character") + .WithOne() + .HasForeignKey("Maple2.Database.Model.GuildMember", "CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Maple2.Database.Model.Guild", null) + .WithMany("Members") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Maple2.Database.Model.Home", b => + { + b.HasOne("Maple2.Database.Model.Account", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.Home", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.HomeLayoutCube", b => + { + b.HasOne("Maple2.Database.Model.HomeLayout", null) + .WithMany("Cubes") + .HasForeignKey("HomeLayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.ItemStorage", b => + { + b.HasOne("Maple2.Database.Model.Account", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.ItemStorage", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.Marriage", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("Partner1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("Partner2Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.MesoListing", b => + { + b.HasOne("Maple2.Database.Model.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.PetConfig", b => + { + b.HasOne("Maple2.Database.Model.Item", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.PetConfig", "ItemUid") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.SkillTab", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.SoldUgcMarketItem", b => + { + b.HasOne("Maple2.Database.Model.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.UgcMapCube", b => + { + b.HasOne("Maple2.Database.Model.UgcMap", null) + .WithMany("Cubes") + .HasForeignKey("UgcMapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.WeddingHall", b => + { + b.HasOne("Maple2.Database.Model.Marriage", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.WeddingHall", "MarriageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.Account", b => + { + b.Navigation("Characters"); + }); + + modelBuilder.Entity("Maple2.Database.Model.Club", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Maple2.Database.Model.Guild", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Maple2.Database.Model.HomeLayout", b => + { + b.Navigation("Cubes"); + }); + + modelBuilder.Entity("Maple2.Database.Model.UgcMap", b => + { + b.Navigation("Cubes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Maple2.Server.World/Migrations/20250406064924_DeathCount.cs b/Maple2.Server.World/Migrations/20250406064924_DeathCount.cs new file mode 100644 index 000000000..e613d6a68 --- /dev/null +++ b/Maple2.Server.World/Migrations/20250406064924_DeathCount.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Maple2.Server.World.Migrations +{ + /// + public partial class DeathCount : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DeathTick", + table: "character-config"); + + migrationBuilder.RenameColumn( + name: "DeathCount", + table: "character-config", + newName: "InstantRevivalCount"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "InstantRevivalCount", + table: "character-config", + newName: "DeathCount"); + + migrationBuilder.AddColumn( + name: "DeathTick", + table: "character-config", + type: "bigint", + nullable: false, + defaultValue: 0L); + } + } +} diff --git a/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs b/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs index 09152f683..976b8ccd3 100644 --- a/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs +++ b/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs @@ -324,12 +324,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CharacterId") .HasColumnType("bigint"); - b.Property("DeathCount") - .HasColumnType("int"); - - b.Property("DeathTick") - .HasColumnType("bigint"); - b.Property("ExplorationProgress") .HasColumnType("int"); @@ -348,6 +342,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("HotBars") .HasColumnType("json"); + b.Property("InstantRevivalCount") + .HasColumnType("int"); + b.Property("KeyBinds") .HasColumnType("json"); diff --git a/Maple2.Server.World/Service/WorldService.PlayerConfig.cs b/Maple2.Server.World/Service/WorldService.PlayerConfig.cs index 0f724775a..aff28b4a3 100644 --- a/Maple2.Server.World/Service/WorldService.PlayerConfig.cs +++ b/Maple2.Server.World/Service/WorldService.PlayerConfig.cs @@ -5,10 +5,10 @@ namespace Maple2.Server.World.Service; public partial class WorldService { public override Task PlayerConfig(PlayerConfigRequest request, ServerCallContext context) { - switch (request.BuffCase) { - case PlayerConfigRequest.BuffOneofCase.Get: + switch (request.PlayerConfigCase) { + case PlayerConfigRequest.PlayerConfigOneofCase.Get: return Task.FromResult(Get(request.Get, request.RequesterId)); - case PlayerConfigRequest.BuffOneofCase.Save: + case PlayerConfigRequest.PlayerConfigOneofCase.Save: return Task.FromResult(Save(request.Save, request.RequesterId)); default: return Task.FromResult(new PlayerConfigResponse()); @@ -16,7 +16,7 @@ public override Task PlayerConfig(PlayerConfigRequest requ } private PlayerConfigResponse Get(PlayerConfigRequest.Types.Get get, long requesterId) { - (List buffs, List skillCooldowns) = playerConfigLookUp.Retrieve(requesterId); + (List buffs, List skillCooldowns, DeathInfo death) = playerConfigLookUp.Retrieve(requesterId); return new PlayerConfigResponse { Buffs = { buffs.Select(b => new BuffInfo { @@ -38,11 +38,12 @@ private PlayerConfigResponse Get(PlayerConfigRequest.Types.Get get, long request Charges = c.Charges, }), }, + DeathInfo = death, }; } private PlayerConfigResponse Save(PlayerConfigRequest.Types.Save save, long requesterId) { - playerConfigLookUp.Save(save.Buffs.ToList(), save.SkillCooldowns.ToList(), requesterId); + playerConfigLookUp.Save(save.Buffs.ToList(), save.SkillCooldowns.ToList(), save.DeathInfo, requesterId); return new PlayerConfigResponse(); } } diff --git a/Maple2.Server.World/Service/WorldService.Sync.cs b/Maple2.Server.World/Service/WorldService.Sync.cs index c39e6258d..29f05fb48 100644 --- a/Maple2.Server.World/Service/WorldService.Sync.cs +++ b/Maple2.Server.World/Service/WorldService.Sync.cs @@ -36,6 +36,7 @@ public override Task PlayerInfo(PlayerInfoRequest request, S CurrentHp = info.CurrentHp, TotalHp = info.TotalHp, }, + DeathState = (int) info.DeathState, GuildName = info.GuildName, GuildId = info.GuildId, Home = new HomeUpdate {