diff --git a/Maple2.Database/Storage/Game/GameStorage.User.cs b/Maple2.Database/Storage/Game/GameStorage.User.cs index edc3a208b..8aa5dd983 100644 --- a/Maple2.Database/Storage/Game/GameStorage.User.cs +++ b/Maple2.Database/Storage/Game/GameStorage.User.cs @@ -128,9 +128,15 @@ from outdoor in plot.DefaultIfEmpty() return null; } + Tuple guild = Context.GuildMember + .Where(member => member.CharacterId == characterId) + .Join(Context.Guild, member => member.GuildId, guild => guild.Id, + (member, guild) => new Tuple(guild.Id, guild.Name)) + .FirstOrDefault() ?? new Tuple(0, string.Empty); + AchievementInfo achievementInfo = GetAchievementInfo(result.character.AccountId, result.character.Id); IList clubs = ListClubs(result.character.Id); - return BuildPlayerInfo(result.character, result.indoor, result.outdoor, achievementInfo, result.PremiumTime, clubs); + return BuildPlayerInfo(result.character, result.indoor, result.outdoor, achievementInfo, guild.Item1, guild.Item2, result.PremiumTime, clubs); } public Home? GetHome(long ownerId) { diff --git a/Maple2.Database/Storage/Game/GameStorage.cs b/Maple2.Database/Storage/Game/GameStorage.cs index db3e50a15..09dbf18d6 100644 --- a/Maple2.Database/Storage/Game/GameStorage.cs +++ b/Maple2.Database/Storage/Game/GameStorage.cs @@ -46,11 +46,13 @@ public Request(GameStorage game, Ms2Context context, ILogger logger) : base(cont this.game = game; } - private static PlayerInfo BuildPlayerInfo(Model.Character character, UgcMap indoor, UgcMap? outdoor, AchievementInfo achievementInfo, long premiumTime, IList clubs) { + private static PlayerInfo BuildPlayerInfo(Model.Character character, UgcMap indoor, UgcMap? outdoor, AchievementInfo achievementInfo, long guildId, string guildName, long premiumTime, IList clubs) { if (outdoor == null) { return new PlayerInfo(character, indoor.Name, achievementInfo, clubs) { PremiumTime = premiumTime, LastOnlineTime = character.LastModified.ToEpochSeconds(), + GuildId = guildId, + GuildName = guildName, }; } @@ -61,6 +63,8 @@ private static PlayerInfo BuildPlayerInfo(Model.Character character, UgcMap indo ApartmentNumber = outdoor.ApartmentNumber, PlotExpiryTime = outdoor.ExpiryTime.ToUnixTimeSeconds(), LastOnlineTime = character.LastModified.ToEpochSeconds(), + GuildId = guildId, + GuildName = guildName, }; } } diff --git a/Maple2.Database/Storage/Metadata/ServerTableMetadataStorage.cs b/Maple2.Database/Storage/Metadata/ServerTableMetadataStorage.cs index 941ace984..cc6c41286 100644 --- a/Maple2.Database/Storage/Metadata/ServerTableMetadataStorage.cs +++ b/Maple2.Database/Storage/Metadata/ServerTableMetadataStorage.cs @@ -16,6 +16,7 @@ public class ServerTableMetadataStorage { private readonly Lazy userStatTable; private readonly Lazy individualDropItemTable; private readonly Lazy prestigeExpTable; + private readonly Lazy prestigeIdExpTable; private readonly Lazy timeEventTable; private readonly Lazy gameEventTable; private readonly Lazy oxQuizTable; @@ -36,6 +37,7 @@ public class ServerTableMetadataStorage { public UserStatTable UserStatTable => userStatTable.Value; public IndividualDropItemTable IndividualDropItemTable => individualDropItemTable.Value; public PrestigeExpTable PrestigeExpTable => prestigeExpTable.Value; + public PrestigeIdExpTable PrestigeIdExpTable => prestigeIdExpTable.Value; public TimeEventTable TimeEventTable => timeEventTable.Value; public GameEventTable GameEventTable => gameEventTable.Value; public OxQuizTable OxQuizTable => oxQuizTable.Value; @@ -57,6 +59,7 @@ public ServerTableMetadataStorage(MetadataContext context) { userStatTable = Retrieve(context, "userStat*.xml"); individualDropItemTable = Retrieve(context, "individualItemDrop.xml"); prestigeExpTable = Retrieve(context, "adventureExpTable.xml"); + prestigeIdExpTable = Retrieve(context, "adventureExpIdTable.xml"); timeEventTable = Retrieve(context, "timeEventData.xml"); gameEventTable = Retrieve(context, "gameEvent.xml"); oxQuizTable = Retrieve(context, "oxQuiz.xml"); diff --git a/Maple2.Database/Storage/Metadata/TableMetadataStorage.cs b/Maple2.Database/Storage/Metadata/TableMetadataStorage.cs index 53084a390..9b2e4c758 100644 --- a/Maple2.Database/Storage/Metadata/TableMetadataStorage.cs +++ b/Maple2.Database/Storage/Metadata/TableMetadataStorage.cs @@ -45,6 +45,7 @@ public class TableMetadataStorage { private readonly Lazy survivalSkinInfoTable; private readonly Lazy bannerTable; private readonly Lazy weddingTable; + private readonly Lazy rewardContentTable; private readonly Lazy enchantScrollTable; private readonly Lazy itemRemakeScrollTable; @@ -105,6 +106,7 @@ public class TableMetadataStorage { public SurvivalSkinInfoTable SurvivalSkinInfoTable => survivalSkinInfoTable.Value; public BannerTable BannerTable => bannerTable.Value; public WeddingTable WeddingTable => weddingTable.Value; + public RewardContentTable RewardContentTable => rewardContentTable.Value; public EnchantScrollTable EnchantScrollTable => enchantScrollTable.Value; public ItemRemakeScrollTable ItemRemakeScrollTable => itemRemakeScrollTable.Value; @@ -166,6 +168,7 @@ public TableMetadataStorage(MetadataContext context) { survivalSkinInfoTable = Retrieve(context, "maplesurvivalskininfo.xml"); bannerTable = Retrieve(context, "banner.xml"); weddingTable = Retrieve(context, "wedding*.xml"); + rewardContentTable = Retrieve(context, "rewardcontent*.xml"); enchantScrollTable = Retrieve(context, "enchantscroll.xml"); itemRemakeScrollTable = Retrieve(context, "itemremakescroll.xml"); itemRepackingScrollTable = Retrieve(context, "itemrepackingscroll.xml"); diff --git a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs index b963fdabe..b431adec5 100644 --- a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs +++ b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs @@ -43,6 +43,7 @@ protected override IEnumerable Map() { yield return new ServerTableMetadata { Name = "userStat*.xml", Table = ParseUserStat() }; yield return new ServerTableMetadata { Name = "individualItemDrop.xml", Table = ParseIndividualItemDropTable() }; yield return new ServerTableMetadata { Name = "adventureExpTable.xml", Table = ParsePrestigeExpTable() }; + yield return new ServerTableMetadata { Name = "adventureIdExpTable.xml", Table = ParsePrestigeIdExpTable() }; yield return new ServerTableMetadata { Name = "timeEventData.xml", Table = ParseTimeEventTable() }; yield return new ServerTableMetadata { Name = "gameEvent.xml", Table = ParseGameEventTable() }; yield return new ServerTableMetadata { Name = "OxQuiz.xml", Table = ParseOxQuizTable() }; @@ -63,7 +64,7 @@ private InstanceFieldTable ParseInstanceField() { InstanceFieldMetadata instanceFieldMetadata = new( MapId: fieldId, - Type: (InstanceType) instanceField.instanceType, + Type: Enum.TryParse(instanceField.instanceType.ToString(), out InstanceType instanceType) ? instanceType : InstanceType.none, InstanceId: instanceId, BackupSourcePortal: instanceField.backupSourcePortal, PoolCount: instanceField.poolCount, @@ -642,6 +643,18 @@ private PrestigeExpTable ParsePrestigeExpTable() { return new PrestigeExpTable(results); } + private PrestigeIdExpTable ParsePrestigeIdExpTable() { + var results = new Dictionary(); + foreach ((int id, AdventureIdExpTable table) in parser.ParseAdventureIdExp()) { + results.Add(id, new PrestigeIdExpTable.Entry( + Id: id, + Value: table.value, + Type: ToExpType(table.expType))); + } + + return new PrestigeIdExpTable(results); + } + private static ExpType ToExpType(AdventureExpType type) { return type switch { AdventureExpType.Exp_MapCommon => ExpType.mapCommon, diff --git a/Maple2.File.Ingest/Mapper/TableMapper.cs b/Maple2.File.Ingest/Mapper/TableMapper.cs index 269b22a16..922d2887a 100644 --- a/Maple2.File.Ingest/Mapper/TableMapper.cs +++ b/Maple2.File.Ingest/Mapper/TableMapper.cs @@ -79,6 +79,7 @@ protected override IEnumerable Map() { yield return new TableMetadata { Name = "banner.xml", Table = ParseBanner() }; yield return new TableMetadata { Name = "masteryugchousing.xml", Table = ParseMasteryUgcHousingTable() }; yield return new TableMetadata { Name = "ugchousingpointreward.xml", Table = ParseUgcHousingPointRewardTable() }; + yield return new TableMetadata { Name = "rewardcontent*.xml", Table = ParseRewardContentTable() }; // Marriage/Wedding yield return new TableMetadata { Name = "wedding*.xml", Table = ParseWeddingTable() }; @@ -1629,4 +1630,58 @@ private DungeonRankRewardTable ParseDungeonRankReward() { return new DungeonRankRewardTable(results); } + + private RewardContentTable ParseRewardContentTable() { + var baseResults = new Dictionary(); + foreach ((int id, RewardContent rewardContent) in parser.ParseRewardContent()) { + baseResults.Add(id, new RewardContentTable.Base( + Id: id, + ExpTableId: rewardContent.expTableID, + MesoTableId: rewardContent.mesoTableID, + ExpFactor: rewardContent.expFactor, + MesoFactor: rewardContent.mesoFactor, + ItemTableId: rewardContent.itemTableID, + PrestigeExpTableId: rewardContent.adventureExpTableID)); + } + + var itemResults = new Dictionary(); + foreach ((int id, RewardContentItem content) in parser.ParseRewardContentItem()) { + List itemData = []; + foreach (RewardContentValue value in content.v) { + List items = []; + foreach (RewardContentItemEntry item in value.item) { + items.Add(new RewardItem(item.itemID, (short) item.grade, item.count)); + } + itemData.Add(new RewardContentTable.Item.Data( + MinLevel: value.minLevel, + MaxLevel: value.maxLevel, + RewardItems: items.ToArray())); + } + itemResults.Add(id, new RewardContentTable.Item( + Id: id, + ItemData: itemData.ToArray())); + } + + var mesoStaticResults = new Dictionary(); + foreach ((int id, RewardContentMesoStatic reward) in parser.ParseRewardContentMesoStatic()) { + mesoStaticResults.Add(id, reward.v.FirstOrDefault()?.meso ?? 0); + } + + var mesoResults = new Dictionary>(); + foreach ((int id, RewardContentMeso reward) in parser.ParseRewardContentMeso()) { + var entries = new Dictionary(); + foreach (RewardContentMesoValue value in reward.v) { + entries.Add(value.level, value.meso); + } + + mesoResults.Add(id, entries); + } + + var expStaticResults = new Dictionary(); + foreach ((int id, RewardContentExpStatic reward) in parser.ParseRewardContentExpStatic()) { + expStaticResults.Add(id, reward.@base.FirstOrDefault()?.exp ?? 0); + } + + return new RewardContentTable(baseResults, itemResults, mesoStaticResults, mesoResults, expStaticResults); + } } diff --git a/Maple2.File.Ingest/Utils/PyParameter.cs b/Maple2.File.Ingest/Utils/PyParameter.cs index b0f4acfb0..ee32d9557 100644 --- a/Maple2.File.Ingest/Utils/PyParameter.cs +++ b/Maple2.File.Ingest/Utils/PyParameter.cs @@ -144,7 +144,7 @@ public string FormatValue(bool validate = true) { //"2" => "", "3" => "BannerType.Winner", "4" => "BannerType.Lose", - "5" => "BannerType.Fail", + "5" => "BannerType.GameOver", "6" => "BannerType.Bonus", "7" => "BannerType.Success", _ => throw new ArgumentException($"Unexpected BannerType: {value}"), diff --git a/Maple2.File.Ingest/Utils/TriggerDefinitionOverride.cs b/Maple2.File.Ingest/Utils/TriggerDefinitionOverride.cs index bce821926..90b37fdca 100644 --- a/Maple2.File.Ingest/Utils/TriggerDefinitionOverride.cs +++ b/Maple2.File.Ingest/Utils/TriggerDefinitionOverride.cs @@ -261,7 +261,7 @@ static TriggerDefinitionOverride() { }; ActionOverride["dungeon_clear"] = new TriggerDefinitionOverride("dungeon_clear") { Names = BuildNameOverride(), - Types = BuildTypeOverride(("uiType", Str, "None")), + Types = BuildTypeOverride(("uiType", Str, null)), }; ActionOverride["dungeon_clear_round"] = new TriggerDefinitionOverride("dungeon_clear_round") { Names = BuildNameOverride(), diff --git a/Maple2.Model/Enum/Dungeon.cs b/Maple2.Model/Enum/Dungeon.cs index a7f5470a0..dcca7d4dc 100644 --- a/Maple2.Model/Enum/Dungeon.cs +++ b/Maple2.Model/Enum/Dungeon.cs @@ -75,3 +75,69 @@ public enum DungeonRoomModify : byte { [Description("s_room_dungeon_record_notify_change_expert - You're now a veteran in {0}. Collect Dungeon Helper rewards by clearing the dungeon with rookies!")] ChangeToExpert = 4, } + +public enum DungeonAccumulationRecordType { + [Description("s_dungeon_record_accum_damage - Total Damage: {0}")] + TotalDamage = 0, + [Description("s_dungeon_record_accum_heal - Total Healing: {0}")] + TotalHealing = 1, + [Description("s_dungeon_record_accum_hit_count - Total Hit Count: {0}")] + TotalHitCount = 2, + [Description("s_dungeon_record_boss_last_hit - Boss Final Blows: {0}")] + BossFinalBlows = 3, + [Description("s_dungeon_record_accum_move_distance - Total Move Distance: {0}")] + TotalMoveDistance = 4, + [Description("s_dungeon_record_accum_critical_damage - Total Critical Damage: {0}")] + TotalCriticalDamage = 5, + [Description("s_dungeon_record_max_critial_damage - Maximum Critical Damage: {0}")] + MaximumCriticalDamage = 6, + [Description("s_dungeon_record_accum_monster_kill - Defeated Monsters: {0}")] + DefeatedMonsters = 7, + [Description("s_dungeon_record_accum_be_hit_count - Incoming Damage: {0}")] + IncomingDamage = 8, + [Description("s_dungeon_record_accum_default_skill_damage - Basic Attack Damage: {0}")] + BasicAttackDamage = 9, +} + +public enum DungeonState : byte { + None = 0, + [Description("s_room_dungeon_clear - Dungeon Cleared.")] + Clear = 1, + [Description("s_room_dungeon_fail - Dungeon Failed.")] + Fail = 2, +} + +public enum DungeonGrade { + None = -1, + F = 0, + C = 1, + B = 2, + A = 3, + S = 4, + SPlus = 5, +} + +public enum DungeonRewardType : byte { + Meso = 1, + Exp = 2, + Prestige = 3, +} + +[Flags] +public enum DungeonBonusFlag : byte { + None = 0, + Clears = 2, + Event = 4, + Rank = 8, + Helper = 16, + MutualHelp = 32, + Mentor = 64, + Returning = 128, +} + +[Flags] +public enum DungeonBonusFlag2 : byte { + None = 0, + ReturningPlayerParty = 1, + NormalHardAdventure = 2, +} diff --git a/Maple2.Model/Enum/FieldType.cs b/Maple2.Model/Enum/FieldType.cs new file mode 100644 index 000000000..79a3933e9 --- /dev/null +++ b/Maple2.Model/Enum/FieldType.cs @@ -0,0 +1,7 @@ +namespace Maple2.Model.Enum; + +public enum FieldType : byte { + Default = 0, + Random = 1, // Random Rooms like Pocket Realms + Dungeon = 2 +} diff --git a/Maple2.Model/Enum/InstanceType.cs b/Maple2.Model/Enum/InstanceType.cs index e044aa8b1..5e65a2919 100644 --- a/Maple2.Model/Enum/InstanceType.cs +++ b/Maple2.Model/Enum/InstanceType.cs @@ -6,15 +6,15 @@ public enum InstanceType : byte { channelScale = 2, massiveEvent = 3, ugcMap = 4, - GameMaker = 5, - GuildPvp = 6, - GuildEvent = 7, - RankingPvp = 8, + GameMaker = 5, //UGD + GuildEvent = 6, + GuildPvp = 7, // Not confirmed + DungeonLobby = 8, GuildHouse = 9, - GuildVsGame = 10, - DungeonLobby = 11, - MapleSurvival = 12, - MapleSurvivalSquad = 13, + GuildVsGame = 10, // Not confirmed + RankingPvp = 11, // Not confirmed + MapleSurvival = 12, // Not confirmed + MapleSurvivalSquad = 13, // Not confirmed FieldWar = 14, WeddingHall = 15, } diff --git a/Maple2.Model/Error/DungeonRoomError.cs b/Maple2.Model/Error/DungeonRoomError.cs index 9220771cc..4d49d8c4c 100644 --- a/Maple2.Model/Error/DungeonRoomError.cs +++ b/Maple2.Model/Error/DungeonRoomError.cs @@ -30,7 +30,7 @@ public enum DungeonRoomError { s_room_dungeon_canEnterDayOfWeeks = 12, [Description("You are already in the hall.")] s_room_dungeon_AlreadyChaosHall = 13, - [Description("You are already $map:63000063$.")] + [Description("You are already Within the Anomaly.")] s_room_dungeon_AlreadyLapentaHall = 14, [Description("You are already in the Queen's Parlor.")] s_room_dungeon_AlreadyColosseumHall = 15, diff --git a/Maple2.Model/Game/Dungeon/DungeonRoomRecord.cs b/Maple2.Model/Game/Dungeon/DungeonRoomRecord.cs new file mode 100644 index 000000000..9b841561c --- /dev/null +++ b/Maple2.Model/Game/Dungeon/DungeonRoomRecord.cs @@ -0,0 +1,17 @@ +using System.Collections.Concurrent; +using Maple2.Model.Enum; +using Maple2.Model.Metadata; + +namespace Maple2.Model.Game.Dungeon; + +public class DungeonRoomRecord { + public ConcurrentDictionary UserResults = []; + public readonly DungeonRoomTable.DungeonRoomMetadata Metadata; + public long StartTick { get; set; } + public long EndTick { get; set; } + public DungeonState State { get; set; } = DungeonState.None; + + public DungeonRoomRecord(DungeonRoomTable.DungeonRoomMetadata metadata) { + Metadata = metadata; + } +} diff --git a/Maple2.Model/Game/Dungeon/DungeonUserRecord.cs b/Maple2.Model/Game/Dungeon/DungeonUserRecord.cs new file mode 100644 index 000000000..df608c7da --- /dev/null +++ b/Maple2.Model/Game/Dungeon/DungeonUserRecord.cs @@ -0,0 +1,91 @@ +using System.Collections.Concurrent; +using Maple2.Model.Enum; +using Maple2.PacketLib.Tools; + +namespace Maple2.Model.Game.Dungeon; + +public class DungeonUserRecord : IUserContentRecord { + public readonly int DungeonId; + public long CharacterId { get; init; } + public bool IsDungeonSuccess; + public bool WithParty { get; set; } + public readonly ConcurrentDictionary AccumulationRecords; + public DungeonBonusFlag BonusFlag = DungeonBonusFlag.None; + public DungeonBonusFlag2 BonusFlag2 = DungeonBonusFlag2.None; + public Dictionary Rewards { get; init; } = []; + public ICollection RewardItems { get; init; } = []; + public Dictionary BonusRewards { get; init; } = []; + public ICollection BonusRewardItems { get; init; } = []; + public int TotalSeconds; + public int Score = -1; + public int HighestScore = -1; + + public DungeonUserRecord(int dungeonId, long characterId) { + DungeonId = dungeonId; + CharacterId = characterId; + AccumulationRecords = []; + var accumulationEnumList = new List((DungeonAccumulationRecordType[]) System.Enum.GetValues(typeof(DungeonAccumulationRecordType))); + foreach (DungeonAccumulationRecordType type in accumulationEnumList) { + AccumulationRecords[type] = 0; + } + var rewardTypeEnumList = new List((DungeonRewardType[]) System.Enum.GetValues(typeof(DungeonRewardType))); + foreach (DungeonRewardType type in rewardTypeEnumList) { + Rewards[type] = 0; + } + } + + public void Add(RewardRecord record) { + Rewards[DungeonRewardType.Exp] += (int) record.Exp; + Rewards[DungeonRewardType.Meso] += (int) record.Meso; + Rewards[DungeonRewardType.Prestige] += (int) record.PrestigeExp; + + foreach (RewardItem item in record.Items) { + RewardItems.Add(item); + } + } + + public void WriteTo(IByteWriter writer) { + writer.WriteBool(IsDungeonSuccess); + writer.WriteInt(DungeonId); + writer.WriteBool(WithParty); + writer.WriteInt(TotalSeconds); + writer.WriteInt(Score); + writer.WriteInt(HighestScore); + writer.Write(BonusFlag); + writer.Write(BonusFlag2); + writer.WriteByte(); + writer.WriteByte(); + + writer.WriteInt(Rewards.Count); + foreach ((DungeonRewardType type, int value) in Rewards) { + writer.Write(type); + writer.WriteInt(value); + } + + writer.WriteInt(RewardItems.Count); + foreach (RewardItem item in RewardItems) { + writer.WriteInt(item.ItemId); + writer.WriteInt(item.Rarity); + writer.WriteInt(item.Amount); + writer.WriteBool(item.Unknown1); + writer.WriteBool(item.Unknown2); + writer.WriteBool(item.Unknown3); + } + + writer.WriteInt(BonusRewards.Count); + foreach ((DungeonRewardType type, int value) in BonusRewards) { + writer.Write(type); + writer.WriteInt(value); + } + + writer.WriteInt(BonusRewardItems.Count); + foreach (RewardItem item in BonusRewardItems) { + writer.WriteInt(item.ItemId); + writer.WriteInt(item.Rarity); + writer.WriteInt(item.Amount); + writer.WriteBool(item.Unknown1); + writer.WriteBool(item.Unknown2); + writer.WriteBool(item.Unknown3); + } + } +} diff --git a/Maple2.Model/Game/Dungeon/DungeonUserResult.cs b/Maple2.Model/Game/Dungeon/DungeonUserResult.cs new file mode 100644 index 000000000..7689b866b --- /dev/null +++ b/Maple2.Model/Game/Dungeon/DungeonUserResult.cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; +using Maple2.Model.Enum; + +namespace Maple2.Model.Game.Dungeon; + +[StructLayout(LayoutKind.Sequential, Pack = 4, Size = 16)] +public struct DungeonUserResult { + public readonly long CharacterId; + public readonly DungeonAccumulationRecordType RecordType; + public readonly int Value; + public DungeonGrade Grade = DungeonGrade.None; + + public DungeonUserResult(long characterId, DungeonAccumulationRecordType recordType, int value) { + CharacterId = characterId; + RecordType = recordType; + Value = value; + } +} diff --git a/Maple2.Model/Game/Dungeon/IUserContentRecord.cs b/Maple2.Model/Game/Dungeon/IUserContentRecord.cs new file mode 100644 index 000000000..66343ad2f --- /dev/null +++ b/Maple2.Model/Game/Dungeon/IUserContentRecord.cs @@ -0,0 +1,10 @@ +using Maple2.Model.Enum; +using Maple2.Tools; + +namespace Maple2.Model.Game.Dungeon; + +public interface IUserContentRecord : IByteSerializable { + public long CharacterId { get; } + public Dictionary Rewards { get; } + public ICollection RewardItems { get; } +} diff --git a/Maple2.Model/Game/Dungeon/MiniGameUserRecord.cs b/Maple2.Model/Game/Dungeon/MiniGameUserRecord.cs new file mode 100644 index 000000000..7a68f9027 --- /dev/null +++ b/Maple2.Model/Game/Dungeon/MiniGameUserRecord.cs @@ -0,0 +1,53 @@ +using Maple2.Model.Enum; +using Maple2.PacketLib.Tools; + +namespace Maple2.Model.Game.Dungeon; + +public class MiniGameUserRecord : IUserContentRecord { + public long CharacterId { get; init; } + public Dictionary Rewards { get; init; } = []; + public ICollection RewardItems { get; init; } = []; + public int ClearedRounds { get; set; } + public required int MinRound { get; init; } + public required int TotalRounds { get; init; } + public required bool ShowResult { get; init; } + + public MiniGameUserRecord(long characterId) { + CharacterId = characterId; + var enumList = new List((DungeonRewardType[]) System.Enum.GetValues(typeof(DungeonRewardType))); + foreach (DungeonRewardType type in enumList) { + Rewards[type] = 0; + } + } + + public void Add(RewardRecord record) { + Rewards[DungeonRewardType.Exp] += (int) record.Exp; + Rewards[DungeonRewardType.Meso] += (int) record.Meso; + Rewards[DungeonRewardType.Prestige] += (int) record.PrestigeExp; + + foreach (RewardItem item in record.Items) { + RewardItems.Add(item); + } + } + + public void WriteTo(IByteWriter writer) { + writer.WriteInt(ClearedRounds); + writer.WriteInt(TotalRounds); + + writer.WriteInt(Rewards.Count); + foreach ((DungeonRewardType type, int value) in Rewards) { + writer.Write(type); + writer.WriteInt(value); + } + + writer.WriteInt(RewardItems.Count); + foreach (RewardItem item in RewardItems) { + writer.WriteInt(item.ItemId); + writer.WriteInt(item.Rarity); + writer.WriteInt(item.Amount); + writer.WriteBool(item.Unknown1); + writer.WriteBool(item.Unknown2); + writer.WriteBool(item.Unknown3); + } + } +} diff --git a/Maple2.Model/Game/FieldInstance.cs b/Maple2.Model/Game/FieldInstance.cs index 183779b6a..3b6b1b78a 100644 --- a/Maple2.Model/Game/FieldInstance.cs +++ b/Maple2.Model/Game/FieldInstance.cs @@ -1,25 +1,21 @@ -using System.Runtime.InteropServices; -using Maple2.Model.Enum; +using Maple2.Model.Enum; using Maple2.PacketLib.Tools; using Maple2.Tools; namespace Maple2.Model.Game; public sealed class FieldInstance : IByteSerializable { - public static FieldInstance Default = new(false, InstanceType.none, 0); + public static FieldInstance Default = new(InstanceType.none, 0); - public readonly bool BlockChangeChannel; public readonly InstanceType InstanceType; public readonly int InstanceId; - public FieldInstance(bool blockChangeChannel, InstanceType instanceType, int instanceId) { - BlockChangeChannel = blockChangeChannel; + public FieldInstance(InstanceType instanceType, int instanceId) { InstanceType = instanceType; InstanceId = instanceId; } public void WriteTo(IByteWriter writer) { - writer.WriteBool(BlockChangeChannel); writer.Write(InstanceType); writer.WriteInt(InstanceId); } diff --git a/Maple2.Model/Game/RewardItem.cs b/Maple2.Model/Game/RewardItem.cs index 1aaf2f021..a52b6366d 100644 --- a/Maple2.Model/Game/RewardItem.cs +++ b/Maple2.Model/Game/RewardItem.cs @@ -4,7 +4,7 @@ namespace Maple2.Model.Game; [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 14)] -public readonly struct RewardItem { +public readonly record struct RewardItem { public int ItemId { get; } public short Rarity { get; } public int Amount { get; } @@ -19,4 +19,29 @@ public RewardItem(int itemId, short rarity, int amount) { Rarity = rarity; Amount = amount; } + + public static implicit operator RewardItem(Item item) { + return new RewardItem(item.Id, (short) item.Rarity, item.Amount); + } +} + +public readonly struct RewardRecord { + public ICollection Items { get; } = []; + public long Exp { get; } + public long PrestigeExp { get; } + public long Meso { get; } + + public RewardRecord(List items, long exp, long prestigeExp, long meso) { + Items = items; + Exp = exp; + PrestigeExp = prestigeExp; + Meso = meso; + } + + public RewardRecord(List items, long exp, long prestigeExp, long meso) { + Items = items.Select(item => (RewardItem) item).ToList(); + Exp = exp; + PrestigeExp = prestigeExp; + Meso = meso; + } } diff --git a/Maple2.Model/Game/User/PlotInfo.cs b/Maple2.Model/Game/User/PlotInfo.cs index fa9db5aa8..fdda880b6 100644 --- a/Maple2.Model/Game/User/PlotInfo.cs +++ b/Maple2.Model/Game/User/PlotInfo.cs @@ -51,12 +51,6 @@ public PlotState State { public class Plot(UgcMapGroup metadata) : PlotInfo(metadata) { public readonly Dictionary Cubes = new(); - public static readonly Plot Default = new(new UgcMapGroup(0, - 0, - 0, - new UgcMapGroup.Cost(0, 0, 0), - new UgcMapGroup.Cost(0, 0, 0), - new UgcMapGroup.Limits(0, 0, 0, 0, 0, 0))); public void SetPlannerMode(PlotMode mode) { PlotMode = mode; diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index fec65f37e..128384126 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -1,6 +1,5 @@ // ReSharper disable InconsistentNaming -using System; using Maple2.Model.Enum; #pragma warning disable IDE1006 // Naming Styles @@ -112,6 +111,13 @@ public static class Constant { public const bool EnableRollEverywhere = false; public const bool HideHomeCommands = true; + public static IReadOnlyDictionary ContentRewards { get; } = new Dictionary { + {"miniGame", 1005}, + {"dungeonHelper", 1006}, + {"MiniGameType2", 0}, // Shanghai Runners + {"UserOpenMiniGameExtraReward", 0}, // Player hosted mini game extra rewards + }; + public const bool MailQuestItems = false; // Mail quest item rewards if inventory is full #region Field diff --git a/Maple2.Model/Metadata/ServerTable/PrestigeIdExpTable.cs b/Maple2.Model/Metadata/ServerTable/PrestigeIdExpTable.cs new file mode 100644 index 000000000..d645ce258 --- /dev/null +++ b/Maple2.Model/Metadata/ServerTable/PrestigeIdExpTable.cs @@ -0,0 +1,13 @@ +using Maple2.Model.Enum; + +namespace Maple2.Model.Metadata; + +public record PrestigeIdExpTable( + IReadOnlyDictionary Entries) : ServerTable { + + public record Entry( + int Id, + long Value, + ExpType Type); +} + diff --git a/Maple2.Model/Metadata/ServerTableMetadata.cs b/Maple2.Model/Metadata/ServerTableMetadata.cs index 694b57684..040b985e3 100644 --- a/Maple2.Model/Metadata/ServerTableMetadata.cs +++ b/Maple2.Model/Metadata/ServerTableMetadata.cs @@ -34,6 +34,7 @@ public override int GetHashCode() { [JsonDerivedType(typeof(UserStatTable), typeDiscriminator: "userStat*")] [JsonDerivedType(typeof(IndividualDropItemTable), typeDiscriminator: "individualItemDrop")] [JsonDerivedType(typeof(PrestigeExpTable), typeDiscriminator: "adventureExpTable")] +[JsonDerivedType(typeof(PrestigeIdExpTable), typeDiscriminator: "prestigeIdExpTable")] [JsonDerivedType(typeof(TimeEventTable), typeDiscriminator: "timeEvent")] [JsonDerivedType(typeof(GameEventTable), typeDiscriminator: "gameEvent")] [JsonDerivedType(typeof(OxQuizTable), typeDiscriminator: "oxQuiz")] diff --git a/Maple2.Model/Metadata/Table/RewardContentTable.cs b/Maple2.Model/Metadata/Table/RewardContentTable.cs new file mode 100644 index 000000000..68e055dd2 --- /dev/null +++ b/Maple2.Model/Metadata/Table/RewardContentTable.cs @@ -0,0 +1,32 @@ +using Maple2.Model.Game; + +namespace Maple2.Model.Metadata; + +public record RewardContentTable(IReadOnlyDictionary BaseEntries, + IReadOnlyDictionary ItemEntries, + IReadOnlyDictionary MesoStaticEntries, + IReadOnlyDictionary> MesoEntries, + IReadOnlyDictionary ExpStaticEntries) : Table { + public record Base( + int Id, + int MesoTableId, + int ExpTableId, + float MesoFactor, + float ExpFactor, + int ItemTableId, + int PrestigeExpTableId); + + public record Item( + int Id, + Item.Data[] ItemData) { + + public record Data( + int MinLevel, + int MaxLevel, + RewardItem[] RewardItems); + } + + public record Meso( + int Id, + IReadOnlyDictionary Table); // Level, Meso +} diff --git a/Maple2.Model/Metadata/TableMetadata.cs b/Maple2.Model/Metadata/TableMetadata.cs index 5b3c662bd..85e1192d6 100644 --- a/Maple2.Model/Metadata/TableMetadata.cs +++ b/Maple2.Model/Metadata/TableMetadata.cs @@ -79,4 +79,5 @@ public override int GetHashCode() { [JsonDerivedType(typeof(UgcHousingPointRewardTable), typeDiscriminator: "ugchousingpointreward")] [JsonDerivedType(typeof(DungeonRoomTable), typeDiscriminator: "dungeonroom")] [JsonDerivedType(typeof(DungeonRankRewardTable), typeDiscriminator: "dungeonrankreward")] +[JsonDerivedType(typeof(RewardContentTable), typeDiscriminator: "rewardcontent")] public abstract record Table; diff --git a/Maple2.Server.Core/proto/channel/channel.proto b/Maple2.Server.Core/proto/channel/channel.proto index 29189e167..6a6592a28 100644 --- a/Maple2.Server.Core/proto/channel/channel.proto +++ b/Maple2.Server.Core/proto/channel/channel.proto @@ -7,6 +7,8 @@ import "sync.proto"; // The channel service definition. service Channel { + // Manage field + rpc Field(maple2.FieldRequest) returns (maple2.FieldResponse); // Send a chat message. rpc Chat(maple2.ChatRequest) returns (maple2.ChatResponse); // Manage buddy list. diff --git a/Maple2.Server.Core/proto/common.proto b/Maple2.Server.Core/proto/common.proto index 4b6a558df..a85970fce 100644 --- a/Maple2.Server.Core/proto/common.proto +++ b/Maple2.Server.Core/proto/common.proto @@ -2,6 +2,27 @@ syntax = "proto3"; package maple2; +message FieldRequest { + message CreateDungeon { + int32 dungeon_id = 1; + int32 size = 2; + int32 party_id = 3; + } + message DestroyDungeon { + int32 room_id = 1; + } + int64 requester_id = 1; + oneof field { + CreateDungeon create_dungeon = 2; + DestroyDungeon destroy_dungeon = 3; + } +} + +message FieldResponse { + int32 error = 1; + int32 room_id = 2; +} + message ChatRequest { message Whisper { int64 recipient_id = 1; diff --git a/Maple2.Server.Core/proto/world/world.proto b/Maple2.Server.Core/proto/world/world.proto index 2c9e1040e..a4904b72c 100644 --- a/Maple2.Server.Core/proto/world/world.proto +++ b/Maple2.Server.Core/proto/world/world.proto @@ -12,6 +12,8 @@ service World { rpc MigrateOut(MigrateOutRequest) returns (MigrateOutResponse); // Migrate into a server. rpc MigrateIn(MigrateInRequest) returns (MigrateInResponse); + // Cross field + rpc Field(maple2.FieldRequest) returns (maple2.FieldResponse); // Retrieve information about channels. rpc Channels(ChannelsRequest) returns (ChannelsResponse); // Send a chat message. diff --git a/Maple2.Server.Game/GameServer.cs b/Maple2.Server.Game/GameServer.cs index 6ab35aff0..be992261f 100644 --- a/Maple2.Server.Game/GameServer.cs +++ b/Maple2.Server.Game/GameServer.cs @@ -4,12 +4,11 @@ using Autofac; using Maple2.Database.Storage; using Maple2.Model.Enum; +using Maple2.Model.Error; using Maple2.Model.Game; using Maple2.Model.Game.Event; using Maple2.PacketLib.Tools; -using Maple2.Model.Game.Shop; using Maple2.Model.Metadata; -using Maple2.Server.Core.Constants; using Maple2.Server.Core.Network; using Maple2.Server.Core.Packets; using Maple2.Server.Game.Manager.Field; @@ -30,7 +29,6 @@ public class GameServer : Server { private readonly ItemMetadataStorage itemMetadataStorage; - private static short _channel = 0; public GameServer(FieldManager.Factory fieldFactory, PacketRouter router, IComponentContext context, GameStorage gameStorage, ItemMetadataStorage itemMetadataStorage, ServerTableMetadataStorage serverTableMetadataStorage, IGraphicsContext debugGraphicsContext, int port, int channel) @@ -104,6 +102,14 @@ protected override void AddSession(GameSession session) { return fieldFactory.Get(mapId, roomId: roomId); } + public DungeonFieldManager? CreateDungeon(DungeonRoomTable.DungeonRoomMetadata metadata, long requesterId, int size, int partyId) { + return fieldFactory.CreateDungeon(metadata, requesterId, size, partyId); + } + + public MigrationError DestroyDungeon(int roomId) { + return fieldFactory.DestroyDungeon(roomId); + } + public IList FindEvent(GameEventType type) { return eventCache.Values.Where(gameEvent => gameEvent.Metadata.Type == type && gameEvent.IsActive()).ToList(); } diff --git a/Maple2.Server.Game/Manager/DungeonManager.cs b/Maple2.Server.Game/Manager/DungeonManager.cs index 1cbdf3554..3dd157fec 100644 --- a/Maple2.Server.Game/Manager/DungeonManager.cs +++ b/Maple2.Server.Game/Manager/DungeonManager.cs @@ -25,8 +25,10 @@ public class DungeonManager { private IDictionary EnterLimits => session.Player.Value.Character.DungeonEnterLimits; private IDictionary RankRewards => session.Player.Value.Unlock.DungeonRankRewards; - public DungeonFieldManager? Field { get; set; } + public DungeonFieldManager? Lobby { get; private set; } public DungeonRoomTable.DungeonRoomMetadata? Metadata; + public DungeonRoomRecord? DungeonRoomRecord => Lobby?.DungeonRoomRecord; + public DungeonUserRecord? UserRecord; private int LobbyRoomId { get; set; } private Party? Party => session.Party.Party; @@ -42,10 +44,7 @@ public DungeonManager(GameSession session) { private void Init() { using GameStorage.Request db = session.GameStorage.Context(); Records = db.GetDungeonRecords(session.CharacterId); - } - public void Load() { - using GameStorage.Request db = session.GameStorage.Context(); foreach (DungeonRoomTable.DungeonRoomMetadata metadata in session.TableMetadata.DungeonRoomTable.Entries.Values) { if (!Records.TryGetValue(metadata.Id, out DungeonRecord? record)) { record = new DungeonRecord(metadata.Id); @@ -60,12 +59,24 @@ record = db.CreateDungeonRecord(record, session.CharacterId); DungeonEnterLimit limit = GetEnterLimit(metadata.Limit); EnterLimits[metadata.Id] = limit; } + } + public void Load() { session.Send(DungeonRoomPacket.Load(Records)); session.Send(DungeonRoomPacket.RankRewards(RankRewards)); session.Send(FieldEntrancePacket.Load(EnterLimits)); } + public void LoadField() { + if (Lobby == null) { + return; + } + + if (session.Field.FieldInstance.InstanceType == InstanceType.DungeonLobby) { + session.Send(DungeonWaitingPacket.Set(Lobby.DungeonId, Lobby.Size)); + } + } + public void UpdateDungeonEnterLimit() { bool updated = false; foreach (int dungeonId in EnterLimits.Keys) { @@ -194,11 +205,16 @@ private DungeonEnterLimit GetEnterLimit(DungeonRoomLimit metadata) { } public void SetDungeon(DungeonFieldManager field) { - Field = field; - Metadata = field.Metadata; + Lobby = field; + LobbyRoomId = field.RoomId; + Metadata = field.DungeonMetadata; + UserRecord = new DungeonUserRecord(field.DungeonId, session.CharacterId); + + field.DungeonRoomRecord.UserResults.TryAdd(session.CharacterId, UserRecord); } public void CreateDungeonRoom(int dungeonId, bool withParty) { + int size = 1; if (!session.TableMetadata.DungeonRoomTable.Entries.TryGetValue(dungeonId, out DungeonRoomTable.DungeonRoomMetadata? metadata)) { session.Send(DungeonRoomPacket.Error(DungeonRoomError.s_room_dungeon_notOpenTimeDungeon)); return; @@ -218,17 +234,27 @@ public void CreateDungeonRoom(int dungeonId, bool withParty) { session.Send(DungeonRoomPacket.Error(DungeonRoomError.s_room_party_err_not_chief)); return; } + size = Party.Members.Count; } + try { + FieldResponse response = session.World.Field(new FieldRequest { + CreateDungeon = new FieldRequest.Types.CreateDungeon { + DungeonId = dungeonId, + Size = size, + PartyId = session.Party.Id, + }, + }); + if ((MigrationError) response.Error != MigrationError.ok) { + session.Send(MigrationPacket.GameToGameError((MigrationError) response.Error)); + return; + } - DungeonFieldManager? dungeonField = session.FieldFactory.CreateDungeon(metadata, session.CharacterId, Party); - if (dungeonField == null) { - session.Send(DungeonRoomPacket.Error(DungeonRoomError.s_room_dungeon_NotAllowTime)); + LobbyRoomId = response.RoomId; + } catch (RpcException) { + session.Send(MigrationPacket.GameToGameError(MigrationError.s_move_err_no_server)); return; } - LobbyRoomId = dungeonField.RoomId; - - SetDungeon(dungeonField); if (withParty) { var request = new PartyRequest { @@ -236,7 +262,7 @@ public void CreateDungeonRoom(int dungeonId, bool withParty) { SetDungeon = new PartyRequest.Types.SetDungeon { PartyId = Party!.Id, DungeonId = dungeonId, - DungeonRoomId = dungeonField.RoomId, + DungeonRoomId = LobbyRoomId, Set = true, }, }; @@ -246,6 +272,7 @@ public void CreateDungeonRoom(int dungeonId, bool withParty) { } catch (RpcException) { } } + Metadata = metadata; MigrateToDungeon(); } @@ -253,26 +280,24 @@ public void EnterLobby() { if (Party == null) { return; } - FieldManager? field = session.Field.FieldFactory.Get(roomId: Party.DungeonLobbyRoomId); - if (field is not DungeonFieldManager dungeonField) { - return; - } - SetDungeon(dungeonField); MigrateToDungeon(); } public void EnterInitField() { - if (Field == null || Metadata == null) { + if (Lobby == null || Metadata == null || Lobby.RoomFields.IsEmpty) { logger.Error("Field is null, cannot enter dungeon"); return; } - if (!Field.RoomFields.TryGetValue(Metadata.FieldIds[0], out DungeonFieldManager? firstField)) { + if (!Lobby.RoomFields.TryGetValue(Metadata.FieldIds[0], out DungeonFieldManager? firstField)) { logger.Error("First field is null, cannot enter dungeon"); return; } + if (firstField.DungeonRoomRecord.StartTick == 0) { + firstField.DungeonRoomRecord.StartTick = Environment.TickCount; + } session.Send(session.PrepareField(firstField.MapId, roomId: firstField.RoomId) ? FieldEnterPacket.Request(session.Player) : FieldEnterPacket.Error(MigrationError.s_move_err_default)); } @@ -288,11 +313,82 @@ public void SetDungeon(int dungeonId, int roomId, bool set) { LobbyRoomId = roomId; } else { Metadata = null; - Field = null; LobbyRoomId = 0; } } + public void CompleteDungeon() { + if (Lobby == null || Metadata == null || UserRecord == null) { + return; + } + DungeonRoomReward metadata = Metadata.Reward; + UserRecord.IsDungeonSuccess = Lobby.DungeonRoomRecord.State == DungeonState.Clear; + UserRecord.TotalSeconds = (int) (Lobby.DungeonRoomRecord.EndTick - Lobby.DungeonRoomRecord.StartTick) / 1000; + + int exp = (int) metadata.Exp; + if (metadata.ExpRate > 0f) { + exp = (int) (metadata.Exp * metadata.ExpRate); + } + UserRecord.Rewards[DungeonRewardType.Exp] = exp; + UserRecord.Rewards[DungeonRewardType.Meso] = (int) metadata.Meso; + session.Exp.AddExp(UserRecord.Rewards[DungeonRewardType.Exp]); + session.Currency.Meso += UserRecord.Rewards[DungeonRewardType.Meso]; + session.Send(DungeonRoomPacket.Modify(DungeonRoomModify.GiveReward, Lobby.DungeonId)); + + ICollection items = []; + if (metadata.UnlimitedDropBoxIds.Length > 0) { + foreach (int boxId in metadata.UnlimitedDropBoxIds) { + items = items.Concat(Lobby.ItemDrop.GetIndividualDropItems(boxId)).ToList(); + items = items.Concat(Lobby.ItemDrop.GetGlobalDropItems(boxId, Metadata.Level)).ToList(); + } + } + + foreach (Item item in items) { + UserRecord.RewardItems.Add(item); + if (!session.Item.Inventory.Add(item, true)) { + session.Item.MailItem(item); + } + } + + session.Send(DungeonRewardPacket.Dungeon(UserRecord)); + } + + public void Reset() { + if (session.Party.Party == null || session.Party.Party.LeaderCharacterId != session.CharacterId) { + return; + } + + try { + FieldResponse response = session.World.Field(new FieldRequest { + DestroyDungeon = new FieldRequest.Types.DestroyDungeon { + RoomId = LobbyRoomId, + }, + }); + if ((MigrationError) response.Error != MigrationError.ok) { + session.Send(MigrationPacket.GameToGameError((MigrationError) response.Error)); + return; + } + } catch (RpcException) { + session.Send(MigrationPacket.GameToGameError(MigrationError.s_move_err_no_server)); + return; + } + + var request = new PartyRequest { + RequestorId = session.CharacterId, + SetDungeon = new PartyRequest.Types.SetDungeon { + PartyId = session.Party.Id, + DungeonId = 0, + DungeonRoomId = 0, + Set = false, + }, + }; + + try { + session.World.Party(request); + } catch (RpcException) { } + + } + private void MigrateToDungeon() { if (Metadata == null) { logger.Error("Dungeon metadata is null, cannot migrate to dungeon"); diff --git a/Maple2.Server.Game/Manager/ExperienceManager.cs b/Maple2.Server.Game/Manager/ExperienceManager.cs index da02fa59a..d5983513e 100644 --- a/Maple2.Server.Game/Manager/ExperienceManager.cs +++ b/Maple2.Server.Game/Manager/ExperienceManager.cs @@ -96,21 +96,22 @@ private long GetRestExp(long expGained) { return addedRestExp; } - public void AddExp(long expGained, ExpMessageCode message = ExpMessageCode.s_msg_take_exp) { + public long AddExp(long expGained, ExpMessageCode message = ExpMessageCode.s_msg_take_exp) { if (expGained <= 0) { - return; + return 0; } expGained += GetRestExp(expGained); LevelUp(); AddPrestigeExp(message.Type()); session.Send(ExperienceUpPacket.Add(expGained, Exp, RestExp, message)); session.ConditionUpdate(ConditionType.exp, counter: expGained); + return expGained; } - public void AddExp(ExpType expType, float modifier = 1f, long additionalExp = 0) { + public long AddExp(ExpType expType, float modifier = 1f, long additionalExp = 0) { if (!session.TableMetadata.CommonExpTable.Entries.TryGetValue(expType, out CommonExpTable.Entry? entry) || !session.TableMetadata.ExpTable.ExpBase.TryGetValue(entry.ExpTableId, out IReadOnlyDictionary? expBase)) { - return; + return 0; } long expValue; @@ -124,8 +125,10 @@ public void AddExp(ExpType expType, float modifier = 1f, long additionalExp = 0) case ExpType.gathering: case ExpType.arcade: case ExpType.expDrop: + case ExpType.miniGame: + case ExpType.userMiniGame: if (!expBase.TryGetValue(Level, out expValue)) { - return; + return 0; } break; case ExpType.taxi: @@ -136,15 +139,15 @@ public void AddExp(ExpType expType, float modifier = 1f, long additionalExp = 0) int fieldLevel = session.Field.Metadata.Drop.Level; int correctedLevel = fieldLevel > Level ? Level : fieldLevel; if (!expBase.TryGetValue(correctedLevel, out expValue)) { - return; + return 0; } break; default: Log.Logger.Warning("Unhandled ExpType: {ExpType}", expType); - return; + return 0; } - AddExp((long) ((expValue * modifier) * entry.Factor) + additionalExp, expType.Message()); + return AddExp((long) ((expValue * modifier) * entry.Factor) + additionalExp, expType.Message()); } public void AddMobExp(int moblevel, float modifier = 1f, long additionalExp = 0) { diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/DungeonFieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/DungeonFieldManager.cs index 073a39cc9..160c6c901 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/DungeonFieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/DungeonFieldManager.cs @@ -1,25 +1,69 @@ using System.Collections.Concurrent; using Maple2.Database.Storage; -using Maple2.Model.Game.Party; +using Maple2.Model.Enum; +using Maple2.Model.Game.Dungeon; using Maple2.Model.Metadata; +using Maple2.Server.Game.Model; +using Maple2.Server.Game.Packets; namespace Maple2.Server.Game.Manager.Field; public class DungeonFieldManager : FieldManager { - public DungeonRoomTable.DungeonRoomMetadata Metadata { get; private set; } - public int DungeonId => Metadata.Id; - + public readonly DungeonRoomTable.DungeonRoomMetadata DungeonMetadata; public DungeonFieldManager? Lobby { get; init; } public readonly ConcurrentDictionary RoomFields = []; + public required DungeonRoomRecord DungeonRoomRecord { get; init; } + public int PartyId { get; init; } - public Party? Party { get; init; } - - public DungeonFieldManager(DungeonRoomTable.DungeonRoomMetadata metadata, MapMetadata mapMetadata, UgcMapMetadata ugcMetadata, MapEntityMetadata entities, NpcMetadataStorage npcMetadata, long ownerId = 0, Party? party = null) + public DungeonFieldManager(DungeonRoomTable.DungeonRoomMetadata dungeonMetadata, MapMetadata mapMetadata, UgcMapMetadata ugcMetadata, MapEntityMetadata entities, NpcMetadataStorage npcMetadata, long ownerId = 0, int size = 1, int partyId = 0) : base(mapMetadata, ugcMetadata, entities, npcMetadata, ownerId) { - Party = party; - Metadata = metadata; - if (metadata.LobbyFieldId == mapMetadata.Id) { + DungeonMetadata = dungeonMetadata; + if (dungeonMetadata.LobbyFieldId == mapMetadata.Id) { Lobby = this; } + DungeonId = dungeonMetadata.Id; + Size = size; + PartyId = partyId; + } + + public void ChangeState(DungeonState state) { + DungeonRoomRecord.State = state; + DungeonRoomRecord.EndTick = FieldTick; + + var compiledResults = new Dictionary(); + + // Get player's best record + 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; + } + } + + Broadcast(DungeonRoomPacket.DungeonResult(DungeonRoomRecord.State, compiledResults)); + + if (DungeonRoomRecord.State == DungeonState.Clear) { + foreach (long characterId in compiledResults.Keys) { + if (!TryGetPlayerById(characterId, out FieldPlayer? player)) { + continue; + } + + player.Session.Dungeon.CompleteDungeon(); + + } + } } } diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.Factory.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.Factory.cs index 7e6da67fe..772d15944 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.Factory.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.Factory.cs @@ -5,10 +5,10 @@ using Maple2.Database.Storage; using Maple2.Database.Storage.Metadata; using Maple2.Model.Enum; +using Maple2.Model.Error; using Maple2.Model.Game; -using Maple2.Model.Game.Party; +using Maple2.Model.Game.Dungeon; using Maple2.Model.Metadata; -using Maple2.Server.World.Service; using Serilog; using WorldClient = Maple2.Server.World.Service.World.WorldClient; @@ -143,7 +143,7 @@ private HomeFieldManager CreateHome(long ownerId, MapMetadata mapMetadata, UgcMa return field; } - public DungeonFieldManager? CreateDungeon(DungeonRoomTable.DungeonRoomMetadata dungeonMetadata, long ownerId, Party? party = null) { + public DungeonFieldManager? CreateDungeon(DungeonRoomTable.DungeonRoomMetadata dungeonMetadata, long ownerId, int size = 1, int partyId = 0) { var sw = new Stopwatch(); sw.Start(); @@ -160,8 +160,14 @@ private HomeFieldManager CreateHome(long ownerId, MapMetadata mapMetadata, UgcMa if (entities == null) { throw new InvalidOperationException($"Failed to load entities for map: {dungeonMetadata.LobbyFieldId}"); } - var lobbyField = new DungeonFieldManager(dungeonMetadata, mapMetadata, ugcMetadata, entities, NpcMetadata, ownerId, party); + + var dungeonRoomRecord = new DungeonRoomRecord(dungeonMetadata); + var lobbyField = new DungeonFieldManager(dungeonMetadata, mapMetadata, ugcMetadata, entities, NpcMetadata, ownerId, size, partyId) { + DungeonRoomRecord = dungeonRoomRecord, + FieldType = FieldType.Default, + }; context.InjectProperties(lobbyField); + lobbyField.Init(); foreach (int fieldId in dungeonMetadata.FieldIds) { if (!MapMetadata.TryGet(fieldId, out mapMetadata)) { @@ -177,8 +183,10 @@ private HomeFieldManager CreateHome(long ownerId, MapMetadata mapMetadata, UgcMa if (entities == null) { throw new InvalidOperationException($"Failed to load entities for map: {fieldId}"); } - var field = new DungeonFieldManager(dungeonMetadata, mapMetadata, ugcMetadata, entities, NpcMetadata) { + var field = new DungeonFieldManager(dungeonMetadata, mapMetadata, ugcMetadata, entities, NpcMetadata, ownerId, size, partyId) { Lobby = lobbyField, + DungeonRoomRecord = dungeonRoomRecord, + FieldType = FieldType.Dungeon, }; context.InjectProperties(field); field.Init(); @@ -192,6 +200,14 @@ private HomeFieldManager CreateHome(long ownerId, MapMetadata mapMetadata, UgcMa return lobbyField; } + public MigrationError DestroyDungeon(int roomId) { + if (!dungeons.TryGetValue(roomId, out DungeonFieldManager? dungeon)) { + return MigrationError.s_move_err_dungeon_not_exist; + } + + return DisposeDungeon(dungeon); + } + /// /// Disposes the field managers that have no players and have been empty for more than 10 minutes. /// @@ -261,35 +277,7 @@ private void DisposeLoop() { } foreach (DungeonFieldManager dungeonField in dungeons.Values) { - int playerCount = dungeonField.Players.Values.Count + dungeonField.RoomFields.Values.Sum(x => x.Players.Values.Count); - if (playerCount > 0) { - continue; - } - if (dungeonField.Party != null) { - // Check if party still exists - PartyInfoResponse response = World.PartyInfo(new PartyInfoRequest { - PartyId = dungeonField.Party.Id, - }); - if (response.Party == null) { - logger.Debug("Dungeon {DungeonId} party {PartyId} does not exist, disposing", dungeonField.DungeonId, dungeonField.Party.Id); - dungeonField.Dispose(); - } - continue; - } - - dungeonField.Dispose(); - } - - foreach (int roomId in dungeons.Keys) { - if (dungeons[roomId].Disposed) { - List roomIds = dungeons[roomId].RoomFields.Values.Select(x => x.RoomId).ToList(); - foreach (DungeonFieldManager roomField in dungeons[roomId].RoomFields.Values) { - roomField.Dispose(); - subDungeonFields.TryRemove(roomField.RoomId, out _); - } - dungeons.TryRemove(roomId, out _); - logger.Debug("Dungeon disposed room Ids: {roomIds}", string.Join(",", roomIds)); - } + DisposeDungeon(dungeonField); } logger.Verbose("Field dispose loop sleeping for {Interval}ms", Constant.FieldDisposeLoopInterval); @@ -301,6 +289,24 @@ private void DisposeLoop() { } } + private MigrationError DisposeDungeon(DungeonFieldManager dungeon) { + int playerCount = dungeon.Players.Values.Count + dungeon.RoomFields.Values.Sum(x => x.Players.Values.Count); + if (playerCount > 0) { + return MigrationError.s_move_err_InsideDungeonUser; + } + + dungeon.Dispose(); + + List roomIds = dungeon.RoomFields.Values.Select(x => x.RoomId).ToList(); + foreach (DungeonFieldManager roomField in dungeon.RoomFields.Values) { + roomField.Dispose(); + subDungeonFields.TryRemove(roomField.RoomId, out _); + } + dungeons.TryRemove(dungeon.RoomId, out _); + logger.Debug("Dungeon disposed room Ids: {lobbyRoomId}, {roomIds}", dungeon.RoomId, string.Join(",", roomIds)); + return MigrationError.ok; + } + public void Dispose() { foreach (ConcurrentDictionary fields in fields.Values) { foreach (FieldManager field in fields.Values) { diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs index ac737c3ed..a9b1e7450 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs @@ -71,6 +71,9 @@ public partial class FieldManager : IField { public int MapId { get; init; } public int RoomId { get; init; } + public int DungeonId { get; init; } + public int Size { get; init; } + public FieldType FieldType { get; init; } public FieldInstance FieldInstance { get; private set; } public readonly AiManager Ai; public IFieldRenderer? DebugRenderer { get; private set; } @@ -98,6 +101,8 @@ public FieldManager(MapMetadata metadata, UgcMapMetadata ugcMetadata, MapEntityM if (MapId is Constant.PerformanceMapId) { PerformanceStage = new PerformanceStageManager(this); } + + FieldType = FieldType.Default; } // Init is separate from constructor to allow properties to be injected first. @@ -115,7 +120,7 @@ public virtual void Init() { } if (ServerTableMetadata.InstanceFieldTable.Entries.TryGetValue(Metadata.Id, out InstanceFieldMetadata? instanceField)) { - FieldInstance = new FieldInstance(blockChangeChannel: true, instanceField.Type, instanceField.InstanceId); + FieldInstance = new FieldInstance(instanceField.Type, instanceField.InstanceId); } if (ugcMetadata.Plots.Count > 0) { @@ -126,7 +131,12 @@ public virtual void Init() { } // Create default to place liftable cubes - Plots[0] = Plot.Default; + Plots[0] = new Plot(new UgcMapGroup(0, + 0, + 0, + new UgcMapGroup.Cost(0, 0, 0), + new UgcMapGroup.Cost(0, 0, 0), + new UgcMapGroup.Limits(0, 0, 0, 0, 0, 0))); foreach (TriggerModel trigger in Entities.TriggerModels.Values) { AddTrigger(trigger); diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs b/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs index 33a6398ab..05ab32032 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs @@ -20,6 +20,7 @@ namespace Maple2.Server.Game.Manager.Field; public interface IField : IDisposable { public int RoomId { get; } + public int DungeonId { get; } public bool Disposed { get; } #region Autofac Autowired @@ -52,7 +53,8 @@ public interface IField : IDisposable { public RoomTimer? RoomTimer { get; } public FieldInstance FieldInstance { get; } - + public FieldType FieldType { get; } + public int Size { get; init; } public int MapId { get; init; } public long FieldTick { get; } diff --git a/Maple2.Server.Game/Manager/Items/ItemDropManager.cs b/Maple2.Server.Game/Manager/Items/ItemDropManager.cs index 6261f970e..660902a4d 100644 --- a/Maple2.Server.Game/Manager/Items/ItemDropManager.cs +++ b/Maple2.Server.Game/Manager/Items/ItemDropManager.cs @@ -102,7 +102,7 @@ public ICollection GetIndividualDropItems(GameSession session, int level, if (index > 0 && dropGroupId > 0) { if (entryDict.TryGetValue(dropGroupId, out IndividualDropItemTable.Entry? entry)) { - return GetAllGroups(session, level, new List { entry }, index).ToList(); + return GetAllGroups(session, level, [entry], index).ToList(); } } return GetAllGroups(session, level, entryDict.Values.ToList()).ToList(); diff --git a/Maple2.Server.Game/Manager/PartyManager.cs b/Maple2.Server.Game/Manager/PartyManager.cs index a5d0f6546..86efc745d 100644 --- a/Maple2.Server.Game/Manager/PartyManager.cs +++ b/Maple2.Server.Game/Manager/PartyManager.cs @@ -61,18 +61,6 @@ public void Dispose() { }); } } - - if (!CheckDisband(session.CharacterId)) { - // Find new leader - if (session.CharacterId == Party.LeaderCharacterId) { - world.Party(new PartyRequest { - RequestorId = session.CharacterId, - UpdateLeader = new PartyRequest.Types.UpdateLeader { - PartyId = Party.Id, - }, - }); - } - } } public bool SetParty(PartyInfo info) { @@ -102,7 +90,9 @@ public bool SetParty(PartyInfo info) { var party = new Party(info.Id, leader) { CreationTime = info.CreationTime, + DungeonSet = info.DungeonSet, DungeonId = info.DungeonId, + DungeonLobbyRoomId = info.DungeonRoomId, }; Party = party; foreach (PartyMember member in members) { @@ -111,6 +101,8 @@ public bool SetParty(PartyInfo info) { session.Send(PartyPacket.Load(Party)); + session.Dungeon.SetDungeon(Party.DungeonId, Party.DungeonLobbyRoomId, Party.DungeonSet); + // Listening happens after loading the party foreach (PartyMember member in party.Members.Values) { BeginListen(member); @@ -208,17 +200,17 @@ public bool Disband() { return true; } - public bool CheckDisband(long characterId) { - // Check if any other player is online - if (!Party!.Members.Values.Any(partyMember => partyMember.Info.Online && partyMember.CharacterId != characterId)) { - world.Party(new PartyRequest { - Disband = new PartyRequest.Types.Disband { - PartyId = Id, - }, - }); - return true; + public void CheckDisband() { + if (Party == null) { + return; } - return false; + + world.Party(new PartyRequest { + Disband = new PartyRequest.Types.Disband { + PartyId = Id, + }, + RequestorId = session.CharacterId, + }); } public void StartReadyCheck(long characterId) { @@ -304,6 +296,9 @@ public void SetDungeon(int dungeonId, int dungeonRoomId, bool set) { session.Dungeon.SetDungeon(dungeonId, dungeonRoomId, set); session.Send(PartyPacket.DungeonReset(set, dungeonId)); + if (Party.DungeonId == 0) { + session.Send(PartyPacket.PartyNotice(PartyMessage.s_field_enteracne_party_notify_reset_dungeon, "Field_Enterance_Reset_Dungeon")); + } } public int GuildMemberCount() { diff --git a/Maple2.Server.Game/Model/Field/Widget/OxQuizWidget.cs b/Maple2.Server.Game/Model/Field/Widget/OxQuizWidget.cs index e84bf5297..fcdf9a75f 100644 --- a/Maple2.Server.Game/Model/Field/Widget/OxQuizWidget.cs +++ b/Maple2.Server.Game/Model/Field/Widget/OxQuizWidget.cs @@ -51,7 +51,7 @@ private void ShowQuiz(string durationStr, int numericArg) { } string answer = string.Empty; if (isDevMode) { - answer = question.IsTrue ? "TRUE" : "FALSE"; + answer = question.IsTrue ? "\n[DebugMode] TRUE" : "\n[DebugMode] FALSE"; } if (!int.TryParse(durationStr, out int duration)) { duration = 15; diff --git a/Maple2.Server.Game/PacketHandlers/DungeonRoomHandler.cs b/Maple2.Server.Game/PacketHandlers/DungeonRoomHandler.cs index 25c362434..001aeb967 100644 --- a/Maple2.Server.Game/PacketHandlers/DungeonRoomHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/DungeonRoomHandler.cs @@ -43,6 +43,7 @@ public override void Handle(GameSession session, IByteReader packet) { } private void HandleReset(GameSession session) { + session.Dungeon.Reset(); } private void HandleCreate(GameSession session, IByteReader packet) { diff --git a/Maple2.Server.Game/Packets/DungeonRewardPacket.cs b/Maple2.Server.Game/Packets/DungeonRewardPacket.cs new file mode 100644 index 000000000..9d837bcff --- /dev/null +++ b/Maple2.Server.Game/Packets/DungeonRewardPacket.cs @@ -0,0 +1,32 @@ +using Maple2.Model.Game.Dungeon; +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.Packets; +using Maple2.Tools.Extensions; + +namespace Maple2.Server.Game.Packets; + +public static class DungeonRewardPacket { + private enum Command : byte { + Dungeon = 0, + MiniGame = 1, + Unknown2 = 2, + Unknown3 = 3, + } + + public static ByteWriter Dungeon(DungeonUserRecord userRecord) { + var pWriter = Packet.Of(SendOp.DungeonReward); + pWriter.Write(Command.Dungeon); + pWriter.WriteClass(userRecord); + + return pWriter; + } + + public static ByteWriter MiniGame(MiniGameUserRecord userRecord) { + var pWriter = Packet.Of(SendOp.DungeonReward); + pWriter.Write(Command.MiniGame); + pWriter.WriteClass(userRecord); + + return pWriter; + } +} diff --git a/Maple2.Server.Game/Packets/DungeonRoomPacket.cs b/Maple2.Server.Game/Packets/DungeonRoomPacket.cs index b1323bb5c..308e381f0 100644 --- a/Maple2.Server.Game/Packets/DungeonRoomPacket.cs +++ b/Maple2.Server.Game/Packets/DungeonRoomPacket.cs @@ -13,6 +13,7 @@ private enum Command : byte { Load = 5, Update = 6, Modify = 7, + DungeonResult = 11, Error = 18, RankRewards = 20, } @@ -46,6 +47,21 @@ public static ByteWriter Modify(DungeonRoomModify modifyType, int dungeonId = 0) return pWriter; } + public static ByteWriter DungeonResult(DungeonState result, Dictionary statistics) { + var pWriter = Packet.Of(SendOp.RoomDungeon); + pWriter.Write(Command.DungeonResult); + pWriter.Write(result); + pWriter.WriteInt(statistics.Count); + foreach (DungeonUserResult userResult in statistics.Values) { + pWriter.WriteLong(userResult.CharacterId); + pWriter.Write(userResult.Grade); + pWriter.Write(userResult.RecordType); + pWriter.WriteInt(userResult.Value); + } + + return pWriter; + } + public static ByteWriter Error(DungeonRoomError error, int arg = 0) { var pWriter = Packet.Of(SendOp.RoomDungeon); pWriter.Write(Command.Error); diff --git a/Maple2.Server.Game/Packets/DungeonWaitingPacket.cs b/Maple2.Server.Game/Packets/DungeonWaitingPacket.cs new file mode 100644 index 000000000..bb3f51c66 --- /dev/null +++ b/Maple2.Server.Game/Packets/DungeonWaitingPacket.cs @@ -0,0 +1,15 @@ +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.Packets; + +namespace Maple2.Server.Game.Packets; + +public static class DungeonWaitingPacket { + public static ByteWriter Set(int dungeonId, int size) { + var pWriter = Packet.Of(SendOp.DungeonWaiting); + pWriter.WriteInt(dungeonId); + pWriter.WriteInt(size); + + return pWriter; + } +} diff --git a/Maple2.Server.Game/Packets/FieldEnterPacket.cs b/Maple2.Server.Game/Packets/FieldEnterPacket.cs index b3fb22c9e..b73e3bb95 100644 --- a/Maple2.Server.Game/Packets/FieldEnterPacket.cs +++ b/Maple2.Server.Game/Packets/FieldEnterPacket.cs @@ -1,7 +1,7 @@ using System.Numerics; +using Maple2.Model.Enum; using Maple2.Model.Error; using Maple2.Model.Game; -using Maple2.Model.Metadata; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Core.Packets; @@ -16,8 +16,9 @@ public static ByteWriter Request(IActor player) { var pWriter = Packet.Of(SendOp.RequestFieldEnter); pWriter.Write(MigrationError.ok); pWriter.WriteInt(player.Value.Character.MapId); + pWriter.Write(player.Field.FieldType); pWriter.WriteClass(player.Field.FieldInstance); - pWriter.WriteInt(); + pWriter.WriteInt(player.Field.DungeonId); pWriter.Write(player.Position); pWriter.Write(player.Rotation); pWriter.WriteInt(GameSession.FIELD_KEY); diff --git a/Maple2.Server.Game/Packets/PartyPacket.cs b/Maple2.Server.Game/Packets/PartyPacket.cs index e9fd7e1e5..bb7c457ba 100644 --- a/Maple2.Server.Game/Packets/PartyPacket.cs +++ b/Maple2.Server.Game/Packets/PartyPacket.cs @@ -126,7 +126,7 @@ public static ByteWriter Load(Party party, bool joinNotify = false) { } pWriter.WriteBool(party.DungeonSet); pWriter.WriteInt(party.DungeonId); - pWriter.WriteBool(false); + pWriter.WriteBool(false); // Party Search pWriter.WriteByte(); pWriter.WriteBool(party.Search != null); if (party.Search != null) { @@ -199,12 +199,12 @@ public static ByteWriter UpdateStats(PartyMember member) { return pWriter; } - public static ByteWriter PartyNotice(PartyMessage message, string message2 = "") { + public static ByteWriter PartyNotice(PartyMessage message, string systemSoundKey = "") { var pWriter = Packet.Of(SendOp.Party); pWriter.Write(Command.DungeonNotice); pWriter.WriteUnicodeString(message.ToString()); - pWriter.WriteUnicodeString(); // "Field_Enterance_Reset_Dungeon" + pWriter.WriteUnicodeString(systemSoundKey); // "Field_Enterance_Reset_Dungeon" pWriter.WriteUnicodeString(); return pWriter; diff --git a/Maple2.Server.Game/Packets/RoomStageDungeonPacket.cs b/Maple2.Server.Game/Packets/RoomStageDungeonPacket.cs new file mode 100644 index 000000000..351b53ca0 --- /dev/null +++ b/Maple2.Server.Game/Packets/RoomStageDungeonPacket.cs @@ -0,0 +1,21 @@ +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.Packets; + +namespace Maple2.Server.Game.Packets; + +public static class RoomStageDungeonPacket { + private enum Command : byte { + Set = 0, + } + + public static ByteWriter Set(int dungeonId) { + var pWriter = Packet.Of(SendOp.RoomStageDungeon); + pWriter.Write(Command.Set); + pWriter.WriteInt(dungeonId); + pWriter.WriteInt(); + pWriter.WriteShort(); + + return pWriter; + } +} diff --git a/Maple2.Server.Game/Scripting/Scripts b/Maple2.Server.Game/Scripting/Scripts index 0a1dda00a..a5694aea9 160000 --- a/Maple2.Server.Game/Scripting/Scripts +++ b/Maple2.Server.Game/Scripting/Scripts @@ -1 +1 @@ -Subproject commit 0a1dda00a48a8523c124310274391c4ce78e7aff +Subproject commit a5694aea90d5ed1ee4c5f6e9ee06dcf6aab6810d diff --git a/Maple2.Server.Game/Service/ChannelService.Field.cs b/Maple2.Server.Game/Service/ChannelService.Field.cs new file mode 100644 index 000000000..338dedded --- /dev/null +++ b/Maple2.Server.Game/Service/ChannelService.Field.cs @@ -0,0 +1,47 @@ +using Grpc.Core; +using Maple2.Model.Error; +using Maple2.Model.Metadata; +using Maple2.Server.Game.Manager.Field; + +namespace Maple2.Server.Game.Service; + +public partial class ChannelService { + public override Task Field(FieldRequest request, ServerCallContext context) { + switch (request.FieldCase) { + case FieldRequest.FieldOneofCase.CreateDungeon: + return Task.FromResult(CreateDungeon(request.CreateDungeon, request.RequesterId)); + case FieldRequest.FieldOneofCase.DestroyDungeon: + return Task.FromResult(DestroyDungeon(request.DestroyDungeon)); + default: + return Task.FromResult(new FieldResponse()); + } + } + + private FieldResponse CreateDungeon(FieldRequest.Types.CreateDungeon create, long requestorId) { + if (!tableMetadata.DungeonRoomTable.Entries.TryGetValue(create.DungeonId, out DungeonRoomTable.DungeonRoomMetadata? dungeonRoom)) { + return new FieldResponse { + Error = (int) MigrationError.s_move_err_dungeon_not_exist, + }; + } + + DungeonFieldManager? dungeonField = server.CreateDungeon(dungeonRoom, requestorId, create.Size, create.PartyId); + if (dungeonField == null) { + return new FieldResponse { + Error = (int) MigrationError.s_move_err_FailCreateDungeon, + }; + } + + return new FieldResponse { + Error = (int) MigrationError.ok, + RoomId = dungeonField.RoomId, + }; + } + + private FieldResponse DestroyDungeon(FieldRequest.Types.DestroyDungeon destroy) { + MigrationError error = server.DestroyDungeon(destroy.RoomId); + + return new FieldResponse { + Error = (int) error, + }; + } +} diff --git a/Maple2.Server.Game/Service/ChannelService.cs b/Maple2.Server.Game/Service/ChannelService.cs index 8b2e4a3bf..0739219bb 100644 --- a/Maple2.Server.Game/Service/ChannelService.cs +++ b/Maple2.Server.Game/Service/ChannelService.cs @@ -8,14 +8,16 @@ public partial class ChannelService : Channel.Service.Channel.ChannelBase { private readonly GameServer server; private readonly PlayerInfoStorage playerInfos; private readonly GameStorage gameStorage; + private readonly TableMetadataStorage tableMetadata; private readonly ServerTableMetadataStorage serverTableMetadata; private readonly ILogger logger = Log.Logger.ForContext(); - public ChannelService(GameServer server, PlayerInfoStorage playerInfos, GameStorage gameStorage, ServerTableMetadataStorage serverTableMetadata) { + public ChannelService(GameServer server, PlayerInfoStorage playerInfos, GameStorage gameStorage, ServerTableMetadataStorage serverTableMetadata, TableMetadataStorage tableMetadata) { this.server = server; this.playerInfos = playerInfos; this.gameStorage = gameStorage; this.serverTableMetadata = serverTableMetadata; + this.tableMetadata = tableMetadata; } } diff --git a/Maple2.Server.Game/Session/GameSession.State.cs b/Maple2.Server.Game/Session/GameSession.State.cs index a72bb1c85..da03e1915 100644 --- a/Maple2.Server.Game/Session/GameSession.State.cs +++ b/Maple2.Server.Game/Session/GameSession.State.cs @@ -1,4 +1,5 @@ using Maple2.Model.Game; +using Maple2.Model.Game.Dungeon; using Maple2.Model.Game.Shop; using Maple2.Server.Game.Manager; using Maple2.Server.Game.Manager.Config; @@ -44,6 +45,8 @@ public partial class GameSession { public int BonusGameId; + public MiniGameUserRecord? MiniGameRecord; + public int SuperChatId; public int SuperChatItemId; diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index e3bfe57ca..b42a4011b 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -245,8 +245,6 @@ public bool EnterServer(long accountId, Guid machineId, MigrateInResponse migrat } Buddy.Load(); - // We load Party after partyInfo update to properly set the player's online status. - Party = new PartyManager(World, this); Send(SurvivalPacket.UpdateStats(player.Account)); @@ -302,6 +300,7 @@ public bool EnterServer(long accountId, Guid machineId, MigrateInResponse migrat Dungeon.Load(); // InGameRank Send(FieldEnterPacket.Request(Player)); + Party = new PartyManager(World, this); Send(HomeCommandPacket.LoadHome(AccountId)); // ResponseCube // Mentor @@ -330,6 +329,7 @@ private void LeaveField() { HeldLiftup = null; ActiveSkills.Clear(); NpcScript = null; + MiniGameRecord = null; if (Field != null) { Scheduler.Stop(); @@ -351,8 +351,16 @@ private bool PrepareFieldInternal(int mapId, out FieldManager? newField, int por ownerId = AccountId; } - if (Field is DungeonFieldManager dungeonField && dungeonField.RoomFields.TryGetValue(mapId, out DungeonFieldManager? nextDungeonField)) { - newField = nextDungeonField; + if (Field is DungeonFieldManager dungeonField) { + if (dungeonField.Lobby!.RoomFields.TryGetValue(mapId, out DungeonFieldManager? nextDungeonField)) { + newField = nextDungeonField; + } else if (mapId == dungeonField.Lobby.MapId) { + newField = dungeonField.Lobby; + } else { + MigrateOutOfInstance(mapId); + newField = null; + return false; + } } else { newField = FieldFactory.Get(mapId, ownerId: ownerId, roomId: roomId); if (newField == null) { @@ -411,6 +419,7 @@ public bool EnterField() { Send(EmotePacket.Load(Player.Value.Unlock.Emotes.Select(id => new Emote(id)).ToList())); Config.LoadMacros(); Config.LoadSkillCooldowns(); + Dungeon.LoadField(); Marriage.Load(); Send(CubePacket.DesignRankReward(Player.Value.Home)); @@ -512,6 +521,63 @@ public void ConditionUpdate(ConditionType conditionType, long counter = 1, strin public PremiumMarketItem? GetPremiumMarketItem(int id, int subId = 0) => server.GetPremiumMarketItem(id, subId); + public RewardRecord GetRewardContent(int id) { + if (!TableMetadata.RewardContentTable.BaseEntries.TryGetValue(id, out RewardContentTable.Base? baseMetadata)) { + return new RewardRecord(); + } + + long exp = 0; + if (baseMetadata.ExpTableId > 0) { + if (baseMetadata.ExpTableId > 100000 && TableMetadata.RewardContentTable.ExpStaticEntries.TryGetValue(baseMetadata.ExpTableId, out exp)) { + } else if (TableMetadata.ExpTable.ExpBase.TryGetValue(baseMetadata.ExpTableId, out IReadOnlyDictionary? expTable)) { + exp = expTable[Player.Value.Character.Level]; + } + if (baseMetadata.ExpFactor > 0f) { + exp = (long) (exp * baseMetadata.ExpFactor); + } + Exp.AddExp(exp); + } + + long meso = 0; + if (baseMetadata.MesoTableId > 0) { + if (baseMetadata.MesoTableId > 100000 && TableMetadata.RewardContentTable.MesoStaticEntries.TryGetValue(baseMetadata.MesoTableId, out meso)) { + } else if (TableMetadata.RewardContentTable.MesoEntries.TryGetValue(baseMetadata.MesoTableId, out Dictionary? mesoTable)) { + meso = mesoTable[Player.Value.Character.Level]; + } + if (baseMetadata.MesoFactor > 0f) { + meso = (long) (meso * baseMetadata.MesoFactor); + } + Currency.Meso += meso; + } + + long prestigeExp = 0; + if (baseMetadata.PrestigeExpTableId > 0 && ServerTableMetadata.PrestigeIdExpTable.Entries.TryGetValue(baseMetadata.PrestigeExpTableId, out PrestigeIdExpTable.Entry? prestigeExpTable)) { + // TODO: Prestige exp given this way is fixed. Need to revise how exp is given. + } + + List items = []; + if (baseMetadata.ItemTableId > 0 && TableMetadata.RewardContentTable.ItemEntries.TryGetValue(baseMetadata.ItemTableId, out RewardContentTable.Item? itemMetadata)) { + foreach (RewardContentTable.Item.Data data in itemMetadata.ItemData) { + if ((data.MinLevel > 0 && Player.Value.Character.Level < data.MinLevel) || + (data.MaxLevel > 0 && Player.Value.Character.Level > data.MaxLevel)) { + continue; + } + foreach (RewardItem rewardItem in data.RewardItems) { + Item? item = Field.ItemDrop.CreateItem(rewardItem.ItemId, rewardItem.Rarity, rewardItem.Amount); + if (item != null) { + items.Add(item); + } + } + } + foreach (Item item in items) { + if (!Item.Inventory.Add(item)) { + Item.MailItem(item); + } + } + } + return new RewardRecord(items, exp, prestigeExp, meso); + } + public void ChannelBroadcast(ByteWriter packet) { server.Broadcast(packet); } @@ -628,6 +694,8 @@ protected override void Dispose(bool disposing) { Channel = 0, Async = true, }); + + Party.CheckDisband(); } try { diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Dungeon.cs b/Maple2.Server.Game/Trigger/TriggerContext.Dungeon.cs index 8e73d23af..aa9702202 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Dungeon.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Dungeon.cs @@ -4,7 +4,17 @@ namespace Maple2.Server.Game.Trigger; public partial class TriggerContext { public void DungeonClear(string uiType) { - ErrorLog("[DungeonClear] uiType:{UiType}", uiType); + DebugLog("[DungeonClear] uiType:{UiType}", uiType); + if (Field is not DungeonFieldManager dungeonField) { + return; + } + + if (uiType == "None") { + // Do not send dungeon clear UI + return; + } + + dungeonField.ChangeState(Maple2.Model.Enum.DungeonState.Clear); } public void DungeonClearRound(int round) { @@ -107,16 +117,12 @@ public void ShadowExpeditionCloseBossGauge() { #region Conditions public bool CheckDungeonLobbyUserCount() { + DebugLog("[CheckDungeonLobbyUserCount]"); if (Field is not DungeonFieldManager dungeonField) { return false; } - if (dungeonField.Party is null) { - return Field.Players.Values.Count >= 1; - } - - DebugLog("[CheckDungeonLobbyUserCount]"); - return dungeonField.Party.Members.Count == Field.Players.Values.Count; + return Field.Players.Values.Count >= dungeonField.Size; } public bool DungeonTimeout() { @@ -125,15 +131,8 @@ public bool DungeonTimeout() { } public bool IsDungeonRoom() { - if (Field is not DungeonFieldManager dungeonField) { - return false; - } - - if (dungeonField.Party is not null) { - return true; - } DebugLog("[IsDungeonRoom]"); - return false; + return Field is DungeonFieldManager; } public bool IsPlayingMapleSurvival() { diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Interface.cs b/Maple2.Server.Game/Trigger/TriggerContext.Interface.cs index a2a9b0386..cd20d0d5e 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Interface.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Interface.cs @@ -75,6 +75,9 @@ public void SetEventUiRound(int[] rounds, int arg3, int vOffset) { int round = rounds.ElementAtOrDefault(0, 1); int maxRound = rounds.ElementAtOrDefault(1, 1); int minRound = rounds.ElementAtOrDefault(2, 1); + if (minRound == maxRound) { + return; + } Broadcast(MassiveEventPacket.Round(round, maxRound, minRound, vOffset)); } diff --git a/Maple2.Server.Game/Trigger/TriggerContext.MiniGame.cs b/Maple2.Server.Game/Trigger/TriggerContext.MiniGame.cs index c8c157306..16533fd2b 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.MiniGame.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.MiniGame.cs @@ -1,25 +1,145 @@  +using Maple2.Model.Enum; +using Maple2.Model.Game; +using Maple2.Model.Game.Dungeon; +using Maple2.Model.Metadata; using Maple2.Server.Game.Model; +using Maple2.Server.Game.Packets; using Maple2.Server.Game.Scripting.Trigger; +using Locale = Maple2.Server.Game.Scripting.Trigger.Locale; namespace Maple2.Server.Game.Trigger; public partial class TriggerContext { - public void EndMiniGame(int winnerBoxId, string gameName, bool isOnlyWinner) { } + public void EndMiniGame(int winnerBoxId, string gameName, bool isOnlyWinner) { + IEnumerable players = PlayersInBox(winnerBoxId); + foreach (FieldPlayer player in players) { + if (player.Session.MiniGameRecord == null) { + continue; + } + + if (player.Session.MiniGameRecord.ShowResult) { + player.Session.Send(DungeonRewardPacket.MiniGame(player.Session.MiniGameRecord)); + } + player.Session.MiniGameRecord = null; + } + + if (isOnlyWinner) { + return; + } + + IEnumerable losingPlayers = PlayersNotInBox(winnerBoxId); + foreach (FieldPlayer player in losingPlayers) { + if (player.Session.MiniGameRecord != null) { + if (player.Session.MiniGameRecord.ShowResult) { + player.Session.Send(DungeonRewardPacket.MiniGame(player.Session.MiniGameRecord)); + } + player.Session.MiniGameRecord = null; + } + } + } - public void EndMiniGameRound(int winnerBoxId, float expRate, float meso, bool isOnlyWinner, bool isGainLoserBonus, string gameName) { } + public void EndMiniGameRound(int winnerBoxId, float expRate, float meso, bool isOnlyWinner, bool isGainLoserBonus, string gameName) { + ExpType expType = gameName switch { + "UserMassive_Crazyrunner" or "UserMassive_Springbeach" or "UserMassive_Escape" => ExpType.userMiniGame, + _ => ExpType.miniGame, + }; + IEnumerable players = PlayersInBox(winnerBoxId); + foreach (FieldPlayer player in players) { + MiniGameUserRecord? record = player.Session.MiniGameRecord; + if (record == null) { + continue; + } + + long totalExp = player.Session.Exp.AddExp(expType, expRate); + record.Rewards[DungeonRewardType.Exp] += (int) totalExp; + + if (meso > 0.0f) { + player.Session.Currency.Meso += (long) meso; + record.Rewards[DungeonRewardType.Meso] += (int) meso; + } + record.ClearedRounds++; + } + if (isOnlyWinner) { + return; + } - public void MiniGameCameraDirection(int boxId, int cameraId) { } + IEnumerable losingPlayers = PlayersNotInBox(winnerBoxId); + foreach (FieldPlayer player in losingPlayers) { + if (player.Session.MiniGameRecord != null) { + if (isGainLoserBonus) { + long totalExp = player.Session.Exp.AddExp(ExpType.miniGame, expRate); + player.Session.MiniGameRecord.Rewards[DungeonRewardType.Exp] += (int) totalExp; + } + if (player.Session.MiniGameRecord.ShowResult) { + player.Session.Send(DungeonRewardPacket.MiniGame(player.Session.MiniGameRecord)); + } + player.Session.MiniGameRecord = null; + } + } + } - public void MiniGameGiveExp(int boxId, float expRate, bool isOutSide) { } + public void MiniGameCameraDirection(int boxId, int cameraId) { + IEnumerable players = PlayersInBox(boxId); + foreach (FieldPlayer player in players) { + player.Session.Send(CameraPacket.Local(cameraId, true)); + } + } - public void MiniGameGiveReward(int winnerBoxId, string contentType, string type) { } + public void MiniGameGiveExp(int boxId, float expRate, bool isOutSide) { + IEnumerable players = PlayersInBox(boxId); + foreach (FieldPlayer player in players) { + if (player.Session.MiniGameRecord == null) { + continue; + } + + long totalExp = player.Session.Exp.AddExp(ExpType.miniGame, expRate); + player.Session.MiniGameRecord.Rewards[DungeonRewardType.Exp] += (int) totalExp; + } + } + + public void MiniGameGiveReward(int winnerBoxId, string contentType, string type) { + if (!Constant.ContentRewards.TryGetValue(contentType, out int rewardId)) { + WarnLog("MiniGameGiveReward: {Type} not found", type); + return; + } + IEnumerable players = PlayersInBox(winnerBoxId); + foreach (FieldPlayer player in players) { + if (player.Session.MiniGameRecord == null) { + continue; + } + RewardRecord rewardRecord = player.Session.GetRewardContent(rewardId); + player.Session.MiniGameRecord.Add(rewardRecord); + } + } public void SetMiniGameAreaForHack(int boxId) { } - public void StartMiniGame(int boxId, int round, string type, bool isShowResultUi) { } + public void StartMiniGame(int boxId, int round, string type, bool isShowResultUi) { + IEnumerable players = PlayersInBox(boxId); + + foreach (FieldPlayer player in players) { + player.Session.MiniGameRecord = new MiniGameUserRecord(player.Session.CharacterId) { + TotalRounds = round, + ShowResult = isShowResultUi, + MinRound = 1, + }; + } + } - public void StartMiniGameRound(int boxId, int round) { } + public void StartMiniGameRound(int boxId, int round) { + IEnumerable players = PlayersInBox(boxId); + + foreach (FieldPlayer player in players) { + if (player.Session.MiniGameRecord == null) { + continue; + } + if (player.Session.MiniGameRecord.TotalRounds == player.Session.MiniGameRecord.MinRound) { + return; + } + player.Session.Send(MassiveEventPacket.Round(round, player.Session.MiniGameRecord.TotalRounds, player.Session.MiniGameRecord.MinRound)); + } + } public void UnsetMiniGameAreaForHack() { } diff --git a/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs b/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs index 6cc77a3a6..9b4bf0178 100644 --- a/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs +++ b/Maple2.Server.Game/Util/Sync/PlayerInfoStorage.cs @@ -43,6 +43,8 @@ public bool GetOrFetch(long characterId, [NotNullWhen(true)] out PlayerInfo? inf MapId = response.MapId, Channel = (short) response.Channel, UpdateTime = response.UpdateTime, + GuildName = response.GuildName, + GuildId = response.GuildId, }; var trophy = new AchievementInfo { Adventure = response.Trophy.Adventure, diff --git a/Maple2.Server.World/Containers/PartyManager.cs b/Maple2.Server.World/Containers/PartyManager.cs index 8695bcc1b..c65d0e856 100644 --- a/Maple2.Server.World/Containers/PartyManager.cs +++ b/Maple2.Server.World/Containers/PartyManager.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Collections.Concurrent; using Grpc.Core; using Maple2.Model.Enum; using Maple2.Model.Error; @@ -88,7 +83,8 @@ public void FindNewLeader(long characterId) { } public bool CheckForDisband() { - if (Party.Members.Count <= 2) { + if (Party.Members.Values.Count(member => member.Info.Online) < 2 || + Party.Members.Count <= 2) { PartyLookup.Disband(Party.Id); return true; } @@ -143,7 +139,7 @@ public PartyError Join(PlayerInfo info) { var member = new PartyMember { PartyId = Party.Id, - Info = info.Clone(), + Info = info, JoinTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), }; @@ -242,6 +238,17 @@ public PartyError UpdateLeader(long requestorId, long characterId) { return PartyError.none; } + public void Disband(long requestorId) { + int onlineCount = Party.Members.Values.Count(member => member.Info.Online); + if (onlineCount == 0) { + PartyLookup.Disband(Party.Id); + return; + } + if (requestorId == Party.LeaderCharacterId) { + FindNewLeader(requestorId); + } + } + public string ConsumeInvite(long characterId) { foreach ((long id, (string name, DateTime expiryTime)) in pendingInvites) { // Remove any expired entries while iterating. @@ -409,6 +416,7 @@ public PartyError SetDungeon(long requestorId, int dungeonId, bool set, int dung return PartyError.s_party_err_not_chief; } + Party.DungeonSet = set; Party.DungeonId = dungeonId; Party.DungeonLobbyRoomId = dungeonRoomId; Broadcast(new PartyRequest { diff --git a/Maple2.Server.World/Service/WorldService.Migrate.cs b/Maple2.Server.World/Service/WorldService.Migrate.cs index 3564c3f93..f41225b09 100644 --- a/Maple2.Server.World/Service/WorldService.Migrate.cs +++ b/Maple2.Server.World/Service/WorldService.Migrate.cs @@ -2,6 +2,7 @@ using System.Net; using System.Security.Cryptography; using Grpc.Core; +using Maple2.Model.Error; using Maple2.Server.Core.Constants; using Maple2.Server.Core.Helpers; using Microsoft.Extensions.Caching.Memory; @@ -98,4 +99,35 @@ private ulong UniqueToken() { return token; } + + public override Task Field(FieldRequest request, ServerCallContext context) { + switch (request.FieldCase) { + case FieldRequest.FieldOneofCase.CreateDungeon: + return Task.FromResult(CreateDungeon(request)); + case FieldRequest.FieldOneofCase.DestroyDungeon: + return Task.FromResult(DestroyDungeon(request)); + default: + return Task.FromResult(new FieldResponse { Error = (int) MigrationError.ok }); + } + } + + private FieldResponse CreateDungeon(FieldRequest request) { + if (!channelClients.TryGetClient(channelClients.FirstChannel(), out Channel.Service.Channel.ChannelClient? client)) { + return new FieldResponse { + Error = (int) MigrationError.s_move_err_no_server, + }; + } + + return client.Field(request); + } + + private FieldResponse DestroyDungeon(FieldRequest request) { + if (!channelClients.TryGetClient(channelClients.FirstChannel(), out Channel.Service.Channel.ChannelClient? client)) { + return new FieldResponse { + Error = (int) MigrationError.s_move_err_no_server, + }; + } + + return client.Field(request); + } } diff --git a/Maple2.Server.World/Service/WorldService.Party.cs b/Maple2.Server.World/Service/WorldService.Party.cs index 300385843..8fbf86d3a 100644 --- a/Maple2.Server.World/Service/WorldService.Party.cs +++ b/Maple2.Server.World/Service/WorldService.Party.cs @@ -22,7 +22,7 @@ public override Task Party(PartyRequest request, ServerCallContex case PartyRequest.PartyOneofCase.Create: return Task.FromResult(CreateParty(request.RequestorId, request.Create)); case PartyRequest.PartyOneofCase.Disband: - return Task.FromResult(DisbandParty(request.Disband)); + return Task.FromResult(DisbandParty(request.RequestorId, request.Disband)); case PartyRequest.PartyOneofCase.Invite: return Task.FromResult(InviteParty(request.RequestorId, request.Invite)); case PartyRequest.PartyOneofCase.RespondInvite: @@ -60,12 +60,12 @@ private PartyResponse CreateParty(long requestorId, PartyRequest.Types.Create cr return new PartyResponse { Party = ToPartyInfo(manager.Party) }; } - private PartyResponse DisbandParty(PartyRequest.Types.Disband disband) { - PartyError error = partyLookup.Disband(disband.PartyId); - if (error != PartyError.none) { - return new PartyResponse { Error = (int) error }; + private PartyResponse DisbandParty(long requestorId, PartyRequest.Types.Disband disband) { + if (!partyLookup.TryGet(disband.PartyId, out PartyManager? manager)) { + return new PartyResponse { Error = (int) PartyError.s_party_err_not_found }; } + manager.Disband(requestorId); return new PartyResponse { PartyId = disband.PartyId }; } diff --git a/Maple2.Server.World/Service/WorldService.Sync.cs b/Maple2.Server.World/Service/WorldService.Sync.cs index 99f94a3a0..c39e6258d 100644 --- a/Maple2.Server.World/Service/WorldService.Sync.cs +++ b/Maple2.Server.World/Service/WorldService.Sync.cs @@ -36,6 +36,8 @@ public override Task PlayerInfo(PlayerInfoRequest request, S CurrentHp = info.CurrentHp, TotalHp = info.TotalHp, }, + GuildName = info.GuildName, + GuildId = info.GuildId, Home = new HomeUpdate { Name = info.HomeName, MapId = info.PlotMapId,