diff --git a/.env.example b/.env.example index 16db42413..a8f0f7353 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,10 @@ # To use with docker, rename MS2_DATA_FOLDER to MS2_DOCKER_DATA_FOLDER MS2_DATA_FOLDER=C:/Nexon/Library/maplestory2/Data/ +# Primary language. Mainly used for searching on black market, commands, etc. +# EN, KR, CN, JP, DE, PR +LANGUAGE=EN + # Database connection DB_IP=localhost diff --git a/Maple2.Database/Context/MetadataContext.cs b/Maple2.Database/Context/MetadataContext.cs index 6090ed5ed..29146ddc9 100644 --- a/Maple2.Database/Context/MetadataContext.cs +++ b/Maple2.Database/Context/MetadataContext.cs @@ -174,6 +174,7 @@ private static void ConfigureQuestMetadata(EntityTypeBuilder buil builder.Property(quest => quest.GoToDungeon).HasJsonConversion(); builder.Property(quest => quest.Dispatch).HasJsonConversion(); builder.Property(quest => quest.Mentoring).HasJsonConversion(); + builder.Property(quest => quest.SummonPortal).HasJsonConversion(); } private static void ConfigureRideMetadata(EntityTypeBuilder builder) { diff --git a/Maple2.Database/Storage/Metadata/TableMetadataStorage.cs b/Maple2.Database/Storage/Metadata/TableMetadataStorage.cs index 7b247dfc3..8caf0f403 100644 --- a/Maple2.Database/Storage/Metadata/TableMetadataStorage.cs +++ b/Maple2.Database/Storage/Metadata/TableMetadataStorage.cs @@ -46,6 +46,9 @@ public class TableMetadataStorage { private readonly Lazy bannerTable; private readonly Lazy weddingTable; private readonly Lazy rewardContentTable; + private readonly Lazy seasonDataTable; + private readonly Lazy smartPushTable; + private readonly Lazy autoActionTable; private readonly Lazy enchantScrollTable; private readonly Lazy itemRemakeScrollTable; @@ -109,6 +112,9 @@ public class TableMetadataStorage { public BannerTable BannerTable => bannerTable.Value; public WeddingTable WeddingTable => weddingTable.Value; public RewardContentTable RewardContentTable => rewardContentTable.Value; + public SeasonDataTable SeasonDataTable => seasonDataTable.Value; + public SmartPushTable SmartPushTable => smartPushTable.Value; + public AutoActionTable AutoActionTable => autoActionTable.Value; public EnchantScrollTable EnchantScrollTable => enchantScrollTable.Value; public ItemRemakeScrollTable ItemRemakeScrollTable => itemRemakeScrollTable.Value; @@ -191,6 +197,10 @@ public TableMetadataStorage(MetadataContext context) { dungeonRankRewardTable = Retrieve(context, TableNames.DUNGEON_RANK_REWARD); dungeonConfigTable = Retrieve(context, TableNames.DUNGEON_CONFIG); dungeonMissionTable = Retrieve(context, TableNames.DUNGEON_MISSION); + seasonDataTable = Retrieve(context, TableNames.SEASON_DATA); + smartPushTable = Retrieve(context, TableNames.SMART_PUSH); + autoActionTable = Retrieve(context, TableNames.AUTO_ACTION); + } private static Lazy Retrieve(MetadataContext context, string key) where T : Table { diff --git a/Maple2.File.Ingest/Maple2.File.Ingest.csproj b/Maple2.File.Ingest/Maple2.File.Ingest.csproj index 5d3c7d3f0..fe9431516 100644 --- a/Maple2.File.Ingest/Maple2.File.Ingest.csproj +++ b/Maple2.File.Ingest/Maple2.File.Ingest.csproj @@ -19,7 +19,7 @@ - + diff --git a/Maple2.File.Ingest/Mapper/AchievementMapper.cs b/Maple2.File.Ingest/Mapper/AchievementMapper.cs index 963b7d58c..0ef1966d9 100644 --- a/Maple2.File.Ingest/Mapper/AchievementMapper.cs +++ b/Maple2.File.Ingest/Mapper/AchievementMapper.cs @@ -26,7 +26,7 @@ protected override IEnumerable Map() { Value: grade.condition.value == 0 ? 1 : grade.condition.value, Codes: grade.condition.code.ConvertCodes(), Target: grade.condition.target.ConvertCodes()), - Reward: grade.reward.type == AchieveRewardType.unknown ? null : new AchievementMetadataReward( + Reward: grade.reward is null || grade.reward.type == AchieveRewardType.unknown ? null : new AchievementMetadataReward( Type: (AchievementRewardType) grade.reward.type, Code: grade.reward.code, Value: grade.reward.value, diff --git a/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs b/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs index ebab28ffb..428651366 100644 --- a/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs +++ b/Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs @@ -222,7 +222,7 @@ private static AdditionalEffectMetadataStatus Convert(StatusProperty status, Off Element: (Element) dotDamage.element, UseGrade: dotDamage.useGrade, Rate: dotDamage.rate, - HpValue: dotDamage.value, + HpValue: (int) dotDamage.value, SpValue: dotDamage.spValue, EpValue: dotDamage.epValue, DamageByTargetMaxHp: dotDamage.damageByTargetMaxHP, diff --git a/Maple2.File.Ingest/Mapper/ItemMapper.cs b/Maple2.File.Ingest/Mapper/ItemMapper.cs index b81f6541f..24724d99b 100644 --- a/Maple2.File.Ingest/Mapper/ItemMapper.cs +++ b/Maple2.File.Ingest/Mapper/ItemMapper.cs @@ -13,10 +13,12 @@ namespace Maple2.File.Ingest.Mapper; public class ItemMapper : TypeMapper { private readonly ItemParser parser; private readonly TableParser tableParser; + private readonly bool newXml; - public ItemMapper(M2dReader xmlReader) { - parser = new ItemParser(xmlReader); - tableParser = new TableParser(xmlReader); + public ItemMapper(M2dReader xmlReader, string language, bool newXml) { + parser = new ItemParser(xmlReader, language); + tableParser = new TableParser(xmlReader, language); + this.newXml = newXml; } protected override IEnumerable Map() { @@ -31,7 +33,7 @@ protected override IEnumerable Map() { } } - foreach ((int id, string name, ItemData data) in parser.Parse()) { + foreach ((int id, string name, ItemData data) in parser.Parse()) { int transferType = data.limit.transferType; int tradableCount = data.property.tradableCount; int tradableCountDeduction = data.property.tradableCountDeduction; diff --git a/Maple2.File.Ingest/Mapper/MapMapper.cs b/Maple2.File.Ingest/Mapper/MapMapper.cs index 476975e15..7fcd6dc12 100644 --- a/Maple2.File.Ingest/Mapper/MapMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapMapper.cs @@ -11,9 +11,9 @@ public class MapMapper : TypeMapper { private readonly TableParser spawnParser; private readonly MapParser parser; - public MapMapper(M2dReader xmlReader) { - spawnParser = new TableParser(xmlReader); - parser = new MapParser(xmlReader); + public MapMapper(M2dReader xmlReader, string language) { + spawnParser = new TableParser(xmlReader, language); + parser = new MapParser(xmlReader, language); } protected override IEnumerable Map() { diff --git a/Maple2.File.Ingest/Mapper/NpcMapper.cs b/Maple2.File.Ingest/Mapper/NpcMapper.cs index ddba15d26..11af95275 100644 --- a/Maple2.File.Ingest/Mapper/NpcMapper.cs +++ b/Maple2.File.Ingest/Mapper/NpcMapper.cs @@ -12,8 +12,8 @@ namespace Maple2.File.Ingest.Mapper; public class NpcMapper : TypeMapper { private readonly NpcParser parser; - public NpcMapper(M2dReader xmlReader) { - parser = new NpcParser(xmlReader); + public NpcMapper(M2dReader xmlReader, string language) { + parser = new NpcParser(xmlReader, language); } protected override IEnumerable Map() { diff --git a/Maple2.File.Ingest/Mapper/QuestMapper.cs b/Maple2.File.Ingest/Mapper/QuestMapper.cs index 059dd84c8..cea97ec6a 100644 --- a/Maple2.File.Ingest/Mapper/QuestMapper.cs +++ b/Maple2.File.Ingest/Mapper/QuestMapper.cs @@ -14,8 +14,8 @@ namespace Maple2.File.Ingest.Mapper; public class QuestMapper : TypeMapper { private readonly QuestParser parser; - public QuestMapper(M2dReader xmlReader) { - parser = new QuestParser(xmlReader); + public QuestMapper(M2dReader xmlReader, string language) { + parser = new QuestParser(xmlReader, language); } protected override IEnumerable Map() { @@ -80,10 +80,14 @@ protected override IEnumerable Map() { PortalId: data.dispatch.portal, Script: data.dispatch.script ), - Mentoring: data.mentoringMission == null ? null : new QuestMentoringMission( + Mentoring: data.mentoringMission == null || string.IsNullOrEmpty(data.mentoringMission.mentoringIcon) ? null : new QuestMentoringMission( OpeningDay: data.mentoringMission.openingDay, Season: data.mentoringMission.mentoringSeason ), + SummonPortal: data.summonPortal is { fieldID: 0, portalID: 0 } ? null : new QuestSummonPortal( + MapId: data.summonPortal.fieldID, + PortalId: data.summonPortal.portalID + ), EventMissionType: Enum.TryParse(data.eventMission.@event, true, out QuestEventMissionType eventMissionType) ? eventMissionType : QuestEventMissionType.none, Conditions: data.condition.Select(condition => new ConditionMetadata( Type: (ConditionType) condition.type, diff --git a/Maple2.File.Ingest/Mapper/ScriptMapper.cs b/Maple2.File.Ingest/Mapper/ScriptMapper.cs index c61ff4a26..e885fde18 100644 --- a/Maple2.File.Ingest/Mapper/ScriptMapper.cs +++ b/Maple2.File.Ingest/Mapper/ScriptMapper.cs @@ -13,8 +13,8 @@ namespace Maple2.File.Ingest.Mapper; public class ScriptMapper : TypeMapper { private readonly ScriptParser parser; - public ScriptMapper(M2dReader xmlReader) { - parser = new ScriptParser(xmlReader); + public ScriptMapper(M2dReader xmlReader, string language) { + parser = new ScriptParser(xmlReader, language); } protected override IEnumerable Map() { diff --git a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs index c4e504759..468b2ec17 100644 --- a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs +++ b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs @@ -1384,6 +1384,20 @@ DayOfWeek ParseDayOfWeek(Maple2.File.Parser.Enum.DayOfWeek dayofWeek) { return new MapleSurvivalOpenPeriod(); case GameEventType.ShutdownMapleSurvival: return new ShutdownMapleSurvival(); + case GameEventType.SaleAutoPlayInstrument: + if (!int.TryParse(value1, out int performanceDiscount) && string.IsNullOrEmpty(value2)) { + return null; + } + return new SaleAutoPlayInstrument( + Discount: performanceDiscount, + ContentType: value2); + case GameEventType.SaleAutoFishing: + if (!int.TryParse(value1, out int fishingDiscount) && string.IsNullOrEmpty(value2)) { + return null; + } + return new SaleAutoFishing( + Discount: fishingDiscount, + ContentType: value2); default: return null; } diff --git a/Maple2.File.Ingest/Mapper/SkillMapper.cs b/Maple2.File.Ingest/Mapper/SkillMapper.cs index c163a458e..b57ad891c 100644 --- a/Maple2.File.Ingest/Mapper/SkillMapper.cs +++ b/Maple2.File.Ingest/Mapper/SkillMapper.cs @@ -12,8 +12,8 @@ namespace Maple2.File.Ingest.Mapper; public class SkillMapper : TypeMapper { private readonly SkillParser parser; - public SkillMapper(M2dReader xmlReader) { - parser = new SkillParser(xmlReader); + public SkillMapper(M2dReader xmlReader, string language) { + parser = new SkillParser(xmlReader, language); } protected override IEnumerable Map() { diff --git a/Maple2.File.Ingest/Mapper/TableMapper.cs b/Maple2.File.Ingest/Mapper/TableMapper.cs index e0920307f..fcf294c95 100644 --- a/Maple2.File.Ingest/Mapper/TableMapper.cs +++ b/Maple2.File.Ingest/Mapper/TableMapper.cs @@ -38,9 +38,10 @@ namespace Maple2.File.Ingest.Mapper; public class TableMapper : TypeMapper { private readonly TableParser parser; private readonly ItemOptionParser optionParser; + private readonly string language; - public TableMapper(M2dReader xmlReader) { - parser = new TableParser(xmlReader); + public TableMapper(M2dReader xmlReader, string language) { + parser = new TableParser(xmlReader, language); optionParser = new ItemOptionParser(xmlReader); } @@ -82,6 +83,8 @@ protected override IEnumerable Map() { yield return new TableMetadata { Name = TableNames.UGC_HOUSING_POINT_REWARD, Table = ParseUgcHousingPointRewardTable() }; yield return new TableMetadata { Name = TableNames.REWARD_CONTENT, Table = ParseRewardContentTable() }; yield return new TableMetadata { Name = TableNames.SEASON_DATA, Table = ParseSeasonDataTable() }; + yield return new TableMetadata { Name = TableNames.SMART_PUSH, Table = ParseSmartPushTable() }; + yield return new TableMetadata { Name = TableNames.AUTO_ACTION, Table = ParseAutoActionTable() }; // Marriage/Wedding yield return new TableMetadata { Name = TableNames.WEDDING, Table = ParseWeddingTable() }; @@ -1771,8 +1774,53 @@ private SeasonDataTable ParseSeasonDataTable() { seasonData.grade7, ])); } - return results; } } + + private SmartPushTable ParseSmartPushTable() { + var results = new Dictionary(); + foreach ((int id, SmartPush smartPush) in parser.ParseSmartPush()) { + var requiredItem = new IngredientInfo(ItemTag.None, 0); + var requiredItemTag = ItemTag.None; + if (smartPush.requireItem.Length != 0) { + string[] requiredItemArray = smartPush.requireItem[0].Split(":"); + if (requiredItemArray.Length == 2) { + requiredItemTag = Enum.TryParse(requiredItemArray[1], out ItemTag tag) ? tag : ItemTag.None; + } + requiredItem = new IngredientInfo( + tag: requiredItemTag, + amount: int.Parse(smartPush.requireItem[2])); + } + + results.Add(id, new SmartPushMetadata( + Id: id, + Content: smartPush.content, + Type: Enum.TryParse(smartPush.actionType, out SmartPushType type) ? type : SmartPushType.none, + Value: smartPush.actionValue, + MeretCost: smartPush.requireMerat, + RequiredItem: requiredItem)); + } + return new SmartPushTable(results); + } + + private AutoActionTable ParseAutoActionTable() { + var results = new Dictionary>(); + IEnumerable> groups = parser.ParseAutoActionPricePackage() + .Select(entry => entry.Data) + .GroupBy(entry => entry.content); + foreach (IGrouping group in groups) { + var packages = new Dictionary(); + foreach (AutoActionPricePackage package in group) { + packages.Add(package.id, new AutoActionMetaData( + Content: package.content, + Id: package.id, + Duration: package.duration, + MeretCost: package.merat, + MesoCost: package.meso)); + } + results.Add(group.Key, packages); + } + return new AutoActionTable(results); + } } diff --git a/Maple2.File.Ingest/Program.cs b/Maple2.File.Ingest/Program.cs index a5e396fb1..816f2e73d 100644 --- a/Maple2.File.Ingest/Program.cs +++ b/Maple2.File.Ingest/Program.cs @@ -17,6 +17,7 @@ using Microsoft.EntityFrameworkCore; const string locale = "NA"; +string language = "en"; const string env = "Live"; Console.OutputEncoding = System.Text.Encoding.UTF8; @@ -36,10 +37,16 @@ } // Force Globalization to en-US because we use periods instead of commas for decimals -CultureInfo.CurrentCulture = new("en-US"); +CultureInfo.CurrentCulture = new CultureInfo("en-US"); DotEnv.Load(); +string? languageEnv = Environment.GetEnvironmentVariable("LANGUAGE"); +if (languageEnv == null) { + throw new ArgumentException("LANGUAGE environment variable was not set"); +} +language = languageEnv.ToLower(); + string? ms2Root = Environment.GetEnvironmentVariable("MS2_DATA_FOLDER"); if (ms2Root == null) { throw new ArgumentException("MS2_DATA_FOLDER environment variable was not set"); @@ -193,17 +200,17 @@ UpdateDatabase(metadataContext, new AdditionalEffectMapper(xmlReader)); UpdateDatabase(metadataContext, new AnimationMapper(xmlReader)); -UpdateDatabase(metadataContext, new ItemMapper(xmlReader)); -UpdateDatabase(metadataContext, new NpcMapper(xmlReader)); +UpdateDatabase(metadataContext, new ItemMapper(xmlReader, language, false)); +UpdateDatabase(metadataContext, new NpcMapper(xmlReader, language)); UpdateDatabase(metadataContext, new PetMapper(xmlReader)); -UpdateDatabase(metadataContext, new MapMapper(xmlReader)); +UpdateDatabase(metadataContext, new MapMapper(xmlReader, language)); UpdateDatabase(metadataContext, new UgcMapMapper(xmlReader)); UpdateDatabase(metadataContext, new ExportedUgcMapMapper(xmlReader)); -UpdateDatabase(metadataContext, new QuestMapper(xmlReader)); +UpdateDatabase(metadataContext, new QuestMapper(xmlReader, language)); UpdateDatabase(metadataContext, new RideMapper(xmlReader)); -UpdateDatabase(metadataContext, new ScriptMapper(xmlReader)); -UpdateDatabase(metadataContext, new SkillMapper(xmlReader)); -UpdateDatabase(metadataContext, new TableMapper(xmlReader)); +UpdateDatabase(metadataContext, new ScriptMapper(xmlReader, language)); +UpdateDatabase(metadataContext, new SkillMapper(xmlReader, language)); +UpdateDatabase(metadataContext, new TableMapper(xmlReader, language)); UpdateDatabase(metadataContext, new AchievementMapper(xmlReader)); UpdateDatabase(metadataContext, new FunctionCubeMapper(xmlReader)); diff --git a/Maple2.Model/Common/TableNames.cs b/Maple2.Model/Common/TableNames.cs index dd6b6ab95..6df45fdaf 100644 --- a/Maple2.Model/Common/TableNames.cs +++ b/Maple2.Model/Common/TableNames.cs @@ -40,6 +40,8 @@ public static class TableNames { public const string WEDDING = "wedding*.xml"; public const string REWARD_CONTENT = "rewardcontent*.xml"; public const string SEASON_DATA = "seasondata*.xml"; + public const string SMART_PUSH = "smartpush.xml"; + public const string AUTO_ACTION = "autoactionpricepackage.xml"; // Prestige / Adventure public const string PRESTIGE_LEVEL_ABILITY = "adventurelevelability.xml"; diff --git a/Maple2.Model/Enum/CurrencyType.cs b/Maple2.Model/Enum/CurrencyType.cs index 92b3ad454..916f06ff2 100644 --- a/Maple2.Model/Enum/CurrencyType.cs +++ b/Maple2.Model/Enum/CurrencyType.cs @@ -12,3 +12,9 @@ public enum CurrencyType : byte { StarPoint = 12, MesoToken = 13, } + +public enum SmartPushCurrencyType { + None = 0, + Meso = 1, + Meret = 2, +} diff --git a/Maple2.Model/Enum/Portal.cs b/Maple2.Model/Enum/Portal.cs index 84fd68be0..76293e914 100644 --- a/Maple2.Model/Enum/Portal.cs +++ b/Maple2.Model/Enum/Portal.cs @@ -5,6 +5,7 @@ public enum PortalType : byte { DungeonReturnToLobby = 1, Event = 3, // Pocket Realms & Player Hosted Mini games FieldToHome = 5, + Quest = 6, Boss = 8, DungeonEnter = 9, InHome = 11, diff --git a/Maple2.Model/Enum/SmartPushType.cs b/Maple2.Model/Enum/SmartPushType.cs new file mode 100644 index 000000000..a9b274998 --- /dev/null +++ b/Maple2.Model/Enum/SmartPushType.cs @@ -0,0 +1,7 @@ +namespace Maple2.Model.Enum; + +public enum SmartPushType { + none, + additionalEffect, + autoInteraction +} diff --git a/Maple2.Model/Game/Event/GameEvent.cs b/Maple2.Model/Game/Event/GameEvent.cs index d4287d0e1..2234c8880 100644 --- a/Maple2.Model/Game/Event/GameEvent.cs +++ b/Maple2.Model/Game/Event/GameEvent.cs @@ -234,7 +234,16 @@ public void WriteTo(IByteWriter writer) { writer.WriteInt(steps); writer.Write(rewardItem); } - + break; + case SaleAutoFishing fishing: + writer.WriteInt(Id); + writer.WriteInt(fishing.Discount); + writer.WriteUnicodeString(fishing.ContentType); + break; + case SaleAutoPlayInstrument instrument: + writer.WriteInt(Id); + writer.WriteInt(); + writer.WriteUnicodeString(instrument.ContentType); break; } } diff --git a/Maple2.Model/Metadata/AdditionalEffectMetadata.cs b/Maple2.Model/Metadata/AdditionalEffectMetadata.cs index 72b3eb691..964d8517b 100644 --- a/Maple2.Model/Metadata/AdditionalEffectMetadata.cs +++ b/Maple2.Model/Metadata/AdditionalEffectMetadata.cs @@ -79,7 +79,7 @@ public record AdditionalEffectMetadataStatus( IReadOnlyDictionary SpecialValues, IReadOnlyDictionary SpecialRates, IReadOnlyDictionary Resistances, - long DeathResistanceHp, + bool DeathResistanceHp, AdditionalEffectMetadataStatus.CompulsionEvent? Compulsion, AdditionalEffectMetadataStatus.StatConversion? Conversion, int ImmuneBreak, diff --git a/Maple2.Model/Metadata/QuestMetadata.cs b/Maple2.Model/Metadata/QuestMetadata.cs index 69dfa3c9b..518f6b77c 100644 --- a/Maple2.Model/Metadata/QuestMetadata.cs +++ b/Maple2.Model/Metadata/QuestMetadata.cs @@ -16,6 +16,7 @@ public record QuestMetadata( QuestMetadataGoToDungeon GoToDungeon, QuestDispatch? Dispatch, QuestMentoringMission? Mentoring, + QuestSummonPortal? SummonPortal, QuestEventMissionType EventMissionType, ConditionMetadata[] Conditions) : ISearchResult; @@ -90,3 +91,7 @@ public record QuestMentoringMission( int OpeningDay, int Season); +public record QuestSummonPortal( + int MapId, + int PortalId); + diff --git a/Maple2.Model/Metadata/ServerTable/GameEventTable.cs b/Maple2.Model/Metadata/ServerTable/GameEventTable.cs index e1e9f6d32..c9a3e0cbc 100644 --- a/Maple2.Model/Metadata/ServerTable/GameEventTable.cs +++ b/Maple2.Model/Metadata/ServerTable/GameEventTable.cs @@ -255,6 +255,14 @@ public record DailyLoginReward( int MailId, RewardItem Item) : GameEventData; +public record SaleAutoPlayInstrument( + int Discount, + string ContentType) : GameEventData; + +public record SaleAutoFishing( + int Discount, + string ContentType) : GameEventData; + public record LoginNotice : GameEventData; [JsonPolymorphic(TypeDiscriminatorPropertyName = "!")] @@ -288,4 +296,6 @@ public record LoginNotice : GameEventData; [JsonDerivedType(typeof(ShutdownMapleSurvival), "ShutdownMapleSurvival")] [JsonDerivedType(typeof(Ontime), "Ontime")] [JsonDerivedType(typeof(DailyLoginReward), "DailyLoginReward")] +[JsonDerivedType(typeof(SaleAutoPlayInstrument), "SaleAutoPlayInstrument")] +[JsonDerivedType(typeof(SaleAutoFishing), "SaleAutoFishing")] public abstract record GameEventData; diff --git a/Maple2.Model/Metadata/Table/AutoActionTable.cs b/Maple2.Model/Metadata/Table/AutoActionTable.cs new file mode 100644 index 000000000..679f7465a --- /dev/null +++ b/Maple2.Model/Metadata/Table/AutoActionTable.cs @@ -0,0 +1,10 @@ +namespace Maple2.Model.Metadata; + +public record AutoActionTable(IReadOnlyDictionary> Entries) : Table; + +public record AutoActionMetaData( + string Content, + int Id, + int Duration, + long MeretCost, + long MesoCost); diff --git a/Maple2.Model/Metadata/Table/ItemSocketTable.cs b/Maple2.Model/Metadata/Table/ItemSocketTable.cs index c1b25a7ea..4fec7f4eb 100644 --- a/Maple2.Model/Metadata/Table/ItemSocketTable.cs +++ b/Maple2.Model/Metadata/Table/ItemSocketTable.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Maple2.Model.Metadata; +namespace Maple2.Model.Metadata; public record ItemSocketTable(IReadOnlyDictionary> Entries) : Table; diff --git a/Maple2.Model/Metadata/Table/SmartPushTable.cs b/Maple2.Model/Metadata/Table/SmartPushTable.cs new file mode 100644 index 000000000..104511189 --- /dev/null +++ b/Maple2.Model/Metadata/Table/SmartPushTable.cs @@ -0,0 +1,14 @@ +using Maple2.Model.Enum; +using Maple2.Model.Game; + +namespace Maple2.Model.Metadata; + +public record SmartPushTable(IReadOnlyDictionary Entries) : Table; + +public record SmartPushMetadata( + int Id, + string Content, + SmartPushType Type, + long Value, + long MeretCost, + IngredientInfo RequiredItem); diff --git a/Maple2.Model/Metadata/TableMetadata.cs b/Maple2.Model/Metadata/TableMetadata.cs index 3b616e7c2..5cf278918 100644 --- a/Maple2.Model/Metadata/TableMetadata.cs +++ b/Maple2.Model/Metadata/TableMetadata.cs @@ -83,4 +83,6 @@ public override int GetHashCode() { [JsonDerivedType(typeof(DungeonMissionTable), typeDiscriminator: "dungeonmission")] [JsonDerivedType(typeof(RewardContentTable), typeDiscriminator: "rewardcontent")] [JsonDerivedType(typeof(SeasonDataTable), typeDiscriminator: "seasondata")] +[JsonDerivedType(typeof(SmartPushTable), typeDiscriminator: "smartpush")] +[JsonDerivedType(typeof(AutoActionTable), typeDiscriminator: "autoaction")] public abstract record Table; diff --git a/Maple2.Server.Core/proto/channel/channel.proto b/Maple2.Server.Core/proto/channel/channel.proto index e60fa4614..7c3911cb4 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 { + // Retrieve information about channels. + rpc UpdateChannels(ChannelsUpdateRequest) returns (ChannelsUpdateResponse); // Manage field rpc Field(maple2.FieldRequest) returns (maple2.FieldResponse); // Send a chat message. @@ -48,6 +50,14 @@ service Channel { rpc Heartbeat(maple2.HeartbeatRequest) returns (maple2.HeartbeatResponse); } +message ChannelsUpdateRequest { + repeated int32 channels = 1; +} + +message ChannelsUpdateResponse { + int32 error = 1; +} + message GuildRequest { message Invite { string guild_name = 1; diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index 994b6b79f..ab1f55730 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -170,6 +170,22 @@ public FieldPortal SpawnPortal(Portal portal, int roomId, Vector3 position = def return fieldPortal; } + 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, + 0, true, false, true); + var fieldPortal = new FieldQuestPortal(owner, this, NextLocalId(), portal) { + Position = portal.Position, + Rotation = portal.Rotation, + EndTick = (int) (FieldTick + TimeSpan.FromSeconds(Constant.QuestPortalKeepTime).TotalMilliseconds), + StartTick = (int) FieldTick, + Model = Constant.QuestPortalKeepNif, + }; + fieldPortals[fieldPortal.ObjectId] = fieldPortal; + + return fieldPortal; + } + public FieldPortal? SpawnCubePortal(PlotCube plotCube) { int targetMapId = MapId; long targetHomeAccountId = 0; @@ -669,6 +685,7 @@ public virtual bool RemovePlayer(int objectId, [NotNullWhen(true)] out FieldPlay CommitPlot(fieldPlayer.Session); Broadcast(FieldPacket.RemovePlayer(objectId)); Broadcast(ProxyObjectPacket.RemovePlayer(objectId)); + return true; } @@ -765,7 +782,11 @@ public bool RemovePortal(int objectId) { return false; } - Broadcast(PortalPacket.Remove(portal.Value.Id)); + if (portal is FieldQuestPortal questPortal) { + questPortal.Owner.Session.Send(PortalPacket.Remove(questPortal.Value.Id)); + } else { + Broadcast(PortalPacket.Remove(portal.Value.Id)); + } return true; } @@ -839,7 +860,16 @@ public void OnAddPlayer(FieldPlayer added) { added.Session.Send(FieldPacket.AddPet(fieldPet)); } foreach (FieldPortal fieldPortal in fieldPortals.Values) { - added.Session.Send(PortalPacket.Add(fieldPortal)); + switch (fieldPortal) { + case FieldQuestPortal questPortal: + if (questPortal.Owner.ObjectId == added.ObjectId) { + added.Session.Send(PortalPacket.Add(questPortal)); + } + continue; + default: + added.Session.Send(PortalPacket.Add(fieldPortal)); + continue; + } } // ProxyGameObj foreach (FieldPlayer fieldPlayer in Players.Values) { diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs index 5a8977fb2..5a1b23166 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs @@ -516,6 +516,9 @@ public bool UsePortal(GameSession session, int portalId, string password) { ? FieldEnterPacket.Request(session.Player) : FieldEnterPacket.Error(MigrationError.s_move_err_default)); return true; + case PortalType.Quest: + session.Field.RemovePortal(fieldPortal.ObjectId); + break; } diff --git a/Maple2.Server.Game/Manager/Items/InventoryManager.cs b/Maple2.Server.Game/Manager/Items/InventoryManager.cs index 5e1f4013f..482762607 100644 --- a/Maple2.Server.Game/Manager/Items/InventoryManager.cs +++ b/Maple2.Server.Game/Manager/Items/InventoryManager.cs @@ -417,9 +417,9 @@ public bool ConsumeItemComponents(IReadOnlyList components, int q lock (session.Item) { // Check for components - Dictionary> materialsById = components.ToDictionary( + Dictionary> materialsById = components.ToDictionary( ingredient => ingredient.ItemId, - ingredient => session.Item.Inventory.Find(ingredient.ItemId, ingredient.Rarity).ToList() + ingredient => Filter(item => !item.IsExpired() && item.Id == ingredient.ItemId && item.Rarity == ingredient.Rarity) ); var materialsByTag = new Dictionary>(); foreach (ItemComponent ingredient in components) { diff --git a/Maple2.Server.Game/Manager/MasteryManager.cs b/Maple2.Server.Game/Manager/MasteryManager.cs index 3b6f2deb8..398886be1 100644 --- a/Maple2.Server.Game/Manager/MasteryManager.cs +++ b/Maple2.Server.Game/Manager/MasteryManager.cs @@ -103,47 +103,47 @@ public short GetLevel(MasteryType type) { } public void Gather(FieldInteract fieldInteract) { - ByteWriter successPacket = InteractObjectPacket.Interact(fieldInteract, decreaseAmount: 1); - ByteWriter failPacket = InteractObjectPacket.Interact(fieldInteract, GatherResult.Fail, decreaseAmount: 0); - - GatherCommon(fieldInteract.Value.Item.RecipeId, successPacket, failPacket, fieldInteract.Position, fieldInteract.Rotation); + if (GatherCommon(fieldInteract.Value.Item.RecipeId, fieldInteract.Position, fieldInteract.Rotation)) { + session.Send(InteractObjectPacket.Interact(fieldInteract, decreaseAmount: 1)); + } else { + session.Send(InteractObjectPacket.Interact(fieldInteract, GatherResult.Fail, decreaseAmount: 0)); + } } public void Gather(FieldFunctionInteract fieldFunctionInteract) { - ByteWriter successPacket = FunctionCubePacket.SuccessLifeSkill(session.CharacterId, fieldFunctionInteract.InteractCube); - ByteWriter failPacket = FunctionCubePacket.FailLifeSkill(session.CharacterId, fieldFunctionInteract.InteractCube); - - GatherCommon(fieldFunctionInteract.Value.RecipeId, successPacket, failPacket, fieldFunctionInteract.Position, fieldFunctionInteract.Rotation); + if (GatherCommon(fieldFunctionInteract.Value.RecipeId, fieldFunctionInteract.Position, fieldFunctionInteract.Rotation)) { + session.Send(FunctionCubePacket.SuccessLifeSkill(session.CharacterId, fieldFunctionInteract.InteractCube)); + } else { + session.Send(FunctionCubePacket.FailLifeSkill(session.CharacterId, fieldFunctionInteract.InteractCube)); + } } - private void GatherCommon(int recipeId, ByteWriter successPacket, ByteWriter failPacket, Vector3 position, Vector3 rotation) { + private bool GatherCommon(int recipeId, Vector3 position, Vector3 rotation) { if (!session.TableMetadata.MasteryRecipeTable.Entries.TryGetValue(recipeId, out MasteryRecipeTable.Entry? recipeMetadata)) { session.Send(MasteryPacket.Error(MasteryError.s_mastery_error_unknown)); - return; + return false; } if (recipeMetadata.RequiredMastery > this[recipeMetadata.Type]) { session.Send(MasteryPacket.Error(MasteryError.s_mastery_error_lack_mastery)); - return; - } - - if (!gatheringCounts.TryGetValue(recipeMetadata.Id, out int currentCount)) { - gatheringCounts[recipeMetadata.Id] = 0; + return false; } + float successRate = GatherSuccessRate(recipeMetadata); - int myHome = 1; // For outside gathering, it should be always 1 - if (session.Field is HomeFieldManager homeField) { - myHome = homeField.OwnerId == session.AccountId ? 1 : 0; + BeforeGather(recipeMetadata); + if (Random.Shared.NextDouble() > (successRate / 100)) { + return false; } - float successRate = Lua.CalcGatheringObjectSuccessRate(currentCount, recipeMetadata.HighRateLimitCount, recipeMetadata.NormalRateLimitCount, myHome); - int gatheringAmount = 0; - BeforeConditionUpdate(recipeMetadata); - if (Random.Shared.NextDouble() > (successRate / 100)) { - session.Send(failPacket); - return; + Gather(recipeMetadata, position, rotation); + this[recipeMetadata.Type] += recipeMetadata.RewardMastery; + if (!recipeMetadata.NoRewardExp) { + session.Exp.AddExp(ExpType.gathering); } + return true; + } + public void Gather(MasteryRecipeTable.Entry recipeMetadata, Vector3 position, Vector3 rotation) { foreach (ItemComponent itemReward in recipeMetadata.RewardItems) { Item? item = session.Field.ItemDrop.CreateItem(itemReward.ItemId, itemReward.Rarity, itemReward.Amount); if (item == null) { @@ -153,18 +153,22 @@ private void GatherCommon(int recipeId, ByteWriter successPacket, ByteWriter fai session.Field.Broadcast(FieldPacket.DropItem(fieldItem)); } - gatheringAmount++; - AfterConditionUpdate(recipeMetadata, gatheringAmount); + AfterGather(recipeMetadata, 1); + } - this[recipeMetadata.Type] += recipeMetadata.RewardMastery; - if (!recipeMetadata.NoRewardExp) { - session.Exp.AddExp(ExpType.gathering); + public float GatherSuccessRate(MasteryRecipeTable.Entry recipeMetadata) { + if (!gatheringCounts.TryGetValue(recipeMetadata.Id, out int currentCount)) { + gatheringCounts[recipeMetadata.Id] = 0; } - gatheringCounts[recipeMetadata.Id] += gatheringAmount; - session.Send(successPacket); + + int myHome = 1; // For outside gathering, it should be always 1 + if (session.Field is HomeFieldManager homeField) { + myHome = homeField.OwnerId == session.AccountId ? 1 : 0; + } + return Lua.CalcGatheringObjectSuccessRate(currentCount, recipeMetadata.HighRateLimitCount, recipeMetadata.NormalRateLimitCount, myHome); } - private void AfterConditionUpdate(MasteryRecipeTable.Entry recipeMetadata, int gatheringAmount) { + private void AfterGather(MasteryRecipeTable.Entry recipeMetadata, int gatheringAmount) { switch (recipeMetadata.Type) { case MasteryType.Breeding: case MasteryType.Farming: @@ -181,9 +185,11 @@ private void AfterConditionUpdate(MasteryRecipeTable.Entry recipeMetadata, int g session.ConditionUpdate(ConditionType.mastery_gathering, counter: gatheringAmount, codeLong: recipeMetadata.Id); break; } + + gatheringCounts[recipeMetadata.Id] += 1; } - private void BeforeConditionUpdate(MasteryRecipeTable.Entry recipeMetadata) { + public void BeforeGather(MasteryRecipeTable.Entry recipeMetadata) { switch (recipeMetadata.Type) { case MasteryType.Farming: case MasteryType.Breeding: diff --git a/Maple2.Server.Game/Manager/QuestManager.cs b/Maple2.Server.Game/Manager/QuestManager.cs index 34efd9f93..cda394f17 100644 --- a/Maple2.Server.Game/Manager/QuestManager.cs +++ b/Maple2.Server.Game/Manager/QuestManager.cs @@ -8,6 +8,7 @@ using Maple2.Model.Game; using Maple2.Model.Game.Event; using Maple2.Model.Metadata; +using Maple2.Server.Game.Model; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; using Maple2.Server.Game.Util; @@ -167,6 +168,9 @@ public QuestError Start(int questId, bool bypassRequirements = false) { session.ConditionUpdate(ConditionType.quest_accept, codeLong: quest.Id); session.Send(QuestPacket.Start(quest)); + if (quest.Metadata.SummonPortal != null) { + SummonPortal(quest); + } return QuestError.none; } @@ -583,6 +587,17 @@ public void LevelPotion(int level, int lastQuest = 0) { Load(); } + private void SummonPortal(Quest quest) { + if (session.NpcScript?.Npc == null) { + logger.Warning("Cannot summon quest portal for quest {QuestId}: No NPC script context", quest.Id); + return; + } + + FieldPortal portal = session.Field.SpawnPortal(quest.Metadata.SummonPortal!, session.NpcScript.Npc, session.Player); + session.Send(PortalPacket.Add(portal)); + session.Send(QuestPacket.SummonPortal(session.NpcScript.Npc.ObjectId, portal.Value.Id, portal.StartTick)); + } + /// /// Only used for debugging purposes. Completes all quests in the chapter silently. /// @@ -628,6 +643,7 @@ public void DebugCompleteChapter(int chapterId) { Load(); } + public void Save(GameStorage.Request db) { db.SaveQuests(session.AccountId, accountValues.Values); db.SaveQuests(session.CharacterId, characterValues.Values); diff --git a/Maple2.Server.Game/Model/Field/Entity/FieldPortal.cs b/Maple2.Server.Game/Model/Field/Entity/FieldPortal.cs index b7a8ebcfc..bcbd33f8a 100644 --- a/Maple2.Server.Game/Model/Field/Entity/FieldPortal.cs +++ b/Maple2.Server.Game/Model/Field/Entity/FieldPortal.cs @@ -8,6 +8,7 @@ public class FieldPortal : FieldEntity { public bool Enabled; public bool MinimapVisible; public int EndTick; + public int StartTick; public string Model = ""; public long HomeId; public string OwnerName = ""; diff --git a/Maple2.Server.Game/Model/Field/Entity/FieldQuestPortal.cs b/Maple2.Server.Game/Model/Field/Entity/FieldQuestPortal.cs new file mode 100644 index 000000000..42f7649c4 --- /dev/null +++ b/Maple2.Server.Game/Model/Field/Entity/FieldQuestPortal.cs @@ -0,0 +1,12 @@ +using Maple2.Model.Metadata; +using Maple2.Server.Game.Manager.Field; + +namespace Maple2.Server.Game.Model; + +public class FieldQuestPortal : FieldPortal { + public readonly FieldPlayer Owner; + + public FieldQuestPortal(FieldPlayer owner, FieldManager field, int objectId, Portal value) : base(field, objectId, value) { + Owner = owner; + } +} diff --git a/Maple2.Server.Game/PacketHandlers/ChannelHandler.cs b/Maple2.Server.Game/PacketHandlers/ChannelHandler.cs index fda3f6105..aedf51f77 100644 --- a/Maple2.Server.Game/PacketHandlers/ChannelHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ChannelHandler.cs @@ -46,7 +46,7 @@ public override void Handle(GameSession session, IByteReader packet) { // Update the client with the latest channel list. ChannelsResponse response = World.Channels(new ChannelsRequest()); - session.Send(ChannelPacket.Dynamic(response.Channels)); + session.Send(ChannelPacket.Load(response.Channels)); } } } diff --git a/Maple2.Server.Game/PacketHandlers/SmartPushHandler.cs b/Maple2.Server.Game/PacketHandlers/SmartPushHandler.cs new file mode 100644 index 000000000..dd531232e --- /dev/null +++ b/Maple2.Server.Game/PacketHandlers/SmartPushHandler.cs @@ -0,0 +1,138 @@ +using Maple2.Model.Enum; +using Maple2.Model.Game; +using Maple2.Model.Game.Event; +using Maple2.Model.Metadata; +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.PacketHandlers; +using Maple2.Server.Core.Packets; +using Maple2.Server.Game.Packets; +using Maple2.Server.Game.Session; + +namespace Maple2.Server.Game.PacketHandlers; + +public class SmartPushHandler : PacketHandler { + public override RecvOp OpCode => RecvOp.SmartPush; + + public override void Handle(GameSession session, IByteReader packet) { + int smartPushId = packet.ReadInt(); + if (!session.TableMetadata.SmartPushTable.Entries.TryGetValue(smartPushId, out SmartPushMetadata? metadata)) { + return; + } + + switch (metadata.Type) { + case SmartPushType.autoInteraction: + HandleAutoInteraction(session, packet, metadata); + break; + case SmartPushType.additionalEffect: + HandleAdditionalEffect(session, packet, metadata); + break; + default: + Logger.Error("Unhandled smart push type: {SmartPushType}", metadata.Type); + break; + } + + } + + private void HandleAutoInteraction(GameSession session, IByteReader packet, SmartPushMetadata metadata) { + int amount = packet.ReadInt(); + int recipeId = packet.ReadInt(); + + if (!session.TableMetadata.MasteryRecipeTable.Entries.TryGetValue(recipeId, out MasteryRecipeTable.Entry? recipeMetadata)) { + return; + } + + int successCount = 0; + for (int i = 0; i < amount; i++) { + if (metadata.RequiredItem.Tag != ItemTag.None && !session.Item.Inventory.Consume([metadata.RequiredItem])) { + return; + } + + float successRate = session.Mastery.GatherSuccessRate(recipeMetadata); + if (successRate <= 0.0f) { + break; + } + + session.Mastery.BeforeGather(recipeMetadata); + session.Mastery.Gather(recipeMetadata, session.Player.Position, session.Player.Rotation); + successCount++; + } + + session.Send(SmartPushPacket.ActivateGather(metadata.Id, successCount)); + } + + private void HandleAdditionalEffect(GameSession session, IByteReader packet, SmartPushMetadata metadata) { + int packageId = packet.ReadInt(); + if (metadata.RequiredItem.Tag != ItemTag.None && !session.Item.Inventory.Consume([metadata.RequiredItem])) { + return; + } + + if (metadata.Content is "AutoFishing" or "AutoPlayInstrument") { + if (!AutoActionPackage(session, metadata, packageId)) { + return; + } + } else { + if (session.Currency.Meret < metadata.MeretCost) { + return; + } + session.Currency.Meret -= metadata.MeretCost; + } + + session.Buffs.AddBuff(session.Player, session.Player, (int) metadata.Value, 1, session.Field.FieldTick); + } + + private bool AutoActionPackage(GameSession session, SmartPushMetadata smartPushMetadata, int packageId) { + string content = smartPushMetadata.Content; + switch (content) { + case "AutoFishing": + if (session.FindEvent(GameEventType.SaleAutoFishing).FirstOrDefault()?.Metadata.Data is SaleAutoFishing saleAutoFishing) { + content = saleAutoFishing.ContentType; + } + break; + case "AutoPlayInstrument": + if (session.FindEvent(GameEventType.SaleAutoPlayInstrument).FirstOrDefault()?.Metadata.Data is SaleAutoPlayInstrument saleAutoPlayInstrument) { + content = saleAutoPlayInstrument.ContentType; + } + break; + default: + Logger.Error("Unhandled auto action content: {Content}", content); + return false; + } + + + if (!session.TableMetadata.AutoActionTable.Entries.TryGetValue(content, out IReadOnlyDictionary? packages) || + !packages.TryGetValue(packageId, out AutoActionMetaData? packageMetadata)) { + return false; + } + + SmartPushCurrencyType currencyType; + if (packageMetadata.MeretCost > 0) { + currencyType = SmartPushCurrencyType.Meret; + } else if (packageMetadata.MesoCost > 0) { + currencyType = SmartPushCurrencyType.Meso; + } else { + currencyType = SmartPushCurrencyType.None; + } + + // Check if player has enough currency + switch (currencyType) { + case SmartPushCurrencyType.Meret: + if (session.Currency.Meret < packageMetadata.MeretCost) { + return false; + } + session.Currency.Meret -= packageMetadata.MeretCost; + break; + case SmartPushCurrencyType.Meso: + if (session.Currency.Meso < packageMetadata.MesoCost) { + return false; + } + session.Currency.Meso -= packageMetadata.MesoCost; + break; + case SmartPushCurrencyType.None: + break; + } + + session.Send(SmartPushPacket.ActivateEffect(currencyType, (int) smartPushMetadata.Value)); + return true; + } +} diff --git a/Maple2.Server.Game/Packets/ChannelPacket.cs b/Maple2.Server.Game/Packets/ChannelPacket.cs index ed665fb2c..6045ded80 100644 --- a/Maple2.Server.Game/Packets/ChannelPacket.cs +++ b/Maple2.Server.Game/Packets/ChannelPacket.cs @@ -5,20 +5,35 @@ namespace Maple2.Server.Game.Packets; public static class ChannelPacket { - public static ByteWriter Dynamic(ICollection channels) { - short count = (short) channels.Count; + private enum Command : byte { + Load = 0, + Update = 1, + } + public static ByteWriter Load(ICollection _) { var pWriter = Packet.Of(SendOp.DynamicChannel); - pWriter.WriteByte(); + pWriter.Write(Command.Load); + // Unsure as to why these values seem to work properly for Update packet to update. pWriter.WriteShort(10); - pWriter.WriteShort(count); - pWriter.WriteShort(count); - pWriter.WriteShort(count); - pWriter.WriteShort(count); + pWriter.WriteShort(100); + pWriter.WriteShort(100); + pWriter.WriteShort(100); + pWriter.WriteShort(100); pWriter.WriteShort(10); pWriter.WriteShort(10); pWriter.WriteShort(10); return pWriter; } + + public static ByteWriter Update(ICollection channels) { + var pWriter = Packet.Of(SendOp.DynamicChannel); + pWriter.Write(Command.Update); + pWriter.WriteShort((short) channels.Count); + foreach (short channel in channels) { + pWriter.WriteShort(channel); + } + + return pWriter; + } } diff --git a/Maple2.Server.Game/Packets/PortalPacket.cs b/Maple2.Server.Game/Packets/PortalPacket.cs index 078a78444..76b3c40fb 100644 --- a/Maple2.Server.Game/Packets/PortalPacket.cs +++ b/Maple2.Server.Game/Packets/PortalPacket.cs @@ -36,7 +36,7 @@ public static ByteWriter Add(FieldPortal fieldPortal) { pWriter.Write(portal.Type); pWriter.WriteInt(fieldPortal.EndTick); pWriter.WriteShort(); - pWriter.WriteInt(); // unknown + pWriter.WriteInt(fieldPortal.StartTick); pWriter.WriteBool(!string.IsNullOrEmpty(fieldPortal.Password)); pWriter.WriteUnicodeString(fieldPortal.OwnerName); pWriter.WriteUnicodeString(); diff --git a/Maple2.Server.Game/Packets/QuestPacket.cs b/Maple2.Server.Game/Packets/QuestPacket.cs index 9ab93882b..3a22f62e7 100644 --- a/Maple2.Server.Game/Packets/QuestPacket.cs +++ b/Maple2.Server.Game/Packets/QuestPacket.cs @@ -21,7 +21,7 @@ private enum Command : byte { Abandon = 6, Expired = 7, SetTracking = 9, - Unknown18 = 18, + SummonPortal = 18, ExplorationProgress = 21, LoadQuestStates = 22, LoadQuests = 23, @@ -128,12 +128,12 @@ public static ByteWriter SetTracking(int questId, bool tracked) { return pWriter; } - public static ByteWriter Unknown18() { + public static ByteWriter SummonPortal(int npcObjectId, int portalId, int startTick) { var pWriter = Packet.Of(SendOp.Quest); - pWriter.Write(Command.Unknown18); - pWriter.WriteInt(); - pWriter.WriteInt(); - pWriter.WriteInt(); + pWriter.Write(Command.SummonPortal); + pWriter.WriteInt(npcObjectId); + pWriter.WriteInt(portalId); + pWriter.WriteInt(startTick); return pWriter; } diff --git a/Maple2.Server.Game/Packets/SkillMacroPacket.cs b/Maple2.Server.Game/Packets/SkillMacroPacket.cs index 14a516de4..46b748348 100644 --- a/Maple2.Server.Game/Packets/SkillMacroPacket.cs +++ b/Maple2.Server.Game/Packets/SkillMacroPacket.cs @@ -8,13 +8,13 @@ namespace Maple2.Server.Game.Packets; public static class SkillMacroPacket { private enum Command : byte { - Load = 0, - Update = 2, + Update = 0, + Load = 2, } - public static ByteWriter Load(ICollection skillMacros) { + public static ByteWriter Update(ICollection skillMacros) { var pWriter = Packet.Of(SendOp.SkillMacro); - pWriter.Write(Command.Load); + pWriter.Write(Command.Update); pWriter.WriteInt(skillMacros.Count); foreach (SkillMacro macro in skillMacros) { pWriter.WriteClass(macro); @@ -23,9 +23,9 @@ public static ByteWriter Load(ICollection skillMacros) { return pWriter; } - public static ByteWriter Update(ICollection skillMacros) { + public static ByteWriter Load(ICollection skillMacros) { var pWriter = Packet.Of(SendOp.SkillMacro); - pWriter.Write(Command.Update); + pWriter.Write(Command.Load); pWriter.WriteInt(skillMacros.Count); foreach (SkillMacro macro in skillMacros) { pWriter.WriteClass(macro); diff --git a/Maple2.Server.Game/Packets/SmartPushPacket.cs b/Maple2.Server.Game/Packets/SmartPushPacket.cs new file mode 100644 index 000000000..31ce27c1e --- /dev/null +++ b/Maple2.Server.Game/Packets/SmartPushPacket.cs @@ -0,0 +1,30 @@ +using Maple2.Model.Enum; +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.Packets; + +namespace Maple2.Server.Game.Packets; + +public static class SmartPushPacket { + private enum Command : byte { + Activate = 1, + } + + public static ByteWriter ActivateEffect(SmartPushCurrencyType currencyType, int buffId) { + var pWriter = Packet.Of(SendOp.SmartPush); + pWriter.Write(Command.Activate); + pWriter.Write(currencyType); + pWriter.WriteInt(buffId); + + return pWriter; + } + + public static ByteWriter ActivateGather(int id, int amount) { + var pWriter = Packet.Of(SendOp.SmartPush); + pWriter.Write(Command.Activate); + pWriter.WriteInt(id); + pWriter.WriteInt(amount); + + return pWriter; + } +} diff --git a/Maple2.Server.Game/Service/ChannelService.Sync.cs b/Maple2.Server.Game/Service/ChannelService.Sync.cs index 11dd54034..5c587593e 100644 --- a/Maple2.Server.Game/Service/ChannelService.Sync.cs +++ b/Maple2.Server.Game/Service/ChannelService.Sync.cs @@ -2,6 +2,7 @@ using Maple2.Model.Game; using Maple2.Server.Channel.Service; using Maple2.Server.Core.Packets; +using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; namespace Maple2.Server.Game.Service; @@ -24,6 +25,12 @@ public override Task UpdatePlayer(PlayerUpdateRequest requ return Task.FromResult(new PlayerUpdateResponse()); } + public override Task UpdateChannels(ChannelsUpdateRequest request, ServerCallContext context) { + List channels = request.Channels.Select(channel => (short) channel).ToList(); + server.Broadcast(ChannelPacket.Update(channels)); + return Task.FromResult(new ChannelsUpdateResponse()); + } + public override Task Disconnect(DisconnectRequest request, ServerCallContext context) { if (request is { CharacterId: <= 0 }) { throw new RpcException(new Status(StatusCode.InvalidArgument, $"CharacterId not specified")); diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index 4a48e7a9d..61c132dcf 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 try { ChannelsResponse response = World.Channels(new ChannelsRequest()); - Send(ChannelPacket.Dynamic(response.Channels)); + Send(ChannelPacket.Load(response.Channels)); } catch (RpcException ex) { Logger.Warning(ex, "Failed to populate channels"); } @@ -479,7 +479,7 @@ public bool EnterField() { // Update the client with the latest channel list. ChannelsResponse response = World.Channels(new ChannelsRequest()); - Send(ChannelPacket.Dynamic(response.Channels)); + Send(ChannelPacket.Load(response.Channels)); Send(ServerListPacket.Load(Target.SERVER_NAME, [new IPEndPoint(Target.LoginIp, Target.LoginPort)], response.Channels)); return true; } diff --git a/Maple2.Server.World/Containers/ChannelClientLookup.cs b/Maple2.Server.World/Containers/ChannelClientLookup.cs index 9298f8e51..b82f446c3 100644 --- a/Maple2.Server.World/Containers/ChannelClientLookup.cs +++ b/Maple2.Server.World/Containers/ChannelClientLookup.cs @@ -184,24 +184,13 @@ private async Task MonitorChannel(Channel channel, CancellationTokenSource cance case HealthCheckResponse.Types.ServingStatus.Serving: if (channel.Status is ChannelStatus.Inactive) { logger.Information("Channel {Channel} has become active", channel.Id); - channel.Status = ChannelStatus.Active; - - // Load custom string boards - foreach ((int id, string message) in worldServer.GetCustomStringBoards()) { - channel.Client.Admin(new AdminRequest { - AddStringBoard = new AdminRequest.Types.AddStringBoard { - Id = id, - Message = message, - }, - }); - } + Active(channel); } break; default: if (channel.Status is ChannelStatus.Active) { - channel.Status = ChannelStatus.Inactive; + Inactive(channel); logger.Information("Channel {Channel} has become inactive due to {Status}", channel.Id, response.Status); - UpdateAllPlayersToOffline(channel.Id); #if !DEBUG await cancellationTokenSource.CancelAsync(); #endif @@ -214,7 +203,8 @@ private async Task MonitorChannel(Channel channel, CancellationTokenSource cance } if (channel.Status is ChannelStatus.Active) { logger.Information("Channel {Channel} has become inactive", channel.Id); - UpdateAllPlayersToOffline(channel.Id); + Inactive(channel); + #if !DEBUG await cancellationTokenSource.CancelAsync(); #endif @@ -243,6 +233,26 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + private void Inactive(Channel channel) { + channel.Status = ChannelStatus.Inactive; + UpdateAllPlayersToOffline(channel.Id); + UpdateChannels(channel.Id); + } + + private void Active(Channel channel) { + channel.Status = ChannelStatus.Active; + UpdateChannels(channel.Id); + // Load custom string boards + foreach ((int id, string message) in worldServer.GetCustomStringBoards()) { + channel.Client.Admin(new AdminRequest { + AddStringBoard = new AdminRequest.Types.AddStringBoard { + Id = id, + Message = message, + }, + }); + } + } + private void UpdateAllPlayersToOffline(int channelId) { foreach (PlayerInfo playerInfo in playerInfoLookup.GetPlayersOnChannel(channelId)) { playerInfo.Channel = -1; @@ -260,4 +270,17 @@ private void UpdateAllPlayersToOffline(int channelId) { } } } + + private void UpdateChannels(int exclueChannelBroadcast = -1) { + foreach ((int id, ChannelClient channelClient) in this) { + if (id == exclueChannelBroadcast) { + continue; + } + + List channelList = channels.Values.Where(ch => ch.Status is ChannelStatus.Active && !ch.InstancedContent).Select(ch => ch.Id).ToList(); + channelClient.UpdateChannels(new Maple2.Server.Channel.Service.ChannelsUpdateRequest { + Channels = { channelList }, + }); + } + } } diff --git a/Maple2.Server.World/Containers/PlayerConfigLookUp.cs b/Maple2.Server.World/Containers/PlayerConfigLookUp.cs index 7f3789361..e7640c7bc 100644 --- a/Maple2.Server.World/Containers/PlayerConfigLookUp.cs +++ b/Maple2.Server.World/Containers/PlayerConfigLookUp.cs @@ -137,6 +137,10 @@ private DeathInfo RetrieveDeath(long characterId) { } death.MsRemaining -= (int) msSurpassed; + if (death.MsRemaining <= 0) { + deaths.Remove(characterId, out _); + return new DeathInfo(); + } return death; }