diff --git a/Maple2.Database/Context/Ms2Context.cs b/Maple2.Database/Context/Ms2Context.cs index 624ea24d8..dfd766270 100644 --- a/Maple2.Database/Context/Ms2Context.cs +++ b/Maple2.Database/Context/Ms2Context.cs @@ -42,6 +42,7 @@ public sealed class Ms2Context(DbContextOptions options) : DbContext(options) { internal DbSet HomeLayout { get; set; } = null!; internal DbSet UgcCubeLayout { get; set; } = null!; internal DbSet Marriage { get; set; } = null!; + internal DbSet WeddingHall { get; set; } = null!; internal DbSet Nurturing { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -75,6 +76,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(Maple2.Database.Model.HomeLayout.Configure); modelBuilder.Entity(Maple2.Database.Model.HomeLayoutCube.Configure); modelBuilder.Entity(Maple2.Database.Model.Marriage.Configure); + modelBuilder.Entity(Maple2.Database.Model.WeddingHall.Configure); modelBuilder.Entity(Maple2.Database.Model.Nurturing.Configure); modelBuilder.Entity(MesoListing.Configure); diff --git a/Maple2.Database/Model/Mail.cs b/Maple2.Database/Model/Mail.cs index 169188207..7cfc1a98a 100644 --- a/Maple2.Database/Model/Mail.cs +++ b/Maple2.Database/Model/Mail.cs @@ -18,6 +18,7 @@ internal class Mail { public string SenderName { get; set; } = string.Empty; public string Title { get; set; } = string.Empty; public string Content { get; set; } = string.Empty; + public string WeddingInvite { get; set; } = string.Empty; // List is used here to preserve order public IList TitleArgs { get; set; } = Array.Empty(); @@ -41,6 +42,7 @@ internal class Mail { Content = other.Content, TitleArgs = other.TitleArgs.Select(entry => $"{entry.Key}={entry.Value}").ToArray(), ContentArgs = other.ContentArgs.Select(entry => $"{entry.Key}={entry.Value}").ToArray(), + WeddingInvite = other.WeddingInvite, Currency = new MailCurrency { Meso = other.Meso, MesoCollectTime = other.MesoCollectTime, @@ -72,6 +74,7 @@ internal class Mail { string[] split = arg.Split("=", 2); return split.Length > 1 ? (split[0], split[1]) : ("key", split[0]); }).ToArray(), + WeddingInvite = other.WeddingInvite, Meso = other.Currency.Meso, MesoCollectTime = other.Currency.MesoCollectTime, Meret = other.Currency.Meret, diff --git a/Maple2.Database/Model/WeddingHall.cs b/Maple2.Database/Model/WeddingHall.cs new file mode 100644 index 000000000..e083da58c --- /dev/null +++ b/Maple2.Database/Model/WeddingHall.cs @@ -0,0 +1,67 @@ +using System.Diagnostics.CodeAnalysis; +using Maple2.Database.Extensions; +using Maple2.Model.Enum; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Maple2.Database.Model; + +internal class WeddingHall { + public long Id { get; set; } + public long MarriageId { get; set; } + public DateTime CeremonyTime { get; set; } + public int PackageId { get; set; } + public int PackageHallId { get; set; } + public long OwnerId { get; set; } + public bool Public { get; set; } + public Dictionary GuestList { get; set; } = new(); // CharacterId, AccountId + public DateTime CreationTime { get; set; } + + [return: NotNullIfNotNull(nameof(other))] + public static implicit operator WeddingHall?(Maple2.Model.Game.WeddingHall? other) { + return other == null ? null : new WeddingHall { + Id = other.Id, + MarriageId = other.MarriageId, + CeremonyTime = other.CeremonyTime.FromEpochSeconds(), + PackageId = other.PackageId, + PackageHallId = other.PackageHallId, + Public = other.Public, + CreationTime = other.CreationTime.FromEpochSeconds(), + GuestList = other.GuestList, + OwnerId = other.ReserverCharacterId, + }; + } + + // Use explicit Convert() here because we need marriage data to construct Wedding Hall. + public Maple2.Model.Game.WeddingHall Convert(Maple2.Model.Game.Marriage marriage) { + Maple2.Model.Game.MarriagePartner reserver = marriage.Partner1.Info!.CharacterId == OwnerId ? marriage.Partner1 : marriage.Partner2; + Maple2.Model.Game.MarriagePartner partner = marriage.Partner1.Info!.CharacterId == OwnerId ? marriage.Partner2 : marriage.Partner1; + var hall = new Maple2.Model.Game.WeddingHall { + Id = Id, + MarriageId = MarriageId, + CeremonyTime = CeremonyTime.ToEpochSeconds(), + PackageId = PackageId, + PackageHallId = PackageHallId, + Public = Public, + ReserverCharacterId = reserver.CharacterId, + ReserverAccountId = reserver.AccountId, + ReserverName = reserver.Info!.Name, + PartnerName = partner.Info!.Name, + CreationTime = CreationTime.ToEpochSeconds(), + GuestList = GuestList, + }; + + return hall; + } + + public static void Configure(EntityTypeBuilder builder) { + builder.ToTable("wedding-hall"); + builder.HasKey(hall => hall.Id); + builder.Property(hall => hall.GuestList).HasJsonConversion().IsRequired(); + + builder.OneToOne() + .HasForeignKey(hall => hall.MarriageId); + builder.Property(hall => hall.CreationTime) + .ValueGeneratedOnAdd(); + } +} diff --git a/Maple2.Database/Storage/Game/GameStorage.Wedding.cs b/Maple2.Database/Storage/Game/GameStorage.Wedding.cs index 5aaae332d..3c5ed2cf7 100644 --- a/Maple2.Database/Storage/Game/GameStorage.Wedding.cs +++ b/Maple2.Database/Storage/Game/GameStorage.Wedding.cs @@ -3,6 +3,7 @@ using Maple2.Model.Game; using Marriage = Maple2.Model.Game.Marriage; using MarriageExp = Maple2.Model.Game.MarriageExp; +using WeddingHall = Maple2.Model.Game.WeddingHall; namespace Maple2.Database.Storage; @@ -25,11 +26,32 @@ public partial class Request { return null; } - return Commit() ? GetMarriage(marriageId: marriage.Id) : null; + return Commit() ? GetMarriage(partner1Id) : null; } - public Marriage? GetMarriage(long marriageId = 0, long characterId = 0) { - Model.Marriage? marriage = marriageId > 0 ? Context.Marriage.Find(marriageId) : + public bool SaveMarriage(Marriage marriage) { + // Don't save marriage if it was disbanded. + if (!Context.Marriage.Any(model => model.Id == marriage.Id)) { + return false; + } + + Context.Marriage.Update(marriage); + + return Context.TrySaveChanges(); + } + + public bool DeleteMarriage(long marriageId) { + Model.Marriage? marriage = Context.Marriage.Find(marriageId); + if (marriage == null) { + return false; + } + + Context.Marriage.Remove(marriage); + return Context.TrySaveChanges(); + } + + public Marriage? GetMarriage(long characterId = 0, long weddingId = 0) { + Model.Marriage? marriage = weddingId > 0 ? Context.Marriage.Find(weddingId) : Context.Marriage.FirstOrDefault(marriage => marriage.Partner1Id == characterId || marriage.Partner2Id == characterId); if (marriage == null) { return null; @@ -40,23 +62,23 @@ public partial class Request { if (partner1 == null || partner2 == null) { return null; } + return new Marriage { Id = marriage.Id, CreationTime = marriage.CreationTime.ToEpochSeconds(), ExpHistory = marriage.ExpHistory.Select(exp => exp).ToList(), Partner1 = new MarriagePartner { - Info = partner1, - Message = marriage.Partner1Message, + Info = partner1.CharacterId == characterId ? partner2 : partner1, + Message = partner1.CharacterId == characterId ? marriage.Partner2Message : marriage.Partner1Message, }, Partner2 = new MarriagePartner { - Info = partner2, - Message = marriage.Partner2Message, + Info = partner1.CharacterId == characterId ? partner1 : partner2, + Message = partner1.CharacterId == characterId ? marriage.Partner1Message : marriage.Partner2Message, }, Status = marriage.Status, }; } - public MarriageInfo GetMarriageInfo(long characterId) { Model.Marriage? model = Context.Marriage.FirstOrDefault(member => member.Partner1Id == characterId || member.Partner2Id == characterId); if (model == null) { @@ -73,5 +95,67 @@ public MarriageInfo GetMarriageInfo(long characterId) { Partner2Name = partner.Name, }; } + + public WeddingHall? CreateWeddingHall(WeddingHall hall, Marriage marriage) { + BeginTransaction(); + + Model.WeddingHall model = hall; + model.MarriageId = marriage.Id; + model.OwnerId = marriage.Partner1.CharacterId; + model.CreationTime = DateTime.Now; + Context.WeddingHall.Add(model); + if (!SaveChanges()) { + return null; + } + + return Commit() ? GetWeddingHall(marriage) : null; + } + + public WeddingHall? GetWeddingHall(Marriage marriage) { + Model.WeddingHall? model = Context.WeddingHall.FirstOrDefault(hall => hall.MarriageId == marriage.Id); + return model?.Convert(marriage); + } + + public WeddingHall? GetWeddingHall(long hallId = 0, long marriageId = 0) { + Model.WeddingHall? model = hallId > 0 ? Context.WeddingHall.Find(hallId) : + Context.WeddingHall.FirstOrDefault(hall => hall.MarriageId == marriageId); + + if (model == null) { + return null; + } + + Marriage? marriage = GetMarriage(weddingId: marriageId); + return marriage == null ? null : model.Convert(marriage); + } + + public bool DeleteWeddingHall(long hallId) { + Model.WeddingHall? hall = Context.WeddingHall.Find(hallId); + if (hall == null) { + return false; + } + + Context.WeddingHall.Remove(hall); + return Context.TrySaveChanges(); + } + + public bool WeddingHallTimeIsAvailable(long ceremonyTime) { + DateTime ceremonyDateTime = ceremonyTime.FromEpochSeconds(); + return Context.WeddingHall.FirstOrDefault(hall => hall.CeremonyTime == ceremonyDateTime) == null; + } + + public IEnumerable GetWeddingHalls() { + List entries = Context.WeddingHall + .Where(hall => hall.CeremonyTime > DateTime.Now) + .AsEnumerable() + .ToList(); + + foreach (Model.WeddingHall entry in entries) { + Marriage? marriage = GetMarriage(weddingId: entry.MarriageId); + if (marriage == null) { + continue; + } + yield return entry.Convert(marriage); + } + } } } diff --git a/Maple2.Database/Storage/Metadata/TableMetadataStorage.cs b/Maple2.Database/Storage/Metadata/TableMetadataStorage.cs index d7ecc6e12..53d732b1f 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 worldMapTable; private readonly Lazy survivalSkinInfoTable; private readonly Lazy bannerTable; + private readonly Lazy weddingTable; private readonly Lazy enchantScrollTable; private readonly Lazy itemRemakeScrollTable; @@ -102,6 +103,7 @@ public class TableMetadataStorage { public WorldMapTable WorldMapTable => worldMapTable.Value; public SurvivalSkinInfoTable SurvivalSkinInfoTable => survivalSkinInfoTable.Value; public BannerTable BannerTable => bannerTable.Value; + public WeddingTable WeddingTable => weddingTable.Value; public EnchantScrollTable EnchantScrollTable => enchantScrollTable.Value; public ItemRemakeScrollTable ItemRemakeScrollTable => itemRemakeScrollTable.Value; @@ -160,6 +162,7 @@ public TableMetadataStorage(MetadataContext context) { worldMapTable = Retrieve(context, "newworldmap.xml"); survivalSkinInfoTable = Retrieve(context, "maplesurvivalskininfo.xml"); bannerTable = Retrieve(context, "banner.xml"); + weddingTable = Retrieve(context, "wedding*.xml"); enchantScrollTable = Retrieve(context, "enchantscroll.xml"); itemRemakeScrollTable = Retrieve(context, "itemremakescroll.xml"); itemRepackingScrollTable = Retrieve(context, "itemrepackingscroll.xml"); diff --git a/Maple2.File.Ingest/Maple2.File.Ingest.csproj b/Maple2.File.Ingest/Maple2.File.Ingest.csproj index 4d7e140da..33956a563 100644 --- a/Maple2.File.Ingest/Maple2.File.Ingest.csproj +++ b/Maple2.File.Ingest/Maple2.File.Ingest.csproj @@ -20,7 +20,7 @@ - + diff --git a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs index 4764ebe0f..f4b29632b 100644 --- a/Maple2.File.Ingest/Mapper/ServerTableMapper.cs +++ b/Maple2.File.Ingest/Mapper/ServerTableMapper.cs @@ -88,19 +88,19 @@ private Dictionary> MergeNpcScript foreach ((int scriptId, NpcScriptCondition scriptCondition) in scripts) { var questStarted = new Dictionary(); foreach (string quest in scriptCondition.quest_start) { - KeyValuePair parsedQuest = ParseToKeyValuePair(quest); + KeyValuePair parsedQuest = ParseToIntKeyValuePair(quest); questStarted.Add(parsedQuest.Key, parsedQuest.Value); } var questsCompleted = new Dictionary(); foreach (string quest in scriptCondition.quest_complete) { - KeyValuePair parsedQuest = ParseToKeyValuePair(quest); + KeyValuePair parsedQuest = ParseToIntKeyValuePair(quest); questsCompleted.Add(parsedQuest.Key, parsedQuest.Value); } var items = new List>(); for (int i = 0; i < scriptCondition.item.Length; i++) { - KeyValuePair parsedItem = ParseToKeyValuePair(scriptCondition.item[i]); + KeyValuePair parsedItem = ParseToIntKeyValuePair(scriptCondition.item[i]); string itemCount = scriptCondition.itemCount.ElementAtOrDefault(i) ?? "1"; if (!int.TryParse(itemCount, out int itemAmount)) { itemAmount = 1; @@ -113,21 +113,29 @@ private Dictionary> MergeNpcScript Id: npcId, ScriptId: scriptId, Type: ScriptType.Npc, - MaidAuthority: scriptCondition.maid_auth, - MaidExpired: scriptCondition.maid_expired != "!1", - MaidReadyToPay: scriptCondition.maid_ready_to_pay != "!1", - MaidClosenessRank: scriptCondition.maid_affinity_grade, - MaidClosenessTime: ParseToKeyValuePair(scriptCondition.maid_affinity_time), - MaidMoodTime: ParseToKeyValuePair(scriptCondition.maid_mood_time), - MaidDaysBeforeExpired: ParseToKeyValuePair(scriptCondition.maid_day_before_expired), + Maid: new ScriptConditionMetadata.MaidData( + Authority: scriptCondition.maid_auth, + Expired: scriptCondition.maid_expired != "!1", + ReadyToPay: scriptCondition.maid_ready_to_pay != "!1", + ClosenessRank: scriptCondition.maid_affinity_grade, + ClosenessTime: ParseToIntKeyValuePair(scriptCondition.maid_affinity_time), + MoodTime: ParseToIntKeyValuePair(scriptCondition.maid_mood_time), + DaysBeforeExpired: ParseToIntKeyValuePair(scriptCondition.maid_day_before_expired) + ), + Wedding: new ScriptConditionMetadata.WeddingData( + HasReservation: scriptCondition.weddingHallBooking < 0 ? null : scriptCondition.weddingHallBooking == 1, + MarriageDays: scriptCondition.marriageDate, + UserState: scriptCondition.weddingState < 0 ? null : (MaritalStatus) scriptCondition.weddingState, + HallState: ParseToStringKeyValuePair(scriptCondition.weddingHallState), + CoolingOff: scriptCondition.coolingOff), JobCode: scriptCondition.job?.Select(job => (JobCode) job).ToList() ?? [], QuestStarted: questStarted, QuestCompleted: questsCompleted, Items: items, - Buff: ParseToKeyValuePair(scriptCondition.buff), - Meso: ParseToKeyValuePair(scriptCondition.meso), - Level: ParseToKeyValuePair(scriptCondition.level), - AchieveCompleted: ParseToKeyValuePair(scriptCondition.achieve_complete), + Buff: ParseToIntKeyValuePair(scriptCondition.buff), + Meso: ParseToIntKeyValuePair(scriptCondition.meso), + Level: ParseToIntKeyValuePair(scriptCondition.level), + AchieveCompleted: ParseToIntKeyValuePair(scriptCondition.achieve_complete), InGuild: scriptCondition.guild )); } @@ -142,19 +150,19 @@ private Dictionary> MergeQuestScri foreach ((int scriptId, QuestScriptCondition scriptCondition) in scripts) { var questStarted = new Dictionary(); foreach (string quest in scriptCondition.quest_start) { - KeyValuePair parsedQuest = ParseToKeyValuePair(quest); + KeyValuePair parsedQuest = ParseToIntKeyValuePair(quest); questStarted.Add(parsedQuest.Key, parsedQuest.Value); } var questsCompleted = new Dictionary(); foreach (string quest in scriptCondition.quest_complete) { - KeyValuePair parsedQuest = ParseToKeyValuePair(quest); + KeyValuePair parsedQuest = ParseToIntKeyValuePair(quest); questsCompleted.Add(parsedQuest.Key, parsedQuest.Value); } var items = new List>(); for (int i = 0; i < scriptCondition.item.Length; i++) { - KeyValuePair parsedItem = ParseToKeyValuePair(scriptCondition.item[i]); + KeyValuePair parsedItem = ParseToIntKeyValuePair(scriptCondition.item[i]); string itemCount = scriptCondition.itemCount.ElementAtOrDefault(i) ?? "1"; if (!int.TryParse(itemCount, out int itemAmount)) { itemAmount = 1; @@ -167,21 +175,29 @@ private Dictionary> MergeQuestScri Id: questId, ScriptId: scriptId, Type: ScriptType.Quest, - MaidAuthority: scriptCondition.maid_auth, - MaidExpired: scriptCondition.maid_expired != "!1", - MaidReadyToPay: scriptCondition.maid_ready_to_pay != "!1", - MaidClosenessRank: scriptCondition.maid_affinity_grade, - MaidClosenessTime: ParseToKeyValuePair(scriptCondition.maid_affinity_time), - MaidMoodTime: ParseToKeyValuePair(scriptCondition.maid_mood_time), - MaidDaysBeforeExpired: ParseToKeyValuePair(scriptCondition.maid_day_before_expired), + Maid: new ScriptConditionMetadata.MaidData( + Authority: scriptCondition.maid_auth, + Expired: scriptCondition.maid_expired != "!1", + ReadyToPay: scriptCondition.maid_ready_to_pay != "!1", + ClosenessRank: scriptCondition.maid_affinity_grade, + ClosenessTime: ParseToIntKeyValuePair(scriptCondition.maid_affinity_time), + MoodTime: ParseToIntKeyValuePair(scriptCondition.maid_mood_time), + DaysBeforeExpired: ParseToIntKeyValuePair(scriptCondition.maid_day_before_expired) + ), + Wedding: new ScriptConditionMetadata.WeddingData( + HasReservation: scriptCondition.weddingHallBooking < 0 ? null : scriptCondition.weddingHallBooking == 1, + MarriageDays: scriptCondition.marriageDate, + UserState: scriptCondition.weddingState < 0 ? null : (MaritalStatus) scriptCondition.weddingState, + HallState: ParseToStringKeyValuePair(scriptCondition.weddingHallState), + CoolingOff: scriptCondition.coolingOff), JobCode: scriptCondition.job?.Select(job => (JobCode) job).ToList() ?? [], QuestStarted: questStarted, QuestCompleted: questsCompleted, Items: items, - Buff: ParseToKeyValuePair(scriptCondition.buff), - Meso: ParseToKeyValuePair(scriptCondition.meso), - Level: ParseToKeyValuePair(scriptCondition.level), - AchieveCompleted: ParseToKeyValuePair(scriptCondition.achieve_complete), + Buff: ParseToIntKeyValuePair(scriptCondition.buff), + Meso: ParseToIntKeyValuePair(scriptCondition.meso), + Level: ParseToIntKeyValuePair(scriptCondition.level), + AchieveCompleted: ParseToIntKeyValuePair(scriptCondition.achieve_complete), InGuild: scriptCondition.guild )); } @@ -190,7 +206,7 @@ private Dictionary> MergeQuestScri return results; } - private static KeyValuePair ParseToKeyValuePair(string input) { + private static KeyValuePair ParseToIntKeyValuePair(string input) { bool value = !input.StartsWith("!"); if (!value) { @@ -203,6 +219,15 @@ private static KeyValuePair ParseToKeyValuePair(string input) { return new KeyValuePair(key, value); } + private static KeyValuePair ParseToStringKeyValuePair(string input) { + bool value = !input.StartsWith("!"); + + if (!value) { + input = input.Substring(1); + } + return new KeyValuePair(input, value); + } + private ScriptFunctionTable ParseScriptFunction() { var results = new Dictionary>>(); results = MergeNpcScriptFunctions(results, parser.ParseNpcScriptFunction()); diff --git a/Maple2.File.Ingest/Mapper/TableMapper.cs b/Maple2.File.Ingest/Mapper/TableMapper.cs index 9685fd0b8..166c97ad2 100644 --- a/Maple2.File.Ingest/Mapper/TableMapper.cs +++ b/Maple2.File.Ingest/Mapper/TableMapper.cs @@ -25,6 +25,8 @@ using MagicPath = Maple2.Model.Metadata.MagicPath; using MasteryType = Maple2.Model.Enum.MasteryType; using MeretMarketCategory = Maple2.File.Parser.Xml.Table.MeretMarketCategory; +using WeddingHall = Maple2.File.Parser.Xml.Table.WeddingHall; +using WeddingPackage = Maple2.Model.Metadata.WeddingPackage; using WeddingReward = Maple2.Model.Metadata.WeddingReward; namespace Maple2.File.Ingest.Mapper; @@ -74,7 +76,7 @@ protected override IEnumerable Map() { yield return new TableMetadata { Name = "banner.xml", Table = ParseBanner() }; // Marriage/Wedding - yield return new TableMetadata { Name = "weddingreward.xml", Table = ParseWeddingReward() }; + yield return new TableMetadata { Name = "wedding*.xml", Table = ParseWeddingTable() }; // Prestige yield return new TableMetadata { Name = "adventurelevelability.xml", Table = ParsePrestigeLevelAbilityTable() }; @@ -1507,15 +1509,51 @@ private BannerTable ParseBanner() { return new BannerTable(results); } - private WeddingRewardTable ParseWeddingReward() { - var results = new Dictionary(); + private WeddingTable ParseWeddingTable() { + var rewardsResults = new Dictionary(); foreach ((WeddingRewardType type, Parser.Xml.Table.WeddingReward? reward) in parser.ParseWeddingReward()) { - results.Add((MarriageExpType) type, new WeddingReward( + rewardsResults.Add((MarriageExpType) type, new WeddingReward( Type: (MarriageExpType) type, Amount: reward.rewardExp, Limit: (MarriageExpLimit) reward.rewardLimit)); } - return new WeddingRewardTable(results); + var packageResults = new Dictionary(); + foreach ((int id, Parser.Xml.Table.WeddingPackage package) in parser.ParseWeddingPackage()) { + var hallDataDic = new Dictionary(); + foreach (WeddingHall hall in package.weddingHall) { + List hallItems = []; + foreach (WeddingItem item in hall.weddingItem) { + hallItems.Add(new WeddingPackage.HallData.Item( + ItemId: item.itemID, + Amount: item.count, + Rarity: item.grade, + NightOnly: item.nightReward)); + } + + List completeHallItems = []; + foreach (WeddingItem item in hall.weddingCompleteItem) { + completeHallItems.Add(new WeddingPackage.HallData.Item( + ItemId: item.itemID, + Amount: item.count, + Rarity: item.grade, + NightOnly: item.nightReward)); + } + + hallDataDic.Add(hall.id, new WeddingPackage.HallData( + Id: hall.id, + MapId: hall.fieldID, + NightMapId: hall.nightFieldID, + Tier: hall.grade, + MeretCost: hall.merat, + Items: hallItems, + CompleteItems: completeHallItems)); + } + packageResults.Add(id, new WeddingPackage( + Id: id, + PlannerId: package.planner, + Halls: hallDataDic)); + } + return new WeddingTable(rewardsResults, packageResults); } } diff --git a/Maple2.Model/Enum/Wedding.cs b/Maple2.Model/Enum/Wedding.cs index 56b70ddae..c1c799461 100644 --- a/Maple2.Model/Enum/Wedding.cs +++ b/Maple2.Model/Enum/Wedding.cs @@ -4,7 +4,9 @@ public enum MaritalStatus : short { Single = 0, Engaged = 1, Married = 2, - Divorce = 3, + ConsensualDivorce = 3, + ForceDivorce = 4, + DivorceCoolOff = 5, } public enum MarriageExpType : short { @@ -25,8 +27,15 @@ public enum MarriageExpLimit { month, } -public enum ProposalResponse : short { - Accept = 1, - Decline = 2, - Timeout = 6, +[Flags] +public enum WeddingHallEntryType { + Guest = 1, + Bride = 2, + Groom = 4, + GroomBride = Groom | Bride, +} + +public enum WeddingHallState { + weddingComplete, + } diff --git a/Maple2.Model/Error/WeddingError.cs b/Maple2.Model/Error/WeddingError.cs new file mode 100644 index 000000000..6190e07b7 --- /dev/null +++ b/Maple2.Model/Error/WeddingError.cs @@ -0,0 +1,126 @@ +// ReSharper disable InconsistentNaming, IdentifierTypo + +using System.ComponentModel; + +namespace Maple2.Model.Error; + +public enum WeddingError : short { + none = 0, + [Description("You are now engaged to {0}")] + s_wedding_propose_ok = 1, + [Description("{0} declined your proposal.")] + s_wedding_result_decline_propose = 2, + [Description("You have proposed to {0}. \\nIf they accept, you both will be engaged.")] + s_wedding_msg_box_propose_to = 3, + [Description("You cancelled the proposal.")] + s_wedding_result_cancel_propose_to = 4, + [Description("{0} cancelled the proposal.")] + s_wedding_result_cancel_propose_from = 5, + [Description("The proposal has been cancelled because the waiting time has expired.")] + s_wedding_result_cancel_propose_expire = 6, + confirm_reservation = 7, // Unknown what string this is supposed to be + decline_reservation = 8, // Unknown what string this is supposed to be + accept_reservation_cancel = 9, // Unknown what string this is supposed to be + decline_reservation_cancel = 10, // Unknown what string this is supposed to be + accept_reservation_change = 11, // Unknown what string this is supposed to be + decline_reservation_change = 12, // Unknown what string this is supposed to be + [Description("You sent a request to your fiancee to confirm the wedding hall reservation. The reservation will be complete when they confirm.")] + s_wedding_result_send_wedding_hall_reservation = 15, + [Description("You have sent a request to your fiancee to change the wedding reservation time.\\nThe reservation will be complete when they confirm.")] + s_wedding_result_send_wedding_hall_reservation_change = 16, + [Description("You have sent a request to your fiancee to cancel the wedding reservation time.\\nOnce confirmed, the cancellation will be complete and a free reservation coupon will be given to book a wedding venue of the same grade in the future.")] + s_wedding_result_send_wedding_hall_reservation_cancel = 17, + s_wedding_result_err_wedding_block_age_to = 19, + s_wedding_result_err_wedding_block_age_from = 20, + [Description("You cannot propose to a character of the same gender.")] + s_wedding_result_err_wedding_block_gender = 21, + [Description("You cannot propose to {0} because you are not friends.\\nBecome friends in order to propose.")] + s_wedding_result_err_not_buddy = 22, + [Description("You are currently engaged and cannot propose to {0}.")] + s_wedding_result_err_propose_state_to = 23, + [Description("You are currently married and cannot propose to {0}.")] + s_wedding_result_err_marriage_state_to = 24, + [Description("You currently have a divorce in progress and cannot propose to {0}.")] + s_wedding_result_err_coolingoff_state_to = 25, + [Description("You have to wait 48 hours after a divorce to remarry.")] + s_wedding_result_err_prpose_cooltime_state_to = 26, + [Description("You cannot propose to a character who is engaged.")] + s_wedding_result_err_propose_state_from = 27, + [Description("You cannot propose to a married character.")] + s_wedding_result_err_mareriage_state_from = 28, + [Description("You cannot propose to a character with a divorce in progress.")] + s_wedding_result_err_coolingoff_state_from = 29, + [Description("You cannot propose to a character with less than 48 hours after divorce.")] + s_wedding_result_err_propose_cooltime_state_from = 30, + [Description("Both characters must have {0} in order to propose.")] + s_wedding_result_err_not_find_propose_item = 31, + [Description("Distance is too far to propose.")] + s_wedding_result_err_propose_distance = 32, + [Description("You have already been proposed.")] + s_wedding_result_err_already_propose_request = 33, + [Description("In order to be engaged, reserve a wedding hall, change reservation, or settle a divorce, you must be together in the same map as the other individual.")] + s_wedding_result_err_same_field = 34, + [Description("Only available when engaged.")] + s_wedding_result_err_only_promise_user = 35, + [Description("This is an unknown wedding hall.")] + s_wedding_result_err_invalid_wedding_hall = 36, + [Description("Insufficent merets.")] + s_wedding_result_err_lack_meratal = 37, + [Description("There is a maintenance soon. Please make a reservation at a different time.")] + s_wedding_result_err_maintenance_time = 38, + [Description("Could not find the wedding hall.")] + s_wedding_result_not_find_weddinghall = 39, + [Description("The wedding invitation has already been sent.")] + s_wedding_result_already_send_invitation = 40, + [Description("The engagement has expired.")] + s_wedding_result_late_promise_time = 41, + [Description("It is too late to modify the reservation.")] + s_wedding_result_late_wedding_reserve_modify_time = 42, + [Description("You already have a wedding reservation.")] + s_wedding_result_already_reservation = 43, + [Description("There is a wedding already booked for this time.")] + s_wedding_result_already_same_reservation = 44, + [Description("You can only divorce if you are married.")] + s_wedding_result_invaild_state_divorce = 45, + [Description("Your marriage is too new and cannot file for divorce.")] + s_wedding_result_lack_marriage_date = 46, + [Description("You do not have enough mesos to divorce.")] + s_wedding_result_lack_meso_divorce = 47, + [Description("The divorce has been completed.")] + s_wedding_result_late_divorce_agree = 48, + [Description("Divorce consent can only be made by the person who asked for a divorce.")] + s_wedding_result_invalid_divorce_agree_user = 49, + [Description("Can only be used by married users.")] + s_wedding_result_only_marriage_user = 50, + [Description("You have exceeded the number of invitations you can send at one time.")] + s_wedding_result_wedding_invitation_count = 51, + [Description("You do not have enough mesos to send a wedding invitation.")] + s_wedding_result_lack_meso_invitation = 52, + [Description("Cannot send reminder not too long after a wedding.")] + s_wedding_result_early_remind_wedding = 53, + [Description("The reservation time is too early.")] + s_wedding_result_early_wedding_reserve = 54, + [Description("Forced divorce request cannot be canceled after the divorce meditation period has passed.")] + s_wedding_result_divorcecancel = 55, + [Description("You cannot enter the wedding at this time.")] + s_wedding_visit_failed_common = 56, + [Description("You cannot enter the wedding ceremony in this map.")] + s_wedding_visit_failed_disablemap = 57, + [Description("You cannot attend a wedding while you are tombstoned.")] + s_wedding_visit_failed_dead = 58, + [Description("You have already entered the wedding")] + s_wedding_visit_failed_same_place = 59, + [Description("You cannot enter the wedding ceremony while in a dungeon.")] + s_wedding_visit_failed_solo_instance = 60, + [Description("It is not possible to enter the wedding at this time.")] + s_wedding_visit_failed_invalid_time = 61, + [Description("Only the character who reserved the wedding hall can modify or cancel the reservation.")] + s_wedding_result_only_booking_character = 62, + [Description("A wedding hall has already been booked.")] + s_wedding_result_already_weddinghall_reserve = 63, + [Description("Not enough coupons")] + s_wedding_result_lack_coupon = 64, + [Description("System error. [{0}]")] + s_wedding_result_err_system = short.MaxValue, + +} diff --git a/Maple2.Model/Game/Mail.cs b/Maple2.Model/Game/Mail.cs index 42c130c19..7e23d1298 100644 --- a/Maple2.Model/Game/Mail.cs +++ b/Maple2.Model/Game/Mail.cs @@ -19,6 +19,7 @@ public class Mail : IByteSerializable { public string SenderName = string.Empty; public string Title = string.Empty; public string Content = string.Empty; + public string WeddingInvite = string.Empty; public IList<(string Key, string Value)> TitleArgs { get; init; } public IList<(string Key, string Value)> ContentArgs { get; init; } @@ -71,6 +72,10 @@ public void SetContent(StringCode content) { Content = $""""""; } + public void SetWeddingInvite(WeddingHall hall) { + WeddingInvite = $""""""; + } + public bool MesoCollected() { return Meso == 0 || MesoCollectTime > 0; } @@ -147,8 +152,7 @@ public void WriteTo(IByteWriter writer) { writer.WriteLong(ReadTime); writer.WriteLong(ExpiryTime); writer.WriteLong(SendTime); - writer.WriteUnicodeString(); // wedding invite information. example: - // // + writer.WriteUnicodeString(WeddingInvite); } private static string FormatArgs(ICollection<(string Key, string Value)> args) { diff --git a/Maple2.Model/Game/Sync/StateSync.cs b/Maple2.Model/Game/Sync/StateSync.cs index 17162a0fa..8d430b663 100644 --- a/Maple2.Model/Game/Sync/StateSync.cs +++ b/Maple2.Model/Game/Sync/StateSync.cs @@ -15,7 +15,7 @@ public enum Flag : byte { Flag1 = 1, Flag2 = 2, Flag3 = 4, - Flag4 = 8, + Animation = 8, Flag5 = 16, Flag6 = 32, } @@ -50,8 +50,8 @@ public enum Flag : byte { public string? Flag3Unknown2; #endregion - #region Flag4 - public string? Flag4Animation; + #region AnimationFlag + public string? AnimationName; #endregion #region Flag5 @@ -98,8 +98,8 @@ public virtual void WriteTo(IByteWriter writer) { writer.WriteInt(Flag3Unknown1); writer.WriteUnicodeString(Flag3Unknown2 ?? ""); } - if (Flags.HasFlag(Flag.Flag4)) { - writer.WriteUnicodeString(Flag4Animation ?? ""); + if (Flags.HasFlag(Flag.Animation)) { + writer.WriteUnicodeString(AnimationName ?? ""); } if (Flags.HasFlag(Flag.Flag5)) { writer.WriteInt(Flag5Unknown1); @@ -146,8 +146,8 @@ public virtual void ReadFrom(IByteReader reader) { Flag3Unknown1 = reader.ReadInt(); Flag3Unknown2 = reader.ReadUnicodeString(); } - if (Flags.HasFlag(Flag.Flag4)) { - Flag4Animation = reader.ReadUnicodeString(); + if (Flags.HasFlag(Flag.Animation)) { + AnimationName = reader.ReadUnicodeString(); } if (Flags.HasFlag(Flag.Flag5)) { Flag5Unknown1 = reader.ReadInt(); @@ -178,8 +178,8 @@ public override string ToString() { if (Flags.HasFlag(Flag.Flag3)) { builder.Append($"Flag3: {Flag3Unknown1}, {Flag3Unknown2}"); } - if (Flags.HasFlag(Flag.Flag4)) { - builder.Append($"Flag4: {Flag4Animation}"); + if (Flags.HasFlag(Flag.Animation)) { + builder.Append($"Animation: {AnimationName}"); } if (Flags.HasFlag(Flag.Flag5)) { builder.Append($"Flag5: {Flag5Unknown1}, {Flag5Unknown2}"); diff --git a/Maple2.Model/Game/User/Marriage.cs b/Maple2.Model/Game/User/Marriage.cs index bd27d02f5..68609fbf1 100644 --- a/Maple2.Model/Game/User/Marriage.cs +++ b/Maple2.Model/Game/User/Marriage.cs @@ -27,9 +27,9 @@ public class Marriage : IByteSerializable { public void WriteTo(IByteWriter writer) { writer.WriteLong(Id); - writer.Write(Status); - writer.WriteLong(CreationTime); + writer.WriteByte((byte) Status); writer.WriteLong(CreationTime); + writer.WriteLong(); // wedding time ? writer.WriteClass(Partner1); writer.WriteClass(Partner2); writer.WriteLong(Exp); @@ -63,11 +63,13 @@ public class MarriagePartner : IByteSerializable, IDisposable { public long CharacterId => Info?.CharacterId ?? 0; public string Name => Info?.Name ?? string.Empty; public string Message { get; set; } = string.Empty; + public bool IsOnline => Info?.Online ?? false; public CancellationTokenSource? TokenSource; public void WriteTo(IByteWriter writer) { - writer.WriteLong(AccountId); writer.WriteLong(CharacterId); + writer.WriteLong(AccountId); writer.WriteUnicodeString(Name); + writer.WriteBool(!IsOnline); } public void Dispose() { diff --git a/Maple2.Model/Game/User/WeddingHall.cs b/Maple2.Model/Game/User/WeddingHall.cs new file mode 100644 index 000000000..497b41390 --- /dev/null +++ b/Maple2.Model/Game/User/WeddingHall.cs @@ -0,0 +1,82 @@ +using Maple2.PacketLib.Tools; +using Maple2.Tools; + +namespace Maple2.Model.Game; + +public class WeddingHall : IByteSerializable, IByteDeserializable { + public static WeddingHall Default => new() { + Id = 0, + MarriageId = 0, + ReserverAccountId = 0, + ReserverCharacterId = 0, + ReserverName = string.Empty, + PartnerName = string.Empty, + CeremonyTime = 0, + PackageId = 0, + PackageHallId = 0, + Public = false, + }; + public required long Id { get; set; } + public required long MarriageId { get; init; } + public long CeremonyTime { get; set; } + public int PackageId { get; set; } + public int PackageHallId { get; set; } + public bool Public { get; set; } + public required long ReserverAccountId { get; set; } + public required long ReserverCharacterId { get; set; } + public required string ReserverName { get; set; } = string.Empty; + public required string PartnerName { get; set; } = string.Empty; + public Dictionary GuestList { get; set; } = new(); // CharacterId, AccountId + public long CreationTime { get; set; } + + public void ReadFrom(IByteReader reader) { + Id = reader.ReadLong(); + PackageId = reader.ReadInt(); + PackageHallId = reader.ReadInt(); + CeremonyTime = reader.ReadLong(); + Public = reader.ReadBool(); + ReserverAccountId = reader.ReadLong(); + ReserverCharacterId = reader.ReadLong(); + ReserverName = reader.ReadUnicodeString(); + PartnerName = reader.ReadUnicodeString(); + reader.ReadLong(); + reader.ReadLong(); + reader.ReadLong(); + reader.ReadLong(); + reader.ReadLong(); + reader.ReadInt(); + + ReserverName = string.Empty; + PartnerName = string.Empty; + GuestList = []; + /*int guestCount = reader.ReadInt(); + for (int i = 0; i < guestCount; i++) { + long characterId = reader.ReadLong(); + long accountId = reader.ReadLong(); + GuestList[characterId] = accountId; + }*/ + } + public void WriteTo(IByteWriter writer) { + writer.WriteLong(Id); + writer.WriteInt(PackageId); + writer.WriteInt(PackageHallId); + writer.WriteLong(CeremonyTime); + writer.WriteBool(Public); + writer.WriteLong(ReserverAccountId); + writer.WriteLong(ReserverCharacterId); + writer.WriteUnicodeString(ReserverName); + writer.WriteUnicodeString(PartnerName); + + writer.WriteLong(); + writer.WriteLong(); + writer.WriteLong(); + writer.WriteLong(); + writer.WriteLong(); + + writer.WriteInt(GuestList.Count); + /*foreach ((long characterId, long accountId) in GuestList) { + writer.WriteLong(accountId); + writer.WriteLong(characterId); + }*/ + } +} diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index 029dd1ad2..4ff8a8186 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -102,6 +102,9 @@ public static class Constant { public const int EmpowermentNpc = 11003416; public const int InteriorPortalCubeId = 50400158; public const int PortalEntryId = 50400190; + public const int Grade1WeddingCouponItemId = 20303166; + public const int Grade2WeddingCouponItemId = 20303167; + public const int Grade3WeddingCouponItemId = 20303168; public const long FurnishingBaseId = 2870000000000000000; public const bool AllowWaterOnGround = false; @@ -169,6 +172,7 @@ public static class Constant { public const float UGCShopProfitFee = 0.25f; public const int UGCShopProfitDelayDays = 10; public const int PartyFinderListingsPageCount = 12; + public const int ProposalItemId = 11600482; #endregion #region table/constants.xml diff --git a/Maple2.Model/Metadata/ServerTable/ScriptConditionTable.cs b/Maple2.Model/Metadata/ServerTable/ScriptConditionTable.cs index 957af81f7..77b53d6e3 100644 --- a/Maple2.Model/Metadata/ServerTable/ScriptConditionTable.cs +++ b/Maple2.Model/Metadata/ServerTable/ScriptConditionTable.cs @@ -1,6 +1,7 @@ -using System.Collections.Generic; -using Maple2.Model.Enum; +using Maple2.Model.Enum; using Maple2.Model.Game; +using MaidData = Maple2.Model.Metadata.ScriptConditionMetadata.MaidData; +using WeddingData = Maple2.Model.Metadata.ScriptConditionMetadata.WeddingData; namespace Maple2.Model.Metadata; @@ -10,13 +11,8 @@ public record ScriptConditionMetadata( int Id, // QuestId or NpcId int ScriptId, ScriptType Type, - bool MaidAuthority, - bool MaidExpired, - bool MaidReadyToPay, - int MaidClosenessRank, - KeyValuePair MaidClosenessTime, - KeyValuePair MaidMoodTime, - KeyValuePair MaidDaysBeforeExpired, + MaidData Maid, + WeddingData Wedding, IReadOnlyList JobCode, IReadOnlyDictionary QuestStarted, IReadOnlyDictionary QuestCompleted, @@ -25,4 +21,22 @@ public record ScriptConditionMetadata( KeyValuePair Meso, KeyValuePair Level, KeyValuePair AchieveCompleted, - bool InGuild); + bool InGuild) { + + public record MaidData( + bool Authority, + bool Expired, + bool ReadyToPay, + int ClosenessRank, + KeyValuePair ClosenessTime, + KeyValuePair MoodTime, + KeyValuePair DaysBeforeExpired + ); + + public record WeddingData( + MaritalStatus? UserState, + KeyValuePair HallState, + bool? HasReservation, + int MarriageDays, + string CoolingOff); +} diff --git a/Maple2.Model/Metadata/Table/WeddingTable.cs b/Maple2.Model/Metadata/Table/WeddingTable.cs index 4c06e8946..4f162c91d 100644 --- a/Maple2.Model/Metadata/Table/WeddingTable.cs +++ b/Maple2.Model/Metadata/Table/WeddingTable.cs @@ -2,9 +2,31 @@ namespace Maple2.Model.Metadata; -public record WeddingRewardTable(IReadOnlyDictionary Entries) : Table; +public record WeddingTable(IReadOnlyDictionary Rewards, + IReadOnlyDictionary Packages) : Table; public record WeddingReward( MarriageExpType Type, int Amount, MarriageExpLimit Limit); + +public record WeddingPackage( + int Id, + int PlannerId, + IReadOnlyDictionary Halls) { + public record HallData( + int Id, + int MapId, + int NightMapId, + int Tier, + int MeretCost, + IReadOnlyList Items, + IReadOnlyList CompleteItems) { + + public record Item( + int ItemId, + short Rarity, + int Amount, + bool NightOnly); + } +} diff --git a/Maple2.Model/Metadata/TableMetadata.cs b/Maple2.Model/Metadata/TableMetadata.cs index f9e74522d..433b32a02 100644 --- a/Maple2.Model/Metadata/TableMetadata.cs +++ b/Maple2.Model/Metadata/TableMetadata.cs @@ -77,5 +77,5 @@ public override int GetHashCode() { [JsonDerivedType(typeof(WorldMapTable), typeDiscriminator: "worldmap")] [JsonDerivedType(typeof(SurvivalSkinInfoTable), typeDiscriminator: "survivalskininfo")] [JsonDerivedType(typeof(BannerTable), typeDiscriminator: "banner")] -[JsonDerivedType(typeof(WeddingRewardTable), typeDiscriminator: "weddingreward")] +[JsonDerivedType(typeof(WeddingTable), typeDiscriminator: "wedding*")] public abstract record Table; diff --git a/Maple2.Server.Core/proto/channel/channel.proto b/Maple2.Server.Core/proto/channel/channel.proto index 25404b449..3b06df01f 100644 --- a/Maple2.Server.Core/proto/channel/channel.proto +++ b/Maple2.Server.Core/proto/channel/channel.proto @@ -34,6 +34,8 @@ service Channel { rpc TimeEvent(TimeEventRequest) returns (TimeEventResponse); // Game Events rpc GameEvent(GameEventRequest) returns (GameEventResponse); + // Marriage + rpc Marriage(maple2.MarriageRequest) returns (maple2.MarriageResponse); } message GuildRequest { diff --git a/Maple2.Server.Core/proto/common.proto b/Maple2.Server.Core/proto/common.proto index 4b7c33bdc..4b6a558df 100644 --- a/Maple2.Server.Core/proto/common.proto +++ b/Maple2.Server.Core/proto/common.proto @@ -242,3 +242,19 @@ message ClubInfoRequest { message ClubInfoResponse { repeated ClubInfo clubs = 1; } + +message MarriageRequest { + message RemoveMarriage { } + message RemoveWeddingHall { } + + int64 id = 1; + int64 receiver_id = 2; + oneof marriage { + RemoveMarriage remove_marriage = 3; + RemoveWeddingHall remove_wedding_hall = 4; + } +} + +message MarriageResponse { + int32 error = 1; +} diff --git a/Maple2.Server.Core/proto/world/world.proto b/Maple2.Server.Core/proto/world/world.proto index 31d8550bf..710c2cb97 100644 --- a/Maple2.Server.Core/proto/world/world.proto +++ b/Maple2.Server.Core/proto/world/world.proto @@ -46,6 +46,8 @@ service World { rpc TimeEvent(TimeEventRequest) returns (TimeEventResponse); // Add channel rpc AddChannel(AddChannelRequest) returns (AddChannelResponse); + // Marriage + rpc Marriage(maple2.MarriageRequest) returns (maple2.MarriageResponse); } enum Server { diff --git a/Maple2.Server.Game/Commands/DebugCommand.cs b/Maple2.Server.Game/Commands/DebugCommand.cs index 5102a7866..674b9adaf 100644 --- a/Maple2.Server.Game/Commands/DebugCommand.cs +++ b/Maple2.Server.Game/Commands/DebugCommand.cs @@ -115,6 +115,19 @@ public SendRawPacketCommand(GameSession session) : base("packet", "Sends a raw p } private void Handle(InvocationContext ctx, string[] packet) { + // Ensure packet is a valid hex string + if (packet.Any(x => x.Length % 2 != 0)) { + ctx.Console.Error.WriteLine("Invalid packet. Each byte must be represented by 2 characters."); + return; + } + + // If packet is less than 4 bytes, it's invalid + if (packet.Length < 4) { + ctx.Console.Error.WriteLine("Invalid packet. Must be at least 4 bytes (8 characters)."); + return; + } + + byte[] bytes = packet.Select(x => byte.Parse(x, System.Globalization.NumberStyles.HexNumber)).ToArray(); session.Send(bytes); diff --git a/Maple2.Server.Game/Manager/Items/InventoryManager.cs b/Maple2.Server.Game/Manager/Items/InventoryManager.cs index 0b5176599..4cd36bc1f 100644 --- a/Maple2.Server.Game/Manager/Items/InventoryManager.cs +++ b/Maple2.Server.Game/Manager/Items/InventoryManager.cs @@ -531,12 +531,25 @@ public IEnumerable Find(int id, int rarity = -1) { foreach (Item item in items) { if (item.Id != id) continue; if (rarity != -1 && item.Rarity != rarity) continue; + if (item.IsExpired()) continue; yield return item; } } } + public IEnumerable Find(ItemTag itemTag) { + lock (session.Item) { + foreach ((InventoryType type, ItemCollection items) in tabs) { + foreach (Item item in items) { + if (item.IsExpired()) continue; + if (item.Metadata.Property.Tag != itemTag) continue; + yield return item; + } + } + } + } + #region Internal (No Locks) private bool RemoveInternal(long uid, int amount, [NotNullWhen(true)] out Item? removed) { ItemCollection? items = tabs.Values.FirstOrDefault(collection => collection.Contains(uid)); diff --git a/Maple2.Server.Game/Manager/MarriageManager.cs b/Maple2.Server.Game/Manager/MarriageManager.cs index 31dfb6fcd..8d8598862 100644 --- a/Maple2.Server.Game/Manager/MarriageManager.cs +++ b/Maple2.Server.Game/Manager/MarriageManager.cs @@ -1,6 +1,11 @@ -using Maple2.Database.Storage; +using Maple2.Database.Extensions; +using Maple2.Database.Storage; +using Maple2.Model.Enum; +using Maple2.Model.Error; using Maple2.Model.Game; +using Maple2.Model.Metadata; using Maple2.Server.Core.Sync; +using Maple2.Server.Game.Model; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; using Maple2.Server.Game.Util.Sync; @@ -12,8 +17,14 @@ public sealed class MarriageManager { private readonly GameSession session; public Marriage Marriage; + public WeddingHall WeddingHall; private readonly CancellationTokenSource tokenSource; + public MarriagePartner Partner => session.CharacterId == Marriage.Partner1.CharacterId ? Marriage.Partner2 : Marriage.Partner1; + + public WeddingHall? UnconfirmedReservation; + private long couponItemUid; + private readonly ILogger logger = Log.Logger.ForContext(); public MarriageManager(GameSession session) { @@ -22,10 +33,115 @@ public MarriageManager(GameSession session) { using GameStorage.Request db = session.GameStorage.Context(); Marriage = db.GetMarriage(session.CharacterId) ?? Marriage.Default; Marriage.Exp = GetExp(); + + WeddingHall = db.GetWeddingHall(Marriage) ?? WeddingHall.Default; } public void Load() { - session.Send(MarriagePacket.Load(Marriage)); + CheckEngagementExpiration(); + CheckMarriageHallExpiration(); + session.Send(WeddingPacket.UpdateMarriage(Marriage)); + session.Send(WeddingPacket.UpdateHall(WeddingHall)); + } + + private void CheckEngagementExpiration() { + if (Marriage.CreationTime <= 0 || Marriage.Status != MaritalStatus.Engaged || + Marriage.CreationTime.FromEpochSeconds().AddDays(7) >= DateTime.Now) { + return; + } + + using GameStorage.Request db = session.GameStorage.Context(); + if (!db.DeleteMarriage(Marriage.Id)) { + session.Send(WeddingPacket.Error(WeddingError.s_wedding_result_err_system)); + return; + } + + var mail = new Mail { + ReceiverId = Marriage.Partner2.CharacterId, + SenderId = Marriage.Partner2.CharacterId, + Type = MailType.System, + Content = 40000006.ToString(), + }; + + mail = db.CreateMail(mail); + if (mail == null) { + logger.Error("Failed to create mail for expired engagement for {CharacterId}", Marriage.Partner2.CharacterId); + } else { + try { + session.World.MailNotification(new MailNotificationRequest { + CharacterId = Marriage.Partner2.CharacterId, + MailId = mail.Id, + }); + } catch { /* ignored */ + } + } + + var partnerMail = new Mail { + ReceiverId = Marriage.Partner1.CharacterId, + SenderId = Marriage.Partner1.CharacterId, + Type = MailType.System, + Content = 40000006.ToString(), + }; + + partnerMail = db.CreateMail(partnerMail); + if (partnerMail == null) { + logger.Error("Failed to create mail for expired engagement for {CharacterId}", Marriage.Partner1.CharacterId); + } else { + try { + session.World.MailNotification(new MailNotificationRequest { + CharacterId = Marriage.Partner1.CharacterId, + MailId = partnerMail.Id, + }); + } catch { /* ignored */ + } + } + + + try { + session.World.Marriage(new MarriageRequest { + ReceiverId = Marriage.Partner1.CharacterId, + RemoveMarriage = new MarriageRequest.Types.RemoveMarriage(), + }); + } catch { /* ignored */ + } + + RemoveMarriage(false); + } + + public void CheckMarriageHallExpiration() { + if (WeddingHall.Id == 0 || WeddingHall.CeremonyTime.FromEpochSeconds().AddHours(1) > DateTime.Now) { + return; + } + + using GameStorage.Request db = session.GameStorage.Context(); + if (!db.DeleteWeddingHall(WeddingHall.Id)) { + logger.Error("Failed to delete wedding hall for {CharacterId}. Hall Id: {WeddingHallId}", session.CharacterId, WeddingHall.Id); + return; + } + + try { + session.World.Marriage(new MarriageRequest { + ReceiverId = Partner.CharacterId, + RemoveWeddingHall = new MarriageRequest.Types.RemoveWeddingHall(), + }); + } catch { /* ignored */ } + + RemoveMarriage(false); + } + + public void RemoveMarriage(bool update = true) { + session.Marriage.Marriage = Marriage.Default; + session.Player.Value.Character.MarriageInfo = new MarriageInfo(); + if (update) { + session.Send(WeddingPacket.UpdateMarriage(Marriage)); + } + } + + public void RemoveWeddingHall(bool update = true) { + session.Marriage.WeddingHall = WeddingHall.Default; + if (update) { + session.Send(WeddingPacket.UpdateHall(WeddingHall)); + } } private long GetExp() { @@ -35,6 +151,450 @@ private long GetExp() { return daysMarried * 5 + Marriage.ExpHistory.Sum(exp => exp.Amount); } + public WeddingError Propose(FieldPlayer target) { + if (!session.Item.Inventory.Find(Constant.ProposalItemId).Any()) { + return WeddingError.s_wedding_result_err_not_find_propose_item; + } + + if (session.Marriage.Marriage.Status == MaritalStatus.Engaged) { + return WeddingError.s_wedding_result_err_propose_state_to; + } + + if (session.Marriage.Marriage.Status == MaritalStatus.Married) { + return WeddingError.s_wedding_result_err_marriage_state_to; + } + + + if (!session.Buddy.IsBuddy(target.Value.Character.Id)) { + return WeddingError.s_wedding_result_err_not_buddy; + } + + if (!target.Session.Item.Inventory.Find(Constant.ProposalItemId).Any()) { + return WeddingError.s_wedding_result_err_not_find_propose_item; + } + + return WeddingError.s_wedding_msg_box_propose_to; + } + + public WeddingError AcceptProposal(FieldPlayer target) { + // Find the proposal items of each player + Item? proposalItem = session.Item.Inventory.Find(Constant.ProposalItemId).FirstOrDefault(); + if (proposalItem == null) { + return WeddingError.s_wedding_result_err_not_find_propose_item; + } + + Item? otherProposalItem = target.Session.Item.Inventory.Find(Constant.ProposalItemId).FirstOrDefault(); + if (otherProposalItem == null) { + return WeddingError.s_wedding_result_err_not_find_propose_item; + } + + // Remove the proposal items from each player + if (!session.Item.Inventory.Consume(proposalItem.Uid, 1)) { + return WeddingError.s_wedding_result_err_not_find_propose_item; + + } + + if (!target.Session.Item.Inventory.Consume(otherProposalItem.Uid, 1)) { + // TODO: Something is broken about this + //return WeddingError.s_wedding_result_err_not_find_propose_item; + } + + using GameStorage.Request db = session.GameStorage.Context(); + Marriage? marriage = db.CreateMarriage(session.CharacterId, target.Session.CharacterId); + if (marriage == null) { + return WeddingError.s_wedding_result_err_system; + } + Marriage = marriage; + session.Send(WeddingPacket.Engaged(Marriage)); + + Marriage? targetMarriage = db.GetMarriage(target.Session.CharacterId); + if (targetMarriage == null) { + return WeddingError.s_wedding_result_err_system; + } + + target.Session.Marriage.Marriage = targetMarriage; + target.Session.Send(WeddingPacket.Engaged(targetMarriage)); + return WeddingError.none; + } + + public bool ValidHallTime(long ceremonyTime, out WeddingError error) { + using GameStorage.Request db = session.GameStorage.Context(); + if (ceremonyTime > 0) { + if (!db.WeddingHallTimeIsAvailable(ceremonyTime)) { + error = WeddingError.s_wedding_result_already_same_reservation; + return false; + } + + if (ceremonyTime < DateTime.Now.ToEpochSeconds()) { + error = WeddingError.s_wedding_result_early_wedding_reserve; + return false; + } + } + error = WeddingError.none; + return true; + } + + public WeddingError ReserveHall(WeddingHall hall, bool useCoupon = false) { + if (!ValidHallTime(hall.CeremonyTime, out WeddingError error)) { + return error; + } + + if (!session.TableMetadata.WeddingTable.Packages.TryGetValue(hall.PackageId, out WeddingPackage? package) || + !package.Halls.TryGetValue(hall.PackageHallId, out WeddingPackage.HallData? hallData)) { + return WeddingError.s_wedding_result_err_invalid_wedding_hall; + } + + if (useCoupon) { + ItemTag couponTag = package.Id switch { + 1 => ItemTag.WeddingHallCoupon_Grade1, + 2 => ItemTag.WeddingHallCoupon_Grade2, + 3 => ItemTag.WeddingHallCoupon_Grade3, + _ => ItemTag.None, + }; + IList items = session.Item.Inventory.Find(couponTag).ToList(); + if (!items.Any()) { + return WeddingError.s_wedding_result_lack_coupon; + } + + couponItemUid = items.First().Uid; + } else { + if (session.Currency.CanAddMeret(-hallData.MeretCost) != -hallData.MeretCost) { + return WeddingError.s_wedding_result_err_lack_meratal; + } + } + + if (!session.Field.TryGetPlayer(session.Marriage.Marriage.Partner1.Name, out FieldPlayer? partner)) { + return WeddingError.s_wedding_result_err_same_field; + } + + session.Marriage.UnconfirmedReservation = hall; + partner.Session.Marriage.UnconfirmedReservation = hall; + + partner.Session.Send(WeddingPacket.Reserve(hall)); + return WeddingError.s_wedding_result_send_wedding_hall_reservation; + } + + public WeddingError ConfirmReservation() { + if (UnconfirmedReservation == null) { + return WeddingError.s_wedding_result_err_invalid_wedding_hall; + } + + if (!session.TableMetadata.WeddingTable.Packages.TryGetValue(UnconfirmedReservation.PackageId, out WeddingPackage? package) || + !package.Halls.TryGetValue(UnconfirmedReservation.PackageHallId, out WeddingPackage.HallData? hallData)) { + return WeddingError.s_wedding_result_err_invalid_wedding_hall; + } + + if (!session.Field.TryGetPlayerById(session.Marriage.Marriage.Partner1.CharacterId, out FieldPlayer? partner)) { + return WeddingError.s_wedding_result_err_same_field; + } + + if (!PayWeddingHall(partner, hallData, 0, out WeddingError payError)) { + return payError; + } + + using GameStorage.Request db = session.GameStorage.Context(); + WeddingHall? weddingHall = db.CreateWeddingHall(UnconfirmedReservation, Marriage); + if (weddingHall == null) { + logger.Error("Failed to create wedding hall for {CharacterId}", session.CharacterId); + return WeddingError.s_wedding_result_err_system; + } + + WeddingHall = weddingHall; + partner.Session.Marriage.WeddingHall = weddingHall; + + partner.Session.Send(WeddingPacket.ConfirmReservation(WeddingHall)); + session.Send(WeddingPacket.ConfirmReservation(WeddingHall)); + + session.ConditionUpdate(ConditionType.wedding_hall_reserve); + partner.Session.ConditionUpdate(ConditionType.wedding_hall_reserve); + + return WeddingError.s_wedding_propose_ok; + } + + private bool PayWeddingHall(FieldPlayer partner, WeddingPackage.HallData hallData, long differenceCost, out WeddingError error) { + if (couponItemUid != 0) { + if (!partner.Session.Item.Inventory.Consume(couponItemUid, 1)) { + error = WeddingError.s_wedding_result_lack_coupon; + return false; + } + couponItemUid = 0; + } else { + long cost = differenceCost > 0 ? differenceCost : hallData.MeretCost; + if (partner.Session.Currency.CanAddMeret(-cost) != -cost) { + error = WeddingError.s_wedding_result_err_lack_meratal; + return false; + } + partner.Session.Currency.Meret -= cost; + + // Mail gift only when paying with Meret, otherwise users can exploit. + MailReservationGifts(partner, hallData); + } + + error = WeddingError.none; + return true; + } + + private void MailReservationGifts(FieldPlayer partner, WeddingPackage.HallData metadata) { + var mail = new Mail { + ReceiverId = partner.Value.Character.Id, + SenderId = partner.Value.Character.Id, + Content = 40000008.ToString(), // Specific ID in string/systemmailcontent[locale].xml + Type = MailType.System, + }; + + using GameStorage.Request db = session.GameStorage.Context(); + mail = db.CreateMail(mail); + if (mail == null) { + logger.Error("Failed to create mail for wedding hall reservation for {CharacterId}", partner.Value.Character.Id); + return; + } + + foreach (WeddingPackage.HallData.Item rewardItem in metadata.CompleteItems) { + Item? item = session.Field.ItemDrop.CreateItem(rewardItem.ItemId, rewardItem.Rarity, rewardItem.Amount); + if (item == null) { + continue; + } + + item = db.CreateItem(mail.Id, item); + if (item == null) { + logger.Error("Failed to create item for wedding hall reservation for {CharacterId}", partner.Value.Character.Id); + continue; + } + + mail.Items.Add(item); + } + + try { + session.World.MailNotification(new MailNotificationRequest { + CharacterId = partner.Value.Character.Id, + MailId = mail.Id, + }); + } catch { /* ignored */ + } + } + + public void CancelReservationRequest() { + if (session.CharacterId != WeddingHall.ReserverCharacterId) { + session.Send(WeddingPacket.Error(WeddingError.s_wedding_result_only_booking_character)); + return; + } + + // If the wedding is about to start or has started, cannot cancel + if (DateTime.Now >= WeddingHall.CeremonyTime.FromEpochSeconds().AddMinutes(-5)) { + session.Send(WeddingPacket.Error(WeddingError.s_wedding_result_late_wedding_reserve_modify_time)); + return; + } + + if (!session.Field.TryGetPlayerById(Partner.CharacterId, out FieldPlayer? fieldPartner)) { + session.Send(WeddingPacket.Error(WeddingError.s_wedding_result_err_same_field)); + return; + } + + session.Send(WeddingPacket.Error(WeddingError.s_wedding_result_send_wedding_hall_reservation_cancel)); + fieldPartner.Session.Send(WeddingPacket.RequestCancelReservation()); + } + + public WeddingError CancelReservationReply(WeddingError response) { + if (!session.Field.TryGetPlayerById(Partner.CharacterId, out FieldPlayer? fieldPartner)) { + return WeddingError.s_wedding_result_err_same_field; + } + + if (response == WeddingError.decline_reservation_cancel) { + session.Send(WeddingPacket.DeclineCancelReservation()); + fieldPartner.Session.Send(WeddingPacket.DeclinedCancelReservation(response)); + return WeddingError.none; + } + + // Give coupon + int couponItemId = WeddingHall.PackageId switch { + 1 => Constant.Grade1WeddingCouponItemId, + 2 => Constant.Grade2WeddingCouponItemId, + 3 => Constant.Grade3WeddingCouponItemId, + _ => 0, + }; + + if (couponItemId == 0) { + logger.Error("Failed to find coupon item for wedding hall {WeddingHallId}", WeddingHall.Id); + return WeddingError.s_wedding_result_err_system; + } + + // TODO: Fix rarity ? + Item? coupon = session.Field.ItemDrop.CreateItem(couponItemId, 1, 1); + if (coupon == null) { + logger.Error("Failed to create wedding coupon for {CharacterId}", session.CharacterId); + return WeddingError.s_wedding_result_err_system; + } + + var mail = new Mail { + ReceiverId = Partner.CharacterId, + SenderId = Partner.CharacterId, + Type = MailType.System, + Content = 40000013.ToString(), // Specific ID in string/systemmailcontent[locale].xml + }; + + using GameStorage.Request db = session.GameStorage.Context(); + + mail = db.CreateMail(mail); + if (mail == null) { + logger.Error("Failed to create mail for wedding hall cancellation for {CharacterId}", Partner.CharacterId); + return WeddingError.s_wedding_result_err_system; + } + + coupon = db.CreateItem(mail.Id, coupon); + if (coupon == null) { + logger.Error("Failed to create coupon item for wedding hall cancellation for {CharacterId}", Partner.CharacterId); + return WeddingError.s_wedding_result_err_system; + } + + mail.Items.Add(coupon); + try { + session.World.MailNotification(new MailNotificationRequest { + CharacterId = Partner.CharacterId, + MailId = mail.Id, + }); + } catch { /* ignored */ + } + + if (!db.DeleteWeddingHall(WeddingHall.Id)) { + logger.Error("Failed to delete wedding hall for {CharacterId}. Hall Id: {WeddingHallId}", session.CharacterId, WeddingHall.Id); + } + + session.Send(WeddingPacket.AcceptCancelReservation(response)); + fieldPartner.Session.Send(WeddingPacket.AcceptCancelReservation(response)); + WeddingHall = WeddingHall.Default; + fieldPartner.Session.Marriage.WeddingHall = WeddingHall.Default; + + session.ConditionUpdate(ConditionType.wedding_hall_cancel); + fieldPartner.Session.ConditionUpdate(ConditionType.wedding_hall_cancel); + return WeddingError.none; + } + + public WeddingError CancelNoRefund() { + if (!session.Field.TryGetPlayerById(Partner.CharacterId, out FieldPlayer? fieldPartner)) { + return WeddingError.s_wedding_result_err_same_field; + } + + using GameStorage.Request db = session.GameStorage.Context(); + if (!db.DeleteWeddingHall(WeddingHall.Id)) { + logger.Error("Failed to delete wedding hall for {CharacterId}. Hall Id: {WeddingHallId}", session.CharacterId, WeddingHall.Id); + } + + session.Send(WeddingPacket.AcceptCancelReservation(WeddingError.accept_reservation_cancel)); + fieldPartner.Session.Send(WeddingPacket.AcceptCancelReservation(WeddingError.accept_reservation_cancel)); + WeddingHall = WeddingHall.Default; + fieldPartner.Session.Marriage.WeddingHall = WeddingHall.Default; + + session.ConditionUpdate(ConditionType.wedding_hall_cancel); + fieldPartner.Session.ConditionUpdate(ConditionType.wedding_hall_cancel); + + return WeddingError.none; + } + + public WeddingError ChangeReservationRequest(WeddingHall hall) { + if (WeddingHall.Id == 0) { + return WeddingError.s_wedding_result_err_invalid_wedding_hall; + } + + // If it's all the same, it's unnecessary to change + if (hall.CeremonyTime == WeddingHall.CeremonyTime && + hall.PackageId == WeddingHall.PackageId && + hall.PackageHallId == WeddingHall.PackageHallId && + hall.Public == WeddingHall.Public) { + return WeddingError.s_wedding_result_already_weddinghall_reserve; + } + + if (!session.TableMetadata.WeddingTable.Packages.TryGetValue(hall.PackageId, out WeddingPackage? unconfirmedPackage) || + !unconfirmedPackage.Halls.TryGetValue(hall.PackageHallId, out WeddingPackage.HallData? unconfirmedHallData)) { + return WeddingError.s_wedding_result_err_invalid_wedding_hall; + } + + if (!session.TableMetadata.WeddingTable.Packages.TryGetValue(WeddingHall.PackageId, out WeddingPackage? currentWeddingPackage) || + !currentWeddingPackage.Halls.TryGetValue(WeddingHall.PackageHallId, out WeddingPackage.HallData? currentHallData)) { + return WeddingError.s_wedding_result_err_invalid_wedding_hall; + } + + // Cannot tier down + if (unconfirmedHallData.Tier < currentHallData.Tier) { + return WeddingError.s_wedding_result_err_invalid_wedding_hall; + } + + long cost = GetWeddingHallChangeCost(currentHallData, unconfirmedHallData); + if (session.Currency.CanAddMeret(-cost) != -cost) { + return WeddingError.s_wedding_result_err_lack_meratal; + } + + if (!ValidHallTime(hall.CeremonyTime, out WeddingError error)) { + return error; + } + + if (!session.Field.TryGetPlayerById(Partner.CharacterId, out FieldPlayer? partnerPlayer)) { + return WeddingError.s_wedding_result_err_same_field; + } + + UnconfirmedReservation = hall; + partnerPlayer.Session.Marriage.UnconfirmedReservation = hall; + + partnerPlayer.Session.Send(WeddingPacket.ChangeReservationRequest(hall, cost)); + return WeddingError.s_wedding_result_send_wedding_hall_reservation_change; + } + + public WeddingError ChangeReservationReply(WeddingError response) { + if (UnconfirmedReservation == null) { + return WeddingError.s_wedding_result_err_invalid_wedding_hall; + } + + if (!session.TableMetadata.WeddingTable.Packages.TryGetValue(UnconfirmedReservation.PackageId, out WeddingPackage? package) || + !package.Halls.TryGetValue(UnconfirmedReservation.PackageHallId, out WeddingPackage.HallData? hallData)) { + return WeddingError.s_wedding_result_err_invalid_wedding_hall; + } + + if (!session.Field.TryGetPlayerById(session.Marriage.Marriage.Partner1.CharacterId, out FieldPlayer? partner)) { + return WeddingError.s_wedding_result_err_same_field; + } + + if (response == WeddingError.decline_reservation_change) { + // TODO: Confirm these packets. There seems to be no response. + session.Send(WeddingPacket.DeclineReservationChange()); + partner.Session.Send(WeddingPacket.DeclineReservationChange()); + return WeddingError.none; + } + + if (!session.TableMetadata.WeddingTable.Packages.TryGetValue(WeddingHall.PackageId, out WeddingPackage? currentWeddingPackage) || + !currentWeddingPackage.Halls.TryGetValue(WeddingHall.PackageHallId, out WeddingPackage.HallData? currentHallData)) { + logger.Error("Failed to find current wedding hall data for {CharacterId}", session.CharacterId); + return WeddingError.s_wedding_result_err_system; + } + + long differenceCost = GetWeddingHallChangeCost(currentHallData, hallData); + + if (!PayWeddingHall(partner, hallData, differenceCost, out WeddingError payError)) { + return payError; + } + + using GameStorage.Request db = session.GameStorage.Context(); + if (!db.DeleteWeddingHall(WeddingHall.Id)) { + logger.Error("Failed to delete wedding hall for {CharacterId}. Hall Id: {WeddingHallId}", session.CharacterId, WeddingHall.Id); + return WeddingError.s_wedding_result_err_system; + } + + WeddingHall? weddingHall = db.CreateWeddingHall(UnconfirmedReservation, Marriage); + if (weddingHall == null) { + logger.Error("Failed to create wedding hall for {CharacterId}", session.CharacterId); + return WeddingError.s_wedding_result_err_system; + } + + WeddingHall = weddingHall; + partner.Session.Marriage.WeddingHall = weddingHall; + session.Send(WeddingPacket.AcceptReservationChange(WeddingHall, differenceCost)); + partner.Session.Send(WeddingPacket.AcceptReservationChange(WeddingHall, differenceCost)); + + return WeddingError.none; + } + + private long GetWeddingHallChangeCost(WeddingPackage.HallData currentHallData, WeddingPackage.HallData unconfirmedHallData) { + return Math.Max(0, unconfirmedHallData.MeretCost - currentHallData.MeretCost); + } + #region PlayerInfo Events private void BeginListen(MarriagePartner partner) { if (Marriage.Id == 0) { @@ -73,8 +633,8 @@ private bool SyncUpdate(CancellationToken cancel, long id, UpdateField type, IPl string name = partner.Info.Name; partner.Info.Update(type, info); - if (name != partner.Info.Name) { - session.Send(MarriagePacket.Load(Marriage)); + if (name != partner.Info.Name || wasOnline != partner.Info.Online) { + session.Send(WeddingPacket.UpdateMarriage(Marriage)); } return false; } diff --git a/Maple2.Server.Game/PacketHandlers/UserSyncHandler.cs b/Maple2.Server.Game/PacketHandlers/UserSyncHandler.cs index 23f3db0e4..31b654021 100644 --- a/Maple2.Server.Game/PacketHandlers/UserSyncHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/UserSyncHandler.cs @@ -39,7 +39,9 @@ public override void Handle(GameSession session, IByteReader packet) { stateSyncs[i] = stateSync; packet.ReadInt(); // ClientTicks - packet.ReadInt(); // ServerTicks + if (playerState != ActorState.WeddingEmotion) { + packet.ReadInt(); // ServerTicks + } } using (var buffer = new PoolByteWriter()) { diff --git a/Maple2.Server.Game/PacketHandlers/WeddingBillboardHandler.cs b/Maple2.Server.Game/PacketHandlers/WeddingBillboardHandler.cs new file mode 100644 index 000000000..3b53ca71e --- /dev/null +++ b/Maple2.Server.Game/PacketHandlers/WeddingBillboardHandler.cs @@ -0,0 +1,45 @@ +using System.Reflection.Metadata; +using Maple2.Database.Extensions; +using Maple2.Database.Storage; +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.PacketHandlers; +using Maple2.Server.Game.Manager; +using Maple2.Server.Game.Model; +using Maple2.Server.Game.Packets; +using Maple2.Server.Game.Session; +using Maple2.Tools.Extensions; +using Constant = Maple2.Model.Metadata.Constant; + +namespace Maple2.Server.Game.PacketHandlers; + +public class WeddingBillboardHandler : PacketHandler { + public override RecvOp OpCode => RecvOp.WeddingBillboard; + + private enum Command : byte { + Load = 4, + } + + public override void Handle(GameSession session, IByteReader packet) { + var command = packet.Read(); + + switch (command) { + case Command.Load: + HandleLoad(session, packet); + return; + } + } + + private static void HandleLoad(GameSession session, IByteReader packet) { + int unknown = packet.ReadInt(); // 0 + + using GameStorage.Request db = session.GameStorage.Context(); + IList halls = db.GetWeddingHalls().ToList(); + + session.Send(WeddingBillboardPacket.Load(halls)); + } +} diff --git a/Maple2.Server.Game/PacketHandlers/WeddingHandler.cs b/Maple2.Server.Game/PacketHandlers/WeddingHandler.cs index 3e809cd8d..1e9e92eb8 100644 --- a/Maple2.Server.Game/PacketHandlers/WeddingHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/WeddingHandler.cs @@ -1,13 +1,15 @@ -using Maple2.Database.Storage; +using Maple2.Database.Extensions; +using Maple2.Database.Storage; using Maple2.Model.Enum; +using Maple2.Model.Error; using Maple2.Model.Game; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Core.PacketHandlers; -using Maple2.Server.Game.Manager; using Maple2.Server.Game.Model; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; +using Maple2.Tools.Extensions; namespace Maple2.Server.Game.PacketHandlers; @@ -19,8 +21,8 @@ private enum Command : byte { ProposalReply = 4, CancelProposal = 5, DivorceReply = 8, - ReverseHall = 13, - ReverseHallReply = 14, + ReserveHall = 13, + ReserveHallReply = 14, CancelReservation = 16, CancelNoRefundReply = 17, CancelReservationReply = 18, @@ -42,26 +44,58 @@ public override void Handle(GameSession session, IByteReader packet) { case Command.ProposalReply: HandleProposalReply(session, packet); return; + case Command.ReserveHall: + HandleReserveHall(session, packet); + return; + case Command.ReserveHallReply: + HandleReserveHallReply(session, packet); + return; + case Command.CancelReservation: + HandleCancelReservation(session); + return; + case Command.CancelNoRefundReply: + CancelNoRefundReply(session, packet); + return; + case Command.CancelReservationReply: + HandleCancelReservationReply(session, packet); + return; + case Command.ChangeReservation: + HandleChangeReservation(session, packet); + return; + case Command.ChangeReservationReply: + HandleChangeReservationReply(session, packet); + return; + case Command.EnterCeremony: + HandleEnterCeremony(session, packet); + return; + case Command.InvitationFindUser: + HandleInvitationFindUser(session, packet); + return; + case Command.SendInvitation: + HandleSendInvitation(session, packet); + return; } } private static void HandlePropose(GameSession session, IByteReader packet) { string otherPlayerName = packet.ReadUnicodeString(); - // other partner must be in the same field and a buddy. if (!session.Field.TryGetPlayer(otherPlayerName, out FieldPlayer? otherPlayer)) { + session.Send(WeddingPacket.Error(WeddingError.s_wedding_result_err_same_field)); return; } - if (!session.Buddy.IsBuddy(otherPlayer.Value.Character.Id)) { - return; - } + WeddingError error = session.Marriage.Propose(otherPlayer); - otherPlayer.Session.Send(MarriagePacket.Proposal(session.PlayerName, session.CharacterId)); + session.Send(WeddingPacket.Error(error)); + if (error == WeddingError.s_wedding_msg_box_propose_to) { + otherPlayer.Session.Send(WeddingPacket.Proposal(session.PlayerName, session.CharacterId)); + session.ConditionUpdate(ConditionType.wedding_propose); + } } private static void HandleProposalReply(GameSession session, IByteReader packet) { - var response = packet.Read(); + var response = packet.Read(); long playerId = packet.ReadLong(); if (!session.PlayerInfo.GetOrFetch(playerId, out PlayerInfo? playerInfo) || @@ -69,23 +103,131 @@ private static void HandleProposalReply(GameSession session, IByteReader packet) return; } - if (!session.Buddy.IsBuddy(otherPlayer.Value.Character.Id)) { + if (response != WeddingError.s_wedding_propose_ok) { + session.ConditionUpdate(ConditionType.wedding_propose_decline); + otherPlayer.Session.ConditionUpdate(ConditionType.wedding_propose_declined); + otherPlayer.Session.Send(WeddingPacket.Error(response)); + return; + } + + WeddingError error = session.Marriage.AcceptProposal(otherPlayer); + if (error != WeddingError.none) { + session.Send(WeddingPacket.Error(error)); + otherPlayer.Session.Send(WeddingPacket.Error(WeddingError.s_wedding_result_decline_propose)); + } + } + + private static void HandleReserveHall(GameSession session, IByteReader packet) { + if (session.Marriage.Marriage.Status != MaritalStatus.Engaged) { + session.Send(WeddingPacket.Error(WeddingError.s_wedding_result_err_only_promise_user)); + return; + } + + if (session.Marriage.WeddingHall.Id != 0) { + session.Send(WeddingPacket.Error(WeddingError.s_wedding_result_already_reservation)); return; } + var hall = packet.ReadClass(); + bool useCoupon = packet.ReadBool(); - switch (response) { - case ProposalResponse.Accept: - using (GameStorage.Request db = session.GameStorage.Context()) { - Marriage? marriage = db.CreateMarriage(session.CharacterId, playerId); - if (marriage == null) { - return; - } - } - break; - case ProposalResponse.Decline: - case ProposalResponse.Timeout: - break; + WeddingError error = session.Marriage.ReserveHall(hall, useCoupon); + session.Send(WeddingPacket.Error(error)); + } + + private static void HandleReserveHallReply(GameSession session, IByteReader packet) { + var response = packet.Read(); + + if (response != WeddingError.confirm_reservation) { + session.Send(WeddingPacket.Error(response)); + return; } + + WeddingError error = session.Marriage.ConfirmReservation(); + if (error != WeddingError.s_wedding_propose_ok) { + session.Send(WeddingPacket.Error(error)); + } + } + + private static void HandleCancelReservation(GameSession session) { + if (session.Marriage.WeddingHall.Id == 0) { + session.Send(WeddingPacket.Error(WeddingError.s_wedding_result_not_find_weddinghall)); + return; + } + + session.Marriage.CancelReservationRequest(); + } + + private static void CancelNoRefundReply(GameSession session, IByteReader packet) { + var response = packet.Read(); + + if (response == WeddingError.decline_reservation_cancel) { + session.Send(WeddingPacket.DeclineCancelReservation()); + return; + } + + WeddingError error = session.Marriage.CancelNoRefund(); + if (error != WeddingError.none) { + session.Send(WeddingPacket.Error(error)); + } + } + + private static void HandleCancelReservationReply(GameSession session, IByteReader packet) { + var response = packet.Read(); + + WeddingError error = session.Marriage.CancelReservationReply(response); + if (error != WeddingError.none) { + session.Send(WeddingPacket.Error(error)); + } + } + + private static void HandleChangeReservation(GameSession session, IByteReader packet) { + var hall = packet.ReadClass(); + + WeddingError error = session.Marriage.ChangeReservationRequest(hall); + if (error != WeddingError.none) { + session.Send(WeddingPacket.Error(error)); + } + } + + private static void HandleChangeReservationReply(GameSession session, IByteReader packet) { + var response = packet.Read(); + + WeddingError error = session.Marriage.ChangeReservationReply(response); + if (error != WeddingError.none) { + session.Send(WeddingPacket.Error(error)); + } + } + + private static void HandleEnterCeremony(GameSession session, IByteReader packet) { + long marriageId = packet.ReadLong(); + using GameStorage.Request db = session.GameStorage.Context(); + WeddingHall? hall = db.GetWeddingHall(marriageId: marriageId); + if (hall == null) { + session.Send(WeddingPacket.Error(WeddingError.s_wedding_result_err_invalid_wedding_hall)); + return; + } + + DateTime ceremonyTime = hall.CeremonyTime.FromEpochSeconds(); + + // Can enter only within 5 minutes before the ceremony + if (DateTime.Now >= ceremonyTime.AddMinutes(-5)) { + session.Send(WeddingPacket.Error(WeddingError.s_wedding_visit_failed_invalid_time)); + return; + } + + // Cannot enter after 30 minutes from the ceremony scheduled time + if (DateTime.Now > ceremonyTime.AddMinutes(30)) { + session.Send(WeddingPacket.Error(WeddingError.s_wedding_visit_failed_common)); + return; + } + } + + private static void HandleInvitationFindUser(GameSession session, IByteReader packet) { + string inviteeName = packet.ReadUnicodeString(); + } + + private static void HandleSendInvitation(GameSession session, IByteReader packet) { + int inviteCount = packet.ReadInt(); } } diff --git a/Maple2.Server.Game/Packets/MarriagePacket.cs b/Maple2.Server.Game/Packets/MarriagePacket.cs deleted file mode 100644 index 53d66e35e..000000000 --- a/Maple2.Server.Game/Packets/MarriagePacket.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Maple2.Model.Error; -using Maple2.Model.Game; -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 MarriagePacket { - private enum Command : byte { - Load = 0, - Proposal = 3, - } - - public static ByteWriter Load(Marriage marriage) { - var pWriter = Packet.Of(SendOp.Wedding); - pWriter.Write(Command.Load); - pWriter.WriteClass(marriage); - - return pWriter; - } - - public static ByteWriter Proposal(string name, long characterId) { - var pWriter = Packet.Of(SendOp.Wedding); - pWriter.Write(Command.Proposal); - pWriter.WriteLong(characterId); - pWriter.WriteUnicodeString(name); - - return pWriter; - } -} diff --git a/Maple2.Server.Game/Packets/WeddingBillboardPacket.cs b/Maple2.Server.Game/Packets/WeddingBillboardPacket.cs new file mode 100644 index 000000000..770a34b55 --- /dev/null +++ b/Maple2.Server.Game/Packets/WeddingBillboardPacket.cs @@ -0,0 +1,25 @@ +using Maple2.Model.Game; +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 WeddingBillboardPacket { + private enum Command : byte { + Load = 4, + } + + public static ByteWriter Load(IList halls) { + var pWriter = Packet.Of(SendOp.WeddingBillboard); + pWriter.Write(Command.Load); + pWriter.WriteInt(halls.Count); + foreach (WeddingHall hall in halls) { + pWriter.WriteLong(hall.MarriageId); + pWriter.WriteClass(hall); + } + + return pWriter; + } +} diff --git a/Maple2.Server.Game/Packets/WeddingPacket.cs b/Maple2.Server.Game/Packets/WeddingPacket.cs new file mode 100644 index 000000000..08fda7dca --- /dev/null +++ b/Maple2.Server.Game/Packets/WeddingPacket.cs @@ -0,0 +1,155 @@ +using Maple2.Model.Error; +using Maple2.Model.Game; +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 WeddingPacket { + private enum Command : byte { + UpdateMarriage = 0, + UpdateHall = 1, + Error = 2, + Proposal = 3, + Engaged = 6, + Reserve = 13, + ConfirmReservation = 15, + RequestCancelReservation = 16, + DeclinedCancelReservation = 17, + DeclineCancelReservation = 18, + AcceptCancelReservation = 19, + ChangeReservationRequest = 20, + DeclineReservationChange = 21, + AcceptReservationChange = 22, + Unknown23 = 23, + SearchInvitee = 27, + SendInvite = 29, + StartCeremony = 30, + } + + public static ByteWriter UpdateMarriage(Marriage marriage) { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.UpdateMarriage); + pWriter.WriteClass(marriage); + + return pWriter; + } + + public static ByteWriter UpdateHall(WeddingHall hall) { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.UpdateHall); + pWriter.WriteClass(hall); + pWriter.WriteInt(); + + return pWriter; + } + + public static ByteWriter Error(WeddingError error) { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.Error); + pWriter.Write(error); + + return pWriter; + } + + public static ByteWriter Proposal(string name, long characterId) { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.Proposal); + pWriter.WriteLong(characterId); + pWriter.WriteUnicodeString(name); + + return pWriter; + } + + public static ByteWriter Engaged(Marriage marriage) { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.Engaged); + pWriter.WriteClass(marriage); + + return pWriter; + } + + public static ByteWriter Reserve(WeddingHall hall) { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.Reserve); + pWriter.WriteClass(hall); + + return pWriter; + } + + public static ByteWriter ConfirmReservation(WeddingHall hall) { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.ConfirmReservation); + pWriter.WriteClass(hall); + + return pWriter; + } + + public static ByteWriter RequestCancelReservation() { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.RequestCancelReservation); + + return pWriter; + } + + public static ByteWriter DeclineCancelReservation() { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.DeclineCancelReservation); + + return pWriter; + } + + public static ByteWriter DeclinedCancelReservation(WeddingError reply) { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.DeclinedCancelReservation); + pWriter.Write(reply); + + return pWriter; + } + + public static ByteWriter AcceptCancelReservation(WeddingError reply) { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.AcceptCancelReservation); + pWriter.Write(reply); + + return pWriter; + } + + public static ByteWriter ChangeReservationRequest(WeddingHall hall, long differenceCost) { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.ChangeReservationRequest); + pWriter.WriteClass(hall); + pWriter.WriteLong(differenceCost); + + return pWriter; + } + + public static ByteWriter DeclineReservationChange() { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.DeclineReservationChange); + + return pWriter; + } + + public static ByteWriter AcceptReservationChange(WeddingHall hall, long differenceCost) { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.AcceptReservationChange); + pWriter.WriteClass(hall); + pWriter.WriteLong(differenceCost); + + return pWriter; + } + + /// + /// Gets sent every field load? not sure why + /// + /// + public static ByteWriter Unknown23() { + var pWriter = Packet.Of(SendOp.Wedding); + pWriter.Write(Command.Unknown23); + + return pWriter; + } +} diff --git a/Maple2.Server.Game/Service/ChannelService.Marriage.cs b/Maple2.Server.Game/Service/ChannelService.Marriage.cs new file mode 100644 index 000000000..240d7a1a8 --- /dev/null +++ b/Maple2.Server.Game/Service/ChannelService.Marriage.cs @@ -0,0 +1,23 @@ +using Grpc.Core; +using Maple2.Server.Game.Session; + +namespace Maple2.Server.Game.Service; + +public partial class ChannelService { + public override Task Marriage(MarriageRequest request, ServerCallContext context) { + if (!server.GetSession(request.ReceiverId, out GameSession? session)) { + throw new RpcException(new Status(StatusCode.NotFound, $"Unable to find: {request.ReceiverId}")); + } + + switch (request.MarriageCase) { + case MarriageRequest.MarriageOneofCase.RemoveMarriage: + session.Marriage.RemoveMarriage(); + break; + case MarriageRequest.MarriageOneofCase.RemoveWeddingHall: + session.Marriage.RemoveWeddingHall(); + break; + } + + return Task.FromResult(new MarriageResponse()); + } +} diff --git a/Maple2.Server.Game/Util/ConditionUtil.cs b/Maple2.Server.Game/Util/ConditionUtil.cs index de28afc6c..263ba56ea 100644 --- a/Maple2.Server.Game/Util/ConditionUtil.cs +++ b/Maple2.Server.Game/Util/ConditionUtil.cs @@ -136,6 +136,10 @@ private static bool CheckCode(this ConditionMetadata.Parameters code, GameSessio case ConditionType.extend_house: case ConditionType.exp: case ConditionType.item_merge_success: + case ConditionType.wedding_propose: + case ConditionType.wedding_propose_decline: + case ConditionType.wedding_propose_declined: + case ConditionType.wedding_hall_cancel: return true; } return false; @@ -253,6 +257,10 @@ private static bool CheckTarget(this ConditionMetadata.Parameters target, GameSe case ConditionType.extend_house: case ConditionType.exp: case ConditionType.item_merge_success: + case ConditionType.wedding_propose: + case ConditionType.wedding_propose_decline: + case ConditionType.wedding_propose_declined: + case ConditionType.wedding_hall_cancel: return true; } return false; diff --git a/Maple2.Server.World/Migrations/20241012062127_WeddingHall.Designer.cs b/Maple2.Server.World/Migrations/20241012062127_WeddingHall.Designer.cs new file mode 100644 index 000000000..c23e3e172 --- /dev/null +++ b/Maple2.Server.World/Migrations/20241012062127_WeddingHall.Designer.cs @@ -0,0 +1,1811 @@ +// +using System; +using Maple2.Database.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Maple2.Server.World.Migrations +{ + [DbContext(typeof(Ms2Context))] + [Migration("20241012062127_WeddingHall")] + partial class WeddingHall + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Maple2.Database.Model.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ActiveGoldPass") + .HasColumnType("tinyint(1)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("json"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("MachineId") + .HasColumnType("binary(16)"); + + b.Property("MarketLimits") + .IsRequired() + .HasColumnType("json"); + + b.Property("MaxCharacters") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(4); + + b.Property("Online") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumRewardsClaimed") + .IsRequired() + .HasColumnType("json"); + + b.Property("PremiumTime") + .HasColumnType("bigint"); + + b.Property("PrestigeCurrentExp") + .HasColumnType("bigint"); + + b.Property("PrestigeExp") + .HasColumnType("bigint"); + + b.Property("PrestigeLevel") + .HasColumnType("int"); + + b.Property("PrestigeLevelsGained") + .HasColumnType("int"); + + b.Property("PrestigeMissions") + .IsRequired() + .HasColumnType("json"); + + b.Property("PrestigeRewardsClaimed") + .IsRequired() + .HasColumnType("json"); + + b.Property("SurvivalExp") + .HasColumnType("bigint"); + + b.Property("SurvivalGoldLevelRewardClaimed") + .HasColumnType("int"); + + b.Property("SurvivalLevel") + .HasColumnType("int"); + + b.Property("SurvivalSilverLevelRewardClaimed") + .HasColumnType("int"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("account", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Achievement", b => + { + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CompletedCount") + .HasColumnType("int"); + + b.Property("Counter") + .HasColumnType("bigint"); + + b.Property("CurrentGrade") + .HasColumnType("int"); + + b.Property("Favorite") + .HasColumnType("tinyint(1)"); + + b.Property("Grades") + .IsRequired() + .HasColumnType("json"); + + b.Property("RewardGrade") + .HasColumnType("int"); + + b.HasKey("OwnerId", "Id"); + + b.ToTable("achievement", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.BannerSlot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ActivateTime") + .HasColumnType("datetime(6)"); + + b.Property("BannerId") + .HasColumnType("bigint"); + + b.Property("Template") + .HasColumnType("json"); + + b.HasKey("Id"); + + b.HasIndex("BannerId"); + + b.ToTable("ugc-banner-slot", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.BlackMarketListing", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Deposit") + .HasColumnType("bigint"); + + b.Property("ExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("ItemUid") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("black-market-listing", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Buddy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("BuddyId") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("BuddyId"); + + b.HasIndex("OwnerId"); + + b.ToTable("buddy", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Channel") + .HasColumnType("smallint"); + + b.Property("Cooldown") + .IsRequired() + .HasColumnType("json"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("json"); + + b.Property("DeleteTime") + .HasColumnType("datetime(6)"); + + b.Property("Experience") + .IsRequired() + .HasColumnType("json"); + + b.Property("Gender") + .HasColumnType("tinyint unsigned"); + + b.Property("Job") + .HasColumnType("int"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("Level") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((short)1); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("Mastery") + .IsRequired() + .HasColumnType("json"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Profile") + .IsRequired() + .HasColumnType("json"); + + b.Property("ReturnMapId") + .HasColumnType("int"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("character", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.CharacterConfig", b => + { + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("DeathCount") + .HasColumnType("int"); + + b.Property("DeathTick") + .HasColumnType("bigint"); + + b.Property("ExplorationProgress") + .HasColumnType("int"); + + b.Property("FavoriteDesigners") + .HasColumnType("json"); + + b.Property("FavoriteStickers") + .HasColumnType("json"); + + b.Property("GatheringCounts") + .HasColumnType("json"); + + b.Property("GuideRecords") + .HasColumnType("json"); + + b.Property("HotBars") + .HasColumnType("json"); + + b.Property("KeyBinds") + .HasColumnType("json"); + + b.Property("Lapenshards") + .HasColumnType("json"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("SkillCooldowns") + .HasColumnType("json"); + + b.Property("SkillMacros") + .HasColumnType("json"); + + b.Property("SkillPoint") + .HasColumnType("json"); + + b.Property("StatAllocation") + .HasColumnType("json"); + + b.Property("StatPoints") + .HasColumnType("json"); + + b.Property("Wardrobes") + .HasColumnType("json"); + + b.HasKey("CharacterId"); + + b.ToTable("character-config", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.CharacterUnlock", b => + { + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CollectedItems") + .IsRequired() + .HasColumnType("json"); + + b.Property("Emotes") + .IsRequired() + .HasColumnType("json"); + + b.Property("Expand") + .IsRequired() + .HasColumnType("json"); + + b.Property("FishAlbum") + .IsRequired() + .HasColumnType("json"); + + b.Property("HairSlotExpand") + .HasColumnType("smallint"); + + b.Property("InteractedObjects") + .IsRequired() + .HasColumnType("json"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("Maps") + .IsRequired() + .HasColumnType("json"); + + b.Property("MasteryRewardsClaimed") + .IsRequired() + .HasColumnType("json"); + + b.Property("Pets") + .IsRequired() + .HasColumnType("json"); + + b.Property("StickerSets") + .IsRequired() + .HasColumnType("json"); + + b.Property("Taxis") + .IsRequired() + .HasColumnType("json"); + + b.Property("Titles") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("CharacterId"); + + b.ToTable("character-unlock", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Club", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("BuffId") + .HasColumnType("int"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("LeaderId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("NameChangeCooldown") + .HasColumnType("datetime(6)"); + + b.Property("State") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("LeaderId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("club", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.ClubMember", b => + { + b.Property("ClubId") + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.HasKey("ClubId", "CharacterId"); + + b.HasIndex("CharacterId"); + + b.ToTable("club-member", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.GameEventUserValue", b => + { + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("EventId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("ExpirationTime") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("CharacterId", "EventId", "Type"); + + b.ToTable("game-event-user-value", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Guild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Buffs") + .IsRequired() + .HasColumnType("json"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Emblem") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Experience") + .HasColumnType("int"); + + b.Property("Focus") + .HasColumnType("int"); + + b.Property("Funds") + .HasColumnType("int"); + + b.Property("HouseRank") + .HasColumnType("int"); + + b.Property("HouseTheme") + .HasColumnType("int"); + + b.Property("LeaderId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Notice") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Npcs") + .IsRequired() + .HasColumnType("json"); + + b.Property("Posters") + .IsRequired() + .HasColumnType("json"); + + b.Property("Ranks") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("Id"); + + b.HasIndex("LeaderId") + .IsUnique(); + + b.ToTable("guild", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.GuildApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApplicantId") + .HasColumnType("bigint"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("GuildId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("GuildId"); + + b.ToTable("guild-application", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.GuildMember", b => + { + b.Property("GuildId") + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CheckinTime") + .HasColumnType("datetime(6)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("DailyDonationCount") + .HasColumnType("int"); + + b.Property("DonationTime") + .HasColumnType("datetime(6)"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Rank") + .HasColumnType("tinyint unsigned"); + + b.Property("TotalContribution") + .HasColumnType("int"); + + b.Property("WeeklyContribution") + .HasColumnType("int"); + + b.HasKey("GuildId", "CharacterId"); + + b.HasIndex("CharacterId") + .IsUnique(); + + b.ToTable("guild-member", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Home", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("ArchitectScore") + .HasColumnType("int"); + + b.Property("Area") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint unsigned") + .HasDefaultValue((byte)4); + + b.Property("Background") + .HasColumnType("tinyint unsigned"); + + b.Property("Blueprints") + .IsRequired() + .HasColumnType("json"); + + b.Property("Camera") + .HasColumnType("tinyint unsigned"); + + b.Property("CurrentArchitectScore") + .HasColumnType("int"); + + b.Property("Height") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint unsigned") + .HasDefaultValue((byte)3); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("Layouts") + .IsRequired() + .HasColumnType("json"); + + b.Property("Lighting") + .HasColumnType("tinyint unsigned"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Passcode") + .HasColumnType("longtext"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("AccountId"); + + b.ToTable("home", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.HomeLayout", b => + { + b.Property("Uid") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Area") + .HasColumnType("tinyint unsigned"); + + b.Property("Background") + .HasColumnType("tinyint unsigned"); + + b.Property("Camera") + .HasColumnType("tinyint unsigned"); + + b.Property("Height") + .HasColumnType("tinyint unsigned"); + + b.Property("Id") + .HasColumnType("int"); + + b.Property("Lighting") + .HasColumnType("tinyint unsigned"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Uid"); + + b.ToTable("home-layout", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.HomeLayoutCube", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CubeSettings") + .HasColumnType("json"); + + b.Property("HomeLayoutId") + .HasColumnType("bigint"); + + b.Property("HousingCategory") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Rotation") + .HasColumnType("float"); + + b.Property("Template") + .HasColumnType("json"); + + b.Property("X") + .HasColumnType("tinyint"); + + b.Property("Y") + .HasColumnType("tinyint"); + + b.Property("Z") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("HomeLayoutId"); + + b.ToTable("home-layout-cube", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Amount") + .HasColumnType("int"); + + b.Property("Appearance") + .IsRequired() + .HasColumnType("json"); + + b.Property("Binding") + .HasColumnType("json"); + + b.Property("CoupleInfo") + .HasColumnType("json"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Enchant") + .HasColumnType("json"); + + b.Property("ExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("GachaDismantleId") + .HasColumnType("int"); + + b.Property("GlamorForges") + .HasColumnType("smallint"); + + b.Property("Group") + .HasColumnType("tinyint unsigned"); + + b.Property("IsLocked") + .HasColumnType("tinyint(1)"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("LimitBreak") + .HasColumnType("json"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Rarity") + .HasColumnType("int"); + + b.Property("RemainUses") + .HasColumnType("int"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.Property("Socket") + .HasColumnType("json"); + + b.Property("Stats") + .HasColumnType("json"); + + b.Property("SubType") + .HasColumnType("json"); + + b.Property("TimeChangedOption") + .HasColumnType("int"); + + b.Property("Transfer") + .HasColumnType("json"); + + b.Property("UnlockTime") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("item", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.ItemStorage", b => + { + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Expand") + .HasColumnType("smallint"); + + b.Property("Meso") + .HasColumnType("bigint"); + + b.HasKey("AccountId"); + + b.ToTable("item-storage", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Mail", b => + { + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ContentArgs") + .IsRequired() + .HasColumnType("json"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("json"); + + b.Property("ExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("ReadTime") + .HasColumnType("datetime(6)"); + + b.Property("SendTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("SenderId") + .HasColumnType("bigint"); + + b.Property("SenderName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TitleArgs") + .IsRequired() + .HasColumnType("json"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WeddingInvite") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ReceiverId", "Id"); + + b.ToTable("mail", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Marriage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("ExpHistory") + .IsRequired() + .HasColumnType("json"); + + b.Property("Partner1Id") + .HasColumnType("bigint"); + + b.Property("Partner1Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Partner2Id") + .HasColumnType("bigint"); + + b.Property("Partner2Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Profile") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("Partner1Id"); + + b.HasIndex("Partner2Id"); + + b.ToTable("marriage", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Medal", b => + { + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("int"); + + b.Property("ExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.HasKey("OwnerId", "Id"); + + b.ToTable("medal", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.MesoListing", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("ExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CharacterId"); + + b.ToTable("meso-market", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.PetConfig", b => + { + b.Property("ItemUid") + .HasColumnType("bigint"); + + b.Property("LootConfig") + .IsRequired() + .HasColumnType("json"); + + b.Property("PotionConfigs") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("ItemUid"); + + b.ToTable("pet-config", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Quest", b => + { + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("int"); + + b.Property("CompletionCount") + .HasColumnType("int"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("json"); + + b.Property("EndTime") + .HasColumnType("bigint"); + + b.Property("StartTime") + .HasColumnType("bigint"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("Track") + .HasColumnType("tinyint(1)"); + + b.HasKey("OwnerId", "Id"); + + b.ToTable("quest", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.ServerInfo", b => + { + b.Property("Key") + .HasColumnType("varchar(255)"); + + b.Property("LastModified") + .HasColumnType("datetime(6)"); + + b.HasKey("Key"); + + b.ToTable("server-info", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Shop.CharacterShopData", b => + { + b.Property("ShopId") + .HasColumnType("int"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Interval") + .HasColumnType("tinyint unsigned"); + + b.Property("RestockCount") + .HasColumnType("int"); + + b.Property("RestockTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ShopId", "OwnerId"); + + b.ToTable("character-shop-data", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Shop.CharacterShopItemData", b => + { + b.Property("ShopItemId") + .HasColumnType("int"); + + b.Property("ShopId") + .HasColumnType("int"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Item") + .IsRequired() + .HasColumnType("json"); + + b.Property("StockPurchased") + .HasColumnType("int"); + + b.HasKey("ShopItemId", "ShopId", "OwnerId"); + + b.ToTable("character-shop-item-data", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.SkillTab", b => + { + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Skills") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("CharacterId", "Id"); + + b.HasIndex("CharacterId"); + + b.ToTable("skill-tab", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.SoldMeretMarketItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("MarketId") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("SoldTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("meret-market-sold", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.SoldMesoListing", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("ListedTime") + .HasColumnType("datetime(6)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("SoldTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("meso-market-sold", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.SoldUgcMarketItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("Profit") + .HasColumnType("bigint"); + + b.Property("SoldTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("ugc-market-item-sold", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.SystemBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BeginTime") + .HasColumnType("datetime(6)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("Function") + .HasColumnType("int"); + + b.Property("FunctionParameter") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Language") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("system-banner", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.UgcMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApartmentNumber") + .HasColumnType("int"); + + b.Property("ExpiryTime") + .HasColumnType("datetime(6)"); + + b.Property("Indoor") + .HasColumnType("tinyint(1)"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("MapId"); + + b.HasIndex("OwnerId"); + + b.ToTable("ugcmap", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.UgcMapCube", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CubeSettings") + .HasColumnType("json"); + + b.Property("HousingCategory") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Rotation") + .HasColumnType("float"); + + b.Property("Template") + .HasColumnType("json"); + + b.Property("UgcMapId") + .HasColumnType("bigint"); + + b.Property("X") + .HasColumnType("tinyint"); + + b.Property("Y") + .HasColumnType("tinyint"); + + b.Property("Z") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("UgcMapId"); + + b.ToTable("ugcmap-cube", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.UgcMarketItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("Blueprint") + .IsRequired() + .HasColumnType("json"); + + b.Property("CharacterId") + .HasColumnType("bigint"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("ListingEndTime") + .HasColumnType("datetime(6)"); + + b.Property("Look") + .IsRequired() + .HasColumnType("json"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("PromotionEndTime") + .HasColumnType("datetime(6)"); + + b.Property("SalesCount") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("TabId") + .HasColumnType("int"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("json"); + + b.HasKey("Id"); + + b.ToTable("ugc-market-item", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.UgcResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("LastModified") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("Path") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("ugcresource", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.WeddingHall", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CeremonyTime") + .HasColumnType("datetime(6)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("GuestList") + .IsRequired() + .HasColumnType("json"); + + b.Property("MarriageId") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("PackageHallId") + .HasColumnType("int"); + + b.Property("PackageId") + .HasColumnType("int"); + + b.Property("Public") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("MarriageId") + .IsUnique(); + + b.ToTable("wedding-hall", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.Buddy", b => + { + b.HasOne("Maple2.Database.Model.Character", "BuddyCharacter") + .WithMany() + .HasForeignKey("BuddyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BuddyCharacter"); + }); + + modelBuilder.Entity("Maple2.Database.Model.Character", b => + { + b.HasOne("Maple2.Database.Model.Account", null) + .WithMany("Characters") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.CharacterConfig", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.CharacterConfig", "CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Maple2.Database.Model.SkillBook", "SkillBook", b1 => + { + b1.Property("CharacterConfigCharacterId") + .HasColumnType("bigint"); + + b1.Property("ActiveSkillTabId") + .HasColumnType("bigint"); + + b1.Property("MaxSkillTabs") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + + b1.HasKey("CharacterConfigCharacterId"); + + b1.HasIndex("ActiveSkillTabId") + .IsUnique(); + + b1.ToTable("character-config"); + + b1.HasOne("Maple2.Database.Model.SkillTab", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.CharacterConfig.SkillBook#Maple2.Database.Model.SkillBook", "ActiveSkillTabId") + .HasPrincipalKey("Maple2.Database.Model.SkillTab", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("CharacterConfigCharacterId"); + }); + + b.Navigation("SkillBook"); + }); + + modelBuilder.Entity("Maple2.Database.Model.CharacterUnlock", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.CharacterUnlock", "CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.Club", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("LeaderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.ClubMember", b => + { + b.HasOne("Maple2.Database.Model.Character", "Character") + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Maple2.Database.Model.Club", null) + .WithMany("Members") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Maple2.Database.Model.GameEventUserValue", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.Guild", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.Guild", "LeaderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.GuildApplication", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Maple2.Database.Model.Guild", null) + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.GuildMember", b => + { + b.HasOne("Maple2.Database.Model.Character", "Character") + .WithOne() + .HasForeignKey("Maple2.Database.Model.GuildMember", "CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Maple2.Database.Model.Guild", null) + .WithMany("Members") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Maple2.Database.Model.Home", b => + { + b.HasOne("Maple2.Database.Model.Account", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.Home", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.HomeLayoutCube", b => + { + b.HasOne("Maple2.Database.Model.HomeLayout", null) + .WithMany("Cubes") + .HasForeignKey("HomeLayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.ItemStorage", b => + { + b.HasOne("Maple2.Database.Model.Account", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.ItemStorage", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.Mail", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.Marriage", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("Partner1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("Partner2Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.MesoListing", b => + { + b.HasOne("Maple2.Database.Model.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.PetConfig", b => + { + b.HasOne("Maple2.Database.Model.Item", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.PetConfig", "ItemUid") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.SkillTab", b => + { + b.HasOne("Maple2.Database.Model.Character", null) + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.SoldUgcMarketItem", b => + { + b.HasOne("Maple2.Database.Model.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.UgcMapCube", b => + { + b.HasOne("Maple2.Database.Model.UgcMap", null) + .WithMany("Cubes") + .HasForeignKey("UgcMapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.WeddingHall", b => + { + b.HasOne("Maple2.Database.Model.Marriage", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.WeddingHall", "MarriageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Maple2.Database.Model.Account", b => + { + b.Navigation("Characters"); + }); + + modelBuilder.Entity("Maple2.Database.Model.Club", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Maple2.Database.Model.Guild", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Maple2.Database.Model.HomeLayout", b => + { + b.Navigation("Cubes"); + }); + + modelBuilder.Entity("Maple2.Database.Model.UgcMap", b => + { + b.Navigation("Cubes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Maple2.Server.World/Migrations/20241012062127_WeddingHall.cs b/Maple2.Server.World/Migrations/20241012062127_WeddingHall.cs new file mode 100644 index 000000000..7e20cbbbc --- /dev/null +++ b/Maple2.Server.World/Migrations/20241012062127_WeddingHall.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Maple2.Server.World.Migrations +{ + /// + public partial class WeddingHall : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "WeddingInvite", + table: "mail", + type: "longtext", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "wedding-hall", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + MarriageId = table.Column(type: "bigint", nullable: false), + CeremonyTime = table.Column(type: "datetime(6)", nullable: false), + PackageId = table.Column(type: "int", nullable: false), + PackageHallId = table.Column(type: "int", nullable: false), + OwnerId = table.Column(type: "bigint", nullable: false), + Public = table.Column(type: "tinyint(1)", nullable: false), + GuestList = table.Column(type: "json", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreationTime = table.Column(type: "datetime(6)", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn) + }, + constraints: table => + { + table.PrimaryKey("PK_wedding-hall", x => x.Id); + table.ForeignKey( + name: "FK_wedding-hall_marriage_MarriageId", + column: x => x.MarriageId, + principalTable: "marriage", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_wedding-hall_MarriageId", + table: "wedding-hall", + column: "MarriageId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "wedding-hall"); + + migrationBuilder.DropColumn( + name: "WeddingInvite", + table: "mail"); + } + } +} diff --git a/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs b/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs index 5325f55e6..5174239c6 100644 --- a/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs +++ b/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs @@ -935,6 +935,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Type") .HasColumnType("tinyint unsigned"); + b.Property("WeddingInvite") + .IsRequired() + .HasColumnType("longtext"); + b.HasKey("ReceiverId", "Id"); b.ToTable("mail", (string)null); @@ -1504,6 +1508,46 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ugcresource", (string)null); }); + modelBuilder.Entity("Maple2.Database.Model.WeddingHall", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CeremonyTime") + .HasColumnType("datetime(6)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("GuestList") + .IsRequired() + .HasColumnType("json"); + + b.Property("MarriageId") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .HasColumnType("bigint"); + + b.Property("PackageHallId") + .HasColumnType("int"); + + b.Property("PackageId") + .HasColumnType("int"); + + b.Property("Public") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("MarriageId") + .IsUnique(); + + b.ToTable("wedding-hall", (string)null); + }); + modelBuilder.Entity("Maple2.Database.Model.Buddy", b => { b.HasOne("Maple2.Database.Model.Character", "BuddyCharacter") @@ -1759,6 +1803,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Maple2.Database.Model.WeddingHall", b => + { + b.HasOne("Maple2.Database.Model.Marriage", null) + .WithOne() + .HasForeignKey("Maple2.Database.Model.WeddingHall", "MarriageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Maple2.Database.Model.Account", b => { b.Navigation("Characters"); diff --git a/Maple2.Server.World/Service/WorldService.Marriage.cs b/Maple2.Server.World/Service/WorldService.Marriage.cs new file mode 100644 index 000000000..34cebcf83 --- /dev/null +++ b/Maple2.Server.World/Service/WorldService.Marriage.cs @@ -0,0 +1,26 @@ +using Grpc.Core; +using Maple2.Model.Game; +using ChannelClient = Maple2.Server.Channel.Service.Channel.ChannelClient; + +namespace Maple2.Server.World.Service; + +public partial class WorldService { + public override Task Marriage(MarriageRequest request, ServerCallContext context) { + if (!playerLookup.TryGet(request.ReceiverId, out PlayerInfo? info)) { + return Task.FromResult(new MarriageResponse()); + } + + int channel = info.Channel; + if (!channelClients.TryGetClient(channel, out ChannelClient? channelClient)) { + logger.Error("No registry for channel: {Channel}", channel); + return Task.FromResult(new MarriageResponse()); + } + + try { + return Task.FromResult(channelClient.Marriage(request)); + } catch (RpcException ex) when (ex.StatusCode is StatusCode.NotFound) { + logger.Information("{ReceiverId} not found...", request.ReceiverId); + return Task.FromResult(new MarriageResponse()); + } + } +}