diff --git a/Maple2.Database/Model/CharacterConfig.cs b/Maple2.Database/Model/CharacterConfig.cs index 1e2475f89..ae6094115 100644 --- a/Maple2.Database/Model/CharacterConfig.cs +++ b/Maple2.Database/Model/CharacterConfig.cs @@ -23,7 +23,6 @@ internal class CharacterConfig { public IList? FavoriteStickers { get; set; } public IList? FavoriteDesigners { get; set; } public IDictionary? Lapenshards { get; set; } - public List? SkillCooldowns { get; set; } public long DeathTick { get; set; } public int DeathCount { get; set; } public IDictionary? GatheringCounts { get; set; } @@ -56,7 +55,6 @@ public static void Configure(EntityTypeBuilder builder) { builder.Property(config => config.FavoriteStickers).HasJsonConversion(); builder.Property(config => config.FavoriteDesigners).HasJsonConversion(); builder.Property(config => config.Lapenshards).HasJsonConversion(); - builder.Property(config => config.SkillCooldowns).HasJsonConversion(); builder.Property(config => config.GatheringCounts).HasJsonConversion(); builder.Property(config => config.GuideRecords).HasJsonConversion(); @@ -86,29 +84,6 @@ public static implicit operator Maple2.Model.Game.SkillMacro(SkillMacro? other) } } -internal class SkillCooldown { - public int SkillId { get; set; } - public int OriginSkillId { get; set; } - public long EndTick { get; set; } - - [return: NotNullIfNotNull(nameof(other))] - public static implicit operator SkillCooldown?(Maple2.Model.Game.SkillCooldown? other) { - return other == null ? null : new SkillCooldown { - SkillId = other.SkillId, - OriginSkillId = other.OriginSkillId, - EndTick = other.EndTick, - }; - } - - [return: NotNullIfNotNull(nameof(other))] - public static implicit operator Maple2.Model.Game.SkillCooldown?(SkillCooldown? other) { - return other == null ? null : new Maple2.Model.Game.SkillCooldown(other.SkillId) { - OriginSkillId = other.OriginSkillId, - EndTick = other.EndTick, - }; - } -} - internal class Wardrobe { public int Type { get; set; } public int KeyId { get; set; } diff --git a/Maple2.Database/Storage/Game/GameStorage.User.cs b/Maple2.Database/Storage/Game/GameStorage.User.cs index 8aa5dd983..2650c1338 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, IList? SkillCooldowns, long DeathTick, int DeathCount, int ExplorationProgress, IDictionary?, + IDictionary? Lapenshards, long DeathTick, int DeathCount, 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, null, 0, 0, 0, null, null, null, null, null, null); + return (null, null, null, null, null, null, null, 0, 0, 0, null, null, null, null, null, null); } SkillBook? skillBook = config.SkillBook == null ? null : new SkillBook { @@ -344,7 +344,6 @@ public bool SaveCharacter(Character character) { config.FavoriteStickers?.Select(stickers => stickers).ToList(), config.FavoriteDesigners?.Select(designer => designer).ToList(), config.Lapenshards, - config.SkillCooldowns?.Select(cooldown => cooldown).ToList(), config.DeathTick, config.DeathCount, config.ExplorationProgress, @@ -390,9 +389,6 @@ public bool SaveCharacterConfig( config.FavoriteStickers = favoriteStickers; config.FavoriteDesigners = favoriteDesigners; config.Lapenshards = lapenshards; - config.SkillCooldowns = skillCooldowns.Where(cooldown => cooldown.EndTick > Environment.TickCount64) - .Select(cooldown => cooldown) - .ToList(); config.DeathTick = deathTick; config.DeathCount = deathCount; config.ExplorationProgress = explorationProgress; diff --git a/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs b/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs index e2f53fd35..56633714f 100644 --- a/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs +++ b/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs @@ -33,6 +33,7 @@ protected override IEnumerable Map() { IntervalTick: data.BasicProperty.intervalTick, DelayTick: data.BasicProperty.delayTick, MaxCount: data.BasicProperty.maxBuffCount, + UseInGameTime: data.BasicProperty.useInGameTime, KeepOnDeath: data.BasicProperty.deadKeepEffect, RemoveOnLogout: data.BasicProperty.logoutClearEffect, RemoveOnLeaveField: data.BasicProperty.leaveFieldClearEffect, diff --git a/Maple2.File.Ingest/Mapper/SkillMapper.cs b/Maple2.File.Ingest/Mapper/SkillMapper.cs index f5e0d9044..deaa9a317 100644 --- a/Maple2.File.Ingest/Mapper/SkillMapper.cs +++ b/Maple2.File.Ingest/Mapper/SkillMapper.cs @@ -121,7 +121,11 @@ protected override IEnumerable Map() { MaxLevel: levels.Keys.Max()), State: new SkillMetadataState( InBattle: data.basic.stateAttr.battle == 1, - SuperArmor: data.basic.stateAttr.superArmor), + SuperArmor: data.basic.stateAttr.superArmor, + UseInGameTime: data.basic.stateAttr.useInGameTime == 1, + IgnoreReduceCooldown: data.basic.stateAttr.ignoreReduceCooldown == 1, + CooldownGroupId: data.basic.stateAttr.cooldownGroupID, + RechargeMaxCount: data.basic.stateAttr.rechargeMaxCount), Levels: levels ); } diff --git a/Maple2.Model/Enum/PlayerObjectFlag.cs b/Maple2.Model/Enum/PlayerObjectFlag.cs index 54ec69aa3..67a835ad0 100644 --- a/Maple2.Model/Enum/PlayerObjectFlag.cs +++ b/Maple2.Model/Enum/PlayerObjectFlag.cs @@ -13,4 +13,5 @@ public enum PlayerObjectFlag : byte { Motto = 16, GearScore = 32, State = 64, + All = Dead | Position | Level | Job | Motto | GearScore | State, } diff --git a/Maple2.Model/Game/User/SkillCooldown.cs b/Maple2.Model/Game/User/SkillCooldown.cs index c61e5befc..eb8e90efe 100644 --- a/Maple2.Model/Game/User/SkillCooldown.cs +++ b/Maple2.Model/Game/User/SkillCooldown.cs @@ -5,17 +5,22 @@ namespace Maple2.Model.Game; public class SkillCooldown : IByteSerializable { public readonly int SkillId; - public int OriginSkillId { get; init; } + public readonly short Level; + public int GroupId { get; init; } public long EndTick; + public int Charges; - public SkillCooldown(int skillId) { + public SkillCooldown(int skillId, short level) { SkillId = skillId; + Level = level; + GroupId = 0; + Charges = 0; } public void WriteTo(IByteWriter writer) { writer.WriteInt(SkillId); - writer.WriteInt(OriginSkillId); + writer.WriteInt(GroupId); writer.WriteInt((int) EndTick); - writer.WriteInt(); // Unknown Tick. Origin Skill tick maybe? + writer.WriteInt(Charges); } } diff --git a/Maple2.Model/Metadata/AdditionalEffectMetadata.cs b/Maple2.Model/Metadata/AdditionalEffectMetadata.cs index f93017d03..510194e16 100644 --- a/Maple2.Model/Metadata/AdditionalEffectMetadata.cs +++ b/Maple2.Model/Metadata/AdditionalEffectMetadata.cs @@ -28,6 +28,7 @@ public record AdditionalEffectMetadataProperty( int IntervalTick, int DelayTick, int MaxCount, + bool UseInGameTime, bool KeepOnDeath, bool RemoveOnLogout, bool RemoveOnLeaveField, diff --git a/Maple2.Model/Metadata/SkillMetadata.cs b/Maple2.Model/Metadata/SkillMetadata.cs index 69a15221d..9ceefbf09 100644 --- a/Maple2.Model/Metadata/SkillMetadata.cs +++ b/Maple2.Model/Metadata/SkillMetadata.cs @@ -41,7 +41,11 @@ public record SkillMetadataProperty( public record SkillMetadataState( bool InBattle, - int SuperArmor); // 0, 1, 3 + int SuperArmor, // 0, 1, 3 + bool UseInGameTime, + int CooldownGroupId, + bool IgnoreReduceCooldown, + int RechargeMaxCount); public record SkillMetadataLevel( BeginCondition Condition, diff --git a/Maple2.Server.Core/proto/world/world.proto b/Maple2.Server.Core/proto/world/world.proto index 688b18a03..98938f254 100644 --- a/Maple2.Server.Core/proto/world/world.proto +++ b/Maple2.Server.Core/proto/world/world.proto @@ -54,6 +54,8 @@ service World { rpc PlayerWarp(maple2.PlayerWarpRequest) returns (maple2.PlayerWarpResponse); // Admin rpc Admin(maple2.AdminRequest) returns (maple2.AdminResponse); + // Buff + rpc PlayerConfig(PlayerConfigRequest) returns (PlayerConfigResponse); } enum Server { @@ -521,3 +523,41 @@ message AddChannelResponse { int32 grpc_port = 2; int32 game_channel = 3; } + +message PlayerConfigRequest { + message Save { + repeated BuffInfo buffs = 1; + repeated SkillCooldownInfo skill_cooldowns = 2; + } + + message Get { } + + int64 requester_id = 1; + oneof buff { + Save save = 2; + Get get = 3; + } +} + +message PlayerConfigResponse { + repeated BuffInfo buffs = 1; + repeated SkillCooldownInfo skill_cooldowns = 2; +} + +message BuffInfo { + int32 id = 1; + int32 level = 2; + int32 ms_remaining = 3; + int64 stop_time = 4; + bool enabled = 5; + int32 stacks = 6; +} + +message SkillCooldownInfo { + int32 skill_id = 1; + int32 skill_level = 2; + int32 group_id = 3; + int32 ms_remaining = 4; + int64 stop_time = 5; + int32 charges = 6; +} diff --git a/Maple2.Server.Game/Commands/BuffCommand.cs b/Maple2.Server.Game/Commands/BuffCommand.cs index e61501a9b..e8eb58cd8 100644 --- a/Maple2.Server.Game/Commands/BuffCommand.cs +++ b/Maple2.Server.Game/Commands/BuffCommand.cs @@ -46,17 +46,18 @@ private void Handle(InvocationContext ctx, int buffId, int level, int stack, int } if (duration > 0) { - duration *= 1000; // convert to milliseconds + duration = (int) TimeSpan.FromSeconds(1).TotalMilliseconds * duration; } + long startTick = session.Field.FieldTick; if (all) { foreach (FieldPlayer player in session.Field.Players.Values) { if (remove) { player.Buffs.Remove(buffId); } else { - player.Buffs.AddBuff(session.Player, player, buffId, (short) level, duration); + player.Buffs.AddBuff(session.Player, player, buffId, (short) level, startTick, duration); if (stack > 1) { - player.Buffs.Buffs[buffId].Stack(stack); + player.Buffs.Buffs[buffId].Stack(player.Field.FieldTick, stack); session.Field?.Broadcast(BuffPacket.Update(player.Buffs.Buffs[buffId])); } } @@ -74,9 +75,9 @@ private void Handle(InvocationContext ctx, int buffId, int level, int stack, int player.Buffs.Remove(buffId); return; } - player.Buffs.AddBuff(session.Player, player, buffId, (short) level, duration); + player.Buffs.AddBuff(session.Player, player, buffId, (short) level, startTick, duration); if (stack > 1) { - player.Buffs.Buffs[buffId].Stack(stack); + player.Buffs.Buffs[buffId].Stack(player.Field.FieldTick, stack); session.Field.Broadcast(BuffPacket.Update(player.Buffs.Buffs[buffId])); } } else { @@ -84,9 +85,9 @@ private void Handle(InvocationContext ctx, int buffId, int level, int stack, int session.Player.Buffs.Remove(buffId); return; } - session.Player.Buffs.AddBuff(session.Player, session.Player, buffId, (short) level, duration); + session.Player.Buffs.AddBuff(session.Player, session.Player, buffId, (short) level, startTick, duration); if (stack > 1) { - session.Player.Buffs.Buffs[buffId].Stack(stack); + session.Player.Buffs.Buffs[buffId].Stack(session.Field.FieldTick, stack); session.Field?.Broadcast(BuffPacket.Update(session.Player.Buffs.Buffs[buffId])); } } diff --git a/Maple2.Server.Game/Manager/Config/BuffManager.cs b/Maple2.Server.Game/Manager/Config/BuffManager.cs index 66760abd6..d10612112 100644 --- a/Maple2.Server.Game/Manager/Config/BuffManager.cs +++ b/Maple2.Server.Game/Manager/Config/BuffManager.cs @@ -1,11 +1,14 @@ using System.Collections.Concurrent; +using Maple2.Database.Extensions; using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.Model.Metadata; using Maple2.Server.Game.Model; using Maple2.Server.Game.Model.Skill; using Maple2.Server.Game.Packets; +using Maple2.Server.Game.Session; using Maple2.Server.Game.Util; +using Maple2.Server.World.Service; using Maple2.Tools.Extensions; using Serilog; @@ -58,14 +61,14 @@ public void LoadFieldBuffs() { } } - public void AddBuff(IActor caster, IActor owner, int id, short level, int durationSec = -1, bool notifyField = true) { + public void AddBuff(IActor caster, IActor owner, int id, short level, long startTick, int durationMs = -1, bool notifyField = true) { if (!owner.Field.SkillMetadata.TryGetEffect(id, level, out AdditionalEffectMetadata? additionalEffect)) { logger.Error("Invalid buff: {SkillId},{Level}", id, level); return; } - if (durationSec < 0) { - durationSec = additionalEffect.Property.DurationTick; + if (durationMs < 0) { + durationMs = additionalEffect.Property.DurationTick; } // Check if immune to any present buffs @@ -79,7 +82,7 @@ public void AddBuff(IActor caster, IActor owner, int id, short level, int durati if (level > existing.Level) { } - if (!existing.Stack()) { + if (!existing.Stack(startTick)) { return; } if (notifyField) { @@ -97,9 +100,9 @@ public void AddBuff(IActor caster, IActor owner, int id, short level, int durati } } - var buff = new Buff(owner.Field, additionalEffect, NextLocalId(), caster, owner, durationSec); + var buff = new Buff(additionalEffect, NextLocalId(), caster, owner, startTick, durationMs); if (!Buffs.TryAdd(id, buff)) { - Buffs[id].Stack(); + Buffs[id].Stack(startTick); owner.Field.Broadcast(BuffPacket.Update(buff)); return; } @@ -309,7 +312,7 @@ public void LeaveField() { private void AddMapBuffs() { foreach (MapEntranceBuff buff in Actor.Field.Metadata.EntranceBuffs) { - AddBuff(Actor, Actor, buff.Id, buff.Level); + AddBuff(Actor, Actor, buff.Id, buff.Level, Actor.Field.FieldTick); } if (Actor.Field.Metadata.Property.Type == MapType.Pvp) { @@ -325,21 +328,21 @@ private void AddMapBuffs() { } if (Actor.Field.Metadata.Property.Region == MapRegion.ShadowWorld) { - AddBuff(Actor, Actor, Constant.shadowWorldBuffHpUp, 1); - AddBuff(Actor, Actor, Constant.shadowWorldBuffMoveProtect, 1); + AddBuff(Actor, Actor, Constant.shadowWorldBuffHpUp, 1, Actor.Field.FieldTick); + AddBuff(Actor, Actor, Constant.shadowWorldBuffMoveProtect, 1, Actor.Field.FieldTick); } } public void AddItemBuffs(Item item) { foreach (ItemMetadataAdditionalEffect buff in item.Metadata.AdditionalEffects) { - AddBuff(Actor, Actor, buff.Id, buff.Level); + AddBuff(Actor, Actor, buff.Id, buff.Level, Actor.Field.FieldTick); } if (item.Socket != null) { foreach (ItemGemstone? gem in item.Socket.Sockets) { if (gem != null && Actor.Field.ItemMetadata.TryGet(gem.ItemId, out ItemMetadata? metadata)) { foreach (ItemMetadataAdditionalEffect buff in metadata.AdditionalEffects) { - AddBuff(Actor, Actor, buff.Id, buff.Level); + AddBuff(Actor, Actor, buff.Id, buff.Level, Actor.Field.FieldTick); } } } @@ -362,6 +365,23 @@ public void RemoveItemBuffs(Item item) { } } + public void SetCacheBuffs(IList buffs, long currentTick) { + if (Actor is not FieldPlayer player) { + return; + } + + foreach (BuffInfo info in buffs) { + if (!player.Field.SkillMetadata.TryGetEffect(info.Id, (short) info.Level, out AdditionalEffectMetadata? additionalEffect)) { + logger.Error("Invalid buff: {SkillId},{Level}", info.Id, info.Level); + continue; + } + + if (additionalEffect.Property.UseInGameTime || info.MsRemaining > 0) { + AddBuff(Actor, Actor, info.Id, (short) info.Level, currentTick, info.MsRemaining); + } + } + } + public bool Remove(int id) { //TODO: Check if buff is removable/should be removed if (!Buffs.Remove(id, out Buff? buff)) { @@ -406,4 +426,8 @@ private bool CheckImmunity(int newBuffId, BuffCategory category) { return true; } + + public List GetSaveCacheBuffs() { + return Buffs.Values.Where(buff => !buff.Metadata.Property.RemoveOnLogout).ToList(); + } } diff --git a/Maple2.Server.Game/Manager/Config/ConfigManager.cs b/Maple2.Server.Game/Manager/Config/ConfigManager.cs index 1e5df41d4..c0256f6e4 100644 --- a/Maple2.Server.Game/Manager/Config/ConfigManager.cs +++ b/Maple2.Server.Game/Manager/Config/ConfigManager.cs @@ -6,8 +6,10 @@ 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; namespace Maple2.Server.Game.Manager.Config; @@ -51,7 +53,6 @@ public ConfigManager(GameStorage.Request db, GameSession session) { IList? FavoriteStickers, IList? FavoriteDesigners, IDictionary? Lapenshards, - IList? SkillCooldowns, long deathPenaltyTick, int DeathCounter, int ExplorationProgress, @@ -89,15 +90,6 @@ public ConfigManager(GameStorage.Request db, GameSession session) { DeathCount = load.DeathCounter; ExplorationProgress = load.ExplorationProgress; - if (load.SkillCooldowns != null) { - foreach (SkillCooldown cooldown in load.SkillCooldowns) { - if (Environment.TickCount64 > cooldown.EndTick) { - continue; - } - skillCooldowns[cooldown.SkillId] = cooldown; - } - } - statAttributes = new StatAttributes(); if (load.StatPoints != null) { foreach ((AttributePointSource source, int amount) in load.StatPoints) { @@ -166,16 +158,29 @@ public void LoadWardrobe() { } #region SkillCooldowns - public void SaveSkillCooldown(SkillMetadata skill) { - var cooldown = new SkillCooldown(skill.Id) { - EndTick = (long) (skill.Data.Condition.CooldownTime * TimeSpan.FromSeconds(1).TotalMilliseconds) + Environment.TickCount64, - OriginSkillId = skill.Data.Change?.Origin.Id ?? 0, - }; - skillCooldowns[skill.Id] = cooldown; + public void SetCacheSkillCooldowns(IList cooldowns, long currentTick) { + foreach (SkillCooldownInfo cooldown in cooldowns) { + skillCooldowns[cooldown.SkillId] = new SkillCooldown(cooldown.SkillId, (short) cooldown.SkillLevel) { + EndTick = currentTick + cooldown.MsRemaining, + GroupId = cooldown.GroupId, + }; + } + } + public void SaveSkillCooldown(SkillMetadata skill, long startTick) { + if (!skillCooldowns.TryGetValue(skill.Id, out SkillCooldown? cooldown) || startTick > cooldown.EndTick) { + cooldown = new SkillCooldown(skill.Id, skill.Level) { + EndTick = (long) (skill.Data.Condition.CooldownTime * TimeSpan.FromSeconds(1).TotalMilliseconds) + startTick, + GroupId = skill.State.CooldownGroupId, + }; + skillCooldowns[skill.Id] = cooldown; + } + if (skill.State.RechargeMaxCount > 0) { + cooldown.Charges = Math.Clamp(cooldown.Charges + 1, cooldown.Charges, skill.State.RechargeMaxCount); + } } - public void SetSkillCooldown(int skillId, int endTick = 0) { - var cooldown = new SkillCooldown(skillId) { + public void SetSkillCooldown(int skillId, short level, int endTick = 0) { + var cooldown = new SkillCooldown(skillId, level) { EndTick = endTick, }; skillCooldowns[skillId] = cooldown; @@ -230,7 +235,7 @@ public void UpdatePremiumTime(long hours) { public void RefreshPremiumClubBuffs() { if (session.Player.Value.Account.PremiumTime > DateTime.Now.ToEpochSeconds()) { foreach ((int buffId, PremiumClubTable.Buff buff) in session.TableMetadata.PremiumClubTable.Buffs) { - session.Player.Buffs.AddBuff(session.Player, session.Player, buff.Id, buff.Level); + session.Player.Buffs.AddBuff(session.Player, session.Player, buff.Id, buff.Level, session.Field.FieldTick); } } } diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index e00d0b488..310761e53 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -81,7 +81,7 @@ public virtual void ApplyEffect(IActor caster, IActor owner, SkillEffectMetadata Debug.Assert(effect.Condition != null); foreach (SkillEffectMetadata.Skill skill in effect.Skills) { - Buffs.AddBuff(caster, owner, skill.Id, skill.Level, notifyField: notifyField); + Buffs.AddBuff(caster, owner, skill.Id, skill.Level, Field.FieldTick, notifyField: notifyField); } } @@ -137,7 +137,7 @@ public virtual void Reflect(IActor target) { if (record.Counter >= record.Metadata.Count) { Buffs.Remove(record.SourceBuffId); } - target.Buffs.AddBuff(this, target, record.Metadata.EffectId, record.Metadata.EffectLevel); + target.Buffs.AddBuff(this, target, record.Metadata.EffectId, record.Metadata.EffectLevel, Field.FieldTick); // TODO: Reflect should also amend the target's damage record from Reflect.ReflectValues and ReflectRates } @@ -165,12 +165,13 @@ public virtual void TargetAttack(SkillRecord record) { Field.Broadcast(SkillDamagePacket.Damage(damage)); + long startTick = Field.FieldTick; foreach (SkillEffectMetadata effect in record.Attack.Skills) { if (effect.Condition != null) { foreach (IActor actor in record.Targets) { IActor owner = GetTarget(effect.Condition.Target, record.Caster, actor); if (effect.Condition.Condition.Check(record.Caster, owner, actor)) { - actor.ApplyEffect(record.Caster, owner, effect); + actor.ApplyEffect(record.Caster, owner, effect, startTick); } } } else if (effect.Splash != null) { @@ -181,9 +182,9 @@ public virtual void TargetAttack(SkillRecord record) { } } - public virtual void ApplyEffect(IActor caster, IActor target, SkillEffectMetadata effect) { + public virtual void ApplyEffect(IActor caster, IActor target, SkillEffectMetadata effect, long startTick) { foreach (SkillEffectMetadata.Skill skill in effect.Skills) { - Buffs.AddBuff(caster, target, skill.Id, skill.Level); + Buffs.AddBuff(caster, target, skill.Id, skill.Level, startTick); } } diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AiState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AiState.cs index 389c98735..cf3c5ca35 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AiState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/AiState.cs @@ -525,7 +525,7 @@ private void ProcessNode(BuffNode node) { return; } - target.Buffs.AddBuff(actor, actor, node.Id, node.Level); + target.Buffs.AddBuff(actor, actor, node.Id, node.Level, actor.Field.FieldTick); } private void ProcessNode(TargetEffectNode node) { diff --git a/Maple2.Server.Game/Model/Field/Actor/IActor.cs b/Maple2.Server.Game/Model/Field/Actor/IActor.cs index 1df9e2016..e7a0f0d1b 100644 --- a/Maple2.Server.Game/Model/Field/Actor/IActor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/IActor.cs @@ -21,7 +21,7 @@ public interface IActor : IFieldEntity { public bool IsDead { get; } public IPrism Shape { get; } - public virtual void ApplyEffect(IActor caster, IActor owner, SkillEffectMetadata effect) { } + public virtual void ApplyEffect(IActor caster, IActor owner, SkillEffectMetadata effect, long startTick) { } public virtual void ApplyDamage(IActor caster, DamageRecord damage, SkillMetadataAttack attack) { } public virtual void TargetAttack(SkillRecord record) { } diff --git a/Maple2.Server.Game/Model/Field/Buff.cs b/Maple2.Server.Game/Model/Field/Buff.cs index 636e06901..5ef05dbab 100644 --- a/Maple2.Server.Game/Model/Field/Buff.cs +++ b/Maple2.Server.Game/Model/Field/Buff.cs @@ -11,7 +11,7 @@ namespace Maple2.Server.Game.Model; public class Buff : IUpdatable, IByteSerializable { - private readonly FieldManager field; + private FieldManager Field => Owner.Field; public readonly AdditionalEffectMetadata Metadata; public readonly int ObjectId; public long CastUid { get; set; } @@ -38,8 +38,7 @@ public class Buff : IUpdatable, IByteSerializable { private readonly ILogger logger = Log.ForContext(); - public Buff(FieldManager field, AdditionalEffectMetadata metadata, int objectId, IActor caster, IActor owner, int duration) { - this.field = field; + public Buff(AdditionalEffectMetadata metadata, int objectId, IActor caster, IActor owner, long startTick, int durationMs) { Metadata = metadata; ObjectId = objectId; @@ -49,23 +48,19 @@ public Buff(FieldManager field, AdditionalEffectMetadata metadata, int objectId, // Buffs with IntervalTick=0 will just proc a single time IntervalTick = metadata.Property.IntervalTick > 0 ? metadata.Property.IntervalTick : metadata.Property.DurationTick + 1000; - Stack(duration: duration); - NextProcTick = StartTick + this.Metadata.Property.DelayTick + this.Metadata.Property.IntervalTick; + Stack(startTick, durationMs: durationMs); + NextProcTick = startTick + Metadata.Property.DelayTick + Metadata.Property.IntervalTick; UpdateEnabled(false); canProc = metadata.Property.KeepCondition != BuffKeepCondition.UnlimitedDuration; - canExpire = metadata.Property.KeepCondition != BuffKeepCondition.UnlimitedDuration && EndTick >= StartTick; + canExpire = metadata.Property.KeepCondition != BuffKeepCondition.UnlimitedDuration && EndTick >= startTick; } - public bool Stack(int amount = 1, int duration = 0) { + public bool Stack(long startTick, int amount = 1, int durationMs = 0) { Stacks = Math.Min(Stacks + amount, Metadata.Property.MaxCount); - StartTick = Environment.TickCount64; + StartTick = startTick; if (Stacks == 1 || Metadata.Property.ResetCondition != BuffResetCondition.PersistEndTick) { - EndTick = StartTick + Metadata.Property.DurationTick; - } - - if (duration > 0) { - EndTick = StartTick + duration; + EndTick = StartTick + durationMs; } return true; } @@ -113,7 +108,7 @@ private bool UpdateEnabled(bool notifyField = true) { if (Enabled != enabled) { Enabled = enabled; if (notifyField) { - field.Broadcast(BuffPacket.Update(this)); + Field.Broadcast(BuffPacket.Update(this)); } } @@ -140,17 +135,17 @@ private void Proc() { // logger.Error("Buff Condition-Effect unimplemented from {Id} on {Owner}", Id, Owner.ObjectId); switch (effect.Condition.Target) { case SkillEntity.Target: - Owner.ApplyEffect(Caster, Owner, effect); + Owner.ApplyEffect(Caster, Owner, effect, Field.FieldTick); break; case SkillEntity.Caster: - Caster.ApplyEffect(Caster, Owner, effect); + Caster.ApplyEffect(Caster, Owner, effect, Field.FieldTick); break; default: logger.Error("Invalid Buff Target: {Target}", effect.Condition.Target); break; } } else if (effect.Splash != null) { - field.AddSkill(Caster, effect, [Owner.Position], Owner.Rotation); + Caster.Field.AddSkill(Caster, effect, [Owner.Position], Owner.Rotation); } } @@ -181,9 +176,9 @@ private void ApplyRecovery() { } if (updated.Count > 0) { - field.Broadcast(StatsPacket.Update(Owner, updated.ToArray())); + Field.Broadcast(StatsPacket.Update(Owner, updated.ToArray())); } - field.Broadcast(SkillDamagePacket.Heal(record)); + Field.Broadcast(SkillDamagePacket.Heal(record)); } private void ApplyDotDamage() { @@ -212,11 +207,11 @@ private void ApplyDotDamage() { return; } - field.Broadcast(StatsPacket.Update(Owner, targetUpdated.ToArray())); - field.Broadcast(SkillDamagePacket.DotDamage(record)); + Field.Broadcast(StatsPacket.Update(Owner, targetUpdated.ToArray())); + Field.Broadcast(SkillDamagePacket.DotDamage(record)); if (record.RecoverHp != 0) { Caster.Stats.Values[BasicAttribute.Health].Add(record.RecoverHp); - field.Broadcast(StatsPacket.Update(Caster, BasicAttribute.Health)); + Field.Broadcast(StatsPacket.Update(Caster, BasicAttribute.Health)); } } @@ -227,9 +222,9 @@ private void ApplyDotBuff() { AdditionalEffectMetadataDot.DotBuff dotBuff = Metadata.Dot.Buff; if (dotBuff.Target == SkillEntity.Target) { - Owner.Buffs.AddBuff(Caster, Owner, dotBuff.Id, dotBuff.Level); + Owner.Buffs.AddBuff(Caster, Owner, dotBuff.Id, dotBuff.Level, Field.FieldTick); } else { - Caster.Buffs.AddBuff(Caster, Owner, dotBuff.Id, dotBuff.Level); + Caster.Buffs.AddBuff(Caster, Owner, dotBuff.Id, dotBuff.Level, Field.FieldTick); } } @@ -271,7 +266,7 @@ public void ModifyDuration() { if (NextProcTick < EndTick) { canProc = true; } - field.Broadcast(BuffPacket.Update(buff)); + Field.Broadcast(BuffPacket.Update(buff)); } } diff --git a/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs b/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs index 2d6f30de9..c27b74d49 100644 --- a/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs +++ b/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs @@ -215,11 +215,11 @@ private void ApplyEffect(SkillMetadataAttack attack, params IActor[] targets) { switch (effect.Condition.Target) { case SkillEntity.Owner: foreach (IActor target in targets) { - target.ApplyEffect(Caster, target, effect); + target.ApplyEffect(Caster, target, effect, Field.FieldTick); } break; case SkillEntity.Target: - Caster.ApplyEffect(Caster, Caster, effect); + Caster.ApplyEffect(Caster, Caster, effect, Field.FieldTick); break; default: logger.Error("Invalid FieldSkill Target: {Target}", effect.Condition.Target); diff --git a/Maple2.Server.Game/PacketHandlers/InsigniaHandler.cs b/Maple2.Server.Game/PacketHandlers/InsigniaHandler.cs index 182da698b..88799b5bd 100644 --- a/Maple2.Server.Game/PacketHandlers/InsigniaHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/InsigniaHandler.cs @@ -62,7 +62,7 @@ public override void Handle(GameSession session, IByteReader packet) { } if (display && newInsigniaMetadata.BuffId > 0) { - session.Player.Buffs.AddBuff(session.Player, session.Player, newInsigniaMetadata.BuffId, newInsigniaMetadata.BuffLevel); + session.Player.Buffs.AddBuff(session.Player, session.Player, newInsigniaMetadata.BuffId, newInsigniaMetadata.BuffLevel, session.Field.FieldTick); } session.Field.Broadcast(InsigniaPacket.Update(session.Player.ObjectId, insigniaId, display)); diff --git a/Maple2.Server.Game/PacketHandlers/ItemPickupHandler.cs b/Maple2.Server.Game/PacketHandlers/ItemPickupHandler.cs index e4e227521..9a04f68ac 100644 --- a/Maple2.Server.Game/PacketHandlers/ItemPickupHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ItemPickupHandler.cs @@ -56,7 +56,7 @@ public override void Handle(GameSession session, IByteReader packet) { // Items with Pick up effects are not added to player inventory. if (item.Metadata.AdditionalEffects.Any(buff => buff.PickUpEffect)) { foreach (ItemMetadataAdditionalEffect additionalEffect in item.Metadata.AdditionalEffects.Where(buff => buff.PickUpEffect)) { - session.Player.Buffs.AddBuff(session.Player, session.Player, additionalEffect.Id, additionalEffect.Level); + session.Player.Buffs.AddBuff(session.Player, session.Player, additionalEffect.Id, additionalEffect.Level, session.Field.FieldTick); } return; } diff --git a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs index 0ac328541..c0c216387 100644 --- a/Maple2.Server.Game/PacketHandlers/SkillHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/SkillHandler.cs @@ -154,17 +154,18 @@ private void HandleUse(GameSession session, IByteReader packet) { session.Player.InBattle = metadata.State.InBattle; session.ActiveSkills.Add(record); - session.Field?.Broadcast(SkillPacket.Use(record)); + session.Field.Broadcast(SkillPacket.Use(record)); - SkillMetadataMotionProperty motion = metadata.Data.Motions[0].MotionProperty; + SkillMetadataMotionProperty motion = metadata.Data.Motions.First().MotionProperty; session.Player.AnimationState.TryPlaySequence(motion.SequenceName, motion.SequenceSpeed, AnimationType.Skill); + long startTick = session.Field.FieldTick; foreach (SkillEffectMetadata effect in metadata.Data.Skills) { - session.Player.ApplyEffect(session.Player, session.Player, effect); + session.Player.ApplyEffect(session.Player, session.Player, effect, startTick); } - session.Config.SaveSkillCooldown(metadata); + session.Config.SaveSkillCooldown(metadata, startTick); } private void HandlePoint(GameSession session, IByteReader packet) { diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index 9da1007c5..a4414cff7 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -247,6 +247,19 @@ public bool EnterServer(long accountId, Guid machineId, MigrateInResponse migrat Buddy.Load(); + try { + PlayerConfigResponse configResponse = World.PlayerConfig(new PlayerConfigRequest { + Get = new PlayerConfigRequest.Types.Get(), + RequesterId = CharacterId, + }); + long currentTick = Environment.TickCount64; + Buffs.SetCacheBuffs(configResponse.Buffs, currentTick); + Config.SetCacheSkillCooldowns(configResponse.SkillCooldowns, currentTick); + + } catch (RpcException ex) { + Logger.Warning(ex, "Failed to load cache player config"); + } + Send(SurvivalPacket.UpdateStats(player.Account)); Send(TimeSyncPacket.Reset(DateTimeOffset.UtcNow)); @@ -724,6 +737,7 @@ protected override void Dispose(bool disposing) { club.Dispose(); } + SaveCacheConfig(); using (GameStorage.Request db = GameStorage.Context()) { db.BeginTransaction(); db.SavePlayer(Player); @@ -741,6 +755,44 @@ protected override void Dispose(bool disposing) { base.Dispose(disposing); } + return; + + void SaveCacheConfig() { + List buffs = Buffs.GetSaveCacheBuffs(); + IList skillCooldowns = Config.GetCurrentSkillCooldowns(); + + long stopTime = DateTime.Now.ToEpochSeconds(); + long fieldTick = Field.FieldTick; + try { + PlayerConfigResponse _ = World.PlayerConfig(new PlayerConfigRequest { + Save = new PlayerConfigRequest.Types.Save { + Buffs = { + buffs.Select(buff => new BuffInfo { + Id = buff.Id, + Level = buff.Level, + MsRemaining = (int) (buff.EndTick - fieldTick), + Stacks = buff.Stacks, + Enabled = buff.Enabled, + StopTime = stopTime, + }), + }, + SkillCooldowns = { + skillCooldowns.Select(cooldown => new SkillCooldownInfo { + SkillId = cooldown.SkillId, + SkillLevel = cooldown.Level, + GroupId = cooldown.GroupId, + MsRemaining = (int) (cooldown.EndTick - fieldTick), + StopTime = stopTime, + Charges = cooldown.Charges, + }), + }, + }, + RequesterId = CharacterId, + }); + } catch (Exception ex) { + Logger.Error(ex, "Error saving buffs for {Player}", PlayerName); + } + } } #endregion } diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Field.cs b/Maple2.Server.Game/Trigger/TriggerContext.Field.cs index b3a07717a..dde1400c4 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Field.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Field.cs @@ -372,13 +372,14 @@ public void AddBuff(int[] boxIds, int buffId, int level, bool ignorePlayer, bool return; } + long startTick = Field.FieldTick; if (!ignorePlayer) { foreach (IActor player in PlayersInBox(boxIds)) { - player.Buffs.AddBuff(Field.FieldActor, player, buffId, (short) level); + player.Buffs.AddBuff(Field.FieldActor, player, buffId, (short) level, startTick); } } else { foreach (IActor monster in MonstersInBox(boxIds)) { - monster.Buffs.AddBuff(Field.FieldActor, monster, buffId, (short) level); + monster.Buffs.AddBuff(Field.FieldActor, monster, buffId, (short) level, startTick); } } } diff --git a/Maple2.Server.World/Containers/PlayerConfigLookUp.cs b/Maple2.Server.World/Containers/PlayerConfigLookUp.cs new file mode 100644 index 000000000..730ef3e19 --- /dev/null +++ b/Maple2.Server.World/Containers/PlayerConfigLookUp.cs @@ -0,0 +1,118 @@ +using System.Collections.Concurrent; +using Maple2.Database.Extensions; +using Maple2.Database.Storage; +using Maple2.Model.Metadata; +using Maple2.Server.World.Service; + +namespace Maple2.Server.World.Containers; + +public class PlayerConfigLookUp { + private readonly SkillMetadataStorage skillMetadataStorage; + + private readonly ConcurrentDictionary> buffs; + private readonly ConcurrentDictionary> skillCooldowns; + + public PlayerConfigLookUp(SkillMetadataStorage skillMetadataStorage) { + this.skillMetadataStorage = skillMetadataStorage; + buffs = []; + skillCooldowns = []; + } + + public void Save(List saveBuffs, List skillCooldownInfos, long characterId) { + // Save buffs + if (!buffs.TryGetValue(characterId, out ConcurrentDictionary? list)) { + buffs.TryAdd(characterId, new ConcurrentDictionary()); + list = buffs[characterId]; + } + + // Add to existing list + foreach (BuffInfo buff in saveBuffs) { + if (list.TryGetValue(buff.Id, out BuffInfo? _)) { + list[buff.Id] = buff; + continue; + } + + list.TryAdd(buff.Id, buff); + } + + // Save SkillCooldowns + if (!skillCooldowns.TryGetValue(characterId, out ConcurrentDictionary? skillList)) { + skillCooldowns.TryAdd(characterId, new ConcurrentDictionary()); + skillList = skillCooldowns[characterId]; + } + + // Add to existing list + foreach (SkillCooldownInfo skillCooldown in skillCooldownInfos) { + if (skillList.TryGetValue(skillCooldown.SkillId, out SkillCooldownInfo? _)) { + skillList[skillCooldown.SkillId] = skillCooldown; + continue; + } + + skillList.TryAdd(skillCooldown.SkillId, skillCooldown); + } + } + + public (List, List) Retrieve(long characterId) { + long currentTime = DateTime.Now.ToEpochSeconds(); + List buffs = RetrieveBuffs(characterId, currentTime); + List skillCooldowns = RetrieveSkillCooldowns(characterId, currentTime); + return (buffs, skillCooldowns); + } + + private List RetrieveBuffs(long characterId, long currentTime) { + if (!buffs.TryGetValue(characterId, out ConcurrentDictionary? list)) { + return []; + } + + var removeBuffs = new List(); + foreach ((int buffId, BuffInfo buffInfo) in list) { + if (!skillMetadataStorage.TryGetEffect(buffInfo.Id, (short) buffInfo.Level, out AdditionalEffectMetadata? metadata)) { + throw new InvalidDataException($"Buff not found: {buffInfo.Id}, level: {buffInfo.Level}"); + } + + if (!metadata.Property.UseInGameTime) { + long msSurpassed = (long) ((currentTime - buffInfo.StopTime) * TimeSpan.FromSeconds(1).TotalMilliseconds); + if (msSurpassed > buffInfo.MsRemaining) { + removeBuffs.Add(buffInfo); + } else { + buffInfo.MsRemaining -= (int) msSurpassed; + } + } + } + + foreach (BuffInfo buffInfo in removeBuffs) { + list.Remove(buffInfo.Id, out _); + } + + return list.Values.ToList(); + } + + private List RetrieveSkillCooldowns(long characterId, long currentTime) { + if (!skillCooldowns.TryGetValue(characterId, out ConcurrentDictionary? list)) { + return []; + } + + var removeSkillCooldowns = new List(); + foreach ((int skillId, SkillCooldownInfo skillCooldownInfo) in list) { + if (!skillMetadataStorage.TryGet(skillCooldownInfo.SkillId, (short) skillCooldownInfo.SkillLevel, out SkillMetadata? skillMetadata)) { + throw new InvalidDataException($"Skill not found: {skillCooldownInfo.SkillId}"); + } + + if (!skillMetadata.State.UseInGameTime) { + long msSurpassed = (long) ((currentTime - skillCooldownInfo.StopTime) * TimeSpan.FromSeconds(1).TotalMilliseconds); + if (msSurpassed > skillCooldownInfo.MsRemaining) { + removeSkillCooldowns.Add(skillCooldownInfo); + } else { + skillCooldownInfo.MsRemaining -= (int) msSurpassed; + } + } + } + + foreach (SkillCooldownInfo skillCooldownInfo in removeSkillCooldowns) { + list.Remove(skillCooldownInfo.SkillId, out _); + } + + return list.Values.ToList(); + } + +} diff --git a/Maple2.Server.World/Migrations/20250404235104_RemoveSkillCooldown.Designer.cs b/Maple2.Server.World/Migrations/20250404235104_RemoveSkillCooldown.Designer.cs new file mode 100644 index 000000000..9d042f841 --- /dev/null +++ b/Maple2.Server.World/Migrations/20250404235104_RemoveSkillCooldown.Designer.cs @@ -0,0 +1,1944 @@ +// +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("20250404235104_RemoveSkillCooldown")] + partial class RemoveSkillCooldown + { + /// + 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("DeathCount") + .HasColumnType("int"); + + b.Property("DeathTick") + .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("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/20250404235104_RemoveSkillCooldown.cs b/Maple2.Server.World/Migrations/20250404235104_RemoveSkillCooldown.cs new file mode 100644 index 000000000..f65bb16e5 --- /dev/null +++ b/Maple2.Server.World/Migrations/20250404235104_RemoveSkillCooldown.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Maple2.Server.World.Migrations +{ + /// + public partial class RemoveSkillCooldown : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SkillCooldowns", + table: "character-config"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SkillCooldowns", + table: "character-config", + type: "json", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs b/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs index 2a19de502..09152f683 100644 --- a/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs +++ b/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs @@ -359,9 +359,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAddOrUpdate() .HasColumnType("datetime(6)"); - b.Property("SkillCooldowns") - .HasColumnType("json"); - b.Property("SkillMacros") .HasColumnType("json"); diff --git a/Maple2.Server.World/Program.cs b/Maple2.Server.World/Program.cs index 211ffe059..7545d0afe 100644 --- a/Maple2.Server.World/Program.cs +++ b/Maple2.Server.World/Program.cs @@ -69,6 +69,8 @@ .SingleInstance(); autofac.RegisterType() .SingleInstance(); + autofac.RegisterType() + .SingleInstance(); // Register WorldServer and inject ChannelClientLookup autofac.RegisterType() diff --git a/Maple2.Server.World/Service/WorldService.PlayerConfig.cs b/Maple2.Server.World/Service/WorldService.PlayerConfig.cs new file mode 100644 index 000000000..0f724775a --- /dev/null +++ b/Maple2.Server.World/Service/WorldService.PlayerConfig.cs @@ -0,0 +1,48 @@ +using Grpc.Core; + + +namespace Maple2.Server.World.Service; + +public partial class WorldService { + public override Task PlayerConfig(PlayerConfigRequest request, ServerCallContext context) { + switch (request.BuffCase) { + case PlayerConfigRequest.BuffOneofCase.Get: + return Task.FromResult(Get(request.Get, request.RequesterId)); + case PlayerConfigRequest.BuffOneofCase.Save: + return Task.FromResult(Save(request.Save, request.RequesterId)); + default: + return Task.FromResult(new PlayerConfigResponse()); + } + } + + private PlayerConfigResponse Get(PlayerConfigRequest.Types.Get get, long requesterId) { + (List buffs, List skillCooldowns) = playerConfigLookUp.Retrieve(requesterId); + return new PlayerConfigResponse { + Buffs = { + buffs.Select(b => new BuffInfo { + Id = b.Id, + Stacks = b.Stacks, + Enabled = b.Enabled, + Level = b.Level, + MsRemaining = b.MsRemaining, + StopTime = b.StopTime, + }), + }, + SkillCooldowns = { + skillCooldowns.Select(c => new SkillCooldownInfo { + SkillId = c.SkillId, + SkillLevel = c.SkillLevel, + GroupId = c.GroupId, + MsRemaining = c.MsRemaining, + StopTime = c.StopTime, + Charges = c.Charges, + }), + }, + }; + } + + private PlayerConfigResponse Save(PlayerConfigRequest.Types.Save save, long requesterId) { + playerConfigLookUp.Save(save.Buffs.ToList(), save.SkillCooldowns.ToList(), requesterId); + return new PlayerConfigResponse(); + } +} diff --git a/Maple2.Server.World/Service/WorldService.cs b/Maple2.Server.World/Service/WorldService.cs index 5a47bd2df..32b73cb0f 100644 --- a/Maple2.Server.World/Service/WorldService.cs +++ b/Maple2.Server.World/Service/WorldService.cs @@ -16,13 +16,14 @@ public partial class WorldService : World.WorldBase { private readonly GroupChatLookup groupChatLookup; private readonly BlackMarketLookup blackMarketLookup; private readonly GlobalPortalLookup globalPortalLookup; + private readonly PlayerConfigLookUp playerConfigLookUp; private readonly ILogger logger = Log.Logger.ForContext(); public WorldService( IMemoryCache tokenCache, WorldServer worldServer, ChannelClientLookup channelClients, PlayerInfoLookup playerLookup, GuildLookup guildLookup, PartyLookup partyLookup, PartySearchLookup partySearchLookup, GroupChatLookup groupChatLookup, BlackMarketLookup blackMarketLookup, - ClubLookup clubLookup, GlobalPortalLookup globalPortalLookup + ClubLookup clubLookup, GlobalPortalLookup globalPortalLookup, PlayerConfigLookUp playerConfigLookUp ) { this.tokenCache = tokenCache; this.worldServer = worldServer; @@ -35,6 +36,7 @@ public WorldService( this.clubLookup = clubLookup; this.blackMarketLookup = blackMarketLookup; this.globalPortalLookup = globalPortalLookup; + this.playerConfigLookUp = playerConfigLookUp; } public override Task Channels(ChannelsRequest request, ServerCallContext context) {