From 3c5c5d5e1bfd558f2274778c2bc15d80dcf4ba2c Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Fri, 20 Feb 2026 16:58:18 -0600 Subject: [PATCH 01/26] Add Constants WIP --- .../Storage/Metadata/ServerTableMetadataStorage.cs | 3 +++ Maple2.File.Ingest/Mapper/ServerTableMapper.cs | 13 +++++++++++++ Maple2.Model/Common/ServerTableNames.cs | 1 + .../Metadata/ServerTable/ConstantsTable.cs | 14 ++++++++++++++ Maple2.Model/Metadata/ServerTableMetadata.cs | 1 + 5 files changed, 32 insertions(+) create mode 100644 Maple2.Model/Metadata/ServerTable/ConstantsTable.cs diff --git a/Maple2.Database/Storage/Metadata/ServerTableMetadataStorage.cs b/Maple2.Database/Storage/Metadata/ServerTableMetadataStorage.cs index 9a3c1d591..187856d3b 100644 --- a/Maple2.Database/Storage/Metadata/ServerTableMetadataStorage.cs +++ b/Maple2.Database/Storage/Metadata/ServerTableMetadataStorage.cs @@ -29,6 +29,7 @@ public class ServerTableMetadataStorage { private readonly Lazy combineSpawnTable; private readonly Lazy enchantOptionTable; private readonly Lazy unlimitedEnchantOptionTable; + private readonly Lazy constantsTable; public InstanceFieldTable InstanceFieldTable => instanceFieldTable.Value; public ScriptConditionTable ScriptConditionTable => scriptConditionTable.Value; @@ -53,6 +54,7 @@ public class ServerTableMetadataStorage { public CombineSpawnTable CombineSpawnTable => combineSpawnTable.Value; public EnchantOptionTable EnchantOptionTable => enchantOptionTable.Value; public UnlimitedEnchantOptionTable UnlimitedEnchantOptionTable => unlimitedEnchantOptionTable.Value; + public ConstantsTable ConstantsTable => constantsTable.Value; public ServerTableMetadataStorage(MetadataContext context) { instanceFieldTable = Retrieve(context, ServerTableNames.INSTANCE_FIELD); @@ -78,6 +80,7 @@ public ServerTableMetadataStorage(MetadataContext context) { combineSpawnTable = Retrieve(context, ServerTableNames.COMBINE_SPAWN); enchantOptionTable = Retrieve(context, ServerTableNames.ENCHANT_OPTION); unlimitedEnchantOptionTable = Retrieve(context, ServerTableNames.UNLIMITED_ENCHANT_OPTION); + constantsTable = Retrieve(context, ServerTableNames.CONSTANTS); } public IEnumerable GetGameEvents() { diff --git a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs index 0a8043c75..e34ba9d51 100644 --- a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs +++ b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs @@ -5,6 +5,7 @@ using Maple2.File.IO; using Maple2.File.Parser; using Maple2.File.Parser.Enum; +using Maple2.File.Parser.Xml.Table; using Maple2.File.Parser.Xml.Table.Server; using Maple2.Model; using Maple2.Model.Common; @@ -128,6 +129,10 @@ protected override IEnumerable Map() { Name = ServerTableNames.UNLIMITED_ENCHANT_OPTION, Table = ParseUnlimitedEnchantOption(), }; + yield return new ServerTableMetadata { + Name = ServerTableNames.CONSTANTS, + Table = ParseConstants(), + }; } @@ -2107,4 +2112,12 @@ void AddSpecial(Dictionary values, Dictionary(); + foreach ((string key, Parser.Xml.Table.Constants.Key constants) in parser.ParseConstants()) { + results.Add(key, new Model.Metadata.Constants(constants.key, constants.value)); + } + return new ConstantsTable(results); + } } diff --git a/Maple2.Model/Common/ServerTableNames.cs b/Maple2.Model/Common/ServerTableNames.cs index 7a328840f..0761b6ea1 100644 --- a/Maple2.Model/Common/ServerTableNames.cs +++ b/Maple2.Model/Common/ServerTableNames.cs @@ -24,4 +24,5 @@ public static class ServerTableNames { public const string COMBINE_SPAWN = "combineSpawn*.xml"; public const string ENCHANT_OPTION = "enchantOption.xml"; public const string UNLIMITED_ENCHANT_OPTION = "unlimitedEnchantOption.xml"; + public const string CONSTANTS = "constants.xml"; } diff --git a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs new file mode 100644 index 000000000..ae2b913b7 --- /dev/null +++ b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Maple2.Model.Metadata; + +public record ConstantsTable(IReadOnlyDictionary Constants) : ServerTable; + +public record Constants ( + string Key, + string Value + ); diff --git a/Maple2.Model/Metadata/ServerTableMetadata.cs b/Maple2.Model/Metadata/ServerTableMetadata.cs index 097a0b79b..484a6715d 100644 --- a/Maple2.Model/Metadata/ServerTableMetadata.cs +++ b/Maple2.Model/Metadata/ServerTableMetadata.cs @@ -47,4 +47,5 @@ public override int GetHashCode() { [JsonDerivedType(typeof(CombineSpawnTable), typeDiscriminator: "combineSpawn")] [JsonDerivedType(typeof(EnchantOptionTable), typeDiscriminator: "enchantOption")] [JsonDerivedType(typeof(UnlimitedEnchantOptionTable), typeDiscriminator: "unlimitedEnchantOption")] +[JsonDerivedType(typeof(ConstantsTable), typeDiscriminator: "constants")] public abstract record ServerTable; From 55c8d2ad3b06d4181a8c4ff392b651b0638d70e1 Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Sun, 22 Feb 2026 21:31:45 -0600 Subject: [PATCH 02/26] Organize and clean-up old constants values WIP. Start using new server constants that are processed during file ingest WIP. --- .../Mapper/ServerTableMapper.cs | 21 +- Maple2.Model/Metadata/Constants.cs | 737 +-------------- .../Metadata/ServerTable/ConstantsTable.cs | 879 +++++++++++++++++- .../MovementState.CleanupTask.cs | 3 +- 4 files changed, 925 insertions(+), 715 deletions(-) diff --git a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs index e34ba9d51..d9af81fd0 100644 --- a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs +++ b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs @@ -1,11 +1,8 @@ -using System.Globalization; -using System.Xml; -using Maple2.Database.Extensions; +using Maple2.Database.Extensions; using Maple2.File.Ingest.Utils; using Maple2.File.IO; using Maple2.File.Parser; using Maple2.File.Parser.Enum; -using Maple2.File.Parser.Xml.Table; using Maple2.File.Parser.Xml.Table.Server; using Maple2.Model; using Maple2.Model.Common; @@ -14,6 +11,9 @@ using Maple2.Model.Game; using Maple2.Model.Game.Shop; using Maple2.Model.Metadata; +using System.Globalization; +using System.Reflection; +using System.Xml; using DayOfWeek = System.DayOfWeek; using ExpType = Maple2.Model.Enum.ExpType; using Fish = Maple2.File.Parser.Xml.Table.Server.Fish; @@ -2114,10 +2114,15 @@ void AddSpecial(Dictionary values, Dictionary(); - foreach ((string key, Parser.Xml.Table.Constants.Key constants) in parser.ParseConstants()) { - results.Add(key, new Model.Metadata.Constants(constants.key, constants.value)); + var constants = new Constants(); + PropertyInfo[] constantsProperties = constants.GetType().GetProperties(); + foreach (PropertyInfo constantsProperty in constantsProperties) { + foreach ((string key, Parser.Xml.Table.Constants.Key constant) in parser.ParseConstants()) { + if (!key.Normalize().Trim().Equals(constantsProperty.Name.Normalize().Trim())) continue; + constantsProperty.SetValue(constants, Convert.ChangeType(constant.value, constantsProperty.PropertyType)); + break; + } } - return new ConstantsTable(results); + return new ConstantsTable(constants); } } diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index 38f3b9574..13f94dac4 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -18,8 +18,6 @@ public static class Constant { public const long MesoTokenMax = 100000; public const int MaxSkillTabCount = 3; public const int BuddyMessageLengthMax = 25; - public const int MaxBuddyCount = 100; - public const int MaxBlockCount = 100; public const int GemstoneGrade = 4; public const int LapenshardGrade = 3; public const int InventoryExpandRowCount = 6; @@ -105,22 +103,52 @@ public static class Constant { public const int Grade3WeddingCouponItemId = 20303168; public const int MinStatIntervalTick = 100; public const int HomePollMaxCount = 5; - + public const int DummyNpcMale = 2040998; + public const int DummyNpcFemale = 2040999; + public const int NextStateTriggerDefaultTick = 100; public const int MaxMentees = 3; - public const long FurnishingBaseId = 2870000000000000000; public const bool AllowWaterOnGround = false; - public const int HomeDecorationMaxLevel = 10; - public const bool EnableRollEverywhere = false; public const bool HideHomeCommands = true; - public const int MaxAllowedLatency = 2000; - public const bool DebugTriggers = false; // Set to true to enable debug triggers. (It'll write triggers to files and load triggers from files instead of DB) - public const bool AllowUnicodeInNames = false; // Allow Unicode characters in character and guild names + public const bool MailQuestItems = false; // Mail quest item rewards if inventory is full + public const int MaxClosetMaxCount = 5; + public const int MaxClosetTabNameLength = 10; + public const int CharacterNameLengthMin = 2; + public const int BlockSize = 150; + public const float SouthEast = 0; + public const float NorthEast = 90; + public const float NorthWest = -180; + public const float SouthWest = -90; + public const short HairSlotCount = 30; + public const ShopCurrencyType InitialTierExcessRestockCurrency = ShopCurrencyType.Meso; + public const float UGCShopProfitFee = 0.25f; + public const int UGCShopProfitDelayDays = 10; + public const int PartyFinderListingsPageCount = 12; + public const int ProposalItemId = 11600482; + public const int bagSlotTabPetEquipCount = 48; + public const int BagSlotTabGameCountMax = 48; + public const int BagSlotTabSkinCountMax = 150; + public const int BagSlotTabSummonCountMax = 48; + public const int BagSlotTabMaterialCountMax = 48; + public const int BagSlotTabMasteryCountMax = 48; + public const int BagSlotTabLifeCountMax = 48; + public const int BagSlotTabQuestCountMax = 48; + public const int BagSlotTabGemCountMax = 48; + public const int BagSlotTabPetCountMax = 78; + public const int BagSlotTabPetEquipCountMax = 48; + public const int BagSlotTabActiveSkillCountMax = 48; + public const int BagSlotTabCoinCountMax = 48; + public const int BagSlotTabBadgeCountMax = 48; + public const int BagSlotTabMiscCountMax = 48; + public const int BagSlotTabLapenshardCountMax = 48; + public const int BagSlotTabPieceCountMax = 48; + public const int MeretAirTaxiPrice = 15; + public const int ClubMaxCount = 3; public static IReadOnlyDictionary ContentRewards { get; } = new Dictionary { {"miniGame", 1005}, @@ -148,7 +176,9 @@ public static class Constant { {"QueenBeanArenaRound10Reward", 10000018}, }; - public const bool MailQuestItems = false; // Mail quest item rewards if inventory is full + public static int DummyNpc(Gender gender) => gender is Gender.Female ? DummyNpcFemale : DummyNpcMale; + + #endregion #region Field public static readonly TimeSpan FieldUgcBannerRemoveAfter = TimeSpan.FromHours(4); @@ -188,65 +218,10 @@ public static class Constant { public static readonly int DefaultMaxCharacters = 4; #endregion - #endregion - - #region client constants - public const int MaxClosetMaxCount = 5; - public const int MaxClosetTabNameLength = 10; - public const int WeddingProposeItemID = 11600482; - public const int WeddingInvitationMaxCount = 70; - public const int WeddingProposeCooltime = 2; - public const int WeddingDivorceFieldID = 84000002; - public const int WeddingInvitationMeso = 1000; - public const int WeddingDivorceMeso = 1000000; - public const int WeddingCoolingOffDay = 7; - public const int WeddingPromiseLimitDay = 7; - public const int WeddingHallModifyLimitHour = 3; - public const int WeddingDivorceRequireMarriageDay = 30; - public const int CharacterNameLengthMin = 2; - public const int BlockSize = 150; - public const float SouthEast = 0; - public const float NorthEast = 90; - public const float NorthWest = -180; - public const float SouthWest = -90; - public const short HairSlotCount = 30; - public const ShopCurrencyType InitialTierExcessRestockCurrency = ShopCurrencyType.Meso; - public const float UGCShopProfitFee = 0.25f; - public const int UGCShopProfitDelayDays = 10; - public const int PartyFinderListingsPageCount = 12; - public const int ProposalItemId = 11600482; - #endregion - - #region table/constants.xml - - public const float NPCColorScale = 2.0f; - public const float NPCDuration = 0.2f; - public const float PCColorScale = 2.0f; - public const float PCDuration = 0.2f; - public const float GetEXPColorScale = 0.5f; - public const float GetEXPDuration = 0.2f; - public const float AccumulationRatio = 0.1f; + #region XML table/constants.xml public const float NPCCliffHeight = 50.0f; - public const float NPCRandomDeadPushRate = 0.2f; public const float CustomizingRotationSpeed = 75.0f; - public const float CustomizingWheelSpeed_Morph = 0.1f; - public const float CustomizingWheelSpeed_Item = 0.1f; - public const float CustomizingWheelSpeed_Makeup = 0.1f; - public const float CustomizingRotationSpeed_Makeup = 1.0f; - public const float CustomizingHairFirstPartHighlight = 0.1f; - public const float CustomizingHairSecondPartHighlight = 1.0f; - public const float LookAtInterval = 15.0f; - public const float LookAtDistanceNPC = 500.0f; - public const float LookAtDistanceCry = 500.0f; - public const bool EnableSkillJumpDown = true; - public const bool EscapeHitMethodSkill = false; - public const bool EscapeHitMethodJump = false; - public const bool EscapeHitMethodMove = false; - public const bool EscapeHitMoveKeyIsDown = false; public const bool AllowComboAtComboPoint = true; - public const bool CancelSwing_KeyIsDown = true; - public const bool SkillGlobalCooldown = false; - public const bool SkillGlobalCooldown_CheckSameSkill = true; public const int AttackRotationSpeed = 90; public const int ChaosModeTime = 20; public const int ChaosPointPerBlock = 20; @@ -261,79 +236,12 @@ public static class Constant { public const int OnEnterTriggerClientSideOnlyTick = 100; public const int OnEnterTriggerDefaultTick = 1000; public const int TalkTimeover = 60000; - public const int DamageDistance = 2500; - public const int TalkableDistance = 150; - public const bool TalkableFrontOnly = true; - public const int DropIconVisibleDistance = 400; - public const int ChatBalloonDistance = 2000; - public const int HpBarDistance = 9999999; - public const int EmoticonVisibleDistance = 2500; - public const int RegisterUgcDistance = 150; - public const int RegisterUgcDistanceClose = 300; - public const int ConstructUgcDistance = 150; - public const int FunctionCubeDistance = 125; - public const int InteractionDistance = 155; - public const int HouseMarkShowDistance = 2000; - public const int HouseMarkShowClippingUp = 1000; - public const int HouseMarkShowClippingDown = 500; - public const int HouseMarkPopupDistance = 160; - public const int UgcBoundaryStartDistance = 1; - public const int UgcBoundaryEndDistance = 7; - public const int DurationForBoundaryDisplay = 3000; - public static TimeSpan UgcHomeSaleWaitingTime = TimeSpan.FromSeconds(259200); - public const int UgcContainerExpireDurationNormal = 90; - public const int UgcContainerExpireDurationCash = 365; - public const int UgcContainerExpireDurationMeret = 365; - public const int UgcHomeExtensionNoticeDate = 30; - public const int UgcHomePasswordExpireDuration = 86400; - public const bool CubeLiftHeightLimitUp = true; - public const bool CubeLiftHeightLimitDown = true; - public const int CubeCraftSafetyCapID = 11300053; - public const int CubeCraftLightStickLeftID = 13100014; - public const int CubeCraftLightStickRightID = 13100046; - public const float DropIconDistance = 200.0f; - public const int DropIconHeadOffset = 40; - public const int DropItemMaxLength = 300; - public const int DropMoneyMaxLength = 300; - public const float DropItemTargetZPos = 200.0f; - public const float DropItemPickUpVel = 200.0f; - public const float DropItemPickUpGravity = -38.0f; - public const float DropItemPickUpCompleteRotateTime = 0.1f; - public const int DropItemPickUpCompleteRotateVel = 5; public const int DropMoneyActiveProbability = 0; public const int DropMoneyProbability = 0; - public const int ChatBalloonDuration = 5000; - public const int BoreWaitingTick = 50000; - public const int OffsetPcHpBar = 32; - public const int OffsetPcNameTag = 30; - public const int OffsetPcChatBalloon = -30; - public const int OffsetPcDamageNumber = 0; public const int OffsetPcMissionIndicator = 20; - public const int OffsetPcProfileTag = 0; - public const float fOffsetOnTombstoneNameTag = -5.0f; - public const int OffsetNpcHpBar = 5; - public const int OffsetNpcNameTag = 5; - public const int OffsetNpcEmoticon = -30; - public const int OffsetNpcChatBalloon = -30; - public const int OffsetNpcDamageNumber = 0; - public const int OffsetNpcMonologue = 40; - public const int OffsetActionTooltipX = 70; - public const int OffsetActionTooltipY = -40; - public const int OffsetPcPopupMenu = 60; - public const int DamageGap = 30; - public const int DamageRenderCount = 3; - public const int DamageRenderTotalCount = 25; - public const float DamageOtherScale = 0.5f; - public const float DamageOtherAlpha = 0.3f; - public const int DamageEffectMinHPPercent = 30; - public const int DamageEffectCriticalPercent = 10; public const int questHideTime = 30; public const int questIntervalTime = 60; public const int ShopResetChance = 10; - public const int ShopSeedResetTime = 60; - public const int ShopRepurchaseMax = 12; - public const int ShopSellConfirmPrice = 10000; - public const int ShopBuyConfirmPrice = 0; public const int DashKeyInputDelay = 500; public const int DashSwimConsumeSP = 20; public const int DashSwimMoveVel = 2; @@ -352,51 +260,14 @@ public static class Constant { public const string Glide_Ani_Left = "Gliding_Left_A"; public const string Glide_Ani_Right = "Gliding_Right_A"; public const string Glide_Ani_Run = "Fly_Run_A"; - public const float ClimbVelocityV = 3.0f; - public const float ClimbVelocityH = 1.5f; - public const int StoreExpandMaxSlotCount = 144; - public const int StoreExpandPrice1Row = 330; - public const int StoreDepositMax = 2000000000; - public const int StoreWithdrawMax = 2000000000; - public const int CameraExtraMoveScaleByMonster = 3; - public const int CameraExtraMoveScaleByMap = 2; - public const int CameraExtraDistance = 200; - public const float CameraFinalLoose = 0.08f; - public const float CameraCurrentLoose = 0.002f; - public const float CameraUpdateLoose = 0.03f; - public const int CameraVelocityInPortalMove = 6000; public const int ConsumeCritical = 5; - public const int MonologueInterval = 15; - public const int MonologueRandom = 10; - public const int MonologueShowTime = 5; - public const int ShowKillCountMin = 3; - public const int UserRevivalInvincibleTick = 5000; - public const int UserRevivalPenaltyPercent = 15; - public const string UserRevivalIconPath = "./data/resource/image/skill/icon/deathPenalty.png"; - public const string UserRevivalInvincibleIconPath = "./data/resource/image/skill/icon/deathInvincible.png"; - public const int GetExpMinVelocity = 250; - public const int GetExpVelocityPer1Length = 2; - public const string GetExpControlValue0 = "-0.5,0,0.25"; - public const string GetExpControlValue1 = "0.5,-0.25,0.5"; - public const string GetExpTargetPCDummyName = "Eff_Body"; - public const float GetExpTimeAcceleration = 1.02f; - public const float GetExpCollisionRadius = 15.0f; public const int DayToNightTime = 10000; - public const float MyPCDayTiming = 0.5f; - public const float MyPCNightTiming = 0.5f; + public const float myPCdayTiming = 0.5f; + public const float myPCNightTiming = 0.5f; public const float BGMTiming = 0.5f; public const int dayBaseMinute = 1; public const int dayMinute = 1439; public const int nightMinute = 1; - public const int SkipFrameGameObject = 5; - public const int SkipFrameDistanceGameObject = 2000; - public const float RegionSkillFadeOutDuration = 0.3f; - public const int PassengerProfileImageSize = 50; - public const int PassengerProfileImageLifeTime = 3; - public const int PassengerProfileImageShowNumber = 3; - public const int PassengerProfileImageShowCooldown = 57; - public const int PassengerProfileImageShowCooldownParty = 57; - public const int PassengerProfileImageShowRange = 400; public const int QuestRewardSkillSlotQuestID1 = 1010002; public const int QuestRewardSkillSlotQuestID2 = 1010003; public const int QuestRewardSkillSlotQuestID3 = 1010004; @@ -407,559 +278,37 @@ public static class Constant { public const int QuestRewardSkillSlotItemID3 = 20000001; public const int QuestRewardSkillSlotItemID4 = 40000055; public const int QuestRewardSkillSlotItemID5 = 40000056; - public const int UGCCameraDefaultSize = 320; - public const int UGCCameraMinSize = 160; - public const int UGCCameraMaxSize = 640; - public const int UGCCameraSnapshotPreviewTime = 3000; - public const int UGCImgUploadSizeLimit = 1024; - public const int UGCImgFileCountCheck = 200; - public const int WindAmp2Cloak = 1500; - public const float WindPeriod2Cloak = 0.7f; - public const float WindPeriodVar2Cloak = 0.4f; public const int autoTargetingMaxDegree = 210; - public const float VolumeMyPcToNpc = 1.0f; - public const float VolumeMyPcToObject = 0.5f; - public const float VolumeMyPcToBreakableObject = 0.8f; - public const float VolumeNpcToMyPc = 0.7f; - public const float VolumePcToNpc = 0.3f; - public const float VolumePcToBreakableObject = 0.3f; - public const float VolumeNpcToPc = 0.5f; - public const float VolumeOtherPc = 0.9f; - public const int ItemDropLevelMaxBoundary = 1; - public const float moneyTreeDropHeight = 300.0f; - public const float moneyTreeDropBase = 150.0f; - public const int moneyTreeDropRandom = 200; - public const int WhisperIgnoreTime = 1000; - public const int WhisperMaxCount = 3; - public const int WhisperDurationTime = 3000; public const float BossHitVibrateFreq = 10.0f; public const float BossHitVibrateAmp = 5.5f; public const float BossHitVibrateDamping = 0.7f; public const float BossHitVibrateDuration = 0.1f; - public const float BossHpBarAutoDetectRange = 1500.0f; - public const float BossHpBarDuration = 5.0f; - public const float FindHoldTargetRange = 230.0f; - public const int FindGrabNodeRange = 2000; - public const string UgcShopCharCameraLookat = "0,0,70"; - public const string UgcShopCharCameraPos = "220,0,0"; - public const int UgcShopCharCameraMinDistance = 150; - public const int UgcShopCharCameraZoomVelocity = 700; - public const string UgcShopCubeCameraLookat = "0,0,80"; - public const string UgcShopCubeCameraPos = "420,0,350"; - public const int UgcShopCubeCameraMinDistance = 450; - public const int UgcShopCubeCameraZoomVelocity = 700; - public const string UgcShopRideeCameraLookat = "10,-5,50"; - public const string UgcShopRideeCameraPos = "275,0,150"; - public const int UgcShopRideeCameraMinDistance = 250; - public const int UgcShopRideeCameraZoomVelocity = 700; - public const int FieldCachingCount = 2; - public const float FieldCachingTime = 300.0f; - public const int FieldCachingMaxCount = 4; - public const int FieldUnloadThreshold = 10; - public const float EffectLODOneStepDistance = 450.0f; - public const float EffectLODTwoStepDistance = 500.0f; - public const float EffectLODThreeStepDistance = 550.0f; - public const int TelescopeFindDistance = 200; - public const int BoatPrice = 500; - public const int QuestGuidePageCount = 3; - public const int QuestGuideMaxCount = 60; - public const float CameraInterpolationTime = 0.4f; public const int OneTimeWeaponItemID = 15000001; - public const int TransparencyCP = 11399999; - public const int TransparencyEY = 11199999; - public const int TransparencyCL = 11499999; - public const int TransparencyPA = 11599999; - public const int TransparencyMT = 11899999; - public const int TransparencyEA = 11299999; - public const int TransparencyFH = 11099999; - public const int TransparencyGL = 11699999; - public const int TransparencyRI = 12099999; - public const int TransparencySH = 11799999; - public const float DefaultDropItemAlpha = 0.3f; - public const float DropItemPickFailHeight = 50.0f; - public const float DropItemPickFailTime = 0.3f; - public const int TaxiStationFindDistance = 200; - public const int TaxiCallDuration = 3000; - public const int TaxiCallBestDriverDuration = 1000; - public const int TaxiCallBestDriverLevel = 25; - public const int AirTaxiCashCallDuration = 500; - public const int AirTaxiMesoCallDuration = 3000; - public const int TradeRequestDuration = 20; - public const int UserPortalInvincibleTick = 5000; - public const string UserPortalInvincibleIconPath = "./data/resource/image/skill/icon/deathInvincible.png"; - public const int SummonRideeDuration = 1000; - public const int WorldMapAdjustTileX = 0; - public const int WorldMapAdjustTileY = 0; - public const float TimeScalePCScale = 0.1f; - public const float TimeScalePCDuration = 1.0f; - public const int GoToHomeCastingTime = 0; - public const int returnHomeSkill = 100000000; - public const int returnHomeSkillMeret = 100000013; - public const int TutorialIntroSkipTime = 5; - public const string AvatarDefaultItemMale = "10200032,10300198"; - public const string AvatarDefaultItemFemale = "10200033,10300199"; public const int ModelHouse = 62000027; - public const int TalkCooldown = 1000; - public const int AddressPopupDuration = 3000; - public const int MaxFPS = 120; - public const int UGCShopSellMinPrice = 150; - public const int UGCShopSellMaxPrice = 3000; - public const int UGCShopSaleDay = 90; - public const int UGCShopAdFeeMeret = 30; - public const int UGCShopAdHour = 72; - public const int UGCShopSellingRestrictAmount = 200000; - public const int MeretMarketHomeBannerShowTick = 6000; - public const int BlackMarketSellMinPrice = 100; - public const int BlackMarketSellMaxPrice = 500000000; - public const int BlackMarketSellEndDay = 2; - public const int ItemTransferBlackMarketGrade = 4; - public const int UgcBannerCheckTime = 4; - public const int FastChat_CheckTime = 2000; - public const int FastChat_CheckCount = 5; - public const int SameChat_CheckTime = 3000; - public const int SameChat_CheckCount = 5; - public const int SameChat_RestrictTime = 10000; - public const int FastChat_RestrictTime = 30000; - public const int RestrictChat_AddRestrictTime = 10000; - public const int AccumWarning_AddRestrictTime = 60000; - public const int RestrictWarning_ReleaseTime = 10000; - public const int MaxChatLength = 100; public const int UsingNoPhysXModelUserCount = 10; public const int UsingNoPhysXModelActorCount = 10; public const int UsingNoPhysXModelJointCount = 10; - public const int EmotionBoreAnimProbability = 100; - public const float FallMoveSpeed = 1.0f; - public const int GuildCreatePrice = 2000; - public const int GuildCreateMinLevel = 0; - public const int GuildNameLengthMin = 2; - public const int GuildNameLengthMax = 25; - public const int guildFundMax = 20000; - public const float guildFundRate = 0.1f; - public const int guildExpMaxCountForPlayTime = 2; - public const int guildDonateMeso = 10000; - public const string mirrorGuideMoviePath = "Common/Customize_Hat.usm"; - public const string hairGuideMoviePath = "Common/Customize_Hair.usm"; - public const string makeUpGuideMoviePath = "Common/Customize_MakeUp.usm"; - public const int FastShimmerRadius = 600; - public const int FastShimmerHeight = 450; - public const int SmartRecommendNotify_DurationTick = 15000; - public const int BootyPopupDuration = 3000; public const bool EnableSoundMute = true; public const int BossKillSoundRange = 1500; - public const string charCreateGuideMoviePath = "Common/Customize_Intro.usm"; public const int monsterPeakTimeNotifyDuration = 300; - public const int KeyIsDownSkill_MaxDurationTick = 30000; - public const int shadowWorldBuffHpUp = 70000027; - public const int shadowWorldBuffMoveProtect = 70000032; public const int AirTaxiItemID = 20300003; - public const int PeriodOfMaidEmployment = 30; - public const int MaidReadyToPay = 7; - public const int MaidAffinityMax = 10; - public const int MeretRevivalDebuffCode = 100000001; - public const float MeretRevivalFeeReduceLimit = 0.5f; - public const int MeretConsumeWorldChat = 30; - public const int MeretConsumeChannelChat = 3; - public const int MeretConsumeSuperChat = 200; - public const int pvpBtiRewardItem = 90000006; - public const int pvpBtiRewardWinnerCount = 30; - public const int pvpBtiRewardLoserCount = 10; - public const int PvpFFAReward1Count = 30; - public const int PvpFFAReward2Count = 25; - public const int PvpFFAReward3Count = 20; - public const int PvpFFAReward4Count = 15; - public const int PvpFFAReward5Count = 15; - public const int PvpFFAReward6Count = 15; - public const int PvpFFAReward7Count = 15; - public const int PvpFFAReward8Count = 10; - public const int PvpFFAReward9Count = 10; - public const int PvpFFAReward10Count = 10; - public const int PvpFFARewardItem = 90000006; - public const int PvpFFAAdditionRewardRate = 0; - public const int MailExpiryDays = 30; - public const int WorldMapBossTooltipCount = 30; - public const int ShowNameTagEnchantItemGrade = 4; - public const int ShowNameTagEnchantLevel = 12; - public const int BossNotifyAbsLevel = 1; - public const int RoomExitWaitSecond = 10; - public const int AdditionalMesoMaxRate = 7; - public const int AdditionalExpMaxRate = 9; - public const int HonorTokenMax = 30000; - public const int KarmaTokenMax = 75000; - public const int LuTokenMax = 2000; - public const int HaviTokenMax = 35000; - public const int ReverseCoinMax = 2000; - public const int MentorTokenMax = 10000; // From KMS - public const int MenteeTokenMax = 35000; // From KMS - public const int CharacterDestroyDivisionLevel = 20; - public const int CharacterDestroyWaitSecond = 86400; - public const int BossShimmerScaleUpActiveDistance = 5000; - public const float BossShimmerScaleUpSize = 3.0f; public const int ShowNameTagSellerTitle = 10000153; public const int ShowNameTagChampionTitle = 10000152; public const int ShowNameTagTrophy1000Title = 10000170; public const int ShowNameTagTrophy2000Title = 10000171; public const int ShowNameTagTrophy3000Title = 10000172; public const int ShowNameTagArchitectTitle = 10000158; - public const float SwimDashSpeed = 5.4f; - public const int UserTriggerStateMax = 10; - public const int UserTriggerEnterActionMax = 3; - public const int UserTriggerConditionMax = 3; - public const int UserTriggerConditionActionMax = 3; - public const int PCBangAdditionalEffectID = 100000006; - public const int PCBangAdditionalEffectExp = 1; - public const int PCBangAdditionalEffectMeso = 2; - public const int PCBangItemDefaultPeriod = 1440; - public const int ShadowWorldAutoReviveDeadAction = 1; - public const int GoodInteriorRecommendUICloseTime = 15; - public const string UGCInfoDetailViewPage = "http://www.nexon.net/en/legal/user-generated-content-policy"; - public const int UGCInfoStoryBookID = 39000038; - public const int HomePasswordUsersKickDelay = 10; - public const string TriggerEditorHelpURL = "http://maplestory2.nexon.net/en/news/article/32326"; - public const int QuestRewardSAIgnoreLevel = 10; - public const int RecallCastingTime = 3000; - public const int PartyRecallMeret = 30; - public const float CashCallMedicLeaveDelay = 0.5f; public const int characterMaxLevel = 99; // Updated - public const int DropSPEPBallMaxLength = 300; - public const int DropSPEPBallTargetZPos = 100; - public const int DropSPEPBallPickUpVel = 250; - public const int DropSPEPBallPickUpGravity = -120; - public const float DropSPEPBallPickUpCompleteRotateTime = 0.05f; - public const int DropSPEPBallPickUpCompleteRotateVel = 5; - public const int EnchantItemBindingRequireLevel = 1; - public const int enchantSuccessBroadcastingLevel = 12; - public const int EnchantEquipIngredientMaxCount = 1000; - public const int EnchantFailStackUsingMaxCount = 100; - public const int EnchantFailStackTakeMaxCount = 1000; - public const int EnchantEquipIngredientOpenLevel = 11; - public const int EnchantEquipIngredientOpenRank = 4; - public const int EnchantEquipIngredientMaxSuccessProb = 3000; - public const int EnchantFailStackOpenLevel = 1; - public const int EnchantFailStackTakeMaxSuccessProb = 10000; - public const int BankCallDuration = 500; - public const string NoticeDialogUrl = "http://nxcache.nexon.net/maplestory2/ingame-banners/index.html"; - public const string NoticeDialogUrlPubTest = "maview:/Game/BannerTest"; - public const int NoticeDialogOpenSeconds = 5000; - public const int RemakeOptionMaxCount = 10; - public const int FisherBoreDuration = 10000; - public const string fishingStartCastingBarText0 = "s_fishing_start_castingbar_text0"; - public const string fishingStartCastingBarText1 = "s_fishing_start_castingbar_text1"; - public const string fishingStartCastingBarText2 = "s_fishing_start_castingbar_text2"; - public const string fishingStartCastingBarText3 = "s_fishing_start_castingbar_text3"; - public const string fishingStartCastingBarText4 = "s_fishing_start_castingbar_text4"; - public const string fishingStartBalloonText0 = "s_fishing_start_balloon_text0"; - public const string fishingStartBalloonText1 = "s_fishing_start_balloon_text1"; - public const string fishingStartBalloonText2 = "s_fishing_start_balloon_text2"; - public const string fishingStartBalloonText3 = "s_fishing_start_balloon_text3"; - public const string fishingStartBalloonText4 = "s_fishing_start_balloon_text4"; - public const string fishingStartBalloonText5 = "s_fishing_start_balloon_text5"; - public const string fishingStartBalloonText6 = "s_fishing_start_balloon_text6"; - public const string fishingStartBalloonText7 = "s_fishing_start_balloon_text7"; - public const string fishingStartBalloonText8 = "s_fishing_start_balloon_text8"; - public const string fishingStartBalloonText9 = "s_fishing_start_balloon_text9"; - public const string fishFightingCastingBarText0 = "s_fishing_fishfighting_castingbar_text0"; - public const string fishFightingBalloonText0 = "s_fishing_fishfighting_balloon_text0"; - public const string fishFightingBalloonText1 = "s_fishing_fishfighting_balloon_text1"; - public const string fishFightingBalloonText2 = "s_fishing_fishfighting_balloon_text2"; - public const string fishFightingBalloonText3 = "s_fishing_fishfighting_balloon_text3"; - public const string fishFightingBalloonText4 = "s_fishing_fishfighting_balloon_text4"; - public const string fishFightingBalloonText5 = "s_fishing_fishfighting_balloon_text5"; - public const int WorldMapSpecialFunctionNpcID0 = 11001276; - public const string WorldMapSpecialFunctionNpcFrame0 = "airship_enabled"; - public const string WorldMapSpecialFunctionNpcTooltip0 = "s_worldmap_special_function_npc0"; - public const int WorldMapSpecialFunctionNpcID1 = 11001403; - public const string WorldMapSpecialFunctionNpcFrame1 = "airship_enabled"; - public const string WorldMapSpecialFunctionNpcTooltip1 = "s_worldmap_special_function_npc0"; - public const int WarpOpenContinent0 = 102; - public const int WarpOpenContinent1 = 103; - public const int WarpOpenContinent2 = 202; - public const int WarpOpenContinent3 = 105; - public const string WriteMusicDetailWebPage = "http://maplestory2.nexon.net/en/news/article/32329"; - public const int WriteMusicStoryBookID = 39000047; - public const int MusicListenInRadius = 900; - public const int MusicListenOutRadius = 2200; - public const int DungeonRoomMaxRewardCount = 99; - public const int DungeonMatchRecommendPickCount = 6; - public const int DungeonSeasonRankMinLevel = 99; - public const int LimitMeretRevival = 1; - public const int MinimapScaleSkipDuration = 5000; - public const int MinimapScaleSkipSplitPixel = 20; - public const int TradeMinMeso = 100; - public const int TradeMaxMeso = 500000000; - public const int TradeFeePercent = 20; - public const int DailyMissionRequireLevel = 50; - public const int MesoMarketBasePrice = 5000000; - public const int MesoMarketProductUnit0 = 5000000; - public const int MesoMarketBuyPayType = 16; - public const int MesoMarketIconType = 0; - public const string MesoMarketTokenDetailUrl = "http://maplestory2.nexon.net/en/news/article/45213"; - public const int BeautyHairShopGotoFieldID = 52000008; - public const int BeautyHairShopGotoPortalID = 1; - public const int BeautyColorShopGotoFieldID = 52000009; - public const int BeautyColorShopGotoPortalID = 1; - public const int BeautyFaceShopGotoFieldID = 52000010; - public const int BeautyFaceShopGotoPortalID = 1; - public const int BeautyStyleExpandSlotPrice = 980; - public const int BeautyStyleMaxSlotCount = 0; - public const int BeautyStyleDefaultSlotCount = 30; - public const int BeautyStyleExpandSlotCount1time = 3; - public const string CashShopFigureAddressPage = "http://maplestory2.nexon.com/cashshop/address"; - public const int NxaCashChargeWebPageWidth = 650; - public const int NxaCashChargeWebPageHeight = 650; - public const int ItemUnLockTime = 259200; - public const int PropertyProtectionTime = 60; - public const string TencentSecurityWebPage = "http://mxd2.qq.com/safe/index.shtml"; - public const int HomeBankCallDuration = 1000; - public const int HomeBankCallCooldown = 30000; - public const string HomeBankCallSequence = "Object_React_A"; - public const int HomeDoctorCallDuration = 1000; - public const int HomeDoctorCallCooldown = 30000; - public const string HomeDoctorCallSequence = "EmergencyHelicopter_A"; - public const int HomeDoctorNpcID = 11001668; - public const int HomeDoctorScriptID0 = 1; - public const int HomeDoctorScriptID1 = 10; - public const int EnchantMasterScriptID = 31; - public const int RestExpAcquireRate = 10000; - public const int RestExpMaxAcquireRate = 100000; - public const int ApartmentPreviewRequireLevel = 50; - public const int ApartmentPreviewRequireQuestID = 90000060; - public const int KeyboardGuideShowLevel = 13; - public const int extendAutoFishMaxCount = 8; - public const int extendAutoPlayInstrumentMaxCount = 8; - public const int ResetShadowBuffMeret = 100; - public const int InventoryExpandPrice1Row = 390; - public const int VIPServicePeriodLimitDay = 100000000; - public const int VIPMarketCommissionSale = 20; - public const int BreedDuration = 767; - public const int HarvestDuration = 767; - public const int RestartQuestStartField = 52000056; - public const int RestartQuestStartFieldRuneblader = 63000006; - public const int RestartQuestStartFieldStriker = 63000015; - public const int RestartQuestStartFieldSoulBinder = 63000035; - public const int QuestPortalKeepTime = 300; - public const string QuestPortalKeepNif = "Eff_Com_Portal_E_Quest"; - public const int QuestPortalDimensionY = 50; - public const int QuestPortalDimensionZ = 350; - public const int QuestPortalSummonTime = 600; - public const int QuestPortalDistanceFromNpc = 200; - public const int PetChangeNameMeret = 100; - public const int PetRunSpeed = 350; - public const int PetPickDistance = 1050; - public const int PetSummonCastTime = 800; - public const int PetBoreTime = 60000; - public const int PetIdleTime = 70000; - public const int PetTiredTime = 10000; - public const int PetSkillTime = 13000; - public const string PetEffectUse = "Pet/Eff_Pet_Use.xml"; - public const string PetEffectSkill = "Pet/Eff_Pet_Skill.xml"; - public const string PetEffectHappy = "Pet/Eff_Pet_Happy.xml"; - public const string PetGemChatBalloon = "pet"; - public const int PetTrapAreaDistanceEasy = 150; - public const int PetTrapAreaDistanceNormal = 150; - public const int PetTrapAreaDistanceHard = 150; - public const string PetTrapAreaEffectEasy = "Pet/Eff_Pet_TrapInstallArea_easy.xml"; - public const string PetTrapAreaEffectNormal = "Pet/Eff_Pet_TrapInstallArea_normal.xml"; - public const string PetTrapAreaEffectHard = "Pet/Eff_Pet_TrapInstallArea_hard.xml"; - public const string PetTrapAreaEffectOtherUser = "Pet/Eff_Pet_TrapArea_OtherUser.xml"; - public const string PetTamingMaxPointEffect = "Pet/Eff_PetTaming_MaxPoint.xml"; - public const string PetTamingAttackMissEffect = "Pet/Eff_PetTaming_Attack_Miss.xml"; - public const string PetTrapDropItemEffect = "Pet/Eff_PetTrap_DropItem.xml"; - public const int TamingPetEscapeTime = 300; - public const int TamingPetMaxPoint = 10000; - public const int PetNameLengthMin = 2; - public const int PetNameLengthMax = 25; - public const int PetTrapDropVisibleDelay = 2000; - public const int PetMaxLevel = 50; - public const string VisitorBookURL = ""; public const int OneShotSkillID = 19900061; - public const int BagSlotTabGameCount = 48; - public const int BagSlotTabSkinCount = 150; - public const int BagSlotTabSummonCount = 48; - public const int BagSlotTabMaterialCount = 48; - public const int BagSlotTabMasteryCount = 126; - public const int BagSlotTabLifeCount = 48; - public const int BagSlotTabQuestCount = 48; - public const int BagSlotTabGemCount = 48; - public const int BagSlotTabPetCount = 60; - public const int BagSlotTabPetEquipCount = 48; - public const int BagSlotTabActiveSkillCount = 84; - public const int BagSlotTabCoinCount = 48; - public const int BagSlotTabBadgeCount = 60; - public const int BagSlotTabMiscCount = 84; - public const int BagSlotTabLapenshardCount = 48; - public const int BagSlotTabPieceCount = 48; - public const int BagSlotTabGameCountMax = 48; - public const int BagSlotTabSkinCountMax = 150; - public const int BagSlotTabSummonCountMax = 48; - public const int BagSlotTabMaterialCountMax = 48; - public const int BagSlotTabMasteryCountMax = 48; - public const int BagSlotTabLifeCountMax = 48; - public const int BagSlotTabQuestCountMax = 48; - public const int BagSlotTabGemCountMax = 48; - public const int BagSlotTabPetCountMax = 78; - public const int BagSlotTabPetEquipCountMax = 48; - public const int BagSlotTabActiveSkillCountMax = 48; - public const int BagSlotTabCoinCountMax = 48; - public const int BagSlotTabBadgeCountMax = 48; - public const int BagSlotTabMiscCountMax = 48; - public const int BagSlotTabLapenshardCountMax = 48; - public const int BagSlotTabPieceCountMax = 48; - public const int MasteryObjectInteractionDistance = 150; - public const float GatheringObjectMarkOffsetX = 0.0f; - public const float GatheringObjectMarkOffsetY = 0.0f; - public const float BreedingObjectMarkOffsetX = 0.0f; - public const float BreedingObjectMarkOffsetY = 0.0f; - public const int UGCAttention = 0; - public const int UGCInfringementCenter = 1; - public const string CharacterSelectBoreIdleEffect_Ranger = ""; - public const string CharacterSelectBoreIdleEffect_SoulBinder = ""; - public const int DisableSoloPlayHighLevelDungeon = 0; - public const int MergeSmithScriptID = 10; - public const int AutoPressActionKeyDuration = 500; - public const int WebBrowserSizeWidthMin = 438; - public const int WebBrowserSizeWidthMax = 1700; - public const int WebBrowserSizeHeightMin = 708; - public const int WebBrowserSizeHeightMax = 1003; - public const bool WebBrowserEnableSizingButton = true; - public const int MeretAirTaxiPrice = 15; - public const int GlobalPortalMinLevel = 10; - public const int userMassiveExtraRewardMax = 5; - public const int SkillBookTreeAddTabFeeMeret = 990; - public const int MentorRequireLevel = 50; - public const int MenteeRequireLevel = 30; - public const int MentorMaxWaitingCount = 100; - public const int MenteeMaxReceivedCount = 20; public const int FindDungeonHelpEasyDungeonLevel = 50; - public const int CoupleEffectCheckTick = 5000; - public const int CoupleEffectCheckRadius = 150; - public const int FameContentsSkyFortressMapID0 = 02000421; - public const int FameContentsSkyFortressMapID1 = 02000422; - public const int FameContentsSkyFortressMapID2 = 52010039; - public const int FameContentsSkyFortressMapID3 = 52010040; - public const int AllianceQuestPickCount = 2; - public const int FieldQuestPickCount = 1; - public const int FameContentsSkyFortressGotoMapID = 02000422; - public const int FameContentsSkyFortressGotoPortalID = 3; public const int FameContentsRequireQuestID = 91000013; public const int FameExpedContentsRequireQuestID = 50101050; - public const int DailyPetEnchantMaxCount = 24; - public const int MouseCursorHideTime = 30; - public const int EnchantTransformScriptID = 10; - public const float AutoHideGroupAlpha = 0.6f; - public const int AutoHideGroupHitVisibleTick = 3000; - public const int UgcShopCharRotateStartDegreeY = 178; - public const int UgcShopCharRotateEndDegreeY = 8; public const int SurvivalScanAdditionalID = 71000052; public const int MapleSurvivalTopNRanking = 5; public const string MapleSurvivalSeasonRewardUrl = "http://maplestory2.nexon.net/en/news/article/32249/mushking-royale-championship-rewards"; - public const int TreeWateringEmotion = 10000; - public const int AdventureLevelLimit = 10000; - public const int AdventureLevelLvUpExp = 1000000; - public const int AdventureLevelMaxExp = 1500000; - public const float AdventureLevelFactor = 0.02f; - public const int AdventureExpFactorElite = 10; - public const int AdventureExpFactorBoss = 100; - public const int AdventureLevelStartLevel = 50; - public const int AdventureLevelLvUpRewardItem = 30001133; - public const int NameColorDeadDuration = 2000; - public const float MesoRevivalFeeReduceLimit = 0.5f; - public const float IngredientFeeReduceLimit = 0.5f; - public const int StatPointLimit_str = 100; - public const int StatPointLimit_dex = 100; - public const int StatPointLimit_int = 100; - public const int StatPointLimit_luk = 100; - public const int StatPointLimit_hp = 100; - public const int StatPointLimit_cap = 60; - public const float GamePadRumbleMultiple = 3.0f; - public const int NurturingEatMaxCount = 0; - public const int NurturingPlayMaxCount = 3; - public const string NurturingQuestTag = "NurturingGhostCats"; - public const int NurturingDuration = 3000; - public const int NurturingInteractionDistance = 150; - public const int NurturingEatGrowth = 10; - public const int NurturingPlayGrowth = 10; - public const int NurturingPlayMailId = 19101804; - public const int NurturingPlayMaxGrowth = 3; - public const int NurturingHungryTime = 1000; - public const int SkillPointLimitLevel1 = 80; - public const int SkillPointLimitLevel2 = 70; - public const int SellPriceNormalMax = 4628; - public const int SellPriceRareMax = 5785; - public const int SellPriceEliteMax = 7405; - public const int SellPriceExcellentMax = 9256; - public const int SellPriceLegendaryMax = 11339; - public const int SellPriceArtifactMax = 13653; - public const string RegionServerUrl_de = "http://ugc.maplestory2.nexon.net/region/region_DE.xml"; - public const string RegionServerUrl_en = "http://ugc.maplestory2.nexon.net/region/region_EN.xml"; - public const string RegionServerUrl_bpo = "http://ugc.maplestory2.nexon.net/region/region_BPO.xml"; public const int HoldAttackSkillID = 10700252; - public const int TooltipLabelMaxWidth = 408; - public const int ClubNameLengthMin = 2; - public const int ClubNameLengthMax = 25; - public const int ClubMaxCount = 3; - public const int UgcNameLengthMin = 3; - public const int UgcNameLengthMax = 25; - public const int UgcTagLengthMax = 12; - public const int ChangeJobLevel = 60; - public const int LapenshardOpenQuestID = 20002391; - public const int MaidNameLengthMin = 1; - public const int MaidNameLengthMax = 35; - public const int MaidDescLengthMin = 1; - public const int MaidDescLengthMax = 35; - public const int GamePadStickMoveValue = 50; - public const int HighlightMenuUsingLevel = 5; - public const int PartyVoteReadyDurationSeconds = 20; - public const int PartyVoteReadyTagExpireSeconds = 10; - public const int ShieldBarOffsetY = -10; - public const int MouseInteractLimitDistance = 2000; - public const int AutoInstallEquipmentMinLevel = 5; - public const int AutoInstallEquipmentMaxLevel = 49; - public const string PartySearchRegisterComboValues = "4,6,10"; - public const int StatScaleMarkingAdditionalEffect = 70000174; - public const string DungeonRewardFailEmotions = "90200001,90200009,90200005,90200018"; - public const int SummonPetSkillID = 82100001; - public const int UGCMapSetItemEffectCountLimit = 10; public const string DiscordAppID = "555204064091045904"; - public const int ItemBoxMultiOpenMaxCount = 10; - public const int ItemBoxMultiOpenLimitCount = 500; - public const int BuffBalloonDistance = 3800; - public const int PaybackStartDate = 20191024; - public const int PaybackMailId = 50000020; - public const int PaybackMailPeriodDay = 90; - public const int PaybackMaxRewardMeret = 10000; - public const string PaybackGuideUrl = "http://maplestory2.nexon.com/News/Events"; - public const int DummyNpcMale = 2040998; - public const int DummyNpcFemale = 2040999; - public static int DummyNpc(Gender gender) => gender is Gender.Female ? DummyNpcFemale : DummyNpcMale; - - #endregion - - #region server table/constants.xml - 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; - public const float NpcLastSightHeightUp = 525; - public const float NpcLastSightHeightDown = 225; - - public const int RecoveryHPWaitTick = 1000; - public const int RecoverySPWaitTick = 1000; - public const int RecoveryEPWaitTick = 1000; - public const float FallBoundingAddedDistance = 750f; - - public const int UserBattleDurationTick = 5000; - - public const int SystemShopNPCIDConstruct = 11000486; - public const int SystemShopNpcIDUGCDesign = 11000166; - public const int SystemShopNPCIDHonorToken = 11001562; - public const int SystemShopNPCIDFishing = 11001609; - public const int SystemShopNPCIDMentor = 11003561; - public const int SystemShopNPCIDMentee = 11003562; #endregion } diff --git a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs index ae2b913b7..1f056e34c 100644 --- a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs +++ b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs @@ -1,14 +1,869 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Maple2.Model.Metadata; -namespace Maple2.Model.Metadata; +public record ConstantsTable(Constants Constants) : ServerTable; -public record ConstantsTable(IReadOnlyDictionary Constants) : ServerTable; - -public record Constants ( - string Key, - string Value - ); +public record Constants( + float NPCColorScale = 0f, + float NPCDuration = 0f, + float PCColorScale = 0f, + float PCDuration = 0f, + float GetEXPColorScale = 0f, + float GetEXPDuration = 0f, + float AccumulationRatio = 0f, + float NPCRandomDeadPushRate = 0f, + float CustomizingWheelSpeed_Morph = 0f, + float CustomizingWheelSpeed_Item = 0f, + float CustomizingWheelSpeed_Makeup = 0f, + float CustomizingRotationSpeed_Makeup = 0f, + float CustomizingHairFirstPartHighlight = 0f, + float CustomizingHairSecondPartHighlight = 0f, + float LookAtInterval = 0f, + float LookAtDistanceNPC = 0f, + float LookAtDistanceCry = 0f, + bool EnableSkillJumpDown = false, + bool EscapeHitMethodSkill = false, + bool EscapeHitMethodJump = false, + bool EscapeHitMethodMove = false, + bool EscapeHitMoveKeyIsDown = false, + bool CancelSwing_KeyIsDown = false, + bool SkillGlobalCooldown = false, + bool SkillGlobalCooldown_CheckSameSkill = false, + int DamageDistance = 0, + int TalkableDistance = 0, + bool TalkableFrontOnly = false, + int DropIconVisibleDistance = 0, + int ChatBalloonDistance = 0, + int HpBarDistance = 0, + int EmoticonVisibleDistance = 0, + int RegistUgcDistance = 0, + int RegistUgcDistanceClose = 0, + int ConstructUgcDistance = 0, + int FunctionCubeDistance = 0, + int InteractionDistance = 0, + int HouseMarkShowDistance = 0, + int HouseMarkShowClippingUp = 0, + int HouseMarkShowClippingDown = 0, + int HouseMarkPopupDistance = 0, + int UgcBoundaryStartDistance = 0, + int UgcBoundaryEndDistance = 0, + int DurationForBoundaryDisplay = 0, + TimeSpan UgcHomeSaleWaitingTime = new(), + int UgcContainerExpireDurationNormal = 0, + int UgcContainerExpireDurationCash = 0, + int UgcContainerExpireDurationMeret = 0, + int UgcHomeExtensionNoticeDate = 0, + int UgcHomePasswordExpireDuration = 0, + bool CubeLiftHeightLimitUp = false, + bool CubeLiftHeightLimitDown = false, + int CubeCraftSafetyCapID = 0, + int CubeCraftLightStickLeftID = 0, + int CubeCraftLightStickRightID = 0, + float DropIconDistance = 0f, + int DropIconHeadOffset = 0, + int DropItemMaxLength = 0, + int DropMoneyMaxLength = 0, + float DropItemTargetZPos = 0f, + float DropItemPickUpVel = 0f, + float DropItemPickUpGravity = 0f, + float DropItemPickUpCompleteRotateTime = 0f, + int DropItemPickUpCompleteRotateVel = 0, + int ChatBalloonDuration = 0, + int BoreWaitingTick = 0, + int OffsetPcHpBar = 0, + int OffsetPcNameTag = 0, + int OffsetPcChatBalloon = 0, + int OffsetPcDamageNumber = 0, + int OffsetPcProfileTag = 0, + float fOffsetOnTombstoneNameTag = 0f, + int OffsetNpcHpBar = 0, + int OffsetNpcNameTag = 0, + int OffsetNpcEmoticon = 0, + int OffsetNpcChatBalloon = 0, + int OffsetNpcDamageNumber = 0, + int OffsetNpcMonologue = 0, + int OffsetActionTooltipX = 0, + int OffsetActionTooltipY = 0, + int OffsetPcPopupMenu = 0, + int DamageGap = 0, + int DamageRenderCount = 0, + int DamageRenderTotalCount = 0, + float DamageOtherScale = 0f, + float DamageOtherAlpha = 0f, + int DamageEffectMinHPPercent = 0, + int DamageEffectCriticalPercent = 0, + int ShopSeedResetTime = 0, + int ShopRepurchaseMax = 0, + int ShopSellConfirmPrice = 0, + int ShopBuyConfirmPrice = 0, + float ClimbVelocityV = 0f, + float ClimbVelocityH = 0f, + int StoreExpandMaxSlotCount = 0, + int StoreExpandPrice1Row = 0, + int StoreDepositMax = 0, + int StoreWithdrawMax = 0, + int CameraExtraMoveScaleByMonster = 0, + int CameraExtraMoveScaleByMap = 0, + int CameraExtraDistance = 0, + float CameraFinalLoose = 0f, + float CameraCurrentLoose = 0f, + float CameraUpdateLoose = 0f, + int CameraVelocityInPortalMove = 0, + int MonologueInterval = 0, + int MonologueRandom = 0, + int MonologueShowTime = 0, + int ShowKillCountMin = 0, + int UserRevivalInvincibleTick = 0, + int UserRevivalPenaltyPercent = 0, + string UserRevivalIconPath = "", + string UserRevivalInvincibleIconPath = "", + int GetExpMinVelocity = 0, + int GetExpVelocityPer1Length = 0, + string GetExpControlValue0 = "", + string GetExpControlValue1 = "", + string GetExpTargetPCDummyName = "", + float GetExpTimeAcceleration = 0f, + float GetExpCollisionRadius = 0f, + int SkipFrameGameObject = 0, + int SkipFrameDistanceGameObject = 0, + float RegionSkillFadeOutDuration = 0f, + int PassengerProfileImageSize = 0, + int PassengerProfileImageLifeTime = 0, + int PassengerProfileImageShowNumber = 0, + int PassengerProfileImageShowCooldown = 0, + int PassengerProfileImageShowCooldownParty = 0, + int PassengerProfileImageShowRange = 0, + int UGCCameraDefaultSize = 0, + int UGCCameraMinSize = 0, + int UGCCameraMaxSize = 0, + int UGCCameraSnapshotPreviewTime = 0, + int UGCImgUploadSizeLimit = 0, + int UGCImgFileCountCheck = 0, + int WindAmp2Cloak = 0, + float WindPeriod2Cloak = 0f, + float WindPeriodVar2Cloak = 0f, + float VolumeMyPcToNpc = 0f, + float VolumeMyPcToObject = 0f, + float VolumeMyPcToBreakableObject = 0f, + float VolumeNpcToMyPc = 0f, + float VolumePcToNpc = 0f, + float VolumePcToBreakableObject = 0f, + float VolumeNpcToPc = 0f, + float VolumeOtherPc = 0f, + int ItemDropLevelMaxBoundary = 0, + float moneyTreeDropHeight = 0f, + float moneyTreeDropBase = 0f, + int moneyTreeDropRandom = 0, + int WhisperIgnoreTime = 0, + int WhisperMaxCount = 0, + int WhisperDurationTime = 0, + float BossHpBarAutoDetectRange = 0f, + float BossHpBarDuration = 0f, + float FindHoldTargetRange = 0f, + int FindGrabNodeRange = 0, + string UgcShopCharCameraLookat = "", + string UgcShopCharCameraPos = "", + int UgcShopCharCameraMinDistance = 0, + int UgcShopCharCameraZoomVelocity = 0, + string UgcShopCubeCameraLookat = "", + string UgcShopCubeCameraPos = "", + int UgcShopCubeCameraMinDistance = 0, + int UgcShopCubeCameraZoomVelocity = 0, + string UgcShopRideeCameraLookat = "", + string UgcShopRideeCameraPos = "", + int UgcShopRideeCameraMinDistance = 0, + int UgcShopRideeCameraZoomVelocity = 0, + int FieldCachingCount = 0, + float FieldCachingTime = 0f, + int FieldCachingMaxCount = 0, + int FieldUnloadThreshold = 0, + float EffectLODOneStepDistance = 0f, + float EffectLODTwoStepDistance = 0f, + float EffectLODThreeStepDistance = 0f, + int TelescopeFindDistance = 0, + int BoatPrice = 0, + int QuestGuidePageCount = 0, + int QuestGuideMaxCount = 0, + float CameraInterpolationTime = 0f, + int TransparencyCP = 0, + int TransparencyEY = 0, + int TransparencyCL = 0, + int TransparencyPA = 0, + int TransparencyMT = 0, + int TransparencyEA = 0, + int TransparencyFH = 0, + int TransparencyGL = 0, + int TransparencyRI = 0, + int TransparencySH = 0, + float DefaultDropItemAlpha = 0f, + float DropItemPickFailHeight = 0f, + float DropItemPickFailTime = 0f, + int TaxiStationFindDistance = 0, + int TaxiCallDuration = 0, + int TaxiCallBestDriverDuration = 0, + int TaxiCallBestDriverLevel = 0, + int AirTaxiCashCallDuration = 0, + int AirTaxiMesoCallDuration = 0, + int TradeRequestDuration = 0, + int UserPortalInvincibleTick = 0, + string UserPortalInvincibleIconPath = "", + int SummonRideeDuration = 0, + int WorldMapAdjustTileX = 0, + int WorldMapAdjustTileY = 0, + float TimeScalePCScale = 0f, + float TimeScalePCDuration = 0f, + int GoToHomeCastingTime = 0, + int returnHomeSkill = 0, + int returnHomeSkillMeret = 0, + int TutorialIntroSkipTime = 0, + string AvatarDefaultItemMale = "", + string AvatarDefaultItemFemale = "", + int TalkCooldown = 0, + int AddressPopupDuration = 0, + int MaxFPS = 0, + int UGCShopSellMinPrice = 0, + int UGCShopSellMaxPrice = 0, + int UGCShopSaleDay = 0, + int UGCShopAdFeeMeret = 0, + int UGCShopAdHour = 0, + int UGCShopSellingRestrictAmount = 0, + int MeretMarketHomeBannerShowTick = 0, + int BlackMarketSellMinPrice = 0, + int BlackMarketSellMaxPrice = 0, + int BlackMarketSellEndDay = 0, + int ItemTransferBlackMarketGrade = 0, + int UgcBannerCheckTime = 0, + int FastChat_CheckTime = 0, + int FastChat_CheckCount = 0, + int SameChat_CheckTime = 0, + int SameChat_CheckCount = 0, + int SameChat_RestrictTime = 0, + int FastChat_RestrictTime = 0, + int RestrictChat_AddRestrictTime = 0, + int AccumWarning_AddRestrictTime = 0, + int RestrictWarning_ReleaseTime = 0, + int MaxChatLength = 0, + int EmotionBoreAnimProbability = 0, + float FallMoveSpeed = 0f, + int GuildCreatePrice = 0, + int GuildCreateMinLevel = 0, + int GuildNameLengthMin = 0, + int GuildNameLengthMax = 0, + int guildFundMax = 0, + float guildFundRate = 0f, + int guildExpMaxCountForPlayTime = 0, + int guildDonateMeso = 0, + int guildStorageGiftNeedPeriod = 0, + int guildStorageItemMail = 0, + string mirrorGuideMoviePath = "", + string hairGuideMoviePath = "", + string makeUpGuideMoviePath = "", + int FastShimmerRadius = 0, + int FastShimmerHeight = 0, + int SmartRecommendNotify_DurationTick = 0, + int BootyPopupDuration = 0, + string charCreateGuideMoviePath = "", + int KeyIsDownSkill_MaxDurationTick = 0, + int MaxBuddyCount = 0, + int MaxBlockCount = 0, + int UserPendingRemoveTime = 0, + int HideCubeDuration = 0, + int DailyTrophyPickDiffLevel = 0, + int dailyAchievePickCount = 0, + int MovementEventDistance = 0, + int HomeReturnPortalKeepTime = 0, + string HomeReturnPortalKeepNif = "", + int HomeReturnPortalDimensionY = 0, + float GlobalCubeSkillIntervalTime = 0f, + int RoomEnterPortalDurationTick = 0, + int NpcBossCubeSkillCreateHeight = 0, + int NPCUpdateTickNoUser = 0, + int NPCUpdateTickWanderIdle = 0, + int NpcSmallSize = 0, + int NpcMidSize = 0, + int NpcBigSize = 0, + int NpcMidCutline = 0, + int NpcBigCutline = 0, + int NpcHpRegenStartTime = 0, + int NpcBossHpRegenStartTime = 0, + int NpcHpRegenPeriod = 0, + float NpcHpRegenPercent = 0f, + float NpcBossHpRegenPercent = 0f, + int NpcCombatAbandon = 0, + int BossCombatAbandon = 0, + int NpcImpossibleCombatAbandon = 0, + int MobLifeTimeExtend = 0, + float CanGetRewardDistance = 0f, + float CanGetRewardEliteDistance = 0f, + float CanGetRewardBossDistance = 0f, + int ExpLevelMaxBoundry = 0, + int CorpseHitDeadAfterCoolDown = 0, + int CorpseHitDropCoolDown = 0, + int CorpseHitDeadAfterAssistBonusCoolDown = 0, + int CorpseHitDropAssistBonusCoolDown = 0, + int UserBattleDurationTick = 0, + int RecoveryHPWaitTick = 0, + int RecoverySPWaitTick = 0, + int RecoveryEPWaitTick = 0, + int timeResetDead = 180, + int maxDeadCount = 0, + byte hitPerDeadCount = 0, + int spawntimePerDeadCount = 0, + int UserRevivalPaneltyTick = 0, + int UserRevivalPaneltyMinLevel = 0, + int revivalRate = 0, + int ChaosPointGetDefault = 0, + int ChaosPointGetPenaltyLevel = 0, + int ChaosPointGetPenalty = 0, + float FallBoundingAddedDistance = 0f, + int BoatDestinationID = 0, + int NpcKillRecoveryProbability = 0, + int InteractRemoveRetryTick = 0, + int TradeDistance = 0, + int PlayTimeDurationTick = 0, + TimeSpan DailyTrophyResetDate = new(), + float NpcLastingSightRadius = 0f, + float NpcLastingSightHeightUp = 0f, + float NpcLastingSightHeightDown = 0f, + int HoldTimeEventTick = 5000, + int OnixItemID = 0, + int BindOnixItemID = 0, + int ChaosOnixItemID = 0, + int BindChaosOnixItemID = 0, + int CrystalItemID = 0, + int ChaosCrystalItemID = 0, + int SkillChaosCrystalItemID = 0, + int RedCrystalItemID = 0, + int BlueCrystalItemID = 0, + int GreenCrystalItemID = 0, + int enchantReturnItemID = 0, + int PetCapsuleItemID = 0, + int shadowWorldBuffHpUp = 0, + int shadowWorldBuffMoveProtect = 0, + int pvpZoneUserGlobalDropID = 0, + int pvpZoneUserIndividualDropID = 0, + int pvpZoneUserDropRank = 0, + int shadowWorldUserGlobalDropID = 0, + int shadowWorldUserIndividualDropID = 0, + int shadowWorldUserDropRank = 0, + int userKillDuration = 0, + int userKillSlayerCount = 0, + int userKillRulerCount = 0, + int SystemShopNPCIDConstruct = 0, + int SystemShopNpcIDUGCDesign = 0, + int SystemShopNPCIDHonorToken = 0, + int SystemShopNPCIDFishing = 0, + int SystemShopNPCIDMentor = 0, + int SystemShopNPCIDMentee = 0, + string BlackMarketOpeningTime = "", + string BlackMarketClosingTime = "", + int BlackMarketCollectWaitSecond = 0, + int GmGlideSkillID = 0, + int normalChannelMin = 0, + int normalChannelUser = 0, + int shadowChannelMin = 0, + int shadowChannelUser = 0, + int dynamicChannelDecreaseTick = 0, + int PeriodOfMaidEmployment = 0, + int MaidReadyToPay = 0, + int MaidAffinityMax = 0, + float invokeEffectTargetCountFactor1 = 0f, + float invokeEffectTargetCountFactor2 = 0f, + float invokeEffectTargetCountFactor3 = 0f, + float invokeEffectTargetCountFactor4 = 0f, + float invokeEffectTargetCountFactor5 = 0f, + float invokeEffectTargetCountFactor6 = 0f, + float invokeEffectTargetCountFactor7 = 0f, + float invokeEffectTargetCountFactor8 = 0f, + float invokeEffectTargetCountFactor9 = 0f, + float invokeEffectTargetCountFactor10 = 0f, + int MeratRevivalDebuffCode = 0, + float MeratRevivalFeeReduceLimit = 0f, + int MeratConsumeWorldChat = 0, + int MeratConsumeChannelChat = 0, + int MeratConsumeSuperChat = 0, + int guildPVPMatchingTime = 0, + int guildPVPWinPoint = 0, + int guildPVPLosePoint = 0, + string guildPVPAdditionalEffect = "", + int ModePvPRecoverySkill = 0, + int ModePvPRecoverySP = 0, + int ModePvPInvincibleTime = 0, + int ModePVPAdditionalEffect = 0, + int ModePvPReviveBuff = 0, + int pvpBtiRewardItem = 0, + int pvpBtiRewardWinnerCount = 0, + int pvpBtiRewardLoserCount = 0, + int PvpGuildRewardItem = 0, + int PvpGuildRewardWinnerCount = 0, + int PvpGuildRewardLoserCount = 0, + string ModePVPRedArenaAdditionalEffect = "", + int ModePvPScoreDead = -50, + int ModePvPScoreKill = 100, + string ModePVPBloodMineAdditionalEffect = "", + int pvpFFAShortComboTick = 0, + int pvpFFALongComboTick = 0, + int pvpFFASlayerCount = 0, + int pvpFFARulerCount = 0, + int PvpFFAReward1Count = 0, + int PvpFFAReward2Count = 0, + int PvpFFAReward3Count = 0, + int PvpFFAReward4Count = 0, + int PvpFFAReward5Count = 0, + int PvpFFAReward6Count = 0, + int PvpFFAReward7Count = 0, + int PvpFFAReward8Count = 0, + int PvpFFAReward9Count = 0, + int PvpFFAReward10Count = 0, + int PvpFFARewardItem = 0, + int PvpFFAAdditionRewardRate = 0, + int rankDuelPvpMatchingTime = 0, + int rankDuelPvpMatchingMinGap = 0, + string ModePVPDuelRankArenaAdditionalEffect = "", + int MailExpiryDays = 0, + int MailExpiryDaysPremium = 0, + int MailExpiryDaysBlackMarket = 0, + int MailExpiryDaysFittingDoll = 0, + int decreaseMaidMoodValue = 0, + int decreaseMaidMoodMinutes = 0, + int WorldmapBossTooltipCount = 0, + int ShowNameTagEnchantItemGrade = 0, + int ShowNameTagEnchantLevel = 0, + int BossNotifyAbsLevel = 0, + int RoomExitWaitSecond = 0, + int AdditionalMesoMaxRate = 0, + int AdditionalExpMaxRate = 0, + int HonorTokenMax = 0, + int KarmaTokenMax = 0, + int LuTokenMax = 0, + int HabiTokenMax = 0, + int ReverseCoinMax = 0, + int MentorTokenMax = 0, // From KMS + int MenteeTokenMax = 0, // From KMS + int CharacterDestroyDivisionLevel = 0, + int CharacterDestroyWaitSecond = 0, + int BossShimmerScaleUpActiveDistance = 0, + float BossShimmerScaleUpSize = 0f, + float SwimDashSpeed = 0f, + int UserTriggerStateMax = 0, + int UserTriggerEnterActionMax = 0, + int UserTriggerConditionMax = 0, + int UserTriggerConditionActionMax = 0, + int PCBangAdditionalEffectID = 0, + int PCBangAdditionalEffectExp = 0, + int PCBangAdditionalEffectMeso = 0, + int PCBangItemDefaultPeriod = 0, + int ShadowWorldAutoReviveDeadAction = 0, + int GoodIteriorRecommendUICloseTime = 0, + string UGCInfoDetailViewPage = "", + int UGCInfoStoryBookID = 0, + int HomePasswordUsersKickDelay = 0, + string TriggerEditorHelpURL = "", + int partyBuffID0 = 0, + int partyBuffID1 = 0, + int returnUserPartyBuffID0 = 0, + int returnUserPartyBuffID1 = 0, + int QuestRewardSAIgnoreLevel = 0, + int ugcmapMaxUserCount = 0, + int RecallCastingTime = 0, + string RecallGuildPortalNif = "", + string RecallPartyPortalNif = "", + string RecallWeddingPortalNif = "", + int PartyRecallMerat = 0, + int RecallPortalKeepTime = 0, + int RecallPartyPortalKeepTime = 0, + float CashCallMedicLeaveDelay = 0f, + int characterSlotBaseCount = 0, + int characterSlotMaxExtraCount = 0, + int HitNPCDropCooldown = 0, + int DropSPEPBallMaxLength = 0, + int DropSPEPBallTargetZPos = 0, + int DropSPEPBallPickUpVel = 0, + int DropSPEPBallPickUpGravity = 0, + float DropSPEPBallPickUpCompleteRotateTime = 0f, + int DropSPEPBallPickUpCompleteRotateVel = 0, + int EnchantItemBindingRequireLevel = 0, + int enchantSuccessBroadcastingLevel = 0, + int EnchantEquipIngredientMaxCount = 0, + int EnchantFailStackUsingMaxCount = 0, + int EnchantFailStackTakeMaxCount = 0, + int EnchantEquipIngredientOpenLevel = 0, + int EnchantEquipIngredientOpenRank = 0, + int EnchantEquipIngredientMaxSuccessProb = 0, + int EnchantFailStackOpenLevel = 0, + int EnchantFailStackTakeMaxSuccessProb = 0, + int EnchantExpRefundMail = 0, + int BankCallDuration = 0, + string NoticeDialogUrl = "", + string NoticeDialogUrlPubTest = "", + int NoticeDialogOpenSeconds = 0, + int RemakeOptionMaxCount = 0, + int fishFightingProp = 0, + int FisherBoreDuration = 0, + string fishingStartCastingBarText0 = "", + string fishingStartCastingBarText1 = "", + string fishingStartCastingBarText2 = "", + string fishingStartCastingBarText3 = "", + string fishingStartCastingBarText4 = "", + string fishingStartBalloonText0 = "", + string fishingStartBalloonText1 = "", + string fishingStartBalloonText2 = "", + string fishingStartBalloonText3 = "", + string fishingStartBalloonText4 = "", + string fishingStartBalloonText5 = "", + string fishingStartBalloonText6 = "", + string fishingStartBalloonText7 = "", + string fishingStartBalloonText8 = "", + string fishingStartBalloonText9 = "", + string fishFightingCastingBarText0 = "", + string fishFightingBalloonText0 = "", + string fishFightingBalloonText1 = "", + string fishFightingBalloonText2 = "", + string fishFightingBalloonText3 = "", + string fishFightingBalloonText4 = "", + string fishFightingBalloonText5 = "", + int WorldmapSpecialFunctionNpcID0 = 0, + string WorldmapSpecialFunctionNpcFrame0 = "", + string WorldmapSpecialFunctionNpcTooltip0 = "", + int WorldmapSpecialFunctionNpcID1 = 0, + string WorldmapSpecialFunctionNpcFrame1 = "", + string WorldmapSpecialFunctionNpcTooltip1 = "", + int WarpOpenContinent0 = 0, + int WarpOpenContinent1 = 0, + int WarpOpenContinent2 = 0, + int WarpOpenContinent3 = 0, + string WriteMusicDetailWebPage = "", + int WriteMusicStoryBookID = 0, + int MusicListenInRadius = 0, + int MusicListenOutRadius = 0, + int MusicEnsembleRadius = 0, + int MusicEnsembleDisplayAdditionalID = 0, + int DungeonRandomMatchBuffID = 0, + int DungeonRoomMaxRewardCount = 0, + int DungeonMatchRecommendPickCount = 0, + int DungeonSeasonRankMinLevel = 0, + int ChaosDungeonReviveBossBuff = 0, + int ChaosDungeonReviveUserDebuff = 0, + int LimitMeratRevival = 0, + int MinimapScaleSkipDuration = 0, + int MinimapScaleSkipSplitPixel = 0, + int TradeMinMeso = 0, + int TradeMaxMeso = 0, + int TradeFeePercent = 0, + int GuideQuestDailyPickCountCommon = 0, + int GuideQuestDailyPickCountDungeon = 0, + int GuideQuestDailyPickCountBoss = 0, + int DailyMissionPickCount = 0, + int DailyMissionRequireLevel = 0, + float NearDropDistance =0f, + float FarDropDistance = 0f, + int MesoMarketBasePrice = 0, + int MesoMarketProductUnit0 = 0, + int MesoMarketProductUnit1 = 0, + int MesoMarketProductUnit2 = 0, + int MesoMarketProductUnit3 = 0, + int MesoMarketProductUnit4 = 0, + int MesoMarketProductUnit5 = 0, + int MesoMarketProductUnit6 = 0, + int MesoMarketProductUnit7 = 0, + int MesoMarketProductUnit8 = 0, + int MesoMarketProductUnit9 = 0, + int MesoMarketBuyPayType = 0, + int MesoMarketIconType = 0, + string MesoMarketTokenDetailUrl = "", + int BeautyHairShopGotoFieldID = 0, + int BeautyHairShopGotoPortalID = 0, + int BeautyColorShopGotoFieldID = 0, + int BeautyColorShopGotoPortalID = 0, + int BeautyFaceShopGotoFieldID = 0, + int BeautyFaceShopGotoPortalID = 0, + int DropItemSendMail = 0, + int BeautyStyleExpandSlotPrice = 0, + int BeautyStyleMaxSlotCount = 0, + int BeautyStyleDefaultSlotCount = 0, + int BeautyStyleExpandSlotCount1time = 0, + int LuckyBagCouponItemID = 0, + string CashshopFigureAddressPage = "", + string TencentCashChargeWebPage = "", + int TencentCashChargeWebPageWidth = 0, + int TencentCashChargeWebPageHight = 0, + int NxaCashChargeWebPageWidth = 0, + int NxaCashChargeWebPageHeight = 0, + int ItemUnLockTime = 0, + int PropertyProtectionTime = 0, + string TencentSecurityWebPage = "", + int HomeBankCallDuration = 0, + int HomeBankCallCooltime = 0, + string HomeBankCallSequence = "", + int HomeDoctorCallDuration = 0, + int HomeDoctorCallCooltime = 0, + string HomeDoctorCallSequence = "", + int HomeDoctorNpcID = 0, + int HomeDoctorScriptID0 = 0, + int HomeDoctorScriptID1 = 0, + int EnchantMasterScriptID = 0, + int RestExpAcquireRate = 0, + int RestExpMaxAcquireRate = 0, + int ApartmentPreviewRequireLevel = 0, + int ApartmentPreviewRequireQuestID = 0, + int CharacterAbilityDefaultPoint = 0, + int CharacterAbilityResetCoolTimeMinute = 0, + int CharacterAbilityResetMeso = 0, + int CharacterAbilityResetMerat = 0, + int CharacterAbilityOpenQuestID = 0, + int KeyboardGuideShowLevel = 0, + int extendAutoFishMaxCount = 0, + int extendAutoPlayInstrumentMaxCount = 0, + int ResetShadowBuffMerat = 0, + int InventoryExpandPrice1Row = 0, + int VIPServicePeriodLimitDay = 0, + int VIPMarketCommissionSale = 0, + int DungeonMatchNormalTimeOutTick = 0, + int ChaosDungeonHallFieldID = 0, + int ReverseRaidDungeonHallFieldID = 0, + int LapentaDungeonHallFieldID = 0, + int ColosseumDungeonHallFieldID = 0, + int BreedDuration = 0, + int HarvestDuration = 0, + int DungeonRewardUnLimitedMesoPercent = 0, + int DungeonRewardUnLimitedExpPercent = 0, + int RestartQuestStartField = 0, + int RestartQuestStartFieldRuneBlader = 0, + int RestartQuestStartFieldStriker = 0, + int RestartQuestStartFieldSoulBinder = 0, + int QuestPortalKeepTime = 0, + string QuestPortalKeepNif = "", + int QuestPortalDimensionY = 0, + int QuestPortalDimensionZ = 0, + int QuestPortalSummonTime = 0, + int QuestPortalDistanceFromNpc = 0, + int PetChangeNameMerat = 0, + int ConstructAuthorityMax = 0, + int HonorTokenResetDayOfWeek = 0, + int HonorTokenResetTimeHour = 0, + int PetLastAttackSkillCheckTick = 0, + string PetBattleAiPath = "", + int PetRunSpeed = 0, + int PetUpdateTargetInterval = 0, + int PetPickDistance = 0, + int PetSummonCastTime = 0, + int PetBoreTime = 0, + int PetIdleTime = 0, + int PetTiredTime = 0, + int PetSkillTime = 0, + string PetEffectUse = "", + string PetEffectSkill = "", + string PetEffectHappy = "", + string PetGemChatBalloon = "", + int PetTrapAreaDistanceEasy = 0, + int PetTrapAreaDistanceNormal = 0, + int PetTrapAreaDistanceHard = 0, + string PetTrapAreaEffectEasy = "", + string PetTrapAreaEffectNormal = "", + string PetTrapAreaEffectHard = "", + string PetTrapAreaEffectOtherUser = "", + string PetTamingMaxPointEffect = "", + string PetTamingAttackMissEffect = "", + string PetTrapDropItemEffect = "", + float TamingPetEscapeRate = 0f, + int TamingPetEscapeTime = 0, + int TamingPetMaxPoint = 0, + float TamingPetValidDistance = 0f, + int PetNameLengthMin = 0, + int PetNameLengthMax = 0, + int PetTrapDropVisibleDelay = 0, + int PetMaxLevel = 0, + string VisitorBookURL = "", + int bagSlotTabGameCount = 0, + int bagSlotTabSkinCount = 0, + int bagSlotTabSummonCount = 0, + int bagSlotTabMaterialCount = 0, + int bagSlotTabMasteryCount = 0, + int bagSlotTabLifeCount = 0, + int bagSlotTabQuestCount = 0, + int bagSlotTabGemCount = 0, + int bagSlotTabPetCount = 0, + int bagSlotTabActiveSkillCount = 0, + int bagSlotTabCoinCount = 0, + int bagSlotTabBadgeCount = 0, + int bagSlotTabMiscCount = 0, + int bagSlotTabLapenShardCount = 0, + int bagSlotTabPieceCount = 0, + int MasteryObjectInteractionDistance = 0, + float GatheringObjectMarkOffsetX = 0f, + float GatheringObjectMarkOffsetY = 0f, + float BreedingObjectMarkOffsetX = 0f, + float BreedingObjectMarkOffsetY = 0f, + int UGCAttention = 0, + int UGCInfringementCenter = 0, + string CharacterSelectBoreIdleEffect_Ranger = "", + string CharacterSelectBoreIdleEffect_SoulBinder = "", + int DisableSoloPlayHighLevelDungeon = 0, + int DungeonMatchCooldownTime = 0, + int DungeonUnitedRewardCountResetLevel = 0, + int MergeSmithScriptID = 0, + int AutoPressActionKeyDuration = 0, + int WebBrowserSizeWidthMin = 0, + int WebBrowserSizeWidthMax = 0, + int WebBrowserSizeHeightMin = 0, + int WebBrowserSizeHeightMax = 0, + bool WebBrowserEnableSizingButton = false, + string LiveBroadcastURL = "", + string TencentWebURL = "", + int SeasonDataSpareCount = 0, + int WebBrowserPopupSizeWidthMin = 0, + int WebBrowserPopupSizeWidthMax = 0, + int WebBrowserPopupSizeHeightMin = 0, + int WebBrowserPopupSizeHeightMax = 0, + int GlobalPortalMinLevel = 0, + int userMassiveExtraRewardMax = 0, + int SkillBookTreeAddTabFeeMerat = 0, + int MentorRequireLevel = 0, + int MenteeRequireLevel = 0, + int MentorMaxWaitingCount = 0, + int MenteeMaxReceivedCount = 0, + int CoupleEffectCheckTick = 0, + int CoupleEffectCheckRadius = 0, + int FameContentsSkyFortressMapID0 = 0, + int FameContentsSkyFortressMapID1 = 0, + int FameContentsSkyFortressMapID2 = 0, + int FameContentsSkyFortressMapID3 = 0, + int AllianceQuestPickCount = 0, + int FieldQuestPickCount = 0, + int FameContentsSkyFortressGotoMapID = 0, + int FameContentsSkyFortressGotoPortalID = 0, + int FameContentsSkyFortressBridgeID = 0, + int FameContentsMissionAttackCount = 0, + int FameContentsFieldQuestPickAccept = 0, + int FameContentsFieldQuestPickComplete = 0, + int DailyPetEnchantMaxCount = 0, + int MouseCursorHideTime = 0, + int EnchantTransformScriptID = 0, + float AutoHideGroupAlpha = 0f, + int AutoHideGroupHitVisibleTick = 0, + int UgcShopCharRotateStartDegreeY = 0, + int UgcShopCharRotateEndDegreeY = 0, + int TreeWateringEmotion = 0, + string ShopProbInfoUrl = "", + int AdventureLevelLimit = 0, + int AdventureLevelLvUpExp = 0, + int AdventureLevelMaxExp = 0, + float AdventureLevelFactor = 0f, + int AdventureExpFactorElite = 0, + int AdventureExpFactorBoss = 0, + int AdventureLevelStartLevel = 0, + int AdventureLevelLvUpRewardItem = 0, + int NameColorDeadDuration = 0, + int ConstructExpMaxCount = 0, + float MesoRevivalFeeReduceLimit = 0f, + float IngredientFeeReduceLimit = 0f, + int StatPointLimit_str = 0, + int StatPointLimit_dex = 0, + int StatPointLimit_int = 0, + int StatPointLimit_luk = 0, + int StatPointLimit_hp = 0, + int StatPointLimit_cap = 0, + float GamePadRumbleMultiple = 0f, + int WorldChampionRewardDays = 0, + int NurturingEatMaxCount = 0, + int NurturingPlayMaxCount = 0, + string NurturingQuestTag = "", + int NurturingDuration = 0, + int NurturingInteractionDistance = 0, + int NurturingEatGrowth = 0, + int NurturingPlayGrowth = 0, + int NurturingPlayMailId = 0, + int NurturingPlayMaxGrowth = 0, + int NurturingHungryTime = 0, + int SkillPointLimitLevel1 = 0, + int SkillPointLimitLevel2 = 0, + int SellPriceNormalMax = 0, + int SellPriceRareMax = 0, + int SellPriceEliteMax = 0, + int SellPriceExcellentMax = 0, + int SellPriceLegendaryMax = 0, + int SellPriceArtifactMax = 0, + string RegionServerUrl_de = "", + string RegionServerUrl_en = "", + string RegionServerUrl_bpo = "", + int TooltipLabelMaxWidth = 0, + int ClubNameLengthMin = 0, + int ClubNameLengthMax = 0, + int UgcNameLengthMin = 0, + int UgcNameLengthMax = 0, + int UgcTagLengthMax = 0, + int ChangeJobLevel = 0, + int LapenShardOpenQuestID = 0, + int MaidNameLengthMin = 0, + int MaidNameLengthMax = 0, + int MaidDescLengthMin = 0, + int MaidDescLengthMax = 0, + int GamePadStickMoveValue = 0, + int HighlightMenuUsingLevel = 0, + int PartyVoteReadyDurationSeconds = 0, + int PartyVoteReadyTagExpireSeconds = 0, + int ShieldBarOffsetY = 0, + int MouseInteractLimitDistance = 0, + int AutoInstallEquipmentMinLevel = 0, + int AutoInstallEquipmentMaxLevel = 0, + string PartySearchRegisterComboValues = "", + int FieldWarInstanceEnterableDurationSeconds = 0, + int FieldWarRequirePlayerCount = 0, + int FieldWarRequireAchieveID = 0, + int FieldWarRequireLevel = 0, + int StatScaleMarkingAdditionalEffect = 0, + string DungeonRewardFailEmotions = "", + int SummonPetSkillID = 0, + int UGCMapSetItemEffectCountLimit = 0, + int AdventureLevelMissionResetWeekday = 0, + int ItemBoxMultiOpenMaxCount = 0, + int ItemBoxMultiOpenLimitCount = 0, + int BuffBalloonDistance = 0, + int PaybackStartDate = 0, + int PaybackSettleMinutes = 0, + int PaybackMarketProductSnList = 0, + int PaybackMailId = 0, + int PaybackMailPeriodDay = 0, + int PaybackMaxRewardMerat = 0, + string PaybackGuideUrl = "", + DateTime PaybackEndDate = default, + int WeddingProposeItemID = 0, + int WeddingInvitationMaxCount = 0, + int WeddingProposeCooltime = 0, + int WeddingDivorceFieldID = 0, + int WeddingInvitationMeso = 0, + int WeddingDivorceMeso = 0, + int WeddingCoolingOffDay = 0, + int WeddingPromiseLimitDay = 0, + int WeddingHallModifyLimitHour = 0, + int WeddingDivorceRequireMarriageDay = 0, + int AdventureProtectRequireLevel = 0, + int AdventureProtectRequireQuest = 0, + int AdventureProtectCharCreateTime = 0, + int PvpOnePunchReward1Count = 0, + int PvpOnePunchReward2Count = 0, + int PvpOnePunchReward3Count = 0, + int PvpOnePunchReward4Count = 0, + int PvpOnePunchReward5Count = 0, + int PvpOnePunchReward6Count = 0, + int PvpOnePunchReward7Count = 0, + int PvpOnePunchReward8Count = 0, + int PvpOnePunchReward9Count = 0, + int PvpOnePunchReward10Count = 0, + int PvpOnePunchRewardItem = 0, + int PvpOnePunchScoreNpcKill = 0, + int SpecialHairShopID = 0, + string GemStoneProbList = "", + string SkinGemStoneProbList = "", + string PersonalInfoAgreementURL = "", + float BothHandLowDamageRatio = 0f, + float BothHandWeaponDamagePenaltyDiv = 0f, + float BothHandGearScorePenaltyDiv = 0f, + string TencentUserConsultationWebPage = "", + string TencentPricavyGuideWebPage = "", + string TencentThirdPartyInformationSharingListWebPage = "", + int PvpOnePunchUserOpenRewardItem = 0, + string TencentCharacterCreateShutdownLeft = "", + string TencentCharacterCreateShutdownRight = "", + int LeadSkillMaxSlot = 0, + int NextStateTriggerDefaultTick = 0 +); diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs index d2ba0c346..b1c20182b 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs @@ -33,7 +33,8 @@ protected override void TaskResumed() { return; } - const float maxDistance = Constant.TalkableDistance * Constant.TalkableDistance; + float maxDistance = player.Session.ServerTableMetadata.ConstantsTable.Constants.TalkableDistance * + player.Session.ServerTableMetadata.ConstantsTable.Constants.TalkableDistance; // find nearest npc FieldNpc? closestNpc = player.Field.Npcs.Values From e11eec41b8b95a51e6eb1ab76581e8911cc2a66f Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Mon, 23 Feb 2026 16:45:35 -0600 Subject: [PATCH 03/26] Continue switching hard coded constants to server constants parsed during file ingest WIP --- .../Mapper/ServerTableMapper.cs | 6 ++--- Maple2.Model/Game/Cube/Nurturing.cs | 10 +++---- Maple2.Model/Game/Mail.cs | 5 ++-- Maple2.Model/Game/User/StatAttributes.cs | 26 ++++++++++--------- .../Metadata/ServerTable/ConstantsTable.cs | 6 ++--- Maple2.Server.Game/Manager/ConfigManager.cs | 19 +++++++++++--- .../MovementState.CleanupTask.cs | 4 +-- .../PacketHandlers/FunctionCubeHandler.cs | 9 ++++--- 8 files changed, 48 insertions(+), 37 deletions(-) diff --git a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs index d9af81fd0..a90687aa7 100644 --- a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs +++ b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs @@ -2114,15 +2114,15 @@ void AddSpecial(Dictionary values, Dictionary= NurturingMetadata.RequiredGrowth.Last().Exp) { return; } - Exp += Constant.NurturingEatGrowth; + Exp += nurturingEatGrowth; if (Exp >= NurturingMetadata.RequiredGrowth.First(x => x.Stage == Stage).Exp) { Stage++; } LastFeedTime = DateTimeOffset.Now; } - public bool Play(long accountId) { - if (PlayedBy.Count >= Constant.NurturingPlayMaxCount) { + public bool Play(long accountId, int nurturingEatGrowth, int nurturingPlayMaxCount) { + if (PlayedBy.Count >= nurturingPlayMaxCount) { return false; } @@ -70,7 +70,7 @@ public bool Play(long accountId) { } PlayedBy.Add(accountId); - Feed(); + Feed(nurturingEatGrowth); return true; } diff --git a/Maple2.Model/Game/Mail.cs b/Maple2.Model/Game/Mail.cs index e97dc99a8..29b3385af 100644 --- a/Maple2.Model/Game/Mail.cs +++ b/Maple2.Model/Game/Mail.cs @@ -1,6 +1,5 @@ using System.Text; using Maple2.Model.Enum; -using Maple2.Model.Metadata; using Maple2.PacketLib.Tools; using Maple2.Tools; using Maple2.Tools.Extensions; @@ -35,11 +34,11 @@ public class Mail : IByteSerializable { // More than 1 item may not display properly public readonly IList Items; - public Mail() { + public Mail(int mailExpiryDays) { TitleArgs = new List<(string Key, string Value)>(); ContentArgs = new List<(string Key, string Value)>(); Items = new List(); - ExpiryTime = DateTimeOffset.UtcNow.AddDays(Constant.MailExpiryDays).ToUnixTimeSeconds(); + ExpiryTime = DateTimeOffset.UtcNow.AddDays(mailExpiryDays).ToUnixTimeSeconds(); } public void Update(Mail other) { diff --git a/Maple2.Model/Game/User/StatAttributes.cs b/Maple2.Model/Game/User/StatAttributes.cs index 28cf8c61f..9c432db3a 100644 --- a/Maple2.Model/Game/User/StatAttributes.cs +++ b/Maple2.Model/Game/User/StatAttributes.cs @@ -13,9 +13,9 @@ public class StatAttributes : IByteSerializable { public int TotalPoints => Sources.Count; public int UsedPoints => Allocation.Count; - public StatAttributes() { + public StatAttributes(IDictionary statLimits) { Sources = new PointSources(); - Allocation = new PointAllocation(); + Allocation = new PointAllocation(statLimits); } public void WriteTo(IByteWriter writer) { @@ -52,6 +52,7 @@ public void WriteTo(IByteWriter writer) { public class PointAllocation : IByteSerializable { private readonly Dictionary points; + private readonly IDictionary statLimits; public BasicAttribute[] Attributes => points.Keys.ToArray(); public int Count => points.Values.Sum(); @@ -59,7 +60,7 @@ public class PointAllocation : IByteSerializable { public int this[BasicAttribute type] { get => points.GetValueOrDefault(type); set { - if (value < 0 || value > StatLimit(type)) { + if (value < 0 || value > StatLimit(type, statLimits)) { return; } if (value == 0) { @@ -71,19 +72,20 @@ public int this[BasicAttribute type] { } } - public PointAllocation() { + public PointAllocation(IDictionary statLimits) { points = new Dictionary(); + this.statLimits = statLimits; } - public static int StatLimit(BasicAttribute type) { + public static int StatLimit(BasicAttribute type, IDictionary statLimits) { return type switch { - BasicAttribute.Strength => Constant.StatPointLimit_str, - BasicAttribute.Dexterity => Constant.StatPointLimit_dex, - BasicAttribute.Intelligence => Constant.StatPointLimit_int, - BasicAttribute.Luck => Constant.StatPointLimit_luk, - BasicAttribute.Health => Constant.StatPointLimit_hp, - BasicAttribute.CriticalRate => Constant.StatPointLimit_cap, - _ => 0, + BasicAttribute.Strength => statLimits.GetValueOrDefault("StatPointLimit_str"), + BasicAttribute.Dexterity => statLimits.GetValueOrDefault("StatPointLimit_dex"), + BasicAttribute.Intelligence => statLimits.GetValueOrDefault("StatPointLimit_int"), + BasicAttribute.Luck => statLimits.GetValueOrDefault("StatPointLimit_luk"), + BasicAttribute.Health => statLimits.GetValueOrDefault("StatPointLimit_hp"), + BasicAttribute.CriticalRate => statLimits.GetValueOrDefault("StatPointLimit_cap"), + _ => 0 }; } diff --git a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs index 1f056e34c..3ee89be95 100644 --- a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs +++ b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs @@ -1,8 +1,6 @@ namespace Maple2.Model.Metadata; -public record ConstantsTable(Constants Constants) : ServerTable; - -public record Constants( +public record ConstantsTable( float NPCColorScale = 0f, float NPCDuration = 0f, float PCColorScale = 0f, @@ -866,4 +864,4 @@ public record Constants( string TencentCharacterCreateShutdownRight = "", int LeadSkillMaxSlot = 0, int NextStateTriggerDefaultTick = 0 -); +) : ServerTable; diff --git a/Maple2.Server.Game/Manager/ConfigManager.cs b/Maple2.Server.Game/Manager/ConfigManager.cs index 84a83e35b..bfe26ea37 100644 --- a/Maple2.Server.Game/Manager/ConfigManager.cs +++ b/Maple2.Server.Game/Manager/ConfigManager.cs @@ -27,6 +27,8 @@ public class ConfigManager { private readonly IList favoriteDesigners; private readonly IDictionary lapenshards; private readonly IDictionary skillCooldowns; + private readonly IDictionary statLimits; + public long DeathPenaltyEndTick { get => session.Player.Value.Character.DeathTick; private set => session.Player.Value.Character.DeathTick = value; @@ -94,7 +96,16 @@ public ConfigManager(GameStorage.Request db, GameSession session) { skillPoints = load.SkillPoint ?? new SkillPoint(); ExplorationProgress = load.ExplorationProgress; - statAttributes = new StatAttributes(); + statLimits = new Dictionary() { + { "StatPointLimit_str", session.ServerTableMetadata.ConstantsTable.StatPointLimit_str }, + { "StatPointLimit_dex", session.ServerTableMetadata.ConstantsTable.StatPointLimit_dex }, + { "StatPointLimit_int", session.ServerTableMetadata.ConstantsTable.StatPointLimit_int }, + { "StatPointLimit_luk", session.ServerTableMetadata.ConstantsTable.StatPointLimit_luk }, + { "StatPointLimit_hp", session.ServerTableMetadata.ConstantsTable.StatPointLimit_hp }, + { "StatPointLimit_cap", session.ServerTableMetadata.ConstantsTable.StatPointLimit_cap } + }; + + statAttributes = new StatAttributes(statLimits); if (load.StatPoints != null) { foreach ((AttributePointSource source, int amount) in load.StatPoints) { if (source == AttributePointSource.Prestige) { @@ -325,7 +336,7 @@ public void LoadRevival() { /// 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) { + if (session.Player.Value.Character.Level < session.ServerTableMetadata.ConstantsTable.UserRevivalPaneltyMinLevel) { return; } @@ -422,7 +433,7 @@ public bool TryGetWardrobe(int index, [NotNullWhen(true)] out Wardrobe? wardrobe #region StatPoints public void AllocateStatPoint(BasicAttribute type) { // Invalid stat type. - if (StatAttributes.PointAllocation.StatLimit(type) <= 0) { + if (StatAttributes.PointAllocation.StatLimit(type, statLimits) <= 0) { return; } @@ -432,7 +443,7 @@ public void AllocateStatPoint(BasicAttribute type) { } // Reached limit for allocation. - if (session.Config.statAttributes.Allocation[type] >= StatAttributes.PointAllocation.StatLimit(type)) { + if (session.Config.statAttributes.Allocation[type] >= StatAttributes.PointAllocation.StatLimit(type, statLimits)) { session.Send(NoticePacket.Message("s_char_info_limit_stat_point")); return; } diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs index b1c20182b..7ef651123 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs @@ -33,8 +33,8 @@ protected override void TaskResumed() { return; } - float maxDistance = player.Session.ServerTableMetadata.ConstantsTable.Constants.TalkableDistance * - player.Session.ServerTableMetadata.ConstantsTable.Constants.TalkableDistance; + float maxDistance = player.Session.ServerTableMetadata.ConstantsTable.TalkableDistance * + player.Session.ServerTableMetadata.ConstantsTable.TalkableDistance; // find nearest npc FieldNpc? closestNpc = player.Field.Npcs.Values diff --git a/Maple2.Server.Game/PacketHandlers/FunctionCubeHandler.cs b/Maple2.Server.Game/PacketHandlers/FunctionCubeHandler.cs index 0ea79dfe6..8bc5ae575 100644 --- a/Maple2.Server.Game/PacketHandlers/FunctionCubeHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/FunctionCubeHandler.cs @@ -17,6 +17,7 @@ public class FunctionCubeHandler : FieldPacketHandler { #region Autofac Autowired // ReSharper disable MemberCanBePrivate.Global public required FunctionCubeMetadataStorage FunctionCubeMetadataStorage { private get; init; } + public required ServerTableMetadataStorage ServerTableMetadataStorage { private get; init; } // ReSharper restore All #endregion @@ -148,7 +149,7 @@ private void HandleNurturing(GameSession session, FieldFunctionInteract fieldCub // drop the item session.Field.DropItem(fieldCube.Position, fieldCube.Rotation, rewardItem, owner: session.Player, characterId: session.CharacterId); - nurturing.Feed(); + nurturing.Feed(ServerTableMetadataStorage.ConstantsTable.NurturingEatGrowth); db.UpdateNurturing(session.AccountId, fieldCube.InteractCube); session.Field.Broadcast(FunctionCubePacket.UpdateFunctionCube(fieldCube.InteractCube)); @@ -163,12 +164,12 @@ private void HandlePlayNurturing(GameSession session, Plot plot, FieldFunctionIn return; } - if (db.CountNurturingForAccount(cube.InteractCube.Metadata.Id, session.AccountId) >= Constant.NurturingPlayMaxCount) { + if (db.CountNurturingForAccount(cube.InteractCube.Metadata.Id, session.AccountId) >= ServerTableMetadataStorage.ConstantsTable.NurturingEatMaxCount) { session.Send(NoticePacket.Message("You have already played with the maximum number of pets today. TODO: Find correct string id")); // TODO: Find correct string id return; } - if (!nurturing.Play(session.AccountId)) { + if (!nurturing.Play(session.AccountId, ServerTableMetadataStorage.ConstantsTable.NurturingEatGrowth, ServerTableMetadataStorage.ConstantsTable.NurturingEatMaxCount)) { return; } @@ -216,7 +217,7 @@ private void HandlePlayNurturing(GameSession session, Plot plot, FieldFunctionIn return null; } - var mail = new Mail { + var mail = new Mail(ServerTableMetadataStorage.ConstantsTable.MailExpiryDays) { ReceiverId = ownerId, Type = MailType.System, Content = contentId, From af10dca167ee6587e63c457b644b8e80f661200c Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Tue, 24 Feb 2026 16:59:01 -0600 Subject: [PATCH 04/26] Continue switching hard coded constants to server constants pared during file ingest #2 WIP --- Maple2.Database/Model/Mail.cs | 2 +- .../Storage/Game/GameStorage.Map.cs | 4 +- Maple2.File.Ingest/Mapper/NpcMapper.cs | 9 ++-- Maple2.Model/Game/Mail.cs | 8 ++++ Maple2.Model/Metadata/NpcMetadata.cs | 9 ++-- .../Metadata/ServerTable/ConstantsTable.cs | 30 ++++++------- Maple2.Server.Game/Commands/PlayerCommand.cs | 6 +-- .../Manager/BlackMarketManager.cs | 2 +- Maple2.Server.Game/Manager/BuddyManager.cs | 6 +-- Maple2.Server.Game/Manager/BuffManager.cs | 10 ++--- Maple2.Server.Game/Manager/CurrencyManager.cs | 42 +++++++++---------- .../Manager/ExperienceManager.cs | 19 +++++---- .../Field/FieldManager/FieldManager.State.cs | 10 +++-- .../Field/FieldManager/FieldManager.cs | 2 +- Maple2.Server.Game/Manager/FishingManager.cs | 4 +- .../Manager/ItemMergeManager.cs | 2 +- .../Manager/Items/InventoryManager.cs | 36 ++++++++-------- .../Manager/Items/StorageManager.cs | 6 +-- Maple2.Server.Game/Manager/SkillManager.cs | 4 +- Maple2.Server.Game/Manager/TradeManager.cs | 8 ++-- .../Model/Field/Actor/FieldPlayer.cs | 8 ++-- Maple2.Server.Game/Model/Field/Tombstone.cs | 3 +- .../PacketHandlers/BeautyHandler.cs | 12 +++--- .../PacketHandlers/ClubHandler.cs | 4 +- .../PacketHandlers/GuildHandler.cs | 8 ++-- .../PacketHandlers/HomeBankHandler.cs | 2 +- .../PacketHandlers/HomeDoctorHandler.cs | 2 +- .../PacketHandlers/ItemLockHandler.cs | 2 +- .../PacketHandlers/JobHandler.cs | 2 +- .../PacketHandlers/MeretMarketHandler.cs | 10 ++--- .../PacketHandlers/MesoMarketHandler.cs | 4 +- .../PacketHandlers/NpcTalkHandler.cs | 2 +- .../PacketHandlers/PartyHandler.cs | 6 ++- .../PacketHandlers/QuestHandler.cs | 4 +- .../PacketHandlers/SystemShopHandler.cs | 10 ++--- .../PacketHandlers/UserChatHandler.cs | 4 +- Maple2.Server.Game/Session/GameSession.cs | 4 +- Maple2.Server.Game/Util/ChatUtil.cs | 2 +- .../CharacterManagementHandler.cs | 13 +++--- Maple2.Server.World/WorldServer.cs | 4 +- 40 files changed, 172 insertions(+), 153 deletions(-) diff --git a/Maple2.Database/Model/Mail.cs b/Maple2.Database/Model/Mail.cs index cfca17f25..0927f3107 100644 --- a/Maple2.Database/Model/Mail.cs +++ b/Maple2.Database/Model/Mail.cs @@ -55,7 +55,7 @@ internal class Mail { [return: NotNullIfNotNull(nameof(other))] public static implicit operator Maple2.Model.Game.Mail?(Mail? other) { - return other == null ? null : new Maple2.Model.Game.Mail { + return other == null ? null : new Maple2.Model.Game.Mail() { ReceiverId = other.ReceiverId, Id = other.Id, SenderId = other.SenderId, diff --git a/Maple2.Database/Storage/Game/GameStorage.Map.cs b/Maple2.Database/Storage/Game/GameStorage.Map.cs index 0ccfb0e43..37cb8cdfc 100644 --- a/Maple2.Database/Storage/Game/GameStorage.Map.cs +++ b/Maple2.Database/Storage/Game/GameStorage.Map.cs @@ -111,11 +111,11 @@ public IList LoadCubesForOwner(long ownerId) { return Context.TrySaveChanges() ? ToPlotInfo(model) : null; } - public PlotInfo? GetSoonestPlotFromExpire() { + public PlotInfo? GetSoonestPlotFromExpire(TimeSpan ugcHomeSaleWaitingTime) { IQueryable maps = Context.UgcMap.Where(map => map.ExpiryTime > DateTimeOffset.MinValue && !map.Indoor); foreach (UgcMap map in maps) { if (map.OwnerId == 0) { - map.ExpiryTime = map.ExpiryTime.Add(Constant.UgcHomeSaleWaitingTime); + map.ExpiryTime = map.ExpiryTime.Add(ugcHomeSaleWaitingTime); } } UgcMap? model = maps.OrderBy(map => map.ExpiryTime).FirstOrDefault(); diff --git a/Maple2.File.Ingest/Mapper/NpcMapper.cs b/Maple2.File.Ingest/Mapper/NpcMapper.cs index 9b7d7014f..f0871acd6 100644 --- a/Maple2.File.Ingest/Mapper/NpcMapper.cs +++ b/Maple2.File.Ingest/Mapper/NpcMapper.cs @@ -34,10 +34,11 @@ protected override IEnumerable Map() { Avoid: data.distance.avoid, Sight: data.distance.sight, SightHeightUp: data.distance.sightHeightUP, - SightHeightDown: data.distance.sightHeightDown, - LastSightRadius: data.distance.customLastSightRadius == 0 ? Constant.NpcLastSightRadius : data.distance.customLastSightRadius, - LastSightHeightUp: data.distance.customLastSightHeightUp == 0 ? Constant.NpcLastSightHeightUp : data.distance.customLastSightHeightUp, - LastSightHeightDown: data.distance.customLastSightHeightDown == 0 ? Constant.NpcLastSightHeightDown : data.distance.customLastSightHeightDown + SightHeightDown: data.distance.sightHeightDown + // TODO: Need to move to runtime due to server table constants being used as default value + //LastSightRadius: data.distance.customLastSightRadius == 0 ? Constant.NpcLastSightRadius : data.distance.customLastSightRadius, + //LastSightHeightUp: data.distance.customLastSightHeightUp == 0 ? Constant.NpcLastSightHeightUp : data.distance.customLastSightHeightUp, + //LastSightHeightDown: data.distance.customLastSightHeightDown == 0 ? Constant.NpcLastSightHeightDown : data.distance.customLastSightHeightDown ), Skill: new NpcMetadataSkill( Entries: data.skill.ids.Select((skillId, i) => diff --git a/Maple2.Model/Game/Mail.cs b/Maple2.Model/Game/Mail.cs index 29b3385af..ec1faaee4 100644 --- a/Maple2.Model/Game/Mail.cs +++ b/Maple2.Model/Game/Mail.cs @@ -34,6 +34,14 @@ public class Mail : IByteSerializable { // More than 1 item may not display properly public readonly IList Items; + // Specifically for Mail object cloning (Mail.cs:57) + public Mail() { + TitleArgs = new List<(string Key, string Value)>(); + ContentArgs = new List<(string Key, string Value)>(); + Items = new List(); + // ExpiryTime will be overwritten, no need to set it here with a parameter passing server constant value. + } + public Mail(int mailExpiryDays) { TitleArgs = new List<(string Key, string Value)>(); ContentArgs = new List<(string Key, string Value)>(); diff --git a/Maple2.Model/Metadata/NpcMetadata.cs b/Maple2.Model/Metadata/NpcMetadata.cs index db0e23db2..86ecf2af5 100644 --- a/Maple2.Model/Metadata/NpcMetadata.cs +++ b/Maple2.Model/Metadata/NpcMetadata.cs @@ -34,10 +34,11 @@ public record NpcMetadataDistance( float Avoid, float Sight, float SightHeightUp, - float SightHeightDown, - float LastSightRadius, - float LastSightHeightUp, - float LastSightHeightDown); + float SightHeightDown + // TODO: Need to move to runtime due to server table constants being used as default value + /* float LastSightRadius, + float LastSightHeightUp, + float LastSightHeightDown*/); public record NpcMetadataSkill( NpcMetadataSkill.Entry[] Entries, diff --git a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs index 3ee89be95..edb544f78 100644 --- a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs +++ b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs @@ -671,21 +671,21 @@ public record ConstantsTable( int PetTrapDropVisibleDelay = 0, int PetMaxLevel = 0, string VisitorBookURL = "", - int bagSlotTabGameCount = 0, - int bagSlotTabSkinCount = 0, - int bagSlotTabSummonCount = 0, - int bagSlotTabMaterialCount = 0, - int bagSlotTabMasteryCount = 0, - int bagSlotTabLifeCount = 0, - int bagSlotTabQuestCount = 0, - int bagSlotTabGemCount = 0, - int bagSlotTabPetCount = 0, - int bagSlotTabActiveSkillCount = 0, - int bagSlotTabCoinCount = 0, - int bagSlotTabBadgeCount = 0, - int bagSlotTabMiscCount = 0, - int bagSlotTabLapenShardCount = 0, - int bagSlotTabPieceCount = 0, + short bagSlotTabGameCount = 0, + short bagSlotTabSkinCount = 0, + short bagSlotTabSummonCount = 0, + short bagSlotTabMaterialCount = 0, + short bagSlotTabMasteryCount = 0, + short bagSlotTabLifeCount = 0, + short bagSlotTabQuestCount = 0, + short bagSlotTabGemCount = 0, + short bagSlotTabPetCount = 0, + short bagSlotTabActiveSkillCount = 0, + short bagSlotTabCoinCount = 0, + short bagSlotTabBadgeCount = 0, + short bagSlotTabMiscCount = 0, + short bagSlotTabLapenShardCount = 0, + short bagSlotTabPieceCount = 0, int MasteryObjectInteractionDistance = 0, float GatheringObjectMarkOffsetX = 0f, float GatheringObjectMarkOffsetY = 0f, diff --git a/Maple2.Server.Game/Commands/PlayerCommand.cs b/Maple2.Server.Game/Commands/PlayerCommand.cs index c1207348a..ff4f419d6 100644 --- a/Maple2.Server.Game/Commands/PlayerCommand.cs +++ b/Maple2.Server.Game/Commands/PlayerCommand.cs @@ -191,8 +191,8 @@ public PrestigeCommand(GameSession session) : base("prestige", "Sets prestige le private void Handle(InvocationContext ctx, int level) { try { - if (level is < 1 or > Constant.AdventureLevelLimit) { - ctx.Console.Error.WriteLine($"Invalid level: {level}. Must be between 1 and {Constant.AdventureLevelLimit}."); + if (level < 1 || level > session.ServerTableMetadata.ConstantsTable.AdventureLevelLimit) { + ctx.Console.Error.WriteLine($"Invalid level: {level}. Must be between 1 and {session.ServerTableMetadata.ConstantsTable.AdventureLevelLimit}."); return; } @@ -328,7 +328,7 @@ private void JobAdvance(Job job) { session.Player.Buffs.Clear(); session.Player.Buffs.Initialize(); - session.Player.Buffs.LoadFieldBuffs(); + session.Player.Buffs.LoadFieldBuffs(session.ServerTableMetadata.ConstantsTable.shadowWorldBuffHpUp, session.ServerTableMetadata.ConstantsTable.shadowWorldBuffMoveProtect); session.Stats.Refresh(); session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo)); } diff --git a/Maple2.Server.Game/Manager/BlackMarketManager.cs b/Maple2.Server.Game/Manager/BlackMarketManager.cs index b95252a88..5086c8a40 100644 --- a/Maple2.Server.Game/Manager/BlackMarketManager.cs +++ b/Maple2.Server.Game/Manager/BlackMarketManager.cs @@ -63,7 +63,7 @@ public void Add(long itemUid, long price, int quantity) { AccountId = session.AccountId, CharacterId = session.CharacterId, Deposit = depositFee, - ExpiryTime = DateTime.Now.AddDays(Constant.BlackMarketSellEndDay).ToEpochSeconds(), + ExpiryTime = DateTime.Now.AddDays(session.ServerTableMetadata.ConstantsTable.BlackMarketSellEndDay).ToEpochSeconds(), Price = price, Quantity = quantity, }; diff --git a/Maple2.Server.Game/Manager/BuddyManager.cs b/Maple2.Server.Game/Manager/BuddyManager.cs index c4787c0db..707d7076e 100644 --- a/Maple2.Server.Game/Manager/BuddyManager.cs +++ b/Maple2.Server.Game/Manager/BuddyManager.cs @@ -87,7 +87,7 @@ public void SendInvite(string name, string message) { session.Send(BuddyPacket.Invite(error: s_buddy_err_my_id_ex)); return; } - if (buddies.Count >= Constant.MaxBuddyCount) { + if (buddies.Count >= session.ServerTableMetadata.ConstantsTable.MaxBuddyCount) { session.Send(BuddyPacket.Invite(error: s_buddy_err_max_buddy)); return; } @@ -113,7 +113,7 @@ public void SendInvite(string name, string message) { try { db.BeginTransaction(); - if (db.CountBuddy(receiverId) >= Constant.MaxBuddyCount) { + if (db.CountBuddy(receiverId) >= session.ServerTableMetadata.ConstantsTable.MaxBuddyCount) { session.Send(BuddyPacket.Invite(name: name, error: s_buddy_err_target_full)); return; } @@ -262,7 +262,7 @@ public void SendBlock(long entryId, string name, string message) { session.Send(BuddyPacket.Block(error: s_buddy_err_unknown)); return; } - if (blocked.Count >= Constant.MaxBlockCount) { + if (blocked.Count >= session.ServerTableMetadata.ConstantsTable.MaxBlockCount) { session.Send(BuddyPacket.Block(name: name, error: s_buddy_err_max_block)); return; } diff --git a/Maple2.Server.Game/Manager/BuffManager.cs b/Maple2.Server.Game/Manager/BuffManager.cs index f9e378c22..3809822b4 100644 --- a/Maple2.Server.Game/Manager/BuffManager.cs +++ b/Maple2.Server.Game/Manager/BuffManager.cs @@ -60,11 +60,11 @@ public void Clear() { } } - public void LoadFieldBuffs() { + public void LoadFieldBuffs(int shadowWorldBuffHpUp, int shadowWorldBuffMoveProtect) { // Lapenshards // Game Events // Prestige - EnterField(); + EnterField(shadowWorldBuffHpUp, shadowWorldBuffMoveProtect); if (Actor is FieldPlayer player) { player.Session.Config.RefreshPremiumClubBuffs(); } @@ -438,7 +438,7 @@ public void LeaveField() { Remove(buffsToRemove.ToArray()); } - private void EnterField() { + private void EnterField(int shadowWorldBuffHpUp, int shadowWorldBuffMoveProtect) { foreach (MapEntranceBuff buff in Actor.Field.Metadata.EntranceBuffs) { AddBuff(Actor, Actor, buff.Id, buff.Level, Actor.Field.FieldTick); } @@ -458,8 +458,8 @@ private void EnterField() { } if (Actor.Field.Metadata.Property.Region == MapRegion.ShadowWorld) { - AddBuff(Actor, Actor, Constant.shadowWorldBuffHpUp, 1, Actor.Field.FieldTick); - AddBuff(Actor, Actor, Constant.shadowWorldBuffMoveProtect, 1, Actor.Field.FieldTick); + AddBuff(Actor, Actor, shadowWorldBuffHpUp, 1, Actor.Field.FieldTick); + AddBuff(Actor, Actor, shadowWorldBuffMoveProtect, 1, Actor.Field.FieldTick); } } diff --git a/Maple2.Server.Game/Manager/CurrencyManager.cs b/Maple2.Server.Game/Manager/CurrencyManager.cs index 09bf934c8..f056a4229 100644 --- a/Maple2.Server.Game/Manager/CurrencyManager.cs +++ b/Maple2.Server.Game/Manager/CurrencyManager.cs @@ -98,57 +98,57 @@ public long this[CurrencyType type] { long overflow; switch (type) { case CurrencyType.ValorToken: - delta = Math.Min(value, Constant.HonorTokenMax) - Currency.ValorToken; - overflow = Math.Max(0, value - Constant.HonorTokenMax); - Currency.ValorToken = Math.Min(value, Constant.HonorTokenMax); + delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.HonorTokenMax) - Currency.ValorToken; + overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.HonorTokenMax); + Currency.ValorToken = Math.Min(value, session.ServerTableMetadata.ConstantsTable.HonorTokenMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_honor_token, delta); } break; case CurrencyType.Treva: - delta = Math.Min(value, Constant.KarmaTokenMax) - Currency.Treva; - overflow = Math.Max(0, value - Constant.KarmaTokenMax); - Currency.Treva = Math.Min(value, Constant.KarmaTokenMax); + delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.KarmaTokenMax) - Currency.Treva; + overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.KarmaTokenMax); + Currency.Treva = Math.Min(value, session.ServerTableMetadata.ConstantsTable.KarmaTokenMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_karma_token, delta); } break; case CurrencyType.Rue: - delta = Math.Min(value, Constant.LuTokenMax) - Currency.Rue; - overflow = Math.Max(0, value - Constant.LuTokenMax); - Currency.Rue = Math.Min(value, Constant.LuTokenMax); + delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.LuTokenMax) - Currency.Rue; + overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.LuTokenMax); + Currency.Rue = Math.Min(value, session.ServerTableMetadata.ConstantsTable.LuTokenMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_lu_token, delta); } break; case CurrencyType.HaviFruit: - delta = Math.Min(value, Constant.HaviTokenMax) - Currency.HaviFruit; - overflow = Math.Max(0, value - Constant.HaviTokenMax); - Currency.HaviFruit = Math.Min(value, Constant.HaviTokenMax); + delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.HabiTokenMax) - Currency.HaviFruit; + overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.HabiTokenMax); + Currency.HaviFruit = Math.Min(value, session.ServerTableMetadata.ConstantsTable.HabiTokenMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_habi_token, delta); } break; case CurrencyType.ReverseCoin: - delta = Math.Min(value, Constant.ReverseCoinMax) - Currency.ReverseCoin; - overflow = Math.Max(0, value - Constant.ReverseCoinMax); - Currency.ReverseCoin = Math.Min(value, Constant.ReverseCoinMax); + delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.ReverseCoinMax) - Currency.ReverseCoin; + overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.ReverseCoinMax); + Currency.ReverseCoin = Math.Min(value, session.ServerTableMetadata.ConstantsTable.ReverseCoinMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_reverse_coin, delta); } break; case CurrencyType.MentorToken: - delta = Math.Min(value, Constant.MentorTokenMax) - Currency.MentorToken; - overflow = Math.Max(0, value - Constant.MentorTokenMax); - Currency.MentorToken = Math.Min(value, Constant.MentorTokenMax); + delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.MentorTokenMax) - Currency.MentorToken; + overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.MentorTokenMax); + Currency.MentorToken = Math.Min(value, session.ServerTableMetadata.ConstantsTable.MentorTokenMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_mentor_token, delta); } break; case CurrencyType.MenteeToken: - delta = Math.Min(value, Constant.MenteeTokenMax) - Currency.MenteeToken; - overflow = Math.Max(0, value - Constant.MenteeTokenMax); - Currency.MenteeToken = Math.Min(value, Constant.MenteeTokenMax); + delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.MenteeTokenMax) - Currency.MenteeToken; + overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.MenteeTokenMax); + Currency.MenteeToken = Math.Min(value, session.ServerTableMetadata.ConstantsTable.MenteeTokenMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_mentee_token, delta); } diff --git a/Maple2.Server.Game/Manager/ExperienceManager.cs b/Maple2.Server.Game/Manager/ExperienceManager.cs index 2300b62ab..e733c0dac 100644 --- a/Maple2.Server.Game/Manager/ExperienceManager.cs +++ b/Maple2.Server.Game/Manager/ExperienceManager.cs @@ -89,7 +89,7 @@ public void OnKill(IActor npc) { } private long GetRestExp(long expGained) { - long addedRestExp = Math.Min(RestExp, (long) (expGained * (Constant.RestExpAcquireRate / 10000.0f))); // convert int to a percentage + long addedRestExp = Math.Min(RestExp, (long) (expGained * (session.ServerTableMetadata.ConstantsTable.RestExpAcquireRate / 10000.0f))); // convert int to a percentage RestExp = Math.Max(0, RestExp - addedRestExp); Exp += expGained; return addedRestExp; @@ -203,7 +203,7 @@ public bool LevelUp() { } private void AddPrestigeExp(ExpType expType) { - if (Level < Constant.AdventureLevelStartLevel) { + if (Level < session.ServerTableMetadata.ConstantsTable.AdventureLevelStartLevel) { return; } @@ -211,19 +211,20 @@ private void AddPrestigeExp(ExpType expType) { return; } - if (PrestigeCurrentExp - PrestigeExp + (PrestigeLevelsGained * Constant.AdventureLevelLvUpExp) >= Constant.AdventureLevelLvUpExp) { - amount = (long) (amount * Constant.AdventureLevelFactor); + if (PrestigeCurrentExp - PrestigeExp + (PrestigeLevelsGained * session.ServerTableMetadata.ConstantsTable.AdventureLevelLvUpExp) >= + session.ServerTableMetadata.ConstantsTable.AdventureLevelLvUpExp) { + amount = (long) (amount * session.ServerTableMetadata.ConstantsTable.AdventureLevelFactor); } PrestigeCurrentExp = Math.Min(amount + PrestigeCurrentExp, long.MaxValue); int startLevel = PrestigeLevel; - for (int level = startLevel; level < Constant.AdventureLevelLimit; level++) { - if (Constant.AdventureLevelLvUpExp > PrestigeCurrentExp) { + for (int level = startLevel; level < session.ServerTableMetadata.ConstantsTable.AdventureLevelLimit; level++) { + if (session.ServerTableMetadata.ConstantsTable.AdventureLevelLvUpExp > PrestigeCurrentExp) { break; } - PrestigeCurrentExp -= Constant.AdventureLevelLvUpExp; + PrestigeCurrentExp -= session.ServerTableMetadata.ConstantsTable.AdventureLevelLvUpExp; PrestigeLevel++; } session.Send(PrestigePacket.AddExp(PrestigeCurrentExp, amount)); @@ -233,7 +234,7 @@ private void AddPrestigeExp(ExpType expType) { } public void PrestigeLevelUp(int amount = 1) { - PrestigeLevel = Math.Clamp(PrestigeLevel + amount, amount, Constant.AdventureLevelLimit); + PrestigeLevel = Math.Clamp(PrestigeLevel + amount, amount, session.ServerTableMetadata.ConstantsTable.AdventureLevelLimit); PrestigeLevelsGained += amount; session.ConditionUpdate(ConditionType.adventure_level, counter: amount); session.ConditionUpdate(ConditionType.adventure_level_up, counter: amount); @@ -242,7 +243,7 @@ public void PrestigeLevelUp(int amount = 1) { } for (int i = 0; i < amount; i++) { - Item? item = session.Field?.ItemDrop.CreateItem(Constant.AdventureLevelLvUpRewardItem); + Item? item = session.Field?.ItemDrop.CreateItem(session.ServerTableMetadata.ConstantsTable.AdventureLevelLvUpRewardItem); if (item == null) { break; } diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index 3da48aa88..59d0d66e9 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -204,15 +204,17 @@ public FieldPortal SpawnPortal(Portal portal, int roomId, Vector3 position = def } public FieldPortal SpawnPortal(QuestSummonPortal metadata, FieldNpc npc, FieldPlayer owner) { - var portal = new Portal(NextLocalId(), metadata.MapId, metadata.PortalId, PortalType.Quest, PortalActionType.Interact, npc.Position.Offset(Constant.QuestPortalDistanceFromNpc, npc.Rotation), npc.Rotation, - new Vector3(Constant.QuestPortalDistanceFromNpc, Constant.QuestPortalDimensionY, Constant.QuestPortalDimensionZ), Constant.QuestPortalDistanceFromNpc, + var portal = new Portal(NextLocalId(), metadata.MapId, metadata.PortalId, PortalType.Quest, PortalActionType.Interact, + npc.Position.Offset(owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalDistanceFromNpc, npc.Rotation), npc.Rotation, + new Vector3(owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalDistanceFromNpc, owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalDimensionY, + owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalDimensionZ), owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalDistanceFromNpc, 0, true, false, true); var fieldPortal = new FieldQuestPortal(owner, this, NextLocalId(), portal) { Position = portal.Position, Rotation = portal.Rotation, - EndTick = (FieldTick + (long) TimeSpan.FromSeconds(Constant.QuestPortalKeepTime).TotalMilliseconds).Truncate32(), + EndTick = (FieldTick + (long) TimeSpan.FromSeconds(owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalKeepTime).TotalMilliseconds).Truncate32(), StartTick = FieldTickInt, - Model = Constant.QuestPortalKeepNif, + Model = owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalKeepNif, }; fieldPortals[fieldPortal.ObjectId] = fieldPortal; diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs index ea1983ab1..3554058de 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs @@ -355,7 +355,7 @@ public void EnsurePlayerPosition(FieldPlayer player) { return; } - player.FallDamage(Constant.FallBoundingAddedDistance); + player.FallDamage(player.Session.ServerTableMetadata.ConstantsTable.FallBoundingAddedDistance); player.MoveToPosition(player.LastGroundPosition.Align() + new Vector3(0, 0, 150f), default); } diff --git a/Maple2.Server.Game/Manager/FishingManager.cs b/Maple2.Server.Game/Manager/FishingManager.cs index aebd61425..aeffa3c3e 100644 --- a/Maple2.Server.Game/Manager/FishingManager.cs +++ b/Maple2.Server.Game/Manager/FishingManager.cs @@ -239,12 +239,12 @@ public FishingError Start(Vector3 position) { selectedFish = fishes.Get(); - int fishingTick = Constant.FisherBoreDuration; + int fishingTick = session.ServerTableMetadata.ConstantsTable.FisherBoreDuration; bool hasAutoFish = session.Player.Buffs.HasBuff(BuffEventType.AutoFish); // Fishing Success if (Random.Shared.Next(0, 10000) < selectedFish.BaitProbability) { - if (!hasAutoFish && Random.Shared.Next(0, 10000) < Constant.FishFightingProp) { + if (!hasAutoFish && Random.Shared.Next(0, 10000) < session.ServerTableMetadata.ConstantsTable.fishFightingProp) { fishFightGame = true; } diff --git a/Maple2.Server.Game/Manager/ItemMergeManager.cs b/Maple2.Server.Game/Manager/ItemMergeManager.cs index 9d2ba918f..b9c30de3e 100644 --- a/Maple2.Server.Game/Manager/ItemMergeManager.cs +++ b/Maple2.Server.Game/Manager/ItemMergeManager.cs @@ -86,7 +86,7 @@ public void SelectCrystal(long itemUid, long crystalUid) { session.Send(ItemMergePacket.Select(mergeSlot, ItemMerge.CostMultiplier(upgradeItem.Rarity))); if (!session.ScriptMetadata.TryGet(Constant.EmpowermentNpc, out ScriptMetadata? script) || - !script.States.TryGetValue(Constant.MergeSmithScriptID, out ScriptState? state)) { + !script.States.TryGetValue(session.ServerTableMetadata.ConstantsTable.MergeSmithScriptID, out ScriptState? state)) { return; } diff --git a/Maple2.Server.Game/Manager/Items/InventoryManager.cs b/Maple2.Server.Game/Manager/Items/InventoryManager.cs index 83a1c09a8..78fc94e3b 100644 --- a/Maple2.Server.Game/Manager/Items/InventoryManager.cs +++ b/Maple2.Server.Game/Manager/Items/InventoryManager.cs @@ -44,23 +44,23 @@ public InventoryManager(GameStorage.Request db, GameSession session) { } } - private static short BaseSize(InventoryType type) { + private short BaseSize(InventoryType type) { return type switch { - InventoryType.Gear => Constant.BagSlotTabGameCount, - InventoryType.Outfit => Constant.BagSlotTabSkinCount, - InventoryType.Mount => Constant.BagSlotTabSummonCount, - InventoryType.Catalyst => Constant.BagSlotTabMaterialCount, - InventoryType.FishingMusic => Constant.BagSlotTabLifeCount, - InventoryType.Quest => Constant.BagSlotTabQuestCount, - InventoryType.Gemstone => Constant.BagSlotTabGemCount, - InventoryType.Misc => Constant.BagSlotTabMiscCount, - InventoryType.LifeSkill => Constant.BagSlotTabMasteryCount, - InventoryType.Pets => Constant.BagSlotTabPetCount, - InventoryType.Consumable => Constant.BagSlotTabActiveSkillCount, - InventoryType.Currency => Constant.BagSlotTabCoinCount, - InventoryType.Badge => Constant.BagSlotTabBadgeCount, - InventoryType.Lapenshard => Constant.BagSlotTabLapenshardCount, - InventoryType.Fragment => Constant.BagSlotTabPieceCount, + InventoryType.Gear => session.ServerTableMetadata.ConstantsTable.bagSlotTabGameCount, + InventoryType.Outfit => session.ServerTableMetadata.ConstantsTable.bagSlotTabSkinCount, + InventoryType.Mount => session.ServerTableMetadata.ConstantsTable.bagSlotTabSummonCount, + InventoryType.Catalyst => session.ServerTableMetadata.ConstantsTable.bagSlotTabMaterialCount, + InventoryType.FishingMusic => session.ServerTableMetadata.ConstantsTable.bagSlotTabLifeCount, + InventoryType.Quest => session.ServerTableMetadata.ConstantsTable.bagSlotTabQuestCount, + InventoryType.Gemstone => session.ServerTableMetadata.ConstantsTable.bagSlotTabGemCount, + InventoryType.Misc => session.ServerTableMetadata.ConstantsTable.bagSlotTabMiscCount, + InventoryType.LifeSkill => session.ServerTableMetadata.ConstantsTable.bagSlotTabMasteryCount, + InventoryType.Pets => session.ServerTableMetadata.ConstantsTable.bagSlotTabPetCount, + InventoryType.Consumable => session.ServerTableMetadata.ConstantsTable.bagSlotTabActiveSkillCount, + InventoryType.Currency => session.ServerTableMetadata.ConstantsTable.bagSlotTabCoinCount, + InventoryType.Badge => session.ServerTableMetadata.ConstantsTable.bagSlotTabBadgeCount, + InventoryType.Lapenshard => session.ServerTableMetadata.ConstantsTable.bagSlotTabLapenShardCount, + InventoryType.Fragment => session.ServerTableMetadata.ConstantsTable.bagSlotTabPieceCount, _ => throw new ArgumentOutOfRangeException($"Invalid InventoryType: {type}"), }; } @@ -557,7 +557,7 @@ public bool Expand(InventoryType type, int expandRowCount = Constant.InventoryEx return false; } - if (session.Currency.Meret < Constant.InventoryExpandPrice1Row) { + if (session.Currency.Meret < session.ServerTableMetadata.ConstantsTable.InventoryExpandPrice1Row) { session.Send(ItemInventoryPacket.Error(s_cannot_charge_merat)); return false; } @@ -566,7 +566,7 @@ public bool Expand(InventoryType type, int expandRowCount = Constant.InventoryEx return false; } - session.Currency.Meret -= Constant.InventoryExpandPrice1Row; + session.Currency.Meret -= session.ServerTableMetadata.ConstantsTable.InventoryExpandPrice1Row; if (session.Player.Value.Unlock.Expand.ContainsKey(type)) { session.Player.Value.Unlock.Expand[type] = newExpand; } else { diff --git a/Maple2.Server.Game/Manager/Items/StorageManager.cs b/Maple2.Server.Game/Manager/Items/StorageManager.cs index 6ee15adf5..c38cd9a27 100644 --- a/Maple2.Server.Game/Manager/Items/StorageManager.cs +++ b/Maple2.Server.Game/Manager/Items/StorageManager.cs @@ -189,11 +189,11 @@ public void WithdrawMesos(long amount) { public void Expand() { lock (session.Item) { short newSize = (short) (items.Size + Constant.InventoryExpandRowCount); - if (newSize > Constant.StoreExpandMaxSlotCount) { + if (newSize > session.ServerTableMetadata.ConstantsTable.StoreExpandMaxSlotCount) { session.Send(StorageInventoryPacket.Error(s_store_err_expand_max)); return; } - if (session.Currency.Meret < Constant.StoreExpandPrice1Row) { + if (session.Currency.Meret < session.ServerTableMetadata.ConstantsTable.StoreExpandPrice1Row) { session.Send(StorageInventoryPacket.Error(s_cannot_charge_merat)); return; } @@ -203,7 +203,7 @@ public void Expand() { return; } - session.Currency.Meret -= Constant.StoreExpandPrice1Row; + session.Currency.Meret -= session.ServerTableMetadata.ConstantsTable.StoreExpandPrice1Row; expand += Constant.InventoryExpandRowCount; Load(); diff --git a/Maple2.Server.Game/Manager/SkillManager.cs b/Maple2.Server.Game/Manager/SkillManager.cs index 45f5c5754..19a38c285 100644 --- a/Maple2.Server.Game/Manager/SkillManager.cs +++ b/Maple2.Server.Game/Manager/SkillManager.cs @@ -121,11 +121,11 @@ public bool ExpandSkillTabs() { if (SkillBook.MaxSkillTabs >= Constant.MaxSkillTabCount) { return false; } - if (session.Currency.Meret < Constant.SkillBookTreeAddTabFeeMeret) { + if (session.Currency.Meret < session.ServerTableMetadata.ConstantsTable.SkillBookTreeAddTabFeeMerat) { return false; } - session.Currency.Meret -= Constant.SkillBookTreeAddTabFeeMeret; + session.Currency.Meret -= session.ServerTableMetadata.ConstantsTable.SkillBookTreeAddTabFeeMerat; SkillBook.MaxSkillTabs++; session.Send(SkillBookPacket.Expand(SkillBook)); diff --git a/Maple2.Server.Game/Manager/TradeManager.cs b/Maple2.Server.Game/Manager/TradeManager.cs index c356246b8..67fd0f1d9 100644 --- a/Maple2.Server.Game/Manager/TradeManager.cs +++ b/Maple2.Server.Game/Manager/TradeManager.cs @@ -34,7 +34,7 @@ public TradeManager(GameSession sender, GameSession receiver) { // End the trade if not accepted before |TradeRequestDuration|. string receiverName = receiver.Player.Value.Character.Name; Task.Factory.StartNew(() => { - Thread.Sleep(TimeSpan.FromSeconds(Constant.TradeRequestDuration)); + Thread.Sleep(TimeSpan.FromSeconds(sender.ServerTableMetadata.ConstantsTable.TradeRequestDuration)); lock (mutex) { if (state is not (TradeState.Requested or TradeState.Acknowledged)) { return; @@ -168,7 +168,7 @@ public void SetMesos(GameSession caller, long amount) { return; } - if (amount > Constant.TradeMaxMeso) { + if (amount > caller.ServerTableMetadata.ConstantsTable.TradeMaxMeso) { caller.Send(TradePacket.Error(s_trade_error_invalid_meso)); return; } @@ -247,7 +247,7 @@ private void EndTrade(bool success) { } lock (sender.Session.Item) { - long fee = success ? (long) (Constant.TradeFeePercent / 100f * sender.Mesos) : 0; + long fee = success ? (long) (sender.Session.ServerTableMetadata.ConstantsTable.TradeFeePercent / 100f * sender.Mesos) : 0; sender.Session.Currency.Meso += sender.Mesos - fee; foreach (Item item in sender.Items) { if (item.Transfer?.Flag.HasFlag(TransferFlag.LimitTrade) == true) { @@ -260,7 +260,7 @@ private void EndTrade(bool success) { sender.Clear(); } lock (receiver.Session.Item) { - long fee = success ? (long) (Constant.TradeFeePercent / 100f * receiver.Mesos) : 0; + long fee = success ? (long) (receiver.Session.ServerTableMetadata.ConstantsTable.TradeFeePercent / 100f * receiver.Mesos) : 0; receiver.Session.Currency.Meso += receiver.Mesos - fee; foreach (Item item in receiver.Items) { if (item.Transfer?.Flag.HasFlag(TransferFlag.LimitTrade) == true) { diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs index ce3b45acf..4179e9aaf 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs @@ -168,7 +168,7 @@ public override void Update(long tickCount) { return; } - if (InBattle && tickCount - battleTick > Constant.UserBattleDurationTick) { + if (InBattle && tickCount - battleTick > Session.ServerTableMetadata.ConstantsTable.UserBattleDurationTick) { InBattle = false; } @@ -402,7 +402,7 @@ public bool Revive(bool instant = false) { // Apply death penalty if field requires it if (Field.Metadata.Property.DeathPenalty) { - Session.Config.UpdateDeathPenalty(Field.FieldTick + Constant.UserRevivalPaneltyTick); + Session.Config.UpdateDeathPenalty(Field.FieldTick + Session.ServerTableMetadata.ConstantsTable.UserRevivalPaneltyTick); } // Update revival condition @@ -474,7 +474,7 @@ public void ConsumeHp(int amount) { Stat stat = Stats.Values[BasicAttribute.Health]; stat.Add(-amount); if (!IsDead) { - lastRegenTime[BasicAttribute.Health] = Field.FieldTick + Constant.RecoveryHPWaitTick; + lastRegenTime[BasicAttribute.Health] = Field.FieldTick + Session.ServerTableMetadata.ConstantsTable.RecoveryHPWaitTick; } Session.Send(StatsPacket.Update(this, BasicAttribute.Health)); @@ -547,7 +547,7 @@ public void ConsumeStamina(int amount, bool noRegen = false) { Stats.Values[BasicAttribute.Stamina].Add(-amount); if (!IsDead) { - lastRegenTime[BasicAttribute.Stamina] = Field.FieldTick + Constant.RecoveryEPWaitTick; + lastRegenTime[BasicAttribute.Stamina] = Field.FieldTick + Session.ServerTableMetadata.ConstantsTable.RecoveryEPWaitTick; } Field.Broadcast(StatsPacket.Update(this, BasicAttribute.Stamina)); } diff --git a/Maple2.Server.Game/Model/Field/Tombstone.cs b/Maple2.Server.Game/Model/Field/Tombstone.cs index 4eb54f9b7..0d0b7a488 100644 --- a/Maple2.Server.Game/Model/Field/Tombstone.cs +++ b/Maple2.Server.Game/Model/Field/Tombstone.cs @@ -26,7 +26,8 @@ public byte HitsRemaining { public Tombstone(FieldPlayer owner, int totalDeaths) { Owner = owner; - TotalHitCount = (byte) Math.Min(totalDeaths * Constant.hitPerDeadCount, Constant.hitPerDeadCount * Constant.maxDeadCount); + TotalHitCount = (byte) Math.Min(totalDeaths * owner.Session.ServerTableMetadata.ConstantsTable.hitPerDeadCount, + owner.Session.ServerTableMetadata.ConstantsTable.hitPerDeadCount * owner.Session.ServerTableMetadata.ConstantsTable.maxDeadCount); hitsRemaining = TotalHitCount; } public void WriteTo(IByteWriter writer) { diff --git a/Maple2.Server.Game/PacketHandlers/BeautyHandler.cs b/Maple2.Server.Game/PacketHandlers/BeautyHandler.cs index dd3a7c5e2..99cfb24c7 100644 --- a/Maple2.Server.Game/PacketHandlers/BeautyHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/BeautyHandler.cs @@ -351,15 +351,15 @@ private void HandleRandomHair(GameSession session, IByteReader packet) { private void HandleWarp(GameSession session, IByteReader packet) { short type = packet.ReadShort(); int mapId = type switch { - 1 => Constant.BeautyHairShopGotoFieldID, - 3 => Constant.BeautyFaceShopGotoFieldID, - 5 => Constant.BeautyColorShopGotoFieldID, + 1 => session.ServerTableMetadata.ConstantsTable.BeautyHairShopGotoFieldID, + 3 => session.ServerTableMetadata.ConstantsTable.BeautyFaceShopGotoFieldID, + 5 => session.ServerTableMetadata.ConstantsTable.BeautyColorShopGotoFieldID, _ => 0, }; int portalId = type switch { - 1 => Constant.BeautyHairShopGotoPortalID, - 3 => Constant.BeautyFaceShopGotoPortalID, - 5 => Constant.BeautyColorShopGotoPortalID, + 1 => session.ServerTableMetadata.ConstantsTable.BeautyHairShopGotoPortalID, + 3 => session.ServerTableMetadata.ConstantsTable.BeautyFaceShopGotoPortalID, + 5 => session.ServerTableMetadata.ConstantsTable.BeautyColorShopGotoPortalID, _ => 0, }; diff --git a/Maple2.Server.Game/PacketHandlers/ClubHandler.cs b/Maple2.Server.Game/PacketHandlers/ClubHandler.cs index a513f2e05..293589ef0 100644 --- a/Maple2.Server.Game/PacketHandlers/ClubHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ClubHandler.cs @@ -71,7 +71,7 @@ private void HandleCreate(GameSession session, IByteReader packet) { session.Send(ClubPacket.Error(ClubError.s_club_err_name_value)); return; } - if (clubName.Length is < Constant.ClubNameLengthMin or > Constant.ClubNameLengthMax) { + if (clubName.Length < session.ServerTableMetadata.ConstantsTable.ClubNameLengthMin || clubName.Length > session.ServerTableMetadata.ConstantsTable.ClubNameLengthMax) { session.Send(ClubPacket.Error(ClubError.s_club_err_name_value)); return; } @@ -243,7 +243,7 @@ private void HandleRename(GameSession session, IByteReader packet) { session.Send(ClubPacket.Error(ClubError.s_club_err_name_value)); return; } - if (newName.Length is < Constant.ClubNameLengthMin or > Constant.ClubNameLengthMax) { + if (newName.Length < session.ServerTableMetadata.ConstantsTable.ClubNameLengthMin || newName.Length > session.ServerTableMetadata.ConstantsTable.ClubNameLengthMax) { session.Send(ClubPacket.Error(ClubError.s_club_err_name_value)); return; } diff --git a/Maple2.Server.Game/PacketHandlers/GuildHandler.cs b/Maple2.Server.Game/PacketHandlers/GuildHandler.cs index f54052620..8b4e124fd 100644 --- a/Maple2.Server.Game/PacketHandlers/GuildHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/GuildHandler.cs @@ -193,7 +193,7 @@ private void HandleCreate(GameSession session, IByteReader packet) { session.Send(GuildPacket.Error(GuildError.s_guild_err_name_value)); return; } - if (guildName.Length is < Constant.GuildNameLengthMin or > Constant.GuildNameLengthMax) { + if (guildName.Length < session.ServerTableMetadata.ConstantsTable.GuildNameLengthMin || guildName.Length > session.ServerTableMetadata.ConstantsTable.GuildNameLengthMax) { session.Send(GuildPacket.Error(GuildError.s_guild_err_name_value)); return; } @@ -208,11 +208,11 @@ private void HandleCreate(GameSession session, IByteReader packet) { return; } - if (session.Player.Value.Character.Level < Constant.GuildCreateMinLevel) { + if (session.Player.Value.Character.Level < session.ServerTableMetadata.ConstantsTable.GuildCreateMinLevel) { session.Send(GuildPacket.Error(GuildError.s_guild_err_not_enough_level)); return; } - if (session.Currency.CanAddMeso(-Constant.GuildCreatePrice) != -Constant.GuildCreatePrice) { + if (session.Currency.CanAddMeso(-session.ServerTableMetadata.ConstantsTable.GuildCreatePrice) != -session.ServerTableMetadata.ConstantsTable.GuildCreatePrice) { session.Send(GuildPacket.Error(GuildError.s_guild_err_no_money)); return; } @@ -236,7 +236,7 @@ private void HandleCreate(GameSession session, IByteReader packet) { } session.Guild.SetGuild(response.Guild); - session.Currency.Meso -= Constant.GuildCreatePrice; + session.Currency.Meso -= session.ServerTableMetadata.ConstantsTable.GuildCreatePrice; session.Guild.Load(); session.Send(GuildPacket.Created(guildName)); diff --git a/Maple2.Server.Game/PacketHandlers/HomeBankHandler.cs b/Maple2.Server.Game/PacketHandlers/HomeBankHandler.cs index a876688cc..5026761ba 100644 --- a/Maple2.Server.Game/PacketHandlers/HomeBankHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/HomeBankHandler.cs @@ -20,7 +20,7 @@ public override void Handle(GameSession session, IByteReader packet) { switch (command) { case Command.Home: long time = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - if (session.Player.Value.Character.StorageCooldown + Constant.HomeBankCallCooldown > time) { + if (session.Player.Value.Character.StorageCooldown + session.ServerTableMetadata.ConstantsTable.HomeBankCallCooltime > time) { return; } diff --git a/Maple2.Server.Game/PacketHandlers/HomeDoctorHandler.cs b/Maple2.Server.Game/PacketHandlers/HomeDoctorHandler.cs index 2b0e28e15..b306b9867 100644 --- a/Maple2.Server.Game/PacketHandlers/HomeDoctorHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/HomeDoctorHandler.cs @@ -15,7 +15,7 @@ public class HomeDoctorHandler : FieldPacketHandler { public override void Handle(GameSession session, IByteReader packet) { if (session.Field is null) return; long time = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - if (session.Player.Value.Character.DoctorCooldown + Constant.HomeDoctorCallCooldown > time) { + if (session.Player.Value.Character.DoctorCooldown + session.ServerTableMetadata.ConstantsTable.HomeDoctorCallCooltime > time) { return; } diff --git a/Maple2.Server.Game/PacketHandlers/ItemLockHandler.cs b/Maple2.Server.Game/PacketHandlers/ItemLockHandler.cs index aac7be7b5..ac45bf140 100644 --- a/Maple2.Server.Game/PacketHandlers/ItemLockHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ItemLockHandler.cs @@ -86,7 +86,7 @@ private static void HandleCommit(GameSession session, IByteReader packet) { if (unlock && item.IsLocked) { item.IsLocked = false; - item.UnlockTime = DateTimeOffset.UtcNow.AddSeconds(Constant.ItemUnLockTime).ToUnixTimeSeconds(); + item.UnlockTime = DateTimeOffset.UtcNow.AddSeconds(session.ServerTableMetadata.ConstantsTable.ItemUnLockTime).ToUnixTimeSeconds(); updatedItems.Add(item); } else if (!unlock && !item.IsLocked) { item.IsLocked = true; diff --git a/Maple2.Server.Game/PacketHandlers/JobHandler.cs b/Maple2.Server.Game/PacketHandlers/JobHandler.cs index 6ba30b8ca..0e0b720bd 100644 --- a/Maple2.Server.Game/PacketHandlers/JobHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/JobHandler.cs @@ -93,7 +93,7 @@ private void HandleAdvance(GameSession session, IByteReader packet) { session.Player.Buffs.Clear(); session.Player.Buffs.Initialize(); - session.Player.Buffs.LoadFieldBuffs(); + session.Player.Buffs.LoadFieldBuffs(session.ServerTableMetadata.ConstantsTable.shadowWorldBuffHpUp, session.ServerTableMetadata.ConstantsTable.shadowWorldBuffMoveProtect); session.Stats.Refresh(); session.Field.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo)); session.ConditionUpdate(ConditionType.job, codeLong: (int) session.NpcScript.JobCondition.ChangeToJobCode); diff --git a/Maple2.Server.Game/PacketHandlers/MeretMarketHandler.cs b/Maple2.Server.Game/PacketHandlers/MeretMarketHandler.cs index 763f3f9ac..2bb305f3e 100644 --- a/Maple2.Server.Game/PacketHandlers/MeretMarketHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/MeretMarketHandler.cs @@ -151,10 +151,10 @@ private void HandleListItem(GameSession session, IByteReader packet) { Look = item.Template, Blueprint = item.Blueprint ?? new ItemBlueprint(), Status = UgcMarketListingStatus.Active, - PromotionEndTime = promote ? DateTime.Now.AddHours(Constant.UGCShopAdHour).ToEpochSeconds() : 0, - ListingEndTime = DateTime.Now.AddDays(Constant.UGCShopSaleDay).ToEpochSeconds(), + PromotionEndTime = promote ? DateTime.Now.AddHours(session.ServerTableMetadata.ConstantsTable.UGCShopAdHour).ToEpochSeconds() : 0, + ListingEndTime = DateTime.Now.AddDays(session.ServerTableMetadata.ConstantsTable.UGCShopSaleDay).ToEpochSeconds(), CreationTime = DateTime.Now.ToEpochSeconds(), - Price = Math.Clamp(price, Constant.UGCShopSellMinPrice, Constant.UGCShopSellMaxPrice), + Price = Math.Clamp(price, session.ServerTableMetadata.ConstantsTable.UGCShopSellMinPrice, session.ServerTableMetadata.ConstantsTable.UGCShopSellMaxPrice), TabId = tabId, }; @@ -198,8 +198,8 @@ private void HandleRelistItem(GameSession session, IByteReader packet) { } item.Price = price; - item.PromotionEndTime = promote ? DateTime.Now.AddHours(Constant.UGCShopAdHour).ToEpochSeconds() : 0; - item.ListingEndTime = DateTime.Now.AddDays(Constant.UGCShopSaleDay).ToEpochSeconds(); + item.PromotionEndTime = promote ? DateTime.Now.AddHours(session.ServerTableMetadata.ConstantsTable.UGCShopAdHour).ToEpochSeconds() : 0; + item.ListingEndTime = DateTime.Now.AddDays(session.ServerTableMetadata.ConstantsTable.UGCShopSaleDay).ToEpochSeconds(); item.Status = UgcMarketListingStatus.Active; item.Description = description; item.Tags = tags; diff --git a/Maple2.Server.Game/PacketHandlers/MesoMarketHandler.cs b/Maple2.Server.Game/PacketHandlers/MesoMarketHandler.cs index 7fe758d4a..cf3ccffd2 100644 --- a/Maple2.Server.Game/PacketHandlers/MesoMarketHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/MesoMarketHandler.cs @@ -59,7 +59,7 @@ private static void HandleCreate(GameSession session, IByteReader packet) { long amount = packet.ReadLong(); long price = packet.ReadLong(); - if (amount != Constant.MesoMarketBasePrice) { + if (amount != session.ServerTableMetadata.ConstantsTable.MesoMarketBasePrice) { session.Send(MesoMarketPacket.Error(s_mesoMarket_error_invalidSaleMoney)); return; } @@ -96,7 +96,7 @@ private static void HandleCreate(GameSession session, IByteReader packet) { } session.Player.Value.Account.MesoMarketListed++; - session.Currency.Meso -= Constant.MesoMarketBasePrice; + session.Currency.Meso -= session.ServerTableMetadata.ConstantsTable.MesoMarketBasePrice; session.Send(MesoMarketPacket.Create(listing)); session.Send(MesoMarketPacket.Quota(session.Player.Value.Account.MesoMarketListed, session.Player.Value.Account.MesoMarketPurchased)); } diff --git a/Maple2.Server.Game/PacketHandlers/NpcTalkHandler.cs b/Maple2.Server.Game/PacketHandlers/NpcTalkHandler.cs index f1ffb3df4..cf21366ae 100644 --- a/Maple2.Server.Game/PacketHandlers/NpcTalkHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/NpcTalkHandler.cs @@ -237,7 +237,7 @@ private void HandleEnchant(GameSession session, IByteReader packet) { case ScriptEventType.EnchantSelect: case ScriptEventType.PeachySelect: if (!session.ScriptMetadata.TryGet(npcId, out ScriptMetadata? script) || - !script.States.TryGetValue(Constant.EnchantMasterScriptID, out ScriptState? state)) { + !script.States.TryGetValue(session.ServerTableMetadata.ConstantsTable.EnchantMasterScriptID, out ScriptState? state)) { return; } diff --git a/Maple2.Server.Game/PacketHandlers/PartyHandler.cs b/Maple2.Server.Game/PacketHandlers/PartyHandler.cs index 0f8688b2e..2803d3a5c 100644 --- a/Maple2.Server.Game/PacketHandlers/PartyHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/PartyHandler.cs @@ -285,7 +285,8 @@ private void HandleVoteKick(GameSession session, IByteReader packet) { return; } - if (session.Party.Party.LastVoteTime.FromEpochSeconds().AddSeconds(Constant.PartyVoteReadyDurationSeconds) > DateTime.Now && session.Party.Party.Vote != null) { + if (session.Party.Party.LastVoteTime.FromEpochSeconds().AddSeconds(session.ServerTableMetadata.ConstantsTable.PartyVoteReadyDurationSeconds) + > DateTime.Now && session.Party.Party.Vote != null) { session.Send(PartyPacket.Error(PartyError.s_party_err_already_vote)); return; } @@ -315,7 +316,8 @@ private void HandleReadyCheck(GameSession session) { return; } - if (session.Party.Party.LastVoteTime.FromEpochSeconds().AddSeconds(Constant.PartyVoteReadyDurationSeconds) > DateTime.Now && session.Party.Party.Vote != null) { + if (session.Party.Party.LastVoteTime.FromEpochSeconds().AddSeconds(session.ServerTableMetadata.ConstantsTable.PartyVoteReadyDurationSeconds) > + DateTime.Now && session.Party.Party.Vote != null) { session.Send(PartyPacket.Error(PartyError.s_party_err_already_vote)); return; } diff --git a/Maple2.Server.Game/PacketHandlers/QuestHandler.cs b/Maple2.Server.Game/PacketHandlers/QuestHandler.cs index 4a94bcd3f..9da442bb9 100644 --- a/Maple2.Server.Game/PacketHandlers/QuestHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/QuestHandler.cs @@ -270,8 +270,8 @@ private static void HandleSkyFortressTeleport(GameSession session) { return; } - session.Send(session.PrepareField(Constant.FameContentsSkyFortressGotoMapID, - Constant.FameContentsSkyFortressGotoPortalID) + session.Send(session.PrepareField(session.ServerTableMetadata.ConstantsTable.FameContentsSkyFortressGotoMapID, + session.ServerTableMetadata.ConstantsTable.FameContentsSkyFortressGotoPortalID) ? FieldEnterPacket.Request(session.Player) : FieldEnterPacket.Error(MigrationError.s_move_err_default)); } diff --git a/Maple2.Server.Game/PacketHandlers/SystemShopHandler.cs b/Maple2.Server.Game/PacketHandlers/SystemShopHandler.cs index b9a9df184..4572c9405 100644 --- a/Maple2.Server.Game/PacketHandlers/SystemShopHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/SystemShopHandler.cs @@ -47,11 +47,11 @@ private void HandleArena(GameSession session, IByteReader packet) { return; } - if (!session.NpcMetadata.TryGet(Constant.SystemShopNPCIDHonorToken, out NpcMetadata? npc)) { + if (!session.NpcMetadata.TryGet(session.ServerTableMetadata.ConstantsTable.SystemShopNPCIDHonorToken, out NpcMetadata? npc)) { return; } - session.Shop.Load(npc.Basic.ShopId, Constant.SystemShopNPCIDHonorToken); + session.Shop.Load(npc.Basic.ShopId, session.ServerTableMetadata.ConstantsTable.SystemShopNPCIDHonorToken); session.Send(SystemShopPacket.Arena()); } @@ -61,7 +61,7 @@ private void HandleFishing(GameSession session, IByteReader packet) { session.Shop.ClearActiveShop(); return; } - if (!session.NpcMetadata.TryGet(Constant.SystemShopNPCIDFishing, out NpcMetadata? npc)) { + if (!session.NpcMetadata.TryGet(session.ServerTableMetadata.ConstantsTable.SystemShopNPCIDFishing, out NpcMetadata? npc)) { return; } @@ -76,7 +76,7 @@ private void HandleMentee(GameSession session, IByteReader packet) { return; } - if (!session.NpcMetadata.TryGet(Constant.SystemShopNPCIDMentee, out NpcMetadata? npc)) { + if (!session.NpcMetadata.TryGet(session.ServerTableMetadata.ConstantsTable.SystemShopNPCIDMentee, out NpcMetadata? npc)) { return; } @@ -91,7 +91,7 @@ private void HandleMentor(GameSession session, IByteReader packet) { return; } - if (!session.NpcMetadata.TryGet(Constant.SystemShopNPCIDMentor, out NpcMetadata? npc)) { + if (!session.NpcMetadata.TryGet(session.ServerTableMetadata.ConstantsTable.SystemShopNPCIDMentor, out NpcMetadata? npc)) { return; } diff --git a/Maple2.Server.Game/PacketHandlers/UserChatHandler.cs b/Maple2.Server.Game/PacketHandlers/UserChatHandler.cs index 40ecdfe2a..68a17c7d5 100644 --- a/Maple2.Server.Game/PacketHandlers/UserChatHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/UserChatHandler.cs @@ -180,7 +180,7 @@ private void HandleWorld(GameSession session, string message, ICollection } session.Send(NoticePacket.Notice(NoticePacket.Flags.Alert | NoticePacket.Flags.Message, StringCode.s_worldchat_use_coupon)); } else { - int meretCost = Constant.MeretConsumeWorldChat; + int meretCost = session.ServerTableMetadata.ConstantsTable.MeratConsumeWorldChat; if (session.FindEvent(GameEventType.SaleChat).FirstOrDefault()?.Metadata.Data is SaleChat gameEvent) { meretCost -= (int) (meretCost * Convert.ToSingle(gameEvent.WorldChatDiscount) / 10000); } @@ -215,7 +215,7 @@ private void HandleChannel(GameSession session, string message, ICollection item.Enchant?.Enchants) { + if (session.ServerTableMetadata.ConstantsTable.enchantSuccessBroadcastingLevel > item.Enchant?.Enchants) { return; } diff --git a/Maple2.Server.Login/PacketHandlers/CharacterManagementHandler.cs b/Maple2.Server.Login/PacketHandlers/CharacterManagementHandler.cs index 70bc77d38..2a863461d 100644 --- a/Maple2.Server.Login/PacketHandlers/CharacterManagementHandler.cs +++ b/Maple2.Server.Login/PacketHandlers/CharacterManagementHandler.cs @@ -45,6 +45,7 @@ private enum Command : byte { public required BanWordStorage BanWordStorage { private get; init; } public required ItemMetadataStorage ItemMetadata { private get; init; } public required TableMetadataStorage TableMetadata { private get; init; } + public required ServerTableMetadataStorage ServerTableMetadata { private get; init; } // ReSharper restore All #endregion @@ -58,13 +59,15 @@ public override void Handle(LoginSession session, IByteReader packet) { HandleCreate(session, packet); break; case Command.Delete: - HandleDelete(session, packet); + HandleDelete(session, packet, ServerTableMetadata.ConstantsTable.CharacterDestroyDivisionLevel, + ServerTableMetadata.ConstantsTable.CharacterDestroyWaitSecond); break; case Command.CancelDelete: HandleCancelDelete(session, packet); break; case Command.ConfirmDelete: - HandleDelete(session, packet); + HandleDelete(session, packet, ServerTableMetadata.ConstantsTable.CharacterDestroyDivisionLevel, + ServerTableMetadata.ConstantsTable.CharacterDestroyWaitSecond); break; default: throw new ArgumentException($"Invalid CHARACTER_MANAGEMENT type {command}"); @@ -198,7 +201,7 @@ private void HandleCreate(LoginSession session, IByteReader packet) { session.CreateCharacter(character, outfits); } - private void HandleDelete(LoginSession session, IByteReader packet) { + private void HandleDelete(LoginSession session, IByteReader packet, int characterDestroyDivisionLevel, int characterDestroyWaitSecond) { long characterId = packet.ReadLong(); using GameStorage.Request db = GameStorage.Context(); @@ -218,8 +221,8 @@ private void HandleDelete(LoginSession session, IByteReader packet) { return; } - if (character.Level >= Constant.CharacterDestroyDivisionLevel) { - character.DeleteTime = DateTimeOffset.UtcNow.AddSeconds(Constant.CharacterDestroyWaitSecond).ToUnixTimeSeconds(); + if (character.Level >= characterDestroyDivisionLevel) { + character.DeleteTime = DateTimeOffset.UtcNow.AddSeconds(characterDestroyWaitSecond).ToUnixTimeSeconds(); if (db.UpdateDelete(session.AccountId, characterId, character.DeleteTime)) { session.Send(CharacterListPacket.BeginDelete(characterId, character.DeleteTime)); } else { diff --git a/Maple2.Server.World/WorldServer.cs b/Maple2.Server.World/WorldServer.cs index a8046de75..f83e77872 100644 --- a/Maple2.Server.World/WorldServer.cs +++ b/Maple2.Server.World/WorldServer.cs @@ -332,7 +332,7 @@ public void FieldPlotExpiryCheck() { SetPlotAsPending(db, plot); forfeit = true; // mark as open when 3 days has passed since the expiry time - } else if (plot.OwnerId == 0 && plot.ExpiryTime + Constant.UgcHomeSaleWaitingTime.TotalSeconds < DateTimeOffset.UtcNow.ToUnixTimeSeconds()) { + } else if (plot.OwnerId == 0 && plot.ExpiryTime + serverTableMetadata.ConstantsTable.UgcHomeSaleWaitingTime.TotalSeconds < DateTimeOffset.UtcNow.ToUnixTimeSeconds()) { logger.Information("Marking plot {PlotId} as open (no owner)", plot.Id); db.SetPlotOpen(plot.Id); // Mark as open } else { @@ -359,7 +359,7 @@ public void FieldPlotExpiryCheck() { } // Schedule next check for the next soonest expiry - PlotInfo? nextPlot = db.GetSoonestPlotFromExpire(); + PlotInfo? nextPlot = db.GetSoonestPlotFromExpire(serverTableMetadata.ConstantsTable.UgcHomeSaleWaitingTime); TimeSpan delay; if (nextPlot is not null) { DateTimeOffset nextExpiry = DateTimeOffset.FromUnixTimeSeconds(nextPlot.ExpiryTime); From 3f5414d1c78844bff119dee625cd232f73093275 Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Thu, 26 Feb 2026 16:49:06 -0600 Subject: [PATCH 05/26] Continue switching hard coded constants to server constants pared during file ingest #3 WIP --- Maple2.File.Ingest/Mapper/NpcMapper.cs | 15 +++---- Maple2.Model/Game/Npc/Npc.cs | 12 +++++- Maple2.Model/Metadata/NpcMetadata.cs | 43 ++++++++++++++++--- .../Field/FieldManager/FieldManager.State.cs | 8 +++- .../Model/Field/Actor/FieldNpc.cs | 15 ++++--- .../Model/Field/Actor/FieldPet.cs | 4 +- 6 files changed, 69 insertions(+), 28 deletions(-) diff --git a/Maple2.File.Ingest/Mapper/NpcMapper.cs b/Maple2.File.Ingest/Mapper/NpcMapper.cs index f0871acd6..81e76e8f8 100644 --- a/Maple2.File.Ingest/Mapper/NpcMapper.cs +++ b/Maple2.File.Ingest/Mapper/NpcMapper.cs @@ -31,14 +31,13 @@ protected override IEnumerable Map() { AniSpeed: data.model.anispeed ), Distance: new NpcMetadataDistance( - Avoid: data.distance.avoid, - Sight: data.distance.sight, - SightHeightUp: data.distance.sightHeightUP, - SightHeightDown: data.distance.sightHeightDown - // TODO: Need to move to runtime due to server table constants being used as default value - //LastSightRadius: data.distance.customLastSightRadius == 0 ? Constant.NpcLastSightRadius : data.distance.customLastSightRadius, - //LastSightHeightUp: data.distance.customLastSightHeightUp == 0 ? Constant.NpcLastSightHeightUp : data.distance.customLastSightHeightUp, - //LastSightHeightDown: data.distance.customLastSightHeightDown == 0 ? Constant.NpcLastSightHeightDown : data.distance.customLastSightHeightDown + data.distance.avoid, + data.distance.sight, + data.distance.sightHeightUP, + data.distance.sightHeightDown, + data.distance.customLastSightRadius, + data.distance.customLastSightHeightUp, + data.distance.customLastSightHeightDown ), Skill: new NpcMetadataSkill( Entries: data.skill.ids.Select((skillId, i) => diff --git a/Maple2.Model/Game/Npc/Npc.cs b/Maple2.Model/Game/Npc/Npc.cs index d135d541b..a2c341b3e 100644 --- a/Maple2.Model/Game/Npc/Npc.cs +++ b/Maple2.Model/Game/Npc/Npc.cs @@ -10,8 +10,16 @@ public class Npc { public bool IsBoss => Metadata.Basic.Friendly == 0 && Metadata.Basic.Class >= 3; - public Npc(NpcMetadata metadata, AnimationMetadata? animation) { - Metadata = metadata; + public Npc(NpcMetadata metadata, AnimationMetadata? animation, float constLastSightRadius, float constLastSightHeightUp, float constLastSightHeightDown) { + if (metadata.Distance.LastSightRadius == 0) { + Metadata = new NpcMetadata(metadata, constLastSightRadius); + } else if (metadata.Distance.LastSightRadius == 0 && metadata.Distance.LastSightHeightUp == 0) { + Metadata = new NpcMetadata(metadata, constLastSightRadius, constLastSightHeightUp); + } else if (metadata.Distance.LastSightRadius == 0 && metadata.Distance.LastSightHeightUp == 0 && metadata.Distance.LastSightHeightDown == 0) { + Metadata = new NpcMetadata(metadata, constLastSightRadius, constLastSightHeightUp, constLastSightHeightDown); + } else { + Metadata = metadata; + } Animations = animation?.Sequences ?? new Dictionary(); } } diff --git a/Maple2.Model/Metadata/NpcMetadata.cs b/Maple2.Model/Metadata/NpcMetadata.cs index 86ecf2af5..25f7579f7 100644 --- a/Maple2.Model/Metadata/NpcMetadata.cs +++ b/Maple2.Model/Metadata/NpcMetadata.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.ComponentModel.Design; +using System.Numerics; using Maple2.Model.Enum; namespace Maple2.Model.Metadata; @@ -16,7 +17,28 @@ public record NpcMetadata( NpcMetadataDropInfo DropInfo, NpcMetadataAction Action, NpcMetadataDead Dead, - NpcMetadataLookAtTarget LookAtTarget) : ISearchResult; + NpcMetadataLookAtTarget LookAtTarget) : ISearchResult { + public NpcMetadata(NpcMetadata other, float lastSightRadius) : this(other.Id, + other.Name, other.AiPath, other.Model, other.Stat, other.Basic, other.Distance, other.Skill, other.Property, other.DropInfo, + other.Action, other.Dead, other.LookAtTarget) { + Distance = new NpcMetadataDistance(Distance.Avoid, Distance.Sight, Distance.SightHeightUp, + Distance.SightHeightDown, lastSightRadius, Distance.LastSightHeightUp, Distance.LastSightHeightDown); + } + + public NpcMetadata(NpcMetadata other, float lastSightRadius, float lastSightHeightUp) : this(other.Id, + other.Name, other.AiPath, other.Model, other.Stat, other.Basic, other.Distance, other.Skill, other.Property, other.DropInfo, + other.Action, other.Dead, other.LookAtTarget) { + Distance = new NpcMetadataDistance(Distance.Avoid, Distance.Sight, Distance.SightHeightUp, + Distance.SightHeightDown, lastSightRadius, lastSightHeightUp, Distance.LastSightHeightDown); + } + + public NpcMetadata(NpcMetadata other, float lastSightRadius, float lastSightHeightUp, float lastSightHeightDown) : this(other.Id, + other.Name, other.AiPath, other.Model, other.Stat, other.Basic, other.Distance, other.Skill, other.Property, other.DropInfo, + other.Action, other.Dead, other.LookAtTarget) { + Distance = new NpcMetadataDistance(Distance.Avoid, Distance.Sight, Distance.SightHeightUp, + Distance.SightHeightDown, lastSightRadius, lastSightHeightUp, lastSightHeightDown); + } +} public record NpcMetadataModel( string Name, @@ -34,11 +56,18 @@ public record NpcMetadataDistance( float Avoid, float Sight, float SightHeightUp, - float SightHeightDown - // TODO: Need to move to runtime due to server table constants being used as default value - /* float LastSightRadius, - float LastSightHeightUp, - float LastSightHeightDown*/); + float SightHeightDown) { + public NpcMetadataDistance(float avoid, float sight, float sightHeightUp, float sightHeightDown, float lastSightRadius, + float lastSightHeightUp, float lastSightHeightDown) : this(avoid, sight, sightHeightUp, sightHeightDown) { + LastSightRadius = lastSightRadius; + LastSightHeightUp = lastSightHeightUp; + LastSightHeightDown = lastSightHeightDown; + } + + public float LastSightRadius { get; private set; } + public float LastSightHeightUp { get; private set; } + public float LastSightHeightDown { get; private set; } +} public record NpcMetadataSkill( NpcMetadataSkill.Entry[] Entries, diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index 59d0d66e9..655c53dd6 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -133,7 +133,9 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId AnimationMetadata? animation = NpcMetadata.GetAnimation(npc.Model.Name); string aiPath = disableAi ? string.Empty : npc.AiPath; - var fieldNpc = new FieldNpc(this, NextLocalId(), agent, new Npc(npc, animation), aiPath, patrolDataUUID: spawnPointNpc?.PatrolData, spawnAnimation: spawnAnimation) { + var fieldNpc = new FieldNpc(this, NextLocalId(), agent, new Npc(npc, animation, ServerTableMetadata.ConstantsTable.NpcLastingSightRadius, + ServerTableMetadata.ConstantsTable.NpcLastingSightHeightUp, ServerTableMetadata.ConstantsTable.NpcLastingSightHeightDown), aiPath, + patrolDataUUID: spawnPointNpc?.PatrolData, spawnAnimation: spawnAnimation) { Owner = owner, Position = spawnPosition, Rotation = rotation, @@ -171,7 +173,9 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId int objectId = player != null ? NextGlobalId() : NextLocalId(); AnimationMetadata? animation = NpcMetadata.GetAnimation(npc.Model.Name); - var fieldPet = new FieldPet(this, objectId, agent, new Npc(npc, animation), pet, petMetadata, Constant.PetFieldAiPath, player) { + var fieldPet = new FieldPet(this, objectId, agent, new Npc(npc, animation, ServerTableMetadata.ConstantsTable.NpcLastingSightRadius, + ServerTableMetadata.ConstantsTable.NpcLastingSightHeightUp,ServerTableMetadata.ConstantsTable.NpcLastingSightHeightDown), + pet, petMetadata, Constant.PetFieldAiPath, player) { Owner = owner, Position = position, Rotation = rotation, diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index bb94336fe..cacc29a6a 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -1,20 +1,21 @@ -using System.Diagnostics.CodeAnalysis; -using System.Numerics; +using DotRecast.Detour.Crowd; +using Maple2.Database.Storage; using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.Model.Metadata; +using Maple2.Server.Core.Packets; using Maple2.Server.Game.Manager.Field; +using Maple2.Server.Game.Model.ActorStateComponent; +using Maple2.Server.Game.Model.Enum; using Maple2.Server.Game.Model.Skill; using Maple2.Server.Game.Model.State; using Maple2.Server.Game.Packets; +using Maple2.Server.Game.Session; using Maple2.Tools; using Maple2.Tools.Collision; -using Maple2.Server.Game.Session; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; using static Maple2.Server.Game.Model.ActorStateComponent.TaskState; -using Maple2.Server.Game.Model.Enum; -using Maple2.Server.Core.Packets; -using DotRecast.Detour.Crowd; -using Maple2.Server.Game.Model.ActorStateComponent; using MovementState = Maple2.Server.Game.Model.ActorStateComponent.MovementState; namespace Maple2.Server.Game.Model; diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs index 94c2005c5..accb7b90e 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs @@ -64,7 +64,7 @@ public override void ApplyDamage(IActor caster, DamageRecord damage, SkillMetada } var targetRecord = new DamageRecordTarget(this); - int damageAmount = TamingPoint - Math.Min(TamingPoint + attack.Pet.TamingPoint, Constant.TamingPetMaxPoint); + int damageAmount = TamingPoint - Math.Min(TamingPoint + attack.Pet.TamingPoint, Field.ServerTableMetadata.ConstantsTable.TamingPetMaxPoint); TamingPoint -= damageAmount; targetRecord.AddDamage(damageAmount == 0 ? DamageType.Miss : DamageType.Normal, damageAmount); @@ -73,7 +73,7 @@ public override void ApplyDamage(IActor caster, DamageRecord damage, SkillMetada IsDead = true; OnDeath(); DropItem(caster); - } else if (TamingPoint >= Constant.TamingPetMaxPoint) { // trap has chance to fail + } else if (TamingPoint >= Field.ServerTableMetadata.ConstantsTable.TamingPetMaxPoint) { // trap has chance to fail IsDead = true; OnDeath(); DropItem(caster); From f633b68aeb9d662b3930e0c2044eafe28dd8e459 Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Fri, 27 Feb 2026 16:59:02 -0600 Subject: [PATCH 06/26] Fix last hard coded constant value in PartyManager. Add in input cleaning and non-IConvertible data types handling during constant parsing. --- .../Mapper/ServerTableMapper.cs | 73 ++++++++++++++- .../Metadata/ServerTable/ConstantsTable.cs | 88 ++++++++++--------- Maple2.Server.World/Containers/PartyLookup.cs | 9 +- .../Containers/PartyManager.cs | 10 ++- Maple2.Server.World/Program.cs | 3 + 5 files changed, 133 insertions(+), 50 deletions(-) diff --git a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs index a90687aa7..2818ebe62 100644 --- a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs +++ b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs @@ -3,6 +3,7 @@ using Maple2.File.IO; using Maple2.File.Parser; using Maple2.File.Parser.Enum; +using Maple2.File.Parser.Flat.Convert; using Maple2.File.Parser.Xml.Table.Server; using Maple2.Model; using Maple2.Model.Common; @@ -12,6 +13,7 @@ using Maple2.Model.Game.Shop; using Maple2.Model.Metadata; using System.Globalization; +using System.Numerics; using System.Reflection; using System.Xml; using DayOfWeek = System.DayOfWeek; @@ -2118,11 +2120,78 @@ private ConstantsTable ParseConstants() { PropertyInfo[] constantsProperties = constants.GetType().GetProperties(); foreach (PropertyInfo constantsProperty in constantsProperties) { foreach ((string key, Parser.Xml.Table.Constants.Key constant) in parser.ParseConstants()) { - if (!key.Trim().Equals(constantsProperty.Name.Trim())) continue; - constantsProperty.SetValue(constants, Convert.ChangeType(constant.value, constantsProperty.PropertyType)); + string constantPropertyName = constantsProperty.Name.Trim(); + if (!key.Trim().Equals(constantPropertyName)) continue; + Type constantsPropertyType = constantsProperty.PropertyType; + string cleanConstantValue = CleanConstantsInput(constant.value.Trim(), constantPropertyName, constantsPropertyType); + SetValue(constantsProperty, constants, cleanConstantValue); break; } } return constants; } + + private string CleanConstantsInput(string input, string propName, Type type) { + // check if string contains the ending 'f' for float designation, strip it if it does. + if (type == typeof(float) && input.Contains('f')) { + input = input.TrimEnd('f', 'F'); + } + // 1 does not automatically equate to true during bool conversion + if (type == typeof(bool) && input == "1") { + input = "true"; + } + // 0 does not automatically equate to false during bool conversion + if (type == typeof(bool) && input == "0") { + input = "false"; + } + // Convert into a TimeSpan friendly input string instead of an int value + if (type == typeof(TimeSpan) && propName == "UgcHomeSaleWaitingTime") { + input = TimeSpan.FromSeconds(int.Parse(input)).ToString(); // TODO: may not be correct conversion to TimeSpan + } + // Remove prefix 0 on integers since they do not convert properly + if (type == typeof(int) && input[0] == '0' && input.Length > 1) { + input = input.Remove(0, 1); + } + return input; + } + + private void SetValue(PropertyInfo prop, object? obj, object? value) { + if (obj == null && value == null || value == null) return; + HandleNonIConvertibleTypes(prop, ref value); + bool isConvertible = typeof(IConvertible).IsAssignableFrom(prop.PropertyType); + prop.SetValue(obj, isConvertible ? Convert.ChangeType(value, prop.PropertyType, CultureInfo.InvariantCulture) : value); + } + + private object? HandleNonIConvertibleTypes(PropertyInfo prop, ref object? value) { + if (value == null) return value; + // Handle TimeSpan type + if (prop.PropertyType == typeof(TimeSpan)) { + // Special case - dashes (-) are used instead of colons (:) + if (prop.Name == "DailyTrophyResetDate") { + value = ((string)value).Replace('-', ':'); + } + value = TimeSpan.Parse((string)value, CultureInfo.InvariantCulture); + } + // Handle array types (int[], short[], etc.) + if (prop.PropertyType.IsArray) { + var elementType = prop.PropertyType.GetElementType(); + if (elementType == null) return value; + string[] segments = ((string)value).Split(','); + Array destinationArray = Array.CreateInstance(elementType, segments.Length); + for (int i = 0; i < segments.Length; i++) { + object convertedValue = Convert.ChangeType(segments[i].Trim(), elementType); + destinationArray.SetValue(convertedValue, i); + } + value = destinationArray; + } + // Handle Vector3 type + if (prop.PropertyType == typeof(Vector3)) { + string[] parts = ((string) value).Split(','); + if (parts.Length != 3) return value; + value = new Vector3(float.Parse(parts[0], CultureInfo.InvariantCulture), + float.Parse(parts[1], CultureInfo.InvariantCulture), + float.Parse(parts[2], CultureInfo.InvariantCulture)); + } + return value; + } } diff --git a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs index edb544f78..7eb6b6870 100644 --- a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs +++ b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs @@ -1,4 +1,6 @@ -namespace Maple2.Model.Metadata; +using System.Numerics; + +namespace Maple2.Model.Metadata; public record ConstantsTable( float NPCColorScale = 0f, @@ -116,8 +118,8 @@ public record ConstantsTable( string UserRevivalInvincibleIconPath = "", int GetExpMinVelocity = 0, int GetExpVelocityPer1Length = 0, - string GetExpControlValue0 = "", - string GetExpControlValue1 = "", + float[] GetExpControlValue0 = null!, + float[] GetExpControlValue1 = null!, string GetExpTargetPCDummyName = "", float GetExpTimeAcceleration = 0f, float GetExpCollisionRadius = 0f, @@ -158,18 +160,18 @@ public record ConstantsTable( float BossHpBarDuration = 0f, float FindHoldTargetRange = 0f, int FindGrabNodeRange = 0, - string UgcShopCharCameraLookat = "", - string UgcShopCharCameraPos = "", - int UgcShopCharCameraMinDistance = 0, - int UgcShopCharCameraZoomVelocity = 0, - string UgcShopCubeCameraLookat = "", - string UgcShopCubeCameraPos = "", - int UgcShopCubeCameraMinDistance = 0, - int UgcShopCubeCameraZoomVelocity = 0, - string UgcShopRideeCameraLookat = "", - string UgcShopRideeCameraPos = "", - int UgcShopRideeCameraMinDistance = 0, - int UgcShopRideeCameraZoomVelocity = 0, + Vector3 UgcshopCharCameraLookat = default, + Vector3 UgcshopCharCameraPos = default, + int UgcshopCharCameraMinDistance = 0, + int UgcshopCharCameraZoomVelocity = 0, + Vector3 UgcshopCubeCameraLookat = default, + Vector3 UgcshopCubeCameraPos = default, + int UgcshopCubeCameraMinDistance = 0, + int UgcshopCubeCameraZoomVelocity = 0, + Vector3 UgcshopRideeCameraLookat = default, + Vector3 UgcshopRideeCameraPos = default, + int UgcshopRideeCameraMinDistance = 0, + int UgcshopRideeCameraZoomVelocity = 0, int FieldCachingCount = 0, float FieldCachingTime = 0f, int FieldCachingMaxCount = 0, @@ -213,8 +215,8 @@ public record ConstantsTable( int returnHomeSkill = 0, int returnHomeSkillMeret = 0, int TutorialIntroSkipTime = 0, - string AvatarDefaultItemMale = "", - string AvatarDefaultItemFemale = "", + int[] AvatarDefaultItemMale = null!, + int[] AvatarDefaultItemFemale = null!, int TalkCooldown = 0, int AddressPopupDuration = 0, int MaxFPS = 0, @@ -225,8 +227,8 @@ public record ConstantsTable( int UGCShopAdHour = 0, int UGCShopSellingRestrictAmount = 0, int MeretMarketHomeBannerShowTick = 0, - int BlackMarketSellMinPrice = 0, - int BlackMarketSellMaxPrice = 0, + long BlackMarketSellMinPrice = 0, + long BlackMarketSellMaxPrice = 0, int BlackMarketSellEndDay = 0, int ItemTransferBlackMarketGrade = 0, int UgcBannerCheckTime = 0, @@ -382,7 +384,7 @@ public record ConstantsTable( int guildPVPMatchingTime = 0, int guildPVPWinPoint = 0, int guildPVPLosePoint = 0, - string guildPVPAdditionalEffect = "", + int[] guildPVPAdditionalEffect = null!, int ModePvPRecoverySkill = 0, int ModePvPRecoverySP = 0, int ModePvPInvincibleTime = 0, @@ -394,10 +396,10 @@ public record ConstantsTable( int PvpGuildRewardItem = 0, int PvpGuildRewardWinnerCount = 0, int PvpGuildRewardLoserCount = 0, - string ModePVPRedArenaAdditionalEffect = "", + int[] ModePVPRedArenaAdditionalEffect = null!, int ModePvPScoreDead = -50, int ModePvPScoreKill = 100, - string ModePVPBloodMineAdditionalEffect = "", + int[] ModePVPBloodMineAdditionalEffect = null!, int pvpFFAShortComboTick = 0, int pvpFFALongComboTick = 0, int pvpFFASlayerCount = 0, @@ -416,7 +418,7 @@ public record ConstantsTable( int PvpFFAAdditionRewardRate = 0, int rankDuelPvpMatchingTime = 0, int rankDuelPvpMatchingMinGap = 0, - string ModePVPDuelRankArenaAdditionalEffect = "", + int[] ModePVPDuelRankArenaAdditionalEffect = null!, int MailExpiryDays = 0, int MailExpiryDaysPremium = 0, int MailExpiryDaysBlackMarket = 0, @@ -671,21 +673,21 @@ public record ConstantsTable( int PetTrapDropVisibleDelay = 0, int PetMaxLevel = 0, string VisitorBookURL = "", - short bagSlotTabGameCount = 0, - short bagSlotTabSkinCount = 0, - short bagSlotTabSummonCount = 0, - short bagSlotTabMaterialCount = 0, - short bagSlotTabMasteryCount = 0, - short bagSlotTabLifeCount = 0, - short bagSlotTabQuestCount = 0, - short bagSlotTabGemCount = 0, - short bagSlotTabPetCount = 0, - short bagSlotTabActiveSkillCount = 0, - short bagSlotTabCoinCount = 0, - short bagSlotTabBadgeCount = 0, - short bagSlotTabMiscCount = 0, - short bagSlotTabLapenShardCount = 0, - short bagSlotTabPieceCount = 0, + short[] bagSlotTabGameCount = null!, + short[] bagSlotTabSkinCount = null!, + short[] bagSlotTabSummonCount = null!, + short[] bagSlotTabMaterialCount = null!, + short[] bagSlotTabMasteryCount = null!, + short[] bagSlotTabLifeCount = null!, + short[] bagSlotTabQuestCount = null!, + short[] bagSlotTabGemCount = null!, + short[] bagSlotTabPetCount = null!, + short[] bagSlotTabActiveSkillCount = null!, + short[] bagSlotTabCoinCount = null!, + short[] bagSlotTabBadgeCount = null!, + short[] bagSlotTabMiscCount = null!, + short[] bagSlotTabLapenShardCount = null!, + short[] bagSlotTabPieceCount = null!, int MasteryObjectInteractionDistance = 0, float GatheringObjectMarkOffsetX = 0f, float GatheringObjectMarkOffsetY = 0f, @@ -790,7 +792,7 @@ public record ConstantsTable( int UgcNameLengthMax = 0, int UgcTagLengthMax = 0, int ChangeJobLevel = 0, - int LapenShardOpenQuestID = 0, + int[] LapenShardOpenQuestID = null!, int MaidNameLengthMin = 0, int MaidNameLengthMax = 0, int MaidDescLengthMin = 0, @@ -803,13 +805,13 @@ public record ConstantsTable( int MouseInteractLimitDistance = 0, int AutoInstallEquipmentMinLevel = 0, int AutoInstallEquipmentMaxLevel = 0, - string PartySearchRegisterComboValues = "", + int[] PartySearchRegisterComboValues = null!, int FieldWarInstanceEnterableDurationSeconds = 0, int FieldWarRequirePlayerCount = 0, int FieldWarRequireAchieveID = 0, int FieldWarRequireLevel = 0, int StatScaleMarkingAdditionalEffect = 0, - string DungeonRewardFailEmotions = "", + int[] DungeonRewardFailEmotions = null!, int SummonPetSkillID = 0, int UGCMapSetItemEffectCountLimit = 0, int AdventureLevelMissionResetWeekday = 0, @@ -850,8 +852,8 @@ public record ConstantsTable( int PvpOnePunchRewardItem = 0, int PvpOnePunchScoreNpcKill = 0, int SpecialHairShopID = 0, - string GemStoneProbList = "", - string SkinGemStoneProbList = "", + int[] GemStoneProbList = null!, + int[] SkinGemStoneProbList = null!, string PersonalInfoAgreementURL = "", float BothHandLowDamageRatio = 0f, float BothHandWeaponDamagePenaltyDiv = 0f, diff --git a/Maple2.Server.World/Containers/PartyLookup.cs b/Maple2.Server.World/Containers/PartyLookup.cs index 29ece3e4c..a1f774b46 100644 --- a/Maple2.Server.World/Containers/PartyLookup.cs +++ b/Maple2.Server.World/Containers/PartyLookup.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using Maple2.Database.Storage; using Maple2.Model.Error; using Maple2.Model.Game; using Maple2.Model.Game.Party; @@ -10,6 +11,8 @@ public class PartyLookup : IDisposable { private readonly ChannelClientLookup channelClients; private readonly PlayerInfoLookup playerLookup; private readonly PartySearchLookup partySearchLookup; + private ServerTableMetadataStorage serverTableMetadataStorage; + private readonly ConcurrentDictionary parties; private int nextPartyId = 1; @@ -22,6 +25,10 @@ public PartyLookup(ChannelClientLookup channelClients, PlayerInfoLookup playerLo parties = new ConcurrentDictionary(); } + public void InjectDependencies(ServerTableMetadataStorage serverTableMetadataStorage) { + this.serverTableMetadataStorage = serverTableMetadataStorage; + } + public void Dispose() { foreach (PartyManager manager in parties.Values) { manager.Dispose(); @@ -56,7 +63,7 @@ public PartyError Create(long leaderId, out int partyId) { } var party = new Party(partyId, leaderInfo.AccountId, leaderInfo.CharacterId, leaderInfo.Name); - var manager = new PartyManager(party) { + var manager = new PartyManager(party, serverTableMetadataStorage.ConstantsTable.PartyVoteReadyDurationSeconds) { ChannelClients = channelClients, PartyLookup = this, }; diff --git a/Maple2.Server.World/Containers/PartyManager.cs b/Maple2.Server.World/Containers/PartyManager.cs index 837254313..a38467520 100644 --- a/Maple2.Server.World/Containers/PartyManager.cs +++ b/Maple2.Server.World/Containers/PartyManager.cs @@ -1,10 +1,10 @@ using System.Collections.Concurrent; using Grpc.Core; +using Maple2.Database.Storage; using Maple2.Model.Enum; using Maple2.Model.Error; using Maple2.Model.Game; using Maple2.Model.Game.Party; -using Maple2.Model.Metadata; using Maple2.Server.Channel.Service; using ChannelClient = Maple2.Server.Channel.Service.Channel.ChannelClient; @@ -15,10 +15,12 @@ public class PartyManager : IDisposable { public required PartyLookup PartyLookup { get; init; } public readonly Party Party; private readonly ConcurrentDictionary pendingInvites; + private readonly int partyVoteReadyDurationSeconds; - public PartyManager(Party party) { + public PartyManager(Party party, int partyVoteReadyDurationSeconds) { Party = party; pendingInvites = new ConcurrentDictionary(); + this.partyVoteReadyDurationSeconds = partyVoteReadyDurationSeconds; } public void Dispose() { @@ -282,7 +284,7 @@ public PartyError StartReadyCheck(long requestorId) { }); Task.Factory.StartNew(() => { - Thread.Sleep(TimeSpan.FromSeconds(Constant.PartyVoteReadyDurationSeconds)); + Thread.Sleep(TimeSpan.FromSeconds(partyVoteReadyDurationSeconds)); if (Party.Vote == null) { return; } @@ -394,7 +396,7 @@ public PartyError VoteKick(long requestorId, long targetId) { Task.Factory.StartNew(() => { // TODO: The duration is wrong. - Thread.Sleep(TimeSpan.FromSeconds(Constant.PartyVoteReadyDurationSeconds)); + Thread.Sleep(TimeSpan.FromSeconds(partyVoteReadyDurationSeconds)); if (Party.Vote == null) { return; } diff --git a/Maple2.Server.World/Program.cs b/Maple2.Server.World/Program.cs index 6d34eafcd..64a59b226 100644 --- a/Maple2.Server.World/Program.cs +++ b/Maple2.Server.World/Program.cs @@ -84,7 +84,10 @@ .OnActivated(e => { var channelLookup = e.Context.Resolve(); var playerInfoLookup = e.Context.Resolve(); + var partyLookup = e.Context.Resolve(); + var serverTableMetadataStorage = e.Context.Resolve(); channelLookup.InjectDependencies(e.Instance, playerInfoLookup); + partyLookup.InjectDependencies(serverTableMetadataStorage); }) .SingleInstance(); }); From 24c70fa7d70945c6154e4737f93694eea104cd76 Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Fri, 27 Feb 2026 22:28:41 -0600 Subject: [PATCH 07/26] Remove hard coded constants and utilize server constants within Inventory Manager. --- Maple2.Model/Metadata/Constants.cs | 16 ----- .../Manager/Items/InventoryManager.cs | 62 +++++++++---------- 2 files changed, 31 insertions(+), 47 deletions(-) diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index 13f94dac4..c609ce3f1 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -131,22 +131,6 @@ public static class Constant { public const int PartyFinderListingsPageCount = 12; public const int ProposalItemId = 11600482; public const int bagSlotTabPetEquipCount = 48; - public const int BagSlotTabGameCountMax = 48; - public const int BagSlotTabSkinCountMax = 150; - public const int BagSlotTabSummonCountMax = 48; - public const int BagSlotTabMaterialCountMax = 48; - public const int BagSlotTabMasteryCountMax = 48; - public const int BagSlotTabLifeCountMax = 48; - public const int BagSlotTabQuestCountMax = 48; - public const int BagSlotTabGemCountMax = 48; - public const int BagSlotTabPetCountMax = 78; - public const int BagSlotTabPetEquipCountMax = 48; - public const int BagSlotTabActiveSkillCountMax = 48; - public const int BagSlotTabCoinCountMax = 48; - public const int BagSlotTabBadgeCountMax = 48; - public const int BagSlotTabMiscCountMax = 48; - public const int BagSlotTabLapenshardCountMax = 48; - public const int BagSlotTabPieceCountMax = 48; public const int MeretAirTaxiPrice = 15; public const int ClubMaxCount = 3; diff --git a/Maple2.Server.Game/Manager/Items/InventoryManager.cs b/Maple2.Server.Game/Manager/Items/InventoryManager.cs index 78fc94e3b..87d3c6a88 100644 --- a/Maple2.Server.Game/Manager/Items/InventoryManager.cs +++ b/Maple2.Server.Game/Manager/Items/InventoryManager.cs @@ -46,42 +46,42 @@ public InventoryManager(GameStorage.Request db, GameSession session) { private short BaseSize(InventoryType type) { return type switch { - InventoryType.Gear => session.ServerTableMetadata.ConstantsTable.bagSlotTabGameCount, - InventoryType.Outfit => session.ServerTableMetadata.ConstantsTable.bagSlotTabSkinCount, - InventoryType.Mount => session.ServerTableMetadata.ConstantsTable.bagSlotTabSummonCount, - InventoryType.Catalyst => session.ServerTableMetadata.ConstantsTable.bagSlotTabMaterialCount, - InventoryType.FishingMusic => session.ServerTableMetadata.ConstantsTable.bagSlotTabLifeCount, - InventoryType.Quest => session.ServerTableMetadata.ConstantsTable.bagSlotTabQuestCount, - InventoryType.Gemstone => session.ServerTableMetadata.ConstantsTable.bagSlotTabGemCount, - InventoryType.Misc => session.ServerTableMetadata.ConstantsTable.bagSlotTabMiscCount, - InventoryType.LifeSkill => session.ServerTableMetadata.ConstantsTable.bagSlotTabMasteryCount, - InventoryType.Pets => session.ServerTableMetadata.ConstantsTable.bagSlotTabPetCount, - InventoryType.Consumable => session.ServerTableMetadata.ConstantsTable.bagSlotTabActiveSkillCount, - InventoryType.Currency => session.ServerTableMetadata.ConstantsTable.bagSlotTabCoinCount, - InventoryType.Badge => session.ServerTableMetadata.ConstantsTable.bagSlotTabBadgeCount, - InventoryType.Lapenshard => session.ServerTableMetadata.ConstantsTable.bagSlotTabLapenShardCount, - InventoryType.Fragment => session.ServerTableMetadata.ConstantsTable.bagSlotTabPieceCount, + InventoryType.Gear => session.ServerTableMetadata.ConstantsTable.bagSlotTabGameCount[0], + InventoryType.Outfit => session.ServerTableMetadata.ConstantsTable.bagSlotTabSkinCount[0], + InventoryType.Mount => session.ServerTableMetadata.ConstantsTable.bagSlotTabSummonCount[0], + InventoryType.Catalyst => session.ServerTableMetadata.ConstantsTable.bagSlotTabMaterialCount[0], + InventoryType.FishingMusic => session.ServerTableMetadata.ConstantsTable.bagSlotTabLifeCount[0], + InventoryType.Quest => session.ServerTableMetadata.ConstantsTable.bagSlotTabQuestCount[0], + InventoryType.Gemstone => session.ServerTableMetadata.ConstantsTable.bagSlotTabGemCount[0], + InventoryType.Misc => session.ServerTableMetadata.ConstantsTable.bagSlotTabMiscCount[0], + InventoryType.LifeSkill => session.ServerTableMetadata.ConstantsTable.bagSlotTabMasteryCount[0], + InventoryType.Pets => session.ServerTableMetadata.ConstantsTable.bagSlotTabPetCount[0], + InventoryType.Consumable => session.ServerTableMetadata.ConstantsTable.bagSlotTabActiveSkillCount[0], + InventoryType.Currency => session.ServerTableMetadata.ConstantsTable.bagSlotTabCoinCount[0], + InventoryType.Badge => session.ServerTableMetadata.ConstantsTable.bagSlotTabBadgeCount[0], + InventoryType.Lapenshard => session.ServerTableMetadata.ConstantsTable.bagSlotTabLapenShardCount[0], + InventoryType.Fragment => session.ServerTableMetadata.ConstantsTable.bagSlotTabPieceCount[0], _ => throw new ArgumentOutOfRangeException($"Invalid InventoryType: {type}"), }; } - private static short MaxExpandSize(InventoryType type) { + private short MaxExpandSize(InventoryType type) { return type switch { - InventoryType.Gear => Constant.BagSlotTabGameCountMax, - InventoryType.Outfit => Constant.BagSlotTabSkinCountMax, - InventoryType.Mount => Constant.BagSlotTabSummonCountMax, - InventoryType.Catalyst => Constant.BagSlotTabMaterialCountMax, - InventoryType.FishingMusic => Constant.BagSlotTabLifeCountMax, - InventoryType.Quest => Constant.BagSlotTabQuestCountMax, - InventoryType.Gemstone => Constant.BagSlotTabGemCountMax, - InventoryType.Misc => Constant.BagSlotTabMiscCountMax, - InventoryType.LifeSkill => Constant.BagSlotTabMasteryCountMax, - InventoryType.Pets => Constant.BagSlotTabPetCountMax, - InventoryType.Consumable => Constant.BagSlotTabActiveSkillCountMax, - InventoryType.Currency => Constant.BagSlotTabCoinCountMax, - InventoryType.Badge => Constant.BagSlotTabBadgeCountMax, - InventoryType.Lapenshard => Constant.BagSlotTabLapenshardCountMax, - InventoryType.Fragment => Constant.BagSlotTabPieceCountMax, + InventoryType.Gear => session.ServerTableMetadata.ConstantsTable.bagSlotTabGameCount[1], + InventoryType.Outfit => session.ServerTableMetadata.ConstantsTable.bagSlotTabSkinCount[1], + InventoryType.Mount => session.ServerTableMetadata.ConstantsTable.bagSlotTabSummonCount[1], + InventoryType.Catalyst => session.ServerTableMetadata.ConstantsTable.bagSlotTabMaterialCount[1], + InventoryType.FishingMusic => session.ServerTableMetadata.ConstantsTable.bagSlotTabLifeCount[1], + InventoryType.Quest => session.ServerTableMetadata.ConstantsTable.bagSlotTabQuestCount[1], + InventoryType.Gemstone => session.ServerTableMetadata.ConstantsTable.bagSlotTabGemCount[1], + InventoryType.Misc => session.ServerTableMetadata.ConstantsTable.bagSlotTabMiscCount[1], + InventoryType.LifeSkill => session.ServerTableMetadata.ConstantsTable.bagSlotTabMasteryCount[1], + InventoryType.Pets => session.ServerTableMetadata.ConstantsTable.bagSlotTabPetCount[1], + InventoryType.Consumable => session.ServerTableMetadata.ConstantsTable.bagSlotTabActiveSkillCount[1], + InventoryType.Currency => session.ServerTableMetadata.ConstantsTable.bagSlotTabCoinCount[1], + InventoryType.Badge => session.ServerTableMetadata.ConstantsTable.bagSlotTabBadgeCount[1], + InventoryType.Lapenshard => session.ServerTableMetadata.ConstantsTable.bagSlotTabLapenShardCount[1], + InventoryType.Fragment => session.ServerTableMetadata.ConstantsTable.bagSlotTabPieceCount[1], _ => throw new ArgumentOutOfRangeException($"Invalid InventoryType: {type}"), }; } From 059fa693194f4cfe6e15f8a1bc17b6fae5ca764a Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Sun, 1 Mar 2026 01:03:22 +0800 Subject: [PATCH 08/26] JewelRuby After sorting through it, I have roughly fixed the following major bugs, while some minor bugs have not been included We have made certain fixes to address the existing abnormal issues and BUGs on the server. The fixes are as follows 1.Fixed the issue where monsters would walk through walls when using skills 2.Fixed the issue of monsters facing players when using normal attacks and skills. Now, monsters will not launch attacks or use skills with their backs facing players 3.Fixed the issue where monster skills were directed incorrectly and went through walls 4.Fixed the issue where player-range skills did not have damage numbers and did not interact with hit reactions 5.Fixed the issue where monsters using range skills could not correctly identify player positions, resulting in location errors 6.Fixed the issue where summoned NPCs or monsters would instantly die under certain circumstances, making it impossible to complete some dungeons 7.Fixed the issue where players may falsely go offline (unable to use chat, player status is offline, and the team channel displays 65535) when entering or exiting dungeons or houses 8.Fixed the issue where fishing did not earn character experience points 9.Fixed the issue where all item rewards in the group would be distributed at once when reading dungeon rewards (in cases where it was not due to an XML configuration error) --- .../Storage/Game/GameStorage.User.cs | 6 +- Maple2.Model/Game/Market/SoldUgcMarketItem.cs | 22 ++-- Maple2.Model/Metadata/Constants.cs | 2 +- Maple2.Server.Game/GameServer.cs | 23 +++- Maple2.Server.Game/Manager/DungeonManager.cs | 2 +- .../Manager/Field/AgentNavigation.cs | 99 ++++++++++++-- .../Field/FieldManager/FieldManager.State.cs | 16 +++ .../Field/FieldManager/FieldManager.cs | 27 +++- .../Manager/Field/FieldManager/IField.cs | 2 + .../Manager/Field/TriggerCollection.cs | 122 ++++++++++++------ Maple2.Server.Game/Manager/FishingManager.cs | 15 +++ .../MovementState.SkillCastTask.cs | 42 ++++-- .../Actor/ActorStateComponent/SkillState.cs | 10 +- .../Model/Field/Actor/FieldNpc.cs | 9 ++ .../Model/Field/Entity/FieldSkill.cs | 22 +++- .../Packets/SkillDamagePacket.cs | 20 ++- .../Trigger/TriggerContext.Field.cs | 13 +- .../Trigger/TriggerContext.Npc.cs | 16 ++- .../Util/Sync/PlayerInfoStorage.cs | 21 ++- .../Service/WorldService.Migrate.cs | 19 ++- Maple2.Server.World/WorldServer.cs | 25 ++++ Maple2.Tools/VectorMath/Transform.cs | 34 ++++- 22 files changed, 443 insertions(+), 124 deletions(-) diff --git a/Maple2.Database/Storage/Game/GameStorage.User.cs b/Maple2.Database/Storage/Game/GameStorage.User.cs index 35dd05f68..722cd849f 100644 --- a/Maple2.Database/Storage/Game/GameStorage.User.cs +++ b/Maple2.Database/Storage/Game/GameStorage.User.cs @@ -582,9 +582,9 @@ public Account CreateAccount(Account account, string password) { model.Password = BCrypt.Net.BCrypt.HashPassword(password, 13); #if DEBUG model.Currency = new AccountCurrency { - Meret = 9_999_999, + Meret = 1_000_00, }; - model.Permissions = AdminPermissions.Admin.ToString(); +// model.Permissions = AdminPermissions.Admin.ToString(); #endif Context.Account.Add(model); Context.SaveChanges(); // Exception if failed. @@ -609,7 +609,7 @@ public Account CreateAccount(Account account, string password) { model.Channel = -1; #if DEBUG model.Currency = new CharacterCurrency { - Meso = 999999999, + Meso = 500000, }; #endif Context.Character.Add(model); diff --git a/Maple2.Model/Game/Market/SoldUgcMarketItem.cs b/Maple2.Model/Game/Market/SoldUgcMarketItem.cs index b8d22bb6a..0b14e1795 100644 --- a/Maple2.Model/Game/Market/SoldUgcMarketItem.cs +++ b/Maple2.Model/Game/Market/SoldUgcMarketItem.cs @@ -10,21 +10,25 @@ public class SoldUgcMarketItem : IByteSerializable { public required string Name { get; init; } public long SoldTime { get; init; } public long AccountId { get; init; } - + public int Count { get; init; } = 1; public void WriteTo(IByteWriter writer) { writer.WriteLong(Id); - writer.WriteLong(); + writer.WriteLong(); writer.WriteUnicodeString(Name); - writer.WriteInt(); - writer.WriteInt(); - writer.WriteLong(); - writer.WriteLong(); - writer.WriteUnicodeString(); - writer.WriteUnicodeString(); - writer.WriteInt(); + + writer.WriteInt(Count); + writer.WriteInt(); + + writer.WriteLong(); + writer.WriteLong(); + writer.WriteUnicodeString(); + writer.WriteUnicodeString(); + writer.WriteInt(); + writer.WriteLong(Price); writer.WriteLong(SoldTime); writer.WriteLong(Profit); } } + diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index 43c74bf53..f5085e784 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -123,7 +123,7 @@ public static class Constant { public const bool DebugTriggers = false; // Set to true to enable debug triggers. (It'll write triggers to files and load triggers from files instead of DB) - public const bool AllowUnicodeInNames = false; // Allow Unicode characters in character and guild names + public const bool AllowUnicodeInNames = true; // Allow Unicode characters in character and guild names public static IReadOnlyDictionary ContentRewards { get; } = new Dictionary { { "miniGame", 1005 }, diff --git a/Maple2.Server.Game/GameServer.cs b/Maple2.Server.Game/GameServer.cs index 7d2524626..b09bc53f9 100644 --- a/Maple2.Server.Game/GameServer.cs +++ b/Maple2.Server.Game/GameServer.cs @@ -66,17 +66,32 @@ public GameServer(FieldManager.Factory fieldFactory, PacketRouter r public override void OnConnected(GameSession session) { lock (mutex) { connectingSessions.Remove(session); - sessions[session.CharacterId] = session; + + if (session.CharacterId != 0) { + sessions[session.CharacterId] = session; + } + + // 可选:避免残留的 0 键占位 + if (sessions.TryGetValue(0, out GameSession? zero) && ReferenceEquals(zero, session)) { + sessions.Remove(0); + } } } - public override void OnDisconnected(GameSession session) { lock (mutex) { connectingSessions.Remove(session); - sessions.Remove(session.CharacterId); + + long cid = session.CharacterId; + if (cid == 0) return; + + // 关键:只允许“当前登记在 sessions 里的那个会话”删除自己 + // 迁移/换图时会出现:新会话先注册,旧会话后断开 + // 如果这里无条件 Remove,会把新会话也删掉 => Heartbeat unknown => 假离线 + if (sessions.TryGetValue(cid, out GameSession? current) && ReferenceEquals(current, session)) { + sessions.Remove(cid); + } } } - public bool GetSession(long characterId, [NotNullWhen(true)] out GameSession? session) { lock (mutex) { return sessions.TryGetValue(characterId, out session); diff --git a/Maple2.Server.Game/Manager/DungeonManager.cs b/Maple2.Server.Game/Manager/DungeonManager.cs index 98d8dcfe4..83a1697ab 100644 --- a/Maple2.Server.Game/Manager/DungeonManager.cs +++ b/Maple2.Server.Game/Manager/DungeonManager.cs @@ -437,7 +437,7 @@ void GetClearDungeonRewards() { ICollection items = []; if (rewardMetadata.UnlimitedDropBoxIds.Length > 0) { foreach (int boxId in rewardMetadata.UnlimitedDropBoxIds) { - items = items.Concat(Lobby.ItemDrop.GetIndividualDropItems(boxId)).ToList(); + items = items.Concat(Lobby.ItemDrop.GetIndividualDropItems(session, session.Player.Value.Character.Level, boxId)).ToList(); items = items.Concat(Lobby.ItemDrop.GetGlobalDropItems(boxId, Metadata.Level)).ToList(); } } diff --git a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs index 8703ee615..93a8fff78 100644 --- a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs +++ b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs @@ -21,6 +21,7 @@ public sealed class AgentNavigation { public List? currentPath = []; private int currentPathIndex = 0; private float currentPathProgress = 0; + private bool currentPathIsFallback = false; public AgentNavigation(FieldNpc fieldNpc, DtCrowdAgent dtAgent, DtCrowd dtCrowd) { npc = fieldNpc; @@ -158,6 +159,10 @@ public AgentNavigation(FieldNpc fieldNpc, DtCrowdAgent dtAgent, DtCrowd dtCrowd) public bool HasPath => currentPath != null && currentPath.Count > 0; + private static bool IsVayarianGuardian(int npcId) { + return npcId is 21500420 or 21500422 or 21500423 or 21500424; + } + public bool UpdatePosition() { if (!field.FindNearestPoly(npc.Position, out _, out RcVec3f position)) { Logger.Error("Failed to find valid position from {Source} => {Position}", npc.Position, position); @@ -216,6 +221,18 @@ public Vector3 GetAgentPosition() { currentPathProgress += (float) timeSpan.TotalSeconds * speed / distance; RcVec3f end = RcVec3f.Lerp(currentPath[currentPathIndex], currentPath[currentPathIndex + 1], currentPathProgress); + if (currentPathIsFallback) { + // Keep the end point on the navmesh each tick; prevents straight-line fallback from pushing NPCs through walls/out of bounds. + if (field.FindNearestPoly(end, out _, out RcVec3f snappedEnd)) { + end = snappedEnd; + if (currentPath is not null && currentPathIndex + 1 < currentPath.Count) { + currentPath[currentPathIndex + 1] = snappedEnd; + } + } else { + // Can't stay on mesh; stop movement. + currentPath = null; + } + } return (start, DotRecastHelper.FromNavMeshSpace(end)); } @@ -294,14 +311,45 @@ public bool PathTo(Vector3 goal) { private bool SetPathTo(RcVec3f target) { currentPath = []; currentPathIndex = 0; + currentPathIsFallback = false; currentPathProgress = 0; + try { - currentPath = FindPath(agent.npos, target); + List? path = FindPath(agent.npos, target); + if (path is null) { + // Clamp target to nearest navmesh polygon to avoid drifting outside walkable area. + if (!field.FindNearestPoly(target, out _, out RcVec3f snappedTarget)) { + Logger.Warning("[Fallback] Could not clamp target to navmesh. Target(nav)={Target}", target); + return false; + } + target = snappedTarget; + Vector3 worldTarget = DotRecastHelper.FromNavMeshSpace(target); + + if (IsVayarianGuardian(npc.Value.Id)) { + // Scripted NPC jump/warp: advance trigger even when navmesh has no valid path. + Logger.Warning("No path for Vayarian Guardian {NpcId}; snapping from {From} to {To}", npc.Value.Id, npc.Position, worldTarget); + UpdatePosition(worldTarget); + currentPath = null; + return true; + } + + Logger.Warning("Failed to find path from {FromNav} to {ToNav}; fallback: clamped straight-line on navmesh. World(from)={FromWorld} World(to)={ToWorld}", agent.npos, target, DotRecastHelper.FromNavMeshSpace(agent.npos), worldTarget); + currentPath = [agent.npos, target]; + return true; + } + + currentPath = path; + return true; } catch (Exception ex) { Logger.Error(ex, "Failed to find path to {Target}", target); + return false; } - - return currentPath is not null; + } + public void ClearPath() { + currentPath?.Clear(); + currentPathIndex = 0; + currentPathProgress = 0; + currentPathIsFallback = false; } public bool PathAwayFrom(Vector3 goal, int distance) { @@ -315,11 +363,20 @@ public bool PathAwayFrom(Vector3 goal, int distance) { // get distance in navmesh space float fDistance = distance * DotRecastHelper.MapRotation.GetRightAxis().Length(); - // get direction from agent to target - RcVec3f direction = RcVec3f.Normalize(RcVec3f.Subtract(target, position)); + // get direction from agent to target (guard against zero-length vectors which would produce NaNs) + RcVec3f delta = RcVec3f.Subtract(target, position); + float lenSqr = RcVec3f.Dot(delta, delta); + RcVec3f direction; + if (lenSqr > 1e-6f) { + direction = RcVec3f.Normalize(delta); + } else { + // If goal overlaps agent (common during melee hit / on-hit stepback), choose a stable fallback direction. + // Alternate per-object id so mobs don't all backstep the same way. + direction = (npc.ObjectId & 1) == 0 ? new RcVec3f(1, 0, 0) : new RcVec3f(-1, 0, 0); + } - // get the point that is fDistance away from the target in the opposite direction - RcVec3f positionAway = RcVec3f.Add(position, RcVec3f.Normalize(direction) * -fDistance); + // get the point that is fDistance away from the agent in the opposite direction + RcVec3f positionAway = RcVec3f.Add(position, direction * -fDistance); // find the nearest poly to the positionAway if (!field.FindNearestPoly(positionAway, out _, out RcVec3f positionAwayNavMesh)) { @@ -328,17 +385,35 @@ public bool PathAwayFrom(Vector3 goal, int distance) { return SetPathAway(positionAwayNavMesh); } - private bool SetPathAway(RcVec3f target) { currentPath = []; currentPathIndex = 0; + currentPathIsFallback = false; currentPathProgress = 0; + try { - currentPath = FindPath(agent.npos, target); + List? path = FindPath(agent.npos, target); + if (path is null) { + Vector3 worldTarget = DotRecastHelper.FromNavMeshSpace(target); + + if (IsVayarianGuardian(npc.Value.Id)) { + // Scripted NPC jump/warp: advance trigger even when navmesh has no valid path. + Logger.Warning("No path for Vayarian Guardian {NpcId}; snapping from {From} to {To}", npc.Value.Id, npc.Position, worldTarget); + UpdatePosition(worldTarget); + currentPath = null; + return true; + } + + Logger.Warning("Failed to find path from {FromNav} to {ToNav}; fallback: clamped straight-line on navmesh. World(from)={FromWorld} World(to)={ToWorld}", agent.npos, target, DotRecastHelper.FromNavMeshSpace(agent.npos), worldTarget); + currentPath = [agent.npos, target]; + return true; + } + + currentPath = path; + return true; } catch (Exception ex) { - Logger.Error(ex, "Failed to find path away from {Target}", target); + Logger.Error(ex, "Failed to find path to {Target}", target); + return false; } - - return currentPath is not null; } } diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index a68abcf28..986d7b3a6 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -46,6 +46,11 @@ public partial class FieldManager { private readonly ConcurrentDictionary fieldItems = new(); private readonly ConcurrentDictionary fieldMobSpawns = new(); private readonly ConcurrentDictionary fieldSpawnPointNpcs = new(); + + + // Tracks spawnPointIds that have died and may have been removed from Mobs/Npcs dictionaries. + // This allows triggers like MonsterDead(spawnId) to continue working even after the dead NPC is despawned. + private readonly ConcurrentDictionary deadSpawnPoints = new(); private readonly ConcurrentDictionary fieldPlayerSpawnPoints = new(); private readonly ConcurrentDictionary fieldSpawnGroups = new(); private readonly ConcurrentDictionary fieldSkills = new(); @@ -115,6 +120,11 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId } public FieldNpc? SpawnNpc(NpcMetadata npc, Vector3 position, Vector3 rotation, bool disableAi = false, FieldMobSpawn? owner = null, SpawnPointNPC? spawnPointNpc = null, string spawnAnimation = "") { + + if (spawnPointNpc is not null) { + deadSpawnPoints.TryRemove(spawnPointNpc.SpawnPointId, out _); + } + // Apply random offset if SpawnRadius is set Vector3 spawnPosition = position; if (spawnPointNpc?.SpawnRadius > 0) { @@ -525,6 +535,8 @@ public void DespawnWorldBoss() { RemoveNpc(objectId); } + public bool IsSpawnPointDead(int spawnId) => deadSpawnPoints.ContainsKey(spawnId); + public void ToggleNpcSpawnPoint(int spawnId) { List spawns = fieldSpawnPointNpcs.Values.Where(spawn => spawn.Value.SpawnPointId == spawnId).ToList(); foreach (FieldSpawnPointNpc spawn in spawns) { @@ -951,6 +963,10 @@ public bool RemoveNpc(int objectId, TimeSpan removeDelay = default) { } Broadcast(FieldPacket.RemoveNpc(objectId)); Broadcast(ProxyObjectPacket.RemoveNpc(objectId)); + + if (npc.IsDead && npc.SpawnPointId != 0) { + deadSpawnPoints.TryAdd(npc.SpawnPointId, 1); + } npc.Dispose(); }, removeDelay); return true; diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs index 4e6cd94fc..3e09c844b 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs @@ -391,13 +391,30 @@ public bool FindNearestPoly(RcVec3f point, out long nearestRef, out RcVec3f posi return false; } - DtStatus status = Navigation.Crowd.GetNavMeshQuery().FindNearestPoly(point, new RcVec3f(2, 4, 2), Navigation.Crowd.GetFilter(0), out nearestRef, out position, out _); - if (status.Failed()) { - logger.Warning("Failed to find nearest poly from position {Source} in field {MapId}", point, MapId); - return false; + // Try progressively larger search extents; helps when an entity gets pushed off the navmesh (e.g., knockback). + // Keep the first extent small for performance, then widen only on failure. + ReadOnlySpan extentsList = stackalloc RcVec3f[] { + new RcVec3f(2, 4, 2), + new RcVec3f(8, 16, 8), + new RcVec3f(32, 64, 32), + new RcVec3f(128, 256, 128), + }; + + DtNavMeshQuery query = Navigation.Crowd.GetNavMeshQuery(); + IDtQueryFilter filter = Navigation.Crowd.GetFilter(0); + + for (int i = 0; i < extentsList.Length; i++) { + RcVec3f ext = extentsList[i]; + DtStatus status = query.FindNearestPoly(point, ext, filter, out nearestRef, out position, out _); + if (status.Succeeded() && nearestRef != 0) { + return true; + } } - return true; + logger.Warning("Failed to find nearest poly from position {Source} in field {MapId}", point, MapId); + nearestRef = 0; + position = default; + return false; } public bool TryGetPlayerById(long characterId, [NotNullWhen(true)] out FieldPlayer? player) { diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs b/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs index 3b3c93eb3..2bdcbc5ba 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs @@ -95,6 +95,8 @@ public virtual void Init() { } public bool LiftupCube(in Vector3B coordinates, [NotNullWhen(true)] out LiftupWeapon? liftupWeapon); public void MovePlayerAlongPath(string pathName); + public bool IsSpawnPointDead(int spawnId); + public bool RemoveNpc(int objectId, TimeSpan removeDelay = default); public bool RemovePet(int objectId, TimeSpan removeDelay = default); } diff --git a/Maple2.Server.Game/Manager/Field/TriggerCollection.cs b/Maple2.Server.Game/Manager/Field/TriggerCollection.cs index b76c198c3..1efa4ec9e 100644 --- a/Maple2.Server.Game/Manager/Field/TriggerCollection.cs +++ b/Maple2.Server.Game/Manager/Field/TriggerCollection.cs @@ -1,36 +1,55 @@ using System.Collections; +using System.Collections.Generic; using Maple2.Model.Game; using Maple2.Model.Metadata; namespace Maple2.Server.Game.Manager.Field; public sealed class TriggerCollection : IReadOnlyCollection { - public readonly IReadOnlyDictionary Actors; - public readonly IReadOnlyDictionary Cameras; - public readonly IReadOnlyDictionary Cubes; - public readonly IReadOnlyDictionary Effects; - public readonly IReadOnlyDictionary Ladders; - public readonly IReadOnlyDictionary Meshes; - public readonly IReadOnlyDictionary Ropes; - public readonly IReadOnlyDictionary Sounds; - public readonly IReadOnlyDictionary Agents; - - public readonly IReadOnlyDictionary Boxes; + // NOTE: These collections need to be mutable at runtime. + // Some map packs / DB imports are missing Ms2TriggerMesh / Ms2TriggerAgent entries, + // but trigger scripts still reference those IDs (set_mesh / set_agent). + // The client already knows the objects by triggerId (from the map file), and only + // needs server updates to toggle visibility/collision. + // + // We therefore support creating lightweight placeholder trigger objects on demand. + private readonly Dictionary actors; + private readonly Dictionary cameras; + private readonly Dictionary cubes; + private readonly Dictionary effects; + private readonly Dictionary ladders; + private readonly Dictionary meshes; + private readonly Dictionary ropes; + private readonly Dictionary sounds; + private readonly Dictionary agents; + private readonly Dictionary boxes; + + public IReadOnlyDictionary Actors => actors; + public IReadOnlyDictionary Cameras => cameras; + public IReadOnlyDictionary Cubes => cubes; + public IReadOnlyDictionary Effects => effects; + public IReadOnlyDictionary Ladders => ladders; + public IReadOnlyDictionary Meshes => meshes; + public IReadOnlyDictionary Ropes => ropes; + public IReadOnlyDictionary Sounds => sounds; + public IReadOnlyDictionary Agents => agents; + + public IReadOnlyDictionary Boxes => boxes; // These seem to get managed separately... // private readonly IReadOnlyDictionary Agents; // private readonly IReadOnlyDictionary Skills; public TriggerCollection(MapEntityMetadata entities) { - Dictionary actors = new(); - Dictionary cameras = new(); - Dictionary cubes = new(); - Dictionary effects = new(); - Dictionary ladders = new(); - Dictionary meshes = new(); - Dictionary ropes = new(); - Dictionary sounds = new(); - Dictionary agents = new(); + actors = new(); + cameras = new(); + cubes = new(); + effects = new(); + ladders = new(); + meshes = new(); + ropes = new(); + sounds = new(); + agents = new(); foreach (Ms2TriggerActor actor in entities.Trigger.Actors) { actors[actor.TriggerId] = new TriggerObjectActor(actor); @@ -61,36 +80,55 @@ public TriggerCollection(MapEntityMetadata entities) { agents[agent.TriggerId] = new TriggerObjectAgent(agent); } - Actors = actors; - Cameras = cameras; - Cubes = cubes; - Effects = effects; - Ladders = ladders; - Meshes = meshes; - Ropes = ropes; - Sounds = sounds; - Agents = agents; - - Dictionary boxes = new(); + boxes = new(); foreach (Ms2TriggerBox box in entities.Trigger.Boxes) { boxes[box.TriggerId] = new TriggerBox(box); } + } + + /// + /// Creates or retrieves a Trigger Mesh placeholder. Useful when a trigger script references + /// a mesh id that is missing from the DB import. + /// + public TriggerObjectMesh GetOrAddMesh(int triggerId) { + if (meshes.TryGetValue(triggerId, out TriggerObjectMesh? mesh)) { + return mesh; + } + + // Scale/minimapInvisible are not important for updates; the client already has the actual mesh. + Ms2TriggerMesh meta = new Ms2TriggerMesh(1f, triggerId, Visible: true, MinimapInvisible: false); + mesh = new TriggerObjectMesh(meta); + meshes[triggerId] = mesh; + return mesh; + } + + /// + /// Creates or retrieves a Trigger Agent placeholder. Useful when a trigger script references + /// an agent id that is missing from the DB import. + /// + public TriggerObjectAgent GetOrAddAgent(int triggerId) { + if (agents.TryGetValue(triggerId, out TriggerObjectAgent? agent)) { + return agent; + } - Boxes = boxes; + Ms2TriggerAgent meta = new Ms2TriggerAgent(triggerId, Visible: true); + agent = new TriggerObjectAgent(meta); + agents[triggerId] = agent; + return agent; } - public int Count => Actors.Count + Cameras.Count + Cubes.Count + Effects.Count + Ladders.Count + Meshes.Count + Ropes.Count + Sounds.Count + Agents.Count; + public int Count => actors.Count + cameras.Count + cubes.Count + effects.Count + ladders.Count + meshes.Count + ropes.Count + sounds.Count + agents.Count; public IEnumerator GetEnumerator() { - foreach (TriggerObjectActor actor in Actors.Values) yield return actor; - foreach (TriggerObjectCamera camera in Cameras.Values) yield return camera; - foreach (TriggerObjectCube cube in Cubes.Values) yield return cube; - foreach (TriggerObjectEffect effect in Effects.Values) yield return effect; - foreach (TriggerObjectLadder ladder in Ladders.Values) yield return ladder; - foreach (TriggerObjectMesh mesh in Meshes.Values) yield return mesh; - foreach (TriggerObjectRope rope in Ropes.Values) yield return rope; - foreach (TriggerObjectSound sound in Sounds.Values) yield return sound; - foreach (TriggerObjectAgent agent in Agents.Values) yield return agent; + foreach (TriggerObjectActor actor in actors.Values) yield return actor; + foreach (TriggerObjectCamera camera in cameras.Values) yield return camera; + foreach (TriggerObjectCube cube in cubes.Values) yield return cube; + foreach (TriggerObjectEffect effect in effects.Values) yield return effect; + foreach (TriggerObjectLadder ladder in ladders.Values) yield return ladder; + foreach (TriggerObjectMesh mesh in meshes.Values) yield return mesh; + foreach (TriggerObjectRope rope in ropes.Values) yield return rope; + foreach (TriggerObjectSound sound in sounds.Values) yield return sound; + foreach (TriggerObjectAgent agent in agents.Values) yield return agent; } IEnumerator IEnumerable.GetEnumerator() { diff --git a/Maple2.Server.Game/Manager/FishingManager.cs b/Maple2.Server.Game/Manager/FishingManager.cs index aebd61425..60c5b94a4 100644 --- a/Maple2.Server.Game/Manager/FishingManager.cs +++ b/Maple2.Server.Game/Manager/FishingManager.cs @@ -336,6 +336,21 @@ private void AddFish(int fishSize, bool hasAutoFish) { session.Field.Broadcast(FishingPacket.PrizeFish(session.PlayerName, selectedFish.Id)); } session.ConditionUpdate(ConditionType.fish, codeLong: selectedFish.Id, targetLong: session.Field.MapId); + // ====== 钓鱼给角色经验 ====== + long charExp = selectedFish.Exp; + + // 可选倍率:首次 / 奖牌鱼 + if (caughtFishType == CaughtFishType.FirstKind) { + charExp *= 2; + } + if (caughtFishType == CaughtFishType.Prize) { + charExp *= 2; + } + + if (charExp > 0) { + session.Exp.AddExp(ExpType.fishing, modifier: 0f, additionalExp: charExp); + } + // ====== 角色经验结束 ====== if (masteryExp == 0) { return; diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs index 2eb377053..ce570eb6f 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs @@ -41,8 +41,17 @@ protected override void TaskFinished(bool isCompleted) { private void SkillCastFaceTarget(SkillRecord cast, IActor target, int faceTarget) { Vector3 offset = target.Position - actor.Position; - float distance = offset.LengthSquared(); - + float distance = offset.X * offset.X + offset.Y * offset.Y; + float vertical = MathF.Abs(offset.Z); + + // In MS2 data, many AI skill nodes use FaceTarget=0 while the skill's AutoTargeting.MaxDegree + // is a narrow cone. If we gate turning by MaxDegree (dot product), the NPC can end up casting + // while facing away (target behind the cone) and never correct its facing. + // + // To avoid "背对玩家放技能", we always rotate to face the current target when: + // - the motion requests FaceTarget, and + // - AutoTargeting distance/height constraints allow it. + // MaxDegree is ignored for *turning*. if (faceTarget != 1) { if (!cast.Motion.MotionProperty.FaceTarget || cast.Metadata.Data.AutoTargeting is null) { return; @@ -50,26 +59,28 @@ private void SkillCastFaceTarget(SkillRecord cast, IActor target, int faceTarget var autoTargeting = cast.Metadata.Data.AutoTargeting; - bool shouldFaceTarget = autoTargeting.MaxDistance == 0 || distance <= autoTargeting.MaxDistance; - shouldFaceTarget |= autoTargeting.MaxHeight == 0 || offset.Y <= autoTargeting.MaxHeight; + bool inRange = autoTargeting.MaxDistance == 0 || distance <= autoTargeting.MaxDistance * autoTargeting.MaxDistance; + inRange &= autoTargeting.MaxHeight == 0 || vertical <= autoTargeting.MaxHeight; + if (!inRange) { + return; + } - if (!shouldFaceTarget) { + if (distance < 0.0001f) { return; } distance = (float) Math.Sqrt(distance); offset *= (1 / distance); + } else { + if (distance < 0.0001f) { - float degreeCosine = (float) Math.Cos(autoTargeting.MaxDegree / 2); - float dot = Vector3.Dot(offset, actor.Transform.FrontAxis); - - shouldFaceTarget = autoTargeting.MaxDegree == 0 || dot >= degreeCosine; - - if (!shouldFaceTarget) { return; + } - } else { + + distance = (float) Math.Sqrt(distance); + offset *= (1 / distance); } @@ -104,9 +115,12 @@ private void SkillCast(NpcSkillCastTask task, int id, short level, long uid, byt } if (task.FacePos != new Vector3(0, 0, 0)) { - actor.Transform.LookTo(Vector3.Normalize(task.FacePos - actor.Position)); + actor.Transform.LookTo(task.FacePos - actor.Position); // safe: LookTo normalizes with guards } else if (actor.BattleState.Target is not null) { - SkillCastFaceTarget(cast, actor.BattleState.Target, task.FaceTarget); + // Hard guarantee: NPCs should always face their current battle target when casting. + // Some boss skills have MotionProperty.FaceTarget=false or narrow AutoTargeting degrees, + // which previously allowed casting while facing away. + actor.Transform.LookTo(actor.BattleState.Target.Position - actor.Position); // safe: LookTo normalizes with guards } CastTask = task; diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs index 6e53abf3d..4d6577958 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs @@ -54,7 +54,15 @@ public void SkillCastAttack(SkillRecord cast, byte attackPoint, List att if (attackTargets.Count > targetIndex) { // if attack.direction == 3, use direction to target, if attack.direction == 0, use rotation maybe? cast.Position = actor.Position; - cast.Direction = Vector3.Normalize(attackTargets[targetIndex].Position - actor.Position); + Vector3 dir = attackTargets[targetIndex].Position - actor.Position; + if (float.IsNaN(dir.X) || float.IsNaN(dir.Y) || float.IsNaN(dir.Z) || + float.IsInfinity(dir.X) || float.IsInfinity(dir.Y) || float.IsInfinity(dir.Z) || + dir.LengthSquared() < 1e-6f) { + // Keep current facing if target is on top of caster. + cast.Direction = actor.Transform.FrontAxis; + } else { + cast.Direction = Vector3.Normalize(dir); + } } actor.Field.Broadcast(SkillDamagePacket.Target(cast, targets)); diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index 4208ae688..42874181e 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -197,6 +197,15 @@ public override void Update(long tickCount) { playersListeningToDebug = playersListeningToDebugNow; + // Defensive: if any upstream system produced a non-finite position, recover instead of + // crashing during packet serialization (Vector3 -> Vector3S conversion can overflow on NaN). + if (!float.IsFinite(Position.X) || !float.IsFinite(Position.Y) || !float.IsFinite(Position.Z)) { + // Recover instead of crashing during packet serialization. + Position = Origin; + Navigation?.ClearPath(); + SendControl = true; + } + if (SendControl && !IsDead) { SequenceCounter++; Field.BroadcastNpcControl(this); diff --git a/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs b/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs index fa7b3c5d7..c98bb5ffa 100644 --- a/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs +++ b/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs @@ -29,7 +29,16 @@ public class FieldSkill : FieldEntity { private readonly long endTick; public long NextTick { get; private set; } private readonly ILogger logger = Log.ForContext(); - + private static Vector3 SanitizePosition(Vector3 v) { + if (float.IsNaN(v.X) || float.IsNaN(v.Y) || float.IsNaN(v.Z) || + float.IsInfinity(v.X) || float.IsInfinity(v.Y) || float.IsInfinity(v.Z)) { + return Vector3.Zero; + } + v.X = Math.Clamp(v.X, short.MinValue, short.MaxValue); + v.Y = Math.Clamp(v.Y, short.MinValue, short.MaxValue); + v.Z = Math.Clamp(v.Z, short.MinValue, short.MaxValue); + return v; + } private ByteWriter GetDamagePacket(DamageRecord record) => Source switch { SkillSource.Cube => SkillDamagePacket.Tile(record), _ => SkillDamagePacket.Region(record), @@ -126,13 +135,13 @@ public override void Update(long tickCount) { SkillMetadataAttack attack = record.Attack; record.TargetUid++; var damage = new DamageRecord(record.Metadata, attack) { - CasterId = ObjectId, - OwnerId = ObjectId, - SkillId = Value.Id, - Level = Value.Level, + CasterId = record.Caster.ObjectId, + OwnerId = record.Caster.ObjectId, + SkillId = record.SkillId, + Level = record.Level, MotionPoint = record.MotionPoint, AttackPoint = record.AttackPoint, - Position = Position, + Position = SanitizePosition(Position), Direction = Rotation, }; var targetRecords = new List(); @@ -215,6 +224,7 @@ public override void Update(long tickCount) { if (targetRecords.Count > 0) { Field.Broadcast(SkillDamagePacket.Target(record, targetRecords)); Field.Broadcast(GetDamagePacket(damage)); + Field.Broadcast(SkillDamagePacket.Damage(damage)); } Caster.ApplyEffects(attack.SkillsOnDamage, Caster, damage, targets: targets); diff --git a/Maple2.Server.Game/Packets/SkillDamagePacket.cs b/Maple2.Server.Game/Packets/SkillDamagePacket.cs index e7f79fea7..fdef75d66 100644 --- a/Maple2.Server.Game/Packets/SkillDamagePacket.cs +++ b/Maple2.Server.Game/Packets/SkillDamagePacket.cs @@ -20,7 +20,19 @@ private enum Command : byte { Unknown7 = 7, Unknown8 = 8, } - + private static void WriteVector3SSafe(ByteWriter w, Vector3 v) { + if (float.IsNaN(v.X) || float.IsNaN(v.Y) || float.IsNaN(v.Z) || + float.IsInfinity(v.X) || float.IsInfinity(v.Y) || float.IsInfinity(v.Z)) { + v = Vector3.Zero; + } + + // 注意:4200 没问题,但 NaN/Inf 必须先处理,否则 Clamp 也救不了 + v.X = Math.Clamp(v.X, short.MinValue, short.MaxValue); + v.Y = Math.Clamp(v.Y, short.MinValue, short.MaxValue); + v.Z = Math.Clamp(v.Z, short.MinValue, short.MaxValue); + + w.Write(v); + } public static ByteWriter Target(SkillRecord record, ICollection targets) { var pWriter = Packet.Of(SendOp.SkillDamage); pWriter.Write(Command.Target); @@ -30,7 +42,7 @@ public static ByteWriter Target(SkillRecord record, ICollection ta pWriter.WriteShort(record.Level); pWriter.WriteByte(record.MotionPoint); pWriter.WriteByte(record.AttackPoint); - pWriter.Write(record.Position); // Impact + WriteVector3SSafe(pWriter, record.Position); // Impact pWriter.Write(record.Direction); // Impact pWriter.WriteBool(true); // SkillId:10600211 only pWriter.WriteInt(record.ServerTick); @@ -53,8 +65,8 @@ public static ByteWriter Damage(DamageRecord record) { pWriter.WriteShort(record.Level); pWriter.WriteByte(record.MotionPoint); pWriter.WriteByte(record.AttackPoint); - pWriter.Write(record.Position); // Impact - pWriter.Write(record.Direction); + WriteVector3SSafe(pWriter, record.Position); // Impact + WriteVector3SSafe(pWriter, record.Direction); pWriter.WriteByte((byte) record.Targets.Count); foreach (DamageRecordTarget target in record.Targets.Values) { diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Field.cs b/Maple2.Server.Game/Trigger/TriggerContext.Field.cs index c925fbc1a..4855533cb 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Field.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Field.cs @@ -144,9 +144,9 @@ public void SetActor(int triggerId, bool visible, string initialSequence, bool a public void SetAgent(int[] triggerIds, bool visible) { WarnLog("[SetAgent] triggerIds:{Ids}, visible:{Visible}", string.Join(", ", triggerIds), visible); foreach (int triggerId in triggerIds) { - if (!Objects.Agents.TryGetValue(triggerId, out TriggerObjectAgent? agent)) { - continue; - } + // Some data packs are missing Ms2TriggerAgent entries in DB import. + // Create a lightweight placeholder so the client can still receive updates. + TriggerObjectAgent agent = Objects.GetOrAddAgent(triggerId); agent.Visible = visible; Broadcast(TriggerPacket.Update(agent)); @@ -275,10 +275,9 @@ public void SetRandomMesh(int[] triggerIds, bool visible, int startDelay, int in private void UpdateMesh(ArraySegment triggerIds, bool visible, int delay, int interval, int fade = 0) { int intervalTotal = 0; foreach (int triggerId in triggerIds) { - if (!Objects.Meshes.TryGetValue(triggerId, out TriggerObjectMesh? mesh)) { - logger.Warning("Invalid mesh: {Id}", triggerId); - continue; - } + // Some data packs are missing Ms2TriggerMesh entries in DB import. + // Create a lightweight placeholder so the client can still receive updates. + TriggerObjectMesh mesh = Objects.GetOrAddMesh(triggerId); if (mesh.Visible == visible) { continue; } diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs b/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs index 3faacf127..67c8a75bc 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs @@ -211,13 +211,27 @@ public bool MonsterDead(int[] spawnIds, bool autoTarget) { DebugLog("[MonsterDead] spawnIds:{SpawnIds}, arg2:{Arg2}", string.Join(", ", spawnIds), autoTarget); IEnumerable matchingMobs = Field.Mobs.Values.Where(x => spawnIds.Contains(x.SpawnPointId)); + // If no mobs currently exist for these spawnIds, we may still want to treat them as dead + // (e.g. boss already died and was despawned). We use Field.IsSpawnPointDead to track that. + // However, if a spawnId has never existed and is not marked dead, it should NOT be treated as dead + // (prevents instant fail when an NPC failed to spawn). + + foreach (FieldNpc mob in matchingMobs) { if (!mob.IsDead) { return false; } } - // Either no mobs were found or they are all dead + // If we found at least one matching mob, reaching here means all of them are dead. + // If we found none, then only consider the spawnIds dead if they are marked as dead by the field. + if (!matchingMobs.Any()) { + foreach (int spawnId in spawnIds) { + if (!Field.IsSpawnPointDead(spawnId)) { + return false; + } + } + } return true; } diff --git a/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs b/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs index 751100069..6e1271e6a 100644 --- a/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs +++ b/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs @@ -83,10 +83,23 @@ public void Listen(long characterId, PlayerInfoListener listener) { } public void SendUpdate(PlayerUpdateRequest request) { - try { - //PlayerInfoCache - world.UpdatePlayer(request); - } catch (RpcException) { /* ignored */ } + // 对“上线态/频道/地图”更新非常关键,不能静默吞掉失败 + const int maxRetries = 3; + for (int i = 0; i < maxRetries; i++) { + try { + world.UpdatePlayer(request); + return; + } catch (RpcException ex) { + logger.Warning("SendUpdate(UpdatePlayer) failed attempt {Attempt}/{Max}. Status={Status}", + i + 1, maxRetries, ex.Status); + + // 小退避,避免瞬间打爆 + Thread.Sleep(200 * (i + 1)); + } catch (Exception ex) { + logger.Warning(ex, "SendUpdate(UpdatePlayer) unexpected failure"); + Thread.Sleep(200 * (i + 1)); + } + } } public bool ReceiveUpdate(PlayerUpdateRequest request) { diff --git a/Maple2.Server.World/Service/WorldService.Migrate.cs b/Maple2.Server.World/Service/WorldService.Migrate.cs index ab28813eb..ef24074a2 100644 --- a/Maple2.Server.World/Service/WorldService.Migrate.cs +++ b/Maple2.Server.World/Service/WorldService.Migrate.cs @@ -21,6 +21,7 @@ public override Task MigrateOut(MigrateOutRequest request, S switch (request.Server) { case Server.Login: var loginEntry = new TokenEntry(request.Server, request.AccountId, request.CharacterId, new Guid(request.MachineId), 0, 0, 0, 0, 0, MigrationType.Normal); + worldServer.MarkMigrating(request.CharacterId, 45); tokenCache.Set(token, loginEntry, AuthExpiry); return Task.FromResult(new MigrateOutResponse { IpAddress = Target.LoginIp.ToString(), @@ -32,21 +33,26 @@ public override Task MigrateOut(MigrateOutRequest request, S throw new RpcException(new Status(StatusCode.Unavailable, $"No available game channels")); } - // Try to use requested channel or instanced channel - if (request.InstancedContent && channelClients.TryGetInstancedChannelId(out int channel)) { + // Channel selection priority: + // 1) If the Game server specifies a channel, keep it (even for instanced content). + // This avoids routing players to a dedicated/invalid instanced channel (often 0), + // which can desync presence and cause "fake offline" (client shows 65535). + // 2) Otherwise, if instanced content is requested, use an instanced channel if available. + // 3) Fallback to the first available channel. + + int channel; + if (request.HasChannel && channelClients.TryGetActiveEndpoint(request.Channel, out _)) { + channel = request.Channel; + } else if (request.InstancedContent && channelClients.TryGetInstancedChannelId(out channel)) { if (!channelClients.TryGetActiveEndpoint(channel, out _)) { throw new RpcException(new Status(StatusCode.Unavailable, "No available instanced game channel")); } - } else if (request.HasChannel && channelClients.TryGetActiveEndpoint(request.Channel, out _)) { - channel = request.Channel; } else { - // Fall back to first available channel channel = channelClients.FirstChannel(); if (channel == -1) { throw new RpcException(new Status(StatusCode.Unavailable, "No available game channels")); } } - if (!channelClients.TryGetActiveEndpoint(channel, out IPEndPoint? endpoint)) { throw new RpcException(new Status(StatusCode.Unavailable, $"Channel {channel} not found")); } @@ -77,6 +83,7 @@ public override Task MigrateIn(MigrateInRequest request, Serv } tokenCache.Remove(request.Token); + worldServer.ClearMigrating(data.CharacterId); return Task.FromResult(new MigrateInResponse { CharacterId = data.CharacterId, Channel = data.Channel, diff --git a/Maple2.Server.World/WorldServer.cs b/Maple2.Server.World/WorldServer.cs index e283c32de..9bd131cc1 100644 --- a/Maple2.Server.World/WorldServer.cs +++ b/Maple2.Server.World/WorldServer.cs @@ -30,6 +30,7 @@ public class WorldServer { private readonly EventQueue scheduler; private readonly CancellationTokenSource tokenSource = new(); private readonly ConcurrentDictionary memoryStringBoards; + private readonly System.Collections.Concurrent.ConcurrentDictionary _migratingUntil = new(); private static int _globalIdCounter; private readonly ILogger logger = Log.ForContext(); @@ -123,7 +124,31 @@ public void SetOffline(PlayerInfo playerInfo) { Async = true, }); } + public void MarkMigrating(long characterId, int seconds = 45) { + if (characterId == 0) return; + long until = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + seconds; + _migratingUntil[characterId] = until; + } + + public void ClearMigrating(long characterId) { + if (characterId == 0) return; + _migratingUntil.TryRemove(characterId, out _); + } + + private bool IsMigrating(long characterId) { + if (characterId == 0) return false; + if (!_migratingUntil.TryGetValue(characterId, out long until)) { + return false; + } + + long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + if (now <= until) return true; + + // expired + _migratingUntil.TryRemove(characterId, out _); + return false; + } private void Loop() { while (!tokenSource.Token.IsCancellationRequested) { try { diff --git a/Maple2.Tools/VectorMath/Transform.cs b/Maple2.Tools/VectorMath/Transform.cs index 65430220c..cd2a222e4 100644 --- a/Maple2.Tools/VectorMath/Transform.cs +++ b/Maple2.Tools/VectorMath/Transform.cs @@ -138,18 +138,44 @@ public void LookTo(Vector3 direction, bool snapToGroundPlane = true) { } public void LookTo(Vector3 direction, Vector3 up, bool snapToGroundPlane = true) { + // Defensive normalization: callers sometimes pass a zero vector (or already-invalid values), + // which would turn into NaN via Vector3.Normalize and then poison FrontAxis/RightAxis/UpAxis. + // That can later corrupt movement (e.g., skill cast keyframe movement) and ultimately navmesh queries. + if (float.IsNaN(direction.X) || float.IsNaN(direction.Y) || float.IsNaN(direction.Z) || + float.IsInfinity(direction.X) || float.IsInfinity(direction.Y) || float.IsInfinity(direction.Z) || + direction.LengthSquared() < 1e-6f) { + direction = FrontAxis; + } + if (float.IsNaN(up.X) || float.IsNaN(up.Y) || float.IsNaN(up.Z) || + float.IsInfinity(up.X) || float.IsInfinity(up.Y) || float.IsInfinity(up.Z) || + up.LengthSquared() < 1e-6f) { + up = new Vector3(0, 0, 1); + } + direction = Vector3.Normalize(direction); up = Vector3.Normalize(up); if (snapToGroundPlane) { - direction = Vector3.Normalize(direction - Vector3.Dot(direction, up) * up); // plane projection formula - - if (direction.IsNearlyEqual(new Vector3(0, 0, 0), 1e-3f)) { - direction = FrontAxis; + // Project direction onto plane defined by up. + Vector3 projected = direction - Vector3.Dot(direction, up) * up; + if (float.IsNaN(projected.X) || float.IsNaN(projected.Y) || float.IsNaN(projected.Z) || + float.IsInfinity(projected.X) || float.IsInfinity(projected.Y) || float.IsInfinity(projected.Z) || + projected.LengthSquared() < 1e-6f) { + projected = FrontAxis; } + direction = Vector3.Normalize(projected); } Vector3 right = Vector3.Cross(direction, up); + if (float.IsNaN(right.X) || float.IsNaN(right.Y) || float.IsNaN(right.Z) || + float.IsInfinity(right.X) || float.IsInfinity(right.Y) || float.IsInfinity(right.Z) || + right.LengthSquared() < 1e-6f) { + // Fallback: keep current basis if cross product degenerated. + right = RightAxis.LengthSquared() < 1e-6f ? Vector3.UnitX : Vector3.Normalize(RightAxis); + } else { + right = Vector3.Normalize(right); + } + up = Vector3.Cross(right, direction); float scale = Scale; From dff14e7bb686812168b98617f9d83d3b2d9a2107 Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Sun, 1 Mar 2026 21:55:44 -0600 Subject: [PATCH 09/26] Update the constant parsing to make it more efficient by removing the nested foreach loop and indexing the property names to search them later when looping through the parsed XML constants data. --- .../Mapper/ServerTableMapper.cs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs index 2818ebe62..b43896b57 100644 --- a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs +++ b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs @@ -16,6 +16,7 @@ using System.Numerics; using System.Reflection; using System.Xml; +using Maple2.File.Parser.Xml.Table; using DayOfWeek = System.DayOfWeek; using ExpType = Maple2.Model.Enum.ExpType; using Fish = Maple2.File.Parser.Xml.Table.Server.Fish; @@ -2117,16 +2118,18 @@ void AddSpecial(Dictionary values, Dictionary propertyLookup = typeof(ConstantsTable).GetProperties() + .ToDictionary(p => p.Name.Trim(), p => p, StringComparer.OrdinalIgnoreCase); + + foreach ((string key, Constants.Key constant) in parser.ParseConstants()) { + string trimmedKey = key.Trim(); + if (!propertyLookup.TryGetValue(trimmedKey, out PropertyInfo? property)) continue; + string cleanValue = CleanConstantsInput( + constant.value.Trim(), + trimmedKey, + property.PropertyType + ); + SetValue(property, constants, cleanValue); } return constants; } From 5d6f08745f43c26bf17be70bfea57c24408abd35 Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Sun, 1 Mar 2026 22:55:58 -0600 Subject: [PATCH 10/26] Correct some misspelt property names in ConstantsTable record to allow them to retrieve their values. --- Maple2.Model/Metadata/Constants.cs | 3 ++ .../Metadata/ServerTable/ConstantsTable.cs | 50 +++++++++---------- Maple2.Server.Game/Manager/FishingManager.cs | 2 +- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index c609ce3f1..de0408079 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -293,6 +293,9 @@ public static class Constant { "http://maplestory2.nexon.net/en/news/article/32249/mushking-royale-championship-rewards"; public const int HoldAttackSkillID = 10700252; public const string DiscordAppID = "555204064091045904"; + public const int GuildFundMax = 20000; + public const int DropIconVisibleDistance = 400; + public const string MesoMarketTokenDetailUrl = "http://maplestory2.nexon.net/en/news/article/45213"; #endregion } diff --git a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs index 7eb6b6870..7da1b856f 100644 --- a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs +++ b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs @@ -31,7 +31,6 @@ public record ConstantsTable( int DamageDistance = 0, int TalkableDistance = 0, bool TalkableFrontOnly = false, - int DropIconVisibleDistance = 0, int ChatBalloonDistance = 0, int HpBarDistance = 0, int EmoticonVisibleDistance = 0, @@ -50,7 +49,7 @@ public record ConstantsTable( TimeSpan UgcHomeSaleWaitingTime = new(), int UgcContainerExpireDurationNormal = 0, int UgcContainerExpireDurationCash = 0, - int UgcContainerExpireDurationMeret = 0, + int UgcContainerExpireDurationMerat = 0, int UgcHomeExtensionNoticeDate = 0, int UgcHomePasswordExpireDuration = 0, bool CubeLiftHeightLimitUp = false, @@ -69,17 +68,17 @@ public record ConstantsTable( int DropItemPickUpCompleteRotateVel = 0, int ChatBalloonDuration = 0, int BoreWaitingTick = 0, - int OffsetPcHpBar = 0, - int OffsetPcNameTag = 0, - int OffsetPcChatBalloon = 0, - int OffsetPcDamageNumber = 0, + int OffsetPcHpbar = 0, + int OffsetPcNametag = 0, + int OffsetPcChatballoon = 0, + int OffsetPcDamagenumber = 0, int OffsetPcProfileTag = 0, float fOffsetOnTombstoneNameTag = 0f, int OffsetNpcHpBar = 0, - int OffsetNpcNameTag = 0, + int OffsetNpcNametag = 0, int OffsetNpcEmoticon = 0, int OffsetNpcChatBalloon = 0, - int OffsetNpcDamageNumber = 0, + int OffsetNpcDamagenumber = 0, int OffsetNpcMonologue = 0, int OffsetActionTooltipX = 0, int OffsetActionTooltipY = 0, @@ -113,7 +112,7 @@ public record ConstantsTable( int MonologueShowTime = 0, int ShowKillCountMin = 0, int UserRevivalInvincibleTick = 0, - int UserRevivalPenaltyPercent = 0, + int UserRevivalPaneltyPercent = 0, string UserRevivalIconPath = "", string UserRevivalInvincibleIconPath = "", int GetExpMinVelocity = 0, @@ -149,13 +148,13 @@ public record ConstantsTable( float VolumePcToBreakableObject = 0f, float VolumeNpcToPc = 0f, float VolumeOtherPc = 0f, - int ItemDropLevelMaxBoundary = 0, + int ItemDropLevelMaxBoundry = 0, float moneyTreeDropHeight = 0f, float moneyTreeDropBase = 0f, int moneyTreeDropRandom = 0, - int WhisperIgnoreTime = 0, - int WhisperMaxCount = 0, - int WhisperDurationTime = 0, + int WisperIgnoreTime = 0, + int WisperMaxCount = 0, + int WisperDurationTime = 0, float BossHpBarAutoDetectRange = 0f, float BossHpBarDuration = 0f, float FindHoldTargetRange = 0f, @@ -213,7 +212,7 @@ public record ConstantsTable( float TimeScalePCDuration = 0f, int GoToHomeCastingTime = 0, int returnHomeSkill = 0, - int returnHomeSkillMeret = 0, + int returnHomeSkillMerat = 0, int TutorialIntroSkipTime = 0, int[] AvatarDefaultItemMale = null!, int[] AvatarDefaultItemFemale = null!, @@ -223,10 +222,10 @@ public record ConstantsTable( int UGCShopSellMinPrice = 0, int UGCShopSellMaxPrice = 0, int UGCShopSaleDay = 0, - int UGCShopAdFeeMeret = 0, + int UGCShopAdFeeMerat = 0, int UGCShopAdHour = 0, int UGCShopSellingRestrictAmount = 0, - int MeretMarketHomeBannerShowTick = 0, + int MeratMarketHomeBannerShowTick = 0, long BlackMarketSellMinPrice = 0, long BlackMarketSellMaxPrice = 0, int BlackMarketSellEndDay = 0, @@ -248,7 +247,6 @@ public record ConstantsTable( int GuildCreateMinLevel = 0, int GuildNameLengthMin = 0, int GuildNameLengthMax = 0, - int guildFundMax = 0, float guildFundRate = 0f, int guildExpMaxCountForPlayTime = 0, int guildDonateMeso = 0, @@ -498,7 +496,7 @@ public record ConstantsTable( int NoticeDialogOpenSeconds = 0, int RemakeOptionMaxCount = 0, int fishFightingProp = 0, - int FisherBoreDuration = 0, + int fisherBoreDuration = 0, string fishingStartCastingBarText0 = "", string fishingStartCastingBarText1 = "", string fishingStartCastingBarText2 = "", @@ -569,7 +567,6 @@ public record ConstantsTable( int MesoMarketProductUnit9 = 0, int MesoMarketBuyPayType = 0, int MesoMarketIconType = 0, - string MesoMarketTokenDetailUrl = "", int BeautyHairShopGotoFieldID = 0, int BeautyHairShopGotoPortalID = 0, int BeautyColorShopGotoFieldID = 0, @@ -587,7 +584,7 @@ public record ConstantsTable( int TencentCashChargeWebPageWidth = 0, int TencentCashChargeWebPageHight = 0, int NxaCashChargeWebPageWidth = 0, - int NxaCashChargeWebPageHeight = 0, + int NxaCashChargeWebPageHight = 0, int ItemUnLockTime = 0, int PropertyProtectionTime = 0, string TencentSecurityWebPage = "", @@ -616,7 +613,7 @@ public record ConstantsTable( int ResetShadowBuffMerat = 0, int InventoryExpandPrice1Row = 0, int VIPServicePeriodLimitDay = 0, - int VIPMarketCommissionSale = 0, + int VIPMarketCommitionSale = 0, int DungeonMatchNormalTimeOutTick = 0, int ChaosDungeonHallFieldID = 0, int ReverseRaidDungeonHallFieldID = 0, @@ -740,9 +737,9 @@ public record ConstantsTable( int EnchantTransformScriptID = 0, float AutoHideGroupAlpha = 0f, int AutoHideGroupHitVisibleTick = 0, - int UgcShopCharRotateStartDegreeY = 0, - int UgcShopCharRotateEndDegreeY = 0, - int TreeWateringEmotion = 0, + int UgcshopCharRotateStartDegreeY = 0, + int UgcshopCharRotateEndDegreeY = 0, + int TreewateringEmotion = 0, string ShopProbInfoUrl = "", int AdventureLevelLimit = 0, int AdventureLevelLvUpExp = 0, @@ -817,7 +814,7 @@ public record ConstantsTable( int AdventureLevelMissionResetWeekday = 0, int ItemBoxMultiOpenMaxCount = 0, int ItemBoxMultiOpenLimitCount = 0, - int BuffBalloonDistance = 0, + int BuffBallonDistance = 0, int PaybackStartDate = 0, int PaybackSettleMinutes = 0, int PaybackMarketProductSnList = 0, @@ -864,6 +861,5 @@ public record ConstantsTable( int PvpOnePunchUserOpenRewardItem = 0, string TencentCharacterCreateShutdownLeft = "", string TencentCharacterCreateShutdownRight = "", - int LeadSkillMaxSlot = 0, - int NextStateTriggerDefaultTick = 0 + int LeadSkillMaxSlot = 0 ) : ServerTable; diff --git a/Maple2.Server.Game/Manager/FishingManager.cs b/Maple2.Server.Game/Manager/FishingManager.cs index aeffa3c3e..46be60b03 100644 --- a/Maple2.Server.Game/Manager/FishingManager.cs +++ b/Maple2.Server.Game/Manager/FishingManager.cs @@ -239,7 +239,7 @@ public FishingError Start(Vector3 position) { selectedFish = fishes.Get(); - int fishingTick = session.ServerTableMetadata.ConstantsTable.FisherBoreDuration; + int fishingTick = session.ServerTableMetadata.ConstantsTable.fisherBoreDuration; bool hasAutoFish = session.Player.Buffs.HasBuff(BuffEventType.AutoFish); // Fishing Success From a31f930ef3437e181a31515d16662c7fac80d230 Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Mon, 2 Mar 2026 16:24:33 -0600 Subject: [PATCH 11/26] Add Xml.m2d constants.xml within the Server.m2d constants.xml parsing for ConstantsTable. --- Maple2.Model/Metadata/Constants.cs | 98 ------------------- .../Metadata/ServerTable/ConstantsTable.cs | 98 ++++++++++++++++++- Maple2.Server.Game/Commands/PlayerCommand.cs | 4 +- .../Manager/ExperienceManager.cs | 2 +- .../PacketHandlers/QuestHandler.cs | 2 +- .../PacketHandlers/TaxiHandler.cs | 4 +- 6 files changed, 102 insertions(+), 106 deletions(-) diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index de0408079..e10fdc17d 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -130,8 +130,6 @@ public static class Constant { public const int UGCShopProfitDelayDays = 10; public const int PartyFinderListingsPageCount = 12; public const int ProposalItemId = 11600482; - public const int bagSlotTabPetEquipCount = 48; - public const int MeretAirTaxiPrice = 15; public const int ClubMaxCount = 3; public static IReadOnlyDictionary ContentRewards { get; } = new Dictionary { @@ -201,102 +199,6 @@ public static class Constant { public static readonly bool BlockLoginWithMismatchedMachineId = false; public static readonly int DefaultMaxCharacters = 4; #endregion - - #region XML table/constants.xml - public const float NPCCliffHeight = 50.0f; - public const float CustomizingRotationSpeed = 75.0f; - public const bool AllowComboAtComboPoint = true; - public const int AttackRotationSpeed = 90; - public const int ChaosModeTime = 20; - public const int ChaosPointPerBlock = 20; - public const int ChaosPointMaxBlock = 1; - public const int ChaosPointGetLevel0 = 1; - public const int ChaosPointGetPoint0 = 120; - public const int ChaosActionGetLevel0 = 15; - public const int ChaosActionGetLevel1 = 25; - public const int ChaosActionGetLevel2 = 55; - public const int ChaosActionGetLevel3 = 95; - public const int ChaosActionGetLevel4 = 145; - public const int OnEnterTriggerClientSideOnlyTick = 100; - public const int OnEnterTriggerDefaultTick = 1000; - public const int TalkTimeover = 60000; - public const int DropMoneyActiveProbability = 0; - public const int DropMoneyProbability = 0; - public const int OffsetPcMissionIndicator = 20; - public const int questHideTime = 30; - public const int questIntervalTime = 60; - public const int ShopResetChance = 10; - public const int DashKeyInputDelay = 500; - public const int DashSwimConsumeSP = 20; - public const int DashSwimMoveVel = 2; - public const float Glide_Gravity = 0.0f; - public const float Glide_Height_Limit = 0.0f; - public const float Glide_Horizontal_Accelerate = 0.0f; - public const int Glide_Horizontal_Velocity = 500; - public const float Glide_Vertical_Accelerate = 0.0f; - public const int Glide_Vertical_Velocity = 150; - public const int Glide_Vertical_Vibrate_Amplitude = 300; - public const float Glide_Vertical_Vibrate_Frequency = 1500.0f; - public const bool Glide_Effect = true; - public const string Glide_Effect_Run = "CH/Common/Eff_Fly_Balloon_Run.xml"; - public const string Glide_Effect_Idle = "CH/Common/Eff_Fly_Balloon_Idle.xml"; - public const string Glide_Ani_Idle = "Fly_Idle_A"; - public const string Glide_Ani_Left = "Gliding_Left_A"; - public const string Glide_Ani_Right = "Gliding_Right_A"; - public const string Glide_Ani_Run = "Fly_Run_A"; - public const int ConsumeCritical = 5; - public const int DayToNightTime = 10000; - public const float myPCdayTiming = 0.5f; - public const float myPCNightTiming = 0.5f; - public const float BGMTiming = 0.5f; - public const int dayBaseMinute = 1; - public const int dayMinute = 1439; - public const int nightMinute = 1; - public const int QuestRewardSkillSlotQuestID1 = 1010002; - public const int QuestRewardSkillSlotQuestID2 = 1010003; - public const int QuestRewardSkillSlotQuestID3 = 1010004; - public const int QuestRewardSkillSlotQuestID4 = 1010005; - public const int QuestRewardSkillSlotQuestID5 = 1010010; - public const int QuestRewardSkillSlotItemID1 = 40000000; - public const int QuestRewardSkillSlotItemID2 = 40200001; - public const int QuestRewardSkillSlotItemID3 = 20000001; - public const int QuestRewardSkillSlotItemID4 = 40000055; - public const int QuestRewardSkillSlotItemID5 = 40000056; - public const int autoTargetingMaxDegree = 210; - public const float BossHitVibrateFreq = 10.0f; - public const float BossHitVibrateAmp = 5.5f; - public const float BossHitVibrateDamping = 0.7f; - public const float BossHitVibrateDuration = 0.1f; - public const int OneTimeWeaponItemID = 15000001; - public const int ModelHouse = 62000027; - public const int UsingNoPhysXModelUserCount = 10; - public const int UsingNoPhysXModelActorCount = 10; - public const int UsingNoPhysXModelJointCount = 10; - public const bool EnableSoundMute = true; - public const int BossKillSoundRange = 1500; - public const int monsterPeakTimeNotifyDuration = 300; - public const int AirTaxiItemID = 20300003; - public const int ShowNameTagSellerTitle = 10000153; - public const int ShowNameTagChampionTitle = 10000152; - public const int ShowNameTagTrophy1000Title = 10000170; - public const int ShowNameTagTrophy2000Title = 10000171; - public const int ShowNameTagTrophy3000Title = 10000172; - public const int ShowNameTagArchitectTitle = 10000158; - public const int characterMaxLevel = 99; // Updated - public const int OneShotSkillID = 19900061; - public const int FindDungeonHelpEasyDungeonLevel = 50; - public const int FameContentsRequireQuestID = 91000013; - public const int FameExpedContentsRequireQuestID = 50101050; - public const int SurvivalScanAdditionalID = 71000052; - public const int MapleSurvivalTopNRanking = 5; - public const string MapleSurvivalSeasonRewardUrl = - "http://maplestory2.nexon.net/en/news/article/32249/mushking-royale-championship-rewards"; - public const int HoldAttackSkillID = 10700252; - public const string DiscordAppID = "555204064091045904"; - public const int GuildFundMax = 20000; - public const int DropIconVisibleDistance = 400; - public const string MesoMarketTokenDetailUrl = "http://maplestory2.nexon.net/en/news/article/45213"; - #endregion } #pragma warning restore IDE1006 // Naming Styles diff --git a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs index 7da1b856f..394888684 100644 --- a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs +++ b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs @@ -552,7 +552,7 @@ public record ConstantsTable( int GuideQuestDailyPickCountBoss = 0, int DailyMissionPickCount = 0, int DailyMissionRequireLevel = 0, - float NearDropDistance =0f, + float NearDropDistance = 0f, float FarDropDistance = 0f, int MesoMarketBasePrice = 0, int MesoMarketProductUnit0 = 0, @@ -861,5 +861,99 @@ public record ConstantsTable( int PvpOnePunchUserOpenRewardItem = 0, string TencentCharacterCreateShutdownLeft = "", string TencentCharacterCreateShutdownRight = "", - int LeadSkillMaxSlot = 0 + int LeadSkillMaxSlot = 0, + float NPCCliffHeight = 0f, + float CustomizingRotationSpeed = 0f, + bool AllowComboAtComboPoint = false, + int AttackRotationSpeed = 0, + int ChaosModeTime = 0, + int ChaosPointPerBlock = 0, + int ChaosPointMaxBlock = 0, + int ChaosPointGetLevel0 = 0, + int ChaosPointGetPoint0 = 0, + int ChaosActionGetLevel0 = 0, + int ChaosActionGetLevel1 = 0, + int ChaosActionGetLevel2 = 0, + int ChaosActionGetLevel3 = 0, + int ChaosActionGetLevel4 = 0, + int OnEnterTriggerClientSideOnlyTick = 0, + int OnEnterTriggerDefaultTick = 0, + int TalkTimeover = 0, + int DropIconVisibleDistance = 0, + int DropMoneyActiveProbability = 0, + int DropMoneyProbability = 0, + int OffsetPcMissionIndicator = 0, + int questHideTime = 0, + int questIntervalTime = 0, + int ShopResetChance = 0, + int DashKeyInputDelay = 0, + int DashSwimConsumeSP = 0, + int DashSwimMoveVel = 0, + float Glide_Gravity = 0f, + float Glide_Height_Limit = 0f, + float Glide_Horizontal_Accelerate = 0f, + int Glide_Horizontal_Velocity = 0, + float Glide_Vertical_Accelerate = 0f, + int Glide_Vertical_Velocity = 0, + int Glide_Vertical_Vibrate_Amplitude = 0, + float Glide_Vertical_Vibrate_Frequency = 0f, + bool Glide_Effect = false, + string Glide_Effect_Run = "", + string Glide_Effect_Idle = "", + string Glide_Ani_Idle = "", + string Glide_Ani_Left = "", + string Glide_Ani_Right = "", + string Glide_Ani_Run = "", + int ConsumeCritical = 0, + int DayToNightTime = 0, + float myPCdayTiming = 0f, + float myPCNightTiming = 0f, + float BGMTiming = 0f, + int dayBaseMinute = 0, + int dayMinute = 0, + int nightMinute = 0, + int QuestRewardSkillSlotQuestID1 = 0, + int QuestRewardSkillSlotQuestID2 = 0, + int QuestRewardSkillSlotQuestID3 = 0, + int QuestRewardSkillSlotQuestID4 = 0, + int QuestRewardSkillSlotQuestID5 = 0, + int QuestRewardSkillSlotItemID1 = 0, + int QuestRewardSkillSlotItemID2 = 0, + int QuestRewardSkillSlotItemID3 = 0, + int QuestRewardSkillSlotItemID4 = 0, + int QuestRewardSkillSlotItemID5 = 0, + int autoTargetingMaxDegree = 0, + float BossHitVibrateFreq = 0f, + float BossHitVibrateAmp = 0f, + float BossHitVibrateDamping = 0f, + float BossHitVibrateDuration = 0f, + int OneTimeWeaponItemID = 0, + int ModelHouse = 0, + int UsingNoPhysXModelUserCount = 0, + int UsingNoPhysXModelActorCount = 0, + int UsingNoPhysXModelJointCount = 0, + int guildFundMax = 0, + bool EnableSoundMute = false, + int BossKillSoundRange = 0, + int monsterPeakTimeNotifyDuration = 0, + int AirTaxiItemID = 0, + int ShowNameTagSellerTitle = 0, + int ShowNameTagChampionTitle = 0, + int ShowNameTagTrophy1000Title = 0, + int ShowNameTagTrophy2000Title = 0, + int ShowNameTagTrophy3000Title = 0, + int ShowNameTagArchitectTitle = 0, + int characterMaxLevel = 0, + string MesoMarketTokenDetailUrl = "", + int OneShotSkillID = 0, + short[] bagSlotTabPetEquipCount = null!, + int MeratAirTaxiPrice = 0, + int FindDungeonHelpEasyDungeonLevel = 0, + int FameContentsRequireQuestID = 0, + int FameExpedContentsRequireQuestID = 0, + int SurvivalScanAdditionalID = 0, + int MapleSurvivalTopNRanking = 0, + string MapleSurvivalSeasonRewardUrl = "", + int HoldAttackSkillID = 0, + string DiscordAppID = "" ) : ServerTable; diff --git a/Maple2.Server.Game/Commands/PlayerCommand.cs b/Maple2.Server.Game/Commands/PlayerCommand.cs index ff4f419d6..6ceef9728 100644 --- a/Maple2.Server.Game/Commands/PlayerCommand.cs +++ b/Maple2.Server.Game/Commands/PlayerCommand.cs @@ -127,8 +127,8 @@ public LevelCommand(GameSession session) : base("level", "Set player level.") { private void Handle(InvocationContext ctx, short level) { try { - if (level is < 1 or > Constant.characterMaxLevel) { - ctx.Console.Error.WriteLine($"Invalid level: {level}. Must be between 1 and {Constant.characterMaxLevel}."); + if (level < 1 || level > session.ServerTableMetadata.ConstantsTable.characterMaxLevel) { + ctx.Console.Error.WriteLine($"Invalid level: {level}. Must be between 1 and {session.ServerTableMetadata.ConstantsTable.characterMaxLevel}."); return; } diff --git a/Maple2.Server.Game/Manager/ExperienceManager.cs b/Maple2.Server.Game/Manager/ExperienceManager.cs index e733c0dac..52985f35a 100644 --- a/Maple2.Server.Game/Manager/ExperienceManager.cs +++ b/Maple2.Server.Game/Manager/ExperienceManager.cs @@ -175,7 +175,7 @@ public void AddMobExp(int moblevel, float modifier = 1f, long additionalExp = 0) public bool LevelUp() { int startLevel = Level; - for (int level = startLevel; level < Constant.characterMaxLevel; level++) { + for (int level = startLevel; level < session.ServerTableMetadata.ConstantsTable.characterMaxLevel; level++) { if (!session.TableMetadata.ExpTable.NextExp.TryGetValue(level, out long expToNextLevel) || expToNextLevel > Exp) { break; } diff --git a/Maple2.Server.Game/PacketHandlers/QuestHandler.cs b/Maple2.Server.Game/PacketHandlers/QuestHandler.cs index 9da442bb9..a23b4ba12 100644 --- a/Maple2.Server.Game/PacketHandlers/QuestHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/QuestHandler.cs @@ -266,7 +266,7 @@ private static void HandleCompleteFieldMission(GameSession session, IByteReader } private static void HandleSkyFortressTeleport(GameSession session) { - if (!session.Quest.TryGetQuest(Constant.FameContentsRequireQuestID, out Quest? quest) || quest.State != QuestState.Completed) { + if (!session.Quest.TryGetQuest(session.ServerTableMetadata.ConstantsTable.FameContentsRequireQuestID, out Quest? quest) || quest.State != QuestState.Completed) { return; } diff --git a/Maple2.Server.Game/PacketHandlers/TaxiHandler.cs b/Maple2.Server.Game/PacketHandlers/TaxiHandler.cs index 7c62abf4a..5d8e8277a 100644 --- a/Maple2.Server.Game/PacketHandlers/TaxiHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/TaxiHandler.cs @@ -136,12 +136,12 @@ private void HandleMeretAirTaxi(GameSession session, IByteReader packet) { return; } - if (session.Currency.Meret < Constant.MeretAirTaxiPrice) { + if (session.Currency.Meret < session.ServerTableMetadata.ConstantsTable.MeratAirTaxiPrice) { session.Send(NoticePacket.MessageBox(StringCode.s_err_lack_meso)); return; } - session.Currency.Meret -= Constant.MeretAirTaxiPrice; + session.Currency.Meret -= session.ServerTableMetadata.ConstantsTable.MeratAirTaxiPrice; session.Send(session.PrepareField(mapId) ? FieldEnterPacket.Request(session.Player) From d1c3b138100714cc2500aa9a16298f15371f62bc Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Mon, 2 Mar 2026 16:50:37 -0600 Subject: [PATCH 12/26] Fix a crash at character entering world due to JSON deserialization confusion from two constructors and nethier defined as the JSON constructor. --- Maple2.Model/Metadata/NpcMetadata.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Maple2.Model/Metadata/NpcMetadata.cs b/Maple2.Model/Metadata/NpcMetadata.cs index 25f7579f7..b3006e77c 100644 --- a/Maple2.Model/Metadata/NpcMetadata.cs +++ b/Maple2.Model/Metadata/NpcMetadata.cs @@ -1,5 +1,6 @@ using System.ComponentModel.Design; using System.Numerics; +using System.Text.Json.Serialization; using Maple2.Model.Enum; namespace Maple2.Model.Metadata; @@ -57,6 +58,7 @@ public record NpcMetadataDistance( float Sight, float SightHeightUp, float SightHeightDown) { + [JsonConstructor] public NpcMetadataDistance(float avoid, float sight, float sightHeightUp, float sightHeightDown, float lastSightRadius, float lastSightHeightUp, float lastSightHeightDown) : this(avoid, sight, sightHeightUp, sightHeightDown) { LastSightRadius = lastSightRadius; From 80ffb235774aa24ca4b8ad6a7d87c4faa7ad843a Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Tue, 3 Mar 2026 09:05:28 -0600 Subject: [PATCH 13/26] Fix changes that were deleted during merge conflict resolution. Added back GlobalCubeSkillIntervalTime and merged into XML parsing functionality, while also adding back NpcMetadataCorpse parameter in NpcMetadata record. --- Maple2.File.Ingest/Mapper/ServerTableMapper.cs | 3 +++ Maple2.Model/Metadata/NpcMetadata.cs | 7 ++++--- Maple2.Model/Metadata/ServerTable/ConstantsTable.cs | 2 +- .../Manager/Field/FieldManager/FieldManager.State.cs | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs index 5f25f5b0f..32bca1ffb 100644 --- a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs +++ b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs @@ -2201,6 +2201,9 @@ private void SetValue(PropertyInfo prop, object? obj, object? value) { if (prop.Name == "DailyTrophyResetDate") { value = ((string)value).Replace('-', ':'); } + if (prop.Name == "GlobalCubeSkillIntervalTime") { + value = $"0:0:{(string) value}"; + } value = TimeSpan.Parse((string)value, CultureInfo.InvariantCulture); } // Handle array types (int[], short[], etc.) diff --git a/Maple2.Model/Metadata/NpcMetadata.cs b/Maple2.Model/Metadata/NpcMetadata.cs index e13c140bb..0615bce1c 100644 --- a/Maple2.Model/Metadata/NpcMetadata.cs +++ b/Maple2.Model/Metadata/NpcMetadata.cs @@ -18,24 +18,25 @@ public record NpcMetadata( NpcMetadataDropInfo DropInfo, NpcMetadataAction Action, NpcMetadataDead Dead, + NpcMetadataCorpse? Corpse, NpcMetadataLookAtTarget LookAtTarget) : ISearchResult { public NpcMetadata(NpcMetadata other, float lastSightRadius) : this(other.Id, other.Name, other.AiPath, other.Model, other.Stat, other.Basic, other.Distance, other.Skill, other.Property, other.DropInfo, - other.Action, other.Dead, other.LookAtTarget) { + other.Action, other.Dead, other.Corpse, other.LookAtTarget) { Distance = new NpcMetadataDistance(Distance.Avoid, Distance.Sight, Distance.SightHeightUp, Distance.SightHeightDown, lastSightRadius, Distance.LastSightHeightUp, Distance.LastSightHeightDown); } public NpcMetadata(NpcMetadata other, float lastSightRadius, float lastSightHeightUp) : this(other.Id, other.Name, other.AiPath, other.Model, other.Stat, other.Basic, other.Distance, other.Skill, other.Property, other.DropInfo, - other.Action, other.Dead, other.LookAtTarget) { + other.Action, other.Dead, other.Corpse, other.LookAtTarget) { Distance = new NpcMetadataDistance(Distance.Avoid, Distance.Sight, Distance.SightHeightUp, Distance.SightHeightDown, lastSightRadius, lastSightHeightUp, Distance.LastSightHeightDown); } public NpcMetadata(NpcMetadata other, float lastSightRadius, float lastSightHeightUp, float lastSightHeightDown) : this(other.Id, other.Name, other.AiPath, other.Model, other.Stat, other.Basic, other.Distance, other.Skill, other.Property, other.DropInfo, - other.Action, other.Dead, other.LookAtTarget) { + other.Action, other.Dead, other.Corpse, other.LookAtTarget) { Distance = new NpcMetadataDistance(Distance.Avoid, Distance.Sight, Distance.SightHeightUp, Distance.SightHeightDown, lastSightRadius, lastSightHeightUp, lastSightHeightDown); } diff --git a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs index 394888684..815e7fe4a 100644 --- a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs +++ b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs @@ -271,7 +271,7 @@ public record ConstantsTable( int HomeReturnPortalKeepTime = 0, string HomeReturnPortalKeepNif = "", int HomeReturnPortalDimensionY = 0, - float GlobalCubeSkillIntervalTime = 0f, + TimeSpan GlobalCubeSkillIntervalTime = new(), int RoomEnterPortalDurationTick = 0, int NpcBossCubeSkillCreateHeight = 0, int NPCUpdateTickNoUser = 0, diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index d8c204cf2..2897d2211 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -725,7 +725,7 @@ public void RemoveSkillByTriggerId(int triggerId) { private void AddCubeSkill(SkillMetadata metadata, in Vector3 position, in Vector3 rotation = default) { Vector3 adjustedPosition = position; adjustedPosition.Z += FieldAccelerationStructure.BLOCK_SIZE; - var fieldSkill = new FieldSkill(this, NextLocalId(), FieldActor, metadata, (int) Constant.GlobalCubeSkillIntervalTime.TotalMilliseconds, adjustedPosition) { + var fieldSkill = new FieldSkill(this, NextLocalId(), FieldActor, metadata, (int)ServerTableMetadata.ConstantsTable.GlobalCubeSkillIntervalTime.TotalMilliseconds, adjustedPosition) { Position = adjustedPosition, Rotation = rotation, Source = SkillSource.Cube, From 8adfa4ded0ca9e37341a6c25bff61f92a6d308d2 Mon Sep 17 00:00:00 2001 From: mfranca915 Date: Tue, 3 Mar 2026 09:21:42 -0600 Subject: [PATCH 14/26] Took CodeRabbit suggestion since it pointed out unreachable branches and it's much cleaner than what was being used to pass constant values for lasting sight radius, height up, and height down. --- Maple2.Model/Game/Npc/Npc.cs | 13 ++++--------- Maple2.Model/Metadata/NpcMetadata.cs | 14 -------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/Maple2.Model/Game/Npc/Npc.cs b/Maple2.Model/Game/Npc/Npc.cs index a2c341b3e..fc176815c 100644 --- a/Maple2.Model/Game/Npc/Npc.cs +++ b/Maple2.Model/Game/Npc/Npc.cs @@ -11,15 +11,10 @@ public class Npc { public bool IsBoss => Metadata.Basic.Friendly == 0 && Metadata.Basic.Class >= 3; public Npc(NpcMetadata metadata, AnimationMetadata? animation, float constLastSightRadius, float constLastSightHeightUp, float constLastSightHeightDown) { - if (metadata.Distance.LastSightRadius == 0) { - Metadata = new NpcMetadata(metadata, constLastSightRadius); - } else if (metadata.Distance.LastSightRadius == 0 && metadata.Distance.LastSightHeightUp == 0) { - Metadata = new NpcMetadata(metadata, constLastSightRadius, constLastSightHeightUp); - } else if (metadata.Distance.LastSightRadius == 0 && metadata.Distance.LastSightHeightUp == 0 && metadata.Distance.LastSightHeightDown == 0) { - Metadata = new NpcMetadata(metadata, constLastSightRadius, constLastSightHeightUp, constLastSightHeightDown); - } else { - Metadata = metadata; - } + float lastSightRadius = metadata.Distance.LastSightRadius == 0 ? constLastSightRadius : metadata.Distance.LastSightRadius; + float lastSightHeightUp = metadata.Distance.LastSightHeightUp == 0 ? constLastSightHeightUp : metadata.Distance.LastSightHeightUp; + float lastSightHeightDown = metadata.Distance.LastSightHeightDown == 0 ? constLastSightHeightDown : metadata.Distance.LastSightHeightDown; + Metadata = new NpcMetadata(metadata, lastSightRadius, lastSightHeightUp, lastSightHeightDown); Animations = animation?.Sequences ?? new Dictionary(); } } diff --git a/Maple2.Model/Metadata/NpcMetadata.cs b/Maple2.Model/Metadata/NpcMetadata.cs index 0615bce1c..fca9752e6 100644 --- a/Maple2.Model/Metadata/NpcMetadata.cs +++ b/Maple2.Model/Metadata/NpcMetadata.cs @@ -20,20 +20,6 @@ public record NpcMetadata( NpcMetadataDead Dead, NpcMetadataCorpse? Corpse, NpcMetadataLookAtTarget LookAtTarget) : ISearchResult { - public NpcMetadata(NpcMetadata other, float lastSightRadius) : this(other.Id, - other.Name, other.AiPath, other.Model, other.Stat, other.Basic, other.Distance, other.Skill, other.Property, other.DropInfo, - other.Action, other.Dead, other.Corpse, other.LookAtTarget) { - Distance = new NpcMetadataDistance(Distance.Avoid, Distance.Sight, Distance.SightHeightUp, - Distance.SightHeightDown, lastSightRadius, Distance.LastSightHeightUp, Distance.LastSightHeightDown); - } - - public NpcMetadata(NpcMetadata other, float lastSightRadius, float lastSightHeightUp) : this(other.Id, - other.Name, other.AiPath, other.Model, other.Stat, other.Basic, other.Distance, other.Skill, other.Property, other.DropInfo, - other.Action, other.Dead, other.Corpse, other.LookAtTarget) { - Distance = new NpcMetadataDistance(Distance.Avoid, Distance.Sight, Distance.SightHeightUp, - Distance.SightHeightDown, lastSightRadius, lastSightHeightUp, Distance.LastSightHeightDown); - } - public NpcMetadata(NpcMetadata other, float lastSightRadius, float lastSightHeightUp, float lastSightHeightDown) : this(other.Id, other.Name, other.AiPath, other.Model, other.Stat, other.Basic, other.Distance, other.Skill, other.Property, other.DropInfo, other.Action, other.Dead, other.Corpse, other.LookAtTarget) { From 1dadbe8f84238312cf270767dc14ae30bf63f170 Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Wed, 4 Mar 2026 06:19:20 +0800 Subject: [PATCH 15/26] Revert "JewelRuby" This reverts commit 059fa693194f4cfe6e15f8a1bc17b6fae5ca764a. --- .../Storage/Game/GameStorage.User.cs | 6 +- Maple2.Model/Game/Market/SoldUgcMarketItem.cs | 22 ++-- Maple2.Model/Metadata/Constants.cs | 2 +- Maple2.Server.Game/GameServer.cs | 23 +--- Maple2.Server.Game/Manager/DungeonManager.cs | 2 +- .../Manager/Field/AgentNavigation.cs | 99 ++------------ .../Field/FieldManager/FieldManager.State.cs | 16 --- .../Field/FieldManager/FieldManager.cs | 27 +--- .../Manager/Field/FieldManager/IField.cs | 2 - .../Manager/Field/TriggerCollection.cs | 122 ++++++------------ Maple2.Server.Game/Manager/FishingManager.cs | 15 --- .../MovementState.SkillCastTask.cs | 42 ++---- .../Actor/ActorStateComponent/SkillState.cs | 10 +- .../Model/Field/Actor/FieldNpc.cs | 9 -- .../Model/Field/Entity/FieldSkill.cs | 22 +--- .../Packets/SkillDamagePacket.cs | 20 +-- .../Trigger/TriggerContext.Field.cs | 13 +- .../Trigger/TriggerContext.Npc.cs | 16 +-- .../Util/Sync/PlayerInfoStorage.cs | 21 +-- .../Service/WorldService.Migrate.cs | 19 +-- Maple2.Server.World/WorldServer.cs | 25 ---- Maple2.Tools/VectorMath/Transform.cs | 34 +---- 22 files changed, 124 insertions(+), 443 deletions(-) diff --git a/Maple2.Database/Storage/Game/GameStorage.User.cs b/Maple2.Database/Storage/Game/GameStorage.User.cs index 722cd849f..35dd05f68 100644 --- a/Maple2.Database/Storage/Game/GameStorage.User.cs +++ b/Maple2.Database/Storage/Game/GameStorage.User.cs @@ -582,9 +582,9 @@ public Account CreateAccount(Account account, string password) { model.Password = BCrypt.Net.BCrypt.HashPassword(password, 13); #if DEBUG model.Currency = new AccountCurrency { - Meret = 1_000_00, + Meret = 9_999_999, }; -// model.Permissions = AdminPermissions.Admin.ToString(); + model.Permissions = AdminPermissions.Admin.ToString(); #endif Context.Account.Add(model); Context.SaveChanges(); // Exception if failed. @@ -609,7 +609,7 @@ public Account CreateAccount(Account account, string password) { model.Channel = -1; #if DEBUG model.Currency = new CharacterCurrency { - Meso = 500000, + Meso = 999999999, }; #endif Context.Character.Add(model); diff --git a/Maple2.Model/Game/Market/SoldUgcMarketItem.cs b/Maple2.Model/Game/Market/SoldUgcMarketItem.cs index 0b14e1795..b8d22bb6a 100644 --- a/Maple2.Model/Game/Market/SoldUgcMarketItem.cs +++ b/Maple2.Model/Game/Market/SoldUgcMarketItem.cs @@ -10,25 +10,21 @@ public class SoldUgcMarketItem : IByteSerializable { public required string Name { get; init; } public long SoldTime { get; init; } public long AccountId { get; init; } - public int Count { get; init; } = 1; + public void WriteTo(IByteWriter writer) { writer.WriteLong(Id); - writer.WriteLong(); + writer.WriteLong(); writer.WriteUnicodeString(Name); - - writer.WriteInt(Count); - writer.WriteInt(); - - writer.WriteLong(); - writer.WriteLong(); - writer.WriteUnicodeString(); - writer.WriteUnicodeString(); - writer.WriteInt(); - + writer.WriteInt(); + writer.WriteInt(); + writer.WriteLong(); + writer.WriteLong(); + writer.WriteUnicodeString(); + writer.WriteUnicodeString(); + writer.WriteInt(); writer.WriteLong(Price); writer.WriteLong(SoldTime); writer.WriteLong(Profit); } } - diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index f5085e784..43c74bf53 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -123,7 +123,7 @@ public static class Constant { public const bool DebugTriggers = false; // Set to true to enable debug triggers. (It'll write triggers to files and load triggers from files instead of DB) - public const bool AllowUnicodeInNames = true; // Allow Unicode characters in character and guild names + public const bool AllowUnicodeInNames = false; // Allow Unicode characters in character and guild names public static IReadOnlyDictionary ContentRewards { get; } = new Dictionary { { "miniGame", 1005 }, diff --git a/Maple2.Server.Game/GameServer.cs b/Maple2.Server.Game/GameServer.cs index b09bc53f9..7d2524626 100644 --- a/Maple2.Server.Game/GameServer.cs +++ b/Maple2.Server.Game/GameServer.cs @@ -66,32 +66,17 @@ public GameServer(FieldManager.Factory fieldFactory, PacketRouter r public override void OnConnected(GameSession session) { lock (mutex) { connectingSessions.Remove(session); - - if (session.CharacterId != 0) { - sessions[session.CharacterId] = session; - } - - // 可选:避免残留的 0 键占位 - if (sessions.TryGetValue(0, out GameSession? zero) && ReferenceEquals(zero, session)) { - sessions.Remove(0); - } + sessions[session.CharacterId] = session; } } + public override void OnDisconnected(GameSession session) { lock (mutex) { connectingSessions.Remove(session); - - long cid = session.CharacterId; - if (cid == 0) return; - - // 关键:只允许“当前登记在 sessions 里的那个会话”删除自己 - // 迁移/换图时会出现:新会话先注册,旧会话后断开 - // 如果这里无条件 Remove,会把新会话也删掉 => Heartbeat unknown => 假离线 - if (sessions.TryGetValue(cid, out GameSession? current) && ReferenceEquals(current, session)) { - sessions.Remove(cid); - } + sessions.Remove(session.CharacterId); } } + public bool GetSession(long characterId, [NotNullWhen(true)] out GameSession? session) { lock (mutex) { return sessions.TryGetValue(characterId, out session); diff --git a/Maple2.Server.Game/Manager/DungeonManager.cs b/Maple2.Server.Game/Manager/DungeonManager.cs index 83a1697ab..98d8dcfe4 100644 --- a/Maple2.Server.Game/Manager/DungeonManager.cs +++ b/Maple2.Server.Game/Manager/DungeonManager.cs @@ -437,7 +437,7 @@ void GetClearDungeonRewards() { ICollection items = []; if (rewardMetadata.UnlimitedDropBoxIds.Length > 0) { foreach (int boxId in rewardMetadata.UnlimitedDropBoxIds) { - items = items.Concat(Lobby.ItemDrop.GetIndividualDropItems(session, session.Player.Value.Character.Level, boxId)).ToList(); + items = items.Concat(Lobby.ItemDrop.GetIndividualDropItems(boxId)).ToList(); items = items.Concat(Lobby.ItemDrop.GetGlobalDropItems(boxId, Metadata.Level)).ToList(); } } diff --git a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs index 93a8fff78..8703ee615 100644 --- a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs +++ b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs @@ -21,7 +21,6 @@ public sealed class AgentNavigation { public List? currentPath = []; private int currentPathIndex = 0; private float currentPathProgress = 0; - private bool currentPathIsFallback = false; public AgentNavigation(FieldNpc fieldNpc, DtCrowdAgent dtAgent, DtCrowd dtCrowd) { npc = fieldNpc; @@ -159,10 +158,6 @@ public AgentNavigation(FieldNpc fieldNpc, DtCrowdAgent dtAgent, DtCrowd dtCrowd) public bool HasPath => currentPath != null && currentPath.Count > 0; - private static bool IsVayarianGuardian(int npcId) { - return npcId is 21500420 or 21500422 or 21500423 or 21500424; - } - public bool UpdatePosition() { if (!field.FindNearestPoly(npc.Position, out _, out RcVec3f position)) { Logger.Error("Failed to find valid position from {Source} => {Position}", npc.Position, position); @@ -221,18 +216,6 @@ public Vector3 GetAgentPosition() { currentPathProgress += (float) timeSpan.TotalSeconds * speed / distance; RcVec3f end = RcVec3f.Lerp(currentPath[currentPathIndex], currentPath[currentPathIndex + 1], currentPathProgress); - if (currentPathIsFallback) { - // Keep the end point on the navmesh each tick; prevents straight-line fallback from pushing NPCs through walls/out of bounds. - if (field.FindNearestPoly(end, out _, out RcVec3f snappedEnd)) { - end = snappedEnd; - if (currentPath is not null && currentPathIndex + 1 < currentPath.Count) { - currentPath[currentPathIndex + 1] = snappedEnd; - } - } else { - // Can't stay on mesh; stop movement. - currentPath = null; - } - } return (start, DotRecastHelper.FromNavMeshSpace(end)); } @@ -311,45 +294,14 @@ public bool PathTo(Vector3 goal) { private bool SetPathTo(RcVec3f target) { currentPath = []; currentPathIndex = 0; - currentPathIsFallback = false; currentPathProgress = 0; - try { - List? path = FindPath(agent.npos, target); - if (path is null) { - // Clamp target to nearest navmesh polygon to avoid drifting outside walkable area. - if (!field.FindNearestPoly(target, out _, out RcVec3f snappedTarget)) { - Logger.Warning("[Fallback] Could not clamp target to navmesh. Target(nav)={Target}", target); - return false; - } - target = snappedTarget; - Vector3 worldTarget = DotRecastHelper.FromNavMeshSpace(target); - - if (IsVayarianGuardian(npc.Value.Id)) { - // Scripted NPC jump/warp: advance trigger even when navmesh has no valid path. - Logger.Warning("No path for Vayarian Guardian {NpcId}; snapping from {From} to {To}", npc.Value.Id, npc.Position, worldTarget); - UpdatePosition(worldTarget); - currentPath = null; - return true; - } - - Logger.Warning("Failed to find path from {FromNav} to {ToNav}; fallback: clamped straight-line on navmesh. World(from)={FromWorld} World(to)={ToWorld}", agent.npos, target, DotRecastHelper.FromNavMeshSpace(agent.npos), worldTarget); - currentPath = [agent.npos, target]; - return true; - } - - currentPath = path; - return true; + currentPath = FindPath(agent.npos, target); } catch (Exception ex) { Logger.Error(ex, "Failed to find path to {Target}", target); - return false; } - } - public void ClearPath() { - currentPath?.Clear(); - currentPathIndex = 0; - currentPathProgress = 0; - currentPathIsFallback = false; + + return currentPath is not null; } public bool PathAwayFrom(Vector3 goal, int distance) { @@ -363,20 +315,11 @@ public bool PathAwayFrom(Vector3 goal, int distance) { // get distance in navmesh space float fDistance = distance * DotRecastHelper.MapRotation.GetRightAxis().Length(); - // get direction from agent to target (guard against zero-length vectors which would produce NaNs) - RcVec3f delta = RcVec3f.Subtract(target, position); - float lenSqr = RcVec3f.Dot(delta, delta); - RcVec3f direction; - if (lenSqr > 1e-6f) { - direction = RcVec3f.Normalize(delta); - } else { - // If goal overlaps agent (common during melee hit / on-hit stepback), choose a stable fallback direction. - // Alternate per-object id so mobs don't all backstep the same way. - direction = (npc.ObjectId & 1) == 0 ? new RcVec3f(1, 0, 0) : new RcVec3f(-1, 0, 0); - } + // get direction from agent to target + RcVec3f direction = RcVec3f.Normalize(RcVec3f.Subtract(target, position)); - // get the point that is fDistance away from the agent in the opposite direction - RcVec3f positionAway = RcVec3f.Add(position, direction * -fDistance); + // get the point that is fDistance away from the target in the opposite direction + RcVec3f positionAway = RcVec3f.Add(position, RcVec3f.Normalize(direction) * -fDistance); // find the nearest poly to the positionAway if (!field.FindNearestPoly(positionAway, out _, out RcVec3f positionAwayNavMesh)) { @@ -385,35 +328,17 @@ public bool PathAwayFrom(Vector3 goal, int distance) { return SetPathAway(positionAwayNavMesh); } + private bool SetPathAway(RcVec3f target) { currentPath = []; currentPathIndex = 0; - currentPathIsFallback = false; currentPathProgress = 0; - try { - List? path = FindPath(agent.npos, target); - if (path is null) { - Vector3 worldTarget = DotRecastHelper.FromNavMeshSpace(target); - - if (IsVayarianGuardian(npc.Value.Id)) { - // Scripted NPC jump/warp: advance trigger even when navmesh has no valid path. - Logger.Warning("No path for Vayarian Guardian {NpcId}; snapping from {From} to {To}", npc.Value.Id, npc.Position, worldTarget); - UpdatePosition(worldTarget); - currentPath = null; - return true; - } - - Logger.Warning("Failed to find path from {FromNav} to {ToNav}; fallback: clamped straight-line on navmesh. World(from)={FromWorld} World(to)={ToWorld}", agent.npos, target, DotRecastHelper.FromNavMeshSpace(agent.npos), worldTarget); - currentPath = [agent.npos, target]; - return true; - } - - currentPath = path; - return true; + currentPath = FindPath(agent.npos, target); } catch (Exception ex) { - Logger.Error(ex, "Failed to find path to {Target}", target); - return false; + Logger.Error(ex, "Failed to find path away from {Target}", target); } + + return currentPath is not null; } } diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index 986d7b3a6..a68abcf28 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -46,11 +46,6 @@ public partial class FieldManager { private readonly ConcurrentDictionary fieldItems = new(); private readonly ConcurrentDictionary fieldMobSpawns = new(); private readonly ConcurrentDictionary fieldSpawnPointNpcs = new(); - - - // Tracks spawnPointIds that have died and may have been removed from Mobs/Npcs dictionaries. - // This allows triggers like MonsterDead(spawnId) to continue working even after the dead NPC is despawned. - private readonly ConcurrentDictionary deadSpawnPoints = new(); private readonly ConcurrentDictionary fieldPlayerSpawnPoints = new(); private readonly ConcurrentDictionary fieldSpawnGroups = new(); private readonly ConcurrentDictionary fieldSkills = new(); @@ -120,11 +115,6 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId } public FieldNpc? SpawnNpc(NpcMetadata npc, Vector3 position, Vector3 rotation, bool disableAi = false, FieldMobSpawn? owner = null, SpawnPointNPC? spawnPointNpc = null, string spawnAnimation = "") { - - if (spawnPointNpc is not null) { - deadSpawnPoints.TryRemove(spawnPointNpc.SpawnPointId, out _); - } - // Apply random offset if SpawnRadius is set Vector3 spawnPosition = position; if (spawnPointNpc?.SpawnRadius > 0) { @@ -535,8 +525,6 @@ public void DespawnWorldBoss() { RemoveNpc(objectId); } - public bool IsSpawnPointDead(int spawnId) => deadSpawnPoints.ContainsKey(spawnId); - public void ToggleNpcSpawnPoint(int spawnId) { List spawns = fieldSpawnPointNpcs.Values.Where(spawn => spawn.Value.SpawnPointId == spawnId).ToList(); foreach (FieldSpawnPointNpc spawn in spawns) { @@ -963,10 +951,6 @@ public bool RemoveNpc(int objectId, TimeSpan removeDelay = default) { } Broadcast(FieldPacket.RemoveNpc(objectId)); Broadcast(ProxyObjectPacket.RemoveNpc(objectId)); - - if (npc.IsDead && npc.SpawnPointId != 0) { - deadSpawnPoints.TryAdd(npc.SpawnPointId, 1); - } npc.Dispose(); }, removeDelay); return true; diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs index 3e09c844b..4e6cd94fc 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs @@ -391,30 +391,13 @@ public bool FindNearestPoly(RcVec3f point, out long nearestRef, out RcVec3f posi return false; } - // Try progressively larger search extents; helps when an entity gets pushed off the navmesh (e.g., knockback). - // Keep the first extent small for performance, then widen only on failure. - ReadOnlySpan extentsList = stackalloc RcVec3f[] { - new RcVec3f(2, 4, 2), - new RcVec3f(8, 16, 8), - new RcVec3f(32, 64, 32), - new RcVec3f(128, 256, 128), - }; - - DtNavMeshQuery query = Navigation.Crowd.GetNavMeshQuery(); - IDtQueryFilter filter = Navigation.Crowd.GetFilter(0); - - for (int i = 0; i < extentsList.Length; i++) { - RcVec3f ext = extentsList[i]; - DtStatus status = query.FindNearestPoly(point, ext, filter, out nearestRef, out position, out _); - if (status.Succeeded() && nearestRef != 0) { - return true; - } + DtStatus status = Navigation.Crowd.GetNavMeshQuery().FindNearestPoly(point, new RcVec3f(2, 4, 2), Navigation.Crowd.GetFilter(0), out nearestRef, out position, out _); + if (status.Failed()) { + logger.Warning("Failed to find nearest poly from position {Source} in field {MapId}", point, MapId); + return false; } - logger.Warning("Failed to find nearest poly from position {Source} in field {MapId}", point, MapId); - nearestRef = 0; - position = default; - return false; + return true; } public bool TryGetPlayerById(long characterId, [NotNullWhen(true)] out FieldPlayer? player) { diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs b/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs index 2bdcbc5ba..3b3c93eb3 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs @@ -95,8 +95,6 @@ public virtual void Init() { } public bool LiftupCube(in Vector3B coordinates, [NotNullWhen(true)] out LiftupWeapon? liftupWeapon); public void MovePlayerAlongPath(string pathName); - public bool IsSpawnPointDead(int spawnId); - public bool RemoveNpc(int objectId, TimeSpan removeDelay = default); public bool RemovePet(int objectId, TimeSpan removeDelay = default); } diff --git a/Maple2.Server.Game/Manager/Field/TriggerCollection.cs b/Maple2.Server.Game/Manager/Field/TriggerCollection.cs index 1efa4ec9e..b76c198c3 100644 --- a/Maple2.Server.Game/Manager/Field/TriggerCollection.cs +++ b/Maple2.Server.Game/Manager/Field/TriggerCollection.cs @@ -1,55 +1,36 @@ using System.Collections; -using System.Collections.Generic; using Maple2.Model.Game; using Maple2.Model.Metadata; namespace Maple2.Server.Game.Manager.Field; public sealed class TriggerCollection : IReadOnlyCollection { - // NOTE: These collections need to be mutable at runtime. - // Some map packs / DB imports are missing Ms2TriggerMesh / Ms2TriggerAgent entries, - // but trigger scripts still reference those IDs (set_mesh / set_agent). - // The client already knows the objects by triggerId (from the map file), and only - // needs server updates to toggle visibility/collision. - // - // We therefore support creating lightweight placeholder trigger objects on demand. - private readonly Dictionary actors; - private readonly Dictionary cameras; - private readonly Dictionary cubes; - private readonly Dictionary effects; - private readonly Dictionary ladders; - private readonly Dictionary meshes; - private readonly Dictionary ropes; - private readonly Dictionary sounds; - private readonly Dictionary agents; - private readonly Dictionary boxes; - - public IReadOnlyDictionary Actors => actors; - public IReadOnlyDictionary Cameras => cameras; - public IReadOnlyDictionary Cubes => cubes; - public IReadOnlyDictionary Effects => effects; - public IReadOnlyDictionary Ladders => ladders; - public IReadOnlyDictionary Meshes => meshes; - public IReadOnlyDictionary Ropes => ropes; - public IReadOnlyDictionary Sounds => sounds; - public IReadOnlyDictionary Agents => agents; - - public IReadOnlyDictionary Boxes => boxes; + public readonly IReadOnlyDictionary Actors; + public readonly IReadOnlyDictionary Cameras; + public readonly IReadOnlyDictionary Cubes; + public readonly IReadOnlyDictionary Effects; + public readonly IReadOnlyDictionary Ladders; + public readonly IReadOnlyDictionary Meshes; + public readonly IReadOnlyDictionary Ropes; + public readonly IReadOnlyDictionary Sounds; + public readonly IReadOnlyDictionary Agents; + + public readonly IReadOnlyDictionary Boxes; // These seem to get managed separately... // private readonly IReadOnlyDictionary Agents; // private readonly IReadOnlyDictionary Skills; public TriggerCollection(MapEntityMetadata entities) { - actors = new(); - cameras = new(); - cubes = new(); - effects = new(); - ladders = new(); - meshes = new(); - ropes = new(); - sounds = new(); - agents = new(); + Dictionary actors = new(); + Dictionary cameras = new(); + Dictionary cubes = new(); + Dictionary effects = new(); + Dictionary ladders = new(); + Dictionary meshes = new(); + Dictionary ropes = new(); + Dictionary sounds = new(); + Dictionary agents = new(); foreach (Ms2TriggerActor actor in entities.Trigger.Actors) { actors[actor.TriggerId] = new TriggerObjectActor(actor); @@ -80,55 +61,36 @@ public TriggerCollection(MapEntityMetadata entities) { agents[agent.TriggerId] = new TriggerObjectAgent(agent); } - boxes = new(); + Actors = actors; + Cameras = cameras; + Cubes = cubes; + Effects = effects; + Ladders = ladders; + Meshes = meshes; + Ropes = ropes; + Sounds = sounds; + Agents = agents; + + Dictionary boxes = new(); foreach (Ms2TriggerBox box in entities.Trigger.Boxes) { boxes[box.TriggerId] = new TriggerBox(box); } - } - - /// - /// Creates or retrieves a Trigger Mesh placeholder. Useful when a trigger script references - /// a mesh id that is missing from the DB import. - /// - public TriggerObjectMesh GetOrAddMesh(int triggerId) { - if (meshes.TryGetValue(triggerId, out TriggerObjectMesh? mesh)) { - return mesh; - } - - // Scale/minimapInvisible are not important for updates; the client already has the actual mesh. - Ms2TriggerMesh meta = new Ms2TriggerMesh(1f, triggerId, Visible: true, MinimapInvisible: false); - mesh = new TriggerObjectMesh(meta); - meshes[triggerId] = mesh; - return mesh; - } - - /// - /// Creates or retrieves a Trigger Agent placeholder. Useful when a trigger script references - /// an agent id that is missing from the DB import. - /// - public TriggerObjectAgent GetOrAddAgent(int triggerId) { - if (agents.TryGetValue(triggerId, out TriggerObjectAgent? agent)) { - return agent; - } - Ms2TriggerAgent meta = new Ms2TriggerAgent(triggerId, Visible: true); - agent = new TriggerObjectAgent(meta); - agents[triggerId] = agent; - return agent; + Boxes = boxes; } - public int Count => actors.Count + cameras.Count + cubes.Count + effects.Count + ladders.Count + meshes.Count + ropes.Count + sounds.Count + agents.Count; + public int Count => Actors.Count + Cameras.Count + Cubes.Count + Effects.Count + Ladders.Count + Meshes.Count + Ropes.Count + Sounds.Count + Agents.Count; public IEnumerator GetEnumerator() { - foreach (TriggerObjectActor actor in actors.Values) yield return actor; - foreach (TriggerObjectCamera camera in cameras.Values) yield return camera; - foreach (TriggerObjectCube cube in cubes.Values) yield return cube; - foreach (TriggerObjectEffect effect in effects.Values) yield return effect; - foreach (TriggerObjectLadder ladder in ladders.Values) yield return ladder; - foreach (TriggerObjectMesh mesh in meshes.Values) yield return mesh; - foreach (TriggerObjectRope rope in ropes.Values) yield return rope; - foreach (TriggerObjectSound sound in sounds.Values) yield return sound; - foreach (TriggerObjectAgent agent in agents.Values) yield return agent; + foreach (TriggerObjectActor actor in Actors.Values) yield return actor; + foreach (TriggerObjectCamera camera in Cameras.Values) yield return camera; + foreach (TriggerObjectCube cube in Cubes.Values) yield return cube; + foreach (TriggerObjectEffect effect in Effects.Values) yield return effect; + foreach (TriggerObjectLadder ladder in Ladders.Values) yield return ladder; + foreach (TriggerObjectMesh mesh in Meshes.Values) yield return mesh; + foreach (TriggerObjectRope rope in Ropes.Values) yield return rope; + foreach (TriggerObjectSound sound in Sounds.Values) yield return sound; + foreach (TriggerObjectAgent agent in Agents.Values) yield return agent; } IEnumerator IEnumerable.GetEnumerator() { diff --git a/Maple2.Server.Game/Manager/FishingManager.cs b/Maple2.Server.Game/Manager/FishingManager.cs index 60c5b94a4..aebd61425 100644 --- a/Maple2.Server.Game/Manager/FishingManager.cs +++ b/Maple2.Server.Game/Manager/FishingManager.cs @@ -336,21 +336,6 @@ private void AddFish(int fishSize, bool hasAutoFish) { session.Field.Broadcast(FishingPacket.PrizeFish(session.PlayerName, selectedFish.Id)); } session.ConditionUpdate(ConditionType.fish, codeLong: selectedFish.Id, targetLong: session.Field.MapId); - // ====== 钓鱼给角色经验 ====== - long charExp = selectedFish.Exp; - - // 可选倍率:首次 / 奖牌鱼 - if (caughtFishType == CaughtFishType.FirstKind) { - charExp *= 2; - } - if (caughtFishType == CaughtFishType.Prize) { - charExp *= 2; - } - - if (charExp > 0) { - session.Exp.AddExp(ExpType.fishing, modifier: 0f, additionalExp: charExp); - } - // ====== 角色经验结束 ====== if (masteryExp == 0) { return; diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs index ce570eb6f..2eb377053 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs @@ -41,17 +41,8 @@ protected override void TaskFinished(bool isCompleted) { private void SkillCastFaceTarget(SkillRecord cast, IActor target, int faceTarget) { Vector3 offset = target.Position - actor.Position; - float distance = offset.X * offset.X + offset.Y * offset.Y; - float vertical = MathF.Abs(offset.Z); - - // In MS2 data, many AI skill nodes use FaceTarget=0 while the skill's AutoTargeting.MaxDegree - // is a narrow cone. If we gate turning by MaxDegree (dot product), the NPC can end up casting - // while facing away (target behind the cone) and never correct its facing. - // - // To avoid "背对玩家放技能", we always rotate to face the current target when: - // - the motion requests FaceTarget, and - // - AutoTargeting distance/height constraints allow it. - // MaxDegree is ignored for *turning*. + float distance = offset.LengthSquared(); + if (faceTarget != 1) { if (!cast.Motion.MotionProperty.FaceTarget || cast.Metadata.Data.AutoTargeting is null) { return; @@ -59,28 +50,26 @@ private void SkillCastFaceTarget(SkillRecord cast, IActor target, int faceTarget var autoTargeting = cast.Metadata.Data.AutoTargeting; - bool inRange = autoTargeting.MaxDistance == 0 || distance <= autoTargeting.MaxDistance * autoTargeting.MaxDistance; - inRange &= autoTargeting.MaxHeight == 0 || vertical <= autoTargeting.MaxHeight; - if (!inRange) { - return; - } + bool shouldFaceTarget = autoTargeting.MaxDistance == 0 || distance <= autoTargeting.MaxDistance; + shouldFaceTarget |= autoTargeting.MaxHeight == 0 || offset.Y <= autoTargeting.MaxHeight; - if (distance < 0.0001f) { + if (!shouldFaceTarget) { return; } distance = (float) Math.Sqrt(distance); offset *= (1 / distance); - } else { - if (distance < 0.0001f) { - return; - - } + float degreeCosine = (float) Math.Cos(autoTargeting.MaxDegree / 2); + float dot = Vector3.Dot(offset, actor.Transform.FrontAxis); + shouldFaceTarget = autoTargeting.MaxDegree == 0 || dot >= degreeCosine; + if (!shouldFaceTarget) { + return; + } + } else { distance = (float) Math.Sqrt(distance); - offset *= (1 / distance); } @@ -115,12 +104,9 @@ private void SkillCast(NpcSkillCastTask task, int id, short level, long uid, byt } if (task.FacePos != new Vector3(0, 0, 0)) { - actor.Transform.LookTo(task.FacePos - actor.Position); // safe: LookTo normalizes with guards + actor.Transform.LookTo(Vector3.Normalize(task.FacePos - actor.Position)); } else if (actor.BattleState.Target is not null) { - // Hard guarantee: NPCs should always face their current battle target when casting. - // Some boss skills have MotionProperty.FaceTarget=false or narrow AutoTargeting degrees, - // which previously allowed casting while facing away. - actor.Transform.LookTo(actor.BattleState.Target.Position - actor.Position); // safe: LookTo normalizes with guards + SkillCastFaceTarget(cast, actor.BattleState.Target, task.FaceTarget); } CastTask = task; diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs index 4d6577958..6e53abf3d 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs @@ -54,15 +54,7 @@ public void SkillCastAttack(SkillRecord cast, byte attackPoint, List att if (attackTargets.Count > targetIndex) { // if attack.direction == 3, use direction to target, if attack.direction == 0, use rotation maybe? cast.Position = actor.Position; - Vector3 dir = attackTargets[targetIndex].Position - actor.Position; - if (float.IsNaN(dir.X) || float.IsNaN(dir.Y) || float.IsNaN(dir.Z) || - float.IsInfinity(dir.X) || float.IsInfinity(dir.Y) || float.IsInfinity(dir.Z) || - dir.LengthSquared() < 1e-6f) { - // Keep current facing if target is on top of caster. - cast.Direction = actor.Transform.FrontAxis; - } else { - cast.Direction = Vector3.Normalize(dir); - } + cast.Direction = Vector3.Normalize(attackTargets[targetIndex].Position - actor.Position); } actor.Field.Broadcast(SkillDamagePacket.Target(cast, targets)); diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index 42874181e..4208ae688 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -197,15 +197,6 @@ public override void Update(long tickCount) { playersListeningToDebug = playersListeningToDebugNow; - // Defensive: if any upstream system produced a non-finite position, recover instead of - // crashing during packet serialization (Vector3 -> Vector3S conversion can overflow on NaN). - if (!float.IsFinite(Position.X) || !float.IsFinite(Position.Y) || !float.IsFinite(Position.Z)) { - // Recover instead of crashing during packet serialization. - Position = Origin; - Navigation?.ClearPath(); - SendControl = true; - } - if (SendControl && !IsDead) { SequenceCounter++; Field.BroadcastNpcControl(this); diff --git a/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs b/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs index c98bb5ffa..fa7b3c5d7 100644 --- a/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs +++ b/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs @@ -29,16 +29,7 @@ public class FieldSkill : FieldEntity { private readonly long endTick; public long NextTick { get; private set; } private readonly ILogger logger = Log.ForContext(); - private static Vector3 SanitizePosition(Vector3 v) { - if (float.IsNaN(v.X) || float.IsNaN(v.Y) || float.IsNaN(v.Z) || - float.IsInfinity(v.X) || float.IsInfinity(v.Y) || float.IsInfinity(v.Z)) { - return Vector3.Zero; - } - v.X = Math.Clamp(v.X, short.MinValue, short.MaxValue); - v.Y = Math.Clamp(v.Y, short.MinValue, short.MaxValue); - v.Z = Math.Clamp(v.Z, short.MinValue, short.MaxValue); - return v; - } + private ByteWriter GetDamagePacket(DamageRecord record) => Source switch { SkillSource.Cube => SkillDamagePacket.Tile(record), _ => SkillDamagePacket.Region(record), @@ -135,13 +126,13 @@ public override void Update(long tickCount) { SkillMetadataAttack attack = record.Attack; record.TargetUid++; var damage = new DamageRecord(record.Metadata, attack) { - CasterId = record.Caster.ObjectId, - OwnerId = record.Caster.ObjectId, - SkillId = record.SkillId, - Level = record.Level, + CasterId = ObjectId, + OwnerId = ObjectId, + SkillId = Value.Id, + Level = Value.Level, MotionPoint = record.MotionPoint, AttackPoint = record.AttackPoint, - Position = SanitizePosition(Position), + Position = Position, Direction = Rotation, }; var targetRecords = new List(); @@ -224,7 +215,6 @@ public override void Update(long tickCount) { if (targetRecords.Count > 0) { Field.Broadcast(SkillDamagePacket.Target(record, targetRecords)); Field.Broadcast(GetDamagePacket(damage)); - Field.Broadcast(SkillDamagePacket.Damage(damage)); } Caster.ApplyEffects(attack.SkillsOnDamage, Caster, damage, targets: targets); diff --git a/Maple2.Server.Game/Packets/SkillDamagePacket.cs b/Maple2.Server.Game/Packets/SkillDamagePacket.cs index fdef75d66..e7f79fea7 100644 --- a/Maple2.Server.Game/Packets/SkillDamagePacket.cs +++ b/Maple2.Server.Game/Packets/SkillDamagePacket.cs @@ -20,19 +20,7 @@ private enum Command : byte { Unknown7 = 7, Unknown8 = 8, } - private static void WriteVector3SSafe(ByteWriter w, Vector3 v) { - if (float.IsNaN(v.X) || float.IsNaN(v.Y) || float.IsNaN(v.Z) || - float.IsInfinity(v.X) || float.IsInfinity(v.Y) || float.IsInfinity(v.Z)) { - v = Vector3.Zero; - } - - // 注意:4200 没问题,但 NaN/Inf 必须先处理,否则 Clamp 也救不了 - v.X = Math.Clamp(v.X, short.MinValue, short.MaxValue); - v.Y = Math.Clamp(v.Y, short.MinValue, short.MaxValue); - v.Z = Math.Clamp(v.Z, short.MinValue, short.MaxValue); - - w.Write(v); - } + public static ByteWriter Target(SkillRecord record, ICollection targets) { var pWriter = Packet.Of(SendOp.SkillDamage); pWriter.Write(Command.Target); @@ -42,7 +30,7 @@ public static ByteWriter Target(SkillRecord record, ICollection ta pWriter.WriteShort(record.Level); pWriter.WriteByte(record.MotionPoint); pWriter.WriteByte(record.AttackPoint); - WriteVector3SSafe(pWriter, record.Position); // Impact + pWriter.Write(record.Position); // Impact pWriter.Write(record.Direction); // Impact pWriter.WriteBool(true); // SkillId:10600211 only pWriter.WriteInt(record.ServerTick); @@ -65,8 +53,8 @@ public static ByteWriter Damage(DamageRecord record) { pWriter.WriteShort(record.Level); pWriter.WriteByte(record.MotionPoint); pWriter.WriteByte(record.AttackPoint); - WriteVector3SSafe(pWriter, record.Position); // Impact - WriteVector3SSafe(pWriter, record.Direction); + pWriter.Write(record.Position); // Impact + pWriter.Write(record.Direction); pWriter.WriteByte((byte) record.Targets.Count); foreach (DamageRecordTarget target in record.Targets.Values) { diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Field.cs b/Maple2.Server.Game/Trigger/TriggerContext.Field.cs index 4855533cb..c925fbc1a 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Field.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Field.cs @@ -144,9 +144,9 @@ public void SetActor(int triggerId, bool visible, string initialSequence, bool a public void SetAgent(int[] triggerIds, bool visible) { WarnLog("[SetAgent] triggerIds:{Ids}, visible:{Visible}", string.Join(", ", triggerIds), visible); foreach (int triggerId in triggerIds) { - // Some data packs are missing Ms2TriggerAgent entries in DB import. - // Create a lightweight placeholder so the client can still receive updates. - TriggerObjectAgent agent = Objects.GetOrAddAgent(triggerId); + if (!Objects.Agents.TryGetValue(triggerId, out TriggerObjectAgent? agent)) { + continue; + } agent.Visible = visible; Broadcast(TriggerPacket.Update(agent)); @@ -275,9 +275,10 @@ public void SetRandomMesh(int[] triggerIds, bool visible, int startDelay, int in private void UpdateMesh(ArraySegment triggerIds, bool visible, int delay, int interval, int fade = 0) { int intervalTotal = 0; foreach (int triggerId in triggerIds) { - // Some data packs are missing Ms2TriggerMesh entries in DB import. - // Create a lightweight placeholder so the client can still receive updates. - TriggerObjectMesh mesh = Objects.GetOrAddMesh(triggerId); + if (!Objects.Meshes.TryGetValue(triggerId, out TriggerObjectMesh? mesh)) { + logger.Warning("Invalid mesh: {Id}", triggerId); + continue; + } if (mesh.Visible == visible) { continue; } diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs b/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs index 67c8a75bc..3faacf127 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs @@ -211,27 +211,13 @@ public bool MonsterDead(int[] spawnIds, bool autoTarget) { DebugLog("[MonsterDead] spawnIds:{SpawnIds}, arg2:{Arg2}", string.Join(", ", spawnIds), autoTarget); IEnumerable matchingMobs = Field.Mobs.Values.Where(x => spawnIds.Contains(x.SpawnPointId)); - // If no mobs currently exist for these spawnIds, we may still want to treat them as dead - // (e.g. boss already died and was despawned). We use Field.IsSpawnPointDead to track that. - // However, if a spawnId has never existed and is not marked dead, it should NOT be treated as dead - // (prevents instant fail when an NPC failed to spawn). - - foreach (FieldNpc mob in matchingMobs) { if (!mob.IsDead) { return false; } } - // If we found at least one matching mob, reaching here means all of them are dead. - // If we found none, then only consider the spawnIds dead if they are marked as dead by the field. - if (!matchingMobs.Any()) { - foreach (int spawnId in spawnIds) { - if (!Field.IsSpawnPointDead(spawnId)) { - return false; - } - } - } + // Either no mobs were found or they are all dead return true; } diff --git a/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs b/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs index 6e1271e6a..751100069 100644 --- a/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs +++ b/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs @@ -83,23 +83,10 @@ public void Listen(long characterId, PlayerInfoListener listener) { } public void SendUpdate(PlayerUpdateRequest request) { - // 对“上线态/频道/地图”更新非常关键,不能静默吞掉失败 - const int maxRetries = 3; - for (int i = 0; i < maxRetries; i++) { - try { - world.UpdatePlayer(request); - return; - } catch (RpcException ex) { - logger.Warning("SendUpdate(UpdatePlayer) failed attempt {Attempt}/{Max}. Status={Status}", - i + 1, maxRetries, ex.Status); - - // 小退避,避免瞬间打爆 - Thread.Sleep(200 * (i + 1)); - } catch (Exception ex) { - logger.Warning(ex, "SendUpdate(UpdatePlayer) unexpected failure"); - Thread.Sleep(200 * (i + 1)); - } - } + try { + //PlayerInfoCache + world.UpdatePlayer(request); + } catch (RpcException) { /* ignored */ } } public bool ReceiveUpdate(PlayerUpdateRequest request) { diff --git a/Maple2.Server.World/Service/WorldService.Migrate.cs b/Maple2.Server.World/Service/WorldService.Migrate.cs index ef24074a2..ab28813eb 100644 --- a/Maple2.Server.World/Service/WorldService.Migrate.cs +++ b/Maple2.Server.World/Service/WorldService.Migrate.cs @@ -21,7 +21,6 @@ public override Task MigrateOut(MigrateOutRequest request, S switch (request.Server) { case Server.Login: var loginEntry = new TokenEntry(request.Server, request.AccountId, request.CharacterId, new Guid(request.MachineId), 0, 0, 0, 0, 0, MigrationType.Normal); - worldServer.MarkMigrating(request.CharacterId, 45); tokenCache.Set(token, loginEntry, AuthExpiry); return Task.FromResult(new MigrateOutResponse { IpAddress = Target.LoginIp.ToString(), @@ -33,26 +32,21 @@ public override Task MigrateOut(MigrateOutRequest request, S throw new RpcException(new Status(StatusCode.Unavailable, $"No available game channels")); } - // Channel selection priority: - // 1) If the Game server specifies a channel, keep it (even for instanced content). - // This avoids routing players to a dedicated/invalid instanced channel (often 0), - // which can desync presence and cause "fake offline" (client shows 65535). - // 2) Otherwise, if instanced content is requested, use an instanced channel if available. - // 3) Fallback to the first available channel. - - int channel; - if (request.HasChannel && channelClients.TryGetActiveEndpoint(request.Channel, out _)) { - channel = request.Channel; - } else if (request.InstancedContent && channelClients.TryGetInstancedChannelId(out channel)) { + // Try to use requested channel or instanced channel + if (request.InstancedContent && channelClients.TryGetInstancedChannelId(out int channel)) { if (!channelClients.TryGetActiveEndpoint(channel, out _)) { throw new RpcException(new Status(StatusCode.Unavailable, "No available instanced game channel")); } + } else if (request.HasChannel && channelClients.TryGetActiveEndpoint(request.Channel, out _)) { + channel = request.Channel; } else { + // Fall back to first available channel channel = channelClients.FirstChannel(); if (channel == -1) { throw new RpcException(new Status(StatusCode.Unavailable, "No available game channels")); } } + if (!channelClients.TryGetActiveEndpoint(channel, out IPEndPoint? endpoint)) { throw new RpcException(new Status(StatusCode.Unavailable, $"Channel {channel} not found")); } @@ -83,7 +77,6 @@ public override Task MigrateIn(MigrateInRequest request, Serv } tokenCache.Remove(request.Token); - worldServer.ClearMigrating(data.CharacterId); return Task.FromResult(new MigrateInResponse { CharacterId = data.CharacterId, Channel = data.Channel, diff --git a/Maple2.Server.World/WorldServer.cs b/Maple2.Server.World/WorldServer.cs index 9bd131cc1..e283c32de 100644 --- a/Maple2.Server.World/WorldServer.cs +++ b/Maple2.Server.World/WorldServer.cs @@ -30,7 +30,6 @@ public class WorldServer { private readonly EventQueue scheduler; private readonly CancellationTokenSource tokenSource = new(); private readonly ConcurrentDictionary memoryStringBoards; - private readonly System.Collections.Concurrent.ConcurrentDictionary _migratingUntil = new(); private static int _globalIdCounter; private readonly ILogger logger = Log.ForContext(); @@ -124,31 +123,7 @@ public void SetOffline(PlayerInfo playerInfo) { Async = true, }); } - public void MarkMigrating(long characterId, int seconds = 45) { - if (characterId == 0) return; - long until = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + seconds; - _migratingUntil[characterId] = until; - } - - public void ClearMigrating(long characterId) { - if (characterId == 0) return; - _migratingUntil.TryRemove(characterId, out _); - } - - private bool IsMigrating(long characterId) { - if (characterId == 0) return false; - if (!_migratingUntil.TryGetValue(characterId, out long until)) { - return false; - } - - long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - if (now <= until) return true; - - // expired - _migratingUntil.TryRemove(characterId, out _); - return false; - } private void Loop() { while (!tokenSource.Token.IsCancellationRequested) { try { diff --git a/Maple2.Tools/VectorMath/Transform.cs b/Maple2.Tools/VectorMath/Transform.cs index cd2a222e4..65430220c 100644 --- a/Maple2.Tools/VectorMath/Transform.cs +++ b/Maple2.Tools/VectorMath/Transform.cs @@ -138,44 +138,18 @@ public void LookTo(Vector3 direction, bool snapToGroundPlane = true) { } public void LookTo(Vector3 direction, Vector3 up, bool snapToGroundPlane = true) { - // Defensive normalization: callers sometimes pass a zero vector (or already-invalid values), - // which would turn into NaN via Vector3.Normalize and then poison FrontAxis/RightAxis/UpAxis. - // That can later corrupt movement (e.g., skill cast keyframe movement) and ultimately navmesh queries. - if (float.IsNaN(direction.X) || float.IsNaN(direction.Y) || float.IsNaN(direction.Z) || - float.IsInfinity(direction.X) || float.IsInfinity(direction.Y) || float.IsInfinity(direction.Z) || - direction.LengthSquared() < 1e-6f) { - direction = FrontAxis; - } - if (float.IsNaN(up.X) || float.IsNaN(up.Y) || float.IsNaN(up.Z) || - float.IsInfinity(up.X) || float.IsInfinity(up.Y) || float.IsInfinity(up.Z) || - up.LengthSquared() < 1e-6f) { - up = new Vector3(0, 0, 1); - } - direction = Vector3.Normalize(direction); up = Vector3.Normalize(up); if (snapToGroundPlane) { - // Project direction onto plane defined by up. - Vector3 projected = direction - Vector3.Dot(direction, up) * up; - if (float.IsNaN(projected.X) || float.IsNaN(projected.Y) || float.IsNaN(projected.Z) || - float.IsInfinity(projected.X) || float.IsInfinity(projected.Y) || float.IsInfinity(projected.Z) || - projected.LengthSquared() < 1e-6f) { - projected = FrontAxis; + direction = Vector3.Normalize(direction - Vector3.Dot(direction, up) * up); // plane projection formula + + if (direction.IsNearlyEqual(new Vector3(0, 0, 0), 1e-3f)) { + direction = FrontAxis; } - direction = Vector3.Normalize(projected); } Vector3 right = Vector3.Cross(direction, up); - if (float.IsNaN(right.X) || float.IsNaN(right.Y) || float.IsNaN(right.Z) || - float.IsInfinity(right.X) || float.IsInfinity(right.Y) || float.IsInfinity(right.Z) || - right.LengthSquared() < 1e-6f) { - // Fallback: keep current basis if cross product degenerated. - right = RightAxis.LengthSquared() < 1e-6f ? Vector3.UnitX : Vector3.Normalize(RightAxis); - } else { - right = Vector3.Normalize(right); - } - up = Vector3.Cross(right, direction); float scale = Scale; From 94f6024d456c74850a995767ac277d3e86ed7b5e Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Wed, 4 Mar 2026 06:25:57 +0800 Subject: [PATCH 16/26] Reapply "JewelRuby" This reverts commit 1dadbe8f84238312cf270767dc14ae30bf63f170. --- .../Storage/Game/GameStorage.User.cs | 6 +- Maple2.Model/Game/Market/SoldUgcMarketItem.cs | 22 ++-- Maple2.Model/Metadata/Constants.cs | 2 +- Maple2.Server.Game/GameServer.cs | 23 +++- Maple2.Server.Game/Manager/DungeonManager.cs | 2 +- .../Manager/Field/AgentNavigation.cs | 99 ++++++++++++-- .../Field/FieldManager/FieldManager.State.cs | 16 +++ .../Field/FieldManager/FieldManager.cs | 27 +++- .../Manager/Field/FieldManager/IField.cs | 2 + .../Manager/Field/TriggerCollection.cs | 122 ++++++++++++------ Maple2.Server.Game/Manager/FishingManager.cs | 15 +++ .../MovementState.SkillCastTask.cs | 42 ++++-- .../Actor/ActorStateComponent/SkillState.cs | 10 +- .../Model/Field/Actor/FieldNpc.cs | 9 ++ .../Model/Field/Entity/FieldSkill.cs | 22 +++- .../Packets/SkillDamagePacket.cs | 20 ++- .../Trigger/TriggerContext.Field.cs | 13 +- .../Trigger/TriggerContext.Npc.cs | 16 ++- .../Util/Sync/PlayerInfoStorage.cs | 21 ++- .../Service/WorldService.Migrate.cs | 19 ++- Maple2.Server.World/WorldServer.cs | 25 ++++ Maple2.Tools/VectorMath/Transform.cs | 34 ++++- 22 files changed, 443 insertions(+), 124 deletions(-) diff --git a/Maple2.Database/Storage/Game/GameStorage.User.cs b/Maple2.Database/Storage/Game/GameStorage.User.cs index 35dd05f68..722cd849f 100644 --- a/Maple2.Database/Storage/Game/GameStorage.User.cs +++ b/Maple2.Database/Storage/Game/GameStorage.User.cs @@ -582,9 +582,9 @@ public Account CreateAccount(Account account, string password) { model.Password = BCrypt.Net.BCrypt.HashPassword(password, 13); #if DEBUG model.Currency = new AccountCurrency { - Meret = 9_999_999, + Meret = 1_000_00, }; - model.Permissions = AdminPermissions.Admin.ToString(); +// model.Permissions = AdminPermissions.Admin.ToString(); #endif Context.Account.Add(model); Context.SaveChanges(); // Exception if failed. @@ -609,7 +609,7 @@ public Account CreateAccount(Account account, string password) { model.Channel = -1; #if DEBUG model.Currency = new CharacterCurrency { - Meso = 999999999, + Meso = 500000, }; #endif Context.Character.Add(model); diff --git a/Maple2.Model/Game/Market/SoldUgcMarketItem.cs b/Maple2.Model/Game/Market/SoldUgcMarketItem.cs index b8d22bb6a..0b14e1795 100644 --- a/Maple2.Model/Game/Market/SoldUgcMarketItem.cs +++ b/Maple2.Model/Game/Market/SoldUgcMarketItem.cs @@ -10,21 +10,25 @@ public class SoldUgcMarketItem : IByteSerializable { public required string Name { get; init; } public long SoldTime { get; init; } public long AccountId { get; init; } - + public int Count { get; init; } = 1; public void WriteTo(IByteWriter writer) { writer.WriteLong(Id); - writer.WriteLong(); + writer.WriteLong(); writer.WriteUnicodeString(Name); - writer.WriteInt(); - writer.WriteInt(); - writer.WriteLong(); - writer.WriteLong(); - writer.WriteUnicodeString(); - writer.WriteUnicodeString(); - writer.WriteInt(); + + writer.WriteInt(Count); + writer.WriteInt(); + + writer.WriteLong(); + writer.WriteLong(); + writer.WriteUnicodeString(); + writer.WriteUnicodeString(); + writer.WriteInt(); + writer.WriteLong(Price); writer.WriteLong(SoldTime); writer.WriteLong(Profit); } } + diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index 43c74bf53..f5085e784 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -123,7 +123,7 @@ public static class Constant { public const bool DebugTriggers = false; // Set to true to enable debug triggers. (It'll write triggers to files and load triggers from files instead of DB) - public const bool AllowUnicodeInNames = false; // Allow Unicode characters in character and guild names + public const bool AllowUnicodeInNames = true; // Allow Unicode characters in character and guild names public static IReadOnlyDictionary ContentRewards { get; } = new Dictionary { { "miniGame", 1005 }, diff --git a/Maple2.Server.Game/GameServer.cs b/Maple2.Server.Game/GameServer.cs index 7d2524626..b09bc53f9 100644 --- a/Maple2.Server.Game/GameServer.cs +++ b/Maple2.Server.Game/GameServer.cs @@ -66,17 +66,32 @@ public GameServer(FieldManager.Factory fieldFactory, PacketRouter r public override void OnConnected(GameSession session) { lock (mutex) { connectingSessions.Remove(session); - sessions[session.CharacterId] = session; + + if (session.CharacterId != 0) { + sessions[session.CharacterId] = session; + } + + // 可选:避免残留的 0 键占位 + if (sessions.TryGetValue(0, out GameSession? zero) && ReferenceEquals(zero, session)) { + sessions.Remove(0); + } } } - public override void OnDisconnected(GameSession session) { lock (mutex) { connectingSessions.Remove(session); - sessions.Remove(session.CharacterId); + + long cid = session.CharacterId; + if (cid == 0) return; + + // 关键:只允许“当前登记在 sessions 里的那个会话”删除自己 + // 迁移/换图时会出现:新会话先注册,旧会话后断开 + // 如果这里无条件 Remove,会把新会话也删掉 => Heartbeat unknown => 假离线 + if (sessions.TryGetValue(cid, out GameSession? current) && ReferenceEquals(current, session)) { + sessions.Remove(cid); + } } } - public bool GetSession(long characterId, [NotNullWhen(true)] out GameSession? session) { lock (mutex) { return sessions.TryGetValue(characterId, out session); diff --git a/Maple2.Server.Game/Manager/DungeonManager.cs b/Maple2.Server.Game/Manager/DungeonManager.cs index 98d8dcfe4..83a1697ab 100644 --- a/Maple2.Server.Game/Manager/DungeonManager.cs +++ b/Maple2.Server.Game/Manager/DungeonManager.cs @@ -437,7 +437,7 @@ void GetClearDungeonRewards() { ICollection items = []; if (rewardMetadata.UnlimitedDropBoxIds.Length > 0) { foreach (int boxId in rewardMetadata.UnlimitedDropBoxIds) { - items = items.Concat(Lobby.ItemDrop.GetIndividualDropItems(boxId)).ToList(); + items = items.Concat(Lobby.ItemDrop.GetIndividualDropItems(session, session.Player.Value.Character.Level, boxId)).ToList(); items = items.Concat(Lobby.ItemDrop.GetGlobalDropItems(boxId, Metadata.Level)).ToList(); } } diff --git a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs index 8703ee615..93a8fff78 100644 --- a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs +++ b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs @@ -21,6 +21,7 @@ public sealed class AgentNavigation { public List? currentPath = []; private int currentPathIndex = 0; private float currentPathProgress = 0; + private bool currentPathIsFallback = false; public AgentNavigation(FieldNpc fieldNpc, DtCrowdAgent dtAgent, DtCrowd dtCrowd) { npc = fieldNpc; @@ -158,6 +159,10 @@ public AgentNavigation(FieldNpc fieldNpc, DtCrowdAgent dtAgent, DtCrowd dtCrowd) public bool HasPath => currentPath != null && currentPath.Count > 0; + private static bool IsVayarianGuardian(int npcId) { + return npcId is 21500420 or 21500422 or 21500423 or 21500424; + } + public bool UpdatePosition() { if (!field.FindNearestPoly(npc.Position, out _, out RcVec3f position)) { Logger.Error("Failed to find valid position from {Source} => {Position}", npc.Position, position); @@ -216,6 +221,18 @@ public Vector3 GetAgentPosition() { currentPathProgress += (float) timeSpan.TotalSeconds * speed / distance; RcVec3f end = RcVec3f.Lerp(currentPath[currentPathIndex], currentPath[currentPathIndex + 1], currentPathProgress); + if (currentPathIsFallback) { + // Keep the end point on the navmesh each tick; prevents straight-line fallback from pushing NPCs through walls/out of bounds. + if (field.FindNearestPoly(end, out _, out RcVec3f snappedEnd)) { + end = snappedEnd; + if (currentPath is not null && currentPathIndex + 1 < currentPath.Count) { + currentPath[currentPathIndex + 1] = snappedEnd; + } + } else { + // Can't stay on mesh; stop movement. + currentPath = null; + } + } return (start, DotRecastHelper.FromNavMeshSpace(end)); } @@ -294,14 +311,45 @@ public bool PathTo(Vector3 goal) { private bool SetPathTo(RcVec3f target) { currentPath = []; currentPathIndex = 0; + currentPathIsFallback = false; currentPathProgress = 0; + try { - currentPath = FindPath(agent.npos, target); + List? path = FindPath(agent.npos, target); + if (path is null) { + // Clamp target to nearest navmesh polygon to avoid drifting outside walkable area. + if (!field.FindNearestPoly(target, out _, out RcVec3f snappedTarget)) { + Logger.Warning("[Fallback] Could not clamp target to navmesh. Target(nav)={Target}", target); + return false; + } + target = snappedTarget; + Vector3 worldTarget = DotRecastHelper.FromNavMeshSpace(target); + + if (IsVayarianGuardian(npc.Value.Id)) { + // Scripted NPC jump/warp: advance trigger even when navmesh has no valid path. + Logger.Warning("No path for Vayarian Guardian {NpcId}; snapping from {From} to {To}", npc.Value.Id, npc.Position, worldTarget); + UpdatePosition(worldTarget); + currentPath = null; + return true; + } + + Logger.Warning("Failed to find path from {FromNav} to {ToNav}; fallback: clamped straight-line on navmesh. World(from)={FromWorld} World(to)={ToWorld}", agent.npos, target, DotRecastHelper.FromNavMeshSpace(agent.npos), worldTarget); + currentPath = [agent.npos, target]; + return true; + } + + currentPath = path; + return true; } catch (Exception ex) { Logger.Error(ex, "Failed to find path to {Target}", target); + return false; } - - return currentPath is not null; + } + public void ClearPath() { + currentPath?.Clear(); + currentPathIndex = 0; + currentPathProgress = 0; + currentPathIsFallback = false; } public bool PathAwayFrom(Vector3 goal, int distance) { @@ -315,11 +363,20 @@ public bool PathAwayFrom(Vector3 goal, int distance) { // get distance in navmesh space float fDistance = distance * DotRecastHelper.MapRotation.GetRightAxis().Length(); - // get direction from agent to target - RcVec3f direction = RcVec3f.Normalize(RcVec3f.Subtract(target, position)); + // get direction from agent to target (guard against zero-length vectors which would produce NaNs) + RcVec3f delta = RcVec3f.Subtract(target, position); + float lenSqr = RcVec3f.Dot(delta, delta); + RcVec3f direction; + if (lenSqr > 1e-6f) { + direction = RcVec3f.Normalize(delta); + } else { + // If goal overlaps agent (common during melee hit / on-hit stepback), choose a stable fallback direction. + // Alternate per-object id so mobs don't all backstep the same way. + direction = (npc.ObjectId & 1) == 0 ? new RcVec3f(1, 0, 0) : new RcVec3f(-1, 0, 0); + } - // get the point that is fDistance away from the target in the opposite direction - RcVec3f positionAway = RcVec3f.Add(position, RcVec3f.Normalize(direction) * -fDistance); + // get the point that is fDistance away from the agent in the opposite direction + RcVec3f positionAway = RcVec3f.Add(position, direction * -fDistance); // find the nearest poly to the positionAway if (!field.FindNearestPoly(positionAway, out _, out RcVec3f positionAwayNavMesh)) { @@ -328,17 +385,35 @@ public bool PathAwayFrom(Vector3 goal, int distance) { return SetPathAway(positionAwayNavMesh); } - private bool SetPathAway(RcVec3f target) { currentPath = []; currentPathIndex = 0; + currentPathIsFallback = false; currentPathProgress = 0; + try { - currentPath = FindPath(agent.npos, target); + List? path = FindPath(agent.npos, target); + if (path is null) { + Vector3 worldTarget = DotRecastHelper.FromNavMeshSpace(target); + + if (IsVayarianGuardian(npc.Value.Id)) { + // Scripted NPC jump/warp: advance trigger even when navmesh has no valid path. + Logger.Warning("No path for Vayarian Guardian {NpcId}; snapping from {From} to {To}", npc.Value.Id, npc.Position, worldTarget); + UpdatePosition(worldTarget); + currentPath = null; + return true; + } + + Logger.Warning("Failed to find path from {FromNav} to {ToNav}; fallback: clamped straight-line on navmesh. World(from)={FromWorld} World(to)={ToWorld}", agent.npos, target, DotRecastHelper.FromNavMeshSpace(agent.npos), worldTarget); + currentPath = [agent.npos, target]; + return true; + } + + currentPath = path; + return true; } catch (Exception ex) { - Logger.Error(ex, "Failed to find path away from {Target}", target); + Logger.Error(ex, "Failed to find path to {Target}", target); + return false; } - - return currentPath is not null; } } diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index a68abcf28..986d7b3a6 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -46,6 +46,11 @@ public partial class FieldManager { private readonly ConcurrentDictionary fieldItems = new(); private readonly ConcurrentDictionary fieldMobSpawns = new(); private readonly ConcurrentDictionary fieldSpawnPointNpcs = new(); + + + // Tracks spawnPointIds that have died and may have been removed from Mobs/Npcs dictionaries. + // This allows triggers like MonsterDead(spawnId) to continue working even after the dead NPC is despawned. + private readonly ConcurrentDictionary deadSpawnPoints = new(); private readonly ConcurrentDictionary fieldPlayerSpawnPoints = new(); private readonly ConcurrentDictionary fieldSpawnGroups = new(); private readonly ConcurrentDictionary fieldSkills = new(); @@ -115,6 +120,11 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId } public FieldNpc? SpawnNpc(NpcMetadata npc, Vector3 position, Vector3 rotation, bool disableAi = false, FieldMobSpawn? owner = null, SpawnPointNPC? spawnPointNpc = null, string spawnAnimation = "") { + + if (spawnPointNpc is not null) { + deadSpawnPoints.TryRemove(spawnPointNpc.SpawnPointId, out _); + } + // Apply random offset if SpawnRadius is set Vector3 spawnPosition = position; if (spawnPointNpc?.SpawnRadius > 0) { @@ -525,6 +535,8 @@ public void DespawnWorldBoss() { RemoveNpc(objectId); } + public bool IsSpawnPointDead(int spawnId) => deadSpawnPoints.ContainsKey(spawnId); + public void ToggleNpcSpawnPoint(int spawnId) { List spawns = fieldSpawnPointNpcs.Values.Where(spawn => spawn.Value.SpawnPointId == spawnId).ToList(); foreach (FieldSpawnPointNpc spawn in spawns) { @@ -951,6 +963,10 @@ public bool RemoveNpc(int objectId, TimeSpan removeDelay = default) { } Broadcast(FieldPacket.RemoveNpc(objectId)); Broadcast(ProxyObjectPacket.RemoveNpc(objectId)); + + if (npc.IsDead && npc.SpawnPointId != 0) { + deadSpawnPoints.TryAdd(npc.SpawnPointId, 1); + } npc.Dispose(); }, removeDelay); return true; diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs index 4e6cd94fc..3e09c844b 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs @@ -391,13 +391,30 @@ public bool FindNearestPoly(RcVec3f point, out long nearestRef, out RcVec3f posi return false; } - DtStatus status = Navigation.Crowd.GetNavMeshQuery().FindNearestPoly(point, new RcVec3f(2, 4, 2), Navigation.Crowd.GetFilter(0), out nearestRef, out position, out _); - if (status.Failed()) { - logger.Warning("Failed to find nearest poly from position {Source} in field {MapId}", point, MapId); - return false; + // Try progressively larger search extents; helps when an entity gets pushed off the navmesh (e.g., knockback). + // Keep the first extent small for performance, then widen only on failure. + ReadOnlySpan extentsList = stackalloc RcVec3f[] { + new RcVec3f(2, 4, 2), + new RcVec3f(8, 16, 8), + new RcVec3f(32, 64, 32), + new RcVec3f(128, 256, 128), + }; + + DtNavMeshQuery query = Navigation.Crowd.GetNavMeshQuery(); + IDtQueryFilter filter = Navigation.Crowd.GetFilter(0); + + for (int i = 0; i < extentsList.Length; i++) { + RcVec3f ext = extentsList[i]; + DtStatus status = query.FindNearestPoly(point, ext, filter, out nearestRef, out position, out _); + if (status.Succeeded() && nearestRef != 0) { + return true; + } } - return true; + logger.Warning("Failed to find nearest poly from position {Source} in field {MapId}", point, MapId); + nearestRef = 0; + position = default; + return false; } public bool TryGetPlayerById(long characterId, [NotNullWhen(true)] out FieldPlayer? player) { diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs b/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs index 3b3c93eb3..2bdcbc5ba 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs @@ -95,6 +95,8 @@ public virtual void Init() { } public bool LiftupCube(in Vector3B coordinates, [NotNullWhen(true)] out LiftupWeapon? liftupWeapon); public void MovePlayerAlongPath(string pathName); + public bool IsSpawnPointDead(int spawnId); + public bool RemoveNpc(int objectId, TimeSpan removeDelay = default); public bool RemovePet(int objectId, TimeSpan removeDelay = default); } diff --git a/Maple2.Server.Game/Manager/Field/TriggerCollection.cs b/Maple2.Server.Game/Manager/Field/TriggerCollection.cs index b76c198c3..1efa4ec9e 100644 --- a/Maple2.Server.Game/Manager/Field/TriggerCollection.cs +++ b/Maple2.Server.Game/Manager/Field/TriggerCollection.cs @@ -1,36 +1,55 @@ using System.Collections; +using System.Collections.Generic; using Maple2.Model.Game; using Maple2.Model.Metadata; namespace Maple2.Server.Game.Manager.Field; public sealed class TriggerCollection : IReadOnlyCollection { - public readonly IReadOnlyDictionary Actors; - public readonly IReadOnlyDictionary Cameras; - public readonly IReadOnlyDictionary Cubes; - public readonly IReadOnlyDictionary Effects; - public readonly IReadOnlyDictionary Ladders; - public readonly IReadOnlyDictionary Meshes; - public readonly IReadOnlyDictionary Ropes; - public readonly IReadOnlyDictionary Sounds; - public readonly IReadOnlyDictionary Agents; - - public readonly IReadOnlyDictionary Boxes; + // NOTE: These collections need to be mutable at runtime. + // Some map packs / DB imports are missing Ms2TriggerMesh / Ms2TriggerAgent entries, + // but trigger scripts still reference those IDs (set_mesh / set_agent). + // The client already knows the objects by triggerId (from the map file), and only + // needs server updates to toggle visibility/collision. + // + // We therefore support creating lightweight placeholder trigger objects on demand. + private readonly Dictionary actors; + private readonly Dictionary cameras; + private readonly Dictionary cubes; + private readonly Dictionary effects; + private readonly Dictionary ladders; + private readonly Dictionary meshes; + private readonly Dictionary ropes; + private readonly Dictionary sounds; + private readonly Dictionary agents; + private readonly Dictionary boxes; + + public IReadOnlyDictionary Actors => actors; + public IReadOnlyDictionary Cameras => cameras; + public IReadOnlyDictionary Cubes => cubes; + public IReadOnlyDictionary Effects => effects; + public IReadOnlyDictionary Ladders => ladders; + public IReadOnlyDictionary Meshes => meshes; + public IReadOnlyDictionary Ropes => ropes; + public IReadOnlyDictionary Sounds => sounds; + public IReadOnlyDictionary Agents => agents; + + public IReadOnlyDictionary Boxes => boxes; // These seem to get managed separately... // private readonly IReadOnlyDictionary Agents; // private readonly IReadOnlyDictionary Skills; public TriggerCollection(MapEntityMetadata entities) { - Dictionary actors = new(); - Dictionary cameras = new(); - Dictionary cubes = new(); - Dictionary effects = new(); - Dictionary ladders = new(); - Dictionary meshes = new(); - Dictionary ropes = new(); - Dictionary sounds = new(); - Dictionary agents = new(); + actors = new(); + cameras = new(); + cubes = new(); + effects = new(); + ladders = new(); + meshes = new(); + ropes = new(); + sounds = new(); + agents = new(); foreach (Ms2TriggerActor actor in entities.Trigger.Actors) { actors[actor.TriggerId] = new TriggerObjectActor(actor); @@ -61,36 +80,55 @@ public TriggerCollection(MapEntityMetadata entities) { agents[agent.TriggerId] = new TriggerObjectAgent(agent); } - Actors = actors; - Cameras = cameras; - Cubes = cubes; - Effects = effects; - Ladders = ladders; - Meshes = meshes; - Ropes = ropes; - Sounds = sounds; - Agents = agents; - - Dictionary boxes = new(); + boxes = new(); foreach (Ms2TriggerBox box in entities.Trigger.Boxes) { boxes[box.TriggerId] = new TriggerBox(box); } + } + + /// + /// Creates or retrieves a Trigger Mesh placeholder. Useful when a trigger script references + /// a mesh id that is missing from the DB import. + /// + public TriggerObjectMesh GetOrAddMesh(int triggerId) { + if (meshes.TryGetValue(triggerId, out TriggerObjectMesh? mesh)) { + return mesh; + } + + // Scale/minimapInvisible are not important for updates; the client already has the actual mesh. + Ms2TriggerMesh meta = new Ms2TriggerMesh(1f, triggerId, Visible: true, MinimapInvisible: false); + mesh = new TriggerObjectMesh(meta); + meshes[triggerId] = mesh; + return mesh; + } + + /// + /// Creates or retrieves a Trigger Agent placeholder. Useful when a trigger script references + /// an agent id that is missing from the DB import. + /// + public TriggerObjectAgent GetOrAddAgent(int triggerId) { + if (agents.TryGetValue(triggerId, out TriggerObjectAgent? agent)) { + return agent; + } - Boxes = boxes; + Ms2TriggerAgent meta = new Ms2TriggerAgent(triggerId, Visible: true); + agent = new TriggerObjectAgent(meta); + agents[triggerId] = agent; + return agent; } - public int Count => Actors.Count + Cameras.Count + Cubes.Count + Effects.Count + Ladders.Count + Meshes.Count + Ropes.Count + Sounds.Count + Agents.Count; + public int Count => actors.Count + cameras.Count + cubes.Count + effects.Count + ladders.Count + meshes.Count + ropes.Count + sounds.Count + agents.Count; public IEnumerator GetEnumerator() { - foreach (TriggerObjectActor actor in Actors.Values) yield return actor; - foreach (TriggerObjectCamera camera in Cameras.Values) yield return camera; - foreach (TriggerObjectCube cube in Cubes.Values) yield return cube; - foreach (TriggerObjectEffect effect in Effects.Values) yield return effect; - foreach (TriggerObjectLadder ladder in Ladders.Values) yield return ladder; - foreach (TriggerObjectMesh mesh in Meshes.Values) yield return mesh; - foreach (TriggerObjectRope rope in Ropes.Values) yield return rope; - foreach (TriggerObjectSound sound in Sounds.Values) yield return sound; - foreach (TriggerObjectAgent agent in Agents.Values) yield return agent; + foreach (TriggerObjectActor actor in actors.Values) yield return actor; + foreach (TriggerObjectCamera camera in cameras.Values) yield return camera; + foreach (TriggerObjectCube cube in cubes.Values) yield return cube; + foreach (TriggerObjectEffect effect in effects.Values) yield return effect; + foreach (TriggerObjectLadder ladder in ladders.Values) yield return ladder; + foreach (TriggerObjectMesh mesh in meshes.Values) yield return mesh; + foreach (TriggerObjectRope rope in ropes.Values) yield return rope; + foreach (TriggerObjectSound sound in sounds.Values) yield return sound; + foreach (TriggerObjectAgent agent in agents.Values) yield return agent; } IEnumerator IEnumerable.GetEnumerator() { diff --git a/Maple2.Server.Game/Manager/FishingManager.cs b/Maple2.Server.Game/Manager/FishingManager.cs index aebd61425..60c5b94a4 100644 --- a/Maple2.Server.Game/Manager/FishingManager.cs +++ b/Maple2.Server.Game/Manager/FishingManager.cs @@ -336,6 +336,21 @@ private void AddFish(int fishSize, bool hasAutoFish) { session.Field.Broadcast(FishingPacket.PrizeFish(session.PlayerName, selectedFish.Id)); } session.ConditionUpdate(ConditionType.fish, codeLong: selectedFish.Id, targetLong: session.Field.MapId); + // ====== 钓鱼给角色经验 ====== + long charExp = selectedFish.Exp; + + // 可选倍率:首次 / 奖牌鱼 + if (caughtFishType == CaughtFishType.FirstKind) { + charExp *= 2; + } + if (caughtFishType == CaughtFishType.Prize) { + charExp *= 2; + } + + if (charExp > 0) { + session.Exp.AddExp(ExpType.fishing, modifier: 0f, additionalExp: charExp); + } + // ====== 角色经验结束 ====== if (masteryExp == 0) { return; diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs index 2eb377053..ce570eb6f 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.SkillCastTask.cs @@ -41,8 +41,17 @@ protected override void TaskFinished(bool isCompleted) { private void SkillCastFaceTarget(SkillRecord cast, IActor target, int faceTarget) { Vector3 offset = target.Position - actor.Position; - float distance = offset.LengthSquared(); - + float distance = offset.X * offset.X + offset.Y * offset.Y; + float vertical = MathF.Abs(offset.Z); + + // In MS2 data, many AI skill nodes use FaceTarget=0 while the skill's AutoTargeting.MaxDegree + // is a narrow cone. If we gate turning by MaxDegree (dot product), the NPC can end up casting + // while facing away (target behind the cone) and never correct its facing. + // + // To avoid "背对玩家放技能", we always rotate to face the current target when: + // - the motion requests FaceTarget, and + // - AutoTargeting distance/height constraints allow it. + // MaxDegree is ignored for *turning*. if (faceTarget != 1) { if (!cast.Motion.MotionProperty.FaceTarget || cast.Metadata.Data.AutoTargeting is null) { return; @@ -50,26 +59,28 @@ private void SkillCastFaceTarget(SkillRecord cast, IActor target, int faceTarget var autoTargeting = cast.Metadata.Data.AutoTargeting; - bool shouldFaceTarget = autoTargeting.MaxDistance == 0 || distance <= autoTargeting.MaxDistance; - shouldFaceTarget |= autoTargeting.MaxHeight == 0 || offset.Y <= autoTargeting.MaxHeight; + bool inRange = autoTargeting.MaxDistance == 0 || distance <= autoTargeting.MaxDistance * autoTargeting.MaxDistance; + inRange &= autoTargeting.MaxHeight == 0 || vertical <= autoTargeting.MaxHeight; + if (!inRange) { + return; + } - if (!shouldFaceTarget) { + if (distance < 0.0001f) { return; } distance = (float) Math.Sqrt(distance); offset *= (1 / distance); + } else { + if (distance < 0.0001f) { - float degreeCosine = (float) Math.Cos(autoTargeting.MaxDegree / 2); - float dot = Vector3.Dot(offset, actor.Transform.FrontAxis); - - shouldFaceTarget = autoTargeting.MaxDegree == 0 || dot >= degreeCosine; - - if (!shouldFaceTarget) { return; + } - } else { + + distance = (float) Math.Sqrt(distance); + offset *= (1 / distance); } @@ -104,9 +115,12 @@ private void SkillCast(NpcSkillCastTask task, int id, short level, long uid, byt } if (task.FacePos != new Vector3(0, 0, 0)) { - actor.Transform.LookTo(Vector3.Normalize(task.FacePos - actor.Position)); + actor.Transform.LookTo(task.FacePos - actor.Position); // safe: LookTo normalizes with guards } else if (actor.BattleState.Target is not null) { - SkillCastFaceTarget(cast, actor.BattleState.Target, task.FaceTarget); + // Hard guarantee: NPCs should always face their current battle target when casting. + // Some boss skills have MotionProperty.FaceTarget=false or narrow AutoTargeting degrees, + // which previously allowed casting while facing away. + actor.Transform.LookTo(actor.BattleState.Target.Position - actor.Position); // safe: LookTo normalizes with guards } CastTask = task; diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs index 6e53abf3d..4d6577958 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs @@ -54,7 +54,15 @@ public void SkillCastAttack(SkillRecord cast, byte attackPoint, List att if (attackTargets.Count > targetIndex) { // if attack.direction == 3, use direction to target, if attack.direction == 0, use rotation maybe? cast.Position = actor.Position; - cast.Direction = Vector3.Normalize(attackTargets[targetIndex].Position - actor.Position); + Vector3 dir = attackTargets[targetIndex].Position - actor.Position; + if (float.IsNaN(dir.X) || float.IsNaN(dir.Y) || float.IsNaN(dir.Z) || + float.IsInfinity(dir.X) || float.IsInfinity(dir.Y) || float.IsInfinity(dir.Z) || + dir.LengthSquared() < 1e-6f) { + // Keep current facing if target is on top of caster. + cast.Direction = actor.Transform.FrontAxis; + } else { + cast.Direction = Vector3.Normalize(dir); + } } actor.Field.Broadcast(SkillDamagePacket.Target(cast, targets)); diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index 4208ae688..42874181e 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -197,6 +197,15 @@ public override void Update(long tickCount) { playersListeningToDebug = playersListeningToDebugNow; + // Defensive: if any upstream system produced a non-finite position, recover instead of + // crashing during packet serialization (Vector3 -> Vector3S conversion can overflow on NaN). + if (!float.IsFinite(Position.X) || !float.IsFinite(Position.Y) || !float.IsFinite(Position.Z)) { + // Recover instead of crashing during packet serialization. + Position = Origin; + Navigation?.ClearPath(); + SendControl = true; + } + if (SendControl && !IsDead) { SequenceCounter++; Field.BroadcastNpcControl(this); diff --git a/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs b/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs index fa7b3c5d7..c98bb5ffa 100644 --- a/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs +++ b/Maple2.Server.Game/Model/Field/Entity/FieldSkill.cs @@ -29,7 +29,16 @@ public class FieldSkill : FieldEntity { private readonly long endTick; public long NextTick { get; private set; } private readonly ILogger logger = Log.ForContext(); - + private static Vector3 SanitizePosition(Vector3 v) { + if (float.IsNaN(v.X) || float.IsNaN(v.Y) || float.IsNaN(v.Z) || + float.IsInfinity(v.X) || float.IsInfinity(v.Y) || float.IsInfinity(v.Z)) { + return Vector3.Zero; + } + v.X = Math.Clamp(v.X, short.MinValue, short.MaxValue); + v.Y = Math.Clamp(v.Y, short.MinValue, short.MaxValue); + v.Z = Math.Clamp(v.Z, short.MinValue, short.MaxValue); + return v; + } private ByteWriter GetDamagePacket(DamageRecord record) => Source switch { SkillSource.Cube => SkillDamagePacket.Tile(record), _ => SkillDamagePacket.Region(record), @@ -126,13 +135,13 @@ public override void Update(long tickCount) { SkillMetadataAttack attack = record.Attack; record.TargetUid++; var damage = new DamageRecord(record.Metadata, attack) { - CasterId = ObjectId, - OwnerId = ObjectId, - SkillId = Value.Id, - Level = Value.Level, + CasterId = record.Caster.ObjectId, + OwnerId = record.Caster.ObjectId, + SkillId = record.SkillId, + Level = record.Level, MotionPoint = record.MotionPoint, AttackPoint = record.AttackPoint, - Position = Position, + Position = SanitizePosition(Position), Direction = Rotation, }; var targetRecords = new List(); @@ -215,6 +224,7 @@ public override void Update(long tickCount) { if (targetRecords.Count > 0) { Field.Broadcast(SkillDamagePacket.Target(record, targetRecords)); Field.Broadcast(GetDamagePacket(damage)); + Field.Broadcast(SkillDamagePacket.Damage(damage)); } Caster.ApplyEffects(attack.SkillsOnDamage, Caster, damage, targets: targets); diff --git a/Maple2.Server.Game/Packets/SkillDamagePacket.cs b/Maple2.Server.Game/Packets/SkillDamagePacket.cs index e7f79fea7..fdef75d66 100644 --- a/Maple2.Server.Game/Packets/SkillDamagePacket.cs +++ b/Maple2.Server.Game/Packets/SkillDamagePacket.cs @@ -20,7 +20,19 @@ private enum Command : byte { Unknown7 = 7, Unknown8 = 8, } - + private static void WriteVector3SSafe(ByteWriter w, Vector3 v) { + if (float.IsNaN(v.X) || float.IsNaN(v.Y) || float.IsNaN(v.Z) || + float.IsInfinity(v.X) || float.IsInfinity(v.Y) || float.IsInfinity(v.Z)) { + v = Vector3.Zero; + } + + // 注意:4200 没问题,但 NaN/Inf 必须先处理,否则 Clamp 也救不了 + v.X = Math.Clamp(v.X, short.MinValue, short.MaxValue); + v.Y = Math.Clamp(v.Y, short.MinValue, short.MaxValue); + v.Z = Math.Clamp(v.Z, short.MinValue, short.MaxValue); + + w.Write(v); + } public static ByteWriter Target(SkillRecord record, ICollection targets) { var pWriter = Packet.Of(SendOp.SkillDamage); pWriter.Write(Command.Target); @@ -30,7 +42,7 @@ public static ByteWriter Target(SkillRecord record, ICollection ta pWriter.WriteShort(record.Level); pWriter.WriteByte(record.MotionPoint); pWriter.WriteByte(record.AttackPoint); - pWriter.Write(record.Position); // Impact + WriteVector3SSafe(pWriter, record.Position); // Impact pWriter.Write(record.Direction); // Impact pWriter.WriteBool(true); // SkillId:10600211 only pWriter.WriteInt(record.ServerTick); @@ -53,8 +65,8 @@ public static ByteWriter Damage(DamageRecord record) { pWriter.WriteShort(record.Level); pWriter.WriteByte(record.MotionPoint); pWriter.WriteByte(record.AttackPoint); - pWriter.Write(record.Position); // Impact - pWriter.Write(record.Direction); + WriteVector3SSafe(pWriter, record.Position); // Impact + WriteVector3SSafe(pWriter, record.Direction); pWriter.WriteByte((byte) record.Targets.Count); foreach (DamageRecordTarget target in record.Targets.Values) { diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Field.cs b/Maple2.Server.Game/Trigger/TriggerContext.Field.cs index c925fbc1a..4855533cb 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Field.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Field.cs @@ -144,9 +144,9 @@ public void SetActor(int triggerId, bool visible, string initialSequence, bool a public void SetAgent(int[] triggerIds, bool visible) { WarnLog("[SetAgent] triggerIds:{Ids}, visible:{Visible}", string.Join(", ", triggerIds), visible); foreach (int triggerId in triggerIds) { - if (!Objects.Agents.TryGetValue(triggerId, out TriggerObjectAgent? agent)) { - continue; - } + // Some data packs are missing Ms2TriggerAgent entries in DB import. + // Create a lightweight placeholder so the client can still receive updates. + TriggerObjectAgent agent = Objects.GetOrAddAgent(triggerId); agent.Visible = visible; Broadcast(TriggerPacket.Update(agent)); @@ -275,10 +275,9 @@ public void SetRandomMesh(int[] triggerIds, bool visible, int startDelay, int in private void UpdateMesh(ArraySegment triggerIds, bool visible, int delay, int interval, int fade = 0) { int intervalTotal = 0; foreach (int triggerId in triggerIds) { - if (!Objects.Meshes.TryGetValue(triggerId, out TriggerObjectMesh? mesh)) { - logger.Warning("Invalid mesh: {Id}", triggerId); - continue; - } + // Some data packs are missing Ms2TriggerMesh entries in DB import. + // Create a lightweight placeholder so the client can still receive updates. + TriggerObjectMesh mesh = Objects.GetOrAddMesh(triggerId); if (mesh.Visible == visible) { continue; } diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs b/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs index 3faacf127..67c8a75bc 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs @@ -211,13 +211,27 @@ public bool MonsterDead(int[] spawnIds, bool autoTarget) { DebugLog("[MonsterDead] spawnIds:{SpawnIds}, arg2:{Arg2}", string.Join(", ", spawnIds), autoTarget); IEnumerable matchingMobs = Field.Mobs.Values.Where(x => spawnIds.Contains(x.SpawnPointId)); + // If no mobs currently exist for these spawnIds, we may still want to treat them as dead + // (e.g. boss already died and was despawned). We use Field.IsSpawnPointDead to track that. + // However, if a spawnId has never existed and is not marked dead, it should NOT be treated as dead + // (prevents instant fail when an NPC failed to spawn). + + foreach (FieldNpc mob in matchingMobs) { if (!mob.IsDead) { return false; } } - // Either no mobs were found or they are all dead + // If we found at least one matching mob, reaching here means all of them are dead. + // If we found none, then only consider the spawnIds dead if they are marked as dead by the field. + if (!matchingMobs.Any()) { + foreach (int spawnId in spawnIds) { + if (!Field.IsSpawnPointDead(spawnId)) { + return false; + } + } + } return true; } diff --git a/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs b/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs index 751100069..6e1271e6a 100644 --- a/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs +++ b/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs @@ -83,10 +83,23 @@ public void Listen(long characterId, PlayerInfoListener listener) { } public void SendUpdate(PlayerUpdateRequest request) { - try { - //PlayerInfoCache - world.UpdatePlayer(request); - } catch (RpcException) { /* ignored */ } + // 对“上线态/频道/地图”更新非常关键,不能静默吞掉失败 + const int maxRetries = 3; + for (int i = 0; i < maxRetries; i++) { + try { + world.UpdatePlayer(request); + return; + } catch (RpcException ex) { + logger.Warning("SendUpdate(UpdatePlayer) failed attempt {Attempt}/{Max}. Status={Status}", + i + 1, maxRetries, ex.Status); + + // 小退避,避免瞬间打爆 + Thread.Sleep(200 * (i + 1)); + } catch (Exception ex) { + logger.Warning(ex, "SendUpdate(UpdatePlayer) unexpected failure"); + Thread.Sleep(200 * (i + 1)); + } + } } public bool ReceiveUpdate(PlayerUpdateRequest request) { diff --git a/Maple2.Server.World/Service/WorldService.Migrate.cs b/Maple2.Server.World/Service/WorldService.Migrate.cs index ab28813eb..ef24074a2 100644 --- a/Maple2.Server.World/Service/WorldService.Migrate.cs +++ b/Maple2.Server.World/Service/WorldService.Migrate.cs @@ -21,6 +21,7 @@ public override Task MigrateOut(MigrateOutRequest request, S switch (request.Server) { case Server.Login: var loginEntry = new TokenEntry(request.Server, request.AccountId, request.CharacterId, new Guid(request.MachineId), 0, 0, 0, 0, 0, MigrationType.Normal); + worldServer.MarkMigrating(request.CharacterId, 45); tokenCache.Set(token, loginEntry, AuthExpiry); return Task.FromResult(new MigrateOutResponse { IpAddress = Target.LoginIp.ToString(), @@ -32,21 +33,26 @@ public override Task MigrateOut(MigrateOutRequest request, S throw new RpcException(new Status(StatusCode.Unavailable, $"No available game channels")); } - // Try to use requested channel or instanced channel - if (request.InstancedContent && channelClients.TryGetInstancedChannelId(out int channel)) { + // Channel selection priority: + // 1) If the Game server specifies a channel, keep it (even for instanced content). + // This avoids routing players to a dedicated/invalid instanced channel (often 0), + // which can desync presence and cause "fake offline" (client shows 65535). + // 2) Otherwise, if instanced content is requested, use an instanced channel if available. + // 3) Fallback to the first available channel. + + int channel; + if (request.HasChannel && channelClients.TryGetActiveEndpoint(request.Channel, out _)) { + channel = request.Channel; + } else if (request.InstancedContent && channelClients.TryGetInstancedChannelId(out channel)) { if (!channelClients.TryGetActiveEndpoint(channel, out _)) { throw new RpcException(new Status(StatusCode.Unavailable, "No available instanced game channel")); } - } else if (request.HasChannel && channelClients.TryGetActiveEndpoint(request.Channel, out _)) { - channel = request.Channel; } else { - // Fall back to first available channel channel = channelClients.FirstChannel(); if (channel == -1) { throw new RpcException(new Status(StatusCode.Unavailable, "No available game channels")); } } - if (!channelClients.TryGetActiveEndpoint(channel, out IPEndPoint? endpoint)) { throw new RpcException(new Status(StatusCode.Unavailable, $"Channel {channel} not found")); } @@ -77,6 +83,7 @@ public override Task MigrateIn(MigrateInRequest request, Serv } tokenCache.Remove(request.Token); + worldServer.ClearMigrating(data.CharacterId); return Task.FromResult(new MigrateInResponse { CharacterId = data.CharacterId, Channel = data.Channel, diff --git a/Maple2.Server.World/WorldServer.cs b/Maple2.Server.World/WorldServer.cs index e283c32de..9bd131cc1 100644 --- a/Maple2.Server.World/WorldServer.cs +++ b/Maple2.Server.World/WorldServer.cs @@ -30,6 +30,7 @@ public class WorldServer { private readonly EventQueue scheduler; private readonly CancellationTokenSource tokenSource = new(); private readonly ConcurrentDictionary memoryStringBoards; + private readonly System.Collections.Concurrent.ConcurrentDictionary _migratingUntil = new(); private static int _globalIdCounter; private readonly ILogger logger = Log.ForContext(); @@ -123,7 +124,31 @@ public void SetOffline(PlayerInfo playerInfo) { Async = true, }); } + public void MarkMigrating(long characterId, int seconds = 45) { + if (characterId == 0) return; + long until = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + seconds; + _migratingUntil[characterId] = until; + } + + public void ClearMigrating(long characterId) { + if (characterId == 0) return; + _migratingUntil.TryRemove(characterId, out _); + } + + private bool IsMigrating(long characterId) { + if (characterId == 0) return false; + if (!_migratingUntil.TryGetValue(characterId, out long until)) { + return false; + } + + long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + if (now <= until) return true; + + // expired + _migratingUntil.TryRemove(characterId, out _); + return false; + } private void Loop() { while (!tokenSource.Token.IsCancellationRequested) { try { diff --git a/Maple2.Tools/VectorMath/Transform.cs b/Maple2.Tools/VectorMath/Transform.cs index 65430220c..cd2a222e4 100644 --- a/Maple2.Tools/VectorMath/Transform.cs +++ b/Maple2.Tools/VectorMath/Transform.cs @@ -138,18 +138,44 @@ public void LookTo(Vector3 direction, bool snapToGroundPlane = true) { } public void LookTo(Vector3 direction, Vector3 up, bool snapToGroundPlane = true) { + // Defensive normalization: callers sometimes pass a zero vector (or already-invalid values), + // which would turn into NaN via Vector3.Normalize and then poison FrontAxis/RightAxis/UpAxis. + // That can later corrupt movement (e.g., skill cast keyframe movement) and ultimately navmesh queries. + if (float.IsNaN(direction.X) || float.IsNaN(direction.Y) || float.IsNaN(direction.Z) || + float.IsInfinity(direction.X) || float.IsInfinity(direction.Y) || float.IsInfinity(direction.Z) || + direction.LengthSquared() < 1e-6f) { + direction = FrontAxis; + } + if (float.IsNaN(up.X) || float.IsNaN(up.Y) || float.IsNaN(up.Z) || + float.IsInfinity(up.X) || float.IsInfinity(up.Y) || float.IsInfinity(up.Z) || + up.LengthSquared() < 1e-6f) { + up = new Vector3(0, 0, 1); + } + direction = Vector3.Normalize(direction); up = Vector3.Normalize(up); if (snapToGroundPlane) { - direction = Vector3.Normalize(direction - Vector3.Dot(direction, up) * up); // plane projection formula - - if (direction.IsNearlyEqual(new Vector3(0, 0, 0), 1e-3f)) { - direction = FrontAxis; + // Project direction onto plane defined by up. + Vector3 projected = direction - Vector3.Dot(direction, up) * up; + if (float.IsNaN(projected.X) || float.IsNaN(projected.Y) || float.IsNaN(projected.Z) || + float.IsInfinity(projected.X) || float.IsInfinity(projected.Y) || float.IsInfinity(projected.Z) || + projected.LengthSquared() < 1e-6f) { + projected = FrontAxis; } + direction = Vector3.Normalize(projected); } Vector3 right = Vector3.Cross(direction, up); + if (float.IsNaN(right.X) || float.IsNaN(right.Y) || float.IsNaN(right.Z) || + float.IsInfinity(right.X) || float.IsInfinity(right.Y) || float.IsInfinity(right.Z) || + right.LengthSquared() < 1e-6f) { + // Fallback: keep current basis if cross product degenerated. + right = RightAxis.LengthSquared() < 1e-6f ? Vector3.UnitX : Vector3.Normalize(RightAxis); + } else { + right = Vector3.Normalize(right); + } + up = Vector3.Cross(right, direction); float scale = Scale; From c034216f23fa2ce20dd11eb12a054fd0219f310a Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Thu, 5 Mar 2026 19:57:52 +0800 Subject: [PATCH 17/26] Update RestrictedBuyData.cs --- Maple2.Model/Game/Shop/RestrictedBuyData.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Maple2.Model/Game/Shop/RestrictedBuyData.cs b/Maple2.Model/Game/Shop/RestrictedBuyData.cs index 6690f24b4..8edbc8692 100644 --- a/Maple2.Model/Game/Shop/RestrictedBuyData.cs +++ b/Maple2.Model/Game/Shop/RestrictedBuyData.cs @@ -49,9 +49,11 @@ public readonly struct BuyTimeOfDay { public int EndTimeOfDay { get; } // time end in seconds. ex 10600 = 2:56 AM [JsonConstructor] - public BuyTimeOfDay(int startTime, int endTime) { - StartTimeOfDay = startTime; - EndTimeOfDay = endTime; + // System.Text.Json requires every parameter on the [JsonConstructor] to bind to a + // property/field on the type. Parameter names must match property names (case-insensitive). + public BuyTimeOfDay(int startTimeOfDay, int endTimeOfDay) { + StartTimeOfDay = startTimeOfDay; + EndTimeOfDay = endTimeOfDay; } public BuyTimeOfDay Clone() { From ba981db6f97474254b693e5d53dda736b5ac37ec Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Fri, 6 Mar 2026 08:04:53 +0800 Subject: [PATCH 18/26] Optimize the server archiving mechanism --- Maple2.Server.Game/Program.cs | 3 + Maple2.Server.Game/Service/AutoSaveService.cs | 69 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 Maple2.Server.Game/Service/AutoSaveService.cs diff --git a/Maple2.Server.Game/Program.cs b/Maple2.Server.Game/Program.cs index 66c002a9b..78ca3a023 100644 --- a/Maple2.Server.Game/Program.cs +++ b/Maple2.Server.Game/Program.cs @@ -106,6 +106,9 @@ )); builder.Services.AddHostedService(provider => provider.GetService()!); +// Periodically persist online player state so restarts don't lose progress. +builder.Services.AddHostedService(); + builder.Services.AddGrpcHealthChecks(); builder.Services.Configure(options => { options.Delay = TimeSpan.Zero; diff --git a/Maple2.Server.Game/Service/AutoSaveService.cs b/Maple2.Server.Game/Service/AutoSaveService.cs new file mode 100644 index 000000000..201f31077 --- /dev/null +++ b/Maple2.Server.Game/Service/AutoSaveService.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Maple2.Server.Game; +using Maple2.Server.Game.Session; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Maple2.Server.Game.Service; + +/// +/// Periodically saves online player state so progress isn't lost if the server restarts. +/// +/// Why here (Server.Game): +/// - The database/storage layer doesn't know which players are online. +/// - GameServer owns the live sessions, so it can safely iterate and call SessionSave(). +/// +public sealed class AutoSaveService : BackgroundService { + private static readonly TimeSpan SaveInterval = TimeSpan.FromSeconds(60); + + private readonly GameServer gameServer; + private readonly ILogger logger; + + public AutoSaveService(GameServer gameServer, ILogger logger) { + this.gameServer = gameServer; + this.logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + // Small startup delay so we don't compete with initial login/boot work. + try { + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + } catch (OperationCanceledException) { + return; + } + + while (!stoppingToken.IsCancellationRequested) { + try { + int saved = 0; + + // Snapshot current sessions to avoid issues if the collection changes mid-iteration. + GameSession[] sessions = gameServer.GetSessions().ToArray(); + foreach (GameSession session in sessions) { + if (stoppingToken.IsCancellationRequested) break; + if (session.Player == null) continue; + + // SessionSave() is internally locked and already checks for null Player. + session.SessionSave(); + saved++; + } + + if (saved > 0) { + logger.LogInformation("[AutoSave] Saved {Count} online session(s).", saved); + } + } catch (OperationCanceledException) { + // Normal shutdown. + } catch (Exception ex) { + logger.LogError(ex, "[AutoSave] Unexpected error while saving sessions."); + } + + try { + await Task.Delay(SaveInterval, stoppingToken); + } catch (OperationCanceledException) { + break; + } + } + } +} From 18655d1c5d6ec2c3a1eed6f58b23b2651d5f2fe9 Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Fri, 6 Mar 2026 17:24:07 +0800 Subject: [PATCH 19/26] Revert "Update RestrictedBuyData.cs" This reverts commit c034216f23fa2ce20dd11eb12a054fd0219f310a. --- Maple2.Model/Game/Shop/RestrictedBuyData.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Maple2.Model/Game/Shop/RestrictedBuyData.cs b/Maple2.Model/Game/Shop/RestrictedBuyData.cs index 8edbc8692..6690f24b4 100644 --- a/Maple2.Model/Game/Shop/RestrictedBuyData.cs +++ b/Maple2.Model/Game/Shop/RestrictedBuyData.cs @@ -49,11 +49,9 @@ public readonly struct BuyTimeOfDay { public int EndTimeOfDay { get; } // time end in seconds. ex 10600 = 2:56 AM [JsonConstructor] - // System.Text.Json requires every parameter on the [JsonConstructor] to bind to a - // property/field on the type. Parameter names must match property names (case-insensitive). - public BuyTimeOfDay(int startTimeOfDay, int endTimeOfDay) { - StartTimeOfDay = startTimeOfDay; - EndTimeOfDay = endTimeOfDay; + public BuyTimeOfDay(int startTime, int endTime) { + StartTimeOfDay = startTime; + EndTimeOfDay = endTime; } public BuyTimeOfDay Clone() { From 517fe84f40841f44a3088361424c9ec2a7612fd1 Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Fri, 6 Mar 2026 17:33:54 +0800 Subject: [PATCH 20/26] Reapply "Update RestrictedBuyData.cs" This reverts commit 18655d1c5d6ec2c3a1eed6f58b23b2651d5f2fe9. --- Maple2.Model/Game/Shop/RestrictedBuyData.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Maple2.Model/Game/Shop/RestrictedBuyData.cs b/Maple2.Model/Game/Shop/RestrictedBuyData.cs index 6690f24b4..8edbc8692 100644 --- a/Maple2.Model/Game/Shop/RestrictedBuyData.cs +++ b/Maple2.Model/Game/Shop/RestrictedBuyData.cs @@ -49,9 +49,11 @@ public readonly struct BuyTimeOfDay { public int EndTimeOfDay { get; } // time end in seconds. ex 10600 = 2:56 AM [JsonConstructor] - public BuyTimeOfDay(int startTime, int endTime) { - StartTimeOfDay = startTime; - EndTimeOfDay = endTime; + // System.Text.Json requires every parameter on the [JsonConstructor] to bind to a + // property/field on the type. Parameter names must match property names (case-insensitive). + public BuyTimeOfDay(int startTimeOfDay, int endTimeOfDay) { + StartTimeOfDay = startTimeOfDay; + EndTimeOfDay = endTimeOfDay; } public BuyTimeOfDay Clone() { From c62e56d7dbe7975391a48026b50d4e82a491e805 Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Fri, 6 Mar 2026 17:36:58 +0800 Subject: [PATCH 21/26] Revert "Merge branch 'pr/661' into fixfox" This reverts commit be6578322bf832b622e77d8d6db7a97d5f77f2e0, reversing changes made to c034216f23fa2ce20dd11eb12a054fd0219f310a. --- Maple2.Database/Model/Mail.cs | 2 +- .../Storage/Game/GameStorage.Map.cs | 4 +- .../Metadata/ServerTableMetadataStorage.cs | 3 - Maple2.File.Ingest/Mapper/NpcMapper.cs | 14 +- .../Mapper/ServerTableMapper.cs | 99 +- Maple2.Model/Common/ServerTableNames.cs | 1 - Maple2.Model/Enum/GameEventUserValueType.cs | 18 +- Maple2.Model/Game/Cube/Nurturing.cs | 10 +- Maple2.Model/Game/Mail.cs | 11 +- Maple2.Model/Game/Npc/Npc.cs | 7 +- Maple2.Model/Game/User/StatAttributes.cs | 26 +- Maple2.Model/Metadata/Constants.cs | 806 ++++++++++++++- Maple2.Model/Metadata/NpcMetadata.cs | 30 +- .../Metadata/ServerTable/ConstantsTable.cs | 959 ------------------ Maple2.Model/Metadata/ServerTableMetadata.cs | 1 - Maple2.Server.Core/Network/Session.cs | 8 +- Maple2.Server.Game/Commands/PlayerCommand.cs | 10 +- .../Manager/BlackMarketManager.cs | 2 +- Maple2.Server.Game/Manager/BuddyManager.cs | 6 +- Maple2.Server.Game/Manager/BuffManager.cs | 10 +- Maple2.Server.Game/Manager/ConfigManager.cs | 19 +- Maple2.Server.Game/Manager/CurrencyManager.cs | 42 +- .../Manager/ExperienceManager.cs | 21 +- .../Field/FieldManager/FieldManager.State.cs | 20 +- .../Field/FieldManager/FieldManager.cs | 2 +- Maple2.Server.Game/Manager/FishingManager.cs | 4 +- .../Manager/ItemMergeManager.cs | 2 +- .../Manager/Items/InventoryManager.cs | 68 +- .../Manager/Items/StorageManager.cs | 6 +- Maple2.Server.Game/Manager/SkillManager.cs | 4 +- Maple2.Server.Game/Manager/TradeManager.cs | 8 +- .../MovementState.CleanupTask.cs | 3 +- .../Model/Field/Actor/FieldNpc.cs | 15 +- .../Model/Field/Actor/FieldPet.cs | 4 +- .../Model/Field/Actor/FieldPlayer.cs | 8 +- Maple2.Server.Game/Model/Field/Tombstone.cs | 3 +- .../PacketHandlers/BeautyHandler.cs | 12 +- .../PacketHandlers/ClubHandler.cs | 4 +- .../PacketHandlers/FunctionCubeHandler.cs | 9 +- .../PacketHandlers/GuildHandler.cs | 8 +- .../PacketHandlers/HomeBankHandler.cs | 2 +- .../PacketHandlers/HomeDoctorHandler.cs | 2 +- .../PacketHandlers/ItemLockHandler.cs | 2 +- .../PacketHandlers/JobHandler.cs | 2 +- .../PacketHandlers/MeretMarketHandler.cs | 10 +- .../PacketHandlers/MesoMarketHandler.cs | 4 +- .../PacketHandlers/NpcTalkHandler.cs | 2 +- .../PacketHandlers/PartyHandler.cs | 6 +- .../PacketHandlers/QuestHandler.cs | 6 +- .../PacketHandlers/SystemShopHandler.cs | 10 +- .../PacketHandlers/TaxiHandler.cs | 4 +- .../PacketHandlers/UserChatHandler.cs | 4 +- Maple2.Server.Game/Session/GameSession.cs | 4 +- Maple2.Server.Game/Util/ChatUtil.cs | 2 +- .../CharacterManagementHandler.cs | 13 +- Maple2.Server.World/Containers/PartyLookup.cs | 9 +- .../Containers/PartyManager.cs | 10 +- Maple2.Server.World/Program.cs | 3 - Maple2.Server.World/WorldServer.cs | 4 +- 59 files changed, 1001 insertions(+), 1387 deletions(-) delete mode 100644 Maple2.Model/Metadata/ServerTable/ConstantsTable.cs diff --git a/Maple2.Database/Model/Mail.cs b/Maple2.Database/Model/Mail.cs index 0927f3107..cfca17f25 100644 --- a/Maple2.Database/Model/Mail.cs +++ b/Maple2.Database/Model/Mail.cs @@ -55,7 +55,7 @@ internal class Mail { [return: NotNullIfNotNull(nameof(other))] public static implicit operator Maple2.Model.Game.Mail?(Mail? other) { - return other == null ? null : new Maple2.Model.Game.Mail() { + return other == null ? null : new Maple2.Model.Game.Mail { ReceiverId = other.ReceiverId, Id = other.Id, SenderId = other.SenderId, diff --git a/Maple2.Database/Storage/Game/GameStorage.Map.cs b/Maple2.Database/Storage/Game/GameStorage.Map.cs index 37cb8cdfc..0ccfb0e43 100644 --- a/Maple2.Database/Storage/Game/GameStorage.Map.cs +++ b/Maple2.Database/Storage/Game/GameStorage.Map.cs @@ -111,11 +111,11 @@ public IList LoadCubesForOwner(long ownerId) { return Context.TrySaveChanges() ? ToPlotInfo(model) : null; } - public PlotInfo? GetSoonestPlotFromExpire(TimeSpan ugcHomeSaleWaitingTime) { + public PlotInfo? GetSoonestPlotFromExpire() { IQueryable maps = Context.UgcMap.Where(map => map.ExpiryTime > DateTimeOffset.MinValue && !map.Indoor); foreach (UgcMap map in maps) { if (map.OwnerId == 0) { - map.ExpiryTime = map.ExpiryTime.Add(ugcHomeSaleWaitingTime); + map.ExpiryTime = map.ExpiryTime.Add(Constant.UgcHomeSaleWaitingTime); } } UgcMap? model = maps.OrderBy(map => map.ExpiryTime).FirstOrDefault(); diff --git a/Maple2.Database/Storage/Metadata/ServerTableMetadataStorage.cs b/Maple2.Database/Storage/Metadata/ServerTableMetadataStorage.cs index 187856d3b..9a3c1d591 100644 --- a/Maple2.Database/Storage/Metadata/ServerTableMetadataStorage.cs +++ b/Maple2.Database/Storage/Metadata/ServerTableMetadataStorage.cs @@ -29,7 +29,6 @@ public class ServerTableMetadataStorage { private readonly Lazy combineSpawnTable; private readonly Lazy enchantOptionTable; private readonly Lazy unlimitedEnchantOptionTable; - private readonly Lazy constantsTable; public InstanceFieldTable InstanceFieldTable => instanceFieldTable.Value; public ScriptConditionTable ScriptConditionTable => scriptConditionTable.Value; @@ -54,7 +53,6 @@ public class ServerTableMetadataStorage { public CombineSpawnTable CombineSpawnTable => combineSpawnTable.Value; public EnchantOptionTable EnchantOptionTable => enchantOptionTable.Value; public UnlimitedEnchantOptionTable UnlimitedEnchantOptionTable => unlimitedEnchantOptionTable.Value; - public ConstantsTable ConstantsTable => constantsTable.Value; public ServerTableMetadataStorage(MetadataContext context) { instanceFieldTable = Retrieve(context, ServerTableNames.INSTANCE_FIELD); @@ -80,7 +78,6 @@ public ServerTableMetadataStorage(MetadataContext context) { combineSpawnTable = Retrieve(context, ServerTableNames.COMBINE_SPAWN); enchantOptionTable = Retrieve(context, ServerTableNames.ENCHANT_OPTION); unlimitedEnchantOptionTable = Retrieve(context, ServerTableNames.UNLIMITED_ENCHANT_OPTION); - constantsTable = Retrieve(context, ServerTableNames.CONSTANTS); } public IEnumerable GetGameEvents() { diff --git a/Maple2.File.Ingest/Mapper/NpcMapper.cs b/Maple2.File.Ingest/Mapper/NpcMapper.cs index b8cee68af..943759be6 100644 --- a/Maple2.File.Ingest/Mapper/NpcMapper.cs +++ b/Maple2.File.Ingest/Mapper/NpcMapper.cs @@ -31,13 +31,13 @@ protected override IEnumerable Map() { AniSpeed: data.model.anispeed ), Distance: new NpcMetadataDistance( - data.distance.avoid, - data.distance.sight, - data.distance.sightHeightUP, - data.distance.sightHeightDown, - data.distance.customLastSightRadius, - data.distance.customLastSightHeightUp, - data.distance.customLastSightHeightDown + Avoid: data.distance.avoid, + Sight: data.distance.sight, + SightHeightUp: data.distance.sightHeightUP, + SightHeightDown: data.distance.sightHeightDown, + LastSightRadius: data.distance.customLastSightRadius == 0 ? Constant.NpcLastSightRadius : data.distance.customLastSightRadius, + LastSightHeightUp: data.distance.customLastSightHeightUp == 0 ? Constant.NpcLastSightHeightUp : data.distance.customLastSightHeightUp, + LastSightHeightDown: data.distance.customLastSightHeightDown == 0 ? Constant.NpcLastSightHeightDown : data.distance.customLastSightHeightDown ), Skill: new NpcMetadataSkill( Entries: data.skill.ids.Select((skillId, i) => diff --git a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs index 32bca1ffb..940694898 100644 --- a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs +++ b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs @@ -1,9 +1,10 @@ -using Maple2.Database.Extensions; +using System.Globalization; +using System.Xml; +using Maple2.Database.Extensions; using Maple2.File.Ingest.Utils; using Maple2.File.IO; using Maple2.File.Parser; using Maple2.File.Parser.Enum; -using Maple2.File.Parser.Flat.Convert; using Maple2.File.Parser.Xml.Table.Server; using Maple2.Model; using Maple2.Model.Common; @@ -12,11 +13,6 @@ using Maple2.Model.Game; using Maple2.Model.Game.Shop; using Maple2.Model.Metadata; -using System.Globalization; -using System.Numerics; -using System.Reflection; -using System.Xml; -using Maple2.File.Parser.Xml.Table; using DayOfWeek = System.DayOfWeek; using ExpType = Maple2.Model.Enum.ExpType; using Fish = Maple2.File.Parser.Xml.Table.Server.Fish; @@ -132,10 +128,6 @@ protected override IEnumerable Map() { Name = ServerTableNames.UNLIMITED_ENCHANT_OPTION, Table = ParseUnlimitedEnchantOption(), }; - yield return new ServerTableMetadata { - Name = ServerTableNames.CONSTANTS, - Table = ParseConstants(), - }; } @@ -2143,89 +2135,4 @@ void AddSpecial(Dictionary values, Dictionary propertyLookup = typeof(ConstantsTable).GetProperties() - .ToDictionary(p => p.Name.Trim(), p => p, StringComparer.OrdinalIgnoreCase); - - foreach ((string key, Constants.Key constant) in parser.ParseConstants()) { - string trimmedKey = key.Trim(); - if (!propertyLookup.TryGetValue(trimmedKey, out PropertyInfo? property)) continue; - string cleanValue = CleanConstantsInput( - constant.value.Trim(), - trimmedKey, - property.PropertyType - ); - SetValue(property, constants, cleanValue); - } - return constants; - } - - private string CleanConstantsInput(string input, string propName, Type type) { - // check if string contains the ending 'f' for float designation, strip it if it does. - if (type == typeof(float) && input.Contains('f')) { - input = input.TrimEnd('f', 'F'); - } - // 1 does not automatically equate to true during bool conversion - if (type == typeof(bool) && input == "1") { - input = "true"; - } - // 0 does not automatically equate to false during bool conversion - if (type == typeof(bool) && input == "0") { - input = "false"; - } - // Convert into a TimeSpan friendly input string instead of an int value - if (type == typeof(TimeSpan) && propName == "UgcHomeSaleWaitingTime") { - input = TimeSpan.FromSeconds(int.Parse(input)).ToString(); // TODO: may not be correct conversion to TimeSpan - } - // Remove prefix 0 on integers since they do not convert properly - if (type == typeof(int) && input[0] == '0' && input.Length > 1) { - input = input.Remove(0, 1); - } - return input; - } - - private void SetValue(PropertyInfo prop, object? obj, object? value) { - if (obj == null && value == null || value == null) return; - HandleNonIConvertibleTypes(prop, ref value); - bool isConvertible = typeof(IConvertible).IsAssignableFrom(prop.PropertyType); - prop.SetValue(obj, isConvertible ? Convert.ChangeType(value, prop.PropertyType, CultureInfo.InvariantCulture) : value); - } - - private object? HandleNonIConvertibleTypes(PropertyInfo prop, ref object? value) { - if (value == null) return value; - // Handle TimeSpan type - if (prop.PropertyType == typeof(TimeSpan)) { - // Special case - dashes (-) are used instead of colons (:) - if (prop.Name == "DailyTrophyResetDate") { - value = ((string)value).Replace('-', ':'); - } - if (prop.Name == "GlobalCubeSkillIntervalTime") { - value = $"0:0:{(string) value}"; - } - value = TimeSpan.Parse((string)value, CultureInfo.InvariantCulture); - } - // Handle array types (int[], short[], etc.) - if (prop.PropertyType.IsArray) { - var elementType = prop.PropertyType.GetElementType(); - if (elementType == null) return value; - string[] segments = ((string)value).Split(','); - Array destinationArray = Array.CreateInstance(elementType, segments.Length); - for (int i = 0; i < segments.Length; i++) { - object convertedValue = Convert.ChangeType(segments[i].Trim(), elementType); - destinationArray.SetValue(convertedValue, i); - } - value = destinationArray; - } - // Handle Vector3 type - if (prop.PropertyType == typeof(Vector3)) { - string[] parts = ((string) value).Split(','); - if (parts.Length != 3) return value; - value = new Vector3(float.Parse(parts[0], CultureInfo.InvariantCulture), - float.Parse(parts[1], CultureInfo.InvariantCulture), - float.Parse(parts[2], CultureInfo.InvariantCulture)); - } - return value; - } } diff --git a/Maple2.Model/Common/ServerTableNames.cs b/Maple2.Model/Common/ServerTableNames.cs index 0761b6ea1..7a328840f 100644 --- a/Maple2.Model/Common/ServerTableNames.cs +++ b/Maple2.Model/Common/ServerTableNames.cs @@ -24,5 +24,4 @@ public static class ServerTableNames { public const string COMBINE_SPAWN = "combineSpawn*.xml"; public const string ENCHANT_OPTION = "enchantOption.xml"; public const string UNLIMITED_ENCHANT_OPTION = "unlimitedEnchantOption.xml"; - public const string CONSTANTS = "constants.xml"; } diff --git a/Maple2.Model/Enum/GameEventUserValueType.cs b/Maple2.Model/Enum/GameEventUserValueType.cs index 7547f6474..9d74d5eb9 100644 --- a/Maple2.Model/Enum/GameEventUserValueType.cs +++ b/Maple2.Model/Enum/GameEventUserValueType.cs @@ -6,14 +6,10 @@ public enum GameEventUserValueType { // Attendance Event AttendanceActive = 100, //?? maybe. String is "True" AttendanceCompletedTimestamp = 101, - AttendanceRewardsClaimed = 102, // Also used for Cash Attendance and DT Attendance + AttendanceRewardsClaimed = 102, AttendanceEarlyParticipationRemaining = 103, - AttendanceNear = 105, AttendanceAccumulatedTime = 106, - // ReturnUser - ReturnUser = 320, // IsReturnUser - // DTReward DTRewardStartTime = 700, // start time DTRewardCurrentTime = 701, // current item accumulated time @@ -29,22 +25,10 @@ public enum GameEventUserValueType { GalleryCardFlipCount = 1600, GalleryClaimReward = 1601, - // Snowman Event - SnowflakeCount = 1700, - DailyCompleteCount = 1701, - AccumCompleteCount = 1702, - AccumCompleteRewardReceived = 1703, - // Rock Paper Scissors Event RPSDailyMatches = 1800, RPSRewardsClaimed = 1801, - // Couple Dance - CoupleDanceBannerOpen = 2100, - CoupleDanceRewardState = 2101, // completed/bonus/received flags - - CollectItemGroup = 2200, // Meta Badge event. Serves as a flag for tiers - // Bingo - TODO: These are not the actual confirmed values. Just using it as a way to store this data for now. BingoUid = 4000, BingoRewardsClaimed = 4001, diff --git a/Maple2.Model/Game/Cube/Nurturing.cs b/Maple2.Model/Game/Cube/Nurturing.cs index 229b87474..faad1e1e5 100644 --- a/Maple2.Model/Game/Cube/Nurturing.cs +++ b/Maple2.Model/Game/Cube/Nurturing.cs @@ -48,20 +48,20 @@ public Nurturing(long exp, short claimedGiftForStage, long[] playedBy, DateTimeO } } - public void Feed(int nurturingEatGrowth) { + public void Feed() { if (Exp >= NurturingMetadata.RequiredGrowth.Last().Exp) { return; } - Exp += nurturingEatGrowth; + Exp += Constant.NurturingEatGrowth; if (Exp >= NurturingMetadata.RequiredGrowth.First(x => x.Stage == Stage).Exp) { Stage++; } LastFeedTime = DateTimeOffset.Now; } - public bool Play(long accountId, int nurturingEatGrowth, int nurturingPlayMaxCount) { - if (PlayedBy.Count >= nurturingPlayMaxCount) { + public bool Play(long accountId) { + if (PlayedBy.Count >= Constant.NurturingPlayMaxCount) { return false; } @@ -70,7 +70,7 @@ public bool Play(long accountId, int nurturingEatGrowth, int nurturingPlayMaxCou } PlayedBy.Add(accountId); - Feed(nurturingEatGrowth); + Feed(); return true; } diff --git a/Maple2.Model/Game/Mail.cs b/Maple2.Model/Game/Mail.cs index ec1faaee4..e97dc99a8 100644 --- a/Maple2.Model/Game/Mail.cs +++ b/Maple2.Model/Game/Mail.cs @@ -1,5 +1,6 @@ using System.Text; using Maple2.Model.Enum; +using Maple2.Model.Metadata; using Maple2.PacketLib.Tools; using Maple2.Tools; using Maple2.Tools.Extensions; @@ -34,19 +35,11 @@ public class Mail : IByteSerializable { // More than 1 item may not display properly public readonly IList Items; - // Specifically for Mail object cloning (Mail.cs:57) public Mail() { TitleArgs = new List<(string Key, string Value)>(); ContentArgs = new List<(string Key, string Value)>(); Items = new List(); - // ExpiryTime will be overwritten, no need to set it here with a parameter passing server constant value. - } - - public Mail(int mailExpiryDays) { - TitleArgs = new List<(string Key, string Value)>(); - ContentArgs = new List<(string Key, string Value)>(); - Items = new List(); - ExpiryTime = DateTimeOffset.UtcNow.AddDays(mailExpiryDays).ToUnixTimeSeconds(); + ExpiryTime = DateTimeOffset.UtcNow.AddDays(Constant.MailExpiryDays).ToUnixTimeSeconds(); } public void Update(Mail other) { diff --git a/Maple2.Model/Game/Npc/Npc.cs b/Maple2.Model/Game/Npc/Npc.cs index fc176815c..d135d541b 100644 --- a/Maple2.Model/Game/Npc/Npc.cs +++ b/Maple2.Model/Game/Npc/Npc.cs @@ -10,11 +10,8 @@ public class Npc { public bool IsBoss => Metadata.Basic.Friendly == 0 && Metadata.Basic.Class >= 3; - public Npc(NpcMetadata metadata, AnimationMetadata? animation, float constLastSightRadius, float constLastSightHeightUp, float constLastSightHeightDown) { - float lastSightRadius = metadata.Distance.LastSightRadius == 0 ? constLastSightRadius : metadata.Distance.LastSightRadius; - float lastSightHeightUp = metadata.Distance.LastSightHeightUp == 0 ? constLastSightHeightUp : metadata.Distance.LastSightHeightUp; - float lastSightHeightDown = metadata.Distance.LastSightHeightDown == 0 ? constLastSightHeightDown : metadata.Distance.LastSightHeightDown; - Metadata = new NpcMetadata(metadata, lastSightRadius, lastSightHeightUp, lastSightHeightDown); + public Npc(NpcMetadata metadata, AnimationMetadata? animation) { + Metadata = metadata; Animations = animation?.Sequences ?? new Dictionary(); } } diff --git a/Maple2.Model/Game/User/StatAttributes.cs b/Maple2.Model/Game/User/StatAttributes.cs index 9c432db3a..28cf8c61f 100644 --- a/Maple2.Model/Game/User/StatAttributes.cs +++ b/Maple2.Model/Game/User/StatAttributes.cs @@ -13,9 +13,9 @@ public class StatAttributes : IByteSerializable { public int TotalPoints => Sources.Count; public int UsedPoints => Allocation.Count; - public StatAttributes(IDictionary statLimits) { + public StatAttributes() { Sources = new PointSources(); - Allocation = new PointAllocation(statLimits); + Allocation = new PointAllocation(); } public void WriteTo(IByteWriter writer) { @@ -52,7 +52,6 @@ public void WriteTo(IByteWriter writer) { public class PointAllocation : IByteSerializable { private readonly Dictionary points; - private readonly IDictionary statLimits; public BasicAttribute[] Attributes => points.Keys.ToArray(); public int Count => points.Values.Sum(); @@ -60,7 +59,7 @@ public class PointAllocation : IByteSerializable { public int this[BasicAttribute type] { get => points.GetValueOrDefault(type); set { - if (value < 0 || value > StatLimit(type, statLimits)) { + if (value < 0 || value > StatLimit(type)) { return; } if (value == 0) { @@ -72,20 +71,19 @@ public int this[BasicAttribute type] { } } - public PointAllocation(IDictionary statLimits) { + public PointAllocation() { points = new Dictionary(); - this.statLimits = statLimits; } - public static int StatLimit(BasicAttribute type, IDictionary statLimits) { + public static int StatLimit(BasicAttribute type) { return type switch { - BasicAttribute.Strength => statLimits.GetValueOrDefault("StatPointLimit_str"), - BasicAttribute.Dexterity => statLimits.GetValueOrDefault("StatPointLimit_dex"), - BasicAttribute.Intelligence => statLimits.GetValueOrDefault("StatPointLimit_int"), - BasicAttribute.Luck => statLimits.GetValueOrDefault("StatPointLimit_luk"), - BasicAttribute.Health => statLimits.GetValueOrDefault("StatPointLimit_hp"), - BasicAttribute.CriticalRate => statLimits.GetValueOrDefault("StatPointLimit_cap"), - _ => 0 + BasicAttribute.Strength => Constant.StatPointLimit_str, + BasicAttribute.Dexterity => Constant.StatPointLimit_dex, + BasicAttribute.Intelligence => Constant.StatPointLimit_int, + BasicAttribute.Luck => Constant.StatPointLimit_luk, + BasicAttribute.Health => Constant.StatPointLimit_hp, + BasicAttribute.CriticalRate => Constant.StatPointLimit_cap, + _ => 0, }; } diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index a517368c6..f5085e784 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -17,6 +17,8 @@ public static class Constant { public const long MesoTokenMax = 100000; public const int MaxSkillTabCount = 3; public const int BuddyMessageLengthMax = 25; + public const int MaxBuddyCount = 100; + public const int MaxBlockCount = 100; public const int GemstoneGrade = 4; public const int LapenshardGrade = 3; public const int InventoryExpandRowCount = 6; @@ -102,39 +104,26 @@ public static class Constant { public const int Grade3WeddingCouponItemId = 20303168; public const int MinStatIntervalTick = 100; public const int HomePollMaxCount = 5; - public const int DummyNpcMale = 2040998; - public const int DummyNpcFemale = 2040999; - public const int NextStateTriggerDefaultTick = 100; public static readonly TimeSpan WorldBossIdleWarningThreshold = TimeSpan.FromMinutes(4); public static readonly TimeSpan WorldBossDespawnThreshold = TimeSpan.FromMinutes(5); public static readonly TimeSpan WorldBossMonitorInterval = TimeSpan.FromSeconds(30); public const int MaxMentees = 3; + public const long FurnishingBaseId = 2870000000000000000; public const bool AllowWaterOnGround = false; + public const int HomeDecorationMaxLevel = 10; + public const bool EnableRollEverywhere = false; public const bool HideHomeCommands = true; + public const int MaxAllowedLatency = 2000; - public const bool DebugTriggers = false; + + public const bool DebugTriggers = false; // Set to true to enable debug triggers. (It'll write triggers to files and load triggers from files instead of DB) + public const bool AllowUnicodeInNames = true; // Allow Unicode characters in character and guild names - public const bool MailQuestItems = false; // Mail quest item rewards if inventory is full - public const int MaxClosetMaxCount = 5; - public const int MaxClosetTabNameLength = 10; - public const int CharacterNameLengthMin = 2; - public const int BlockSize = 150; - public const float SouthEast = 0; - public const float NorthEast = 90; - public const float NorthWest = -180; - public const float SouthWest = -90; - public const short HairSlotCount = 30; - public const ShopCurrencyType InitialTierExcessRestockCurrency = ShopCurrencyType.Meso; - public const float UGCShopProfitFee = 0.25f; - public const int UGCShopProfitDelayDays = 10; - public const int PartyFinderListingsPageCount = 12; - public const int ProposalItemId = 11600482; - public const int ClubMaxCount = 3; public static IReadOnlyDictionary ContentRewards { get; } = new Dictionary { { "miniGame", 1005 }, @@ -162,9 +151,7 @@ public static class Constant { { "QueenBeanArenaRound10Reward", 10000018 }, }; - public static int DummyNpc(Gender gender) => gender is Gender.Female ? DummyNpcFemale : DummyNpcMale; - - #endregion + public const bool MailQuestItems = false; // Mail quest item rewards if inventory is full #region Field public static readonly TimeSpan FieldUgcBannerRemoveAfter = TimeSpan.FromHours(4); @@ -203,6 +190,779 @@ public static class Constant { public static readonly bool BlockLoginWithMismatchedMachineId = false; public static readonly int DefaultMaxCharacters = 4; #endregion + #endregion + + #region client constants + public const int MaxClosetMaxCount = 5; + public const int MaxClosetTabNameLength = 10; + public const int WeddingProposeItemID = 11600482; + public const int WeddingInvitationMaxCount = 70; + public const int WeddingProposeCooltime = 2; + public const int WeddingDivorceFieldID = 84000002; + public const int WeddingInvitationMeso = 1000; + public const int WeddingDivorceMeso = 1000000; + public const int WeddingCoolingOffDay = 7; + public const int WeddingPromiseLimitDay = 7; + public const int WeddingHallModifyLimitHour = 3; + public const int WeddingDivorceRequireMarriageDay = 30; + public const int CharacterNameLengthMin = 2; + public const int BlockSize = 150; + public const float SouthEast = 0; + public const float NorthEast = 90; + public const float NorthWest = -180; + public const float SouthWest = -90; + public const short HairSlotCount = 30; + public const ShopCurrencyType InitialTierExcessRestockCurrency = ShopCurrencyType.Meso; + public const float UGCShopProfitFee = 0.25f; + public const int UGCShopProfitDelayDays = 10; + public const int PartyFinderListingsPageCount = 12; + public const int ProposalItemId = 11600482; + #endregion + + #region table/constants.xml + public const float NPCColorScale = 2.0f; + public const float NPCDuration = 0.2f; + public const float PCColorScale = 2.0f; + public const float PCDuration = 0.2f; + public const float GetEXPColorScale = 0.5f; + public const float GetEXPDuration = 0.2f; + public const float AccumulationRatio = 0.1f; + public const float NPCCliffHeight = 50.0f; + public const float NPCRandomDeadPushRate = 0.2f; + public const float CustomizingRotationSpeed = 75.0f; + public const float CustomizingWheelSpeed_Morph = 0.1f; + public const float CustomizingWheelSpeed_Item = 0.1f; + public const float CustomizingWheelSpeed_Makeup = 0.1f; + public const float CustomizingRotationSpeed_Makeup = 1.0f; + public const float CustomizingHairFirstPartHighlight = 0.1f; + public const float CustomizingHairSecondPartHighlight = 1.0f; + public const float LookAtInterval = 15.0f; + public const float LookAtDistanceNPC = 500.0f; + public const float LookAtDistanceCry = 500.0f; + public const bool EnableSkillJumpDown = true; + public const bool EscapeHitMethodSkill = false; + public const bool EscapeHitMethodJump = false; + public const bool EscapeHitMethodMove = false; + public const bool EscapeHitMoveKeyIsDown = false; + public const bool AllowComboAtComboPoint = true; + public const bool CancelSwing_KeyIsDown = true; + public const bool SkillGlobalCooldown = false; + public const bool SkillGlobalCooldown_CheckSameSkill = true; + public const int AttackRotationSpeed = 90; + public const int ChaosModeTime = 20; + public const int ChaosPointPerBlock = 20; + public const int ChaosPointMaxBlock = 1; + public const int ChaosPointGetLevel0 = 1; + public const int ChaosPointGetPoint0 = 120; + public const int ChaosActionGetLevel0 = 15; + public const int ChaosActionGetLevel1 = 25; + public const int ChaosActionGetLevel2 = 55; + public const int ChaosActionGetLevel3 = 95; + public const int ChaosActionGetLevel4 = 145; + public const int OnEnterTriggerClientSideOnlyTick = 100; + public const int OnEnterTriggerDefaultTick = 1000; + public const int TalkTimeover = 60000; + public const int DamageDistance = 2500; + public const int TalkableDistance = 150; + public const bool TalkableFrontOnly = true; + public const int DropIconVisibleDistance = 400; + public const int ChatBalloonDistance = 2000; + public const int HpBarDistance = 9999999; + public const int EmoticonVisibleDistance = 2500; + public const int RegisterUgcDistance = 150; + public const int RegisterUgcDistanceClose = 300; + public const int ConstructUgcDistance = 150; + public const int FunctionCubeDistance = 125; + public const int InteractionDistance = 155; + public const int HouseMarkShowDistance = 2000; + public const int HouseMarkShowClippingUp = 1000; + public const int HouseMarkShowClippingDown = 500; + public const int HouseMarkPopupDistance = 160; + public const int UgcBoundaryStartDistance = 1; + public const int UgcBoundaryEndDistance = 7; + public const int DurationForBoundaryDisplay = 3000; + public static TimeSpan UgcHomeSaleWaitingTime = TimeSpan.FromSeconds(259200); + public const int UgcContainerExpireDurationNormal = 90; + public const int UgcContainerExpireDurationCash = 365; + public const int UgcContainerExpireDurationMeret = 365; + public const int UgcHomeExtensionNoticeDate = 30; + public const int UgcHomePasswordExpireDuration = 86400; + public const bool CubeLiftHeightLimitUp = true; + public const bool CubeLiftHeightLimitDown = true; + public const int CubeCraftSafetyCapID = 11300053; + public const int CubeCraftLightStickLeftID = 13100014; + public const int CubeCraftLightStickRightID = 13100046; + public const float DropIconDistance = 200.0f; + public const int DropIconHeadOffset = 40; + public const int DropItemMaxLength = 300; + public const int DropMoneyMaxLength = 300; + public const float DropItemTargetZPos = 200.0f; + public const float DropItemPickUpVel = 200.0f; + public const float DropItemPickUpGravity = -38.0f; + public const float DropItemPickUpCompleteRotateTime = 0.1f; + public const int DropItemPickUpCompleteRotateVel = 5; + public const int DropMoneyActiveProbability = 0; + public const int DropMoneyProbability = 0; + public const int ChatBalloonDuration = 5000; + public const int BoreWaitingTick = 50000; + public const int OffsetPcHpBar = 32; + public const int OffsetPcNameTag = 30; + public const int OffsetPcChatBalloon = -30; + public const int OffsetPcDamageNumber = 0; + public const int OffsetPcMissionIndicator = 20; + public const int OffsetPcProfileTag = 0; + public const float fOffsetOnTombstoneNameTag = -5.0f; + public const int OffsetNpcHpBar = 5; + public const int OffsetNpcNameTag = 5; + public const int OffsetNpcEmoticon = -30; + public const int OffsetNpcChatBalloon = -30; + public const int OffsetNpcDamageNumber = 0; + public const int OffsetNpcMonologue = 40; + public const int OffsetActionTooltipX = 70; + public const int OffsetActionTooltipY = -40; + public const int OffsetPcPopupMenu = 60; + public const int DamageGap = 30; + public const int DamageRenderCount = 3; + public const int DamageRenderTotalCount = 25; + public const float DamageOtherScale = 0.5f; + public const float DamageOtherAlpha = 0.3f; + public const int DamageEffectMinHPPercent = 30; + public const int DamageEffectCriticalPercent = 10; + public const int questHideTime = 30; + public const int questIntervalTime = 60; + public const int ShopResetChance = 10; + public const int ShopSeedResetTime = 60; + public const int ShopRepurchaseMax = 12; + public const int ShopSellConfirmPrice = 10000; + public const int ShopBuyConfirmPrice = 0; + public const int DashKeyInputDelay = 500; + public const int DashSwimConsumeSP = 20; + public const int DashSwimMoveVel = 2; + public const float Glide_Gravity = 0.0f; + public const float Glide_Height_Limit = 0.0f; + public const float Glide_Horizontal_Accelerate = 0.0f; + public const int Glide_Horizontal_Velocity = 500; + public const float Glide_Vertical_Accelerate = 0.0f; + public const int Glide_Vertical_Velocity = 150; + public const int Glide_Vertical_Vibrate_Amplitude = 300; + public const float Glide_Vertical_Vibrate_Frequency = 1500.0f; + public const bool Glide_Effect = true; + public const string Glide_Effect_Run = "CH/Common/Eff_Fly_Balloon_Run.xml"; + public const string Glide_Effect_Idle = "CH/Common/Eff_Fly_Balloon_Idle.xml"; + public const string Glide_Ani_Idle = "Fly_Idle_A"; + public const string Glide_Ani_Left = "Gliding_Left_A"; + public const string Glide_Ani_Right = "Gliding_Right_A"; + public const string Glide_Ani_Run = "Fly_Run_A"; + public const float ClimbVelocityV = 3.0f; + public const float ClimbVelocityH = 1.5f; + public const int StoreExpandMaxSlotCount = 144; + public const int StoreExpandPrice1Row = 330; + public const int StoreDepositMax = 2000000000; + public const int StoreWithdrawMax = 2000000000; + public const int CameraExtraMoveScaleByMonster = 3; + public const int CameraExtraMoveScaleByMap = 2; + public const int CameraExtraDistance = 200; + public const float CameraFinalLoose = 0.08f; + public const float CameraCurrentLoose = 0.002f; + public const float CameraUpdateLoose = 0.03f; + public const int CameraVelocityInPortalMove = 6000; + public const int ConsumeCritical = 5; + public const int MonologueInterval = 15; + public const int MonologueRandom = 10; + public const int MonologueShowTime = 5; + public const int ShowKillCountMin = 3; + public const int UserRevivalInvincibleTick = 5000; + public const int UserRevivalPenaltyPercent = 15; + public const string UserRevivalIconPath = "./data/resource/image/skill/icon/deathPenalty.png"; + public const string UserRevivalInvincibleIconPath = "./data/resource/image/skill/icon/deathInvincible.png"; + public const int GetExpMinVelocity = 250; + public const int GetExpVelocityPer1Length = 2; + public const string GetExpControlValue0 = "-0.5,0,0.25"; + public const string GetExpControlValue1 = "0.5,-0.25,0.5"; + public const string GetExpTargetPCDummyName = "Eff_Body"; + public const float GetExpTimeAcceleration = 1.02f; + public const float GetExpCollisionRadius = 15.0f; + public const int DayToNightTime = 10000; + public const float MyPCDayTiming = 0.5f; + public const float MyPCNightTiming = 0.5f; + public const float BGMTiming = 0.5f; + public const int dayBaseMinute = 1; + public const int dayMinute = 1439; + public const int nightMinute = 1; + public const int SkipFrameGameObject = 5; + public const int SkipFrameDistanceGameObject = 2000; + public const float RegionSkillFadeOutDuration = 0.3f; + public const int PassengerProfileImageSize = 50; + public const int PassengerProfileImageLifeTime = 3; + public const int PassengerProfileImageShowNumber = 3; + public const int PassengerProfileImageShowCooldown = 57; + public const int PassengerProfileImageShowCooldownParty = 57; + public const int PassengerProfileImageShowRange = 400; + public const int QuestRewardSkillSlotQuestID1 = 1010002; + public const int QuestRewardSkillSlotQuestID2 = 1010003; + public const int QuestRewardSkillSlotQuestID3 = 1010004; + public const int QuestRewardSkillSlotQuestID4 = 1010005; + public const int QuestRewardSkillSlotQuestID5 = 1010010; + public const int QuestRewardSkillSlotItemID1 = 40000000; + public const int QuestRewardSkillSlotItemID2 = 40200001; + public const int QuestRewardSkillSlotItemID3 = 20000001; + public const int QuestRewardSkillSlotItemID4 = 40000055; + public const int QuestRewardSkillSlotItemID5 = 40000056; + public const int UGCCameraDefaultSize = 320; + public const int UGCCameraMinSize = 160; + public const int UGCCameraMaxSize = 640; + public const int UGCCameraSnapshotPreviewTime = 3000; + public const int UGCImgUploadSizeLimit = 1024; + public const int UGCImgFileCountCheck = 200; + public const int WindAmp2Cloak = 1500; + public const float WindPeriod2Cloak = 0.7f; + public const float WindPeriodVar2Cloak = 0.4f; + public const int autoTargetingMaxDegree = 210; + public const float VolumeMyPcToNpc = 1.0f; + public const float VolumeMyPcToObject = 0.5f; + public const float VolumeMyPcToBreakableObject = 0.8f; + public const float VolumeNpcToMyPc = 0.7f; + public const float VolumePcToNpc = 0.3f; + public const float VolumePcToBreakableObject = 0.3f; + public const float VolumeNpcToPc = 0.5f; + public const float VolumeOtherPc = 0.9f; + public const int ItemDropLevelMaxBoundary = 1; + public const float moneyTreeDropHeight = 300.0f; + public const float moneyTreeDropBase = 150.0f; + public const int moneyTreeDropRandom = 200; + public const int WhisperIgnoreTime = 1000; + public const int WhisperMaxCount = 3; + public const int WhisperDurationTime = 3000; + public const float BossHitVibrateFreq = 10.0f; + public const float BossHitVibrateAmp = 5.5f; + public const float BossHitVibrateDamping = 0.7f; + public const float BossHitVibrateDuration = 0.1f; + public const float BossHpBarAutoDetectRange = 1500.0f; + public const float BossHpBarDuration = 5.0f; + public const float FindHoldTargetRange = 230.0f; + public const int FindGrabNodeRange = 2000; + public const string UgcShopCharCameraLookat = "0,0,70"; + public const string UgcShopCharCameraPos = "220,0,0"; + public const int UgcShopCharCameraMinDistance = 150; + public const int UgcShopCharCameraZoomVelocity = 700; + public const string UgcShopCubeCameraLookat = "0,0,80"; + public const string UgcShopCubeCameraPos = "420,0,350"; + public const int UgcShopCubeCameraMinDistance = 450; + public const int UgcShopCubeCameraZoomVelocity = 700; + public const string UgcShopRideeCameraLookat = "10,-5,50"; + public const string UgcShopRideeCameraPos = "275,0,150"; + public const int UgcShopRideeCameraMinDistance = 250; + public const int UgcShopRideeCameraZoomVelocity = 700; + public const int FieldCachingCount = 2; + public const float FieldCachingTime = 300.0f; + public const int FieldCachingMaxCount = 4; + public const int FieldUnloadThreshold = 10; + public const float EffectLODOneStepDistance = 450.0f; + public const float EffectLODTwoStepDistance = 500.0f; + public const float EffectLODThreeStepDistance = 550.0f; + public const int TelescopeFindDistance = 200; + public const int BoatPrice = 500; + public const int QuestGuidePageCount = 3; + public const int QuestGuideMaxCount = 60; + public const float CameraInterpolationTime = 0.4f; + public const int OneTimeWeaponItemID = 15000001; + public const int TransparencyCP = 11399999; + public const int TransparencyEY = 11199999; + public const int TransparencyCL = 11499999; + public const int TransparencyPA = 11599999; + public const int TransparencyMT = 11899999; + public const int TransparencyEA = 11299999; + public const int TransparencyFH = 11099999; + public const int TransparencyGL = 11699999; + public const int TransparencyRI = 12099999; + public const int TransparencySH = 11799999; + public const float DefaultDropItemAlpha = 0.3f; + public const float DropItemPickFailHeight = 50.0f; + public const float DropItemPickFailTime = 0.3f; + public const int TaxiStationFindDistance = 200; + public const int TaxiCallDuration = 3000; + public const int TaxiCallBestDriverDuration = 1000; + public const int TaxiCallBestDriverLevel = 25; + public const int AirTaxiCashCallDuration = 500; + public const int AirTaxiMesoCallDuration = 3000; + public const int TradeRequestDuration = 20; + public const int UserPortalInvincibleTick = 5000; + public const string UserPortalInvincibleIconPath = "./data/resource/image/skill/icon/deathInvincible.png"; + public const int SummonRideeDuration = 1000; + public const int WorldMapAdjustTileX = 0; + public const int WorldMapAdjustTileY = 0; + public const float TimeScalePCScale = 0.1f; + public const float TimeScalePCDuration = 1.0f; + public const int GoToHomeCastingTime = 0; + public const int returnHomeSkill = 100000000; + public const int returnHomeSkillMeret = 100000013; + public const int TutorialIntroSkipTime = 5; + public const string AvatarDefaultItemMale = "10200032,10300198"; + public const string AvatarDefaultItemFemale = "10200033,10300199"; + public const int ModelHouse = 62000027; + public const int TalkCooldown = 1000; + public const int AddressPopupDuration = 3000; + public const int MaxFPS = 120; + public const int UGCShopSellMinPrice = 150; + public const int UGCShopSellMaxPrice = 3000; + public const int UGCShopSaleDay = 90; + public const int UGCShopAdFeeMeret = 30; + public const int UGCShopAdHour = 72; + public const int UGCShopSellingRestrictAmount = 200000; + public const int MeretMarketHomeBannerShowTick = 6000; + public const int BlackMarketSellMinPrice = 100; + public const int BlackMarketSellMaxPrice = 500000000; + public const int BlackMarketSellEndDay = 2; + public const int ItemTransferBlackMarketGrade = 4; + public const int UgcBannerCheckTime = 4; + public const int FastChat_CheckTime = 2000; + public const int FastChat_CheckCount = 5; + public const int SameChat_CheckTime = 3000; + public const int SameChat_CheckCount = 5; + public const int SameChat_RestrictTime = 10000; + public const int FastChat_RestrictTime = 30000; + public const int RestrictChat_AddRestrictTime = 10000; + public const int AccumWarning_AddRestrictTime = 60000; + public const int RestrictWarning_ReleaseTime = 10000; + public const int MaxChatLength = 100; + public const int UsingNoPhysXModelUserCount = 10; + public const int UsingNoPhysXModelActorCount = 10; + public const int UsingNoPhysXModelJointCount = 10; + public const int EmotionBoreAnimProbability = 100; + public const float FallMoveSpeed = 1.0f; + public const int GuildCreatePrice = 2000; + public const int GuildCreateMinLevel = 0; + public const int GuildNameLengthMin = 2; + public const int GuildNameLengthMax = 25; + public const int guildFundMax = 20000; + public const float guildFundRate = 0.1f; + public const int guildExpMaxCountForPlayTime = 2; + public const int guildDonateMeso = 10000; + public const string mirrorGuideMoviePath = "Common/Customize_Hat.usm"; + public const string hairGuideMoviePath = "Common/Customize_Hair.usm"; + public const string makeUpGuideMoviePath = "Common/Customize_MakeUp.usm"; + public const int FastShimmerRadius = 600; + public const int FastShimmerHeight = 450; + public const int SmartRecommendNotify_DurationTick = 15000; + public const int BootyPopupDuration = 3000; + public const bool EnableSoundMute = true; + public const int BossKillSoundRange = 1500; + public const string charCreateGuideMoviePath = "Common/Customize_Intro.usm"; + public const int monsterPeakTimeNotifyDuration = 300; + public const int KeyIsDownSkill_MaxDurationTick = 30000; + public const int shadowWorldBuffHpUp = 70000027; + public const int shadowWorldBuffMoveProtect = 70000032; + public const int AirTaxiItemID = 20300003; + public const int PeriodOfMaidEmployment = 30; + public const int MaidReadyToPay = 7; + public const int MaidAffinityMax = 10; + public const int MeretRevivalDebuffCode = 100000001; + public const float MeretRevivalFeeReduceLimit = 0.5f; + public const int MeretConsumeWorldChat = 30; + public const int MeretConsumeChannelChat = 3; + public const int MeretConsumeSuperChat = 200; + public const int pvpBtiRewardItem = 90000006; + public const int pvpBtiRewardWinnerCount = 30; + public const int pvpBtiRewardLoserCount = 10; + public const int PvpFFAReward1Count = 30; + public const int PvpFFAReward2Count = 25; + public const int PvpFFAReward3Count = 20; + public const int PvpFFAReward4Count = 15; + public const int PvpFFAReward5Count = 15; + public const int PvpFFAReward6Count = 15; + public const int PvpFFAReward7Count = 15; + public const int PvpFFAReward8Count = 10; + public const int PvpFFAReward9Count = 10; + public const int PvpFFAReward10Count = 10; + public const int PvpFFARewardItem = 90000006; + public const int PvpFFAAdditionRewardRate = 0; + public const int MailExpiryDays = 30; + public const int WorldMapBossTooltipCount = 30; + public const int ShowNameTagEnchantItemGrade = 4; + public const int ShowNameTagEnchantLevel = 12; + public const int BossNotifyAbsLevel = 1; + public const int RoomExitWaitSecond = 10; + public const int AdditionalMesoMaxRate = 7; + public const int AdditionalExpMaxRate = 9; + public const int HonorTokenMax = 30000; + public const int KarmaTokenMax = 75000; + public const int LuTokenMax = 2000; + public const int HaviTokenMax = 35000; + public const int ReverseCoinMax = 2000; + public const int MentorTokenMax = 10000; // From KMS + public const int MenteeTokenMax = 35000; // From KMS + public const int CharacterDestroyDivisionLevel = 20; + public const int CharacterDestroyWaitSecond = 86400; + public const int BossShimmerScaleUpActiveDistance = 5000; + public const float BossShimmerScaleUpSize = 3.0f; + public const int ShowNameTagSellerTitle = 10000153; + public const int ShowNameTagChampionTitle = 10000152; + public const int ShowNameTagTrophy1000Title = 10000170; + public const int ShowNameTagTrophy2000Title = 10000171; + public const int ShowNameTagTrophy3000Title = 10000172; + public const int ShowNameTagArchitectTitle = 10000158; + public const float SwimDashSpeed = 5.4f; + public const int UserTriggerStateMax = 10; + public const int UserTriggerEnterActionMax = 3; + public const int UserTriggerConditionMax = 3; + public const int UserTriggerConditionActionMax = 3; + public const int PCBangAdditionalEffectID = 100000006; + public const int PCBangAdditionalEffectExp = 1; + public const int PCBangAdditionalEffectMeso = 2; + public const int PCBangItemDefaultPeriod = 1440; + public const int ShadowWorldAutoReviveDeadAction = 1; + public const int GoodInteriorRecommendUICloseTime = 15; + public const string UGCInfoDetailViewPage = "http://www.nexon.net/en/legal/user-generated-content-policy"; + public const int UGCInfoStoryBookID = 39000038; + public const int HomePasswordUsersKickDelay = 10; + public const string TriggerEditorHelpURL = "http://maplestory2.nexon.net/en/news/article/32326"; + public const int QuestRewardSAIgnoreLevel = 10; + public const int RecallCastingTime = 3000; + public const int PartyRecallMeret = 30; + public const float CashCallMedicLeaveDelay = 0.5f; + public const int characterMaxLevel = 99; // Updated + public const int DropSPEPBallMaxLength = 300; + public const int DropSPEPBallTargetZPos = 100; + public const int DropSPEPBallPickUpVel = 250; + public const int DropSPEPBallPickUpGravity = -120; + public const float DropSPEPBallPickUpCompleteRotateTime = 0.05f; + public const int DropSPEPBallPickUpCompleteRotateVel = 5; + public const int EnchantItemBindingRequireLevel = 1; + public const int enchantSuccessBroadcastingLevel = 12; + public const int EnchantEquipIngredientMaxCount = 1000; + public const int EnchantFailStackUsingMaxCount = 100; + public const int EnchantFailStackTakeMaxCount = 1000; + public const int EnchantEquipIngredientOpenLevel = 11; + public const int EnchantEquipIngredientOpenRank = 4; + public const int EnchantEquipIngredientMaxSuccessProb = 3000; + public const int EnchantFailStackOpenLevel = 1; + public const int EnchantFailStackTakeMaxSuccessProb = 10000; + public const int BankCallDuration = 500; + public const string NoticeDialogUrl = "http://nxcache.nexon.net/maplestory2/ingame-banners/index.html"; + public const string NoticeDialogUrlPubTest = "maview:/Game/BannerTest"; + public const int NoticeDialogOpenSeconds = 5000; + public const int RemakeOptionMaxCount = 10; + public const int FisherBoreDuration = 10000; + public const string fishingStartCastingBarText0 = "s_fishing_start_castingbar_text0"; + public const string fishingStartCastingBarText1 = "s_fishing_start_castingbar_text1"; + public const string fishingStartCastingBarText2 = "s_fishing_start_castingbar_text2"; + public const string fishingStartCastingBarText3 = "s_fishing_start_castingbar_text3"; + public const string fishingStartCastingBarText4 = "s_fishing_start_castingbar_text4"; + public const string fishingStartBalloonText0 = "s_fishing_start_balloon_text0"; + public const string fishingStartBalloonText1 = "s_fishing_start_balloon_text1"; + public const string fishingStartBalloonText2 = "s_fishing_start_balloon_text2"; + public const string fishingStartBalloonText3 = "s_fishing_start_balloon_text3"; + public const string fishingStartBalloonText4 = "s_fishing_start_balloon_text4"; + public const string fishingStartBalloonText5 = "s_fishing_start_balloon_text5"; + public const string fishingStartBalloonText6 = "s_fishing_start_balloon_text6"; + public const string fishingStartBalloonText7 = "s_fishing_start_balloon_text7"; + public const string fishingStartBalloonText8 = "s_fishing_start_balloon_text8"; + public const string fishingStartBalloonText9 = "s_fishing_start_balloon_text9"; + public const string fishFightingCastingBarText0 = "s_fishing_fishfighting_castingbar_text0"; + public const string fishFightingBalloonText0 = "s_fishing_fishfighting_balloon_text0"; + public const string fishFightingBalloonText1 = "s_fishing_fishfighting_balloon_text1"; + public const string fishFightingBalloonText2 = "s_fishing_fishfighting_balloon_text2"; + public const string fishFightingBalloonText3 = "s_fishing_fishfighting_balloon_text3"; + public const string fishFightingBalloonText4 = "s_fishing_fishfighting_balloon_text4"; + public const string fishFightingBalloonText5 = "s_fishing_fishfighting_balloon_text5"; + public const int WorldMapSpecialFunctionNpcID0 = 11001276; + public const string WorldMapSpecialFunctionNpcFrame0 = "airship_enabled"; + public const string WorldMapSpecialFunctionNpcTooltip0 = "s_worldmap_special_function_npc0"; + public const int WorldMapSpecialFunctionNpcID1 = 11001403; + public const string WorldMapSpecialFunctionNpcFrame1 = "airship_enabled"; + public const string WorldMapSpecialFunctionNpcTooltip1 = "s_worldmap_special_function_npc0"; + public const int WarpOpenContinent0 = 102; + public const int WarpOpenContinent1 = 103; + public const int WarpOpenContinent2 = 202; + public const int WarpOpenContinent3 = 105; + public const string WriteMusicDetailWebPage = "http://maplestory2.nexon.net/en/news/article/32329"; + public const int WriteMusicStoryBookID = 39000047; + public const int MusicListenInRadius = 900; + public const int MusicListenOutRadius = 2200; + public const int DungeonRoomMaxRewardCount = 99; + public const int DungeonMatchRecommendPickCount = 6; + public const int DungeonSeasonRankMinLevel = 99; + public const int LimitMeretRevival = 1; + public const int MinimapScaleSkipDuration = 5000; + public const int MinimapScaleSkipSplitPixel = 20; + public const int TradeMinMeso = 100; + public const int TradeMaxMeso = 500000000; + public const int TradeFeePercent = 20; + public const int DailyMissionRequireLevel = 50; + public const int MesoMarketBasePrice = 5000000; + public const int MesoMarketProductUnit0 = 5000000; + public const int MesoMarketBuyPayType = 16; + public const int MesoMarketIconType = 0; + public const string MesoMarketTokenDetailUrl = "http://maplestory2.nexon.net/en/news/article/45213"; + public const int BeautyHairShopGotoFieldID = 52000008; + public const int BeautyHairShopGotoPortalID = 1; + public const int BeautyColorShopGotoFieldID = 52000009; + public const int BeautyColorShopGotoPortalID = 1; + public const int BeautyFaceShopGotoFieldID = 52000010; + public const int BeautyFaceShopGotoPortalID = 1; + public const int BeautyStyleExpandSlotPrice = 980; + public const int BeautyStyleMaxSlotCount = 0; + public const int BeautyStyleDefaultSlotCount = 30; + public const int BeautyStyleExpandSlotCount1time = 3; + public const string CashShopFigureAddressPage = "http://maplestory2.nexon.com/cashshop/address"; + public const int NxaCashChargeWebPageWidth = 650; + public const int NxaCashChargeWebPageHeight = 650; + public const int ItemUnLockTime = 259200; + public const int PropertyProtectionTime = 60; + public const string TencentSecurityWebPage = "http://mxd2.qq.com/safe/index.shtml"; + public const int HomeBankCallDuration = 1000; + public const int HomeBankCallCooldown = 30000; + public const string HomeBankCallSequence = "Object_React_A"; + public const int HomeDoctorCallDuration = 1000; + public const int HomeDoctorCallCooldown = 30000; + public const string HomeDoctorCallSequence = "EmergencyHelicopter_A"; + public const int HomeDoctorNpcID = 11001668; + public const int HomeDoctorScriptID0 = 1; + public const int HomeDoctorScriptID1 = 10; + public const int EnchantMasterScriptID = 31; + public const int RestExpAcquireRate = 10000; + public const int RestExpMaxAcquireRate = 100000; + public const int ApartmentPreviewRequireLevel = 50; + public const int ApartmentPreviewRequireQuestID = 90000060; + public const int KeyboardGuideShowLevel = 13; + public const int extendAutoFishMaxCount = 8; + public const int extendAutoPlayInstrumentMaxCount = 8; + public const int ResetShadowBuffMeret = 100; + public const int InventoryExpandPrice1Row = 390; + public const int VIPServicePeriodLimitDay = 100000000; + public const int VIPMarketCommissionSale = 20; + public const int BreedDuration = 767; + public const int HarvestDuration = 767; + public const int RestartQuestStartField = 52000056; + public const int RestartQuestStartFieldRuneblader = 63000006; + public const int RestartQuestStartFieldStriker = 63000015; + public const int RestartQuestStartFieldSoulBinder = 63000035; + public const int QuestPortalKeepTime = 300; + public const string QuestPortalKeepNif = "Eff_Com_Portal_E_Quest"; + public const int QuestPortalDimensionY = 50; + public const int QuestPortalDimensionZ = 350; + public const int QuestPortalSummonTime = 600; + public const int QuestPortalDistanceFromNpc = 200; + public const int PetChangeNameMeret = 100; + public const int PetRunSpeed = 350; + public const int PetPickDistance = 1050; + public const int PetSummonCastTime = 800; + public const int PetBoreTime = 60000; + public const int PetIdleTime = 70000; + public const int PetTiredTime = 10000; + public const int PetSkillTime = 13000; + public const string PetEffectUse = "Pet/Eff_Pet_Use.xml"; + public const string PetEffectSkill = "Pet/Eff_Pet_Skill.xml"; + public const string PetEffectHappy = "Pet/Eff_Pet_Happy.xml"; + public const string PetGemChatBalloon = "pet"; + public const int PetTrapAreaDistanceEasy = 150; + public const int PetTrapAreaDistanceNormal = 150; + public const int PetTrapAreaDistanceHard = 150; + public const string PetTrapAreaEffectEasy = "Pet/Eff_Pet_TrapInstallArea_easy.xml"; + public const string PetTrapAreaEffectNormal = "Pet/Eff_Pet_TrapInstallArea_normal.xml"; + public const string PetTrapAreaEffectHard = "Pet/Eff_Pet_TrapInstallArea_hard.xml"; + public const string PetTrapAreaEffectOtherUser = "Pet/Eff_Pet_TrapArea_OtherUser.xml"; + public const string PetTamingMaxPointEffect = "Pet/Eff_PetTaming_MaxPoint.xml"; + public const string PetTamingAttackMissEffect = "Pet/Eff_PetTaming_Attack_Miss.xml"; + public const string PetTrapDropItemEffect = "Pet/Eff_PetTrap_DropItem.xml"; + public const int TamingPetEscapeTime = 300; + public const int TamingPetMaxPoint = 10000; + public const int PetNameLengthMin = 2; + public const int PetNameLengthMax = 25; + public const int PetTrapDropVisibleDelay = 2000; + public const int PetMaxLevel = 50; + public const string VisitorBookURL = ""; + public const int OneShotSkillID = 19900061; + public const int BagSlotTabGameCount = 48; + public const int BagSlotTabSkinCount = 150; + public const int BagSlotTabSummonCount = 48; + public const int BagSlotTabMaterialCount = 48; + public const int BagSlotTabMasteryCount = 126; + public const int BagSlotTabLifeCount = 48; + public const int BagSlotTabQuestCount = 48; + public const int BagSlotTabGemCount = 48; + public const int BagSlotTabPetCount = 60; + public const int BagSlotTabPetEquipCount = 48; + public const int BagSlotTabActiveSkillCount = 84; + public const int BagSlotTabCoinCount = 48; + public const int BagSlotTabBadgeCount = 60; + public const int BagSlotTabMiscCount = 84; + public const int BagSlotTabLapenshardCount = 48; + public const int BagSlotTabPieceCount = 48; + public const int BagSlotTabGameCountMax = 48; + public const int BagSlotTabSkinCountMax = 150; + public const int BagSlotTabSummonCountMax = 48; + public const int BagSlotTabMaterialCountMax = 48; + public const int BagSlotTabMasteryCountMax = 48; + public const int BagSlotTabLifeCountMax = 48; + public const int BagSlotTabQuestCountMax = 48; + public const int BagSlotTabGemCountMax = 48; + public const int BagSlotTabPetCountMax = 78; + public const int BagSlotTabPetEquipCountMax = 48; + public const int BagSlotTabActiveSkillCountMax = 48; + public const int BagSlotTabCoinCountMax = 48; + public const int BagSlotTabBadgeCountMax = 48; + public const int BagSlotTabMiscCountMax = 48; + public const int BagSlotTabLapenshardCountMax = 48; + public const int BagSlotTabPieceCountMax = 48; + public const int MasteryObjectInteractionDistance = 150; + public const float GatheringObjectMarkOffsetX = 0.0f; + public const float GatheringObjectMarkOffsetY = 0.0f; + public const float BreedingObjectMarkOffsetX = 0.0f; + public const float BreedingObjectMarkOffsetY = 0.0f; + public const int UGCAttention = 0; + public const int UGCInfringementCenter = 1; + public const string CharacterSelectBoreIdleEffect_Ranger = ""; + public const string CharacterSelectBoreIdleEffect_SoulBinder = ""; + public const int DisableSoloPlayHighLevelDungeon = 0; + public const int MergeSmithScriptID = 10; + public const int AutoPressActionKeyDuration = 500; + public const int WebBrowserSizeWidthMin = 438; + public const int WebBrowserSizeWidthMax = 1700; + public const int WebBrowserSizeHeightMin = 708; + public const int WebBrowserSizeHeightMax = 1003; + public const bool WebBrowserEnableSizingButton = true; + public const int MeretAirTaxiPrice = 15; + public const int GlobalPortalMinLevel = 10; + public const int userMassiveExtraRewardMax = 5; + public const int SkillBookTreeAddTabFeeMeret = 990; + public const int MentorRequireLevel = 50; + public const int MenteeRequireLevel = 30; + public const int MentorMaxWaitingCount = 100; + public const int MenteeMaxReceivedCount = 20; + public const int FindDungeonHelpEasyDungeonLevel = 50; + public const int CoupleEffectCheckTick = 5000; + public const int CoupleEffectCheckRadius = 150; + public const int FameContentsSkyFortressMapID0 = 02000421; + public const int FameContentsSkyFortressMapID1 = 02000422; + public const int FameContentsSkyFortressMapID2 = 52010039; + public const int FameContentsSkyFortressMapID3 = 52010040; + public const int AllianceQuestPickCount = 2; + public const int FieldQuestPickCount = 1; + public const int FameContentsSkyFortressGotoMapID = 02000422; + public const int FameContentsSkyFortressGotoPortalID = 3; + public const int FameContentsRequireQuestID = 91000013; + public const int FameExpedContentsRequireQuestID = 50101050; + public const int DailyPetEnchantMaxCount = 24; + public const int MouseCursorHideTime = 30; + public const int EnchantTransformScriptID = 10; + public const float AutoHideGroupAlpha = 0.6f; + public const int AutoHideGroupHitVisibleTick = 3000; + public const int UgcShopCharRotateStartDegreeY = 178; + public const int UgcShopCharRotateEndDegreeY = 8; + public const int SurvivalScanAdditionalID = 71000052; + public const int MapleSurvivalTopNRanking = 5; + public const string MapleSurvivalSeasonRewardUrl = + "http://maplestory2.nexon.net/en/news/article/32249/mushking-royale-championship-rewards"; + public const int TreeWateringEmotion = 10000; + public const int AdventureLevelLimit = 10000; + public const int AdventureLevelLvUpExp = 1000000; + public const int AdventureLevelMaxExp = 1500000; + public const float AdventureLevelFactor = 0.02f; + public const int AdventureExpFactorElite = 10; + public const int AdventureExpFactorBoss = 100; + public const int AdventureLevelStartLevel = 50; + public const int AdventureLevelLvUpRewardItem = 30001133; + public const int NameColorDeadDuration = 2000; + public const float MesoRevivalFeeReduceLimit = 0.5f; + public const float IngredientFeeReduceLimit = 0.5f; + public const int StatPointLimit_str = 100; + public const int StatPointLimit_dex = 100; + public const int StatPointLimit_int = 100; + public const int StatPointLimit_luk = 100; + public const int StatPointLimit_hp = 100; + public const int StatPointLimit_cap = 60; + public const float GamePadRumbleMultiple = 3.0f; + public const int NurturingEatMaxCount = 0; + public const int NurturingPlayMaxCount = 3; + public const string NurturingQuestTag = "NurturingGhostCats"; + public const int NurturingDuration = 3000; + public const int NurturingInteractionDistance = 150; + public const int NurturingEatGrowth = 10; + public const int NurturingPlayGrowth = 10; + public const int NurturingPlayMailId = 19101804; + public const int NurturingPlayMaxGrowth = 3; + public const int NurturingHungryTime = 1000; + public const int SkillPointLimitLevel1 = 80; + public const int SkillPointLimitLevel2 = 70; + public const int SellPriceNormalMax = 4628; + public const int SellPriceRareMax = 5785; + public const int SellPriceEliteMax = 7405; + public const int SellPriceExcellentMax = 9256; + public const int SellPriceLegendaryMax = 11339; + public const int SellPriceArtifactMax = 13653; + public const string RegionServerUrl_de = "http://ugc.maplestory2.nexon.net/region/region_DE.xml"; + public const string RegionServerUrl_en = "http://ugc.maplestory2.nexon.net/region/region_EN.xml"; + public const string RegionServerUrl_bpo = "http://ugc.maplestory2.nexon.net/region/region_BPO.xml"; + public const int HoldAttackSkillID = 10700252; + public const int TooltipLabelMaxWidth = 408; + public const int ClubNameLengthMin = 2; + public const int ClubNameLengthMax = 25; + public const int ClubMaxCount = 3; + public const int UgcNameLengthMin = 3; + public const int UgcNameLengthMax = 25; + public const int UgcTagLengthMax = 12; + public const int ChangeJobLevel = 60; + public const int LapenshardOpenQuestID = 20002391; + public const int MaidNameLengthMin = 1; + public const int MaidNameLengthMax = 35; + public const int MaidDescLengthMin = 1; + public const int MaidDescLengthMax = 35; + public const int GamePadStickMoveValue = 50; + public const int HighlightMenuUsingLevel = 5; + public const int PartyVoteReadyDurationSeconds = 20; + public const int PartyVoteReadyTagExpireSeconds = 10; + public const int ShieldBarOffsetY = -10; + public const int MouseInteractLimitDistance = 2000; + public const int AutoInstallEquipmentMinLevel = 5; + public const int AutoInstallEquipmentMaxLevel = 49; + public const string PartySearchRegisterComboValues = "4,6,10"; + public const int StatScaleMarkingAdditionalEffect = 70000174; + public const string DungeonRewardFailEmotions = "90200001,90200009,90200005,90200018"; + public const int SummonPetSkillID = 82100001; + public const int UGCMapSetItemEffectCountLimit = 10; + public const string DiscordAppID = "555204064091045904"; + public const int ItemBoxMultiOpenMaxCount = 10; + public const int ItemBoxMultiOpenLimitCount = 500; + public const int BuffBalloonDistance = 3800; + public const int PaybackStartDate = 20191024; + public const int PaybackMailId = 50000020; + public const int PaybackMailPeriodDay = 90; + public const int PaybackMaxRewardMeret = 10000; + public const string PaybackGuideUrl = "http://maplestory2.nexon.com/News/Events"; + public const int DummyNpcMale = 2040998; + public const int DummyNpcFemale = 2040999; + public static int DummyNpc(Gender gender) => gender is Gender.Female ? DummyNpcFemale : DummyNpcMale; + #endregion + + #region server table/constants.xml + 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; + public const float NpcLastSightHeightUp = 525; + public const float NpcLastSightHeightDown = 225; + + public const int RecoveryHPWaitTick = 1000; + public const int RecoverySPWaitTick = 1000; + public const int RecoveryEPWaitTick = 1000; + public const float FallBoundingAddedDistance = 750f; + + public const int UserBattleDurationTick = 5000; + + public const int SystemShopNPCIDConstruct = 11000486; + public const int SystemShopNpcIDUGCDesign = 11000166; + public const int SystemShopNPCIDHonorToken = 11001562; + public const int SystemShopNPCIDFishing = 11001609; + public const int SystemShopNPCIDMentor = 11003561; + public const int SystemShopNPCIDMentee = 11003562; + + public static TimeSpan GlobalCubeSkillIntervalTime = TimeSpan.FromMilliseconds(100); + #endregion } #pragma warning restore IDE1006 // Naming Styles diff --git a/Maple2.Model/Metadata/NpcMetadata.cs b/Maple2.Model/Metadata/NpcMetadata.cs index fca9752e6..55947b432 100644 --- a/Maple2.Model/Metadata/NpcMetadata.cs +++ b/Maple2.Model/Metadata/NpcMetadata.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.Design; -using System.Numerics; -using System.Text.Json.Serialization; +using System.Numerics; using Maple2.Model.Enum; namespace Maple2.Model.Metadata; @@ -19,14 +17,7 @@ public record NpcMetadata( NpcMetadataAction Action, NpcMetadataDead Dead, NpcMetadataCorpse? Corpse, - NpcMetadataLookAtTarget LookAtTarget) : ISearchResult { - public NpcMetadata(NpcMetadata other, float lastSightRadius, float lastSightHeightUp, float lastSightHeightDown) : this(other.Id, - other.Name, other.AiPath, other.Model, other.Stat, other.Basic, other.Distance, other.Skill, other.Property, other.DropInfo, - other.Action, other.Dead, other.Corpse, other.LookAtTarget) { - Distance = new NpcMetadataDistance(Distance.Avoid, Distance.Sight, Distance.SightHeightUp, - Distance.SightHeightDown, lastSightRadius, lastSightHeightUp, lastSightHeightDown); - } -} + NpcMetadataLookAtTarget LookAtTarget) : ISearchResult; public record NpcMetadataModel( string Name, @@ -44,19 +35,10 @@ public record NpcMetadataDistance( float Avoid, float Sight, float SightHeightUp, - float SightHeightDown) { - [JsonConstructor] - public NpcMetadataDistance(float avoid, float sight, float sightHeightUp, float sightHeightDown, float lastSightRadius, - float lastSightHeightUp, float lastSightHeightDown) : this(avoid, sight, sightHeightUp, sightHeightDown) { - LastSightRadius = lastSightRadius; - LastSightHeightUp = lastSightHeightUp; - LastSightHeightDown = lastSightHeightDown; - } - - public float LastSightRadius { get; private set; } - public float LastSightHeightUp { get; private set; } - public float LastSightHeightDown { get; private set; } -} + float SightHeightDown, + float LastSightRadius, + float LastSightHeightUp, + float LastSightHeightDown); public record NpcMetadataSkill( NpcMetadataSkill.Entry[] Entries, diff --git a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs b/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs deleted file mode 100644 index 815e7fe4a..000000000 --- a/Maple2.Model/Metadata/ServerTable/ConstantsTable.cs +++ /dev/null @@ -1,959 +0,0 @@ -using System.Numerics; - -namespace Maple2.Model.Metadata; - -public record ConstantsTable( - float NPCColorScale = 0f, - float NPCDuration = 0f, - float PCColorScale = 0f, - float PCDuration = 0f, - float GetEXPColorScale = 0f, - float GetEXPDuration = 0f, - float AccumulationRatio = 0f, - float NPCRandomDeadPushRate = 0f, - float CustomizingWheelSpeed_Morph = 0f, - float CustomizingWheelSpeed_Item = 0f, - float CustomizingWheelSpeed_Makeup = 0f, - float CustomizingRotationSpeed_Makeup = 0f, - float CustomizingHairFirstPartHighlight = 0f, - float CustomizingHairSecondPartHighlight = 0f, - float LookAtInterval = 0f, - float LookAtDistanceNPC = 0f, - float LookAtDistanceCry = 0f, - bool EnableSkillJumpDown = false, - bool EscapeHitMethodSkill = false, - bool EscapeHitMethodJump = false, - bool EscapeHitMethodMove = false, - bool EscapeHitMoveKeyIsDown = false, - bool CancelSwing_KeyIsDown = false, - bool SkillGlobalCooldown = false, - bool SkillGlobalCooldown_CheckSameSkill = false, - int DamageDistance = 0, - int TalkableDistance = 0, - bool TalkableFrontOnly = false, - int ChatBalloonDistance = 0, - int HpBarDistance = 0, - int EmoticonVisibleDistance = 0, - int RegistUgcDistance = 0, - int RegistUgcDistanceClose = 0, - int ConstructUgcDistance = 0, - int FunctionCubeDistance = 0, - int InteractionDistance = 0, - int HouseMarkShowDistance = 0, - int HouseMarkShowClippingUp = 0, - int HouseMarkShowClippingDown = 0, - int HouseMarkPopupDistance = 0, - int UgcBoundaryStartDistance = 0, - int UgcBoundaryEndDistance = 0, - int DurationForBoundaryDisplay = 0, - TimeSpan UgcHomeSaleWaitingTime = new(), - int UgcContainerExpireDurationNormal = 0, - int UgcContainerExpireDurationCash = 0, - int UgcContainerExpireDurationMerat = 0, - int UgcHomeExtensionNoticeDate = 0, - int UgcHomePasswordExpireDuration = 0, - bool CubeLiftHeightLimitUp = false, - bool CubeLiftHeightLimitDown = false, - int CubeCraftSafetyCapID = 0, - int CubeCraftLightStickLeftID = 0, - int CubeCraftLightStickRightID = 0, - float DropIconDistance = 0f, - int DropIconHeadOffset = 0, - int DropItemMaxLength = 0, - int DropMoneyMaxLength = 0, - float DropItemTargetZPos = 0f, - float DropItemPickUpVel = 0f, - float DropItemPickUpGravity = 0f, - float DropItemPickUpCompleteRotateTime = 0f, - int DropItemPickUpCompleteRotateVel = 0, - int ChatBalloonDuration = 0, - int BoreWaitingTick = 0, - int OffsetPcHpbar = 0, - int OffsetPcNametag = 0, - int OffsetPcChatballoon = 0, - int OffsetPcDamagenumber = 0, - int OffsetPcProfileTag = 0, - float fOffsetOnTombstoneNameTag = 0f, - int OffsetNpcHpBar = 0, - int OffsetNpcNametag = 0, - int OffsetNpcEmoticon = 0, - int OffsetNpcChatBalloon = 0, - int OffsetNpcDamagenumber = 0, - int OffsetNpcMonologue = 0, - int OffsetActionTooltipX = 0, - int OffsetActionTooltipY = 0, - int OffsetPcPopupMenu = 0, - int DamageGap = 0, - int DamageRenderCount = 0, - int DamageRenderTotalCount = 0, - float DamageOtherScale = 0f, - float DamageOtherAlpha = 0f, - int DamageEffectMinHPPercent = 0, - int DamageEffectCriticalPercent = 0, - int ShopSeedResetTime = 0, - int ShopRepurchaseMax = 0, - int ShopSellConfirmPrice = 0, - int ShopBuyConfirmPrice = 0, - float ClimbVelocityV = 0f, - float ClimbVelocityH = 0f, - int StoreExpandMaxSlotCount = 0, - int StoreExpandPrice1Row = 0, - int StoreDepositMax = 0, - int StoreWithdrawMax = 0, - int CameraExtraMoveScaleByMonster = 0, - int CameraExtraMoveScaleByMap = 0, - int CameraExtraDistance = 0, - float CameraFinalLoose = 0f, - float CameraCurrentLoose = 0f, - float CameraUpdateLoose = 0f, - int CameraVelocityInPortalMove = 0, - int MonologueInterval = 0, - int MonologueRandom = 0, - int MonologueShowTime = 0, - int ShowKillCountMin = 0, - int UserRevivalInvincibleTick = 0, - int UserRevivalPaneltyPercent = 0, - string UserRevivalIconPath = "", - string UserRevivalInvincibleIconPath = "", - int GetExpMinVelocity = 0, - int GetExpVelocityPer1Length = 0, - float[] GetExpControlValue0 = null!, - float[] GetExpControlValue1 = null!, - string GetExpTargetPCDummyName = "", - float GetExpTimeAcceleration = 0f, - float GetExpCollisionRadius = 0f, - int SkipFrameGameObject = 0, - int SkipFrameDistanceGameObject = 0, - float RegionSkillFadeOutDuration = 0f, - int PassengerProfileImageSize = 0, - int PassengerProfileImageLifeTime = 0, - int PassengerProfileImageShowNumber = 0, - int PassengerProfileImageShowCooldown = 0, - int PassengerProfileImageShowCooldownParty = 0, - int PassengerProfileImageShowRange = 0, - int UGCCameraDefaultSize = 0, - int UGCCameraMinSize = 0, - int UGCCameraMaxSize = 0, - int UGCCameraSnapshotPreviewTime = 0, - int UGCImgUploadSizeLimit = 0, - int UGCImgFileCountCheck = 0, - int WindAmp2Cloak = 0, - float WindPeriod2Cloak = 0f, - float WindPeriodVar2Cloak = 0f, - float VolumeMyPcToNpc = 0f, - float VolumeMyPcToObject = 0f, - float VolumeMyPcToBreakableObject = 0f, - float VolumeNpcToMyPc = 0f, - float VolumePcToNpc = 0f, - float VolumePcToBreakableObject = 0f, - float VolumeNpcToPc = 0f, - float VolumeOtherPc = 0f, - int ItemDropLevelMaxBoundry = 0, - float moneyTreeDropHeight = 0f, - float moneyTreeDropBase = 0f, - int moneyTreeDropRandom = 0, - int WisperIgnoreTime = 0, - int WisperMaxCount = 0, - int WisperDurationTime = 0, - float BossHpBarAutoDetectRange = 0f, - float BossHpBarDuration = 0f, - float FindHoldTargetRange = 0f, - int FindGrabNodeRange = 0, - Vector3 UgcshopCharCameraLookat = default, - Vector3 UgcshopCharCameraPos = default, - int UgcshopCharCameraMinDistance = 0, - int UgcshopCharCameraZoomVelocity = 0, - Vector3 UgcshopCubeCameraLookat = default, - Vector3 UgcshopCubeCameraPos = default, - int UgcshopCubeCameraMinDistance = 0, - int UgcshopCubeCameraZoomVelocity = 0, - Vector3 UgcshopRideeCameraLookat = default, - Vector3 UgcshopRideeCameraPos = default, - int UgcshopRideeCameraMinDistance = 0, - int UgcshopRideeCameraZoomVelocity = 0, - int FieldCachingCount = 0, - float FieldCachingTime = 0f, - int FieldCachingMaxCount = 0, - int FieldUnloadThreshold = 0, - float EffectLODOneStepDistance = 0f, - float EffectLODTwoStepDistance = 0f, - float EffectLODThreeStepDistance = 0f, - int TelescopeFindDistance = 0, - int BoatPrice = 0, - int QuestGuidePageCount = 0, - int QuestGuideMaxCount = 0, - float CameraInterpolationTime = 0f, - int TransparencyCP = 0, - int TransparencyEY = 0, - int TransparencyCL = 0, - int TransparencyPA = 0, - int TransparencyMT = 0, - int TransparencyEA = 0, - int TransparencyFH = 0, - int TransparencyGL = 0, - int TransparencyRI = 0, - int TransparencySH = 0, - float DefaultDropItemAlpha = 0f, - float DropItemPickFailHeight = 0f, - float DropItemPickFailTime = 0f, - int TaxiStationFindDistance = 0, - int TaxiCallDuration = 0, - int TaxiCallBestDriverDuration = 0, - int TaxiCallBestDriverLevel = 0, - int AirTaxiCashCallDuration = 0, - int AirTaxiMesoCallDuration = 0, - int TradeRequestDuration = 0, - int UserPortalInvincibleTick = 0, - string UserPortalInvincibleIconPath = "", - int SummonRideeDuration = 0, - int WorldMapAdjustTileX = 0, - int WorldMapAdjustTileY = 0, - float TimeScalePCScale = 0f, - float TimeScalePCDuration = 0f, - int GoToHomeCastingTime = 0, - int returnHomeSkill = 0, - int returnHomeSkillMerat = 0, - int TutorialIntroSkipTime = 0, - int[] AvatarDefaultItemMale = null!, - int[] AvatarDefaultItemFemale = null!, - int TalkCooldown = 0, - int AddressPopupDuration = 0, - int MaxFPS = 0, - int UGCShopSellMinPrice = 0, - int UGCShopSellMaxPrice = 0, - int UGCShopSaleDay = 0, - int UGCShopAdFeeMerat = 0, - int UGCShopAdHour = 0, - int UGCShopSellingRestrictAmount = 0, - int MeratMarketHomeBannerShowTick = 0, - long BlackMarketSellMinPrice = 0, - long BlackMarketSellMaxPrice = 0, - int BlackMarketSellEndDay = 0, - int ItemTransferBlackMarketGrade = 0, - int UgcBannerCheckTime = 0, - int FastChat_CheckTime = 0, - int FastChat_CheckCount = 0, - int SameChat_CheckTime = 0, - int SameChat_CheckCount = 0, - int SameChat_RestrictTime = 0, - int FastChat_RestrictTime = 0, - int RestrictChat_AddRestrictTime = 0, - int AccumWarning_AddRestrictTime = 0, - int RestrictWarning_ReleaseTime = 0, - int MaxChatLength = 0, - int EmotionBoreAnimProbability = 0, - float FallMoveSpeed = 0f, - int GuildCreatePrice = 0, - int GuildCreateMinLevel = 0, - int GuildNameLengthMin = 0, - int GuildNameLengthMax = 0, - float guildFundRate = 0f, - int guildExpMaxCountForPlayTime = 0, - int guildDonateMeso = 0, - int guildStorageGiftNeedPeriod = 0, - int guildStorageItemMail = 0, - string mirrorGuideMoviePath = "", - string hairGuideMoviePath = "", - string makeUpGuideMoviePath = "", - int FastShimmerRadius = 0, - int FastShimmerHeight = 0, - int SmartRecommendNotify_DurationTick = 0, - int BootyPopupDuration = 0, - string charCreateGuideMoviePath = "", - int KeyIsDownSkill_MaxDurationTick = 0, - int MaxBuddyCount = 0, - int MaxBlockCount = 0, - int UserPendingRemoveTime = 0, - int HideCubeDuration = 0, - int DailyTrophyPickDiffLevel = 0, - int dailyAchievePickCount = 0, - int MovementEventDistance = 0, - int HomeReturnPortalKeepTime = 0, - string HomeReturnPortalKeepNif = "", - int HomeReturnPortalDimensionY = 0, - TimeSpan GlobalCubeSkillIntervalTime = new(), - int RoomEnterPortalDurationTick = 0, - int NpcBossCubeSkillCreateHeight = 0, - int NPCUpdateTickNoUser = 0, - int NPCUpdateTickWanderIdle = 0, - int NpcSmallSize = 0, - int NpcMidSize = 0, - int NpcBigSize = 0, - int NpcMidCutline = 0, - int NpcBigCutline = 0, - int NpcHpRegenStartTime = 0, - int NpcBossHpRegenStartTime = 0, - int NpcHpRegenPeriod = 0, - float NpcHpRegenPercent = 0f, - float NpcBossHpRegenPercent = 0f, - int NpcCombatAbandon = 0, - int BossCombatAbandon = 0, - int NpcImpossibleCombatAbandon = 0, - int MobLifeTimeExtend = 0, - float CanGetRewardDistance = 0f, - float CanGetRewardEliteDistance = 0f, - float CanGetRewardBossDistance = 0f, - int ExpLevelMaxBoundry = 0, - int CorpseHitDeadAfterCoolDown = 0, - int CorpseHitDropCoolDown = 0, - int CorpseHitDeadAfterAssistBonusCoolDown = 0, - int CorpseHitDropAssistBonusCoolDown = 0, - int UserBattleDurationTick = 0, - int RecoveryHPWaitTick = 0, - int RecoverySPWaitTick = 0, - int RecoveryEPWaitTick = 0, - int timeResetDead = 180, - int maxDeadCount = 0, - byte hitPerDeadCount = 0, - int spawntimePerDeadCount = 0, - int UserRevivalPaneltyTick = 0, - int UserRevivalPaneltyMinLevel = 0, - int revivalRate = 0, - int ChaosPointGetDefault = 0, - int ChaosPointGetPenaltyLevel = 0, - int ChaosPointGetPenalty = 0, - float FallBoundingAddedDistance = 0f, - int BoatDestinationID = 0, - int NpcKillRecoveryProbability = 0, - int InteractRemoveRetryTick = 0, - int TradeDistance = 0, - int PlayTimeDurationTick = 0, - TimeSpan DailyTrophyResetDate = new(), - float NpcLastingSightRadius = 0f, - float NpcLastingSightHeightUp = 0f, - float NpcLastingSightHeightDown = 0f, - int HoldTimeEventTick = 5000, - int OnixItemID = 0, - int BindOnixItemID = 0, - int ChaosOnixItemID = 0, - int BindChaosOnixItemID = 0, - int CrystalItemID = 0, - int ChaosCrystalItemID = 0, - int SkillChaosCrystalItemID = 0, - int RedCrystalItemID = 0, - int BlueCrystalItemID = 0, - int GreenCrystalItemID = 0, - int enchantReturnItemID = 0, - int PetCapsuleItemID = 0, - int shadowWorldBuffHpUp = 0, - int shadowWorldBuffMoveProtect = 0, - int pvpZoneUserGlobalDropID = 0, - int pvpZoneUserIndividualDropID = 0, - int pvpZoneUserDropRank = 0, - int shadowWorldUserGlobalDropID = 0, - int shadowWorldUserIndividualDropID = 0, - int shadowWorldUserDropRank = 0, - int userKillDuration = 0, - int userKillSlayerCount = 0, - int userKillRulerCount = 0, - int SystemShopNPCIDConstruct = 0, - int SystemShopNpcIDUGCDesign = 0, - int SystemShopNPCIDHonorToken = 0, - int SystemShopNPCIDFishing = 0, - int SystemShopNPCIDMentor = 0, - int SystemShopNPCIDMentee = 0, - string BlackMarketOpeningTime = "", - string BlackMarketClosingTime = "", - int BlackMarketCollectWaitSecond = 0, - int GmGlideSkillID = 0, - int normalChannelMin = 0, - int normalChannelUser = 0, - int shadowChannelMin = 0, - int shadowChannelUser = 0, - int dynamicChannelDecreaseTick = 0, - int PeriodOfMaidEmployment = 0, - int MaidReadyToPay = 0, - int MaidAffinityMax = 0, - float invokeEffectTargetCountFactor1 = 0f, - float invokeEffectTargetCountFactor2 = 0f, - float invokeEffectTargetCountFactor3 = 0f, - float invokeEffectTargetCountFactor4 = 0f, - float invokeEffectTargetCountFactor5 = 0f, - float invokeEffectTargetCountFactor6 = 0f, - float invokeEffectTargetCountFactor7 = 0f, - float invokeEffectTargetCountFactor8 = 0f, - float invokeEffectTargetCountFactor9 = 0f, - float invokeEffectTargetCountFactor10 = 0f, - int MeratRevivalDebuffCode = 0, - float MeratRevivalFeeReduceLimit = 0f, - int MeratConsumeWorldChat = 0, - int MeratConsumeChannelChat = 0, - int MeratConsumeSuperChat = 0, - int guildPVPMatchingTime = 0, - int guildPVPWinPoint = 0, - int guildPVPLosePoint = 0, - int[] guildPVPAdditionalEffect = null!, - int ModePvPRecoverySkill = 0, - int ModePvPRecoverySP = 0, - int ModePvPInvincibleTime = 0, - int ModePVPAdditionalEffect = 0, - int ModePvPReviveBuff = 0, - int pvpBtiRewardItem = 0, - int pvpBtiRewardWinnerCount = 0, - int pvpBtiRewardLoserCount = 0, - int PvpGuildRewardItem = 0, - int PvpGuildRewardWinnerCount = 0, - int PvpGuildRewardLoserCount = 0, - int[] ModePVPRedArenaAdditionalEffect = null!, - int ModePvPScoreDead = -50, - int ModePvPScoreKill = 100, - int[] ModePVPBloodMineAdditionalEffect = null!, - int pvpFFAShortComboTick = 0, - int pvpFFALongComboTick = 0, - int pvpFFASlayerCount = 0, - int pvpFFARulerCount = 0, - int PvpFFAReward1Count = 0, - int PvpFFAReward2Count = 0, - int PvpFFAReward3Count = 0, - int PvpFFAReward4Count = 0, - int PvpFFAReward5Count = 0, - int PvpFFAReward6Count = 0, - int PvpFFAReward7Count = 0, - int PvpFFAReward8Count = 0, - int PvpFFAReward9Count = 0, - int PvpFFAReward10Count = 0, - int PvpFFARewardItem = 0, - int PvpFFAAdditionRewardRate = 0, - int rankDuelPvpMatchingTime = 0, - int rankDuelPvpMatchingMinGap = 0, - int[] ModePVPDuelRankArenaAdditionalEffect = null!, - int MailExpiryDays = 0, - int MailExpiryDaysPremium = 0, - int MailExpiryDaysBlackMarket = 0, - int MailExpiryDaysFittingDoll = 0, - int decreaseMaidMoodValue = 0, - int decreaseMaidMoodMinutes = 0, - int WorldmapBossTooltipCount = 0, - int ShowNameTagEnchantItemGrade = 0, - int ShowNameTagEnchantLevel = 0, - int BossNotifyAbsLevel = 0, - int RoomExitWaitSecond = 0, - int AdditionalMesoMaxRate = 0, - int AdditionalExpMaxRate = 0, - int HonorTokenMax = 0, - int KarmaTokenMax = 0, - int LuTokenMax = 0, - int HabiTokenMax = 0, - int ReverseCoinMax = 0, - int MentorTokenMax = 0, // From KMS - int MenteeTokenMax = 0, // From KMS - int CharacterDestroyDivisionLevel = 0, - int CharacterDestroyWaitSecond = 0, - int BossShimmerScaleUpActiveDistance = 0, - float BossShimmerScaleUpSize = 0f, - float SwimDashSpeed = 0f, - int UserTriggerStateMax = 0, - int UserTriggerEnterActionMax = 0, - int UserTriggerConditionMax = 0, - int UserTriggerConditionActionMax = 0, - int PCBangAdditionalEffectID = 0, - int PCBangAdditionalEffectExp = 0, - int PCBangAdditionalEffectMeso = 0, - int PCBangItemDefaultPeriod = 0, - int ShadowWorldAutoReviveDeadAction = 0, - int GoodIteriorRecommendUICloseTime = 0, - string UGCInfoDetailViewPage = "", - int UGCInfoStoryBookID = 0, - int HomePasswordUsersKickDelay = 0, - string TriggerEditorHelpURL = "", - int partyBuffID0 = 0, - int partyBuffID1 = 0, - int returnUserPartyBuffID0 = 0, - int returnUserPartyBuffID1 = 0, - int QuestRewardSAIgnoreLevel = 0, - int ugcmapMaxUserCount = 0, - int RecallCastingTime = 0, - string RecallGuildPortalNif = "", - string RecallPartyPortalNif = "", - string RecallWeddingPortalNif = "", - int PartyRecallMerat = 0, - int RecallPortalKeepTime = 0, - int RecallPartyPortalKeepTime = 0, - float CashCallMedicLeaveDelay = 0f, - int characterSlotBaseCount = 0, - int characterSlotMaxExtraCount = 0, - int HitNPCDropCooldown = 0, - int DropSPEPBallMaxLength = 0, - int DropSPEPBallTargetZPos = 0, - int DropSPEPBallPickUpVel = 0, - int DropSPEPBallPickUpGravity = 0, - float DropSPEPBallPickUpCompleteRotateTime = 0f, - int DropSPEPBallPickUpCompleteRotateVel = 0, - int EnchantItemBindingRequireLevel = 0, - int enchantSuccessBroadcastingLevel = 0, - int EnchantEquipIngredientMaxCount = 0, - int EnchantFailStackUsingMaxCount = 0, - int EnchantFailStackTakeMaxCount = 0, - int EnchantEquipIngredientOpenLevel = 0, - int EnchantEquipIngredientOpenRank = 0, - int EnchantEquipIngredientMaxSuccessProb = 0, - int EnchantFailStackOpenLevel = 0, - int EnchantFailStackTakeMaxSuccessProb = 0, - int EnchantExpRefundMail = 0, - int BankCallDuration = 0, - string NoticeDialogUrl = "", - string NoticeDialogUrlPubTest = "", - int NoticeDialogOpenSeconds = 0, - int RemakeOptionMaxCount = 0, - int fishFightingProp = 0, - int fisherBoreDuration = 0, - string fishingStartCastingBarText0 = "", - string fishingStartCastingBarText1 = "", - string fishingStartCastingBarText2 = "", - string fishingStartCastingBarText3 = "", - string fishingStartCastingBarText4 = "", - string fishingStartBalloonText0 = "", - string fishingStartBalloonText1 = "", - string fishingStartBalloonText2 = "", - string fishingStartBalloonText3 = "", - string fishingStartBalloonText4 = "", - string fishingStartBalloonText5 = "", - string fishingStartBalloonText6 = "", - string fishingStartBalloonText7 = "", - string fishingStartBalloonText8 = "", - string fishingStartBalloonText9 = "", - string fishFightingCastingBarText0 = "", - string fishFightingBalloonText0 = "", - string fishFightingBalloonText1 = "", - string fishFightingBalloonText2 = "", - string fishFightingBalloonText3 = "", - string fishFightingBalloonText4 = "", - string fishFightingBalloonText5 = "", - int WorldmapSpecialFunctionNpcID0 = 0, - string WorldmapSpecialFunctionNpcFrame0 = "", - string WorldmapSpecialFunctionNpcTooltip0 = "", - int WorldmapSpecialFunctionNpcID1 = 0, - string WorldmapSpecialFunctionNpcFrame1 = "", - string WorldmapSpecialFunctionNpcTooltip1 = "", - int WarpOpenContinent0 = 0, - int WarpOpenContinent1 = 0, - int WarpOpenContinent2 = 0, - int WarpOpenContinent3 = 0, - string WriteMusicDetailWebPage = "", - int WriteMusicStoryBookID = 0, - int MusicListenInRadius = 0, - int MusicListenOutRadius = 0, - int MusicEnsembleRadius = 0, - int MusicEnsembleDisplayAdditionalID = 0, - int DungeonRandomMatchBuffID = 0, - int DungeonRoomMaxRewardCount = 0, - int DungeonMatchRecommendPickCount = 0, - int DungeonSeasonRankMinLevel = 0, - int ChaosDungeonReviveBossBuff = 0, - int ChaosDungeonReviveUserDebuff = 0, - int LimitMeratRevival = 0, - int MinimapScaleSkipDuration = 0, - int MinimapScaleSkipSplitPixel = 0, - int TradeMinMeso = 0, - int TradeMaxMeso = 0, - int TradeFeePercent = 0, - int GuideQuestDailyPickCountCommon = 0, - int GuideQuestDailyPickCountDungeon = 0, - int GuideQuestDailyPickCountBoss = 0, - int DailyMissionPickCount = 0, - int DailyMissionRequireLevel = 0, - float NearDropDistance = 0f, - float FarDropDistance = 0f, - int MesoMarketBasePrice = 0, - int MesoMarketProductUnit0 = 0, - int MesoMarketProductUnit1 = 0, - int MesoMarketProductUnit2 = 0, - int MesoMarketProductUnit3 = 0, - int MesoMarketProductUnit4 = 0, - int MesoMarketProductUnit5 = 0, - int MesoMarketProductUnit6 = 0, - int MesoMarketProductUnit7 = 0, - int MesoMarketProductUnit8 = 0, - int MesoMarketProductUnit9 = 0, - int MesoMarketBuyPayType = 0, - int MesoMarketIconType = 0, - int BeautyHairShopGotoFieldID = 0, - int BeautyHairShopGotoPortalID = 0, - int BeautyColorShopGotoFieldID = 0, - int BeautyColorShopGotoPortalID = 0, - int BeautyFaceShopGotoFieldID = 0, - int BeautyFaceShopGotoPortalID = 0, - int DropItemSendMail = 0, - int BeautyStyleExpandSlotPrice = 0, - int BeautyStyleMaxSlotCount = 0, - int BeautyStyleDefaultSlotCount = 0, - int BeautyStyleExpandSlotCount1time = 0, - int LuckyBagCouponItemID = 0, - string CashshopFigureAddressPage = "", - string TencentCashChargeWebPage = "", - int TencentCashChargeWebPageWidth = 0, - int TencentCashChargeWebPageHight = 0, - int NxaCashChargeWebPageWidth = 0, - int NxaCashChargeWebPageHight = 0, - int ItemUnLockTime = 0, - int PropertyProtectionTime = 0, - string TencentSecurityWebPage = "", - int HomeBankCallDuration = 0, - int HomeBankCallCooltime = 0, - string HomeBankCallSequence = "", - int HomeDoctorCallDuration = 0, - int HomeDoctorCallCooltime = 0, - string HomeDoctorCallSequence = "", - int HomeDoctorNpcID = 0, - int HomeDoctorScriptID0 = 0, - int HomeDoctorScriptID1 = 0, - int EnchantMasterScriptID = 0, - int RestExpAcquireRate = 0, - int RestExpMaxAcquireRate = 0, - int ApartmentPreviewRequireLevel = 0, - int ApartmentPreviewRequireQuestID = 0, - int CharacterAbilityDefaultPoint = 0, - int CharacterAbilityResetCoolTimeMinute = 0, - int CharacterAbilityResetMeso = 0, - int CharacterAbilityResetMerat = 0, - int CharacterAbilityOpenQuestID = 0, - int KeyboardGuideShowLevel = 0, - int extendAutoFishMaxCount = 0, - int extendAutoPlayInstrumentMaxCount = 0, - int ResetShadowBuffMerat = 0, - int InventoryExpandPrice1Row = 0, - int VIPServicePeriodLimitDay = 0, - int VIPMarketCommitionSale = 0, - int DungeonMatchNormalTimeOutTick = 0, - int ChaosDungeonHallFieldID = 0, - int ReverseRaidDungeonHallFieldID = 0, - int LapentaDungeonHallFieldID = 0, - int ColosseumDungeonHallFieldID = 0, - int BreedDuration = 0, - int HarvestDuration = 0, - int DungeonRewardUnLimitedMesoPercent = 0, - int DungeonRewardUnLimitedExpPercent = 0, - int RestartQuestStartField = 0, - int RestartQuestStartFieldRuneBlader = 0, - int RestartQuestStartFieldStriker = 0, - int RestartQuestStartFieldSoulBinder = 0, - int QuestPortalKeepTime = 0, - string QuestPortalKeepNif = "", - int QuestPortalDimensionY = 0, - int QuestPortalDimensionZ = 0, - int QuestPortalSummonTime = 0, - int QuestPortalDistanceFromNpc = 0, - int PetChangeNameMerat = 0, - int ConstructAuthorityMax = 0, - int HonorTokenResetDayOfWeek = 0, - int HonorTokenResetTimeHour = 0, - int PetLastAttackSkillCheckTick = 0, - string PetBattleAiPath = "", - int PetRunSpeed = 0, - int PetUpdateTargetInterval = 0, - int PetPickDistance = 0, - int PetSummonCastTime = 0, - int PetBoreTime = 0, - int PetIdleTime = 0, - int PetTiredTime = 0, - int PetSkillTime = 0, - string PetEffectUse = "", - string PetEffectSkill = "", - string PetEffectHappy = "", - string PetGemChatBalloon = "", - int PetTrapAreaDistanceEasy = 0, - int PetTrapAreaDistanceNormal = 0, - int PetTrapAreaDistanceHard = 0, - string PetTrapAreaEffectEasy = "", - string PetTrapAreaEffectNormal = "", - string PetTrapAreaEffectHard = "", - string PetTrapAreaEffectOtherUser = "", - string PetTamingMaxPointEffect = "", - string PetTamingAttackMissEffect = "", - string PetTrapDropItemEffect = "", - float TamingPetEscapeRate = 0f, - int TamingPetEscapeTime = 0, - int TamingPetMaxPoint = 0, - float TamingPetValidDistance = 0f, - int PetNameLengthMin = 0, - int PetNameLengthMax = 0, - int PetTrapDropVisibleDelay = 0, - int PetMaxLevel = 0, - string VisitorBookURL = "", - short[] bagSlotTabGameCount = null!, - short[] bagSlotTabSkinCount = null!, - short[] bagSlotTabSummonCount = null!, - short[] bagSlotTabMaterialCount = null!, - short[] bagSlotTabMasteryCount = null!, - short[] bagSlotTabLifeCount = null!, - short[] bagSlotTabQuestCount = null!, - short[] bagSlotTabGemCount = null!, - short[] bagSlotTabPetCount = null!, - short[] bagSlotTabActiveSkillCount = null!, - short[] bagSlotTabCoinCount = null!, - short[] bagSlotTabBadgeCount = null!, - short[] bagSlotTabMiscCount = null!, - short[] bagSlotTabLapenShardCount = null!, - short[] bagSlotTabPieceCount = null!, - int MasteryObjectInteractionDistance = 0, - float GatheringObjectMarkOffsetX = 0f, - float GatheringObjectMarkOffsetY = 0f, - float BreedingObjectMarkOffsetX = 0f, - float BreedingObjectMarkOffsetY = 0f, - int UGCAttention = 0, - int UGCInfringementCenter = 0, - string CharacterSelectBoreIdleEffect_Ranger = "", - string CharacterSelectBoreIdleEffect_SoulBinder = "", - int DisableSoloPlayHighLevelDungeon = 0, - int DungeonMatchCooldownTime = 0, - int DungeonUnitedRewardCountResetLevel = 0, - int MergeSmithScriptID = 0, - int AutoPressActionKeyDuration = 0, - int WebBrowserSizeWidthMin = 0, - int WebBrowserSizeWidthMax = 0, - int WebBrowserSizeHeightMin = 0, - int WebBrowserSizeHeightMax = 0, - bool WebBrowserEnableSizingButton = false, - string LiveBroadcastURL = "", - string TencentWebURL = "", - int SeasonDataSpareCount = 0, - int WebBrowserPopupSizeWidthMin = 0, - int WebBrowserPopupSizeWidthMax = 0, - int WebBrowserPopupSizeHeightMin = 0, - int WebBrowserPopupSizeHeightMax = 0, - int GlobalPortalMinLevel = 0, - int userMassiveExtraRewardMax = 0, - int SkillBookTreeAddTabFeeMerat = 0, - int MentorRequireLevel = 0, - int MenteeRequireLevel = 0, - int MentorMaxWaitingCount = 0, - int MenteeMaxReceivedCount = 0, - int CoupleEffectCheckTick = 0, - int CoupleEffectCheckRadius = 0, - int FameContentsSkyFortressMapID0 = 0, - int FameContentsSkyFortressMapID1 = 0, - int FameContentsSkyFortressMapID2 = 0, - int FameContentsSkyFortressMapID3 = 0, - int AllianceQuestPickCount = 0, - int FieldQuestPickCount = 0, - int FameContentsSkyFortressGotoMapID = 0, - int FameContentsSkyFortressGotoPortalID = 0, - int FameContentsSkyFortressBridgeID = 0, - int FameContentsMissionAttackCount = 0, - int FameContentsFieldQuestPickAccept = 0, - int FameContentsFieldQuestPickComplete = 0, - int DailyPetEnchantMaxCount = 0, - int MouseCursorHideTime = 0, - int EnchantTransformScriptID = 0, - float AutoHideGroupAlpha = 0f, - int AutoHideGroupHitVisibleTick = 0, - int UgcshopCharRotateStartDegreeY = 0, - int UgcshopCharRotateEndDegreeY = 0, - int TreewateringEmotion = 0, - string ShopProbInfoUrl = "", - int AdventureLevelLimit = 0, - int AdventureLevelLvUpExp = 0, - int AdventureLevelMaxExp = 0, - float AdventureLevelFactor = 0f, - int AdventureExpFactorElite = 0, - int AdventureExpFactorBoss = 0, - int AdventureLevelStartLevel = 0, - int AdventureLevelLvUpRewardItem = 0, - int NameColorDeadDuration = 0, - int ConstructExpMaxCount = 0, - float MesoRevivalFeeReduceLimit = 0f, - float IngredientFeeReduceLimit = 0f, - int StatPointLimit_str = 0, - int StatPointLimit_dex = 0, - int StatPointLimit_int = 0, - int StatPointLimit_luk = 0, - int StatPointLimit_hp = 0, - int StatPointLimit_cap = 0, - float GamePadRumbleMultiple = 0f, - int WorldChampionRewardDays = 0, - int NurturingEatMaxCount = 0, - int NurturingPlayMaxCount = 0, - string NurturingQuestTag = "", - int NurturingDuration = 0, - int NurturingInteractionDistance = 0, - int NurturingEatGrowth = 0, - int NurturingPlayGrowth = 0, - int NurturingPlayMailId = 0, - int NurturingPlayMaxGrowth = 0, - int NurturingHungryTime = 0, - int SkillPointLimitLevel1 = 0, - int SkillPointLimitLevel2 = 0, - int SellPriceNormalMax = 0, - int SellPriceRareMax = 0, - int SellPriceEliteMax = 0, - int SellPriceExcellentMax = 0, - int SellPriceLegendaryMax = 0, - int SellPriceArtifactMax = 0, - string RegionServerUrl_de = "", - string RegionServerUrl_en = "", - string RegionServerUrl_bpo = "", - int TooltipLabelMaxWidth = 0, - int ClubNameLengthMin = 0, - int ClubNameLengthMax = 0, - int UgcNameLengthMin = 0, - int UgcNameLengthMax = 0, - int UgcTagLengthMax = 0, - int ChangeJobLevel = 0, - int[] LapenShardOpenQuestID = null!, - int MaidNameLengthMin = 0, - int MaidNameLengthMax = 0, - int MaidDescLengthMin = 0, - int MaidDescLengthMax = 0, - int GamePadStickMoveValue = 0, - int HighlightMenuUsingLevel = 0, - int PartyVoteReadyDurationSeconds = 0, - int PartyVoteReadyTagExpireSeconds = 0, - int ShieldBarOffsetY = 0, - int MouseInteractLimitDistance = 0, - int AutoInstallEquipmentMinLevel = 0, - int AutoInstallEquipmentMaxLevel = 0, - int[] PartySearchRegisterComboValues = null!, - int FieldWarInstanceEnterableDurationSeconds = 0, - int FieldWarRequirePlayerCount = 0, - int FieldWarRequireAchieveID = 0, - int FieldWarRequireLevel = 0, - int StatScaleMarkingAdditionalEffect = 0, - int[] DungeonRewardFailEmotions = null!, - int SummonPetSkillID = 0, - int UGCMapSetItemEffectCountLimit = 0, - int AdventureLevelMissionResetWeekday = 0, - int ItemBoxMultiOpenMaxCount = 0, - int ItemBoxMultiOpenLimitCount = 0, - int BuffBallonDistance = 0, - int PaybackStartDate = 0, - int PaybackSettleMinutes = 0, - int PaybackMarketProductSnList = 0, - int PaybackMailId = 0, - int PaybackMailPeriodDay = 0, - int PaybackMaxRewardMerat = 0, - string PaybackGuideUrl = "", - DateTime PaybackEndDate = default, - int WeddingProposeItemID = 0, - int WeddingInvitationMaxCount = 0, - int WeddingProposeCooltime = 0, - int WeddingDivorceFieldID = 0, - int WeddingInvitationMeso = 0, - int WeddingDivorceMeso = 0, - int WeddingCoolingOffDay = 0, - int WeddingPromiseLimitDay = 0, - int WeddingHallModifyLimitHour = 0, - int WeddingDivorceRequireMarriageDay = 0, - int AdventureProtectRequireLevel = 0, - int AdventureProtectRequireQuest = 0, - int AdventureProtectCharCreateTime = 0, - int PvpOnePunchReward1Count = 0, - int PvpOnePunchReward2Count = 0, - int PvpOnePunchReward3Count = 0, - int PvpOnePunchReward4Count = 0, - int PvpOnePunchReward5Count = 0, - int PvpOnePunchReward6Count = 0, - int PvpOnePunchReward7Count = 0, - int PvpOnePunchReward8Count = 0, - int PvpOnePunchReward9Count = 0, - int PvpOnePunchReward10Count = 0, - int PvpOnePunchRewardItem = 0, - int PvpOnePunchScoreNpcKill = 0, - int SpecialHairShopID = 0, - int[] GemStoneProbList = null!, - int[] SkinGemStoneProbList = null!, - string PersonalInfoAgreementURL = "", - float BothHandLowDamageRatio = 0f, - float BothHandWeaponDamagePenaltyDiv = 0f, - float BothHandGearScorePenaltyDiv = 0f, - string TencentUserConsultationWebPage = "", - string TencentPricavyGuideWebPage = "", - string TencentThirdPartyInformationSharingListWebPage = "", - int PvpOnePunchUserOpenRewardItem = 0, - string TencentCharacterCreateShutdownLeft = "", - string TencentCharacterCreateShutdownRight = "", - int LeadSkillMaxSlot = 0, - float NPCCliffHeight = 0f, - float CustomizingRotationSpeed = 0f, - bool AllowComboAtComboPoint = false, - int AttackRotationSpeed = 0, - int ChaosModeTime = 0, - int ChaosPointPerBlock = 0, - int ChaosPointMaxBlock = 0, - int ChaosPointGetLevel0 = 0, - int ChaosPointGetPoint0 = 0, - int ChaosActionGetLevel0 = 0, - int ChaosActionGetLevel1 = 0, - int ChaosActionGetLevel2 = 0, - int ChaosActionGetLevel3 = 0, - int ChaosActionGetLevel4 = 0, - int OnEnterTriggerClientSideOnlyTick = 0, - int OnEnterTriggerDefaultTick = 0, - int TalkTimeover = 0, - int DropIconVisibleDistance = 0, - int DropMoneyActiveProbability = 0, - int DropMoneyProbability = 0, - int OffsetPcMissionIndicator = 0, - int questHideTime = 0, - int questIntervalTime = 0, - int ShopResetChance = 0, - int DashKeyInputDelay = 0, - int DashSwimConsumeSP = 0, - int DashSwimMoveVel = 0, - float Glide_Gravity = 0f, - float Glide_Height_Limit = 0f, - float Glide_Horizontal_Accelerate = 0f, - int Glide_Horizontal_Velocity = 0, - float Glide_Vertical_Accelerate = 0f, - int Glide_Vertical_Velocity = 0, - int Glide_Vertical_Vibrate_Amplitude = 0, - float Glide_Vertical_Vibrate_Frequency = 0f, - bool Glide_Effect = false, - string Glide_Effect_Run = "", - string Glide_Effect_Idle = "", - string Glide_Ani_Idle = "", - string Glide_Ani_Left = "", - string Glide_Ani_Right = "", - string Glide_Ani_Run = "", - int ConsumeCritical = 0, - int DayToNightTime = 0, - float myPCdayTiming = 0f, - float myPCNightTiming = 0f, - float BGMTiming = 0f, - int dayBaseMinute = 0, - int dayMinute = 0, - int nightMinute = 0, - int QuestRewardSkillSlotQuestID1 = 0, - int QuestRewardSkillSlotQuestID2 = 0, - int QuestRewardSkillSlotQuestID3 = 0, - int QuestRewardSkillSlotQuestID4 = 0, - int QuestRewardSkillSlotQuestID5 = 0, - int QuestRewardSkillSlotItemID1 = 0, - int QuestRewardSkillSlotItemID2 = 0, - int QuestRewardSkillSlotItemID3 = 0, - int QuestRewardSkillSlotItemID4 = 0, - int QuestRewardSkillSlotItemID5 = 0, - int autoTargetingMaxDegree = 0, - float BossHitVibrateFreq = 0f, - float BossHitVibrateAmp = 0f, - float BossHitVibrateDamping = 0f, - float BossHitVibrateDuration = 0f, - int OneTimeWeaponItemID = 0, - int ModelHouse = 0, - int UsingNoPhysXModelUserCount = 0, - int UsingNoPhysXModelActorCount = 0, - int UsingNoPhysXModelJointCount = 0, - int guildFundMax = 0, - bool EnableSoundMute = false, - int BossKillSoundRange = 0, - int monsterPeakTimeNotifyDuration = 0, - int AirTaxiItemID = 0, - int ShowNameTagSellerTitle = 0, - int ShowNameTagChampionTitle = 0, - int ShowNameTagTrophy1000Title = 0, - int ShowNameTagTrophy2000Title = 0, - int ShowNameTagTrophy3000Title = 0, - int ShowNameTagArchitectTitle = 0, - int characterMaxLevel = 0, - string MesoMarketTokenDetailUrl = "", - int OneShotSkillID = 0, - short[] bagSlotTabPetEquipCount = null!, - int MeratAirTaxiPrice = 0, - int FindDungeonHelpEasyDungeonLevel = 0, - int FameContentsRequireQuestID = 0, - int FameExpedContentsRequireQuestID = 0, - int SurvivalScanAdditionalID = 0, - int MapleSurvivalTopNRanking = 0, - string MapleSurvivalSeasonRewardUrl = "", - int HoldAttackSkillID = 0, - string DiscordAppID = "" -) : ServerTable; diff --git a/Maple2.Model/Metadata/ServerTableMetadata.cs b/Maple2.Model/Metadata/ServerTableMetadata.cs index 484a6715d..097a0b79b 100644 --- a/Maple2.Model/Metadata/ServerTableMetadata.cs +++ b/Maple2.Model/Metadata/ServerTableMetadata.cs @@ -47,5 +47,4 @@ public override int GetHashCode() { [JsonDerivedType(typeof(CombineSpawnTable), typeDiscriminator: "combineSpawn")] [JsonDerivedType(typeof(EnchantOptionTable), typeDiscriminator: "enchantOption")] [JsonDerivedType(typeof(UnlimitedEnchantOptionTable), typeDiscriminator: "unlimitedEnchantOption")] -[JsonDerivedType(typeof(ConstantsTable), typeDiscriminator: "constants")] public abstract record ServerTable; diff --git a/Maple2.Server.Core/Network/Session.cs b/Maple2.Server.Core/Network/Session.cs index e1ea5e78c..554a4cc41 100644 --- a/Maple2.Server.Core/Network/Session.cs +++ b/Maple2.Server.Core/Network/Session.cs @@ -37,7 +37,6 @@ public abstract class Session : IDisposable { private bool disposed; private int disconnecting; // 0 = not disconnecting, 1 = disconnect in progress/already triggered (reentrancy guard) - private volatile bool sendFailed; // set on first SendRaw failure to stop send queue drain private readonly uint siv; private readonly uint riv; @@ -336,7 +335,6 @@ private void SendRaw(ByteWriter packet) { // Use async write with timeout to prevent indefinite blocking Task writeTask = networkStream.WriteAsync(packet.Buffer, 0, packet.Length); if (!writeTask.Wait(SEND_TIMEOUT_MS)) { - sendFailed = true; Logger.Warning("SendRaw timeout after {Timeout}ms, disconnecting account={AccountId} char={CharacterId}", SEND_TIMEOUT_MS, AccountId, CharacterId); @@ -358,12 +356,10 @@ private void SendRaw(ByteWriter packet) { throw writeTask.Exception?.GetBaseException() ?? new Exception("Write task faulted"); } } catch (Exception ex) when (ex.InnerException is IOException or SocketException || ex is IOException or SocketException) { - // Connection was closed by the client or is no longer valid - sendFailed = true; + // Expected when client closes the connection (e.g., during migration) Logger.Debug("SendRaw connection closed account={AccountId} char={CharacterId}", AccountId, CharacterId); Disconnect(); } catch (Exception ex) { - sendFailed = true; Logger.Warning(ex, "[LIFECYCLE] SendRaw write failed account={AccountId} char={CharacterId}", AccountId, CharacterId); Disconnect(); } @@ -372,7 +368,7 @@ private void SendRaw(ByteWriter packet) { private void SendWorker() { try { foreach ((byte[] packet, int length) in sendQueue.GetConsumingEnumerable()) { - if (disposed || sendFailed) break; + if (disposed) break; // Encrypt outside lock, then send with timeout PoolByteWriter encryptedPacket; diff --git a/Maple2.Server.Game/Commands/PlayerCommand.cs b/Maple2.Server.Game/Commands/PlayerCommand.cs index 6ceef9728..c1207348a 100644 --- a/Maple2.Server.Game/Commands/PlayerCommand.cs +++ b/Maple2.Server.Game/Commands/PlayerCommand.cs @@ -127,8 +127,8 @@ public LevelCommand(GameSession session) : base("level", "Set player level.") { private void Handle(InvocationContext ctx, short level) { try { - if (level < 1 || level > session.ServerTableMetadata.ConstantsTable.characterMaxLevel) { - ctx.Console.Error.WriteLine($"Invalid level: {level}. Must be between 1 and {session.ServerTableMetadata.ConstantsTable.characterMaxLevel}."); + if (level is < 1 or > Constant.characterMaxLevel) { + ctx.Console.Error.WriteLine($"Invalid level: {level}. Must be between 1 and {Constant.characterMaxLevel}."); return; } @@ -191,8 +191,8 @@ public PrestigeCommand(GameSession session) : base("prestige", "Sets prestige le private void Handle(InvocationContext ctx, int level) { try { - if (level < 1 || level > session.ServerTableMetadata.ConstantsTable.AdventureLevelLimit) { - ctx.Console.Error.WriteLine($"Invalid level: {level}. Must be between 1 and {session.ServerTableMetadata.ConstantsTable.AdventureLevelLimit}."); + if (level is < 1 or > Constant.AdventureLevelLimit) { + ctx.Console.Error.WriteLine($"Invalid level: {level}. Must be between 1 and {Constant.AdventureLevelLimit}."); return; } @@ -328,7 +328,7 @@ private void JobAdvance(Job job) { session.Player.Buffs.Clear(); session.Player.Buffs.Initialize(); - session.Player.Buffs.LoadFieldBuffs(session.ServerTableMetadata.ConstantsTable.shadowWorldBuffHpUp, session.ServerTableMetadata.ConstantsTable.shadowWorldBuffMoveProtect); + session.Player.Buffs.LoadFieldBuffs(); session.Stats.Refresh(); session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo)); } diff --git a/Maple2.Server.Game/Manager/BlackMarketManager.cs b/Maple2.Server.Game/Manager/BlackMarketManager.cs index 5086c8a40..b95252a88 100644 --- a/Maple2.Server.Game/Manager/BlackMarketManager.cs +++ b/Maple2.Server.Game/Manager/BlackMarketManager.cs @@ -63,7 +63,7 @@ public void Add(long itemUid, long price, int quantity) { AccountId = session.AccountId, CharacterId = session.CharacterId, Deposit = depositFee, - ExpiryTime = DateTime.Now.AddDays(session.ServerTableMetadata.ConstantsTable.BlackMarketSellEndDay).ToEpochSeconds(), + ExpiryTime = DateTime.Now.AddDays(Constant.BlackMarketSellEndDay).ToEpochSeconds(), Price = price, Quantity = quantity, }; diff --git a/Maple2.Server.Game/Manager/BuddyManager.cs b/Maple2.Server.Game/Manager/BuddyManager.cs index 707d7076e..c4787c0db 100644 --- a/Maple2.Server.Game/Manager/BuddyManager.cs +++ b/Maple2.Server.Game/Manager/BuddyManager.cs @@ -87,7 +87,7 @@ public void SendInvite(string name, string message) { session.Send(BuddyPacket.Invite(error: s_buddy_err_my_id_ex)); return; } - if (buddies.Count >= session.ServerTableMetadata.ConstantsTable.MaxBuddyCount) { + if (buddies.Count >= Constant.MaxBuddyCount) { session.Send(BuddyPacket.Invite(error: s_buddy_err_max_buddy)); return; } @@ -113,7 +113,7 @@ public void SendInvite(string name, string message) { try { db.BeginTransaction(); - if (db.CountBuddy(receiverId) >= session.ServerTableMetadata.ConstantsTable.MaxBuddyCount) { + if (db.CountBuddy(receiverId) >= Constant.MaxBuddyCount) { session.Send(BuddyPacket.Invite(name: name, error: s_buddy_err_target_full)); return; } @@ -262,7 +262,7 @@ public void SendBlock(long entryId, string name, string message) { session.Send(BuddyPacket.Block(error: s_buddy_err_unknown)); return; } - if (blocked.Count >= session.ServerTableMetadata.ConstantsTable.MaxBlockCount) { + if (blocked.Count >= Constant.MaxBlockCount) { session.Send(BuddyPacket.Block(name: name, error: s_buddy_err_max_block)); return; } diff --git a/Maple2.Server.Game/Manager/BuffManager.cs b/Maple2.Server.Game/Manager/BuffManager.cs index 3809822b4..f9e378c22 100644 --- a/Maple2.Server.Game/Manager/BuffManager.cs +++ b/Maple2.Server.Game/Manager/BuffManager.cs @@ -60,11 +60,11 @@ public void Clear() { } } - public void LoadFieldBuffs(int shadowWorldBuffHpUp, int shadowWorldBuffMoveProtect) { + public void LoadFieldBuffs() { // Lapenshards // Game Events // Prestige - EnterField(shadowWorldBuffHpUp, shadowWorldBuffMoveProtect); + EnterField(); if (Actor is FieldPlayer player) { player.Session.Config.RefreshPremiumClubBuffs(); } @@ -438,7 +438,7 @@ public void LeaveField() { Remove(buffsToRemove.ToArray()); } - private void EnterField(int shadowWorldBuffHpUp, int shadowWorldBuffMoveProtect) { + private void EnterField() { foreach (MapEntranceBuff buff in Actor.Field.Metadata.EntranceBuffs) { AddBuff(Actor, Actor, buff.Id, buff.Level, Actor.Field.FieldTick); } @@ -458,8 +458,8 @@ private void EnterField(int shadowWorldBuffHpUp, int shadowWorldBuffMoveProtect) } if (Actor.Field.Metadata.Property.Region == MapRegion.ShadowWorld) { - AddBuff(Actor, Actor, shadowWorldBuffHpUp, 1, Actor.Field.FieldTick); - AddBuff(Actor, Actor, shadowWorldBuffMoveProtect, 1, Actor.Field.FieldTick); + AddBuff(Actor, Actor, Constant.shadowWorldBuffHpUp, 1, Actor.Field.FieldTick); + AddBuff(Actor, Actor, Constant.shadowWorldBuffMoveProtect, 1, Actor.Field.FieldTick); } } diff --git a/Maple2.Server.Game/Manager/ConfigManager.cs b/Maple2.Server.Game/Manager/ConfigManager.cs index bfe26ea37..84a83e35b 100644 --- a/Maple2.Server.Game/Manager/ConfigManager.cs +++ b/Maple2.Server.Game/Manager/ConfigManager.cs @@ -27,8 +27,6 @@ public class ConfigManager { private readonly IList favoriteDesigners; private readonly IDictionary lapenshards; private readonly IDictionary skillCooldowns; - private readonly IDictionary statLimits; - public long DeathPenaltyEndTick { get => session.Player.Value.Character.DeathTick; private set => session.Player.Value.Character.DeathTick = value; @@ -96,16 +94,7 @@ public ConfigManager(GameStorage.Request db, GameSession session) { skillPoints = load.SkillPoint ?? new SkillPoint(); ExplorationProgress = load.ExplorationProgress; - statLimits = new Dictionary() { - { "StatPointLimit_str", session.ServerTableMetadata.ConstantsTable.StatPointLimit_str }, - { "StatPointLimit_dex", session.ServerTableMetadata.ConstantsTable.StatPointLimit_dex }, - { "StatPointLimit_int", session.ServerTableMetadata.ConstantsTable.StatPointLimit_int }, - { "StatPointLimit_luk", session.ServerTableMetadata.ConstantsTable.StatPointLimit_luk }, - { "StatPointLimit_hp", session.ServerTableMetadata.ConstantsTable.StatPointLimit_hp }, - { "StatPointLimit_cap", session.ServerTableMetadata.ConstantsTable.StatPointLimit_cap } - }; - - statAttributes = new StatAttributes(statLimits); + statAttributes = new StatAttributes(); if (load.StatPoints != null) { foreach ((AttributePointSource source, int amount) in load.StatPoints) { if (source == AttributePointSource.Prestige) { @@ -336,7 +325,7 @@ public void LoadRevival() { /// 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 < session.ServerTableMetadata.ConstantsTable.UserRevivalPaneltyMinLevel) { + if (session.Player.Value.Character.Level < Constant.UserRevivalPaneltyMinLevel) { return; } @@ -433,7 +422,7 @@ public bool TryGetWardrobe(int index, [NotNullWhen(true)] out Wardrobe? wardrobe #region StatPoints public void AllocateStatPoint(BasicAttribute type) { // Invalid stat type. - if (StatAttributes.PointAllocation.StatLimit(type, statLimits) <= 0) { + if (StatAttributes.PointAllocation.StatLimit(type) <= 0) { return; } @@ -443,7 +432,7 @@ public void AllocateStatPoint(BasicAttribute type) { } // Reached limit for allocation. - if (session.Config.statAttributes.Allocation[type] >= StatAttributes.PointAllocation.StatLimit(type, statLimits)) { + if (session.Config.statAttributes.Allocation[type] >= StatAttributes.PointAllocation.StatLimit(type)) { session.Send(NoticePacket.Message("s_char_info_limit_stat_point")); return; } diff --git a/Maple2.Server.Game/Manager/CurrencyManager.cs b/Maple2.Server.Game/Manager/CurrencyManager.cs index f056a4229..09bf934c8 100644 --- a/Maple2.Server.Game/Manager/CurrencyManager.cs +++ b/Maple2.Server.Game/Manager/CurrencyManager.cs @@ -98,57 +98,57 @@ public long this[CurrencyType type] { long overflow; switch (type) { case CurrencyType.ValorToken: - delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.HonorTokenMax) - Currency.ValorToken; - overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.HonorTokenMax); - Currency.ValorToken = Math.Min(value, session.ServerTableMetadata.ConstantsTable.HonorTokenMax); + delta = Math.Min(value, Constant.HonorTokenMax) - Currency.ValorToken; + overflow = Math.Max(0, value - Constant.HonorTokenMax); + Currency.ValorToken = Math.Min(value, Constant.HonorTokenMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_honor_token, delta); } break; case CurrencyType.Treva: - delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.KarmaTokenMax) - Currency.Treva; - overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.KarmaTokenMax); - Currency.Treva = Math.Min(value, session.ServerTableMetadata.ConstantsTable.KarmaTokenMax); + delta = Math.Min(value, Constant.KarmaTokenMax) - Currency.Treva; + overflow = Math.Max(0, value - Constant.KarmaTokenMax); + Currency.Treva = Math.Min(value, Constant.KarmaTokenMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_karma_token, delta); } break; case CurrencyType.Rue: - delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.LuTokenMax) - Currency.Rue; - overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.LuTokenMax); - Currency.Rue = Math.Min(value, session.ServerTableMetadata.ConstantsTable.LuTokenMax); + delta = Math.Min(value, Constant.LuTokenMax) - Currency.Rue; + overflow = Math.Max(0, value - Constant.LuTokenMax); + Currency.Rue = Math.Min(value, Constant.LuTokenMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_lu_token, delta); } break; case CurrencyType.HaviFruit: - delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.HabiTokenMax) - Currency.HaviFruit; - overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.HabiTokenMax); - Currency.HaviFruit = Math.Min(value, session.ServerTableMetadata.ConstantsTable.HabiTokenMax); + delta = Math.Min(value, Constant.HaviTokenMax) - Currency.HaviFruit; + overflow = Math.Max(0, value - Constant.HaviTokenMax); + Currency.HaviFruit = Math.Min(value, Constant.HaviTokenMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_habi_token, delta); } break; case CurrencyType.ReverseCoin: - delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.ReverseCoinMax) - Currency.ReverseCoin; - overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.ReverseCoinMax); - Currency.ReverseCoin = Math.Min(value, session.ServerTableMetadata.ConstantsTable.ReverseCoinMax); + delta = Math.Min(value, Constant.ReverseCoinMax) - Currency.ReverseCoin; + overflow = Math.Max(0, value - Constant.ReverseCoinMax); + Currency.ReverseCoin = Math.Min(value, Constant.ReverseCoinMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_reverse_coin, delta); } break; case CurrencyType.MentorToken: - delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.MentorTokenMax) - Currency.MentorToken; - overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.MentorTokenMax); - Currency.MentorToken = Math.Min(value, session.ServerTableMetadata.ConstantsTable.MentorTokenMax); + delta = Math.Min(value, Constant.MentorTokenMax) - Currency.MentorToken; + overflow = Math.Max(0, value - Constant.MentorTokenMax); + Currency.MentorToken = Math.Min(value, Constant.MentorTokenMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_mentor_token, delta); } break; case CurrencyType.MenteeToken: - delta = Math.Min(value, session.ServerTableMetadata.ConstantsTable.MenteeTokenMax) - Currency.MenteeToken; - overflow = Math.Max(0, value - session.ServerTableMetadata.ConstantsTable.MenteeTokenMax); - Currency.MenteeToken = Math.Min(value, session.ServerTableMetadata.ConstantsTable.MenteeTokenMax); + delta = Math.Min(value, Constant.MenteeTokenMax) - Currency.MenteeToken; + overflow = Math.Max(0, value - Constant.MenteeTokenMax); + Currency.MenteeToken = Math.Min(value, Constant.MenteeTokenMax); if (delta > 0) { session.ConditionUpdate(ConditionType.get_mentee_token, delta); } diff --git a/Maple2.Server.Game/Manager/ExperienceManager.cs b/Maple2.Server.Game/Manager/ExperienceManager.cs index 52985f35a..2300b62ab 100644 --- a/Maple2.Server.Game/Manager/ExperienceManager.cs +++ b/Maple2.Server.Game/Manager/ExperienceManager.cs @@ -89,7 +89,7 @@ public void OnKill(IActor npc) { } private long GetRestExp(long expGained) { - long addedRestExp = Math.Min(RestExp, (long) (expGained * (session.ServerTableMetadata.ConstantsTable.RestExpAcquireRate / 10000.0f))); // convert int to a percentage + long addedRestExp = Math.Min(RestExp, (long) (expGained * (Constant.RestExpAcquireRate / 10000.0f))); // convert int to a percentage RestExp = Math.Max(0, RestExp - addedRestExp); Exp += expGained; return addedRestExp; @@ -175,7 +175,7 @@ public void AddMobExp(int moblevel, float modifier = 1f, long additionalExp = 0) public bool LevelUp() { int startLevel = Level; - for (int level = startLevel; level < session.ServerTableMetadata.ConstantsTable.characterMaxLevel; level++) { + for (int level = startLevel; level < Constant.characterMaxLevel; level++) { if (!session.TableMetadata.ExpTable.NextExp.TryGetValue(level, out long expToNextLevel) || expToNextLevel > Exp) { break; } @@ -203,7 +203,7 @@ public bool LevelUp() { } private void AddPrestigeExp(ExpType expType) { - if (Level < session.ServerTableMetadata.ConstantsTable.AdventureLevelStartLevel) { + if (Level < Constant.AdventureLevelStartLevel) { return; } @@ -211,20 +211,19 @@ private void AddPrestigeExp(ExpType expType) { return; } - if (PrestigeCurrentExp - PrestigeExp + (PrestigeLevelsGained * session.ServerTableMetadata.ConstantsTable.AdventureLevelLvUpExp) >= - session.ServerTableMetadata.ConstantsTable.AdventureLevelLvUpExp) { - amount = (long) (amount * session.ServerTableMetadata.ConstantsTable.AdventureLevelFactor); + if (PrestigeCurrentExp - PrestigeExp + (PrestigeLevelsGained * Constant.AdventureLevelLvUpExp) >= Constant.AdventureLevelLvUpExp) { + amount = (long) (amount * Constant.AdventureLevelFactor); } PrestigeCurrentExp = Math.Min(amount + PrestigeCurrentExp, long.MaxValue); int startLevel = PrestigeLevel; - for (int level = startLevel; level < session.ServerTableMetadata.ConstantsTable.AdventureLevelLimit; level++) { - if (session.ServerTableMetadata.ConstantsTable.AdventureLevelLvUpExp > PrestigeCurrentExp) { + for (int level = startLevel; level < Constant.AdventureLevelLimit; level++) { + if (Constant.AdventureLevelLvUpExp > PrestigeCurrentExp) { break; } - PrestigeCurrentExp -= session.ServerTableMetadata.ConstantsTable.AdventureLevelLvUpExp; + PrestigeCurrentExp -= Constant.AdventureLevelLvUpExp; PrestigeLevel++; } session.Send(PrestigePacket.AddExp(PrestigeCurrentExp, amount)); @@ -234,7 +233,7 @@ private void AddPrestigeExp(ExpType expType) { } public void PrestigeLevelUp(int amount = 1) { - PrestigeLevel = Math.Clamp(PrestigeLevel + amount, amount, session.ServerTableMetadata.ConstantsTable.AdventureLevelLimit); + PrestigeLevel = Math.Clamp(PrestigeLevel + amount, amount, Constant.AdventureLevelLimit); PrestigeLevelsGained += amount; session.ConditionUpdate(ConditionType.adventure_level, counter: amount); session.ConditionUpdate(ConditionType.adventure_level_up, counter: amount); @@ -243,7 +242,7 @@ public void PrestigeLevelUp(int amount = 1) { } for (int i = 0; i < amount; i++) { - Item? item = session.Field?.ItemDrop.CreateItem(session.ServerTableMetadata.ConstantsTable.AdventureLevelLvUpRewardItem); + Item? item = session.Field?.ItemDrop.CreateItem(Constant.AdventureLevelLvUpRewardItem); if (item == null) { break; } diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index 68291a951..986d7b3a6 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -153,9 +153,7 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId AnimationMetadata? animation = NpcMetadata.GetAnimation(npc.Model.Name); string aiPath = disableAi ? string.Empty : npc.AiPath; - var fieldNpc = new FieldNpc(this, NextLocalId(), agent, new Npc(npc, animation, ServerTableMetadata.ConstantsTable.NpcLastingSightRadius, - ServerTableMetadata.ConstantsTable.NpcLastingSightHeightUp, ServerTableMetadata.ConstantsTable.NpcLastingSightHeightDown), aiPath, - patrolDataUUID: spawnPointNpc?.PatrolData, spawnAnimation: spawnAnimation) { + var fieldNpc = new FieldNpc(this, NextLocalId(), agent, new Npc(npc, animation), aiPath, patrolDataUUID: spawnPointNpc?.PatrolData, spawnAnimation: spawnAnimation) { Owner = owner, Position = spawnPosition, Rotation = rotation, @@ -193,9 +191,7 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId int objectId = player != null ? NextGlobalId() : NextLocalId(); AnimationMetadata? animation = NpcMetadata.GetAnimation(npc.Model.Name); - var fieldPet = new FieldPet(this, objectId, agent, new Npc(npc, animation, ServerTableMetadata.ConstantsTable.NpcLastingSightRadius, - ServerTableMetadata.ConstantsTable.NpcLastingSightHeightUp,ServerTableMetadata.ConstantsTable.NpcLastingSightHeightDown), - pet, petMetadata, Constant.PetFieldAiPath, player) { + var fieldPet = new FieldPet(this, objectId, agent, new Npc(npc, animation), pet, petMetadata, Constant.PetFieldAiPath, player) { Owner = owner, Position = position, Rotation = rotation, @@ -228,17 +224,15 @@ public FieldPortal SpawnPortal(Portal portal, int roomId, Vector3 position = def } public FieldPortal SpawnPortal(QuestSummonPortal metadata, FieldNpc npc, FieldPlayer owner) { - var portal = new Portal(NextLocalId(), metadata.MapId, metadata.PortalId, PortalType.Quest, PortalActionType.Interact, - npc.Position.Offset(owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalDistanceFromNpc, npc.Rotation), npc.Rotation, - new Vector3(owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalDistanceFromNpc, owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalDimensionY, - owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalDimensionZ), owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalDistanceFromNpc, + var portal = new Portal(NextLocalId(), metadata.MapId, metadata.PortalId, PortalType.Quest, PortalActionType.Interact, npc.Position.Offset(Constant.QuestPortalDistanceFromNpc, npc.Rotation), npc.Rotation, + new Vector3(Constant.QuestPortalDistanceFromNpc, Constant.QuestPortalDimensionY, Constant.QuestPortalDimensionZ), Constant.QuestPortalDistanceFromNpc, 0, true, false, true); var fieldPortal = new FieldQuestPortal(owner, this, NextLocalId(), portal) { Position = portal.Position, Rotation = portal.Rotation, - EndTick = (FieldTick + (long) TimeSpan.FromSeconds(owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalKeepTime).TotalMilliseconds).Truncate32(), + EndTick = (FieldTick + (long) TimeSpan.FromSeconds(Constant.QuestPortalKeepTime).TotalMilliseconds).Truncate32(), StartTick = FieldTickInt, - Model = owner.Session.ServerTableMetadata.ConstantsTable.QuestPortalKeepNif, + Model = Constant.QuestPortalKeepNif, }; fieldPortals[fieldPortal.ObjectId] = fieldPortal; @@ -737,7 +731,7 @@ public void RemoveSkillByTriggerId(int triggerId) { private void AddCubeSkill(SkillMetadata metadata, in Vector3 position, in Vector3 rotation = default) { Vector3 adjustedPosition = position; adjustedPosition.Z += FieldAccelerationStructure.BLOCK_SIZE; - var fieldSkill = new FieldSkill(this, NextLocalId(), FieldActor, metadata, (int)ServerTableMetadata.ConstantsTable.GlobalCubeSkillIntervalTime.TotalMilliseconds, adjustedPosition) { + var fieldSkill = new FieldSkill(this, NextLocalId(), FieldActor, metadata, (int) Constant.GlobalCubeSkillIntervalTime.TotalMilliseconds, adjustedPosition) { Position = adjustedPosition, Rotation = rotation, Source = SkillSource.Cube, diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs index 8b5a5f160..3e09c844b 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs @@ -364,7 +364,7 @@ public void EnsurePlayerPosition(FieldPlayer player) { return; } - player.FallDamage(player.Session.ServerTableMetadata.ConstantsTable.FallBoundingAddedDistance); + player.FallDamage(Constant.FallBoundingAddedDistance); player.MoveToPosition(player.LastGroundPosition.Align() + new Vector3(0, 0, 150f), default); } diff --git a/Maple2.Server.Game/Manager/FishingManager.cs b/Maple2.Server.Game/Manager/FishingManager.cs index 8a33c3abe..60c5b94a4 100644 --- a/Maple2.Server.Game/Manager/FishingManager.cs +++ b/Maple2.Server.Game/Manager/FishingManager.cs @@ -239,12 +239,12 @@ public FishingError Start(Vector3 position) { selectedFish = fishes.Get(); - int fishingTick = session.ServerTableMetadata.ConstantsTable.fisherBoreDuration; + int fishingTick = Constant.FisherBoreDuration; bool hasAutoFish = session.Player.Buffs.HasBuff(BuffEventType.AutoFish); // Fishing Success if (Random.Shared.Next(0, 10000) < selectedFish.BaitProbability) { - if (!hasAutoFish && Random.Shared.Next(0, 10000) < session.ServerTableMetadata.ConstantsTable.fishFightingProp) { + if (!hasAutoFish && Random.Shared.Next(0, 10000) < Constant.FishFightingProp) { fishFightGame = true; } diff --git a/Maple2.Server.Game/Manager/ItemMergeManager.cs b/Maple2.Server.Game/Manager/ItemMergeManager.cs index b9c30de3e..9d2ba918f 100644 --- a/Maple2.Server.Game/Manager/ItemMergeManager.cs +++ b/Maple2.Server.Game/Manager/ItemMergeManager.cs @@ -86,7 +86,7 @@ public void SelectCrystal(long itemUid, long crystalUid) { session.Send(ItemMergePacket.Select(mergeSlot, ItemMerge.CostMultiplier(upgradeItem.Rarity))); if (!session.ScriptMetadata.TryGet(Constant.EmpowermentNpc, out ScriptMetadata? script) || - !script.States.TryGetValue(session.ServerTableMetadata.ConstantsTable.MergeSmithScriptID, out ScriptState? state)) { + !script.States.TryGetValue(Constant.MergeSmithScriptID, out ScriptState? state)) { return; } diff --git a/Maple2.Server.Game/Manager/Items/InventoryManager.cs b/Maple2.Server.Game/Manager/Items/InventoryManager.cs index f9ce9fcf6..6fecf069b 100644 --- a/Maple2.Server.Game/Manager/Items/InventoryManager.cs +++ b/Maple2.Server.Game/Manager/Items/InventoryManager.cs @@ -44,44 +44,44 @@ public InventoryManager(GameStorage.Request db, GameSession session) { } } - private short BaseSize(InventoryType type) { + private static short BaseSize(InventoryType type) { return type switch { - InventoryType.Gear => session.ServerTableMetadata.ConstantsTable.bagSlotTabGameCount[0], - InventoryType.Outfit => session.ServerTableMetadata.ConstantsTable.bagSlotTabSkinCount[0], - InventoryType.Mount => session.ServerTableMetadata.ConstantsTable.bagSlotTabSummonCount[0], - InventoryType.Catalyst => session.ServerTableMetadata.ConstantsTable.bagSlotTabMaterialCount[0], - InventoryType.FishingMusic => session.ServerTableMetadata.ConstantsTable.bagSlotTabLifeCount[0], - InventoryType.Quest => session.ServerTableMetadata.ConstantsTable.bagSlotTabQuestCount[0], - InventoryType.Gemstone => session.ServerTableMetadata.ConstantsTable.bagSlotTabGemCount[0], - InventoryType.Misc => session.ServerTableMetadata.ConstantsTable.bagSlotTabMiscCount[0], - InventoryType.LifeSkill => session.ServerTableMetadata.ConstantsTable.bagSlotTabMasteryCount[0], - InventoryType.Pets => session.ServerTableMetadata.ConstantsTable.bagSlotTabPetCount[0], - InventoryType.Consumable => session.ServerTableMetadata.ConstantsTable.bagSlotTabActiveSkillCount[0], - InventoryType.Currency => session.ServerTableMetadata.ConstantsTable.bagSlotTabCoinCount[0], - InventoryType.Badge => session.ServerTableMetadata.ConstantsTable.bagSlotTabBadgeCount[0], - InventoryType.Lapenshard => session.ServerTableMetadata.ConstantsTable.bagSlotTabLapenShardCount[0], - InventoryType.Fragment => session.ServerTableMetadata.ConstantsTable.bagSlotTabPieceCount[0], + InventoryType.Gear => Constant.BagSlotTabGameCount, + InventoryType.Outfit => Constant.BagSlotTabSkinCount, + InventoryType.Mount => Constant.BagSlotTabSummonCount, + InventoryType.Catalyst => Constant.BagSlotTabMaterialCount, + InventoryType.FishingMusic => Constant.BagSlotTabLifeCount, + InventoryType.Quest => Constant.BagSlotTabQuestCount, + InventoryType.Gemstone => Constant.BagSlotTabGemCount, + InventoryType.Misc => Constant.BagSlotTabMiscCount, + InventoryType.LifeSkill => Constant.BagSlotTabMasteryCount, + InventoryType.Pets => Constant.BagSlotTabPetCount, + InventoryType.Consumable => Constant.BagSlotTabActiveSkillCount, + InventoryType.Currency => Constant.BagSlotTabCoinCount, + InventoryType.Badge => Constant.BagSlotTabBadgeCount, + InventoryType.Lapenshard => Constant.BagSlotTabLapenshardCount, + InventoryType.Fragment => Constant.BagSlotTabPieceCount, _ => throw new ArgumentOutOfRangeException($"Invalid InventoryType: {type}"), }; } - private short MaxExpandSize(InventoryType type) { + private static short MaxExpandSize(InventoryType type) { return type switch { - InventoryType.Gear => session.ServerTableMetadata.ConstantsTable.bagSlotTabGameCount[1], - InventoryType.Outfit => session.ServerTableMetadata.ConstantsTable.bagSlotTabSkinCount[1], - InventoryType.Mount => session.ServerTableMetadata.ConstantsTable.bagSlotTabSummonCount[1], - InventoryType.Catalyst => session.ServerTableMetadata.ConstantsTable.bagSlotTabMaterialCount[1], - InventoryType.FishingMusic => session.ServerTableMetadata.ConstantsTable.bagSlotTabLifeCount[1], - InventoryType.Quest => session.ServerTableMetadata.ConstantsTable.bagSlotTabQuestCount[1], - InventoryType.Gemstone => session.ServerTableMetadata.ConstantsTable.bagSlotTabGemCount[1], - InventoryType.Misc => session.ServerTableMetadata.ConstantsTable.bagSlotTabMiscCount[1], - InventoryType.LifeSkill => session.ServerTableMetadata.ConstantsTable.bagSlotTabMasteryCount[1], - InventoryType.Pets => session.ServerTableMetadata.ConstantsTable.bagSlotTabPetCount[1], - InventoryType.Consumable => session.ServerTableMetadata.ConstantsTable.bagSlotTabActiveSkillCount[1], - InventoryType.Currency => session.ServerTableMetadata.ConstantsTable.bagSlotTabCoinCount[1], - InventoryType.Badge => session.ServerTableMetadata.ConstantsTable.bagSlotTabBadgeCount[1], - InventoryType.Lapenshard => session.ServerTableMetadata.ConstantsTable.bagSlotTabLapenShardCount[1], - InventoryType.Fragment => session.ServerTableMetadata.ConstantsTable.bagSlotTabPieceCount[1], + InventoryType.Gear => Constant.BagSlotTabGameCountMax, + InventoryType.Outfit => Constant.BagSlotTabSkinCountMax, + InventoryType.Mount => Constant.BagSlotTabSummonCountMax, + InventoryType.Catalyst => Constant.BagSlotTabMaterialCountMax, + InventoryType.FishingMusic => Constant.BagSlotTabLifeCountMax, + InventoryType.Quest => Constant.BagSlotTabQuestCountMax, + InventoryType.Gemstone => Constant.BagSlotTabGemCountMax, + InventoryType.Misc => Constant.BagSlotTabMiscCountMax, + InventoryType.LifeSkill => Constant.BagSlotTabMasteryCountMax, + InventoryType.Pets => Constant.BagSlotTabPetCountMax, + InventoryType.Consumable => Constant.BagSlotTabActiveSkillCountMax, + InventoryType.Currency => Constant.BagSlotTabCoinCountMax, + InventoryType.Badge => Constant.BagSlotTabBadgeCountMax, + InventoryType.Lapenshard => Constant.BagSlotTabLapenshardCountMax, + InventoryType.Fragment => Constant.BagSlotTabPieceCountMax, _ => throw new ArgumentOutOfRangeException($"Invalid InventoryType: {type}"), }; } @@ -557,7 +557,7 @@ public bool Expand(InventoryType type, int expandRowCount = Constant.InventoryEx return false; } - if (session.Currency.Meret < session.ServerTableMetadata.ConstantsTable.InventoryExpandPrice1Row) { + if (session.Currency.Meret < Constant.InventoryExpandPrice1Row) { session.Send(ItemInventoryPacket.Error(s_cannot_charge_merat)); return false; } @@ -566,7 +566,7 @@ public bool Expand(InventoryType type, int expandRowCount = Constant.InventoryEx return false; } - session.Currency.Meret -= session.ServerTableMetadata.ConstantsTable.InventoryExpandPrice1Row; + session.Currency.Meret -= Constant.InventoryExpandPrice1Row; if (session.Player.Value.Unlock.Expand.ContainsKey(type)) { session.Player.Value.Unlock.Expand[type] = newExpand; } else { diff --git a/Maple2.Server.Game/Manager/Items/StorageManager.cs b/Maple2.Server.Game/Manager/Items/StorageManager.cs index c38cd9a27..6ee15adf5 100644 --- a/Maple2.Server.Game/Manager/Items/StorageManager.cs +++ b/Maple2.Server.Game/Manager/Items/StorageManager.cs @@ -189,11 +189,11 @@ public void WithdrawMesos(long amount) { public void Expand() { lock (session.Item) { short newSize = (short) (items.Size + Constant.InventoryExpandRowCount); - if (newSize > session.ServerTableMetadata.ConstantsTable.StoreExpandMaxSlotCount) { + if (newSize > Constant.StoreExpandMaxSlotCount) { session.Send(StorageInventoryPacket.Error(s_store_err_expand_max)); return; } - if (session.Currency.Meret < session.ServerTableMetadata.ConstantsTable.StoreExpandPrice1Row) { + if (session.Currency.Meret < Constant.StoreExpandPrice1Row) { session.Send(StorageInventoryPacket.Error(s_cannot_charge_merat)); return; } @@ -203,7 +203,7 @@ public void Expand() { return; } - session.Currency.Meret -= session.ServerTableMetadata.ConstantsTable.StoreExpandPrice1Row; + session.Currency.Meret -= Constant.StoreExpandPrice1Row; expand += Constant.InventoryExpandRowCount; Load(); diff --git a/Maple2.Server.Game/Manager/SkillManager.cs b/Maple2.Server.Game/Manager/SkillManager.cs index 19a38c285..45f5c5754 100644 --- a/Maple2.Server.Game/Manager/SkillManager.cs +++ b/Maple2.Server.Game/Manager/SkillManager.cs @@ -121,11 +121,11 @@ public bool ExpandSkillTabs() { if (SkillBook.MaxSkillTabs >= Constant.MaxSkillTabCount) { return false; } - if (session.Currency.Meret < session.ServerTableMetadata.ConstantsTable.SkillBookTreeAddTabFeeMerat) { + if (session.Currency.Meret < Constant.SkillBookTreeAddTabFeeMeret) { return false; } - session.Currency.Meret -= session.ServerTableMetadata.ConstantsTable.SkillBookTreeAddTabFeeMerat; + session.Currency.Meret -= Constant.SkillBookTreeAddTabFeeMeret; SkillBook.MaxSkillTabs++; session.Send(SkillBookPacket.Expand(SkillBook)); diff --git a/Maple2.Server.Game/Manager/TradeManager.cs b/Maple2.Server.Game/Manager/TradeManager.cs index 67fd0f1d9..c356246b8 100644 --- a/Maple2.Server.Game/Manager/TradeManager.cs +++ b/Maple2.Server.Game/Manager/TradeManager.cs @@ -34,7 +34,7 @@ public TradeManager(GameSession sender, GameSession receiver) { // End the trade if not accepted before |TradeRequestDuration|. string receiverName = receiver.Player.Value.Character.Name; Task.Factory.StartNew(() => { - Thread.Sleep(TimeSpan.FromSeconds(sender.ServerTableMetadata.ConstantsTable.TradeRequestDuration)); + Thread.Sleep(TimeSpan.FromSeconds(Constant.TradeRequestDuration)); lock (mutex) { if (state is not (TradeState.Requested or TradeState.Acknowledged)) { return; @@ -168,7 +168,7 @@ public void SetMesos(GameSession caller, long amount) { return; } - if (amount > caller.ServerTableMetadata.ConstantsTable.TradeMaxMeso) { + if (amount > Constant.TradeMaxMeso) { caller.Send(TradePacket.Error(s_trade_error_invalid_meso)); return; } @@ -247,7 +247,7 @@ private void EndTrade(bool success) { } lock (sender.Session.Item) { - long fee = success ? (long) (sender.Session.ServerTableMetadata.ConstantsTable.TradeFeePercent / 100f * sender.Mesos) : 0; + long fee = success ? (long) (Constant.TradeFeePercent / 100f * sender.Mesos) : 0; sender.Session.Currency.Meso += sender.Mesos - fee; foreach (Item item in sender.Items) { if (item.Transfer?.Flag.HasFlag(TransferFlag.LimitTrade) == true) { @@ -260,7 +260,7 @@ private void EndTrade(bool success) { sender.Clear(); } lock (receiver.Session.Item) { - long fee = success ? (long) (receiver.Session.ServerTableMetadata.ConstantsTable.TradeFeePercent / 100f * receiver.Mesos) : 0; + long fee = success ? (long) (Constant.TradeFeePercent / 100f * receiver.Mesos) : 0; receiver.Session.Currency.Meso += receiver.Mesos - fee; foreach (Item item in receiver.Items) { if (item.Transfer?.Flag.HasFlag(TransferFlag.LimitTrade) == true) { diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs index 7ef651123..d2ba0c346 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.CleanupTask.cs @@ -33,8 +33,7 @@ protected override void TaskResumed() { return; } - float maxDistance = player.Session.ServerTableMetadata.ConstantsTable.TalkableDistance * - player.Session.ServerTableMetadata.ConstantsTable.TalkableDistance; + const float maxDistance = Constant.TalkableDistance * Constant.TalkableDistance; // find nearest npc FieldNpc? closestNpc = player.Field.Npcs.Values diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index 5591ddccc..42874181e 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -1,21 +1,20 @@ -using DotRecast.Detour.Crowd; -using Maple2.Database.Storage; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.Model.Metadata; -using Maple2.Server.Core.Packets; using Maple2.Server.Game.Manager.Field; -using Maple2.Server.Game.Model.ActorStateComponent; -using Maple2.Server.Game.Model.Enum; using Maple2.Server.Game.Model.Skill; using Maple2.Server.Game.Model.State; using Maple2.Server.Game.Packets; -using Maple2.Server.Game.Session; using Maple2.Tools; using Maple2.Tools.Collision; -using System.Diagnostics.CodeAnalysis; -using System.Numerics; +using Maple2.Server.Game.Session; using static Maple2.Server.Game.Model.ActorStateComponent.TaskState; +using Maple2.Server.Game.Model.Enum; +using Maple2.Server.Core.Packets; +using DotRecast.Detour.Crowd; +using Maple2.Server.Game.Model.ActorStateComponent; using MovementState = Maple2.Server.Game.Model.ActorStateComponent.MovementState; namespace Maple2.Server.Game.Model; diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs index accb7b90e..94c2005c5 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs @@ -64,7 +64,7 @@ public override void ApplyDamage(IActor caster, DamageRecord damage, SkillMetada } var targetRecord = new DamageRecordTarget(this); - int damageAmount = TamingPoint - Math.Min(TamingPoint + attack.Pet.TamingPoint, Field.ServerTableMetadata.ConstantsTable.TamingPetMaxPoint); + int damageAmount = TamingPoint - Math.Min(TamingPoint + attack.Pet.TamingPoint, Constant.TamingPetMaxPoint); TamingPoint -= damageAmount; targetRecord.AddDamage(damageAmount == 0 ? DamageType.Miss : DamageType.Normal, damageAmount); @@ -73,7 +73,7 @@ public override void ApplyDamage(IActor caster, DamageRecord damage, SkillMetada IsDead = true; OnDeath(); DropItem(caster); - } else if (TamingPoint >= Field.ServerTableMetadata.ConstantsTable.TamingPetMaxPoint) { // trap has chance to fail + } else if (TamingPoint >= Constant.TamingPetMaxPoint) { // trap has chance to fail IsDead = true; OnDeath(); DropItem(caster); diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs index 4179e9aaf..ce3b45acf 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPlayer.cs @@ -168,7 +168,7 @@ public override void Update(long tickCount) { return; } - if (InBattle && tickCount - battleTick > Session.ServerTableMetadata.ConstantsTable.UserBattleDurationTick) { + if (InBattle && tickCount - battleTick > Constant.UserBattleDurationTick) { InBattle = false; } @@ -402,7 +402,7 @@ public bool Revive(bool instant = false) { // Apply death penalty if field requires it if (Field.Metadata.Property.DeathPenalty) { - Session.Config.UpdateDeathPenalty(Field.FieldTick + Session.ServerTableMetadata.ConstantsTable.UserRevivalPaneltyTick); + Session.Config.UpdateDeathPenalty(Field.FieldTick + Constant.UserRevivalPaneltyTick); } // Update revival condition @@ -474,7 +474,7 @@ public void ConsumeHp(int amount) { Stat stat = Stats.Values[BasicAttribute.Health]; stat.Add(-amount); if (!IsDead) { - lastRegenTime[BasicAttribute.Health] = Field.FieldTick + Session.ServerTableMetadata.ConstantsTable.RecoveryHPWaitTick; + lastRegenTime[BasicAttribute.Health] = Field.FieldTick + Constant.RecoveryHPWaitTick; } Session.Send(StatsPacket.Update(this, BasicAttribute.Health)); @@ -547,7 +547,7 @@ public void ConsumeStamina(int amount, bool noRegen = false) { Stats.Values[BasicAttribute.Stamina].Add(-amount); if (!IsDead) { - lastRegenTime[BasicAttribute.Stamina] = Field.FieldTick + Session.ServerTableMetadata.ConstantsTable.RecoveryEPWaitTick; + lastRegenTime[BasicAttribute.Stamina] = Field.FieldTick + Constant.RecoveryEPWaitTick; } Field.Broadcast(StatsPacket.Update(this, BasicAttribute.Stamina)); } diff --git a/Maple2.Server.Game/Model/Field/Tombstone.cs b/Maple2.Server.Game/Model/Field/Tombstone.cs index 0d0b7a488..4eb54f9b7 100644 --- a/Maple2.Server.Game/Model/Field/Tombstone.cs +++ b/Maple2.Server.Game/Model/Field/Tombstone.cs @@ -26,8 +26,7 @@ public byte HitsRemaining { public Tombstone(FieldPlayer owner, int totalDeaths) { Owner = owner; - TotalHitCount = (byte) Math.Min(totalDeaths * owner.Session.ServerTableMetadata.ConstantsTable.hitPerDeadCount, - owner.Session.ServerTableMetadata.ConstantsTable.hitPerDeadCount * owner.Session.ServerTableMetadata.ConstantsTable.maxDeadCount); + TotalHitCount = (byte) Math.Min(totalDeaths * Constant.hitPerDeadCount, Constant.hitPerDeadCount * Constant.maxDeadCount); hitsRemaining = TotalHitCount; } public void WriteTo(IByteWriter writer) { diff --git a/Maple2.Server.Game/PacketHandlers/BeautyHandler.cs b/Maple2.Server.Game/PacketHandlers/BeautyHandler.cs index 99cfb24c7..dd3a7c5e2 100644 --- a/Maple2.Server.Game/PacketHandlers/BeautyHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/BeautyHandler.cs @@ -351,15 +351,15 @@ private void HandleRandomHair(GameSession session, IByteReader packet) { private void HandleWarp(GameSession session, IByteReader packet) { short type = packet.ReadShort(); int mapId = type switch { - 1 => session.ServerTableMetadata.ConstantsTable.BeautyHairShopGotoFieldID, - 3 => session.ServerTableMetadata.ConstantsTable.BeautyFaceShopGotoFieldID, - 5 => session.ServerTableMetadata.ConstantsTable.BeautyColorShopGotoFieldID, + 1 => Constant.BeautyHairShopGotoFieldID, + 3 => Constant.BeautyFaceShopGotoFieldID, + 5 => Constant.BeautyColorShopGotoFieldID, _ => 0, }; int portalId = type switch { - 1 => session.ServerTableMetadata.ConstantsTable.BeautyHairShopGotoPortalID, - 3 => session.ServerTableMetadata.ConstantsTable.BeautyFaceShopGotoPortalID, - 5 => session.ServerTableMetadata.ConstantsTable.BeautyColorShopGotoPortalID, + 1 => Constant.BeautyHairShopGotoPortalID, + 3 => Constant.BeautyFaceShopGotoPortalID, + 5 => Constant.BeautyColorShopGotoPortalID, _ => 0, }; diff --git a/Maple2.Server.Game/PacketHandlers/ClubHandler.cs b/Maple2.Server.Game/PacketHandlers/ClubHandler.cs index 293589ef0..a513f2e05 100644 --- a/Maple2.Server.Game/PacketHandlers/ClubHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ClubHandler.cs @@ -71,7 +71,7 @@ private void HandleCreate(GameSession session, IByteReader packet) { session.Send(ClubPacket.Error(ClubError.s_club_err_name_value)); return; } - if (clubName.Length < session.ServerTableMetadata.ConstantsTable.ClubNameLengthMin || clubName.Length > session.ServerTableMetadata.ConstantsTable.ClubNameLengthMax) { + if (clubName.Length is < Constant.ClubNameLengthMin or > Constant.ClubNameLengthMax) { session.Send(ClubPacket.Error(ClubError.s_club_err_name_value)); return; } @@ -243,7 +243,7 @@ private void HandleRename(GameSession session, IByteReader packet) { session.Send(ClubPacket.Error(ClubError.s_club_err_name_value)); return; } - if (newName.Length < session.ServerTableMetadata.ConstantsTable.ClubNameLengthMin || newName.Length > session.ServerTableMetadata.ConstantsTable.ClubNameLengthMax) { + if (newName.Length is < Constant.ClubNameLengthMin or > Constant.ClubNameLengthMax) { session.Send(ClubPacket.Error(ClubError.s_club_err_name_value)); return; } diff --git a/Maple2.Server.Game/PacketHandlers/FunctionCubeHandler.cs b/Maple2.Server.Game/PacketHandlers/FunctionCubeHandler.cs index 8bc5ae575..0ea79dfe6 100644 --- a/Maple2.Server.Game/PacketHandlers/FunctionCubeHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/FunctionCubeHandler.cs @@ -17,7 +17,6 @@ public class FunctionCubeHandler : FieldPacketHandler { #region Autofac Autowired // ReSharper disable MemberCanBePrivate.Global public required FunctionCubeMetadataStorage FunctionCubeMetadataStorage { private get; init; } - public required ServerTableMetadataStorage ServerTableMetadataStorage { private get; init; } // ReSharper restore All #endregion @@ -149,7 +148,7 @@ private void HandleNurturing(GameSession session, FieldFunctionInteract fieldCub // drop the item session.Field.DropItem(fieldCube.Position, fieldCube.Rotation, rewardItem, owner: session.Player, characterId: session.CharacterId); - nurturing.Feed(ServerTableMetadataStorage.ConstantsTable.NurturingEatGrowth); + nurturing.Feed(); db.UpdateNurturing(session.AccountId, fieldCube.InteractCube); session.Field.Broadcast(FunctionCubePacket.UpdateFunctionCube(fieldCube.InteractCube)); @@ -164,12 +163,12 @@ private void HandlePlayNurturing(GameSession session, Plot plot, FieldFunctionIn return; } - if (db.CountNurturingForAccount(cube.InteractCube.Metadata.Id, session.AccountId) >= ServerTableMetadataStorage.ConstantsTable.NurturingEatMaxCount) { + if (db.CountNurturingForAccount(cube.InteractCube.Metadata.Id, session.AccountId) >= Constant.NurturingPlayMaxCount) { session.Send(NoticePacket.Message("You have already played with the maximum number of pets today. TODO: Find correct string id")); // TODO: Find correct string id return; } - if (!nurturing.Play(session.AccountId, ServerTableMetadataStorage.ConstantsTable.NurturingEatGrowth, ServerTableMetadataStorage.ConstantsTable.NurturingEatMaxCount)) { + if (!nurturing.Play(session.AccountId)) { return; } @@ -217,7 +216,7 @@ private void HandlePlayNurturing(GameSession session, Plot plot, FieldFunctionIn return null; } - var mail = new Mail(ServerTableMetadataStorage.ConstantsTable.MailExpiryDays) { + var mail = new Mail { ReceiverId = ownerId, Type = MailType.System, Content = contentId, diff --git a/Maple2.Server.Game/PacketHandlers/GuildHandler.cs b/Maple2.Server.Game/PacketHandlers/GuildHandler.cs index 8b4e124fd..f54052620 100644 --- a/Maple2.Server.Game/PacketHandlers/GuildHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/GuildHandler.cs @@ -193,7 +193,7 @@ private void HandleCreate(GameSession session, IByteReader packet) { session.Send(GuildPacket.Error(GuildError.s_guild_err_name_value)); return; } - if (guildName.Length < session.ServerTableMetadata.ConstantsTable.GuildNameLengthMin || guildName.Length > session.ServerTableMetadata.ConstantsTable.GuildNameLengthMax) { + if (guildName.Length is < Constant.GuildNameLengthMin or > Constant.GuildNameLengthMax) { session.Send(GuildPacket.Error(GuildError.s_guild_err_name_value)); return; } @@ -208,11 +208,11 @@ private void HandleCreate(GameSession session, IByteReader packet) { return; } - if (session.Player.Value.Character.Level < session.ServerTableMetadata.ConstantsTable.GuildCreateMinLevel) { + if (session.Player.Value.Character.Level < Constant.GuildCreateMinLevel) { session.Send(GuildPacket.Error(GuildError.s_guild_err_not_enough_level)); return; } - if (session.Currency.CanAddMeso(-session.ServerTableMetadata.ConstantsTable.GuildCreatePrice) != -session.ServerTableMetadata.ConstantsTable.GuildCreatePrice) { + if (session.Currency.CanAddMeso(-Constant.GuildCreatePrice) != -Constant.GuildCreatePrice) { session.Send(GuildPacket.Error(GuildError.s_guild_err_no_money)); return; } @@ -236,7 +236,7 @@ private void HandleCreate(GameSession session, IByteReader packet) { } session.Guild.SetGuild(response.Guild); - session.Currency.Meso -= session.ServerTableMetadata.ConstantsTable.GuildCreatePrice; + session.Currency.Meso -= Constant.GuildCreatePrice; session.Guild.Load(); session.Send(GuildPacket.Created(guildName)); diff --git a/Maple2.Server.Game/PacketHandlers/HomeBankHandler.cs b/Maple2.Server.Game/PacketHandlers/HomeBankHandler.cs index 5026761ba..a876688cc 100644 --- a/Maple2.Server.Game/PacketHandlers/HomeBankHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/HomeBankHandler.cs @@ -20,7 +20,7 @@ public override void Handle(GameSession session, IByteReader packet) { switch (command) { case Command.Home: long time = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - if (session.Player.Value.Character.StorageCooldown + session.ServerTableMetadata.ConstantsTable.HomeBankCallCooltime > time) { + if (session.Player.Value.Character.StorageCooldown + Constant.HomeBankCallCooldown > time) { return; } diff --git a/Maple2.Server.Game/PacketHandlers/HomeDoctorHandler.cs b/Maple2.Server.Game/PacketHandlers/HomeDoctorHandler.cs index b306b9867..2b0e28e15 100644 --- a/Maple2.Server.Game/PacketHandlers/HomeDoctorHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/HomeDoctorHandler.cs @@ -15,7 +15,7 @@ public class HomeDoctorHandler : FieldPacketHandler { public override void Handle(GameSession session, IByteReader packet) { if (session.Field is null) return; long time = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - if (session.Player.Value.Character.DoctorCooldown + session.ServerTableMetadata.ConstantsTable.HomeDoctorCallCooltime > time) { + if (session.Player.Value.Character.DoctorCooldown + Constant.HomeDoctorCallCooldown > time) { return; } diff --git a/Maple2.Server.Game/PacketHandlers/ItemLockHandler.cs b/Maple2.Server.Game/PacketHandlers/ItemLockHandler.cs index ac45bf140..aac7be7b5 100644 --- a/Maple2.Server.Game/PacketHandlers/ItemLockHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ItemLockHandler.cs @@ -86,7 +86,7 @@ private static void HandleCommit(GameSession session, IByteReader packet) { if (unlock && item.IsLocked) { item.IsLocked = false; - item.UnlockTime = DateTimeOffset.UtcNow.AddSeconds(session.ServerTableMetadata.ConstantsTable.ItemUnLockTime).ToUnixTimeSeconds(); + item.UnlockTime = DateTimeOffset.UtcNow.AddSeconds(Constant.ItemUnLockTime).ToUnixTimeSeconds(); updatedItems.Add(item); } else if (!unlock && !item.IsLocked) { item.IsLocked = true; diff --git a/Maple2.Server.Game/PacketHandlers/JobHandler.cs b/Maple2.Server.Game/PacketHandlers/JobHandler.cs index 0e0b720bd..6ba30b8ca 100644 --- a/Maple2.Server.Game/PacketHandlers/JobHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/JobHandler.cs @@ -93,7 +93,7 @@ private void HandleAdvance(GameSession session, IByteReader packet) { session.Player.Buffs.Clear(); session.Player.Buffs.Initialize(); - session.Player.Buffs.LoadFieldBuffs(session.ServerTableMetadata.ConstantsTable.shadowWorldBuffHpUp, session.ServerTableMetadata.ConstantsTable.shadowWorldBuffMoveProtect); + session.Player.Buffs.LoadFieldBuffs(); session.Stats.Refresh(); session.Field.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo)); session.ConditionUpdate(ConditionType.job, codeLong: (int) session.NpcScript.JobCondition.ChangeToJobCode); diff --git a/Maple2.Server.Game/PacketHandlers/MeretMarketHandler.cs b/Maple2.Server.Game/PacketHandlers/MeretMarketHandler.cs index 2bb305f3e..763f3f9ac 100644 --- a/Maple2.Server.Game/PacketHandlers/MeretMarketHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/MeretMarketHandler.cs @@ -151,10 +151,10 @@ private void HandleListItem(GameSession session, IByteReader packet) { Look = item.Template, Blueprint = item.Blueprint ?? new ItemBlueprint(), Status = UgcMarketListingStatus.Active, - PromotionEndTime = promote ? DateTime.Now.AddHours(session.ServerTableMetadata.ConstantsTable.UGCShopAdHour).ToEpochSeconds() : 0, - ListingEndTime = DateTime.Now.AddDays(session.ServerTableMetadata.ConstantsTable.UGCShopSaleDay).ToEpochSeconds(), + PromotionEndTime = promote ? DateTime.Now.AddHours(Constant.UGCShopAdHour).ToEpochSeconds() : 0, + ListingEndTime = DateTime.Now.AddDays(Constant.UGCShopSaleDay).ToEpochSeconds(), CreationTime = DateTime.Now.ToEpochSeconds(), - Price = Math.Clamp(price, session.ServerTableMetadata.ConstantsTable.UGCShopSellMinPrice, session.ServerTableMetadata.ConstantsTable.UGCShopSellMaxPrice), + Price = Math.Clamp(price, Constant.UGCShopSellMinPrice, Constant.UGCShopSellMaxPrice), TabId = tabId, }; @@ -198,8 +198,8 @@ private void HandleRelistItem(GameSession session, IByteReader packet) { } item.Price = price; - item.PromotionEndTime = promote ? DateTime.Now.AddHours(session.ServerTableMetadata.ConstantsTable.UGCShopAdHour).ToEpochSeconds() : 0; - item.ListingEndTime = DateTime.Now.AddDays(session.ServerTableMetadata.ConstantsTable.UGCShopSaleDay).ToEpochSeconds(); + item.PromotionEndTime = promote ? DateTime.Now.AddHours(Constant.UGCShopAdHour).ToEpochSeconds() : 0; + item.ListingEndTime = DateTime.Now.AddDays(Constant.UGCShopSaleDay).ToEpochSeconds(); item.Status = UgcMarketListingStatus.Active; item.Description = description; item.Tags = tags; diff --git a/Maple2.Server.Game/PacketHandlers/MesoMarketHandler.cs b/Maple2.Server.Game/PacketHandlers/MesoMarketHandler.cs index cf3ccffd2..7fe758d4a 100644 --- a/Maple2.Server.Game/PacketHandlers/MesoMarketHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/MesoMarketHandler.cs @@ -59,7 +59,7 @@ private static void HandleCreate(GameSession session, IByteReader packet) { long amount = packet.ReadLong(); long price = packet.ReadLong(); - if (amount != session.ServerTableMetadata.ConstantsTable.MesoMarketBasePrice) { + if (amount != Constant.MesoMarketBasePrice) { session.Send(MesoMarketPacket.Error(s_mesoMarket_error_invalidSaleMoney)); return; } @@ -96,7 +96,7 @@ private static void HandleCreate(GameSession session, IByteReader packet) { } session.Player.Value.Account.MesoMarketListed++; - session.Currency.Meso -= session.ServerTableMetadata.ConstantsTable.MesoMarketBasePrice; + session.Currency.Meso -= Constant.MesoMarketBasePrice; session.Send(MesoMarketPacket.Create(listing)); session.Send(MesoMarketPacket.Quota(session.Player.Value.Account.MesoMarketListed, session.Player.Value.Account.MesoMarketPurchased)); } diff --git a/Maple2.Server.Game/PacketHandlers/NpcTalkHandler.cs b/Maple2.Server.Game/PacketHandlers/NpcTalkHandler.cs index cf21366ae..f1ffb3df4 100644 --- a/Maple2.Server.Game/PacketHandlers/NpcTalkHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/NpcTalkHandler.cs @@ -237,7 +237,7 @@ private void HandleEnchant(GameSession session, IByteReader packet) { case ScriptEventType.EnchantSelect: case ScriptEventType.PeachySelect: if (!session.ScriptMetadata.TryGet(npcId, out ScriptMetadata? script) || - !script.States.TryGetValue(session.ServerTableMetadata.ConstantsTable.EnchantMasterScriptID, out ScriptState? state)) { + !script.States.TryGetValue(Constant.EnchantMasterScriptID, out ScriptState? state)) { return; } diff --git a/Maple2.Server.Game/PacketHandlers/PartyHandler.cs b/Maple2.Server.Game/PacketHandlers/PartyHandler.cs index 2803d3a5c..0f8688b2e 100644 --- a/Maple2.Server.Game/PacketHandlers/PartyHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/PartyHandler.cs @@ -285,8 +285,7 @@ private void HandleVoteKick(GameSession session, IByteReader packet) { return; } - if (session.Party.Party.LastVoteTime.FromEpochSeconds().AddSeconds(session.ServerTableMetadata.ConstantsTable.PartyVoteReadyDurationSeconds) - > DateTime.Now && session.Party.Party.Vote != null) { + if (session.Party.Party.LastVoteTime.FromEpochSeconds().AddSeconds(Constant.PartyVoteReadyDurationSeconds) > DateTime.Now && session.Party.Party.Vote != null) { session.Send(PartyPacket.Error(PartyError.s_party_err_already_vote)); return; } @@ -316,8 +315,7 @@ private void HandleReadyCheck(GameSession session) { return; } - if (session.Party.Party.LastVoteTime.FromEpochSeconds().AddSeconds(session.ServerTableMetadata.ConstantsTable.PartyVoteReadyDurationSeconds) > - DateTime.Now && session.Party.Party.Vote != null) { + if (session.Party.Party.LastVoteTime.FromEpochSeconds().AddSeconds(Constant.PartyVoteReadyDurationSeconds) > DateTime.Now && session.Party.Party.Vote != null) { session.Send(PartyPacket.Error(PartyError.s_party_err_already_vote)); return; } diff --git a/Maple2.Server.Game/PacketHandlers/QuestHandler.cs b/Maple2.Server.Game/PacketHandlers/QuestHandler.cs index a23b4ba12..4a94bcd3f 100644 --- a/Maple2.Server.Game/PacketHandlers/QuestHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/QuestHandler.cs @@ -266,12 +266,12 @@ private static void HandleCompleteFieldMission(GameSession session, IByteReader } private static void HandleSkyFortressTeleport(GameSession session) { - if (!session.Quest.TryGetQuest(session.ServerTableMetadata.ConstantsTable.FameContentsRequireQuestID, out Quest? quest) || quest.State != QuestState.Completed) { + if (!session.Quest.TryGetQuest(Constant.FameContentsRequireQuestID, out Quest? quest) || quest.State != QuestState.Completed) { return; } - session.Send(session.PrepareField(session.ServerTableMetadata.ConstantsTable.FameContentsSkyFortressGotoMapID, - session.ServerTableMetadata.ConstantsTable.FameContentsSkyFortressGotoPortalID) + session.Send(session.PrepareField(Constant.FameContentsSkyFortressGotoMapID, + Constant.FameContentsSkyFortressGotoPortalID) ? FieldEnterPacket.Request(session.Player) : FieldEnterPacket.Error(MigrationError.s_move_err_default)); } diff --git a/Maple2.Server.Game/PacketHandlers/SystemShopHandler.cs b/Maple2.Server.Game/PacketHandlers/SystemShopHandler.cs index 4572c9405..b9a9df184 100644 --- a/Maple2.Server.Game/PacketHandlers/SystemShopHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/SystemShopHandler.cs @@ -47,11 +47,11 @@ private void HandleArena(GameSession session, IByteReader packet) { return; } - if (!session.NpcMetadata.TryGet(session.ServerTableMetadata.ConstantsTable.SystemShopNPCIDHonorToken, out NpcMetadata? npc)) { + if (!session.NpcMetadata.TryGet(Constant.SystemShopNPCIDHonorToken, out NpcMetadata? npc)) { return; } - session.Shop.Load(npc.Basic.ShopId, session.ServerTableMetadata.ConstantsTable.SystemShopNPCIDHonorToken); + session.Shop.Load(npc.Basic.ShopId, Constant.SystemShopNPCIDHonorToken); session.Send(SystemShopPacket.Arena()); } @@ -61,7 +61,7 @@ private void HandleFishing(GameSession session, IByteReader packet) { session.Shop.ClearActiveShop(); return; } - if (!session.NpcMetadata.TryGet(session.ServerTableMetadata.ConstantsTable.SystemShopNPCIDFishing, out NpcMetadata? npc)) { + if (!session.NpcMetadata.TryGet(Constant.SystemShopNPCIDFishing, out NpcMetadata? npc)) { return; } @@ -76,7 +76,7 @@ private void HandleMentee(GameSession session, IByteReader packet) { return; } - if (!session.NpcMetadata.TryGet(session.ServerTableMetadata.ConstantsTable.SystemShopNPCIDMentee, out NpcMetadata? npc)) { + if (!session.NpcMetadata.TryGet(Constant.SystemShopNPCIDMentee, out NpcMetadata? npc)) { return; } @@ -91,7 +91,7 @@ private void HandleMentor(GameSession session, IByteReader packet) { return; } - if (!session.NpcMetadata.TryGet(session.ServerTableMetadata.ConstantsTable.SystemShopNPCIDMentor, out NpcMetadata? npc)) { + if (!session.NpcMetadata.TryGet(Constant.SystemShopNPCIDMentor, out NpcMetadata? npc)) { return; } diff --git a/Maple2.Server.Game/PacketHandlers/TaxiHandler.cs b/Maple2.Server.Game/PacketHandlers/TaxiHandler.cs index 5d8e8277a..7c62abf4a 100644 --- a/Maple2.Server.Game/PacketHandlers/TaxiHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/TaxiHandler.cs @@ -136,12 +136,12 @@ private void HandleMeretAirTaxi(GameSession session, IByteReader packet) { return; } - if (session.Currency.Meret < session.ServerTableMetadata.ConstantsTable.MeratAirTaxiPrice) { + if (session.Currency.Meret < Constant.MeretAirTaxiPrice) { session.Send(NoticePacket.MessageBox(StringCode.s_err_lack_meso)); return; } - session.Currency.Meret -= session.ServerTableMetadata.ConstantsTable.MeratAirTaxiPrice; + session.Currency.Meret -= Constant.MeretAirTaxiPrice; session.Send(session.PrepareField(mapId) ? FieldEnterPacket.Request(session.Player) diff --git a/Maple2.Server.Game/PacketHandlers/UserChatHandler.cs b/Maple2.Server.Game/PacketHandlers/UserChatHandler.cs index 68a17c7d5..40ecdfe2a 100644 --- a/Maple2.Server.Game/PacketHandlers/UserChatHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/UserChatHandler.cs @@ -180,7 +180,7 @@ private void HandleWorld(GameSession session, string message, ICollection } session.Send(NoticePacket.Notice(NoticePacket.Flags.Alert | NoticePacket.Flags.Message, StringCode.s_worldchat_use_coupon)); } else { - int meretCost = session.ServerTableMetadata.ConstantsTable.MeratConsumeWorldChat; + int meretCost = Constant.MeretConsumeWorldChat; if (session.FindEvent(GameEventType.SaleChat).FirstOrDefault()?.Metadata.Data is SaleChat gameEvent) { meretCost -= (int) (meretCost * Convert.ToSingle(gameEvent.WorldChatDiscount) / 10000); } @@ -215,7 +215,7 @@ private void HandleChannel(GameSession session, string message, ICollection item.Enchant?.Enchants) { + if (Constant.enchantSuccessBroadcastingLevel > item.Enchant?.Enchants) { return; } diff --git a/Maple2.Server.Login/PacketHandlers/CharacterManagementHandler.cs b/Maple2.Server.Login/PacketHandlers/CharacterManagementHandler.cs index 2a863461d..70bc77d38 100644 --- a/Maple2.Server.Login/PacketHandlers/CharacterManagementHandler.cs +++ b/Maple2.Server.Login/PacketHandlers/CharacterManagementHandler.cs @@ -45,7 +45,6 @@ private enum Command : byte { public required BanWordStorage BanWordStorage { private get; init; } public required ItemMetadataStorage ItemMetadata { private get; init; } public required TableMetadataStorage TableMetadata { private get; init; } - public required ServerTableMetadataStorage ServerTableMetadata { private get; init; } // ReSharper restore All #endregion @@ -59,15 +58,13 @@ public override void Handle(LoginSession session, IByteReader packet) { HandleCreate(session, packet); break; case Command.Delete: - HandleDelete(session, packet, ServerTableMetadata.ConstantsTable.CharacterDestroyDivisionLevel, - ServerTableMetadata.ConstantsTable.CharacterDestroyWaitSecond); + HandleDelete(session, packet); break; case Command.CancelDelete: HandleCancelDelete(session, packet); break; case Command.ConfirmDelete: - HandleDelete(session, packet, ServerTableMetadata.ConstantsTable.CharacterDestroyDivisionLevel, - ServerTableMetadata.ConstantsTable.CharacterDestroyWaitSecond); + HandleDelete(session, packet); break; default: throw new ArgumentException($"Invalid CHARACTER_MANAGEMENT type {command}"); @@ -201,7 +198,7 @@ private void HandleCreate(LoginSession session, IByteReader packet) { session.CreateCharacter(character, outfits); } - private void HandleDelete(LoginSession session, IByteReader packet, int characterDestroyDivisionLevel, int characterDestroyWaitSecond) { + private void HandleDelete(LoginSession session, IByteReader packet) { long characterId = packet.ReadLong(); using GameStorage.Request db = GameStorage.Context(); @@ -221,8 +218,8 @@ private void HandleDelete(LoginSession session, IByteReader packet, int characte return; } - if (character.Level >= characterDestroyDivisionLevel) { - character.DeleteTime = DateTimeOffset.UtcNow.AddSeconds(characterDestroyWaitSecond).ToUnixTimeSeconds(); + if (character.Level >= Constant.CharacterDestroyDivisionLevel) { + character.DeleteTime = DateTimeOffset.UtcNow.AddSeconds(Constant.CharacterDestroyWaitSecond).ToUnixTimeSeconds(); if (db.UpdateDelete(session.AccountId, characterId, character.DeleteTime)) { session.Send(CharacterListPacket.BeginDelete(characterId, character.DeleteTime)); } else { diff --git a/Maple2.Server.World/Containers/PartyLookup.cs b/Maple2.Server.World/Containers/PartyLookup.cs index a1f774b46..29ece3e4c 100644 --- a/Maple2.Server.World/Containers/PartyLookup.cs +++ b/Maple2.Server.World/Containers/PartyLookup.cs @@ -1,6 +1,5 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; -using Maple2.Database.Storage; using Maple2.Model.Error; using Maple2.Model.Game; using Maple2.Model.Game.Party; @@ -11,8 +10,6 @@ public class PartyLookup : IDisposable { private readonly ChannelClientLookup channelClients; private readonly PlayerInfoLookup playerLookup; private readonly PartySearchLookup partySearchLookup; - private ServerTableMetadataStorage serverTableMetadataStorage; - private readonly ConcurrentDictionary parties; private int nextPartyId = 1; @@ -25,10 +22,6 @@ public PartyLookup(ChannelClientLookup channelClients, PlayerInfoLookup playerLo parties = new ConcurrentDictionary(); } - public void InjectDependencies(ServerTableMetadataStorage serverTableMetadataStorage) { - this.serverTableMetadataStorage = serverTableMetadataStorage; - } - public void Dispose() { foreach (PartyManager manager in parties.Values) { manager.Dispose(); @@ -63,7 +56,7 @@ public PartyError Create(long leaderId, out int partyId) { } var party = new Party(partyId, leaderInfo.AccountId, leaderInfo.CharacterId, leaderInfo.Name); - var manager = new PartyManager(party, serverTableMetadataStorage.ConstantsTable.PartyVoteReadyDurationSeconds) { + var manager = new PartyManager(party) { ChannelClients = channelClients, PartyLookup = this, }; diff --git a/Maple2.Server.World/Containers/PartyManager.cs b/Maple2.Server.World/Containers/PartyManager.cs index a38467520..837254313 100644 --- a/Maple2.Server.World/Containers/PartyManager.cs +++ b/Maple2.Server.World/Containers/PartyManager.cs @@ -1,10 +1,10 @@ using System.Collections.Concurrent; using Grpc.Core; -using Maple2.Database.Storage; using Maple2.Model.Enum; using Maple2.Model.Error; using Maple2.Model.Game; using Maple2.Model.Game.Party; +using Maple2.Model.Metadata; using Maple2.Server.Channel.Service; using ChannelClient = Maple2.Server.Channel.Service.Channel.ChannelClient; @@ -15,12 +15,10 @@ public class PartyManager : IDisposable { public required PartyLookup PartyLookup { get; init; } public readonly Party Party; private readonly ConcurrentDictionary pendingInvites; - private readonly int partyVoteReadyDurationSeconds; - public PartyManager(Party party, int partyVoteReadyDurationSeconds) { + public PartyManager(Party party) { Party = party; pendingInvites = new ConcurrentDictionary(); - this.partyVoteReadyDurationSeconds = partyVoteReadyDurationSeconds; } public void Dispose() { @@ -284,7 +282,7 @@ public PartyError StartReadyCheck(long requestorId) { }); Task.Factory.StartNew(() => { - Thread.Sleep(TimeSpan.FromSeconds(partyVoteReadyDurationSeconds)); + Thread.Sleep(TimeSpan.FromSeconds(Constant.PartyVoteReadyDurationSeconds)); if (Party.Vote == null) { return; } @@ -396,7 +394,7 @@ public PartyError VoteKick(long requestorId, long targetId) { Task.Factory.StartNew(() => { // TODO: The duration is wrong. - Thread.Sleep(TimeSpan.FromSeconds(partyVoteReadyDurationSeconds)); + Thread.Sleep(TimeSpan.FromSeconds(Constant.PartyVoteReadyDurationSeconds)); if (Party.Vote == null) { return; } diff --git a/Maple2.Server.World/Program.cs b/Maple2.Server.World/Program.cs index 7a7a954b0..1120e7481 100644 --- a/Maple2.Server.World/Program.cs +++ b/Maple2.Server.World/Program.cs @@ -86,10 +86,7 @@ .OnActivated(e => { var channelLookup = e.Context.Resolve(); var playerInfoLookup = e.Context.Resolve(); - var partyLookup = e.Context.Resolve(); - var serverTableMetadataStorage = e.Context.Resolve(); channelLookup.InjectDependencies(e.Instance, playerInfoLookup); - partyLookup.InjectDependencies(serverTableMetadataStorage); }) .SingleInstance(); }); diff --git a/Maple2.Server.World/WorldServer.cs b/Maple2.Server.World/WorldServer.cs index 58ac7a137..9bd131cc1 100644 --- a/Maple2.Server.World/WorldServer.cs +++ b/Maple2.Server.World/WorldServer.cs @@ -447,7 +447,7 @@ public void FieldPlotExpiryCheck() { SetPlotAsPending(db, plot); forfeit = true; // mark as open when 3 days has passed since the expiry time - } else if (plot.OwnerId == 0 && plot.ExpiryTime + serverTableMetadata.ConstantsTable.UgcHomeSaleWaitingTime.TotalSeconds < DateTimeOffset.UtcNow.ToUnixTimeSeconds()) { + } else if (plot.OwnerId == 0 && plot.ExpiryTime + Constant.UgcHomeSaleWaitingTime.TotalSeconds < DateTimeOffset.UtcNow.ToUnixTimeSeconds()) { logger.Information("Marking plot {PlotId} as open (no owner)", plot.Id); db.SetPlotOpen(plot.Id); // Mark as open } else { @@ -474,7 +474,7 @@ public void FieldPlotExpiryCheck() { } // Schedule next check for the next soonest expiry - PlotInfo? nextPlot = db.GetSoonestPlotFromExpire(serverTableMetadata.ConstantsTable.UgcHomeSaleWaitingTime); + PlotInfo? nextPlot = db.GetSoonestPlotFromExpire(); TimeSpan delay; if (nextPlot is not null) { DateTimeOffset nextExpiry = DateTimeOffset.FromUnixTimeSeconds(nextPlot.ExpiryTime); From 6bf3d5eb0830045cfb75ff78d76c71308e9eac01 Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Sat, 7 Mar 2026 00:37:11 +0800 Subject: [PATCH 22/26] Fix some systems in the guild Fix the management of guild member applications, as well as the management functions of guild permissions and positions --- .../Storage/Game/GameStorage.Guild.cs | 149 ++++++++++++- Maple2.Server.Game/Model/Field/Actor/Actor.cs | 9 + Maple2.Server.Game/Model/Field/Buff.cs | 3 + .../PacketHandlers/GuildHandler.cs | 210 +++++++++++++++++- .../Service/ChannelService.Guild.cs | 20 +- Maple2.Server.Game/Util/DamageCalculator.cs | 5 +- .../Util/Sync/PlayerInfoStorage.cs | 8 +- .../Containers/GuildManager.cs | 4 +- 8 files changed, 389 insertions(+), 19 deletions(-) diff --git a/Maple2.Database/Storage/Game/GameStorage.Guild.cs b/Maple2.Database/Storage/Game/GameStorage.Guild.cs index f4f771d38..893d3f1cf 100644 --- a/Maple2.Database/Storage/Game/GameStorage.Guild.cs +++ b/Maple2.Database/Storage/Game/GameStorage.Guild.cs @@ -2,6 +2,7 @@ using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.Tools.Extensions; +using Microsoft.EntityFrameworkCore; using Z.EntityFramework.Plus; namespace Maple2.Database.Storage; @@ -42,6 +43,140 @@ public IList GetGuildMembers(IPlayerInfoProvider provider, long gui .ToList(); } + + public IList SearchGuilds(IPlayerInfoProvider provider, string guildName = "", GuildFocus? focus = null, int limit = 50) { + IQueryable query = Context.Guild; + if (!string.IsNullOrWhiteSpace(guildName)) { + query = query.Where(guild => EF.Functions.Like(guild.Name, $"%{guildName}%")); + } + if (focus.HasValue && (int) focus.Value != 0) { + query = query.Where(guild => guild.Focus == focus.Value); + } + + List guildIds = query.OrderBy(guild => guild.Name) + .Take(limit) + .Select(guild => guild.Id) + .ToList(); + + var result = new List(); + foreach (long id in guildIds) { + Guild? guild = LoadGuild(id, string.Empty); + if (guild == null) { + continue; + } + + foreach (GuildMember member in GetGuildMembers(provider, id)) { + guild.Members.TryAdd(member.CharacterId, member); + guild.AchievementInfo += member.Info.AchievementInfo; + } + result.Add(guild); + } + + return result; + } + + public GuildApplication? CreateGuildApplication(IPlayerInfoProvider provider, long guildId, long applicantId) { + Guild? guild = LoadGuild(guildId, string.Empty); + PlayerInfo? applicant = provider.GetPlayerInfo(applicantId); + if (guild == null || applicant == null) { + return null; + } + + if (Context.GuildApplication.Any(app => app.GuildId == guildId && app.ApplicantId == applicantId)) { + Model.GuildApplication existing = Context.GuildApplication.First(app => app.GuildId == guildId && app.ApplicantId == applicantId); + return new GuildApplication { + Id = existing.Id, + Guild = guild, + Applicant = applicant, + CreationTime = existing.CreationTime.ToEpochSeconds(), + }; + } + + var app = new Model.GuildApplication { + GuildId = guildId, + ApplicantId = applicantId, + }; + Context.GuildApplication.Add(app); + if (!SaveChanges()) { + return null; + } + + return new GuildApplication { + Id = app.Id, + Guild = guild, + Applicant = applicant, + CreationTime = app.CreationTime.ToEpochSeconds(), + }; + } + + public GuildApplication? GetGuildApplication(IPlayerInfoProvider provider, long applicationId) { + Model.GuildApplication? app = Context.GuildApplication.FirstOrDefault(app => app.Id == applicationId); + if (app == null) { + return null; + } + + Guild? guild = LoadGuild(app.GuildId, string.Empty); + PlayerInfo? applicant = provider.GetPlayerInfo(app.ApplicantId); + if (guild == null || applicant == null) { + return null; + } + + return new GuildApplication { + Id = app.Id, + Guild = guild, + Applicant = applicant, + CreationTime = app.CreationTime.ToEpochSeconds(), + }; + } + + public IList GetGuildApplications(IPlayerInfoProvider provider, long guildId) { + List applications = Context.GuildApplication.Where(app => app.GuildId == guildId) + .OrderByDescending(app => app.CreationTime) + .ToList(); + + return applications + .Select(app => { + Guild? guild = LoadGuild(app.GuildId, string.Empty); + PlayerInfo? applicant = provider.GetPlayerInfo(app.ApplicantId); + if (guild == null || applicant == null) { + return null; + } + + return new GuildApplication { + Id = app.Id, + Guild = guild, + Applicant = applicant, + CreationTime = app.CreationTime.ToEpochSeconds(), + }; + }) + .WhereNotNull() + .ToList(); + } + + public IList GetGuildApplicationsByApplicant(IPlayerInfoProvider provider, long applicantId) { + List applications = Context.GuildApplication.Where(app => app.ApplicantId == applicantId) + .OrderByDescending(app => app.CreationTime) + .ToList(); + + return applications + .Select(app => { + Guild? guild = LoadGuild(app.GuildId, string.Empty); + PlayerInfo? applicant = provider.GetPlayerInfo(app.ApplicantId); + if (guild == null || applicant == null) { + return null; + } + + return new GuildApplication { + Id = app.Id, + Guild = guild, + Applicant = applicant, + CreationTime = app.CreationTime.ToEpochSeconds(), + }; + }) + .WhereNotNull() + .ToList(); + } + public Guild? CreateGuild(string name, long leaderId) { BeginTransaction(); @@ -155,20 +290,18 @@ public bool DeleteGuildApplications(long characterId) { public bool SaveGuildMembers(long guildId, ICollection members) { Dictionary saveMembers = members .ToDictionary(member => member.CharacterId, member => member); - IEnumerable existingMembers = Context.GuildMember + HashSet existingMembers = Context.GuildMember .Where(member => member.GuildId == guildId) - .Select(member => new Model.GuildMember { - CharacterId = member.CharacterId, - }); + .Select(member => member.CharacterId) + .ToHashSet(); - foreach (Model.GuildMember member in existingMembers) { - if (saveMembers.Remove(member.CharacterId, out GuildMember? gameMember)) { + foreach ((long characterId, GuildMember gameMember) in saveMembers) { + if (existingMembers.Contains(characterId)) { Context.GuildMember.Update(gameMember); } else { - Context.GuildMember.Remove(member); + Context.GuildMember.Add(gameMember); } } - Context.GuildMember.AddRange(saveMembers.Values.Select(member => member)); return SaveChanges(); } diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index 5ebe13220..9caa850f3 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -152,6 +152,15 @@ public virtual void ApplyDamage(IActor caster, DamageRecord damage, SkillMetadat Stats.Values[BasicAttribute.Health].Add(damageAmount); Field.Broadcast(StatsPacket.Update(this, BasicAttribute.Health)); OnDamageReceived(caster, positiveDamage); + + if (caster is FieldPlayer casterPlayer && casterPlayer.Session.Dungeon.UserRecord != null) { + casterPlayer.Session.Dungeon.UserRecord.AccumulationRecords[DungeonAccumulationRecordType.TotalDamage] += (int) positiveDamage; + casterPlayer.Session.Dungeon.UserRecord.AccumulationRecords[DungeonAccumulationRecordType.TotalHitCount] += targetRecord.Damage.Count(x => x.Amount > 0 && x.Type is DamageType.Normal or DamageType.Critical); + casterPlayer.Session.Dungeon.UserRecord.AccumulationRecords[DungeonAccumulationRecordType.TotalCriticalDamage] += (int) targetRecord.Damage.Where(x => x.Type == DamageType.Critical).Sum(x => x.Amount); + } + if (this is FieldPlayer targetPlayer && targetPlayer.Session.Dungeon.UserRecord != null) { + targetPlayer.Session.Dungeon.UserRecord.AccumulationRecords[DungeonAccumulationRecordType.IncomingDamage] += (int) positiveDamage; + } } foreach ((DamageType damageType, long amount) in targetRecord.Damage) { diff --git a/Maple2.Server.Game/Model/Field/Buff.cs b/Maple2.Server.Game/Model/Field/Buff.cs index 0cba6abb3..b2c5e4612 100644 --- a/Maple2.Server.Game/Model/Field/Buff.cs +++ b/Maple2.Server.Game/Model/Field/Buff.cs @@ -211,6 +211,9 @@ private void ApplyRecovery() { if (updated.Count > 0) { Field.Broadcast(StatsPacket.Update(Owner, updated.ToArray())); } + if (Caster is FieldPlayer casterPlayer && casterPlayer.Session.Dungeon.UserRecord != null && record.HpAmount > 0) { + casterPlayer.Session.Dungeon.UserRecord.AccumulationRecords[DungeonAccumulationRecordType.TotalHealing] += record.HpAmount; + } Field.Broadcast(SkillDamagePacket.Heal(record)); } diff --git a/Maple2.Server.Game/PacketHandlers/GuildHandler.cs b/Maple2.Server.Game/PacketHandlers/GuildHandler.cs index f54052620..6483e176f 100644 --- a/Maple2.Server.Game/PacketHandlers/GuildHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/GuildHandler.cs @@ -39,8 +39,8 @@ private enum Command : byte { SendApplication = 80, CancelApplication = 81, RespondApplication = 82, - Unknown83 = 83, - ListApplications = 84, + ListApplications = 83, + ListAppliedGuilds = 84, SearchGuilds = 85, SearchGuildName = 86, UseBuff = 88, @@ -129,6 +129,9 @@ public override void Handle(GameSession session, IByteReader packet) { case Command.ListApplications: HandleListApplications(session); return; + case Command.ListAppliedGuilds: + HandleListAppliedGuilds(session); + return; case Command.SearchGuilds: HandleSearchGuilds(session, packet); return; @@ -579,8 +582,48 @@ private void HandleIncreaseCapacity(GameSession session) { } private void HandleUpdateRank(GameSession session, IByteReader packet) { - packet.ReadByte(); + if (session.Guild.Guild == null) { + return; + } + + byte rankId = packet.ReadByte(); var rank = packet.ReadClass(); + rank.Id = rankId; + + if (!session.Guild.HasPermission(session.CharacterId, GuildPermission.EditRank)) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_no_authority)); + return; + } + if (rankId >= session.Guild.Guild.Ranks.Count) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_invalid_grade_index)); + return; + } + if (string.IsNullOrWhiteSpace(rank.Name) || rank.Name.Length > Constant.GuildNameLengthMax) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_invalid_grade_data)); + return; + } + + session.Guild.Guild.Ranks[rankId].Name = rank.Name; + session.Guild.Guild.Ranks[rankId].Permission = rank.Permission; + + using GameStorage.Request db = session.GameStorage.Context(); + if (!db.SaveGuild(session.Guild.Guild)) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_unknown)); + return; + } + + foreach (GuildMember member in session.Guild.Guild.Members.Values) { + if (!session.FindSession(member.CharacterId, out GameSession? other) || other.Guild.Guild == null) { + continue; + } + if (rankId < other.Guild.Guild.Ranks.Count) { + other.Guild.Guild.Ranks[rankId].Name = rank.Name; + other.Guild.Guild.Ranks[rankId].Permission = rank.Permission; + } + other.Send(GuildPacket.NotifyUpdateRank(new InterfaceText("s_guild_change_grade_sucess", false), rank)); + } + + session.Send(GuildPacket.UpdateRank(rank)); } private void HandleUpdateFocus(GameSession session, IByteReader packet) { @@ -595,28 +638,187 @@ private void HandleSendMail(GameSession session, IByteReader packet) { private void HandleSendApplication(GameSession session, IByteReader packet) { long guildId = packet.ReadLong(); + if (session.Guild.Guild != null) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_has_guild)); + return; + } + + using GameStorage.Request db = session.GameStorage.Context(); + Guild? guild = db.GetGuild(guildId); + if (guild == null) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_null_guild)); + return; + } + + GuildApplication? application = db.CreateGuildApplication(session.PlayerInfo, guildId, session.CharacterId); + if (application == null) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_unknown)); + return; + } + + session.Send(GuildPacket.SendApplication(application.Id, guild.Name)); + + foreach (GuildMember member in guild.Members.Values) { + if (!session.FindSession(member.CharacterId, out GameSession? guildSession)) { + continue; + } + + GuildRank? rank = guild.Ranks.FirstOrDefault(x => x.Id == member.Rank); + if (rank == null) { + continue; + } + + if (!rank.Permission.HasFlag(GuildPermission.InviteMembers)) { + continue; + } + + guildSession.Send(GuildPacket.ReceiveApplication(application)); + } } private void HandleCancelApplication(GameSession session, IByteReader packet) { long applicationId = packet.ReadLong(); + using GameStorage.Request db = session.GameStorage.Context(); + GuildApplication? application = db.GetGuildApplication(session.PlayerInfo, applicationId); + if (application == null || application.Applicant.CharacterId != session.CharacterId) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_unknown)); + return; + } + if (!db.DeleteGuildApplication(applicationId)) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_unknown)); + return; + } + + session.Send(GuildPacket.CancelApplication(applicationId, application.Guild.Name)); } private void HandleRespondApplication(GameSession session, IByteReader packet) { + static GuildMember CloneGuildMember(GuildMember member) { + return new GuildMember { + GuildId = member.GuildId, + Info = member.Info.Clone(), + Message = member.Message, + Rank = member.Rank, + WeeklyContribution = member.WeeklyContribution, + TotalContribution = member.TotalContribution, + DailyDonationCount = member.DailyDonationCount, + JoinTime = member.JoinTime, + CheckinTime = member.CheckinTime, + DonationTime = member.DonationTime, + }; + } + long applicationId = packet.ReadLong(); bool accepted = packet.ReadBool(); + if (session.Guild.Guild == null) { + return; + } + if (!session.Guild.HasPermission(session.CharacterId, GuildPermission.InviteMembers)) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_no_authority)); + return; + } + + using GameStorage.Request db = session.GameStorage.Context(); + GuildApplication? application = db.GetGuildApplication(session.PlayerInfo, applicationId); + if (application == null || application.Guild.Id != session.Guild.Id) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_unknown)); + return; + } + + if (accepted) { + GuildMember? newMember = db.CreateGuildMember(session.Guild.Id, application.Applicant); + if (newMember == null) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_fail_addmember)); + return; + } + + if (!db.DeleteGuildApplication(applicationId)) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_unknown)); + return; + } + + GuildMember[] currentMembers = session.Guild.Guild.Members.Values.ToArray(); + session.Guild.Guild.Members.TryAdd(newMember.CharacterId, newMember); + + // Refresh the guild master's member list immediately even if self session lookup fails. + session.Send(GuildPacket.Joined(session.PlayerName, CloneGuildMember(newMember))); + + foreach (GuildMember member in currentMembers) { + if (member.CharacterId == session.CharacterId) { + continue; + } + if (!session.FindSession(member.CharacterId, out GameSession? other)) { + continue; + } + + other.Guild.AddMember(session.PlayerName, CloneGuildMember(newMember)); + } + + if (session.FindSession(newMember.CharacterId, out GameSession? applicantSession)) { + if (applicantSession.Guild.Guild == null) { + GuildInfoResponse guildInfo = applicantSession.World.GuildInfo(new GuildInfoRequest { + GuildId = session.Guild.Id, + }); + if (guildInfo.Guild != null) { + applicantSession.Guild.SetGuild(guildInfo.Guild); + } + } + + if (applicantSession.Guild.Guild != null && applicantSession.Guild.GetMember(newMember.CharacterId) == null) { + applicantSession.Guild.AddMember(session.PlayerName, CloneGuildMember(newMember)); + } + applicantSession.Guild.Load(); + applicantSession.Send(GuildPacket.ListAppliedGuilds(db.GetGuildApplicationsByApplicant(applicantSession.PlayerInfo, applicantSession.CharacterId))); + } + + session.Send(GuildPacket.ListApplications(db.GetGuildApplications(session.PlayerInfo, session.Guild.Id))); + } else { + if (!db.DeleteGuildApplication(applicationId)) { + session.Send(GuildPacket.Error(GuildError.s_guild_err_unknown)); + return; + } + + session.Send(GuildPacket.ListApplications(db.GetGuildApplications(session.PlayerInfo, session.Guild.Id))); + if (session.FindSession(application.Applicant.CharacterId, out GameSession? applicantSession)) { + applicantSession.Send(GuildPacket.ListAppliedGuilds(db.GetGuildApplicationsByApplicant(applicantSession.PlayerInfo, applicantSession.CharacterId))); + } + } + + session.Send(GuildPacket.RespondApplication(applicationId, application.Guild.Name, accepted)); } private void HandleListApplications(GameSession session) { + using GameStorage.Request db = session.GameStorage.Context(); + + if (session.Guild.Guild == null) { + session.Send(GuildPacket.ListApplications(Array.Empty())); + return; + } + session.Send(GuildPacket.ListApplications( + db.GetGuildApplications(session.PlayerInfo, session.Guild.Id) + )); } + private void HandleListAppliedGuilds(GameSession session) { + using GameStorage.Request db = session.GameStorage.Context(); + session.Send(GuildPacket.ListAppliedGuilds( + db.GetGuildApplicationsByApplicant(session.PlayerInfo, session.CharacterId) + )); + } private void HandleSearchGuilds(GameSession session, IByteReader packet) { var focus = packet.Read(); - packet.ReadInt(); // 1 + packet.ReadInt(); + + using GameStorage.Request db = session.GameStorage.Context(); + session.Send(GuildPacket.ListGuilds(db.SearchGuilds(session.PlayerInfo, focus: focus))); } private void HandleSearchGuildName(GameSession session, IByteReader packet) { string guildName = packet.ReadUnicodeString(); + + using GameStorage.Request db = session.GameStorage.Context(); + session.Send(GuildPacket.ListGuilds(db.SearchGuilds(session.PlayerInfo, guildName: guildName))); } private void HandleUseBuff(GameSession session, IByteReader packet) { diff --git a/Maple2.Server.Game/Service/ChannelService.Guild.cs b/Maple2.Server.Game/Service/ChannelService.Guild.cs index 5ddf9f7e2..1cc2fb9c4 100644 --- a/Maple2.Server.Game/Service/ChannelService.Guild.cs +++ b/Maple2.Server.Game/Service/ChannelService.Guild.cs @@ -4,6 +4,8 @@ using Maple2.Server.Channel.Service; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; +using WorldGuildInfoRequest = Maple2.Server.World.Service.GuildInfoRequest; +using WorldGuildInfoResponse = Maple2.Server.World.Service.GuildInfoResponse; namespace Maple2.Server.Game.Service; @@ -82,13 +84,25 @@ private GuildResponse AddGuildMember(long guildId, IEnumerable receiverIds continue; } - // Intentionally create a separate GuildMember instance for each session. - session.Guild.AddMember(add.RequestorName, new GuildMember { + var member = new GuildMember { GuildId = guildId, Info = info.Clone(), Rank = (byte) add.Rank, JoinTime = add.JoinTime, - }); + }; + + if (session.CharacterId == add.CharacterId && session.Guild.Guild == null) { + WorldGuildInfoResponse response = session.World.GuildInfo(new WorldGuildInfoRequest { + GuildId = guildId, + }); + + if (response.Guild != null) { + session.Guild.SetGuild(response.Guild); + session.Guild.Load(); + } + } + + session.Guild.AddMember(add.RequestorName, member); } return new GuildResponse(); diff --git a/Maple2.Server.Game/Util/DamageCalculator.cs b/Maple2.Server.Game/Util/DamageCalculator.cs index 09fafc089..8060b9f93 100644 --- a/Maple2.Server.Game/Util/DamageCalculator.cs +++ b/Maple2.Server.Game/Util/DamageCalculator.cs @@ -113,6 +113,9 @@ public static (DamageType, long) CalculateDamage(IActor caster, IActor target, D BasicAttribute attackTypeAttribute = caster.Stats.Values[BasicAttribute.PhysicalAtk].Total >= caster.Stats.Values[BasicAttribute.MagicalAtk].Total ? BasicAttribute.PhysicalAtk : BasicAttribute.MagicalAtk; + BasicAttribute resistanceAttribute = attackTypeAttribute == BasicAttribute.PhysicalAtk + ? BasicAttribute.PhysicalRes + : BasicAttribute.MagicalRes; SpecialAttribute piercingAttribute = attackTypeAttribute == BasicAttribute.PhysicalAtk ? SpecialAttribute.PhysicalPiercing : SpecialAttribute.MagicalPiercing; @@ -120,7 +123,7 @@ public static (DamageType, long) CalculateDamage(IActor caster, IActor target, D ? SpecialAttribute.OffensivePhysicalDamage : SpecialAttribute.OffensiveMagicalDamage; attackTypeAmount = Math.Max(caster.Stats.Values[BasicAttribute.PhysicalAtk].Total, caster.Stats.Values[BasicAttribute.MagicalAtk].Total) * 0.5f; - resistance = Damage.CalculateResistance(target.Stats.Values[attackTypeAttribute].Total, caster.Stats.Values[piercingAttribute].Multiplier()); + resistance = Damage.CalculateResistance(target.Stats.Values[resistanceAttribute].Total, caster.Stats.Values[piercingAttribute].Multiplier()); finalDamage = caster.Stats.Values[finalDamageAttribute].Multiplier(); break; } diff --git a/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs b/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs index 6e1271e6a..bb3a4e9c6 100644 --- a/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs +++ b/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using Grpc.Core; +using Maple2.Database.Storage; using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.Server.Core.Sync; @@ -9,7 +10,7 @@ namespace Maple2.Server.Game.Util.Sync; -public class PlayerInfoStorage { +public class PlayerInfoStorage : IPlayerInfoProvider { private readonly WorldClient world; // TODO: Just using dictionary for now, might need eviction at some point (LRUCache) private readonly ConcurrentDictionary cache; @@ -26,6 +27,11 @@ public PlayerInfoStorage(WorldClient world) { listeners = new ConcurrentDictionary>(); } + + public PlayerInfo? GetPlayerInfo(long id) { + return GetOrFetch(id, out PlayerInfo? info) ? info : null; + } + public bool GetOrFetch(long characterId, [NotNullWhen(true)] out PlayerInfo? info) { if (cache.TryGetValue(characterId, out info)) { return true; diff --git a/Maple2.Server.World/Containers/GuildManager.cs b/Maple2.Server.World/Containers/GuildManager.cs index ef4030b2d..d41b42b99 100644 --- a/Maple2.Server.World/Containers/GuildManager.cs +++ b/Maple2.Server.World/Containers/GuildManager.cs @@ -100,7 +100,8 @@ public GuildError Join(string requestorName, PlayerInfo info) { return GuildError.s_guild_err_fail_addmember; } - // Broadcast before adding this new member. + // Add the new member before broadcasting so the applicant also receives the guild member sync. + Guild.Members.TryAdd(member.CharacterId, member); Broadcast(new GuildRequest { AddMember = new GuildRequest.Types.AddMember { CharacterId = member.CharacterId, @@ -109,7 +110,6 @@ public GuildError Join(string requestorName, PlayerInfo info) { JoinTime = member.JoinTime, }, }); - Guild.Members.TryAdd(member.CharacterId, member); return GuildError.none; } From 6ff378530a5ed54dea69eaa244fda91d2316a250 Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Sat, 7 Mar 2026 07:07:12 +0800 Subject: [PATCH 23/26] Fix damage statistics and calculation Fix damage statistics and calculation --- .../Game/Dungeon/DungeonUserRecord.cs | 19 +++++++ Maple2.Server.Game/Manager/DungeonManager.cs | 4 +- .../Field/FieldManager/DungeonFieldManager.cs | 18 ++----- Maple2.Server.Game/Model/Field/Actor/Actor.cs | 54 ++++++++++++++++--- Maple2.Server.Game/Model/Field/Buff.cs | 18 +++++-- 5 files changed, 89 insertions(+), 24 deletions(-) diff --git a/Maple2.Model/Game/Dungeon/DungeonUserRecord.cs b/Maple2.Model/Game/Dungeon/DungeonUserRecord.cs index cc8b49eb9..87814dfbd 100644 --- a/Maple2.Model/Game/Dungeon/DungeonUserRecord.cs +++ b/Maple2.Model/Game/Dungeon/DungeonUserRecord.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using Maple2.Model.Enum; using Maple2.PacketLib.Tools; +using Maple2.Tools.Extensions; namespace Maple2.Model.Game.Dungeon; @@ -86,5 +87,23 @@ public void WriteTo(IByteWriter writer) { writer.WriteBool(item.Unknown2); writer.WriteBool(item.Unknown3); } + + // Party meter / performance details. + // Keep this block after the reward data to preserve the packet layout that already works + // for the personal reward/result UI, while still exposing dungeon accumulation records + // to clients that read the extended statistics section. + writer.WriteInt(AccumulationRecords.Count); + foreach ((DungeonAccumulationRecordType type, int value) in AccumulationRecords.OrderBy(entry => (int) entry.Key)) { + writer.Write(type); + writer.WriteInt(value); + } + + writer.WriteInt(Missions.Count); + foreach (DungeonMission mission in Missions.Values.OrderBy(mission => mission.Id)) { + writer.WriteClass(mission); + } + + writer.Write(Flag); + writer.WriteInt(Round); } } diff --git a/Maple2.Server.Game/Manager/DungeonManager.cs b/Maple2.Server.Game/Manager/DungeonManager.cs index 83a1697ab..5b47c2204 100644 --- a/Maple2.Server.Game/Manager/DungeonManager.cs +++ b/Maple2.Server.Game/Manager/DungeonManager.cs @@ -249,7 +249,9 @@ public void SetDungeon(DungeonFieldManager field) { Lobby = field; LobbyRoomId = field.RoomId; Metadata = field.DungeonMetadata; - UserRecord = new DungeonUserRecord(field.DungeonId, session.CharacterId); + UserRecord = new DungeonUserRecord(field.DungeonId, session.CharacterId) { + WithParty = field.Size > 1 || field.PartyId != 0, + }; foreach (int missionId in Metadata.UserMissions) { //TODO diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/DungeonFieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/DungeonFieldManager.cs index 5b3d92441..cfc967784 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/DungeonFieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/DungeonFieldManager.cs @@ -33,25 +33,15 @@ public void ChangeState(DungeonState state) { var compiledResults = new Dictionary(); - // Get player's best record + // Party meter should compare the same metric across all party members. + // Use TotalDamage for everyone instead of assigning each character a different “best” category. foreach ((long characterId, DungeonUserRecord userRecord) in DungeonRoomRecord.UserResults) { - if (compiledResults.ContainsKey(characterId)) { - continue; - } - if (!TryGetPlayerById(characterId, out FieldPlayer? _)) { continue; } - List> recordkvp = userRecord.AccumulationRecords.OrderByDescending(record => record.Value).ToList(); - foreach (KeyValuePair kvp in recordkvp) { - if (compiledResults.ContainsKey(characterId) || compiledResults.Values.Any(x => x.RecordType == kvp.Key)) { - continue; - } - - compiledResults.Add(characterId, new DungeonUserResult(characterId, kvp.Key, kvp.Value + 1)); - break; - } + userRecord.AccumulationRecords.TryGetValue(DungeonAccumulationRecordType.TotalDamage, out int totalDamage); + compiledResults[characterId] = new DungeonUserResult(characterId, DungeonAccumulationRecordType.TotalDamage, totalDamage); } Broadcast(DungeonRoomPacket.DungeonResult(DungeonRoomRecord.State, compiledResults)); diff --git a/Maple2.Server.Game/Model/Field/Actor/Actor.cs b/Maple2.Server.Game/Model/Field/Actor/Actor.cs index 9caa850f3..017bd24a5 100644 --- a/Maple2.Server.Game/Model/Field/Actor/Actor.cs +++ b/Maple2.Server.Game/Model/Field/Actor/Actor.cs @@ -3,6 +3,7 @@ using System.Numerics; using Maple2.Model.Enum; using Maple2.Model.Metadata; +using Maple2.Model.Game.Dungeon; using Maple2.Server.Game.Manager.Field; using Maple2.Server.Game.Model.Skill; using Maple2.Tools.VectorMath; @@ -153,13 +154,36 @@ public virtual void ApplyDamage(IActor caster, DamageRecord damage, SkillMetadat Field.Broadcast(StatsPacket.Update(this, BasicAttribute.Health)); OnDamageReceived(caster, positiveDamage); - if (caster is FieldPlayer casterPlayer && casterPlayer.Session.Dungeon.UserRecord != null) { - casterPlayer.Session.Dungeon.UserRecord.AccumulationRecords[DungeonAccumulationRecordType.TotalDamage] += (int) positiveDamage; - casterPlayer.Session.Dungeon.UserRecord.AccumulationRecords[DungeonAccumulationRecordType.TotalHitCount] += targetRecord.Damage.Count(x => x.Amount > 0 && x.Type is DamageType.Normal or DamageType.Critical); - casterPlayer.Session.Dungeon.UserRecord.AccumulationRecords[DungeonAccumulationRecordType.TotalCriticalDamage] += (int) targetRecord.Damage.Where(x => x.Type == DamageType.Critical).Sum(x => x.Amount); + if (caster is FieldPlayer casterPlayer) { + long totalDamage = 0; + long criticalDamage = 0; + long totalHitCount = 0; + + foreach ((DamageType damageType, long amount) in targetRecord.Damage) { + switch (damageType) { + case DamageType.Normal: + totalDamage += amount; + totalHitCount++; + break; + case DamageType.Critical: + totalDamage += amount; + criticalDamage += amount; + totalHitCount++; + break; + } + } + + AddDungeonAccumulation(casterPlayer.Session.Dungeon.UserRecord, DungeonAccumulationRecordType.TotalDamage, totalDamage); + AddDungeonAccumulation(casterPlayer.Session.Dungeon.UserRecord, DungeonAccumulationRecordType.TotalHitCount, totalHitCount); + AddDungeonAccumulation(casterPlayer.Session.Dungeon.UserRecord, DungeonAccumulationRecordType.TotalCriticalDamage, criticalDamage); + SetDungeonAccumulationMax(casterPlayer.Session.Dungeon.UserRecord, DungeonAccumulationRecordType.MaximumCriticalDamage, criticalDamage); + if (damage.SkillId == 0) { + AddDungeonAccumulation(casterPlayer.Session.Dungeon.UserRecord, DungeonAccumulationRecordType.BasicAttackDamage, totalDamage); + } } - if (this is FieldPlayer targetPlayer && targetPlayer.Session.Dungeon.UserRecord != null) { - targetPlayer.Session.Dungeon.UserRecord.AccumulationRecords[DungeonAccumulationRecordType.IncomingDamage] += (int) positiveDamage; + + if (this is FieldPlayer targetPlayer) { + AddDungeonAccumulation(targetPlayer.Session.Dungeon.UserRecord, DungeonAccumulationRecordType.IncomingDamage, positiveDamage); } } @@ -184,6 +208,24 @@ public virtual void ApplyDamage(IActor caster, DamageRecord damage, SkillMetadat } } + + private static void AddDungeonAccumulation(DungeonUserRecord? userRecord, DungeonAccumulationRecordType type, long amount) { + if (userRecord == null || amount <= 0) { + return; + } + + userRecord.AccumulationRecords.AddOrUpdate(type, (int) Math.Min(amount, int.MaxValue), + (_, current) => (int) Math.Min((long) current + amount, int.MaxValue)); + } + + private static void SetDungeonAccumulationMax(DungeonUserRecord? userRecord, DungeonAccumulationRecordType type, long amount) { + if (userRecord == null || amount <= 0) { + return; + } + + userRecord.AccumulationRecords.AddOrUpdate(type, (int) Math.Min(amount, int.MaxValue), + (_, current) => Math.Max(current, (int) Math.Min(amount, int.MaxValue))); + } protected virtual void OnDamageReceived(IActor caster, long amount) { } public virtual void Reflect(IActor target) { diff --git a/Maple2.Server.Game/Model/Field/Buff.cs b/Maple2.Server.Game/Model/Field/Buff.cs index b2c5e4612..3de375264 100644 --- a/Maple2.Server.Game/Model/Field/Buff.cs +++ b/Maple2.Server.Game/Model/Field/Buff.cs @@ -208,12 +208,17 @@ private void ApplyRecovery() { updated.Add(BasicAttribute.Stamina); } + if (record.HpAmount > 0 && Caster is FieldPlayer casterPlayer) { + casterPlayer.Session.Dungeon.UserRecord?.AccumulationRecords.AddOrUpdate( + DungeonAccumulationRecordType.TotalHealing, + record.HpAmount, + (_, current) => (int) Math.Min((long) current + record.HpAmount, int.MaxValue) + ); + } + if (updated.Count > 0) { Field.Broadcast(StatsPacket.Update(Owner, updated.ToArray())); } - if (Caster is FieldPlayer casterPlayer && casterPlayer.Session.Dungeon.UserRecord != null && record.HpAmount > 0) { - casterPlayer.Session.Dungeon.UserRecord.AccumulationRecords[DungeonAccumulationRecordType.TotalHealing] += record.HpAmount; - } Field.Broadcast(SkillDamagePacket.Heal(record)); } @@ -247,6 +252,13 @@ private void ApplyDotDamage() { Field.Broadcast(SkillDamagePacket.DotDamage(record)); if (record.RecoverHp != 0) { Caster.Stats.Values[BasicAttribute.Health].Add(record.RecoverHp); + if (record.RecoverHp > 0 && Caster is FieldPlayer casterPlayer) { + casterPlayer.Session.Dungeon.UserRecord?.AccumulationRecords.AddOrUpdate( + DungeonAccumulationRecordType.TotalHealing, + record.RecoverHp, + (_, current) => (int) Math.Min((long) current + record.RecoverHp, int.MaxValue) + ); + } Field.Broadcast(StatsPacket.Update(Caster, BasicAttribute.Health)); } } From b1d810ceba34dbc746cbf4cb7e181d1ee69305cf Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Sun, 8 Mar 2026 02:43:40 +0800 Subject: [PATCH 24/26] Fixes for merging the main branch Fixes for merging the main branch --- Maple2.Server.Core/Formulas/BonusAttack.cs | 5 ++--- Maple2.Server.Game/Manager/StatsManager.cs | 9 +++++++-- Maple2.Server.Web/Controllers/WebController.cs | 10 +++++----- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Maple2.Server.Core/Formulas/BonusAttack.cs b/Maple2.Server.Core/Formulas/BonusAttack.cs index d8e433776..0f287425e 100644 --- a/Maple2.Server.Core/Formulas/BonusAttack.cs +++ b/Maple2.Server.Core/Formulas/BonusAttack.cs @@ -9,11 +9,10 @@ public static double Coefficient(int rightHandRarity, int leftHandRarity, JobCod } double weaponBonusAttackCoefficient = RarityMultiplier(rightHandRarity); - if (leftHandRarity == 0) { - return weaponBonusAttackCoefficient; + if (leftHandRarity > 0) { + weaponBonusAttackCoefficient = 0.5 * (weaponBonusAttackCoefficient + RarityMultiplier(leftHandRarity)); } - weaponBonusAttackCoefficient = 0.5 * (weaponBonusAttackCoefficient + RarityMultiplier(leftHandRarity)); return 4.96 * weaponBonusAttackCoefficient * JobBonusMultiplier(jobCode); } diff --git a/Maple2.Server.Game/Manager/StatsManager.cs b/Maple2.Server.Game/Manager/StatsManager.cs index 61c3122d8..34360c1bf 100644 --- a/Maple2.Server.Game/Manager/StatsManager.cs +++ b/Maple2.Server.Game/Manager/StatsManager.cs @@ -73,8 +73,8 @@ public StatsManager(IActor actor) { return (1, 1); double BonusAttackCoefficient(FieldPlayer player) { - int leftHandRarity = player.Session.Item.Equips.Get(EquipSlot.RH)?.Rarity ?? 0; - int rightHandRarity = player.Session.Item.Equips.Get(EquipSlot.LH)?.Rarity ?? 0; + int rightHandRarity = player.Session.Item.Equips.Get(EquipSlot.RH)?.Rarity ?? 0; + int leftHandRarity = player.Session.Item.Equips.Get(EquipSlot.LH)?.Rarity ?? 0; return BonusAttack.Coefficient(rightHandRarity, leftHandRarity, player.Value.Character.Job.Code()); } } @@ -140,6 +140,11 @@ public void Refresh() { AddBuffs(player); Values.Total(); StatConversion(player); + // Stat rebuild via AddBase/AddTotal restores Current to Total, + // but a dead player must stay at 0 HP until revived. + if (player.IsDead) { + Values[BasicAttribute.Health].Current = 0; + } Actor.Field.Broadcast(StatsPacket.Init(player)); Actor.Field.Broadcast(StatsPacket.Update(player), player.Session); diff --git a/Maple2.Server.Web/Controllers/WebController.cs b/Maple2.Server.Web/Controllers/WebController.cs index 84b694b69..444d99390 100644 --- a/Maple2.Server.Web/Controllers/WebController.cs +++ b/Maple2.Server.Web/Controllers/WebController.cs @@ -286,7 +286,7 @@ private static IResult HandleUnknownMode(UgcType mode) { #endregion #region Ranking - public ByteWriter Trophy(string userName) { + private ByteWriter Trophy(string userName) { string cacheKey = $"Trophy_{userName ?? "all"}"; if (!cache.TryGetValue(cacheKey, out byte[]? cachedData)) { @@ -312,7 +312,7 @@ public ByteWriter Trophy(string userName) { return result; } - public ByteWriter PersonalTrophy(long characterId) { + private ByteWriter PersonalTrophy(long characterId) { string cacheKey = $"PersonalTrophy_{characterId}"; if (!cache.TryGetValue(cacheKey, out byte[]? cachedData)) { @@ -328,7 +328,7 @@ public ByteWriter PersonalTrophy(long characterId) { return result; } - public ByteWriter GuildTrophy(string userName) { + private ByteWriter GuildTrophy(string userName) { if (!string.IsNullOrEmpty(userName)) { string cacheKey = $"GuildTrophy_{userName}"; @@ -359,7 +359,7 @@ public ByteWriter GuildTrophy(string userName) { return InGameRankPacket.GuildTrophy(GetCachedGuildTrophyRankings()); } - public ByteWriter PersonalGuildTrophy(long characterId) { + private ByteWriter PersonalGuildTrophy(long characterId) { string cacheKey = $"PersonalGuildTrophy_{characterId}"; if (!cache.TryGetValue(cacheKey, out byte[]? cachedData)) { @@ -397,7 +397,7 @@ private IList GetCachedGuildTrophyRankings() { } #endregion - public ByteWriter MenteeList(long accountId, long characterId) { + private ByteWriter MenteeList(long accountId, long characterId) { using GameStorage.Request db = gameStorage.Context(); IList list = db.GetMentorList(accountId, characterId); From 67702103f9f6ada432dc6933c776a5954fbe7294 Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Sun, 8 Mar 2026 20:54:35 +0800 Subject: [PATCH 25/26] Fix the pet-related system Fix the pet-related system (upgradable, combat-capable), optimize the pet damage calculation formula, and add basic AI to combat pets --- Maple2.Server.Game/Manager/ConfigManager.cs | 74 ++++- .../Field/FieldManager/FieldManager.State.cs | 77 ++++- Maple2.Server.Game/Manager/StatsManager.cs | 1 + .../Actor/ActorStateComponent/BattleState.cs | 12 + .../Actor/ActorStateComponent/SkillState.cs | 22 +- .../PacketHandlers/ChangeAttributesHandler.cs | 6 +- .../ChangeAttributesScrollHandler.cs | 16 +- .../PacketHandlers/PetHandler.cs | 275 +++++++++++++++++- Maple2.Server.Game/Packets/PetPacket.cs | 31 +- Maple2.Server.Game/Util/DamageCalculator.cs | 68 ++++- 10 files changed, 558 insertions(+), 24 deletions(-) diff --git a/Maple2.Server.Game/Manager/ConfigManager.cs b/Maple2.Server.Game/Manager/ConfigManager.cs index 84a83e35b..74094e4cc 100644 --- a/Maple2.Server.Game/Manager/ConfigManager.cs +++ b/Maple2.Server.Game/Manager/ConfigManager.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Maple2.Database.Extensions; using Maple2.Database.Storage; +using Maple2.Model; using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.Model.Metadata; @@ -453,25 +454,88 @@ public void ResetStatPoints() { session.Send(NoticePacket.Message("s_char_info_reset_stat_pointsuccess_msg")); } + public void ReapplyAllocatedStats(bool send = false) { + foreach (BasicAttribute type in statAttributes.Allocation.Attributes) { + int points = statAttributes.Allocation[type]; + if (points <= 0) { + continue; + } + + UpdateStatAttribute(type, points, send); + } + } + private void UpdateStatAttribute(BasicAttribute type, int points, bool send = true) { + var values = session.Player.Stats.Values; + long oldStr = values[BasicAttribute.Strength].Total; + long oldDex = values[BasicAttribute.Dexterity].Total; + long oldInt = values[BasicAttribute.Intelligence].Total; + long oldLuk = values[BasicAttribute.Luck].Total; + JobCode jobCode = session.Player.Value.Character.Job.Code(); + switch (type) { case BasicAttribute.Strength: case BasicAttribute.Dexterity: case BasicAttribute.Intelligence: case BasicAttribute.Luck: - session.Player.Stats.Values[type].AddTotal(1 * points); + values[type].AddTotal(points); break; case BasicAttribute.Health: - session.Player.Stats.Values[BasicAttribute.Health].AddTotal(10 * points); + values[BasicAttribute.Health].AddTotal(10L * points); break; case BasicAttribute.CriticalRate: - session.Player.Stats.Values[BasicAttribute.CriticalRate].AddTotal(3 * points); + values[BasicAttribute.CriticalRate].AddTotal(3L * points); + break; + } + + long newStr = values[BasicAttribute.Strength].Total; + long newDex = values[BasicAttribute.Dexterity].Total; + long newInt = values[BasicAttribute.Intelligence].Total; + long newLuk = values[BasicAttribute.Luck].Total; + + long oldPhysicalAtk = Maple2.Server.Core.Formulas.AttackStat.PhysicalAtk(jobCode, oldStr, oldDex, oldLuk); + long newPhysicalAtk = Maple2.Server.Core.Formulas.AttackStat.PhysicalAtk(jobCode, newStr, newDex, newLuk); + long oldMagicalAtk = Maple2.Server.Core.Formulas.AttackStat.MagicalAtk(jobCode, oldInt); + long newMagicalAtk = Maple2.Server.Core.Formulas.AttackStat.MagicalAtk(jobCode, newInt); + + long physicalAtkDelta = newPhysicalAtk - oldPhysicalAtk; + long magicalAtkDelta = newMagicalAtk - oldMagicalAtk; + if (physicalAtkDelta != 0) { + values[BasicAttribute.PhysicalAtk].AddTotal(physicalAtkDelta); + } + if (magicalAtkDelta != 0) { + values[BasicAttribute.MagicalAtk].AddTotal(magicalAtkDelta); + } + + switch (type) { + case BasicAttribute.Strength: + values[BasicAttribute.PhysicalRes].AddTotal(points); + break; + case BasicAttribute.Dexterity: + values[BasicAttribute.PhysicalRes].AddTotal(points); + values[BasicAttribute.Accuracy].AddTotal(points); + break; + case BasicAttribute.Intelligence: + values[BasicAttribute.MagicalRes].AddTotal(points); + break; + case BasicAttribute.Luck: + values[BasicAttribute.CriticalRate].AddTotal(points); break; } - // Sends packet to notify client, skipped during loading. if (send) { - session.Send(StatsPacket.Update(session.Player, type)); + session.Send(StatsPacket.Update(session.Player, + BasicAttribute.Strength, + BasicAttribute.Dexterity, + BasicAttribute.Intelligence, + BasicAttribute.Luck, + BasicAttribute.Health, + BasicAttribute.CriticalRate, + BasicAttribute.PhysicalAtk, + BasicAttribute.MagicalAtk, + BasicAttribute.PhysicalRes, + BasicAttribute.MagicalRes, + BasicAttribute.Accuracy)); } } #endregion diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index 986d7b3a6..67b7fbb1b 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -191,17 +191,92 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId int objectId = player != null ? NextGlobalId() : NextLocalId(); AnimationMetadata? animation = NpcMetadata.GetAnimation(npc.Model.Name); - var fieldPet = new FieldPet(this, objectId, agent, new Npc(npc, animation), pet, petMetadata, Constant.PetFieldAiPath, player) { + string aiPath = Constant.PetFieldAiPath; + if (player != null) { + string customBattleAiPath = $"Pet/AI_{pet.Id}.xml"; + aiPath = AiMetadata.TryGet(customBattleAiPath, out _) ? customBattleAiPath : "Pet/AI_DefaultPetBattle.xml"; + } + + var fieldPet = new FieldPet(this, objectId, agent, new Npc(npc, animation), pet, petMetadata, aiPath, player) { Owner = owner, Position = position, Rotation = rotation, Origin = owner?.Position ?? position, }; + if (player != null) { + ApplyOwnedPetCombatStats(fieldPet); + } + Pets[fieldPet.ObjectId] = fieldPet; return fieldPet; } + private static void ApplyOwnedPetCombatStats(FieldPet fieldPet) { + Stats stats = fieldPet.Stats.Values; + Item petItem = fieldPet.Pet; + int petLevel = Math.Max(1, (int) (petItem.Pet?.Level ?? (short) 1)); + int petRarity = Math.Max(1, petItem.Rarity); + + double levelScale = 1d + ((petLevel - 1) * 0.08d); + double rarityScale = 1d + ((petRarity - 1) * 0.18d); + double combinedScale = Math.Max(1d, levelScale * rarityScale); + + BoostBase(BasicAttribute.PhysicalAtk, 0.90d); + BoostBase(BasicAttribute.MagicalAtk, 0.90d); + BoostBase(BasicAttribute.MinWeaponAtk, 0.90d); + BoostBase(BasicAttribute.MaxWeaponAtk, 0.90d); + BoostBase(BasicAttribute.Accuracy, 0.35d); + BoostBase(BasicAttribute.CriticalRate, 0.30d); + BoostBase(BasicAttribute.Defense, 0.35d); + BoostBase(BasicAttribute.PhysicalRes, 0.35d); + BoostBase(BasicAttribute.MagicalRes, 0.35d); + BoostBase(BasicAttribute.Health, 0.75d); + + if (petItem.Stats != null) { + foreach (ItemStats.Type type in Enum.GetValues()) { + ApplyPetItemOption(stats, petItem.Stats[type]); + } + } + + stats.Total(); + + if (stats[BasicAttribute.MinWeaponAtk].Total <= 0) { + long fallbackWeapon = Math.Max(1L, (long) Math.Round(Math.Max(stats[BasicAttribute.PhysicalAtk].Total, stats[BasicAttribute.MagicalAtk].Total) * 0.4d)); + stats[BasicAttribute.MinWeaponAtk].AddTotal(fallbackWeapon); + } + + if (stats[BasicAttribute.MaxWeaponAtk].Total <= stats[BasicAttribute.MinWeaponAtk].Total) { + long fallbackSpread = Math.Max(1L, (long) Math.Round(Math.Max(stats[BasicAttribute.PhysicalAtk].Total, stats[BasicAttribute.MagicalAtk].Total) * 0.2d)); + stats[BasicAttribute.MaxWeaponAtk].AddTotal(stats[BasicAttribute.MinWeaponAtk].Total + fallbackSpread - stats[BasicAttribute.MaxWeaponAtk].Total); + } + + stats[BasicAttribute.Health].Current = stats[BasicAttribute.Health].Total; + return; + + void BoostBase(BasicAttribute attribute, double factor) { + long baseValue = stats[attribute].Base; + if (baseValue <= 0) { + return; + } + + long bonus = (long) Math.Round(baseValue * (combinedScale - 1d) * factor); + if (bonus > 0) { + stats[attribute].AddTotal(bonus); + } + } + } + + private static void ApplyPetItemOption(Stats stats, ItemStats.Option option) { + foreach ((BasicAttribute attribute, BasicOption value) in option.Basic) { + stats[attribute].AddTotal(value); + } + + foreach ((SpecialAttribute attribute, SpecialOption value) in option.Special) { + stats[attribute].AddTotal(value); + } + } + public FieldPortal SpawnPortal(Portal portal, Vector3 position = default, Vector3 rotation = default) { var fieldPortal = new FieldPortal(this, NextLocalId(), portal) { Position = position != default ? position : portal.Position, diff --git a/Maple2.Server.Game/Manager/StatsManager.cs b/Maple2.Server.Game/Manager/StatsManager.cs index 34360c1bf..97b109e54 100644 --- a/Maple2.Server.Game/Manager/StatsManager.cs +++ b/Maple2.Server.Game/Manager/StatsManager.cs @@ -138,6 +138,7 @@ public void Refresh() { AddEquips(player); AddBuffs(player); + player.Session.Config.ReapplyAllocatedStats(); Values.Total(); StatConversion(player); // Stat rebuild via AddBase/AddTotal restores Current to Total, diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/BattleState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/BattleState.cs index c3be812ce..17635a3c8 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/BattleState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/BattleState.cs @@ -202,6 +202,8 @@ private bool ShouldKeepTarget() { target = targetPlayer; } else if (actor.Field.Npcs.TryGetValue(TargetId, out FieldNpc? targetNpc)) { target = targetNpc; + } else if (actor.Field.Mobs.TryGetValue(TargetId, out FieldNpc? targetMob)) { + target = targetMob; } @@ -215,6 +217,10 @@ private bool ShouldKeepTarget() { private int GetTargetType() { int friendlyType = actor.Value.Metadata.Basic.Friendly; + if (actor is FieldPet { OwnerId: > 0 }) { + friendlyType = 1; + } + if (friendlyType != 2 && TargetNode is not null && TargetType == NodeTargetType.HasAdditional) { friendlyType = TargetNode.Target switch { NodeAiTarget.Hostile => friendlyType == 0 ? 0 : 1, // enemies target players (0), friendlies target enemies (1) @@ -251,6 +257,12 @@ private void FindNewTarget() { } if (friendlyType == 1) { + foreach (FieldNpc npc in actor.Field.Mobs.Values) { + if (ShouldTargetActor(npc, sightSquared, sightHeightUp, sightHeightDown, ref nextTargetDistance, candidates)) { + nextTarget = npc; + } + } + foreach (FieldNpc npc in actor.Field.Npcs.Values) { if (npc.Value.Metadata.Basic.Friendly != 0) { continue; diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs index 4d6577958..05df5781f 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/SkillState.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using Maple2.Model.Metadata; using Maple2.Server.Game.Model.Skill; using Maple2.Server.Game.Packets; @@ -74,13 +74,23 @@ public void SkillCastAttack(SkillRecord cast, byte attackPoint, List att } - // Apply damage to targets server-side for NPC attacks - // Always use the attack range prism to resolve targets so spatial checks are respected - Tools.Collision.Prism attackPrism = attack.Range.GetPrism(actor.Position, actor.Rotation.Z); + // Apply damage to targets server-side for NPC attacks. + // For player-owned combat pets, prefer the current battle target directly. + // Many pet skills are authored with client-side target metadata that does not + // line up with our generic NPC target query, which can cause the owner/player + // to be selected instead of the hostile mob. var resolvedTargets = new List(); int queryLimit = attack.TargetCount > 0 ? attack.TargetCount : 1; - foreach (IActor target in actor.Field.GetTargets(actor, [attackPrism], attack.Range, queryLimit)) { - resolvedTargets.Add(target); + + if (actor is FieldPet { OwnerId: > 0 } ownedPet && ownedPet.BattleState.Target is FieldNpc hostileTarget && !hostileTarget.IsDead) { + resolvedTargets.Add(hostileTarget); + } + + if (resolvedTargets.Count == 0) { + Tools.Collision.Prism attackPrism = attack.Range.GetPrism(actor.Position, actor.Rotation.Z); + foreach (IActor target in actor.Field.GetTargets(actor, [attackPrism], attack.Range, queryLimit)) { + resolvedTargets.Add(target); + } } if (resolvedTargets.Count > 0) { diff --git a/Maple2.Server.Game/PacketHandlers/ChangeAttributesHandler.cs b/Maple2.Server.Game/PacketHandlers/ChangeAttributesHandler.cs index 330a24047..3cbcb59f4 100644 --- a/Maple2.Server.Game/PacketHandlers/ChangeAttributesHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ChangeAttributesHandler.cs @@ -204,10 +204,14 @@ private static void HandleForceFill(GameSession session, IByteReader packet) { // It needs to be epic or better armor and accessories at level 50 and above. private static bool IsValidItem(Item item) { + if (item.Type.IsCombatPet) { + return item.Rarity is >= 1 and <= Constant.ChangeAttributesMaxRarity; + } + if (item.Rarity is < Constant.ChangeAttributesMinRarity or > Constant.ChangeAttributesMaxRarity) { return false; } - if (!item.Type.IsWeapon && !item.Type.IsArmor && !item.Type.IsAccessory && !item.Type.IsCombatPet) { + if (!item.Type.IsWeapon && !item.Type.IsArmor && !item.Type.IsAccessory) { return false; } if (item.Metadata.Limit.Level < Constant.ChangeAttributesMinLevel) { diff --git a/Maple2.Server.Game/PacketHandlers/ChangeAttributesScrollHandler.cs b/Maple2.Server.Game/PacketHandlers/ChangeAttributesScrollHandler.cs index 4635be90c..13bae3c0a 100644 --- a/Maple2.Server.Game/PacketHandlers/ChangeAttributesScrollHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ChangeAttributesScrollHandler.cs @@ -216,15 +216,21 @@ private static void HandleSelect(GameSession session, IByteReader packet) { private ChangeAttributesScrollError IsCompatibleScroll(Item item, Item scroll, out ItemRemakeScrollMetadata? metadata) { metadata = null; - if (item.Rarity is < Constant.ChangeAttributesMinRarity or > Constant.ChangeAttributesMaxRarity) { - return ChangeAttributesScrollError.s_itemremake_scroll_error_impossible_rank; + if (item.Type.IsPet) { + if (item.Rarity is < 1 or > Constant.ChangeAttributesMaxRarity) { + return ChangeAttributesScrollError.s_itemremake_scroll_error_impossible_rank; + } + } else { + if (item.Rarity is < Constant.ChangeAttributesMinRarity or > Constant.ChangeAttributesMaxRarity) { + return ChangeAttributesScrollError.s_itemremake_scroll_error_impossible_rank; + } + if (item.Metadata.Limit.Level < Constant.ChangeAttributesMinLevel) { + return ChangeAttributesScrollError.s_itemremake_scroll_error_impossible_level; + } } if (!item.Type.IsWeapon && item.Type is { IsArmor: false, IsAccessory: false, IsPet: false }) { return ChangeAttributesScrollError.s_itemremake_scroll_error_impossible_slot; } - if (item.Metadata.Limit.Level < Constant.ChangeAttributesMinLevel) { - return ChangeAttributesScrollError.s_itemremake_scroll_error_impossible_level; - } // Validate scroll conditions if (!int.TryParse(scroll.Metadata.Function?.Parameters, out int remakeId)) { diff --git a/Maple2.Server.Game/PacketHandlers/PetHandler.cs b/Maple2.Server.Game/PacketHandlers/PetHandler.cs index e1e29789e..4f45f45b6 100644 --- a/Maple2.Server.Game/PacketHandlers/PetHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/PetHandler.cs @@ -1,4 +1,6 @@ -using Maple2.Model.Enum; +using System.Collections.Concurrent; +using Maple2.Model.Enum; +using Maple2.Model.Error; using Maple2.Model.Game; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; @@ -7,10 +9,21 @@ using Maple2.Server.Game.Model; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; +using Maple2.Database.Storage; +using Maple2.Model.Metadata; namespace Maple2.Server.Game.PacketHandlers; public class PetHandler : FieldPacketHandler { + private sealed class DailyFusionBonusState { + public DateOnly Day; + public int UsedCount; + } + + private const double StrengthenedBattlePetExpScale = 1.5d; + private const double DailyFusionBonusRate = 0.125d; + private static readonly ConcurrentDictionary DailyFusionBonusByCharacter = new(); + public override RecvOp OpCode => RecvOp.RequestPet; private enum Command : byte { @@ -127,10 +140,99 @@ private void HandleUpdateLootConfig(GameSession session, IByteReader packet) { private void HandleFusion(GameSession session, IByteReader packet) { long petUid = packet.ReadLong(); short count = packet.ReadShort(); + + var fodderUids = new List(count); for (int i = 0; i < count; i++) { - packet.ReadLong(); // fodder uid + fodderUids.Add(packet.ReadLong()); packet.ReadInt(); // count } + + Item? pet = session.Item.Inventory.Get(petUid, InventoryType.Pets); + if (pet?.Pet == null) { + session.Send(PetPacket.Error(PetError.s_common_error_unknown)); + return; + } + + long gainedExp = 0; + int remainingFusionBonuses = GetRemainingFusionBonuses(session); + lock (session.Item) { + foreach (long fodderUid in fodderUids) { + if (fodderUid == petUid) { + continue; + } + + Item? fodder = session.Item.Inventory.Get(fodderUid, InventoryType.Pets); + if (fodder?.Pet == null) { + continue; + } + + long materialExp = GetFusionMaterialExp(fodder); + if (remainingFusionBonuses > 0) { + long bonusExp = (long) Math.Floor(materialExp * DailyFusionBonusRate); + materialExp += bonusExp; + ConsumeFusionBonus(session); + remainingFusionBonuses--; + } + + gainedExp += materialExp; + if (session.Item.Inventory.Remove(fodderUid, out Item? removed)) { + session.Item.Inventory.Discard(removed, commit: true); + } + } + } + + if (gainedExp <= 0) { + session.Send(PetPacket.Error(PetError.s_common_error_unknown)); + return; + } + + pet.Pet.Exp += gainedExp; + + bool leveled = false; + while (pet.Pet.Level < Constant.PetMaxLevel) { + long requiredExp = GetRequiredPetExp(pet.Pet.Level, pet); + if (pet.Pet.Exp < requiredExp) { + break; + } + + pet.Pet.Exp -= requiredExp; + pet.Pet.Level++; + leveled = true; + } + + using (GameStorage.Request db = session.GameStorage.Context()) { + db.UpdateItem(pet); + } + + session.Send(ItemInventoryPacket.UpdateItem(pet)); + session.Send(PetPacket.PetInfo(session.Player.ObjectId, pet)); + FieldPet? summonedPet = GetSummonedFieldPet(session, pet.Uid); + if (summonedPet != null) { + session.Send(PetPacket.Fusion(summonedPet)); + if (leveled) { + session.Send(PetPacket.LevelUp(summonedPet)); + } + } else { + session.Send(PetPacket.Fusion(session.Player.ObjectId, pet)); + if (leveled) { + session.Send(PetPacket.LevelUp(session.Player.ObjectId, pet)); + } + session.Send(PetPacket.FusionCount(GetRemainingFusionBonuses(session))); + } + } + + private static FieldPet? GetSummonedFieldPet(GameSession session, long petUid) { + if (session.Field == null) { + return null; + } + + foreach (FieldPet fieldPet in session.Field.Pets.Values) { + if (fieldPet.OwnerId == session.Player.ObjectId && fieldPet.Pet.Uid == petUid) { + return fieldPet; + } + } + + return null; } private void HandleAttack(GameSession session, IByteReader packet) { @@ -144,14 +246,181 @@ private void HandleUnknown16(GameSession session, IByteReader packet) { private void HandleEvolve(GameSession session, IByteReader packet) { long petUid = packet.ReadLong(); + + Item? pet = session.Item.Inventory.Get(petUid, InventoryType.Pets); + if (pet?.Pet == null) { + session.Send(PetPacket.Error(PetError.s_common_error_unknown)); + return; + } + + int requiredPoints = GetRequiredEvolvePoints(pet.Rarity); + if (pet.Pet.EvolvePoints < requiredPoints) { + session.Send(PetPacket.Error(PetError.s_common_error_unknown)); + return; + } + + pet.Pet.EvolvePoints -= requiredPoints; + using (GameStorage.Request db = session.GameStorage.Context()) { + db.UpdateItem(pet); + } + session.Send(ItemInventoryPacket.UpdateItem(pet)); + session.Send(PetPacket.EvolvePoints(session.Player.ObjectId, pet)); } private void HandleEvolvePoints(GameSession session, IByteReader packet) { long petUid = packet.ReadLong(); short count = packet.ReadShort(); + + var fodderUids = new List(count); for (int i = 0; i < count; i++) { - packet.ReadLong(); // fodder uid + fodderUids.Add(packet.ReadLong()); } + + Item? pet = session.Item.Inventory.Get(petUid, InventoryType.Pets); + if (pet?.Pet == null) { + session.Send(PetPacket.Error(PetError.s_common_error_unknown)); + return; + } + + int gainedPoints = 0; + lock (session.Item) { + foreach (long fodderUid in fodderUids) { + if (fodderUid == petUid) { + continue; + } + + Item? fodder = session.Item.Inventory.Get(fodderUid, InventoryType.Pets); + if (fodder?.Pet == null) { + continue; + } + + gainedPoints += Math.Max(1, fodder.Rarity); + if (session.Item.Inventory.Remove(fodderUid, out Item? removed)) { + session.Item.Inventory.Discard(removed, commit: true); + } + } + } + + if (gainedPoints <= 0) { + session.Send(PetPacket.Error(PetError.s_common_error_unknown)); + return; + } + + pet.Pet.EvolvePoints += gainedPoints; + using (GameStorage.Request db = session.GameStorage.Context()) { + db.UpdateItem(pet); + } + session.Send(ItemInventoryPacket.UpdateItem(pet)); + session.Send(PetPacket.EvolvePoints(session.Player.ObjectId, pet)); + } + + private static long GetRequiredPetExp(Item pet) { + int levelRequirement = pet.Metadata.Limit.Level; + int rarity = pet.Rarity; + + long baseExp = (rarity, levelRequirement) switch { + (1, 50) => 22000L, + (2, 50) => 55000L, + (3, 50) => 90000L, + (3, 60) => 135000L, + (4, 50) => 90000L, + (4, 60) => 135000L, + (_, >= 60) => 135000L, + _ => rarity switch { + <= 1 => 22000L, + 2 => 55000L, + 3 => 90000L, + _ => 90000L, + } + }; + + return (long) Math.Round(baseExp * GetBattlePetExpScale(pet)); + } + + private static long GetFusionBaseExp(Item pet) { + int levelRequirement = pet.Metadata.Limit.Level; + int rarity = pet.Rarity; + + long baseExp = (rarity, levelRequirement) switch { + (1, 50) => 1500L, + (2, 50) => 3000L, + (3, 50) => 12000L, + (3, 60) => 18000L, + (4, 50) => 24000L, + (4, 60) => 36000L, + (_, >= 60) => 18000L, + _ => rarity switch { + <= 1 => 1500L, + 2 => 3000L, + 3 => 12000L, + _ => 24000L, + } + }; + + return (long) Math.Round(baseExp * GetBattlePetExpScale(pet)); + } + + private static long GetFusionMaterialExp(Item pet) { + if (pet.Pet == null) { + return 0L; + } + + long perLevelExp = GetRequiredPetExp(pet); + long investedExp = (Math.Max(1, (int) pet.Pet.Level) - 1L) * perLevelExp + Math.Max(0L, pet.Pet.Exp); + long materialExp = GetFusionBaseExp(pet) + (long) Math.Floor(investedExp * 0.8d); + return Math.Max(0L, materialExp); + } + + private static long GetRequiredPetExp(short level, Item pet) { + return GetRequiredPetExp(pet); + } + + private static int GetRequiredEvolvePoints(int rarity) { + int safeRarity = Math.Max(1, rarity); + return safeRarity * 10; + } + + private static double GetBattlePetExpScale(Item pet) { + if (pet.Id >= 61100000 || pet.Metadata.Limit.Level >= 60) { + return StrengthenedBattlePetExpScale; + } + + return 1d; + } + + private static int GetRemainingFusionBonuses(GameSession session) { + int characterId = (int) session.CharacterId; + DateOnly today = DateOnly.FromDateTime(DateTime.Now); + + DailyFusionBonusState state = DailyFusionBonusByCharacter.AddOrUpdate(characterId, + _ => new DailyFusionBonusState { Day = today, UsedCount = 0 }, + (_, existing) => { + if (existing.Day != today) { + existing.Day = today; + existing.UsedCount = 0; + } + + return existing; + }); + + return Math.Max(0, Constant.DailyPetEnchantMaxCount - state.UsedCount); + } + + private static void ConsumeFusionBonus(GameSession session) { + int characterId = (int) session.CharacterId; + DateOnly today = DateOnly.FromDateTime(DateTime.Now); + + DailyFusionBonusByCharacter.AddOrUpdate(characterId, + _ => new DailyFusionBonusState { Day = today, UsedCount = 1 }, + (_, existing) => { + if (existing.Day != today) { + existing.Day = today; + existing.UsedCount = 0; + } + + existing.UsedCount = Math.Min(Constant.DailyPetEnchantMaxCount, existing.UsedCount + 1); + return existing; + }); } private static void SummonPet(GameSession session, long petUid) { diff --git a/Maple2.Server.Game/Packets/PetPacket.cs b/Maple2.Server.Game/Packets/PetPacket.cs index c147e0cec..b3cba1bb8 100644 --- a/Maple2.Server.Game/Packets/PetPacket.cs +++ b/Maple2.Server.Game/Packets/PetPacket.cs @@ -128,12 +128,23 @@ public static ByteWriter Fusion(FieldPet pet) { var pWriter = Packet.Of(SendOp.ResponsePet); pWriter.Write(Command.Fusion); pWriter.WriteInt(pet.OwnerId); - pWriter.WriteLong(pet.Pet.Pet?.Exp ?? 0); + pWriter.WriteLong(GetFusionDisplayExp(pet.Pet.Pet?.Exp ?? 0)); pWriter.WriteLong(pet.Pet.Uid); return pWriter; } + public static ByteWriter Fusion(int ownerId, Item pet) { + var pWriter = Packet.Of(SendOp.ResponsePet); + pWriter.Write(Command.Fusion); + pWriter.WriteInt(ownerId); + pWriter.WriteLong(GetFusionDisplayExp(pet.Pet?.Exp ?? 0)); + pWriter.WriteLong(pet.Uid); + + return pWriter; + } + + public static ByteWriter LevelUp(FieldPet pet) { var pWriter = Packet.Of(SendOp.ResponsePet); pWriter.Write(Command.LevelUp); @@ -143,6 +154,24 @@ public static ByteWriter LevelUp(FieldPet pet) { return pWriter; } + public static ByteWriter LevelUp(int ownerId, Item pet) { + var pWriter = Packet.Of(SendOp.ResponsePet); + pWriter.Write(Command.LevelUp); + pWriter.WriteInt(ownerId); + pWriter.WriteInt(pet.Pet?.Level ?? 1); + pWriter.WriteLong(pet.Uid); + + return pWriter; + } + + + + private static long GetFusionDisplayExp(long exp) { + // The compose UI appears to interpret the fusion progress value at half scale. + // Send doubled display progress here so the immediate fusion bar matches the + // actual persisted pet EXP shown after reloading pet info. + return Math.Max(0L, exp * 2L); + } public static ByteWriter FusionCount(int count) { var pWriter = Packet.Of(SendOp.ResponsePet); diff --git a/Maple2.Server.Game/Util/DamageCalculator.cs b/Maple2.Server.Game/Util/DamageCalculator.cs index 8060b9f93..bfcbe027f 100644 --- a/Maple2.Server.Game/Util/DamageCalculator.cs +++ b/Maple2.Server.Game/Util/DamageCalculator.cs @@ -1,4 +1,4 @@ -using Maple2.Model.Enum; +using Maple2.Model.Enum; using Maple2.Model.Metadata; using Maple2.Server.Core.Formulas; using Maple2.Server.Game.Model; @@ -27,6 +27,10 @@ public static (DamageType, long) CalculateDamage(IActor caster, IActor target, D var damageType = DamageType.Normal; (double minBonusAtkDamage, double maxBonusAtkDamage) = caster.Stats.GetBonusAttack(target.Buffs.GetResistance(BasicAttribute.BonusAtk), target.Buffs.GetResistance(BasicAttribute.MaxWeaponAtk)); + if (caster is FieldPet { OwnerId: > 0 } ownedPet) { + (minBonusAtkDamage, maxBonusAtkDamage) = GetOwnedPetAttackRange(ownedPet); + } + double attackDamage = minBonusAtkDamage + (maxBonusAtkDamage - minBonusAtkDamage) * double.Lerp(Random.Shared.NextDouble(), Random.Shared.NextDouble(), Random.Shared.NextDouble()); // change the NPCNormalDamage to be changed depending on target? @@ -97,6 +101,7 @@ public static (DamageType, long) CalculateDamage(IActor caster, IActor target, D double attackTypeAmount = 0; double resistance = 0; double finalDamage = 0; + FieldPet? ownedPetCaster = caster as FieldPet; switch (property.AttackType) { case AttackType.Physical: resistance = Damage.CalculateResistance(target.Stats.Values[BasicAttribute.PhysicalRes].Total, caster.Stats.Values[SpecialAttribute.PhysicalPiercing].Multiplier()); @@ -128,6 +133,10 @@ public static (DamageType, long) CalculateDamage(IActor caster, IActor target, D break; } + if (ownedPetCaster is { OwnerId: > 0 }) { + attackTypeAmount = Math.Max(attackTypeAmount, GetOwnedPetAttackFactor(ownedPetCaster)); + } + damageMultiplier *= attackTypeAmount * resistance * (finalDamage == 0 ? 1 : finalDamage); attackDamage *= damageMultiplier * Constant.AttackDamageFactor + property.Value; @@ -146,4 +155,59 @@ public static (DamageType, long) CalculateDamage(IActor caster, IActor target, D return (damageType, (long) Math.Max(1, attackDamage)); } -} + + private static double GetOwnedPetAttackFactor(FieldPet pet) { + long physicalAtk = pet.Stats.Values[BasicAttribute.PhysicalAtk].Total; + long magicalAtk = pet.Stats.Values[BasicAttribute.MagicalAtk].Total; + long minWeaponAtk = pet.Stats.Values[BasicAttribute.MinWeaponAtk].Total; + long maxWeaponAtk = pet.Stats.Values[BasicAttribute.MaxWeaponAtk].Total; + double petBonusAtk = GetTotalPetBonusAttack(pet); + + double directAtk = Math.Max(physicalAtk, magicalAtk); + double weaponAtk = Math.Max(minWeaponAtk, maxWeaponAtk); + double bonusDrivenAtk = petBonusAtk > 0 ? petBonusAtk * 4.96d : 0d; + + double factor = Math.Max(directAtk, weaponAtk); + factor = Math.Max(factor, bonusDrivenAtk); + + if (petBonusAtk > 0) { + factor += petBonusAtk * 1.50d; + } + + return Math.Max(1d, factor); + } + + private static (double Min, double Max) GetOwnedPetAttackRange(FieldPet pet) { + long minWeaponAtk = pet.Stats.Values[BasicAttribute.MinWeaponAtk].Total; + long maxWeaponAtk = pet.Stats.Values[BasicAttribute.MaxWeaponAtk].Total; + long physicalAtk = pet.Stats.Values[BasicAttribute.PhysicalAtk].Total; + long magicalAtk = pet.Stats.Values[BasicAttribute.MagicalAtk].Total; + double petBonusAtk = GetTotalPetBonusAttack(pet); + + double attackBase = Math.Max(physicalAtk, magicalAtk); + if (attackBase <= 0) { + attackBase = Math.Max(minWeaponAtk, maxWeaponAtk); + } + + double bonusDrivenAtk = petBonusAtk > 0 ? petBonusAtk * 4.96d : 0d; + attackBase = Math.Max(attackBase, bonusDrivenAtk); + + if (attackBase <= 0) { + attackBase = 100d; + } + + double min = Math.Max(1d, minWeaponAtk > 0 ? Math.Max(minWeaponAtk, attackBase * 0.65d) : attackBase * 0.65d); + double max = Math.Max(min + 1d, maxWeaponAtk > 0 ? Math.Max(maxWeaponAtk, attackBase * 0.95d) : attackBase * 0.95d); + return (min, max); + } + + private static double GetTotalPetBonusAttack(FieldPet pet) { + double petBonusAtk = pet.Stats.Values[BasicAttribute.PetBonusAtk].Total + pet.Stats.Values[BasicAttribute.BonusAtk].Total; + + if (pet.Field.Players.TryGetValue(pet.OwnerId, out FieldPlayer? owner)) { + petBonusAtk += owner.Stats.Values[BasicAttribute.PetBonusAtk].Total; + } + + return Math.Max(0d, petBonusAtk); + } +} \ No newline at end of file From 4b09c41c19c93de8589e6ff8f4a1b298633caa59 Mon Sep 17 00:00:00 2001 From: JewelRuby Date: Mon, 9 Mar 2026 21:49:45 +0800 Subject: [PATCH 26/26] By repairing the Royal System By repairing the Royal System, he can at least claim rewards and activate the Royal Passport --- .../Config/MushkingPassConfig.cs | 43 ++ .../Config/SurvivalPassXmlConfig.cs | 193 ++++++++ Maple2.Server.Game/Config/survivallevel.xml | 63 +++ .../Config/survivalpassreward.xml | 74 +++ .../Config/survivalpassreward_paid.xml | 73 +++ .../Config/survivalserverconfig.xml | 4 + Maple2.Server.Game/Manager/SurvivalManager.cs | 433 ++++++++++++++++-- Maple2.Server.Game/Maple2.Server.Game.csproj | 3 + .../Model/Field/Actor/FieldNpc.cs | 2 + .../PacketHandlers/ItemUseHandler.cs | 12 + .../PacketHandlers/SurvivalHandler.cs | 33 +- Maple2.Server.Game/Packets/SurvivalPacket.cs | 27 +- Maple2.Server.Game/Session/GameSession.cs | 2 +- config/survivallevel.xml | 63 +++ config/survivallevelreward.xml | 303 ++++++++++++ config/survivalpassreward.xml | 73 +++ config/survivalpassreward_paid.xml | 73 +++ config/survivalserverconfig.xml | 4 + 18 files changed, 1409 insertions(+), 69 deletions(-) create mode 100644 Maple2.Server.Game/Config/MushkingPassConfig.cs create mode 100644 Maple2.Server.Game/Config/SurvivalPassXmlConfig.cs create mode 100644 Maple2.Server.Game/Config/survivallevel.xml create mode 100644 Maple2.Server.Game/Config/survivalpassreward.xml create mode 100644 Maple2.Server.Game/Config/survivalpassreward_paid.xml create mode 100644 Maple2.Server.Game/Config/survivalserverconfig.xml create mode 100644 config/survivallevel.xml create mode 100644 config/survivallevelreward.xml create mode 100644 config/survivalpassreward.xml create mode 100644 config/survivalpassreward_paid.xml create mode 100644 config/survivalserverconfig.xml diff --git a/Maple2.Server.Game/Config/MushkingPassConfig.cs b/Maple2.Server.Game/Config/MushkingPassConfig.cs new file mode 100644 index 000000000..f1541c3ea --- /dev/null +++ b/Maple2.Server.Game/Config/MushkingPassConfig.cs @@ -0,0 +1,43 @@ +using System.Text.Json.Serialization; + +namespace Maple2.Server.Game.Config; + +public sealed class MushkingPassConfig { + public bool Enabled { get; set; } = true; + public string SeasonName { get; set; } = "Pre-Season"; + public DateTime SeasonStartUtc { get; set; } = new(2026, 3, 1, 0, 0, 0, DateTimeKind.Utc); + public DateTime SeasonEndUtc { get; set; } = new(2028, 10, 1, 0, 0, 0, DateTimeKind.Utc); + public int MaxLevel { get; set; } = 30; + public int ExpPerLevel { get; set; } = 100; + public MonsterExpConfig MonsterExp { get; set; } = new(); + public int GoldPassActivationItemId { get; set; } + public int GoldPassActivationItemCount { get; set; } = 1; + public List FreeRewards { get; set; } = []; + public List GoldRewards { get; set; } = []; + + [JsonIgnore] + public IReadOnlyDictionary FreeRewardsByLevel => freeRewardsByLevel ??= FreeRewards + .GroupBy(entry => entry.Level) + .ToDictionary(group => group.Key, group => group.Last()); + + [JsonIgnore] + public IReadOnlyDictionary GoldRewardsByLevel => goldRewardsByLevel ??= GoldRewards + .GroupBy(entry => entry.Level) + .ToDictionary(group => group.Key, group => group.Last()); + + private Dictionary? freeRewardsByLevel; + private Dictionary? goldRewardsByLevel; +} + +public sealed class MonsterExpConfig { + public int Normal { get; set; } = 2; + public int Elite { get; set; } = 8; + public int Boss { get; set; } = 30; +} + +public sealed class PassRewardConfig { + public int Level { get; set; } + public int ItemId { get; set; } + public int Rarity { get; set; } = -1; + public int Amount { get; set; } = 1; +} diff --git a/Maple2.Server.Game/Config/SurvivalPassXmlConfig.cs b/Maple2.Server.Game/Config/SurvivalPassXmlConfig.cs new file mode 100644 index 000000000..743a1cec6 --- /dev/null +++ b/Maple2.Server.Game/Config/SurvivalPassXmlConfig.cs @@ -0,0 +1,193 @@ +using System.Xml.Linq; +using Serilog; + +namespace Maple2.Server.Game.Config; + +public sealed class SurvivalPassXmlConfig { + private static readonly ILogger Logger = Log.Logger.ForContext(); + + public SortedDictionary LevelThresholds { get; } = new SortedDictionary(); + public Dictionary FreeRewards { get; } = new Dictionary(); + public Dictionary PaidRewards { get; } = new Dictionary(); + + public int ActivationItemId { get; private set; } + public int ActivationItemCount { get; private set; } = 1; + public int MonsterKillExp { get; private set; } = 1; + public int EliteKillExp { get; private set; } = 5; + public int BossKillExp { get; private set; } = 20; + public bool AllowDirectActivateWithoutItem { get; private set; } + + public static SurvivalPassXmlConfig Load() { + var config = new SurvivalPassXmlConfig(); + string baseDir = AppContext.BaseDirectory; + + config.LoadServerConfig(FindFile(baseDir, "survivalserverconfig.xml")); + config.LoadLevels(FindFile(baseDir, "survivallevel.xml")); + config.LoadRewards(FindFile(baseDir, "survivalpassreward.xml"), config.FreeRewards); + config.LoadRewards(FindFile(baseDir, "survivalpassreward_paid.xml"), config.PaidRewards); + + if (config.LevelThresholds.Count == 0) { + config.LevelThresholds[1] = 0; + } + + Logger.Information("Loaded survival config thresholds={Thresholds} freeRewards={FreeRewards} paidRewards={PaidRewards} activationItem={ItemId} x{ItemCount}", + config.LevelThresholds.Count, config.FreeRewards.Count, config.PaidRewards.Count, config.ActivationItemId, config.ActivationItemCount); + return config; + } + + private void LoadServerConfig(string path) { + if (!File.Exists(path)) { + return; + } + + XDocument document = XDocument.Load(path); + XElement? node = document.Root == null ? null : document.Root.Element("survivalPassServer"); + if (node == null) { + return; + } + + ActivationItemId = ParseInt(node.Attribute("activationItemId") != null ? node.Attribute("activationItemId")!.Value : null); + ActivationItemCount = Math.Max(1, ParseInt(node.Attribute("activationItemCount") != null ? node.Attribute("activationItemCount")!.Value : null, 1)); + MonsterKillExp = Math.Max(1, ParseInt(node.Attribute("monsterKillExp") != null ? node.Attribute("monsterKillExp")!.Value : null, 1)); + EliteKillExp = Math.Max(1, ParseInt(node.Attribute("eliteKillExp") != null ? node.Attribute("eliteKillExp")!.Value : null, 5)); + BossKillExp = Math.Max(1, ParseInt(node.Attribute("bossKillExp") != null ? node.Attribute("bossKillExp")!.Value : null, 20)); + AllowDirectActivateWithoutItem = ParseBool(node.Attribute("allowDirectActivateWithoutItem") != null ? node.Attribute("allowDirectActivateWithoutItem")!.Value : null, false); + } + + private void LoadLevels(string path) { + if (!File.Exists(path)) { + Logger.Warning("Missing survival level config: {Path}", path); + return; + } + + XDocument document = XDocument.Load(path); + IEnumerable elements = document.Root != null ? document.Root.Elements("survivalLevelExp") : Enumerable.Empty(); + foreach (XElement node in elements) { + if (!IsFeatureMatch(node)) { + continue; + } + int level = ParseInt(node.Attribute("level") != null ? node.Attribute("level")!.Value : null); + long reqExp = ParseLong(node.Attribute("reqExp") != null ? node.Attribute("reqExp")!.Value : null); + if (level <= 0) { + continue; + } + LevelThresholds[level] = reqExp; + } + } + + private void LoadRewards(string path, Dictionary target) { + if (!File.Exists(path)) { + Logger.Warning("Missing survival reward config: {Path}", path); + return; + } + + XDocument document = XDocument.Load(path); + IEnumerable elements = document.Root != null ? document.Root.Elements("survivalPassReward") : Enumerable.Empty(); + foreach (XElement node in elements) { + if (!IsFeatureMatch(node)) { + continue; + } + + int id = ParseInt(node.Attribute("id") != null ? node.Attribute("id")!.Value : null, 1); + if (id != 1) { + continue; + } + + int level = ParseInt(node.Attribute("level") != null ? node.Attribute("level")!.Value : null); + if (level <= 0) { + continue; + } + + var grants = new List(); + AddGrant(node, 1, grants); + AddGrant(node, 2, grants); + if (grants.Count == 0) { + continue; + } + + target[level] = new SurvivalRewardEntry(level, grants.ToArray()); + } + } + + private static bool IsFeatureMatch(XElement node) { + string feature = node.Attribute("feature") != null ? node.Attribute("feature")!.Value : string.Empty; + return string.IsNullOrEmpty(feature) || string.Equals(feature, "SurvivalContents03", StringComparison.OrdinalIgnoreCase); + } + + private static void AddGrant(XElement node, int index, IList grants) { + string type = node.Attribute("type" + index) != null ? node.Attribute("type" + index)!.Value : string.Empty; + if (string.IsNullOrWhiteSpace(type)) { + return; + } + + string idRaw = node.Attribute("id" + index) != null ? node.Attribute("id" + index)!.Value : string.Empty; + string valueRaw = node.Attribute("value" + index) != null ? node.Attribute("value" + index)!.Value : string.Empty; + string countRaw = node.Attribute("count" + index) != null ? node.Attribute("count" + index)!.Value : string.Empty; + + grants.Add(new SurvivalRewardGrant(type.Trim(), idRaw, valueRaw, countRaw)); + } + + private static string FindFile(string baseDir, string fileName) { + string[] candidates = new[] { + Path.Combine(baseDir, fileName), + Path.Combine(baseDir, "config", fileName), + Path.Combine(Directory.GetCurrentDirectory(), fileName), + Path.Combine(Directory.GetCurrentDirectory(), "config", fileName) + }; + foreach (string candidate in candidates) { + if (File.Exists(candidate)) { + return candidate; + } + } + return Path.Combine(baseDir, "config", fileName); + } + + private static int ParseInt(string? value, int fallback = 0) { + int parsed; + return int.TryParse(value, out parsed) ? parsed : fallback; + } + + private static long ParseLong(string? value, long fallback = 0) { + long parsed; + return long.TryParse(value, out parsed) ? parsed : fallback; + } + + private static bool ParseBool(string? value, bool fallback = false) { + if (string.IsNullOrWhiteSpace(value)) { + return fallback; + } + bool parsedBool; + if (bool.TryParse(value, out parsedBool)) { + return parsedBool; + } + int parsedInt; + if (int.TryParse(value, out parsedInt)) { + return parsedInt != 0; + } + return fallback; + } +} + +public sealed class SurvivalRewardEntry { + public int Level { get; private set; } + public SurvivalRewardGrant[] Grants { get; private set; } + + public SurvivalRewardEntry(int level, SurvivalRewardGrant[] grants) { + Level = level; + Grants = grants; + } +} + +public sealed class SurvivalRewardGrant { + public string Type { get; private set; } + public string IdRaw { get; private set; } + public string ValueRaw { get; private set; } + public string CountRaw { get; private set; } + + public SurvivalRewardGrant(string type, string idRaw, string valueRaw, string countRaw) { + Type = type; + IdRaw = idRaw ?? string.Empty; + ValueRaw = valueRaw ?? string.Empty; + CountRaw = countRaw ?? string.Empty; + } +} diff --git a/Maple2.Server.Game/Config/survivallevel.xml b/Maple2.Server.Game/Config/survivallevel.xml new file mode 100644 index 000000000..b9f99126d --- /dev/null +++ b/Maple2.Server.Game/Config/survivallevel.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Maple2.Server.Game/Config/survivalpassreward.xml b/Maple2.Server.Game/Config/survivalpassreward.xml new file mode 100644 index 000000000..5ee325074 --- /dev/null +++ b/Maple2.Server.Game/Config/survivalpassreward.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Maple2.Server.Game/Config/survivalpassreward_paid.xml b/Maple2.Server.Game/Config/survivalpassreward_paid.xml new file mode 100644 index 000000000..f1995a507 --- /dev/null +++ b/Maple2.Server.Game/Config/survivalpassreward_paid.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Maple2.Server.Game/Config/survivalserverconfig.xml b/Maple2.Server.Game/Config/survivalserverconfig.xml new file mode 100644 index 000000000..dc1ea1cbc --- /dev/null +++ b/Maple2.Server.Game/Config/survivalserverconfig.xml @@ -0,0 +1,4 @@ + + + + diff --git a/Maple2.Server.Game/Manager/SurvivalManager.cs b/Maple2.Server.Game/Manager/SurvivalManager.cs index fd0e506cf..a88ca6bc8 100644 --- a/Maple2.Server.Game/Manager/SurvivalManager.cs +++ b/Maple2.Server.Game/Manager/SurvivalManager.cs @@ -1,8 +1,11 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using Maple2.Database.Extensions; using Maple2.Database.Storage; using Maple2.Model.Enum; using Maple2.Model.Game; +using Maple2.Model.Metadata; +using Maple2.Server.Game.Config; +using Maple2.Server.Game.Model; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; using Maple2.Server.Game.Util; @@ -11,38 +14,41 @@ namespace Maple2.Server.Game.Manager; public sealed class SurvivalManager { - private readonly GameSession session; + private static readonly Lazy ConfigLoader = new Lazy(SurvivalPassXmlConfig.Load); + private static SurvivalPassXmlConfig Config { + get { return ConfigLoader.Value; } + } + private readonly GameSession session; private readonly ILogger logger = Log.Logger.ForContext(); private readonly ConcurrentDictionary> inventory; private readonly ConcurrentDictionary equip; private int SurvivalLevel { - get => session.Player.Value.Account.SurvivalLevel; - set => session.Player.Value.Account.SurvivalLevel = value; + get { return session.Player.Value.Account.SurvivalLevel; } + set { session.Player.Value.Account.SurvivalLevel = value; } } private long SurvivalExp { - get => session.Player.Value.Account.SurvivalExp; - set => session.Player.Value.Account.SurvivalExp = value; + get { return session.Player.Value.Account.SurvivalExp; } + set { session.Player.Value.Account.SurvivalExp = value; } } private int SurvivalSilverLevelRewardClaimed { - get => session.Player.Value.Account.SurvivalSilverLevelRewardClaimed; - set => session.Player.Value.Account.SurvivalSilverLevelRewardClaimed = value; + get { return session.Player.Value.Account.SurvivalSilverLevelRewardClaimed; } + set { session.Player.Value.Account.SurvivalSilverLevelRewardClaimed = value; } } private int SurvivalGoldLevelRewardClaimed { - get => session.Player.Value.Account.SurvivalGoldLevelRewardClaimed; - set => session.Player.Value.Account.SurvivalGoldLevelRewardClaimed = value; + get { return session.Player.Value.Account.SurvivalGoldLevelRewardClaimed; } + set { session.Player.Value.Account.SurvivalGoldLevelRewardClaimed = value; } } private bool ActiveGoldPass { - get => session.Player.Value.Account.ActiveGoldPass; - set => session.Player.Value.Account.ActiveGoldPass = value; + get { return session.Player.Value.Account.ActiveGoldPass; } + set { session.Player.Value.Account.ActiveGoldPass = value; } } - public SurvivalManager(GameSession session) { this.session = session; inventory = new ConcurrentDictionary>(); @@ -51,30 +57,372 @@ public SurvivalManager(GameSession session) { equip[type] = new Medal(0, type); inventory[type] = new Dictionary(); } + using GameStorage.Request db = session.GameStorage.Context(); List medals = db.GetMedals(session.CharacterId); - foreach (Medal medal in medals) { - if (!inventory.TryGetValue(medal.Type, out Dictionary? dict)) { + Dictionary dict; + if (!inventory.TryGetValue(medal.Type, out dict!)) { dict = new Dictionary(); inventory[medal.Type] = dict; } - dict[medal.Id] = medal; if (medal.Slot != -1) { equip[medal.Type] = medal; } } + + NormalizeProgress(); } - public void AddMedal(Item item) { + public void Load() { + NormalizeProgress(); + session.Send(SurvivalPacket.UpdateStats(session.Player.Value.Account, GetDisplayExp(), 0)); + session.Send(SurvivalPacket.LoadMedals(inventory, equip)); + } + + public void Save(GameStorage.Request db) { + var medals = inventory.Values.SelectMany(dict => dict.Values).ToArray(); + db.SaveMedals(session.CharacterId, medals); + } + + public long GetDisplayExp() { + long requiredExp = GetRequiredExpForCurrentLevel(); + if (requiredExp <= 0) { + return 0; + } + + if (SurvivalExp < 0) { + return 0; + } + + return Math.Min(SurvivalExp, requiredExp - 1); + } + + public void AddPassExp(int amount) { + if (amount <= 0) { + return; + } + + int oldLevel = SurvivalLevel; + SurvivalExp += amount; + + while (true) { + long requiredExp = GetRequiredExpForCurrentLevel(); + if (requiredExp <= 0 || SurvivalExp < requiredExp) { + break; + } + + SurvivalExp -= requiredExp; + SurvivalLevel++; + } + + if (oldLevel != SurvivalLevel) { + logger.Information("Survival level up account={AccountId} old={OldLevel} new={NewLevel} expInLevel={Exp}", session.Player.Value.Account.Id, oldLevel, SurvivalLevel, SurvivalExp); + } + + session.Send(SurvivalPacket.UpdateStats(session.Player.Value.Account, GetDisplayExp(), amount)); + } + + public int GetPassExpForNpc(FieldNpc npc) { + if (npc.Value.IsBoss) { + return Config.BossKillExp; + } + + NpcMetadataBasic basic = npc.Value.Metadata.Basic; + bool hasEliteTag = basic.MainTags.Any(tag => string.Equals(tag, "elite", StringComparison.OrdinalIgnoreCase) || string.Equals(tag, "champion", StringComparison.OrdinalIgnoreCase)) + || basic.SubTags.Any(tag => string.Equals(tag, "elite", StringComparison.OrdinalIgnoreCase) || string.Equals(tag, "champion", StringComparison.OrdinalIgnoreCase)); + bool isElite = basic.RareDegree >= 2 && hasEliteTag; + + return isElite ? Config.EliteKillExp : Config.MonsterKillExp; + } + + public bool TryUseGoldPassActivationItem(Item item) { + if (item == null) { + return false; + } + if (Config.ActivationItemId <= 0 || item.Id != Config.ActivationItemId) { + return false; + } + + logger.Information("Gold Pass activation requested by item use account={AccountId} itemId={ItemId} uid={Uid} amount={Amount}", + session.Player.Value.Account.Id, item.Id, item.Uid, item.Amount); + return TryActivateGoldPass(item); + } + + public bool TryUsePassExpItem(Item item) { + if (item == null || item.Metadata.Function?.Type != ItemFunction.SurvivalLevelExp) { + return false; + } + Dictionary parameters = XmlParseUtil.GetParameters(item.Metadata.Function?.Parameters); - if (!parameters.TryGetValue("id", out string? idStr) || !int.TryParse(idStr, out int id)) { + string expStr; + int expAmount; + if (!parameters.TryGetValue("exp", out expStr!) || !int.TryParse(expStr, out expAmount) || expAmount <= 0) { + return false; + } + + if (!session.Item.Inventory.Consume(item.Uid, 1)) { + return true; + } + + AddPassExp(expAmount); + return true; + } + + public bool TryUseSkinItem(Item item) { + if (item == null || item.Metadata.Function?.Type != ItemFunction.SurvivalSkin) { + return false; + } + + AddMedal(item); + session.Item.Inventory.Consume(item.Uid, 1); + return true; + } + + public bool TryActivateGoldPass() { + if (ActiveGoldPass) { + logger.Information("Gold Pass already active account={AccountId}", session.Player.Value.Account.Id); + return true; + } + + if (Config.ActivationItemId <= 0) { + if (!Config.AllowDirectActivateWithoutItem) { + logger.Information("Gold Pass activation rejected: no activation item configured and direct activation disabled account={AccountId}", session.Player.Value.Account.Id); + return false; + } + + ActivateGoldPass(); + logger.Information("Gold Pass activated without item account={AccountId}", session.Player.Value.Account.Id); + return true; + } + + Item? item = session.Item.Inventory.Find(Config.ActivationItemId).FirstOrDefault(); + if (item == null) { + logger.Information("Gold Pass activation failed: item not found account={AccountId} itemId={ItemId}", session.Player.Value.Account.Id, Config.ActivationItemId); + return false; + } + + return TryActivateGoldPass(item); + } + + + private bool TryActivateGoldPass(Item item) { + if (ActiveGoldPass) { + logger.Information("Gold Pass already active account={AccountId}", session.Player.Value.Account.Id); + return true; + } + if (item == null || item.Id != Config.ActivationItemId) { + logger.Information("Gold Pass activation failed: invalid item account={AccountId} itemId={ItemId} expected={ExpectedItemId}", + session.Player.Value.Account.Id, item != null ? item.Id : 0, Config.ActivationItemId); + return false; + } + if (item.Amount < Config.ActivationItemCount) { + logger.Information("Gold Pass activation failed: insufficient item count account={AccountId} itemId={ItemId} have={Have} need={Need}", + session.Player.Value.Account.Id, item.Id, item.Amount, Config.ActivationItemCount); + return false; + } + if (!session.Item.Inventory.Consume(item.Uid, Config.ActivationItemCount)) { + logger.Information("Gold Pass activation failed: consume returned false account={AccountId} itemId={ItemId} uid={Uid}", + session.Player.Value.Account.Id, item.Id, item.Uid); + return false; + } + + ActivateGoldPass(); + logger.Information("Gold Pass activated account={AccountId} by itemId={ItemId} uid={Uid}", session.Player.Value.Account.Id, item.Id, item.Uid); + return true; + } + + private void ActivateGoldPass() { + ActiveGoldPass = true; + session.Send(SurvivalPacket.UpdateStats(session.Player.Value.Account, GetDisplayExp(), 0)); + } + + public bool TryClaimNextReward() { + NormalizeProgress(); + + int nextFree = SurvivalSilverLevelRewardClaimed + 1; + if (CanClaimReward(nextFree, false)) { + return TryClaimReward(nextFree, false); + } + + int nextPaid = SurvivalGoldLevelRewardClaimed + 1; + if (CanClaimReward(nextPaid, true)) { + return TryClaimReward(nextPaid, true); + } + + return false; + } + + public bool TryClaimReward(int level, bool paidTrack) { + NormalizeProgress(); + if (!CanClaimReward(level, paidTrack)) { + return false; + } + + Dictionary rewards = paidTrack ? Config.PaidRewards : Config.FreeRewards; + SurvivalRewardEntry entry; + if (!rewards.TryGetValue(level, out entry!)) { + if (paidTrack) { + SurvivalGoldLevelRewardClaimed = level; + } else { + SurvivalSilverLevelRewardClaimed = level; + } + session.Send(SurvivalPacket.UpdateStats(session.Player.Value.Account, GetDisplayExp(), 0)); + return true; + } + + foreach (SurvivalRewardGrant grant in entry.Grants) { + if (!TryGrantReward(grant)) { + return false; + } + } + + if (paidTrack) { + SurvivalGoldLevelRewardClaimed = level; + } else { + SurvivalSilverLevelRewardClaimed = level; + } + + session.Send(SurvivalPacket.UpdateStats(session.Player.Value.Account, GetDisplayExp(), 0)); + return true; + } + + private bool CanClaimReward(int level, bool paidTrack) { + if (level <= 0 || level > SurvivalLevel) { + return false; + } + if (paidTrack && !ActiveGoldPass) { + return false; + } + return paidTrack ? level == SurvivalGoldLevelRewardClaimed + 1 : level == SurvivalSilverLevelRewardClaimed + 1; + } + + private long GetLevelThreshold(int level) { + long threshold; + return Config.LevelThresholds.TryGetValue(level, out threshold) ? threshold : 0; + } + + private long GetRequiredExpForCurrentLevel() { + long currentThreshold = GetLevelThreshold(SurvivalLevel); + long nextThreshold = GetNextLevelThreshold(SurvivalLevel); + long requiredExp = nextThreshold - currentThreshold; + return Math.Max(0, requiredExp); + } + + private long GetNextLevelThreshold(int level) { + long threshold; + return Config.LevelThresholds.TryGetValue(level + 1, out threshold) ? threshold : GetLevelThreshold(level); + } + + private void NormalizeProgress() { + if (SurvivalLevel <= 0) { + SurvivalLevel = 1; + } + if (SurvivalExp < 0) { + SurvivalExp = 0; + } + + while (true) { + long requiredExp = GetRequiredExpForCurrentLevel(); + if (requiredExp <= 0 || SurvivalExp < requiredExp) { + break; + } + + SurvivalExp -= requiredExp; + SurvivalLevel++; + } + + if (SurvivalSilverLevelRewardClaimed > SurvivalLevel) { + SurvivalSilverLevelRewardClaimed = SurvivalLevel; + } + if (SurvivalGoldLevelRewardClaimed > SurvivalLevel) { + SurvivalGoldLevelRewardClaimed = SurvivalLevel; + } + } + + private bool TryGrantReward(SurvivalRewardGrant grant) { + string type = grant.Type.Trim(); + if (string.Equals(type, "additionalEffect", StringComparison.OrdinalIgnoreCase)) { + logger.Information("Skipping unsupported survival additionalEffect reward id={IdRaw}", grant.IdRaw); + return true; + } + + int[] ids = ParseIntArray(grant.IdRaw); + int[] values = ParseIntArray(grant.ValueRaw); + int[] counts = ParseIntArray(grant.CountRaw); + + if (string.Equals(type, "genderItem", StringComparison.OrdinalIgnoreCase)) { + int chosenIndex = session.Player.Value.Character.Gender == Gender.Female ? 1 : 0; + int itemId = GetValueAt(ids, chosenIndex); + int rarity = Math.Max(1, GetValueAt(values, chosenIndex, 1)); + int count = Math.Max(1, GetValueAt(counts, chosenIndex, 1)); + return TryGrantItem(itemId, rarity, count); + } + + if (string.Equals(type, "item", StringComparison.OrdinalIgnoreCase)) { + int itemId = GetValueAt(ids, 0); + int rarity = Math.Max(1, GetValueAt(values, 0, 1)); + int count = Math.Max(1, GetValueAt(counts, 0, 1)); + return TryGrantItem(itemId, rarity, count); + } + + logger.Information("Skipping unsupported survival reward type={Type}", type); + return true; + } + + private bool TryGrantItem(int itemId, int rarity, int amount) { + if (itemId <= 0) { + return true; + } + if (session.Field == null) { + return false; + } + ItemMetadata metadata; + if (!session.ItemMetadata.TryGet(itemId, out metadata!)) { + logger.Warning("Missing item metadata for survival reward itemId={ItemId}", itemId); + return false; + } + + Item? item = session.Field.ItemDrop.CreateItem(itemId, rarity, amount); + if (item == null) { + logger.Warning("Failed to create survival reward item itemId={ItemId}", itemId); + return false; + } + return session.Item.Inventory.Add(item, true); + } + + private static int[] ParseIntArray(string csv) { + if (string.IsNullOrWhiteSpace(csv)) { + return Array.Empty(); + } + return csv.Split(',').Select(part => { + int parsed; + return int.TryParse(part, out parsed) ? parsed : 0; + }).ToArray(); + } + + private static int GetValueAt(int[] values, int index, int fallback = 0) { + if (values.Length == 0) { + return fallback; + } + if (index < values.Length) { + return values[index]; + } + return values[values.Length - 1]; + } + + public void AddMedal(Item item) { + Dictionary parameters = XmlParseUtil.GetParameters(item.Metadata.Function != null ? item.Metadata.Function.Parameters : null); + string idStr; + int id; + if (!parameters.TryGetValue("id", out idStr!) || !int.TryParse(idStr, out id)) { logger.Warning("Failed to add medal: missing or invalid ID parameter"); return; } - if (!parameters.TryGetValue("type", out string? typeStr)) { + string typeStr; + if (!parameters.TryGetValue("type", out typeStr!)) { logger.Warning("Failed to add medal: missing or invalid type parameter"); return; } @@ -83,20 +431,22 @@ public void AddMedal(Item item) { "effectTail" => MedalType.Tail, "gliding" => MedalType.Gliding, "riding" => MedalType.Riding, - _ => throw new InvalidOperationException($"Invalid medal type: {typeStr}"), + _ => throw new InvalidOperationException("Invalid medal type: " + typeStr), }; long expiryTime = DateTime.MaxValue.ToEpochSeconds() - 1; - // Get expiration - if (parameters.TryGetValue("durationSec", out string? durationStr) && int.TryParse(durationStr, out int durationSec)) { - expiryTime = (long) (DateTime.Now.ToUniversalTime() - DateTime.UnixEpoch).TotalSeconds + durationSec; - } else if (parameters.TryGetValue("endDate", out string? endDateStr) && DateTime.TryParseExact(endDateStr, "yyyy-MM-dd-HH-mm-ss", null, System.Globalization.DateTimeStyles.None, out DateTime endDate)) { - //2018-10-02-00-00-00 - expiryTime = endDate.ToEpochSeconds(); + string durationStr; + if (parameters.TryGetValue("durationSec", out durationStr!) && int.TryParse(durationStr, out int durationSec)) { + expiryTime = (long)(DateTime.Now.ToUniversalTime() - DateTime.UnixEpoch).TotalSeconds + durationSec; + } else { + string endDateStr; + if (parameters.TryGetValue("endDate", out endDateStr!) && DateTime.TryParseExact(endDateStr, "yyyy-MM-dd-HH-mm-ss", null, System.Globalization.DateTimeStyles.None, out DateTime endDate)) { + expiryTime = endDate.ToEpochSeconds(); + } } - // Check if medal already exists - if (inventory[type].TryGetValue(id, out Medal? existing)) { + Medal existing; + if (inventory[type].TryGetValue(id, out existing!)) { existing.ExpiryTime = Math.Min(existing.ExpiryTime + expiryTime, DateTime.MaxValue.ToEpochSeconds() - 1); session.Send(SurvivalPacket.LoadMedals(inventory, equip)); return; @@ -107,13 +457,13 @@ public void AddMedal(Item item) { return; } - if (!inventory.TryGetValue(medal.Type, out Dictionary? dict)) { + Dictionary dict; + if (!inventory.TryGetValue(medal.Type, out dict!)) { dict = new Dictionary(); inventory[medal.Type] = dict; } dict[medal.Id] = medal; - session.Send(SurvivalPacket.LoadMedals(inventory, equip)); } @@ -128,24 +478,22 @@ public bool Equip(MedalType type, int id) { return true; } - if (!inventory[type].TryGetValue(id, out Medal? medal)) { + Medal medal; + if (!inventory[type].TryGetValue(id, out medal!)) { return false; } - // medal is already equipped if (medal.Slot != -1) { return false; } - // unequip existing medal if (equip[type].Id != 0) { Medal equipped = equip[type]; equipped.Slot = -1; } equip[type] = medal; - medal.Slot = (short) type; - + medal.Slot = (short)type; session.Send(SurvivalPacket.LoadMedals(inventory, equip)); return true; } @@ -157,21 +505,8 @@ private void Unequip(MedalType type) { } private Medal? CreateMedal(int id, MedalType type, long expiryTime) { - var medal = new Medal(id, type) { - ExpiryTime = expiryTime, - }; - + var medal = new Medal(id, type) { ExpiryTime = expiryTime }; using GameStorage.Request db = session.GameStorage.Context(); return db.CreateMedal(session.CharacterId, medal); } - - public void Load() { - session.Send(SurvivalPacket.UpdateStats(session.Player.Value.Account)); - session.Send(SurvivalPacket.LoadMedals(inventory, equip)); - } - - public void Save(GameStorage.Request db) { - var medals = inventory.Values.SelectMany(dict => dict.Values).ToArray(); - db.SaveMedals(session.CharacterId, medals); - } } diff --git a/Maple2.Server.Game/Maple2.Server.Game.csproj b/Maple2.Server.Game/Maple2.Server.Game.csproj index 9ae8babff..1efc53224 100644 --- a/Maple2.Server.Game/Maple2.Server.Game.csproj +++ b/Maple2.Server.Game/Maple2.Server.Game.csproj @@ -52,5 +52,8 @@ Always + + Always + \ No newline at end of file diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index 42874181e..d6c567ea7 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -519,6 +519,7 @@ private void HandleDamageDealers() { DropIndividualLoot(player); GiveExp(player); + player.Session.Survival.AddPassExp(player.Session.Survival.GetPassExpForNpc(this)); player.Session.ConditionUpdate(ConditionType.npc, codeLong: Value.Id, targetLong: Field.MapId); foreach (string tag in Value.Metadata.Basic.MainTags) { @@ -541,6 +542,7 @@ private void HandleDamageDealers() { } GiveExp(player); + player.Session.Survival.AddPassExp(player.Session.Survival.GetPassExpForNpc(this)); player.Session.ConditionUpdate(ConditionType.npc, codeLong: Value.Id, targetLong: Field.MapId); foreach (string tag in Value.Metadata.Basic.MainTags) { diff --git a/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs b/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs index d0dddf31b..a313b194d 100644 --- a/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs @@ -44,6 +44,18 @@ public override void Handle(GameSession session, IByteReader packet) { return; } + if (session.Survival.TryUseGoldPassActivationItem(item)) { + return; + } + + if (session.Survival.TryUsePassExpItem(item)) { + return; + } + + if (session.Survival.TryUseSkinItem(item)) { + return; + } + switch (item.Metadata.Function?.Type) { case ItemFunction.BlueprintImport: HandleBlueprintImport(session, item); diff --git a/Maple2.Server.Game/PacketHandlers/SurvivalHandler.cs b/Maple2.Server.Game/PacketHandlers/SurvivalHandler.cs index 54a3de6b1..a10646935 100644 --- a/Maple2.Server.Game/PacketHandlers/SurvivalHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/SurvivalHandler.cs @@ -1,32 +1,55 @@ -using Maple2.Model.Enum; +using Maple2.Model.Enum; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Game.PacketHandlers.Field; using Maple2.Server.Game.Session; +using Serilog; namespace Maple2.Server.Game.PacketHandlers; public class SurvivalHandler : FieldPacketHandler { + private static readonly ILogger SurvivalLogger = Log.Logger.ForContext(); + public override RecvOp OpCode => RecvOp.Survival; private enum Command : byte { + JoinSolo = 0, + WithdrawSolo = 1, Equip = 8, + ClaimRewards = 35, } public override void Handle(GameSession session, IByteReader packet) { - var command = packet.Read(); - + byte rawCommand = packet.ReadByte(); + SurvivalLogger.Information("Survival command received cmd={Command}", rawCommand); + Command command = (Command) rawCommand; switch (command) { case Command.Equip: HandleEquip(session, packet); return; + case Command.ClaimRewards: + HandleClaimRewards(session, packet); + return; + case Command.JoinSolo: + session.Survival.TryActivateGoldPass(); + return; + case Command.WithdrawSolo: + return; + default: + session.Survival.TryActivateGoldPass(); + return; } } private static void HandleEquip(GameSession session, IByteReader packet) { - var slot = packet.Read(); + MedalType slot = packet.Read(); int medalId = packet.ReadInt(); - session.Survival.Equip(slot, medalId); } + + private static void HandleClaimRewards(GameSession session, IByteReader packet) { + if (!session.Survival.TryClaimNextReward()) { + SurvivalLogger.Information("Unhandled Survival claim: no claimable rewards available."); + } + } } diff --git a/Maple2.Server.Game/Packets/SurvivalPacket.cs b/Maple2.Server.Game/Packets/SurvivalPacket.cs index 33f127828..e56575fc7 100644 --- a/Maple2.Server.Game/Packets/SurvivalPacket.cs +++ b/Maple2.Server.Game/Packets/SurvivalPacket.cs @@ -1,4 +1,4 @@ -using Maple2.Model.Enum; +using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; @@ -25,34 +25,33 @@ private enum Command : byte { ClaimRewards = 35, } - public static ByteWriter UpdateStats(Account account, long expGained = 0) { + public static ByteWriter UpdateStats(Account account, long displayExp, long expGained = 0) { var pWriter = Packet.Of(SendOp.Survival); - pWriter.Write(Command.UpdateStats); + pWriter.WriteByte((byte)Command.UpdateStats); pWriter.WriteLong(account.Id); - pWriter.WriteInt(); + pWriter.WriteInt(0); pWriter.WriteBool(account.ActiveGoldPass); - pWriter.WriteLong(account.SurvivalExp); + pWriter.WriteLong(displayExp); pWriter.WriteInt(account.SurvivalLevel); pWriter.WriteInt(account.SurvivalSilverLevelRewardClaimed); pWriter.WriteInt(account.SurvivalGoldLevelRewardClaimed); pWriter.WriteLong(expGained); - return pWriter; } public static ByteWriter LoadMedals(IDictionary> inventory, IDictionary equips) { var pWriter = Packet.Of(SendOp.Survival); - pWriter.Write(Command.LoadMedals); - pWriter.WriteByte((byte) inventory.Keys.Count); - foreach ((MedalType type, Dictionary medals) in inventory) { - pWriter.WriteInt(equips[type].Id); - pWriter.WriteInt(medals.Count); - foreach (Medal medal in medals.Values) { + pWriter.WriteByte((byte)Command.LoadMedals); + pWriter.WriteByte((byte)inventory.Keys.Count); + foreach (KeyValuePair> entry in inventory) { + Medal equipped = equips.ContainsKey(entry.Key) ? equips[entry.Key] : new Medal(0, entry.Key); + pWriter.WriteInt(equipped.Id); + pWriter.WriteInt(entry.Value.Count); + foreach (Medal medal in entry.Value.Values) { pWriter.WriteInt(medal.Id); - pWriter.WriteLong(long.MaxValue); + pWriter.WriteLong(medal.ExpiryTime <= 0 ? long.MaxValue : medal.ExpiryTime); } } - return pWriter; } } diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index d4644d6a6..599830dc7 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -283,7 +283,7 @@ public bool EnterServer(long accountId, Guid machineId, MigrateInResponse migrat Logger.Warning(ex, "Failed to load cache player config"); } - Send(SurvivalPacket.UpdateStats(player.Account)); + Survival.Load(); Send(TimeSyncPacket.Reset(DateTimeOffset.UtcNow)); Send(TimeSyncPacket.Set(DateTimeOffset.UtcNow)); diff --git a/config/survivallevel.xml b/config/survivallevel.xml new file mode 100644 index 000000000..b9f99126d --- /dev/null +++ b/config/survivallevel.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/survivallevelreward.xml b/config/survivallevelreward.xml new file mode 100644 index 000000000..f0bb69dba --- /dev/null +++ b/config/survivallevelreward.xml @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/survivalpassreward.xml b/config/survivalpassreward.xml new file mode 100644 index 000000000..b16b51b57 --- /dev/null +++ b/config/survivalpassreward.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/survivalpassreward_paid.xml b/config/survivalpassreward_paid.xml new file mode 100644 index 000000000..3b818ba04 --- /dev/null +++ b/config/survivalpassreward_paid.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/survivalserverconfig.xml b/config/survivalserverconfig.xml new file mode 100644 index 000000000..ddc73e746 --- /dev/null +++ b/config/survivalserverconfig.xml @@ -0,0 +1,4 @@ + + + +