From a94c3f02820b4d1af069404a3dccff370f9d0727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Wed, 18 Sep 2024 23:31:54 -0300 Subject: [PATCH 01/12] Feat: UGC Blueprints --- .idea/.idea.Maple2/.idea/indexLayout.xml | 4 +- Maple2.Database/Context/Ms2Context.cs | 4 + Maple2.Database/Model/Item/ItemSubType.cs | 46 +- Maple2.Database/Model/Map/Home.cs | 38 +- Maple2.Database/Model/Map/HomeLayout.cs | 43 + Maple2.Database/Model/Map/HomeLayoutCube.cs | 51 + .../Storage/Game/GameStorage.HomeLayout.cs | 36 + .../Storage/Game/GameStorage.Item.cs | 7 + .../Storage/Game/GameStorage.User.cs | 107 +- Maple2.Model/Enum/Ugc.cs | 4 +- Maple2.Model/Game/Item/ItemBlueprint.cs | 50 +- Maple2.Model/Game/User/Home.cs | 22 +- Maple2.Server.Core/Packets/UgcPacket.cs | 16 + Maple2.Server.Game/Manager/HousingManager.cs | 4 +- .../PacketHandlers/ItemUseHandler.cs | 28 + .../PacketHandlers/RequestCubeHandler.cs | 256 ++- .../PacketHandlers/UgcHandler.cs | 85 +- Maple2.Server.Game/Packets/CubePacket.cs | 24 + .../Session/GameSession.State.cs | 2 + .../Controllers/BlueprintController.cs | 23 + .../Controllers/WebController.cs | 51 +- ...30_AddHomeLayoutsAndCubesTable.Designer.cs | 1744 +++++++++++++++++ ...40918223130_AddHomeLayoutsAndCubesTable.cs | 93 + .../Migrations/Ms2ContextModelSnapshot.cs | 79 + 24 files changed, 2636 insertions(+), 181 deletions(-) create mode 100644 Maple2.Database/Model/Map/HomeLayout.cs create mode 100644 Maple2.Database/Model/Map/HomeLayoutCube.cs create mode 100644 Maple2.Database/Storage/Game/GameStorage.HomeLayout.cs create mode 100644 Maple2.Server.Web/Controllers/BlueprintController.cs create mode 100644 Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.Designer.cs create mode 100644 Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.cs diff --git a/.idea/.idea.Maple2/.idea/indexLayout.xml b/.idea/.idea.Maple2/.idea/indexLayout.xml index 7b08163ce..954be2de3 100644 --- a/.idea/.idea.Maple2/.idea/indexLayout.xml +++ b/.idea/.idea.Maple2/.idea/indexLayout.xml @@ -1,7 +1,9 @@ - + + PacketStructures + diff --git a/Maple2.Database/Context/Ms2Context.cs b/Maple2.Database/Context/Ms2Context.cs index 55732ee31..113867c8f 100644 --- a/Maple2.Database/Context/Ms2Context.cs +++ b/Maple2.Database/Context/Ms2Context.cs @@ -39,6 +39,8 @@ public sealed class Ms2Context(DbContextOptions options) : DbContext(options) { internal DbSet ServerInfo { get; set; } = null!; internal DbSet Medal { get; set; } = null!; internal DbSet BannerSlots { get; set; } = null!; + internal DbSet HomeLayouts { get; set; } = null!; + internal DbSet UgcCubeLayouts { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -69,6 +71,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(Maple2.Database.Model.Quest.Configure); modelBuilder.Entity(Maple2.Database.Model.Medal.Configure); modelBuilder.Entity(Maple2.Database.Model.BannerSlot.Configure); + modelBuilder.Entity(Maple2.Database.Model.HomeLayout.Configure); + modelBuilder.Entity(Maple2.Database.Model.HomeLayoutCube.Configure); modelBuilder.Entity(MesoListing.Configure); modelBuilder.Entity(SoldMesoListing.Configure); diff --git a/Maple2.Database/Model/Item/ItemSubType.cs b/Maple2.Database/Model/Item/ItemSubType.cs index a59639702..59d4b94a1 100644 --- a/Maple2.Database/Model/Item/ItemSubType.cs +++ b/Maple2.Database/Model/Item/ItemSubType.cs @@ -12,8 +12,15 @@ internal abstract record ItemSubType; internal record ItemUgc(UgcItemLook Template, ItemBlueprint Blueprint) : ItemSubType; -internal record UgcItemLook(long Id, string FileName, string Name, long AccountId, long CharacterId, string Author, - long CreationTime, string Url) { +internal record UgcItemLook( + long Id, + string FileName, + string Name, + long AccountId, + long CharacterId, + string Author, + long CreationTime, + string Url) { [return: NotNullIfNotNull(nameof(other))] public static implicit operator UgcItemLook?(Maple2.Model.Game.UgcItemLook? other) { return other == null ? null : new UgcItemLook(other.Id, other.FileName, other.Name, other.AccountId, other.CharacterId, @@ -35,15 +42,34 @@ internal record UgcItemLook(long Id, string FileName, string Name, long AccountI } } -internal record ItemBlueprint { +internal record ItemBlueprint( + long BlueprintUid, + int Length, + int Width, + int Height, + DateTimeOffset CreationTime, + int Unknown, + long AccountId, + long CharacterId, + string CharacterName) { [return: NotNullIfNotNull(nameof(other))] public static implicit operator ItemBlueprint?(Maple2.Model.Game.ItemBlueprint? other) { - return other == null ? null : new ItemBlueprint(); + return other == null ? null : new ItemBlueprint(other.BlueprintUid, other.Length, other.Width, other.Height, other.CreationTime, other.Unknown, other.AccountId, other.CharacterId, other.CharacterName); } [return: NotNullIfNotNull(nameof(other))] public static implicit operator Maple2.Model.Game.ItemBlueprint?(ItemBlueprint? other) { - return other == null ? null : new Maple2.Model.Game.ItemBlueprint(); + return other == null ? null : new Maple2.Model.Game.ItemBlueprint { + BlueprintUid = other.BlueprintUid, + Length = other.Length, + Width = other.Width, + Height = other.Height, + CreationTime = other.CreationTime, + Unknown = other.Unknown, + AccountId = other.AccountId, + CharacterId = other.CharacterId, + CharacterName = other.CharacterName, + }; } } @@ -65,8 +91,14 @@ internal record ItemPet(string Name, long Exp, int EvolvePoints, short Level, sh } } -internal record ItemCustomMusicScore(int Length, int Instrument, string Title, string Author, long AuthorId, - bool IsLocked, string Mml) : ItemSubType { +internal record ItemCustomMusicScore( + int Length, + int Instrument, + string Title, + string Author, + long AuthorId, + bool IsLocked, + string Mml) : ItemSubType { [return: NotNullIfNotNull(nameof(other))] public static implicit operator ItemCustomMusicScore?(Maple2.Model.Game.ItemCustomMusicScore? other) { return other == null ? null : new ItemCustomMusicScore(other.Length, other.Instrument, other.Title, diff --git a/Maple2.Database/Model/Map/Home.cs b/Maple2.Database/Model/Map/Home.cs index db87be754..fa3a8d3fb 100644 --- a/Maple2.Database/Model/Map/Home.cs +++ b/Maple2.Database/Model/Map/Home.cs @@ -24,7 +24,8 @@ internal class Home { public string? Passcode { get; set; } public IDictionary Permissions { get; set; } = new Dictionary(); - public List Layouts { get; set; } = []; + public List Layouts { get; set; } = []; + public List Blueprints { get; set; } = []; public DateTime LastModified { get; set; } @@ -43,7 +44,8 @@ internal class Home { Camera = other.Camera, Passcode = other.Passcode, Permissions = other.Permissions, - Layouts = other.Layouts.ConvertAll(layout => (HomeLayout) layout), + Layouts = other.Layouts.Select(layout => layout.Uid).ToList(), + Blueprints = other.Blueprints.Select(layout => layout.Uid).ToList(), }; } @@ -60,7 +62,6 @@ internal class Home { ArchitectScore = other.ArchitectScore, Passcode = other.Passcode, LastModified = other.LastModified.ToEpochSeconds(), - Layouts = other.Layouts.ConvertAll(layout => (Maple2.Model.Game.HomeLayout) layout), }; home.SetArea(other.Area); @@ -88,38 +89,9 @@ public static void Configure(EntityTypeBuilder builder) { builder.Property(home => home.Permissions).HasJsonConversion(); builder.Property(home => home.Layouts).HasJsonConversion(); + builder.Property(home => home.Blueprints).HasJsonConversion(); builder.Property(map => map.LastModified) .ValueGeneratedOnAddOrUpdate(); } } - -internal class HomeLayout { - public int Id { get; set; } - public string Name { get; set; } - public byte Area { get; set; } - public byte Height { get; set; } - public DateTimeOffset Timestamp { get; set; } - public List Cubes { get; set; } - - [return: NotNullIfNotNull(nameof(other))] - public static implicit operator HomeLayout?(Maple2.Model.Game.HomeLayout? other) { - return other == null ? null : new HomeLayout { - Id = other.Id, - Name = other.Name, - Area = other.Area, - Height = other.Height, - Timestamp = other.Timestamp, - Cubes = other.Cubes.ConvertAll(cube => (UgcMapCube) cube), - }; - } - - [return: NotNullIfNotNull(nameof(other))] - public static implicit operator Maple2.Model.Game.HomeLayout?(HomeLayout? other) { - if (other == null) { - return null; - } - - return new Maple2.Model.Game.HomeLayout(other.Id, other.Name, other.Area, other.Height, other.Timestamp, other.Cubes.ConvertAll(cube => (Maple2.Model.Game.PlotCube) cube)); - } -} diff --git a/Maple2.Database/Model/Map/HomeLayout.cs b/Maple2.Database/Model/Map/HomeLayout.cs new file mode 100644 index 000000000..65649d65a --- /dev/null +++ b/Maple2.Database/Model/Map/HomeLayout.cs @@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; +using Maple2.Model.Game; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Maple2.Database.Model; + +internal class HomeLayout { + public long Uid { get; set; } + public int Id { get; set; } + public string Name { get; set; } + public byte Area { get; set; } + public byte Height { get; set; } + public DateTimeOffset Timestamp { get; set; } + public List Cubes { get; set; } = null!; + + [return: NotNullIfNotNull(nameof(other))] + public static implicit operator HomeLayout?(Maple2.Model.Game.HomeLayout? other) { + return other == null ? null : new HomeLayout { + Uid = other.Uid, + Id = other.Id, + Name = other.Name, + Area = other.Area, + Height = other.Height, + Timestamp = other.Timestamp, + Cubes = other.Cubes.ConvertAll(cube => (HomeLayoutCube) cube), + }; + } + + [return: NotNullIfNotNull(nameof(other))] + public static implicit operator Maple2.Model.Game.HomeLayout?(HomeLayout? other) { + if (other == null) { + return null; + } + + return new Maple2.Model.Game.HomeLayout(other.Uid, other.Id, other.Name, other.Area, other.Height, other.Timestamp, other.Cubes.ConvertAll(cube => (PlotCube) cube)); + } + + public static void Configure(EntityTypeBuilder builder) { + builder.ToTable("home-layout"); + builder.HasKey(layout => layout.Uid); + } +} diff --git a/Maple2.Database/Model/Map/HomeLayoutCube.cs b/Maple2.Database/Model/Map/HomeLayoutCube.cs new file mode 100644 index 000000000..fbc531b1b --- /dev/null +++ b/Maple2.Database/Model/Map/HomeLayoutCube.cs @@ -0,0 +1,51 @@ +using System.Diagnostics.CodeAnalysis; +using Maple2.Database.Extensions; +using Maple2.Model.Common; +using Maple2.Model.Game; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Maple2.Database.Model; + +internal class HomeLayoutCube { + public long Id { get; set; } + public long HomeLayoutId { get; set; } + public sbyte X { get; set; } + public sbyte Y { get; set; } + public sbyte Z { get; set; } + public float Rotation { get; set; } + + public int ItemId { get; set; } + public UgcItemLook? Template { get; set; } + + [return: NotNullIfNotNull(nameof(other))] + public static implicit operator PlotCube?(HomeLayoutCube? other) { + return other == null ? null : new PlotCube(other.ItemId, other.Id, other.Template) { + Position = new Vector3B(other.X, other.Y, other.Z), + Rotation = other.Rotation, + }; + } + + [return: NotNullIfNotNull(nameof(other))] + public static implicit operator HomeLayoutCube?(PlotCube? other) { + return other == null ? null : new HomeLayoutCube { + X = other.Position.X, + Y = other.Position.Y, + Z = other.Position.Z, + Rotation = other.Rotation, + ItemId = other.ItemId, + Template = other.Template, + }; + } + + public static void Configure(EntityTypeBuilder builder) { + builder.ToTable("home-layout-cube"); + builder.HasKey(cube => cube.Id); + + builder.HasOne() + .WithMany(ugcMap => ugcMap.Cubes) + .HasForeignKey(cube => cube.HomeLayoutId); + + builder.Property(cube => cube.Template).HasJsonConversion(); + } +} diff --git a/Maple2.Database/Storage/Game/GameStorage.HomeLayout.cs b/Maple2.Database/Storage/Game/GameStorage.HomeLayout.cs new file mode 100644 index 000000000..ba8814798 --- /dev/null +++ b/Maple2.Database/Storage/Game/GameStorage.HomeLayout.cs @@ -0,0 +1,36 @@ +using Maple2.Database.Extensions; +using Maple2.Database.Model; +using Microsoft.EntityFrameworkCore; +using HomeLayout = Maple2.Model.Game.HomeLayout; + +namespace Maple2.Database.Storage; + +public partial class GameStorage { + public partial class Request { + public HomeLayout? SaveHomeLayout(HomeLayout layout) { + Model.HomeLayout homeLayout = layout; + Context.HomeLayouts.Add(homeLayout); + foreach (HomeLayoutCube cubes in homeLayout.Cubes) { + Context.UgcCubeLayouts.Add(cubes); + } + bool success = Context.TrySaveChanges(); + + return success ? homeLayout : null; + } + + public void RemoveHomeLayout(HomeLayout layout) { + Model.HomeLayout homeLayout = layout; + Context.HomeLayouts.Remove(homeLayout); + Context.TrySaveChanges(); + } + + public HomeLayout? GetHomeLayout(long layoutUid) { + HomeLayout? layout = Context.HomeLayouts + .Where(homeLayout => homeLayout.Uid == layoutUid) + .Include(homeLayout => homeLayout.Cubes) + .FirstOrDefault(); + + return layout; + } + } +} diff --git a/Maple2.Database/Storage/Game/GameStorage.Item.cs b/Maple2.Database/Storage/Game/GameStorage.Item.cs index de2efea74..596f41c57 100644 --- a/Maple2.Database/Storage/Game/GameStorage.Item.cs +++ b/Maple2.Database/Storage/Game/GameStorage.Item.cs @@ -139,6 +139,13 @@ public bool SaveItems(long ownerId, params Item[] items) { return Context.TrySaveChanges(); } + public bool UpdateItem(Item item) { + Model.Item model = item; + Context.Item.Update(model); + + return Context.TrySaveChanges(); + } + public bool SaveStorageInfo(long accountId, long mesos, short expand) { ItemStorage? info = Context.ItemStorage.Find(accountId); if (info == null) { diff --git a/Maple2.Database/Storage/Game/GameStorage.User.cs b/Maple2.Database/Storage/Game/GameStorage.User.cs index a60ac60ca..145721f80 100644 --- a/Maple2.Database/Storage/Game/GameStorage.User.cs +++ b/Maple2.Database/Storage/Game/GameStorage.User.cs @@ -8,6 +8,7 @@ using Maple2.Model.Metadata; using Maple2.Server.Game.Manager.Config; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; using Microsoft.Extensions.Logging; using Account = Maple2.Model.Game.Account; using Character = Maple2.Model.Game.Character; @@ -19,6 +20,7 @@ using Wardrobe = Maple2.Model.Game.Wardrobe; using GameEventUserValue = Maple2.Model.Game.GameEventUserValue; using Home = Maple2.Model.Game.Home; +using HomeLayout = Maple2.Database.Model.HomeLayout; namespace Maple2.Database.Storage; @@ -91,11 +93,28 @@ public long GetCharacterId(string name) { var result = (from character in Context.Character where character.Id == characterId join account in Context.Account on character.AccountId equals account.Id join indoor in Context.UgcMap on - new { OwnerId = character.AccountId, Indoor = true } equals new { indoor.OwnerId, indoor.Indoor } + new { + OwnerId = character.AccountId, + Indoor = true + } equals new { + indoor.OwnerId, + indoor.Indoor + } join outdoor in Context.UgcMap on - new { OwnerId = character.AccountId, Indoor = false } equals new { outdoor.OwnerId, outdoor.Indoor } into plot + new { + OwnerId = character.AccountId, + Indoor = false + } equals new { + outdoor.OwnerId, + outdoor.Indoor + } into plot from outdoor in plot.DefaultIfEmpty() - select new { character, indoor, outdoor, account.PremiumTime }) + select new { + character, + indoor, + outdoor, + account.PremiumTime + }) .FirstOrDefault(); if (result == null) { return null; @@ -122,6 +141,26 @@ from outdoor in plot.DefaultIfEmpty() return null; } + foreach (long layoutUid in model.Layouts) { + HomeLayout? layout = GetHomeLayout(layoutUid); + if (layout is null) { + Logger.LogError("Home layout not found: {LayoutUid}", layoutUid); + continue; + } + + home.Layouts.Add(layout); + } + + foreach (long layoutUid in model.Blueprints) { + HomeLayout? layout = GetHomeLayout(layoutUid); + if (layout is null) { + Logger.LogError("Home layout not found: {LayoutUid}", layoutUid); + continue; + } + + home.Blueprints.Add(layout); + } + home.Indoor = indoor; home.Outdoor = ToPlotInfo(ugcMaps.FirstOrDefault(map => !map.Indoor)); return home; @@ -147,7 +186,11 @@ from outdoor in plot.DefaultIfEmpty() Context.Account.Update(account); Context.Character.Update(character); - Context.SaveChanges(); + try { + Context.SaveChanges(); + } catch (Exception e) { + Console.WriteLine(e); + } Tuple guild = Context.GuildMember .Where(member => member.CharacterId == characterId) @@ -286,24 +329,24 @@ public bool SaveCharacter(Character character) { } public bool SaveCharacterConfig( - long characterId, - IList keyBinds, - IList hotBars, - IEnumerable skillMacros, - IEnumerable wardrobes, - IList favoriteStickers, - IList favoriteDesigners, - IDictionary lapenshards, - IList skillCooldowns, - long deathTick, - int deathCount, - int explorationProgress, - StatAttributes.PointAllocation allocation, - StatAttributes.PointSources statSources, - SkillPoint skillPoint, - IDictionary gatheringCounts, - IDictionary guideRecords, - SkillBook skillBook) { + long characterId, + IList keyBinds, + IList hotBars, + IEnumerable skillMacros, + IEnumerable wardrobes, + IList favoriteStickers, + IList favoriteDesigners, + IDictionary lapenshards, + IList skillCooldowns, + long deathTick, + int deathCount, + int explorationProgress, + StatAttributes.PointAllocation allocation, + StatAttributes.PointSources statSources, + SkillPoint skillPoint, + IDictionary gatheringCounts, + IDictionary guideRecords, + SkillBook skillBook) { Context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll; CharacterConfig? config = Context.CharacterConfig.Find(characterId); @@ -318,8 +361,8 @@ public bool SaveCharacterConfig( config.FavoriteStickers = favoriteStickers; config.FavoriteDesigners = favoriteDesigners; config.Lapenshards = lapenshards; - config.SkillCooldowns = skillCooldowns.Where(cooldown => cooldown.EndTick > Environment.TickCount64). - Select(cooldown => cooldown) + config.SkillCooldowns = skillCooldowns.Where(cooldown => cooldown.EndTick > Environment.TickCount64) + .Select(cooldown => cooldown) .ToList(); config.DeathTick = deathTick; config.DeathCount = deathCount; @@ -358,12 +401,16 @@ public Account CreateAccount(Account account) { Model.Account model = account; model.Id = 0; #if DEBUG - model.Currency = new AccountCurrency { Meret = 99999 }; + model.Currency = new AccountCurrency { + Meret = 99999 + }; #endif Context.Account.Add(model); Context.SaveChanges(); // Exception if failed. - Context.Home.Add(new Home { AccountId = model.Id }); + Context.Home.Add(new Home { + AccountId = model.Id + }); Context.UgcMap.Add(new UgcMap { OwnerId = model.Id, MapId = Constant.DefaultHomeMapId, @@ -379,7 +426,9 @@ public Account CreateAccount(Account account) { Model.Character model = character; model.Id = 0; #if DEBUG - model.Currency = new CharacterCurrency { Meso = 999999999 }; + model.Currency = new CharacterCurrency { + Meso = 999999999 + }; #endif Context.Character.Add(model); return Context.TrySaveChanges() ? model : null; @@ -390,7 +439,9 @@ public bool InitNewCharacter(long characterId, Unlock unlock) { model.CharacterId = characterId; Context.CharacterUnlock.Add(model); - SkillTab? defaultTab = CreateSkillTab(characterId, new SkillTab("Build 1") { Id = characterId }); + SkillTab? defaultTab = CreateSkillTab(characterId, new SkillTab("Build 1") { + Id = characterId + }); if (defaultTab == null) { return false; } diff --git a/Maple2.Model/Enum/Ugc.cs b/Maple2.Model/Enum/Ugc.cs index 2cd0c9a46..d81ad6a0d 100644 --- a/Maple2.Model/Enum/Ugc.cs +++ b/Maple2.Model/Enum/Ugc.cs @@ -10,9 +10,9 @@ public enum UgcType : byte { GuildEmblem = 6, Mount = 7, GuildBanner = 8, - Unknown9 = 9, + LayoutBlueprint = 9, Unknown10 = 10, ItemIcon = 201, Unknown11 = 202, - Unknown12 = 209, + BlueprintIcon = 209, } diff --git a/Maple2.Model/Game/Item/ItemBlueprint.cs b/Maple2.Model/Game/Item/ItemBlueprint.cs index 20c87a464..135f4dee0 100644 --- a/Maple2.Model/Game/Item/ItemBlueprint.cs +++ b/Maple2.Model/Game/Item/ItemBlueprint.cs @@ -4,31 +4,45 @@ namespace Maple2.Model.Game; public sealed class ItemBlueprint : IByteSerializable, IByteDeserializable { + public long BlueprintUid; + public int Length; + public int Width; + public int Height; + public DateTimeOffset CreationTime; + public int Unknown; + public long AccountId; + public long CharacterId; + public string CharacterName = ""; + + public ItemBlueprint() { + Unknown = 1; + } + public ItemBlueprint Clone() { return (ItemBlueprint) MemberwiseClone(); } public void WriteTo(IByteWriter writer) { - writer.WriteLong(); - writer.WriteInt(); - writer.WriteInt(); - writer.WriteInt(); - writer.WriteLong(); - writer.WriteInt(); - writer.WriteLong(); - writer.WriteLong(); - writer.WriteUnicodeString(); + writer.WriteLong(BlueprintUid); + writer.WriteInt(Length); + writer.WriteInt(Width); + writer.WriteInt(Height); + writer.WriteLong(CreationTime.ToUnixTimeSeconds()); + writer.WriteInt(Unknown); + writer.WriteLong(AccountId); + writer.WriteLong(CharacterId); + writer.WriteUnicodeString(CharacterName); } public void ReadFrom(IByteReader reader) { - reader.ReadLong(); - reader.ReadInt(); - reader.ReadInt(); - reader.ReadInt(); - reader.ReadLong(); - reader.ReadInt(); - reader.ReadLong(); - reader.ReadLong(); - reader.ReadUnicodeString(); + BlueprintUid = reader.ReadLong(); + Length = reader.ReadInt(); + Width = reader.ReadInt(); + Height = reader.ReadInt(); + CreationTime = DateTimeOffset.FromUnixTimeSeconds(reader.ReadLong()); + Unknown = reader.ReadInt(); + AccountId = reader.ReadLong(); + CharacterId = reader.ReadLong(); + CharacterName = reader.ReadUnicodeString(); } } diff --git a/Maple2.Model/Game/User/Home.cs b/Maple2.Model/Game/User/Home.cs index 0b6cfdd7c..d6f416a0d 100644 --- a/Maple2.Model/Game/User/Home.cs +++ b/Maple2.Model/Game/User/Home.cs @@ -35,6 +35,7 @@ public class Home : IByteSerializable { public long DecorationRewardTimestamp { get; set; } public List InteriorRewardsClaimed { get; set; } public List Layouts { get; set; } + public List Blueprints { get; set; } private string message; public string Message { @@ -63,6 +64,7 @@ public Home() { Permissions = new Dictionary(); InteriorRewardsClaimed = []; Layouts = []; + Blueprints = []; } public bool SetArea(int area) { @@ -173,18 +175,31 @@ public void WriteTo(IByteWriter writer) { foreach (HomeLayout layout in Layouts) { writer.WriteClass(layout); } - writer.WriteByte(); // saved blueprints + writer.WriteByte((byte) Blueprints.Count); + foreach (HomeLayout blueprint in Blueprints) { + writer.WriteClass(blueprint); + } } } public class HomeLayout : IByteSerializable { - public long HomeId { get; private set; } + public long Uid { get; private set; } public int Id { get; private set; } public string Name { get; private set; } public byte Area { get; private set; } public byte Height { get; private set; } public DateTimeOffset Timestamp { get; private set; } - public List Cubes { get; private set; } + public List Cubes { get; set; } + + public HomeLayout(long uid, int layoutId, string layoutName, byte area, byte height, DateTimeOffset timestamp, List plotCubes) { + Uid = uid; + Id = layoutId; + Name = layoutName; + Area = area; + Height = height; + Timestamp = timestamp; + Cubes = plotCubes; + } public HomeLayout(int layoutId, string layoutName, byte area, byte height, DateTimeOffset timestamp, List cubes) { Id = layoutId; @@ -195,7 +210,6 @@ public HomeLayout(int layoutId, string layoutName, byte area, byte height, DateT Cubes = cubes; } - public void WriteTo(IByteWriter pWriter) { pWriter.WriteInt(Id); pWriter.WriteUnicodeString(Name); diff --git a/Maple2.Server.Core/Packets/UgcPacket.cs b/Maple2.Server.Core/Packets/UgcPacket.cs index 35e971f3a..06d4d6ce4 100644 --- a/Maple2.Server.Core/Packets/UgcPacket.cs +++ b/Maple2.Server.Core/Packets/UgcPacket.cs @@ -4,6 +4,8 @@ using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Tools.Extensions; +using Microsoft.EntityFrameworkCore.Query; +// ReSharper disable RedundantTypeArgumentsOfMethod namespace Maple2.Server.Core.Packets; @@ -17,6 +19,7 @@ private enum Command : byte { UpdateItem = 13, UpdateFurnishing = 14, UpdateMount = 15, + UpdateLayoutBlueprint = 16, SetEndpoint = 17, LoadBanner = 18, ReserveBanners = 20, @@ -104,6 +107,19 @@ public static ByteWriter UpdateItem(int objectId, Item item, long createPrice, U return pWriter; } + public static ByteWriter UpdateLayoutBlueprint(int objectId, Item item) { + var pWriter = Packet.Of(SendOp.Ugc); + pWriter.Write(Command.UpdateLayoutBlueprint); + pWriter.WriteInt(objectId); + pWriter.WriteLong(item.Blueprint!.BlueprintUid); + pWriter.WriteLong(item.Uid); + pWriter.WriteInt(item.Id); + pWriter.WriteUnicodeString(item.Template!.Name); + pWriter.WriteClass(item.Template); + + return pWriter; + } + public static ByteWriter LoadBanners(List banners) { var pWriter = Packet.Of(SendOp.Ugc); pWriter.Write(Command.LoadBanner); diff --git a/Maple2.Server.Game/Manager/HousingManager.cs b/Maple2.Server.Game/Manager/HousingManager.cs index 2086f6ff7..861aa7590 100644 --- a/Maple2.Server.Game/Manager/HousingManager.cs +++ b/Maple2.Server.Game/Manager/HousingManager.cs @@ -1,7 +1,5 @@ -using System.Numerics; -using Maple2.Database.Extensions; +using Maple2.Database.Extensions; using Maple2.Database.Storage; -using Maple2.Model.Common; using Maple2.Model.Enum; using Maple2.Model.Error; using Maple2.Model.Game; diff --git a/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs b/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs index 1fe11d04e..43630540a 100644 --- a/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs @@ -39,6 +39,9 @@ public override void Handle(GameSession session, IByteReader packet) { } switch (item.Metadata.Function?.Type) { + case ItemFunction.BlueprintImport: + HandleBlueprintImport(session, item); + break; case ItemFunction.StoryBook: HandleStoryBook(session, item); break; @@ -102,6 +105,31 @@ public override void Handle(GameSession session, IByteReader packet) { return; } } + private void HandleBlueprintImport(GameSession session, Item item) { + if (item.Blueprint is null) { + Logger.Error("Item {ItemUid} is missing blueprint", item.Uid); + return; + } + + Plot? plot = session.Housing.GetFieldPlot(); + if (plot == null) { + return; + } + + if (plot.Cubes.Count != 0) { + session.Send(NoticePacket.Message(StringCode.s_err_ugcmap_package_clear_indoor_first, NoticePacket.Flags.Message | NoticePacket.Flags.Alert)); + return; + } + + using GameStorage.Request db = session.GameStorage.Context(); + HomeLayout? layout = db.GetHomeLayout(item.Blueprint.BlueprintUid); + if (layout == null) { + return; + } + + session.StagedItemBlueprint = item.Blueprint; + RequestCubeHandler.RequestLayout(session, layout, TableMetadata); + } private static void HandleStoryBook(GameSession session, Item item) { if (!int.TryParse(item.Metadata.Function?.Parameters, out int storyBookId)) { diff --git a/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs b/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs index 76fd0fb23..9d1ad04ab 100644 --- a/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs @@ -15,6 +15,7 @@ using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; using Maple2.Tools.Extensions; +using Serilog; namespace Maple2.Server.Game.PacketHandlers; @@ -148,6 +149,9 @@ public override void Handle(GameSession session, IByteReader packet) { case Command.SetCamera: HandleSetCamera(session, packet); break; + case Command.CreateBlueprint: + HandleCreateBlueprint(session); + break; case Command.SaveBlueprint: HandleSaveBlueprint(session, packet); break; @@ -390,36 +394,7 @@ private void HandleRequestLayout(GameSession session, IByteReader packet) { return; } - Dictionary groupedCubes = layout.Cubes.GroupBy(plotCube => plotCube.ItemId).ToDictionary(grouping => grouping.Key, grouping => grouping.Count()); // Dictionary - int cubeCount = 0; - Dictionary cubeCosts = new(); - cubeCosts.Add(FurnishingCurrencyType.Meso, 0); - cubeCosts.Add(FurnishingCurrencyType.Meret, 0); - foreach ((int id, int amount) in groupedCubes) { - TableMetadata.FurnishingShopTable.Entries.TryGetValue(id, out FurnishingShopTable.Entry? shopEntry); - if (shopEntry is null) { - Logger.Error("Failed to get shop entry for cube {cubeId}.", id); - session.Send(CubePacket.Error(UgcMapError.s_err_cannot_buy_limited_item_more)); - return; - } - - Item? item = session.Item.Furnishing.GetItem(id); - if (item is null) { - cubeCosts[shopEntry.FurnishingTokenType] += shopEntry.Price * amount; - cubeCount += amount; - continue; - } - - if (item.Amount >= amount) { - continue; - } - - int missingCubes = amount - item.Amount; - cubeCosts[shopEntry.FurnishingTokenType] += shopEntry.Price * missingCubes; - cubeCount += missingCubes; - } - - session.Send(CubePacket.BuyCubes(cubeCosts, cubeCount)); + RequestLayout(session, layout, TableMetadata); } private void HandleIncreaseArea(GameSession session) { @@ -561,14 +536,20 @@ private void HandleSaveLayout(GameSession session, IByteReader packet) { return; } + using GameStorage.Request db = session.GameStorage.Context(); + HomeLayout? layout = home.Layouts.FirstOrDefault(homeLayout => homeLayout.Id == slot); if (layout is not null) { home.Layouts.Remove(layout); + db.RemoveHomeLayout(layout); } byte area = home.IsPlanner ? home.PlannerArea : home.Area; byte height = home.IsPlanner ? home.PlannerHeight : home.Height; - layout = new HomeLayout(slot, name, area, height, DateTimeOffset.Now, plot.Cubes.Values.ToList()); + layout = db.SaveHomeLayout(new HomeLayout(slot, name, area, height, DateTimeOffset.Now, plot.Cubes.Values.ToList())); + if (layout is null) { + return; + } home.Layouts.Add(layout); session.Housing.SaveHome(); @@ -590,45 +571,28 @@ private void HandleLoadLayout(GameSession session, IByteReader packet) { int slot = packet.ReadInt(); Home home = session.Player.Value.Home; - HomeLayout? layout = home.Layouts.FirstOrDefault(homeLayout => homeLayout.Id == slot); - if (layout is null) { - return; - } - - if (plot.IsPlanner) { - home.SetPlannerArea(layout.Area); - home.SetPlannerHeight(layout.Height); - } else { - home.SetArea(layout.Area); - home.SetHeight(layout.Height); - } - session.Field.Broadcast(CubePacket.UpdateHomeAreaAndHeight(home.Area, home.Height)); - foreach (PlotCube cube in layout.Cubes) { - if (!TryPlaceCube(session, cube, plot, cube.Position, cube.Rotation, out PlotCube? plotCube)) { + HomeLayout? layout; + // blueprint load + if (slot is 0) { + if (session.StagedItemBlueprint is null) { return; } - ByteWriter sendPacket; - if (cube.Position.Z == 0) { - sendPacket = CubePacket.ReplaceCube(session.Player.ObjectId, plotCube); - } else { - sendPacket = CubePacket.PlaceCube(session.Player.ObjectId, plot, plotCube); - } - - session.Field.Broadcast(sendPacket); + using GameStorage.Request db = session.GameStorage.Context(); + layout = db.GetHomeLayout(session.StagedItemBlueprint.BlueprintUid); + } else { + layout = home.Layouts.FirstOrDefault(homeLayout => homeLayout.Id == slot); } - - Vector3 position = home.CalculateSafePosition(plot.Cubes.Values.ToList()); - foreach (FieldPlayer fieldPlayer in session.Field.Players.Values) { - fieldPlayer.MoveToPosition(position, default); + if (layout is null) { + return; } - session.Item.Furnishing.SendStorageCount(); - session.Housing.SaveHome(); - session.Field.Broadcast(NoticePacket.Message(StringCode.s_ugcmap_package_automatic_creation_completed, NoticePacket.Flags.Message | NoticePacket.Flags.Alert)); + session.StagedItemBlueprint = null; + ApplyLayout(session, plot, home, layout); } + private void HandleKickOut(GameSession session) { } private void HandleSetBackground(GameSession session, IByteReader packet) { @@ -652,13 +616,112 @@ private void HandleSetCamera(GameSession session, IByteReader packet) { } } + private void HandleCreateBlueprint(GameSession session) { + Plot? plot = session.Housing.GetFieldPlot(); + if (plot is null) { + return; + } + + int negAmount = -200; + if (session.Currency.CanAddMeret(negAmount) != negAmount) { + session.Send(CubePacket.Error(UgcMapError.s_err_ugcmap_not_enough_meso_balance)); + return; + } + + session.Currency.Meret -= 200; + + Item? item = session.Field.ItemDrop.CreateItem(35200000); + if (item is null) { + return; + } + + Home home = session.Player.Value.Home; + byte area = home.PlannerArea; + byte height = home.PlannerHeight; + using GameStorage.Request db = session.GameStorage.Context(); + + HomeLayout? layout = db.SaveHomeLayout(new HomeLayout(0, "Blueprint", area, height, DateTimeOffset.Now, plot.Cubes.Values.ToList())); + if (layout is null) { + return; + } + + item.Blueprint = new ItemBlueprint { + BlueprintUid = layout.Uid, + Width = home.PlannerArea, + Length = home.PlannerArea, + Height = home.PlannerHeight, + CreationTime = DateTimeOffset.Now, + AccountId = session.AccountId, + CharacterId = session.CharacterId, + CharacterName = session.PlayerName, + }; + + item = db.CreateItem(session.CharacterId, item); + if (item == null) { + return; + } + + session.Item.Inventory.Add(item, notifyNew: true); + + session.StagedUgcItem = item; + session.Send(CubePacket.CreateBlueprint(item)); + } + private void HandleSaveBlueprint(GameSession session, IByteReader packet) { int slot = packet.ReadInt(); string name = packet.ReadUnicodeString(); + + Home home = session.Player.Value.Home; + if (slot is > Constant.HomeMaxLayoutSlots or < 0) { + return; + } + + Plot? plot = session.Housing.GetFieldPlot(); + if (plot == null) { + return; + } + + using GameStorage.Request db = session.GameStorage.Context(); + + HomeLayout? layout = home.Blueprints.FirstOrDefault(homeLayout => homeLayout.Id == slot); + if (layout is not null) { + home.Blueprints.Remove(layout); + db.RemoveHomeLayout(layout); + } + + byte area = home.PlannerArea; + byte height = home.PlannerHeight; + layout = db.SaveHomeLayout(new HomeLayout(slot, name, area, height, DateTimeOffset.Now, plot.Cubes.Values.ToList())); + if (layout is null) { + return; + } + home.Blueprints.Add(layout); + + session.Housing.SaveHome(); + + session.Send(CubePacket.SaveBlueprint(session.AccountId, layout)); } private void HandleLoadBlueprint(GameSession session, IByteReader packet) { + Plot? plot = session.Housing.GetFieldPlot(); + if (plot is null) { + return; + } + + if (plot.Cubes.Count != 0) { + session.Send(NoticePacket.Message(StringCode.s_err_ugcmap_package_clear_indoor_first, NoticePacket.Flags.Message | NoticePacket.Flags.Alert)); + return; + } + int slot = packet.ReadInt(); + + Home home = session.Player.Value.Home; + HomeLayout? layout = home.Blueprints.FirstOrDefault(homeLayout => homeLayout.Id == slot); + if (layout is null) { + return; + } + + ApplyLayout(session, plot, home, layout); } #region Helpers @@ -778,5 +841,76 @@ private static bool IsCoordOutsideArea(Vector3B position, Home home) { return false; } + + public static void RequestLayout(GameSession session, HomeLayout layout, TableMetadataStorage tableMetadata) { + Dictionary groupedCubes = layout.Cubes.GroupBy(plotCube => plotCube.ItemId).ToDictionary(grouping => grouping.Key, grouping => grouping.Count()); // Dictionary + int cubeCount = 0; + Dictionary cubeCosts = new() { + { FurnishingCurrencyType.Meso, 0 }, + { FurnishingCurrencyType.Meret, 0 }, + }; + + foreach ((int id, int amount) in groupedCubes) { + tableMetadata.FurnishingShopTable.Entries.TryGetValue(id, out FurnishingShopTable.Entry? shopEntry); + if (shopEntry is null) { + Log.Logger.Error("Failed to get shop entry for cube {cubeId}.", id); + session.Send(CubePacket.Error(UgcMapError.s_err_cannot_buy_limited_item_more)); + return; + } + + Item? item = session.Item.Furnishing.GetItem(id); + if (item is null) { + cubeCosts[shopEntry.FurnishingTokenType] += shopEntry.Price * amount; + cubeCount += amount; + continue; + } + + if (item.Amount >= amount) { + continue; + } + + int missingCubes = amount - item.Amount; + cubeCosts[shopEntry.FurnishingTokenType] += shopEntry.Price * missingCubes; + cubeCount += missingCubes; + } + + session.Send(CubePacket.BuyCubes(cubeCosts, cubeCount)); + } + + private void ApplyLayout(GameSession session, Plot plot, Home home, HomeLayout layout) { + if (plot.IsPlanner) { + home.SetPlannerArea(layout.Area); + home.SetPlannerHeight(layout.Height); + } else { + home.SetArea(layout.Area); + home.SetHeight(layout.Height); + } + + session.Field.Broadcast(CubePacket.UpdateHomeAreaAndHeight(home.Area, home.Height)); + + foreach (PlotCube cube in layout.Cubes) { + if (!TryPlaceCube(session, cube, plot, cube.Position, cube.Rotation, out PlotCube? plotCube)) { + return; + } + + ByteWriter sendPacket; + if (cube.Position.Z == 0) { + sendPacket = CubePacket.ReplaceCube(session.Player.ObjectId, plotCube); + } else { + sendPacket = CubePacket.PlaceCube(session.Player.ObjectId, plot, plotCube); + } + + session.Field.Broadcast(sendPacket); + } + + Vector3 position = home.CalculateSafePosition(plot.Cubes.Values.ToList()); + foreach (FieldPlayer fieldPlayer in session.Field.Players.Values) { + fieldPlayer.MoveToPosition(position, default); + } + + session.Item.Furnishing.SendStorageCount(); + session.Housing.SaveHome(); + session.Field.Broadcast(NoticePacket.Message(StringCode.s_ugcmap_package_automatic_creation_completed, NoticePacket.Flags.Message | NoticePacket.Flags.Alert)); + } #endregion } diff --git a/Maple2.Server.Game/PacketHandlers/UgcHandler.cs b/Maple2.Server.Game/PacketHandlers/UgcHandler.cs index 77394bd2e..21edf3ee3 100644 --- a/Maple2.Server.Game/PacketHandlers/UgcHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/UgcHandler.cs @@ -52,7 +52,7 @@ public override void Handle(GameSession session, IByteReader packet) { HandleProfilePicture(session, packet); return; case Command.LoadBanners: - HandleLoadBanners(session, packet); + HandleLoadBanners(session); return; case Command.ReserveBanner: HandleReserveBanner(session, packet); @@ -83,6 +83,9 @@ private void HandleUpload(GameSession session, IByteReader packet) { case UgcType.GuildBanner: UploadGuildBanner(session, packet); return; + case UgcType.LayoutBlueprint: + UploadLayoutBlueprint(session, packet); + return; default: Logger.Information("Unimplemented Ugc Type: {type}", info.Type); return; @@ -284,6 +287,38 @@ private void UploadGuildEmblem(GameSession session, IByteReader packet) { session.Send(UgcPacket.Upload(resource)); } + private void UploadLayoutBlueprint(GameSession session, IByteReader packet) { + long blueprintUid = packet.ReadLong(); + long itemUid = packet.ReadLong(); + int itemId = packet.ReadInt(); + string name = packet.ReadUnicodeString(); + + Item? item = session.StagedUgcItem; + if (item is null || item.Uid != itemUid || item.Id != itemId) { + Logger.Error("Failed to find staged item for UGC {ItemUid} {ItemId}", itemUid, itemId); + return; + } + + using WebStorage.Request request = WebStorage.Context(); + UgcResource? resource = request.CreateUgc(UgcType.LayoutBlueprint, session.CharacterId); + if (resource == null) { + Logger.Fatal("Failed to create UGC resource for guild id {GuildId}", session.Guild.Id); + throw new InvalidOperationException($"Fatal: Creating UGC resource: {session.Guild.Id}"); + } + + item.Template = new UgcItemLook { + Id = resource.Id, + AccountId = session.AccountId, + Author = session.PlayerName, + CharacterId = session.CharacterId, + CreationTime = DateTime.Now.ToEpochSeconds(), + Name = name, + }; + + session.StagedUgcItem = item; + session.Send(UgcPacket.Upload(resource)); + } + private void HandleConfirmation(GameSession session, IByteReader packet) { var info = packet.Read(); packet.ReadInt(); @@ -305,16 +340,19 @@ private void HandleConfirmation(GameSession session, IByteReader packet) { switch (info.Type) { case UgcType.Item or UgcType.Furniture or UgcType.Mount: - ConfirmItem(session, info, ugcUid, resource); + ConfirmItem(); break; case UgcType.Banner: - ConfirmBanner(session, ugcUid, resource); + ConfirmBanner(); break; case UgcType.GuildBanner: - ConfirmGuildBanner(session, ugcUid, resource); + ConfirmGuildBanner(); break; case UgcType.GuildEmblem: - ConfirmGuildEmblem(session, ugcUid, resource); + ConfirmGuildEmblem(); + break; + case UgcType.LayoutBlueprint: + ConfirmLayoutBlueprint(); break; default: Logger.Warning("Unhandled Confirmation for UGC Type {UgcType}", info.Type); @@ -323,8 +361,9 @@ private void HandleConfirmation(GameSession session, IByteReader packet) { session.StagedUgcItem = null; session.StagedGuildPoster = null; + return; - void ConfirmGuildBanner(GameSession session, long ugcUid, UgcResource resource) { + void ConfirmGuildBanner() { Guild? guild = session.Guild.Guild; if (guild is null) { Logger.Warning("Failed to find guild for UGC {UgcUid}", ugcUid); @@ -355,10 +394,11 @@ void ConfirmGuildBanner(GameSession session, long ugcUid, UgcResource resource) session.Send(GuildPacket.Error(error)); return; } - } catch (RpcException) { /* ignored */ } + } catch (RpcException) { /* ignored */ + } } - void ConfirmGuildEmblem(GameSession session, long ugcUid, UgcResource resource) { + void ConfirmGuildEmblem() { Guild? guild = session.Guild.Guild; if (guild is null) { Logger.Warning("Failed to find guild for UGC {UgcUid}", ugcUid); @@ -380,10 +420,11 @@ void ConfirmGuildEmblem(GameSession session, long ugcUid, UgcResource resource) session.Send(GuildPacket.Error(error)); return; } - } catch (RpcException) { /* ignored */ } + } catch (RpcException) { /* ignored */ + } } - void ConfirmBanner(GameSession session, long ugcUid, UgcResource resource) { + void ConfirmBanner() { UgcBanner? banner = session.Field.Banners.Values.FirstOrDefault(x => x.Slots.Any(slot => slot.Template?.Id == ugcUid)); if (banner is null) { Logger.Warning("Failed to find banner for UGC {UgcUid}", ugcUid); @@ -400,7 +441,7 @@ void ConfirmBanner(GameSession session, long ugcUid, UgcResource resource) { session.Send(UgcPacket.UpdateBanner(banner)); } - void ConfirmItem(GameSession session, UgcInfo info, long ugcUid, UgcResource resource) { + void ConfirmItem() { Item? item = session.StagedUgcItem; if (item?.Template == null || !TableMetadata.UgcDesignTable.Entries.TryGetValue(item.Id, out UgcDesignTable.Entry? ugcMetadata)) { return; @@ -425,6 +466,24 @@ void ConfirmItem(GameSession session, UgcInfo info, long ugcUid, UgcResource res session.Send(UgcPacket.UpdateItem(session.Player.ObjectId, item, ugcMetadata.CreatePrice, info.Type)); session.Send(UgcPacket.UpdatePath(resource)); } + + void ConfirmLayoutBlueprint() { + Item? item = session.StagedUgcItem; + if (item?.Template is null || item.Blueprint is null) { + return; + } + + item.Template.Url = resource.Path; + + using GameStorage.Request gameRequest = session.GameStorage.Context(); + if (!gameRequest.UpdateItem(item)) { + Logger.Fatal("Failed to update UGC Item {ugcUid}", ugcUid); + throw new InvalidOperationException($"Fatal: UGC Item update: {ugcUid}"); + } + + session.Send(UgcPacket.UpdateLayoutBlueprint(session.Player.ObjectId, item)); + session.Send(UgcPacket.UpdatePath(resource)); + } } private static void HandleProfilePicture(GameSession session, IByteReader packet) { @@ -435,8 +494,8 @@ private static void HandleProfilePicture(GameSession session, IByteReader packet session.Field?.Broadcast(UgcPacket.ProfilePicture(session.Player)); } - private static void HandleLoadBanners(GameSession session, IByteReader packet) { - session.Send(UgcPacket.LoadBanners(session.Field.Banners.Values.Select(x => (UgcBanner) x).ToList())); + private static void HandleLoadBanners(GameSession session) { + session.Send(UgcPacket.LoadBanners(session.Field.Banners.Values.Select(fieldUgcBanner => (UgcBanner) fieldUgcBanner).ToList())); } private void HandleReserveBanner(GameSession session, IByteReader packet) { diff --git a/Maple2.Server.Game/Packets/CubePacket.cs b/Maple2.Server.Game/Packets/CubePacket.cs index 060f0b48c..280e53194 100644 --- a/Maple2.Server.Game/Packets/CubePacket.cs +++ b/Maple2.Server.Game/Packets/CubePacket.cs @@ -7,7 +7,9 @@ using Maple2.Server.Core.Constants; using Maple2.Server.Core.Packets; using Maple2.Server.Game.Model; +using Maple2.Server.Game.Session; using Maple2.Tools.Extensions; +// ReSharper disable RedundantTypeArgumentsOfMethod namespace Maple2.Server.Game.Packets; @@ -61,6 +63,8 @@ private enum Command : byte { // Blueprint stuff: // 60, 61, 63, 64, 67, 69 UpdateHomeAreaAndHeight = 62, + CreateBlueprint = 63, + SaveBlueprint = 64, FunctionCubeError = 71, } @@ -486,6 +490,26 @@ public static ByteWriter UpdateHomeAreaAndHeight(byte area, byte height) { return pWriter; } + public static ByteWriter CreateBlueprint(Item item) { + var pWriter = Packet.Of(SendOp.ResponseCube); + pWriter.Write(Command.CreateBlueprint); + pWriter.WriteByte(1); + pWriter.WriteLong(item.Uid); + pWriter.WriteClass(item.Blueprint!); + + return pWriter; + } + + public static ByteWriter SaveBlueprint(long accountId, HomeLayout layout) { + var pWriter = Packet.Of(SendOp.ResponseCube); + pWriter.Write(Command.SaveBlueprint); + pWriter.Write(UgcMapError.s_ugcmap_ok); + pWriter.WriteLong(accountId); + pWriter.WriteClass(layout); + + return pWriter; + } + public static ByteWriter FunctionCubeError(FunctionCubeError error) { var pWriter = Packet.Of(SendOp.ResponseCube); pWriter.Write(Command.FunctionCubeError); diff --git a/Maple2.Server.Game/Session/GameSession.State.cs b/Maple2.Server.Game/Session/GameSession.State.cs index bc19b0cec..fbd4178f9 100644 --- a/Maple2.Server.Game/Session/GameSession.State.cs +++ b/Maple2.Server.Game/Session/GameSession.State.cs @@ -23,6 +23,8 @@ public partial class GameSession { public Item? StagedScoreItem = null; public bool EnsembleReady = false; + public ItemBlueprint? StagedItemBlueprint = null; + public Item? ChangeAttributesItem = null; public TradeManager? Trade; diff --git a/Maple2.Server.Web/Controllers/BlueprintController.cs b/Maple2.Server.Web/Controllers/BlueprintController.cs new file mode 100644 index 000000000..e100ac276 --- /dev/null +++ b/Maple2.Server.Web/Controllers/BlueprintController.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using Maple2.Tools; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Maple2.Server.Web.Controllers; + +[Route("/blueprint/ms2/01/")] +public class BlueprintController : ControllerBase { + + [HttpGet("{blueprintId}/{ugcUid}.png")] + public IResult GetBlueprint(long blueprintId, string ugcUid) { + Console.WriteLine($"GetBlueprint: blueprintId={blueprintId}, ugcUid={ugcUid}"); + string fullPath = Path.Combine(Paths.WEB_DATA_DIR, "blueprint", blueprintId.ToString(), $"{ugcUid}.png"); + if (!System.IO.File.Exists(fullPath)) { + return Results.NotFound(); + } + + FileStream blueprint = System.IO.File.OpenRead(fullPath); + return Results.File(blueprint, contentType: "image/png"); + } +} diff --git a/Maple2.Server.Web/Controllers/WebController.cs b/Maple2.Server.Web/Controllers/WebController.cs index 8d9f8c22b..e54203ce7 100644 --- a/Maple2.Server.Web/Controllers/WebController.cs +++ b/Maple2.Server.Web/Controllers/WebController.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Serilog; +using Serilog.Core; namespace Maple2.Server.Web.Controllers; @@ -34,7 +35,7 @@ public async Task Upload() { IByteReader packet = new ByteReader(memoryStream.ToArray()); packet.ReadInt(); var type = (UgcType) packet.ReadInt(); - packet.ReadLong(); + long accountId = packet.ReadLong(); long characterId = packet.ReadLong(); long ugcUid = packet.ReadLong(); int id = packet.ReadInt(); // item id, guild id, others? @@ -43,6 +44,8 @@ public async Task Upload() { byte[] fileBytes = packet.ReadBytes(packet.Available); + Log.Logger.Debug("Upload: type={Type}, accountId={AccountId}, characterId={CharacterId}, ugcUid={UgcUid}, id={Id}", type, accountId, characterId, ugcUid, id); + UgcResource? resource = null; if (ugcUid != 0) { using WebStorage.Request db = webStorage.Context(); @@ -50,18 +53,9 @@ public async Task Upload() { if (resource == null) { return Results.NotFound($"{ugcUid} does not exist."); } - - if (type != UgcType.ItemIcon && !string.IsNullOrEmpty(resource.Path)) { - if (System.IO.File.Exists(resource.Path)) { - return Results.Conflict("resource already exists."); - } - - await System.IO.File.WriteAllBytesAsync(resource.Path, fileBytes); - return Results.Text($"0,{resource.Path}"); - } } - if (type is UgcType.ItemIcon or UgcType.Item && resource == null) { + if (type is UgcType.ItemIcon or UgcType.Item or UgcType.BlueprintIcon or UgcType.LayoutBlueprint && resource == null) { return Results.BadRequest("Invalid UGC resource."); } @@ -72,6 +66,8 @@ public async Task Upload() { UgcType.Banner => UploadBanner(fileBytes, id, ugcUid), UgcType.GuildEmblem => HandleGuildEmblem(fileBytes, id, ugcUid), UgcType.GuildBanner => HandleGuildBanner(fileBytes, id, ugcUid), + UgcType.BlueprintIcon => HandleBlueprintIcon(fileBytes, ugcUid, resource!), + UgcType.LayoutBlueprint => HandleBlueprintPreview(fileBytes, ugcUid, resource!), _ => HandleUnknownMode(type), }; } @@ -176,6 +172,39 @@ private IResult HandleGuildBanner(byte[] fileBytes, int guildId, long ugcId) { return Results.Text($"0,{ugcPath}"); } + private IResult HandleBlueprintIcon(byte[] fileBytes, long ugcUid, UgcResource resource) { + string filePath = Path.Combine(Paths.WEB_DATA_DIR, "blueprint", ugcUid.ToString()); + try { + Directory.CreateDirectory(filePath); + } catch (Exception ex) { + Log.Error(ex, "Failed preparing directory: {Path}", filePath); + return Results.Problem("Internal Server Error", statusCode: 500); + } + + string ugcPath = $"blueprint/ms2/01/{ugcUid}/{resource.Id}_icon.png"; + + System.IO.File.WriteAllBytes(Path.Combine(filePath, $"{resource.Id}_icon.png"), fileBytes); + return Results.Text($"0,{ugcPath}"); + } + + private IResult HandleBlueprintPreview(byte[] fileBytes, long ugcUid, UgcResource resource) { + string filePath = Path.Combine(Paths.WEB_DATA_DIR, "blueprint", ugcUid.ToString()); + try { + Directory.CreateDirectory(filePath); + } catch (Exception ex) { + Log.Error(ex, "Failed preparing directory: {Path}", filePath); + return Results.Problem("Internal Server Error", statusCode: 500); + } + using WebStorage.Request db = webStorage.Context(); + string ugcPath = $"blueprint/ms2/01/{ugcUid}/{resource.Id}.png"; + + db.UpdatePath(ugcUid, ugcPath); + + System.IO.File.WriteAllBytes(Path.Combine(filePath, $"{resource.Id}.png"), fileBytes); + return Results.Text($"0,{ugcPath}"); + } + + private static IResult HandleUnknownMode(UgcType mode) { Log.Logger.Warning("Invalid upload mode: {Mode}", mode); return Results.BadRequest($"Invalid upload mode: {mode}"); diff --git a/Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.Designer.cs b/Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.Designer.cs new file mode 100644 index 000000000..e36f78ec2 --- /dev/null +++ b/Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.Designer.cs @@ -0,0 +1,1744 @@ +// +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("20240918223130_AddHomeLayoutsAndCubesTable")] + partial class AddHomeLayoutsAndCubesTable + { + /// + 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("Height") + .HasColumnType("tinyint unsigned"); + + b.Property("Id") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Uid"); + + b.ToTable("home-layout", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.HomeLayoutCube", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("HomeLayoutId") + .HasColumnType("bigint"); + + b.Property("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.HasKey("ReceiverId", "Id"); + + b.ToTable("mail", (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.PremiumMarketItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BannerLabel") + .HasColumnType("int"); + + b.Property("BannerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("BonusQuantity") + .HasColumnType("int"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)"); + + b.Property("CurrencyType") + .HasColumnType("tinyint unsigned"); + + b.Property("Giftable") + .HasColumnType("tinyint(1)"); + + b.Property("ItemDuration") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("JobRequirement") + .HasColumnType("int"); + + b.Property("Label") + .HasColumnType("tinyint unsigned"); + + b.Property("ParentId") + .HasColumnType("int"); + + b.Property("PcCafe") + .HasColumnType("tinyint(1)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("PromoData") + .HasColumnType("json"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("Rarity") + .HasColumnType("tinyint unsigned"); + + b.Property("RequireAchievementId") + .HasColumnType("int"); + + b.Property("RequireAchievementRank") + .HasColumnType("int"); + + b.Property("RequireMaxLevel") + .HasColumnType("smallint"); + + b.Property("RequireMinLevel") + .HasColumnType("smallint"); + + b.Property("RestockUnavailable") + .HasColumnType("tinyint(1)"); + + b.Property("SalePrice") + .HasColumnType("bigint"); + + b.Property("SalesCount") + .HasColumnType("int"); + + b.Property("SellBeginTime") + .HasColumnType("datetime(6)"); + + b.Property("SellEndTime") + .HasColumnType("datetime(6)"); + + b.Property("ShowSaleTime") + .HasColumnType("tinyint(1)"); + + b.Property("TabId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("premium-market-item", (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.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("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("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.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.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.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/20240918223130_AddHomeLayoutsAndCubesTable.cs b/Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.cs new file mode 100644 index 000000000..32fce14a4 --- /dev/null +++ b/Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.cs @@ -0,0 +1,93 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Maple2.Server.World.Migrations { + /// + public partial class AddHomeLayoutsAndCubesTable : Migration { + /// + protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.AddColumn( + name: "Blueprints", + table: "home", + type: "json", + defaultValue: "[]", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Layouts", + table: "home", + type: "json", + defaultValue: "[]", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "home-layout", + columns: table => new { + Uid = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Id = table.Column(type: "int", nullable: false), + Name = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Area = table.Column(type: "tinyint unsigned", nullable: false), + Height = table.Column(type: "tinyint unsigned", nullable: false), + Timestamp = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => { + table.PrimaryKey("PK_home-layout", x => x.Uid); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "home-layout-cube", + columns: table => new { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + HomeLayoutId = table.Column(type: "bigint", nullable: false), + X = table.Column(type: "tinyint", nullable: false), + Y = table.Column(type: "tinyint", nullable: false), + Z = table.Column(type: "tinyint", nullable: false), + Rotation = table.Column(type: "float", nullable: false), + ItemId = table.Column(type: "int", nullable: false), + Template = table.Column(type: "json", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => { + table.PrimaryKey("PK_home-layout-cube", x => x.Id); + table.ForeignKey( + name: "FK_home-layout-cube_home-layout_HomeLayoutId", + column: x => x.HomeLayoutId, + principalTable: "home-layout", + principalColumn: "Uid", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_home-layout-cube_HomeLayoutId", + table: "home-layout-cube", + column: "HomeLayoutId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropTable( + name: "home-layout-cube"); + + migrationBuilder.DropTable( + name: "home-layout"); + + migrationBuilder.DropColumn( + name: "Blueprints", + table: "home"); + + migrationBuilder.DropColumn( + name: "Layouts", + table: "home"); + } + } +} diff --git a/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs b/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs index 2d112b6f0..6a441af53 100644 --- a/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs +++ b/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs @@ -661,6 +661,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Background") .HasColumnType("tinyint unsigned"); + b.Property("Blueprints") + .IsRequired() + .HasColumnType("json"); + b.Property("Camera") .HasColumnType("tinyint unsigned"); @@ -699,6 +703,67 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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("Height") + .HasColumnType("tinyint unsigned"); + + b.Property("Id") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Uid"); + + b.ToTable("home-layout", (string)null); + }); + + modelBuilder.Entity("Maple2.Database.Model.HomeLayoutCube", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("HomeLayoutId") + .HasColumnType("bigint"); + + b.Property("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") @@ -1568,6 +1633,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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) @@ -1652,6 +1726,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Members"); }); + modelBuilder.Entity("Maple2.Database.Model.HomeLayout", b => + { + b.Navigation("Cubes"); + }); + modelBuilder.Entity("Maple2.Database.Model.UgcMap", b => { b.Navigation("Cubes"); From 8145205f5e8dbabd5ab741a8692a938bf7decb29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Thu, 19 Sep 2024 01:29:18 -0300 Subject: [PATCH 02/12] suggestions --- Maple2.Database/Context/Ms2Context.cs | 4 ++-- Maple2.Database/Storage/Game/GameStorage.HomeLayout.cs | 8 ++++---- Maple2.Server.Web/Controllers/BlueprintController.cs | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Maple2.Database/Context/Ms2Context.cs b/Maple2.Database/Context/Ms2Context.cs index 113867c8f..8558d8ce6 100644 --- a/Maple2.Database/Context/Ms2Context.cs +++ b/Maple2.Database/Context/Ms2Context.cs @@ -39,8 +39,8 @@ public sealed class Ms2Context(DbContextOptions options) : DbContext(options) { internal DbSet ServerInfo { get; set; } = null!; internal DbSet Medal { get; set; } = null!; internal DbSet BannerSlots { get; set; } = null!; - internal DbSet HomeLayouts { get; set; } = null!; - internal DbSet UgcCubeLayouts { get; set; } = null!; + internal DbSet HomeLayout { get; set; } = null!; + internal DbSet UgcCubeLayout { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); diff --git a/Maple2.Database/Storage/Game/GameStorage.HomeLayout.cs b/Maple2.Database/Storage/Game/GameStorage.HomeLayout.cs index ba8814798..f027ae85c 100644 --- a/Maple2.Database/Storage/Game/GameStorage.HomeLayout.cs +++ b/Maple2.Database/Storage/Game/GameStorage.HomeLayout.cs @@ -9,9 +9,9 @@ public partial class GameStorage { public partial class Request { public HomeLayout? SaveHomeLayout(HomeLayout layout) { Model.HomeLayout homeLayout = layout; - Context.HomeLayouts.Add(homeLayout); + Context.HomeLayout.Add(homeLayout); foreach (HomeLayoutCube cubes in homeLayout.Cubes) { - Context.UgcCubeLayouts.Add(cubes); + Context.UgcCubeLayout.Add(cubes); } bool success = Context.TrySaveChanges(); @@ -20,12 +20,12 @@ public partial class Request { public void RemoveHomeLayout(HomeLayout layout) { Model.HomeLayout homeLayout = layout; - Context.HomeLayouts.Remove(homeLayout); + Context.HomeLayout.Remove(homeLayout); Context.TrySaveChanges(); } public HomeLayout? GetHomeLayout(long layoutUid) { - HomeLayout? layout = Context.HomeLayouts + HomeLayout? layout = Context.HomeLayout .Where(homeLayout => homeLayout.Uid == layoutUid) .Include(homeLayout => homeLayout.Cubes) .FirstOrDefault(); diff --git a/Maple2.Server.Web/Controllers/BlueprintController.cs b/Maple2.Server.Web/Controllers/BlueprintController.cs index e100ac276..45f9d4be0 100644 --- a/Maple2.Server.Web/Controllers/BlueprintController.cs +++ b/Maple2.Server.Web/Controllers/BlueprintController.cs @@ -11,7 +11,6 @@ public class BlueprintController : ControllerBase { [HttpGet("{blueprintId}/{ugcUid}.png")] public IResult GetBlueprint(long blueprintId, string ugcUid) { - Console.WriteLine($"GetBlueprint: blueprintId={blueprintId}, ugcUid={ugcUid}"); string fullPath = Path.Combine(Paths.WEB_DATA_DIR, "blueprint", blueprintId.ToString(), $"{ugcUid}.png"); if (!System.IO.File.Exists(fullPath)) { return Results.NotFound(); From 7d5c60823fd47a7fb39c35ec4cabb74cc99bd60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Thu, 19 Sep 2024 01:36:18 -0300 Subject: [PATCH 03/12] fix packet --- Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs | 2 +- Maple2.Server.Game/Packets/CubePacket.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs b/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs index 9d1ad04ab..b90909455 100644 --- a/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs @@ -664,7 +664,7 @@ private void HandleCreateBlueprint(GameSession session) { session.Item.Inventory.Add(item, notifyNew: true); session.StagedUgcItem = item; - session.Send(CubePacket.CreateBlueprint(item)); + session.Send(CubePacket.CreateBlueprint(item.Uid, item.Blueprint!)); } private void HandleSaveBlueprint(GameSession session, IByteReader packet) { diff --git a/Maple2.Server.Game/Packets/CubePacket.cs b/Maple2.Server.Game/Packets/CubePacket.cs index 280e53194..da4760b47 100644 --- a/Maple2.Server.Game/Packets/CubePacket.cs +++ b/Maple2.Server.Game/Packets/CubePacket.cs @@ -490,12 +490,12 @@ public static ByteWriter UpdateHomeAreaAndHeight(byte area, byte height) { return pWriter; } - public static ByteWriter CreateBlueprint(Item item) { + public static ByteWriter CreateBlueprint(long itemUid, ItemBlueprint blueprint) { var pWriter = Packet.Of(SendOp.ResponseCube); pWriter.Write(Command.CreateBlueprint); pWriter.WriteByte(1); - pWriter.WriteLong(item.Uid); - pWriter.WriteClass(item.Blueprint!); + pWriter.WriteLong(itemUid); + pWriter.WriteClass(blueprint); return pWriter; } From 3bbb04bab9695553cada243db02f870df3e4a715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Thu, 19 Sep 2024 01:36:26 -0300 Subject: [PATCH 04/12] fix exception --- Maple2.Server.Game/PacketHandlers/UgcHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Maple2.Server.Game/PacketHandlers/UgcHandler.cs b/Maple2.Server.Game/PacketHandlers/UgcHandler.cs index 21edf3ee3..47d4ab1a2 100644 --- a/Maple2.Server.Game/PacketHandlers/UgcHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/UgcHandler.cs @@ -302,8 +302,8 @@ private void UploadLayoutBlueprint(GameSession session, IByteReader packet) { using WebStorage.Request request = WebStorage.Context(); UgcResource? resource = request.CreateUgc(UgcType.LayoutBlueprint, session.CharacterId); if (resource == null) { - Logger.Fatal("Failed to create UGC resource for guild id {GuildId}", session.Guild.Id); - throw new InvalidOperationException($"Fatal: Creating UGC resource: {session.Guild.Id}"); + Logger.Fatal("Failed to create UGC resource for layout blueprint for character {CharacterId}", session.CharacterId); + throw new InvalidOperationException($"Fatal: Creating UGC resource for layout blueprint for character: {session.CharacterId}"); } item.Template = new UgcItemLook { From f2c185d5283a789ca222045abcdf20e0f4df56a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Thu, 19 Sep 2024 01:37:38 -0300 Subject: [PATCH 05/12] Update GameStorage.User.cs --- Maple2.Database/Storage/Game/GameStorage.User.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Maple2.Database/Storage/Game/GameStorage.User.cs b/Maple2.Database/Storage/Game/GameStorage.User.cs index 145721f80..c2e630a55 100644 --- a/Maple2.Database/Storage/Game/GameStorage.User.cs +++ b/Maple2.Database/Storage/Game/GameStorage.User.cs @@ -186,11 +186,7 @@ from outdoor in plot.DefaultIfEmpty() Context.Account.Update(account); Context.Character.Update(character); - try { - Context.SaveChanges(); - } catch (Exception e) { - Console.WriteLine(e); - } + Context.SaveChanges(); Tuple guild = Context.GuildMember .Where(member => member.CharacterId == characterId) From b78fa9ade56fb5286f415022f2939bcd433a8e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Thu, 19 Sep 2024 01:48:31 -0300 Subject: [PATCH 06/12] suggestion --- Maple2.Server.Game/Manager/HousingManager.cs | 208 ++++++++++++++++- .../PacketHandlers/ItemUseHandler.cs | 2 +- .../PacketHandlers/RequestCubeHandler.cs | 210 +----------------- Maple2.Server.Game/Session/GameSession.cs | 2 +- 4 files changed, 218 insertions(+), 204 deletions(-) diff --git a/Maple2.Server.Game/Manager/HousingManager.cs b/Maple2.Server.Game/Manager/HousingManager.cs index 861aa7590..5e1539f9e 100644 --- a/Maple2.Server.Game/Manager/HousingManager.cs +++ b/Maple2.Server.Game/Manager/HousingManager.cs @@ -1,23 +1,33 @@ -using Maple2.Database.Extensions; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using Maple2.Database.Extensions; using Maple2.Database.Storage; +using Maple2.Model.Common; using Maple2.Model.Enum; using Maple2.Model.Error; using Maple2.Model.Game; using Maple2.Model.Metadata; +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Packets; +using Maple2.Server.Game.Manager.Items; +using Maple2.Server.Game.Model; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; using Serilog; +using Serilog.Core; namespace Maple2.Server.Game.Manager; public class HousingManager { private readonly GameSession session; + private readonly TableMetadataStorage tableMetadata; private Home Home => session.Player.Value.Home; private readonly ILogger logger = Log.Logger.ForContext(); - public HousingManager(GameSession session) { + public HousingManager(GameSession session, TableMetadataStorage tableMetadata) { this.session = session; + this.tableMetadata = tableMetadata; } public void SetPlot(PlotInfo? plot) { @@ -307,4 +317,198 @@ public void InitNewHome(string characterName, ExportedUgcMapMetadata? template) db.SavePlotInfo(Home.Indoor); db.SaveCubes(Home.Indoor, plotCubes); } + + #region Helpers + public bool TryPlaceCube(HeldCube cube, Plot plot, in Vector3B position, float rotation, + [NotNullWhen(true)] out PlotCube? result, bool isReplace = false) { + result = null; + if (!session.ItemMetadata.TryGet(cube.ItemId, out ItemMetadata? itemMetadata) || itemMetadata.Install is null) { + logger.Error("Failed to get item metadata for cube {cubeId}.", cube.ItemId); + return false; + } + + bool isSolidCube = itemMetadata.Install.IsSolidCube; + bool isOnGround = position.Z == 0; // TODO: Handle outside plots + bool allowWaterOnGround = itemMetadata.Install.MapAttribute is MapAttribute.water && Constant.AllowWaterOnGround; + + // If the cube is not a solid cube and it's replacing ground, it's not allowed. + if ((!isSolidCube && isOnGround) && !allowWaterOnGround) { + session.Send(CubePacket.Error(UgcMapError.s_ugcmap_cant_create_on_place)); + return false; + } + + // Cannot overlap cubes if not replacing + if (plot.Cubes.ContainsKey(position) && !isReplace) { + session.Send(CubePacket.Error(UgcMapError.s_ugcmap_cant_create_on_place)); + return false; + } + + if (isReplace && plot.Cubes.ContainsKey(position)) { + TryRemoveCube(plot, position, out _); + } + + //TODO: check outside plot - coords belongs to plot + + // TODO: check outside plot bounds + + if (IsCoordOutsideArea(position)) { + session.Send(CubePacket.Error(UgcMapError.s_ugcmap_area_limit)); + return false; + } + + if (plot.IsPlanner) { + result = new PlotCube(cube.ItemId, id: FurnishingManager.NextCubeId(), template: cube.Template) { + Position = position, + Rotation = rotation, + }; + + plot.Cubes.Add(position, result); + return true; + } + + tableMetadata.FurnishingShopTable.Entries.TryGetValue(cube.ItemId, out FurnishingShopTable.Entry? shopEntry); + if (shopEntry is null) { + session.Send(CubePacket.Error(UgcMapError.s_err_cannot_buy_limited_item_more)); + return false; + } + + if (!session.Item.Furnishing.PurchaseCube(shopEntry)) { + return false; + } + + if (!session.Item.Furnishing.TryPlaceCube(cube.Id, out result)) { + long itemUid = session.Item.Furnishing.AddCube(cube.ItemId); + if (itemUid == 0) { + session.Send(CubePacket.Error(UgcMapError.s_ugcmap_not_for_sale)); + return false; + } + + session.Send(CubePacket.PurchaseCube(session.Player.ObjectId)); + // Now that we have purchased the cube, it must be placeable. + if (!session.Item.Furnishing.TryPlaceCube(itemUid, out result)) { + session.Send(CubePacket.Error(UgcMapError.s_ugcmap_not_owned_item)); + return false; + } + } + + result.Position = position; + result.Rotation = rotation; + plot.Cubes.Add(position, result); + return true; + } + + public bool TryRemoveCube(Plot plot, in Vector3B position, [NotNullWhen(true)] out PlotCube? cube) { + if (!plot.Cubes.Remove(position, out cube)) { + session.Send(CubePacket.Error(UgcMapError.s_ugcmap_no_cube_to_remove)); + return false; + } + + if (plot.IsPlanner) { + return true; + } + + if (!session.Item.Furnishing.RetrieveCube(cube.Id)) { + throw new InvalidOperationException($"Failed to deposit cube {cube.Id} back into storage."); + } + + return true; + } + + public bool IsCoordOutsideArea(Vector3B position) { + int height = Home.IsPlanner ? Home.PlannerHeight : Home.Height; + int area = Home.IsPlanner ? Home.PlannerArea : Home.Area; + + // Check if the position is outside the planar area bounds + if (position.X > 0 || position.Y > 0 || position.Z < 0) { + return true; + } + + area *= -1; + if (position.X <= area || position.Y <= area) { + return true; + } + + // Check if the position is outside the height bounds + if (position.Z > height) { + return true; + } + + return false; + } + + public void RequestLayout(HomeLayout layout) { + Dictionary groupedCubes = layout.Cubes.GroupBy(plotCube => plotCube.ItemId) + .ToDictionary(grouping => grouping.Key, grouping => grouping.Count()); // Dictionary + int cubeCount = 0; + Dictionary cubeCosts = new() { + { + FurnishingCurrencyType.Meso, 0 + }, { + FurnishingCurrencyType.Meret, 0 + }, + }; + + foreach ((int id, int amount) in groupedCubes) { + tableMetadata.FurnishingShopTable.Entries.TryGetValue(id, out FurnishingShopTable.Entry? shopEntry); + if (shopEntry is null) { + Log.Logger.Error("Failed to get shop entry for cube {cubeId}.", id); + session.Send(CubePacket.Error(UgcMapError.s_err_cannot_buy_limited_item_more)); + return; + } + + Item? item = session.Item.Furnishing.GetItem(id); + if (item is null) { + cubeCosts[shopEntry.FurnishingTokenType] += shopEntry.Price * amount; + cubeCount += amount; + continue; + } + + if (item.Amount >= amount) { + continue; + } + + int missingCubes = amount - item.Amount; + cubeCosts[shopEntry.FurnishingTokenType] += shopEntry.Price * missingCubes; + cubeCount += missingCubes; + } + + session.Send(CubePacket.BuyCubes(cubeCosts, cubeCount)); + } + + public void ApplyLayout(Plot plot, HomeLayout layout) { + if (plot.IsPlanner) { + Home.SetPlannerArea(layout.Area); + Home.SetPlannerHeight(layout.Height); + } else { + Home.SetArea(layout.Area); + Home.SetHeight(layout.Height); + } + + session.Field.Broadcast(CubePacket.UpdateHomeAreaAndHeight(Home.Area, Home.Height)); + + foreach (PlotCube cube in layout.Cubes) { + if (!TryPlaceCube(cube, plot, cube.Position, cube.Rotation, out PlotCube? plotCube)) { + return; + } + + ByteWriter sendPacket; + if (cube.Position.Z == 0) { + sendPacket = CubePacket.ReplaceCube(session.Player.ObjectId, plotCube); + } else { + sendPacket = CubePacket.PlaceCube(session.Player.ObjectId, plot, plotCube); + } + + session.Field.Broadcast(sendPacket); + } + + Vector3 position = Home.CalculateSafePosition(plot.Cubes.Values.ToList()); + foreach (FieldPlayer fieldPlayer in session.Field.Players.Values) { + fieldPlayer.MoveToPosition(position, default); + } + + session.Item.Furnishing.SendStorageCount(); + session.Housing.SaveHome(); + session.Field.Broadcast(NoticePacket.Message(StringCode.s_ugcmap_package_automatic_creation_completed, NoticePacket.Flags.Message | NoticePacket.Flags.Alert)); + } + #endregion } diff --git a/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs b/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs index 43630540a..658324410 100644 --- a/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs @@ -128,7 +128,7 @@ private void HandleBlueprintImport(GameSession session, Item item) { } session.StagedItemBlueprint = item.Blueprint; - RequestCubeHandler.RequestLayout(session, layout, TableMetadata); + session.Housing.RequestLayout(layout); } private static void HandleStoryBook(GameSession session, Item item) { diff --git a/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs b/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs index b90909455..42d1b6ca3 100644 --- a/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs @@ -214,7 +214,7 @@ private void HandlePlaceCube(GameSession session, IByteReader packet) { return; } - if (!TryPlaceCube(session, cubeItem, plot, position, rotation, out PlotCube? plotCube)) { + if (!session.Housing.TryPlaceCube(cubeItem, plot, position, rotation, out PlotCube? plotCube)) { return; } @@ -259,7 +259,7 @@ private void HandleRemoveCube(GameSession session, IByteReader packet) { return; } - if (!TryRemoveCube(session, plot, position, out PlotCube? cube)) { + if (!session.Housing.TryRemoveCube(plot, position, out PlotCube? cube)) { return; } @@ -308,7 +308,7 @@ private void HandleReplaceCube(GameSession session, IByteReader packet) { return; } - if (TryPlaceCube(session, cubeItem, plot, position, rotation, out PlotCube? placedCube, isReplace: true)) { + if (session.Housing.TryPlaceCube(cubeItem, plot, position, rotation, out PlotCube? placedCube, isReplace: true)) { session.Field?.Broadcast(CubePacket.ReplaceCube(session.Player.ObjectId, placedCube)); } } @@ -368,7 +368,7 @@ private void HandleClearCubes(GameSession session) { } foreach (PlotCube cube in plot.Cubes.Values) { - if (TryRemoveCube(session, plot, cube.Position, out _)) { + if (session.Housing.TryRemoveCube(plot, cube.Position, out _)) { session.Field?.Broadcast(CubePacket.RemoveCube(session.Player.ObjectId, cube.Position)); } } @@ -394,7 +394,7 @@ private void HandleRequestLayout(GameSession session, IByteReader packet) { return; } - RequestLayout(session, layout, TableMetadata); + session.Housing.RequestLayout(layout); } private void HandleIncreaseArea(GameSession session) { @@ -432,9 +432,9 @@ private void HandleDecreaseArea(GameSession session) { } // Remove cubes that are now outside the new area - List cubesToRemove = plot.Cubes.Values.Where(cube => IsCoordOutsideArea(cube.Position, session.Player.Value.Home)).ToList(); + List cubesToRemove = plot.Cubes.Values.Where(cube => session.Housing.IsCoordOutsideArea(cube.Position)).ToList(); foreach (PlotCube cube in cubesToRemove) { - if (TryRemoveCube(session, plot, cube.Position, out _)) { + if (session.Housing.TryRemoveCube(plot, cube.Position, out _)) { session.Field?.Broadcast(CubePacket.RemoveCube(session.Player.ObjectId, cube.Position)); } } @@ -483,7 +483,7 @@ private void HandleDecreaseHeight(GameSession session) { // Remove cubes that are now outside the new height List cubesToRemove = plot.Cubes.Values.Where(cube => cube.Position.Z > newHeight).ToList(); foreach (PlotCube cube in cubesToRemove) { - if (TryRemoveCube(session, plot, cube.Position, out _)) { + if (session.Housing.TryRemoveCube(plot, cube.Position, out _)) { session.Field?.Broadcast(CubePacket.RemoveCube(session.Player.ObjectId, cube.Position)); } } @@ -589,7 +589,7 @@ private void HandleLoadLayout(GameSession session, IByteReader packet) { } session.StagedItemBlueprint = null; - ApplyLayout(session, plot, home, layout); + session.Housing.ApplyLayout(plot, layout); } @@ -721,196 +721,6 @@ private void HandleLoadBlueprint(GameSession session, IByteReader packet) { return; } - ApplyLayout(session, plot, home, layout); + session.Housing.ApplyLayout(plot, layout); } - - #region Helpers - private bool TryPlaceCube(GameSession session, HeldCube cube, Plot plot, in Vector3B position, float rotation, - [NotNullWhen(true)] out PlotCube? result, bool isReplace = false) { - result = null; - if (!session.ItemMetadata.TryGet(cube.ItemId, out ItemMetadata? itemMetadata) || itemMetadata.Install is null) { - Logger.Error("Failed to get item metadata for cube {cubeId}.", cube.ItemId); - return false; - } - - bool isSolidCube = itemMetadata.Install.IsSolidCube; - bool isOnGround = position.Z == 0; // TODO: Handle outside plots - bool allowWaterOnGround = itemMetadata.Install.MapAttribute is MapAttribute.water && Constant.AllowWaterOnGround; - - // If the cube is not a solid cube and it's replacing ground, it's not allowed. - if ((!isSolidCube && isOnGround) && !allowWaterOnGround) { - session.Send(CubePacket.Error(UgcMapError.s_ugcmap_cant_create_on_place)); - return false; - } - - // Cannot overlap cubes if not replacing - if (plot.Cubes.ContainsKey(position) && !isReplace) { - session.Send(CubePacket.Error(UgcMapError.s_ugcmap_cant_create_on_place)); - return false; - } - - if (isReplace && plot.Cubes.ContainsKey(position)) { - TryRemoveCube(session, plot, position, out _); - } - - //TODO: check outside plot - coords belongs to plot - - // TODO: check outside plot bounds - - if (IsCoordOutsideArea(position, session.Player.Value.Home)) { - session.Send(CubePacket.Error(UgcMapError.s_ugcmap_area_limit)); - return false; - } - - if (plot.IsPlanner) { - result = new PlotCube(cube.ItemId, id: FurnishingManager.NextCubeId(), template: cube.Template) { - Position = position, - Rotation = rotation, - }; - - plot.Cubes.Add(position, result); - return true; - } - - TableMetadata.FurnishingShopTable.Entries.TryGetValue(cube.ItemId, out FurnishingShopTable.Entry? shopEntry); - if (shopEntry is null) { - session.Send(CubePacket.Error(UgcMapError.s_err_cannot_buy_limited_item_more)); - return false; - } - - if (!session.Item.Furnishing.PurchaseCube(shopEntry)) { - return false; - } - - if (!session.Item.Furnishing.TryPlaceCube(cube.Id, out result)) { - long itemUid = session.Item.Furnishing.AddCube(cube.ItemId); - if (itemUid == 0) { - session.Send(CubePacket.Error(UgcMapError.s_ugcmap_not_for_sale)); - return false; - } - - session.Send(CubePacket.PurchaseCube(session.Player.ObjectId)); - // Now that we have purchased the cube, it must be placeable. - if (!session.Item.Furnishing.TryPlaceCube(itemUid, out result)) { - session.Send(CubePacket.Error(UgcMapError.s_ugcmap_not_owned_item)); - return false; - } - } - - result.Position = position; - result.Rotation = rotation; - plot.Cubes.Add(position, result); - return true; - } - - private static bool TryRemoveCube(GameSession session, Plot plot, in Vector3B position, [NotNullWhen(true)] out PlotCube? cube) { - if (!plot.Cubes.Remove(position, out cube)) { - session.Send(CubePacket.Error(UgcMapError.s_ugcmap_no_cube_to_remove)); - return false; - } - - if (plot.IsPlanner) { - return true; - } - - if (!session.Item.Furnishing.RetrieveCube(cube.Id)) { - throw new InvalidOperationException($"Failed to deposit cube {cube.Id} back into storage."); - } - - return true; - } - - private static bool IsCoordOutsideArea(Vector3B position, Home home) { - int height = home.IsPlanner ? home.PlannerHeight : home.Height; - int area = home.IsPlanner ? home.PlannerArea : home.Area; - - // Check if the position is outside the planar area bounds - if (position.X > 0 || position.Y > 0 || position.Z < 0) { - return true; - } - - area *= -1; - if (position.X <= area || position.Y <= area) { - return true; - } - - // Check if the position is outside the height bounds - if (position.Z > height) { - return true; - } - - return false; - } - - public static void RequestLayout(GameSession session, HomeLayout layout, TableMetadataStorage tableMetadata) { - Dictionary groupedCubes = layout.Cubes.GroupBy(plotCube => plotCube.ItemId).ToDictionary(grouping => grouping.Key, grouping => grouping.Count()); // Dictionary - int cubeCount = 0; - Dictionary cubeCosts = new() { - { FurnishingCurrencyType.Meso, 0 }, - { FurnishingCurrencyType.Meret, 0 }, - }; - - foreach ((int id, int amount) in groupedCubes) { - tableMetadata.FurnishingShopTable.Entries.TryGetValue(id, out FurnishingShopTable.Entry? shopEntry); - if (shopEntry is null) { - Log.Logger.Error("Failed to get shop entry for cube {cubeId}.", id); - session.Send(CubePacket.Error(UgcMapError.s_err_cannot_buy_limited_item_more)); - return; - } - - Item? item = session.Item.Furnishing.GetItem(id); - if (item is null) { - cubeCosts[shopEntry.FurnishingTokenType] += shopEntry.Price * amount; - cubeCount += amount; - continue; - } - - if (item.Amount >= amount) { - continue; - } - - int missingCubes = amount - item.Amount; - cubeCosts[shopEntry.FurnishingTokenType] += shopEntry.Price * missingCubes; - cubeCount += missingCubes; - } - - session.Send(CubePacket.BuyCubes(cubeCosts, cubeCount)); - } - - private void ApplyLayout(GameSession session, Plot plot, Home home, HomeLayout layout) { - if (plot.IsPlanner) { - home.SetPlannerArea(layout.Area); - home.SetPlannerHeight(layout.Height); - } else { - home.SetArea(layout.Area); - home.SetHeight(layout.Height); - } - - session.Field.Broadcast(CubePacket.UpdateHomeAreaAndHeight(home.Area, home.Height)); - - foreach (PlotCube cube in layout.Cubes) { - if (!TryPlaceCube(session, cube, plot, cube.Position, cube.Rotation, out PlotCube? plotCube)) { - return; - } - - ByteWriter sendPacket; - if (cube.Position.Z == 0) { - sendPacket = CubePacket.ReplaceCube(session.Player.ObjectId, plotCube); - } else { - sendPacket = CubePacket.PlaceCube(session.Player.ObjectId, plot, plotCube); - } - - session.Field.Broadcast(sendPacket); - } - - Vector3 position = home.CalculateSafePosition(plot.Cubes.Values.ToList()); - foreach (FieldPlayer fieldPlayer in session.Field.Players.Values) { - fieldPlayer.MoveToPosition(position, default); - } - - session.Item.Furnishing.SendStorageCount(); - session.Housing.SaveHome(); - session.Field.Broadcast(NoticePacket.Message(StringCode.s_ugcmap_package_automatic_creation_completed, NoticePacket.Flags.Message | NoticePacket.Flags.Alert)); - } - #endregion } diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index 18e1c1663..e5dda5894 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -145,7 +145,7 @@ public bool EnterServer(long accountId, Guid machineId, MigrateInResponse migrat Mastery = new MasteryManager(this, Lua); Stats = new StatsManager(Player, ServerTableMetadata.UserStatTable); Config = new ConfigManager(db, this); - Housing = new HousingManager(this); + Housing = new HousingManager(this, TableMetadata); Mail = new MailManager(this); ItemEnchant = new ItemEnchantManager(this, Lua); ItemBox = new ItemBoxManager(this); From c7868929af13d10b4648b9ce723576a38762ee61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Thu, 19 Sep 2024 01:51:45 -0300 Subject: [PATCH 07/12] move StagedItemBlueprint to HousingManager --- Maple2.Server.Game/Manager/HousingManager.cs | 2 ++ Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs | 2 +- Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs | 6 +++--- Maple2.Server.Game/Session/GameSession.State.cs | 2 -- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Maple2.Server.Game/Manager/HousingManager.cs b/Maple2.Server.Game/Manager/HousingManager.cs index 5e1539f9e..ba846938d 100644 --- a/Maple2.Server.Game/Manager/HousingManager.cs +++ b/Maple2.Server.Game/Manager/HousingManager.cs @@ -25,6 +25,8 @@ public class HousingManager { private readonly ILogger logger = Log.Logger.ForContext(); + public ItemBlueprint? StagedItemBlueprint = null; + public HousingManager(GameSession session, TableMetadataStorage tableMetadata) { this.session = session; this.tableMetadata = tableMetadata; diff --git a/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs b/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs index 658324410..9e87068af 100644 --- a/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs @@ -127,7 +127,7 @@ private void HandleBlueprintImport(GameSession session, Item item) { return; } - session.StagedItemBlueprint = item.Blueprint; + session.Housing.StagedItemBlueprint = item.Blueprint; session.Housing.RequestLayout(layout); } diff --git a/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs b/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs index 42d1b6ca3..6cd214896 100644 --- a/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs @@ -575,12 +575,12 @@ private void HandleLoadLayout(GameSession session, IByteReader packet) { HomeLayout? layout; // blueprint load if (slot is 0) { - if (session.StagedItemBlueprint is null) { + if (session.Housing.StagedItemBlueprint is null) { return; } using GameStorage.Request db = session.GameStorage.Context(); - layout = db.GetHomeLayout(session.StagedItemBlueprint.BlueprintUid); + layout = db.GetHomeLayout(session.Housing.StagedItemBlueprint.BlueprintUid); } else { layout = home.Layouts.FirstOrDefault(homeLayout => homeLayout.Id == slot); } @@ -588,7 +588,7 @@ private void HandleLoadLayout(GameSession session, IByteReader packet) { return; } - session.StagedItemBlueprint = null; + session.Housing.StagedItemBlueprint = null; session.Housing.ApplyLayout(plot, layout); } diff --git a/Maple2.Server.Game/Session/GameSession.State.cs b/Maple2.Server.Game/Session/GameSession.State.cs index fbd4178f9..bc19b0cec 100644 --- a/Maple2.Server.Game/Session/GameSession.State.cs +++ b/Maple2.Server.Game/Session/GameSession.State.cs @@ -23,8 +23,6 @@ public partial class GameSession { public Item? StagedScoreItem = null; public bool EnsembleReady = false; - public ItemBlueprint? StagedItemBlueprint = null; - public Item? ChangeAttributesItem = null; public TradeManager? Trade; From 5b19c2001add88e154bae5676b5fca43cd94e561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Thu, 19 Sep 2024 01:58:00 -0300 Subject: [PATCH 08/12] fix migrations --- ...3_AddHomeLayoutsAndCubesTable.Designer.cs} | 2 +- ...0919045503_AddHomeLayoutsAndCubesTable.cs} | 32 +++++++++---------- .../Migrations/Ms2ContextModelSnapshot.cs | 6 ++-- 3 files changed, 20 insertions(+), 20 deletions(-) rename Maple2.Server.World/Migrations/{20240918223130_AddHomeLayoutsAndCubesTable.Designer.cs => 20240919045503_AddHomeLayoutsAndCubesTable.Designer.cs} (99%) rename Maple2.Server.World/Migrations/{20240918223130_AddHomeLayoutsAndCubesTable.cs => 20240919045503_AddHomeLayoutsAndCubesTable.cs} (88%) diff --git a/Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.Designer.cs b/Maple2.Server.World/Migrations/20240919045503_AddHomeLayoutsAndCubesTable.Designer.cs similarity index 99% rename from Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.Designer.cs rename to Maple2.Server.World/Migrations/20240919045503_AddHomeLayoutsAndCubesTable.Designer.cs index e36f78ec2..7a84ee7b6 100644 --- a/Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.Designer.cs +++ b/Maple2.Server.World/Migrations/20240919045503_AddHomeLayoutsAndCubesTable.Designer.cs @@ -11,7 +11,7 @@ namespace Maple2.Server.World.Migrations { [DbContext(typeof(Ms2Context))] - [Migration("20240918223130_AddHomeLayoutsAndCubesTable")] + [Migration("20240919045503_AddHomeLayoutsAndCubesTable")] partial class AddHomeLayoutsAndCubesTable { /// diff --git a/Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.cs b/Maple2.Server.World/Migrations/20240919045503_AddHomeLayoutsAndCubesTable.cs similarity index 88% rename from Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.cs rename to Maple2.Server.World/Migrations/20240919045503_AddHomeLayoutsAndCubesTable.cs index 32fce14a4..ae22a29bd 100644 --- a/Maple2.Server.World/Migrations/20240918223130_AddHomeLayoutsAndCubesTable.cs +++ b/Maple2.Server.World/Migrations/20240919045503_AddHomeLayoutsAndCubesTable.cs @@ -4,11 +4,14 @@ #nullable disable -namespace Maple2.Server.World.Migrations { +namespace Maple2.Server.World.Migrations +{ /// - public partial class AddHomeLayoutsAndCubesTable : Migration { + public partial class AddHomeLayoutsAndCubesTable : Migration + { /// - protected override void Up(MigrationBuilder migrationBuilder) { + protected override void Up(MigrationBuilder migrationBuilder) + { migrationBuilder.AddColumn( name: "Blueprints", table: "home", @@ -17,17 +20,10 @@ protected override void Up(MigrationBuilder migrationBuilder) { nullable: false) .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.AddColumn( - name: "Layouts", - table: "home", - type: "json", - defaultValue: "[]", - nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateTable( name: "home-layout", - columns: table => new { + columns: table => new + { Uid = table.Column(type: "bigint", nullable: false) .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), Id = table.Column(type: "int", nullable: false), @@ -37,14 +33,16 @@ protected override void Up(MigrationBuilder migrationBuilder) { Height = table.Column(type: "tinyint unsigned", nullable: false), Timestamp = table.Column(type: "datetime(6)", nullable: false) }, - constraints: table => { + constraints: table => + { table.PrimaryKey("PK_home-layout", x => x.Uid); }) .Annotation("MySql:CharSet", "utf8mb4"); migrationBuilder.CreateTable( name: "home-layout-cube", - columns: table => new { + columns: table => new + { Id = table.Column(type: "bigint", nullable: false) .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), HomeLayoutId = table.Column(type: "bigint", nullable: false), @@ -56,7 +54,8 @@ protected override void Up(MigrationBuilder migrationBuilder) { Template = table.Column(type: "json", nullable: true) .Annotation("MySql:CharSet", "utf8mb4") }, - constraints: table => { + constraints: table => + { table.PrimaryKey("PK_home-layout-cube", x => x.Id); table.ForeignKey( name: "FK_home-layout-cube_home-layout_HomeLayoutId", @@ -74,7 +73,8 @@ protected override void Up(MigrationBuilder migrationBuilder) { } /// - protected override void Down(MigrationBuilder migrationBuilder) { + protected override void Down(MigrationBuilder migrationBuilder) + { migrationBuilder.DropTable( name: "home-layout-cube"); diff --git a/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs b/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs index 6a441af53..af6acf6e6 100644 --- a/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs +++ b/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs @@ -1505,7 +1505,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.OwnsOne("Maple2.Database.Model.SkillBook", "SkillBook", b1 => + b.OwnsOne("Maple2.Database.Model.CharacterConfig.SkillBook#Maple2.Database.Model.SkillBook", "SkillBook", b1 => { b1.Property("CharacterConfigCharacterId") .HasColumnType("bigint"); @@ -1523,11 +1523,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasIndex("ActiveSkillTabId") .IsUnique(); - b1.ToTable("character-config"); + b1.ToTable("character-config", (string)null); b1.HasOne("Maple2.Database.Model.SkillTab", null) .WithOne() - .HasForeignKey("Maple2.Database.Model.CharacterConfig.SkillBook#Maple2.Database.Model.SkillBook", "ActiveSkillTabId") + .HasForeignKey("Maple2.Database.Model.CharacterConfig.SkillBook#Maple2.Database.Model.CharacterConfig.SkillBook#Maple2.Database.Model.SkillBook", "ActiveSkillTabId") .HasPrincipalKey("Maple2.Database.Model.SkillTab", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); From 7d52b0f97f667e1ef21c970dc283e495fbbbb20d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Thu, 19 Sep 2024 03:47:38 -0300 Subject: [PATCH 09/12] fix: layouts duping cubes --- Maple2.Model/Game/Cube/HeldCube.cs | 2 +- Maple2.Server.Game/Manager/HousingManager.cs | 19 ++++++-- .../Manager/Items/FurnishingManager.cs | 47 +++++++++++-------- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/Maple2.Model/Game/Cube/HeldCube.cs b/Maple2.Model/Game/Cube/HeldCube.cs index 420c072fc..75b5f4dd1 100644 --- a/Maple2.Model/Game/Cube/HeldCube.cs +++ b/Maple2.Model/Game/Cube/HeldCube.cs @@ -7,7 +7,7 @@ namespace Maple2.Model.Game; public class HeldCube : IByteSerializable, IByteDeserializable { public static readonly HeldCube Default = new(); - public long Id { get; protected set; } + public long Id { get; set; } public int ItemId { get; protected set; } public UgcItemLook? Template { get; protected set; } diff --git a/Maple2.Server.Game/Manager/HousingManager.cs b/Maple2.Server.Game/Manager/HousingManager.cs index ba846938d..f7e8d1490 100644 --- a/Maple2.Server.Game/Manager/HousingManager.cs +++ b/Maple2.Server.Game/Manager/HousingManager.cs @@ -307,11 +307,18 @@ public void InitNewHome(string characterName, ExportedUgcMapMetadata? template) List plotCubes = []; foreach (ExportedUgcMapMetadata.Cube cube in template.Cubes) { - PlotCube plotCube = new(cube.ItemId, 0, null) { - Position = template.BaseCubePosition + cube.OffsetPosition, - Rotation = cube.Rotation - }; - + long itemUid = session.Item.Furnishing.AddCube(cube.ItemId); + if (itemUid == 0) { + logger.Error("Failed to add cube {cubeId} to storage.", cube.ItemId); + continue; + } + session.Item.Furnishing.TryPlaceCube(itemUid, out PlotCube? plotCube); + if (plotCube is null) { + logger.Error("Failed to place cube {cubeId}.", cube.ItemId); + continue; + } + plotCube.Position = template.BaseCubePosition + cube.OffsetPosition; + plotCube.Rotation = cube.Rotation; plotCubes.Add(plotCube); } @@ -489,6 +496,8 @@ public void ApplyLayout(Plot plot, HomeLayout layout) { session.Field.Broadcast(CubePacket.UpdateHomeAreaAndHeight(Home.Area, Home.Height)); foreach (PlotCube cube in layout.Cubes) { + Item? item = session.Item.Furnishing.GetItem(cube.ItemId); + cube.Id = item?.Uid ?? 0; if (!TryPlaceCube(cube, plot, cube.Position, cube.Rotation, out PlotCube? plotCube)) { return; } diff --git a/Maple2.Server.Game/Manager/Items/FurnishingManager.cs b/Maple2.Server.Game/Manager/Items/FurnishingManager.cs index 3095cee61..2ccf8076d 100644 --- a/Maple2.Server.Game/Manager/Items/FurnishingManager.cs +++ b/Maple2.Server.Game/Manager/Items/FurnishingManager.cs @@ -215,33 +215,34 @@ public long AddStorage(Item? item) { if (item == null) { return 0; } + lock (session.Item) { + Item? stored = storage.FirstOrDefault(existing => existing.Id == item.Id); + if (stored == null) { + item.Group = ItemGroup.Furnishing; + using GameStorage.Request db = session.GameStorage.Context(); + item = db.CreateItem(session.AccountId, item); + if (item == null || storage.Add(item).Count <= 0) { + return 0; + } - Item? stored = storage.FirstOrDefault(existing => existing.Id == item.Id); - if (stored == null) { - item.Group = ItemGroup.Furnishing; - using GameStorage.Request db = session.GameStorage.Context(); - item = db.CreateItem(session.AccountId, item); - if (item == null || storage.Add(item).Count <= 0) { - return 0; + session.Send(FurnishingStoragePacket.Add(item)); + return item.Uid; } - session.Send(FurnishingStoragePacket.Add(item)); - return item.Uid; - } + if (stored.Amount + amount > item.Metadata.Property.SlotMax) { + return 0; + } - if (stored.Amount + amount > item.Metadata.Property.SlotMax) { - return 0; - } + int previousAmount = stored.Amount; + stored.Amount += amount; + if (previousAmount == 0) { + session.Send(FurnishingStoragePacket.Add(stored)); + return stored.Uid; + } - int previousAmount = stored.Amount; - stored.Amount += amount; - if (previousAmount == 0) { - session.Send(FurnishingStoragePacket.Add(stored)); + session.Send(FurnishingStoragePacket.Update(stored.Uid, stored.Amount)); return stored.Uid; } - - session.Send(FurnishingStoragePacket.Update(stored.Uid, stored.Amount)); - return stored.Uid; } private bool AddInventory(PlotCube cube) { @@ -268,6 +269,12 @@ public void SendStorageCount() { } } + public Item? GetItemById(int itemId) { + lock (session.Item) { + return storage.FirstOrDefault(item => item.Id == itemId); + } + } + public void Save(GameStorage.Request db) { lock (session.Item) { db.SaveItems(session.AccountId, storage.ToArray()); From d34c06f3d8be65a4b4a6e81c40060e41bed8b7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Thu, 19 Sep 2024 05:32:38 -0300 Subject: [PATCH 10/12] . --- .editorconfig | 10 +++++----- Maple2.Server.Game/Manager/HousingManager.cs | 9 +++------ Maple2.Server.Game/Manager/Items/FurnishingManager.cs | 8 +------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/.editorconfig b/.editorconfig index 2d5d82674..98b15d10e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -65,7 +65,7 @@ dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * -dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none @@ -107,7 +107,7 @@ resharper_csharp_wrap_ternary_expr_style = wrap_if_long resharper_formatter_off_tag = @formatter:off resharper_formatter_on_tag = @formatter:on resharper_formatter_tags_enabled = true -resharper_instance_members_qualify_declared_in = +resharper_instance_members_qualify_declared_in = resharper_keep_existing_attribute_arrangement = true resharper_keep_existing_declaration_block_arrangement = true resharper_keep_existing_embedded_block_arrangement = true @@ -115,7 +115,7 @@ resharper_keep_existing_enum_arrangement = true resharper_keep_existing_initializer_arrangement = false resharper_modifiers_order = protected public override private new internal static async virtual sealed abstract extern unsafe volatile readonly required resharper_outdent_statement_labels = true -resharper_parentheses_non_obvious_operations = none, bitwise_and, bitwise_exclusive_or, bitwise_inclusive_or, bitwise, conditional_and +resharper_parentheses_non_obvious_operations = none, bitwise_and, bitwise_exclusive_or, bitwise_inclusive_or, bitwise resharper_parentheses_redundancy_style = remove resharper_place_expr_accessor_on_single_line = true resharper_place_expr_method_on_single_line = true @@ -124,8 +124,8 @@ resharper_place_field_attribute_on_same_line = if_owner_is_single_line resharper_place_record_field_attribute_on_same_line = true resharper_place_simple_anonymousmethod_on_single_line = false resharper_place_simple_embedded_statement_on_same_line = true -resharper_place_simple_initializer_on_single_line = false -resharper_space_within_single_line_array_initializer_braces = false +resharper_place_simple_initializer_on_single_line = true +resharper_space_within_single_line_array_initializer_braces = true resharper_trailing_comma_in_multiline_lists = true resharper_use_indent_from_vs = false resharper_wrap_array_initializer_style = chop_always diff --git a/Maple2.Server.Game/Manager/HousingManager.cs b/Maple2.Server.Game/Manager/HousingManager.cs index f7e8d1490..f21e6f926 100644 --- a/Maple2.Server.Game/Manager/HousingManager.cs +++ b/Maple2.Server.Game/Manager/HousingManager.cs @@ -329,7 +329,7 @@ public void InitNewHome(string characterName, ExportedUgcMapMetadata? template) #region Helpers public bool TryPlaceCube(HeldCube cube, Plot plot, in Vector3B position, float rotation, - [NotNullWhen(true)] out PlotCube? result, bool isReplace = false) { + [NotNullWhen(true)] out PlotCube? result, bool isReplace = false) { result = null; if (!session.ItemMetadata.TryGet(cube.ItemId, out ItemMetadata? itemMetadata) || itemMetadata.Install is null) { logger.Error("Failed to get item metadata for cube {cubeId}.", cube.ItemId); @@ -450,11 +450,8 @@ public void RequestLayout(HomeLayout layout) { .ToDictionary(grouping => grouping.Key, grouping => grouping.Count()); // Dictionary int cubeCount = 0; Dictionary cubeCosts = new() { - { - FurnishingCurrencyType.Meso, 0 - }, { - FurnishingCurrencyType.Meret, 0 - }, + { FurnishingCurrencyType.Meso, 0 }, + { FurnishingCurrencyType.Meret, 0 }, }; foreach ((int id, int amount) in groupedCubes) { diff --git a/Maple2.Server.Game/Manager/Items/FurnishingManager.cs b/Maple2.Server.Game/Manager/Items/FurnishingManager.cs index 2ccf8076d..f8bf18028 100644 --- a/Maple2.Server.Game/Manager/Items/FurnishingManager.cs +++ b/Maple2.Server.Game/Manager/Items/FurnishingManager.cs @@ -66,7 +66,7 @@ public void Load() { public Item? GetCube(long itemUid) { lock (session.Item) { - return storage.FirstOrDefault(item => item.Uid == itemUid); + return storage.Get(itemUid); } } @@ -269,12 +269,6 @@ public void SendStorageCount() { } } - public Item? GetItemById(int itemId) { - lock (session.Item) { - return storage.FirstOrDefault(item => item.Id == itemId); - } - } - public void Save(GameStorage.Request db) { lock (session.Item) { db.SaveItems(session.AccountId, storage.ToArray()); From f6e4ee8c055217e2a3b249b3ed1e1574e6c90491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 21 Sep 2024 15:54:06 -0300 Subject: [PATCH 11/12] suggestions --- Maple2.Database/Model/Item/ItemSubType.cs | 7 ++++--- Maple2.Model/Enum/BlueprintType.cs | 6 ++++++ Maple2.Model/Game/Item/ItemBlueprint.cs | 13 +++++-------- Maple2.Model/Metadata/Constants.cs | 1 + .../PacketHandlers/RequestCubeHandler.cs | 19 ++++++++++++++++--- Maple2.Server.Game/Packets/CubePacket.cs | 2 +- 6 files changed, 33 insertions(+), 15 deletions(-) create mode 100644 Maple2.Model/Enum/BlueprintType.cs diff --git a/Maple2.Database/Model/Item/ItemSubType.cs b/Maple2.Database/Model/Item/ItemSubType.cs index 59d4b94a1..a63a2e9aa 100644 --- a/Maple2.Database/Model/Item/ItemSubType.cs +++ b/Maple2.Database/Model/Item/ItemSubType.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; +using Maple2.Model.Enum; namespace Maple2.Database.Model; @@ -48,13 +49,13 @@ internal record ItemBlueprint( int Width, int Height, DateTimeOffset CreationTime, - int Unknown, + int Type, long AccountId, long CharacterId, string CharacterName) { [return: NotNullIfNotNull(nameof(other))] public static implicit operator ItemBlueprint?(Maple2.Model.Game.ItemBlueprint? other) { - return other == null ? null : new ItemBlueprint(other.BlueprintUid, other.Length, other.Width, other.Height, other.CreationTime, other.Unknown, other.AccountId, other.CharacterId, other.CharacterName); + return other == null ? null : new ItemBlueprint(other.BlueprintUid, other.Length, other.Width, other.Height, other.CreationTime, (int) other.Type, other.AccountId, other.CharacterId, other.CharacterName); } [return: NotNullIfNotNull(nameof(other))] @@ -65,7 +66,7 @@ internal record ItemBlueprint( Width = other.Width, Height = other.Height, CreationTime = other.CreationTime, - Unknown = other.Unknown, + Type = (BlueprintType) other.Type, AccountId = other.AccountId, CharacterId = other.CharacterId, CharacterName = other.CharacterName, diff --git a/Maple2.Model/Enum/BlueprintType.cs b/Maple2.Model/Enum/BlueprintType.cs new file mode 100644 index 000000000..164fd4bad --- /dev/null +++ b/Maple2.Model/Enum/BlueprintType.cs @@ -0,0 +1,6 @@ +namespace Maple2.Model.Enum; + +public enum BlueprintType { + Copy = 0, + Original = 1, +} diff --git a/Maple2.Model/Game/Item/ItemBlueprint.cs b/Maple2.Model/Game/Item/ItemBlueprint.cs index 135f4dee0..350de5e64 100644 --- a/Maple2.Model/Game/Item/ItemBlueprint.cs +++ b/Maple2.Model/Game/Item/ItemBlueprint.cs @@ -1,4 +1,5 @@ -using Maple2.PacketLib.Tools; +using Maple2.Model.Enum; +using Maple2.PacketLib.Tools; using Maple2.Tools; namespace Maple2.Model.Game; @@ -9,15 +10,11 @@ public sealed class ItemBlueprint : IByteSerializable, IByteDeserializable { public int Width; public int Height; public DateTimeOffset CreationTime; - public int Unknown; + public BlueprintType Type = BlueprintType.Original; public long AccountId; public long CharacterId; public string CharacterName = ""; - public ItemBlueprint() { - Unknown = 1; - } - public ItemBlueprint Clone() { return (ItemBlueprint) MemberwiseClone(); } @@ -28,7 +25,7 @@ public void WriteTo(IByteWriter writer) { writer.WriteInt(Width); writer.WriteInt(Height); writer.WriteLong(CreationTime.ToUnixTimeSeconds()); - writer.WriteInt(Unknown); + writer.Write(Type); writer.WriteLong(AccountId); writer.WriteLong(CharacterId); writer.WriteUnicodeString(CharacterName); @@ -40,7 +37,7 @@ public void ReadFrom(IByteReader reader) { Width = reader.ReadInt(); Height = reader.ReadInt(); CreationTime = DateTimeOffset.FromUnixTimeSeconds(reader.ReadLong()); - Unknown = reader.ReadInt(); + Type = (BlueprintType) reader.ReadInt(); AccountId = reader.ReadLong(); CharacterId = reader.ReadLong(); CharacterName = reader.ReadUnicodeString(); diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index 67077bd99..83db2994a 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -98,6 +98,7 @@ public static class Constant { public const string DefaultAiPath = "AI_Default.xml"; public const int GuildCoinId = 30000861; public const int GuildCoinRarity = 4; + public const int BlueprintId = 35200000; public const long FurnishingBaseId = 2870000000000000000; diff --git a/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs b/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs index 6cd214896..2b474a796 100644 --- a/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs @@ -548,6 +548,8 @@ private void HandleSaveLayout(GameSession session, IByteReader packet) { byte height = home.IsPlanner ? home.PlannerHeight : home.Height; layout = db.SaveHomeLayout(new HomeLayout(slot, name, area, height, DateTimeOffset.Now, plot.Cubes.Values.ToList())); if (layout is null) { + Logger.Error("Failed to save layout for {AccountId}", session.AccountId); + session.Send(CubePacket.Error(UgcMapError.s_ugcmap_db)); return; } home.Layouts.Add(layout); @@ -622,15 +624,22 @@ private void HandleCreateBlueprint(GameSession session) { return; } - int negAmount = -200; + if (!TableMetadata.UgcDesignTable.Entries.TryGetValue(Constant.BlueprintId, out UgcDesignTable.Entry? design)) { + Logger.Error("Failed to find design {BlueprintId}", Constant.BlueprintId); + return; + } + + long blueprintCost = design.CreatePrice; + + long negAmount = -1 * blueprintCost; if (session.Currency.CanAddMeret(negAmount) != negAmount) { session.Send(CubePacket.Error(UgcMapError.s_err_ugcmap_not_enough_meso_balance)); return; } - session.Currency.Meret -= 200; + session.Currency.Meret -= blueprintCost; - Item? item = session.Field.ItemDrop.CreateItem(35200000); + Item? item = session.Field.ItemDrop.CreateItem(Constant.BlueprintId); if (item is null) { return; } @@ -642,6 +651,8 @@ private void HandleCreateBlueprint(GameSession session) { HomeLayout? layout = db.SaveHomeLayout(new HomeLayout(0, "Blueprint", area, height, DateTimeOffset.Now, plot.Cubes.Values.ToList())); if (layout is null) { + Logger.Error("Failed to save layout for {AccountId}", session.AccountId); + session.Send(CubePacket.Error(UgcMapError.s_ugcmap_db)); return; } @@ -693,6 +704,8 @@ private void HandleSaveBlueprint(GameSession session, IByteReader packet) { byte height = home.PlannerHeight; layout = db.SaveHomeLayout(new HomeLayout(slot, name, area, height, DateTimeOffset.Now, plot.Cubes.Values.ToList())); if (layout is null) { + Logger.Error("Failed to save layout for {AccountId}", session.AccountId); + session.Send(CubePacket.Error(UgcMapError.s_ugcmap_db)); return; } home.Blueprints.Add(layout); diff --git a/Maple2.Server.Game/Packets/CubePacket.cs b/Maple2.Server.Game/Packets/CubePacket.cs index da4760b47..0b2199a88 100644 --- a/Maple2.Server.Game/Packets/CubePacket.cs +++ b/Maple2.Server.Game/Packets/CubePacket.cs @@ -493,7 +493,7 @@ public static ByteWriter UpdateHomeAreaAndHeight(byte area, byte height) { public static ByteWriter CreateBlueprint(long itemUid, ItemBlueprint blueprint) { var pWriter = Packet.Of(SendOp.ResponseCube); pWriter.Write(Command.CreateBlueprint); - pWriter.WriteByte(1); + pWriter.Write(UgcMapError.s_empty_string); pWriter.WriteLong(itemUid); pWriter.WriteClass(blueprint); From 442168da11e15f9565ee20083a3fbb93cd1da9ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 21 Sep 2024 16:30:24 -0300 Subject: [PATCH 12/12] save home properties & load when its blueprint --- Maple2.Database/Model/Map/HomeLayout.cs | 13 +- Maple2.Model/Enum/StringCode.cs | 2 + Maple2.Model/Game/User/Home.cs | 3 + Maple2.Server.Game/Manager/HousingManager.cs | 27 +- .../PacketHandlers/ItemUseHandler.cs | 5 +- .../PacketHandlers/RequestCubeHandler.cs | 31 +- ...307_AddHomePropertiesToLayouts.Designer.cs | 1683 +++++++++++++++++ ...240921185307_AddHomePropertiesToLayouts.cs | 51 + .../Migrations/Ms2ContextModelSnapshot.cs | 15 +- 9 files changed, 1816 insertions(+), 14 deletions(-) create mode 100644 Maple2.Server.World/Migrations/20240921185307_AddHomePropertiesToLayouts.Designer.cs create mode 100644 Maple2.Server.World/Migrations/20240921185307_AddHomePropertiesToLayouts.cs diff --git a/Maple2.Database/Model/Map/HomeLayout.cs b/Maple2.Database/Model/Map/HomeLayout.cs index 65649d65a..4125e1796 100644 --- a/Maple2.Database/Model/Map/HomeLayout.cs +++ b/Maple2.Database/Model/Map/HomeLayout.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Maple2.Model.Enum; using Maple2.Model.Game; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -11,6 +12,9 @@ internal class HomeLayout { public string Name { get; set; } public byte Area { get; set; } public byte Height { get; set; } + public HomeBackground Background { get; set; } + public HomeLighting Lighting { get; set; } + public HomeCamera Camera { get; set; } public DateTimeOffset Timestamp { get; set; } public List Cubes { get; set; } = null!; @@ -24,6 +28,9 @@ internal class HomeLayout { Height = other.Height, Timestamp = other.Timestamp, Cubes = other.Cubes.ConvertAll(cube => (HomeLayoutCube) cube), + Background = other.Background, + Lighting = other.Lighting, + Camera = other.Camera, }; } @@ -33,7 +40,11 @@ internal class HomeLayout { return null; } - return new Maple2.Model.Game.HomeLayout(other.Uid, other.Id, other.Name, other.Area, other.Height, other.Timestamp, other.Cubes.ConvertAll(cube => (PlotCube) cube)); + return new Maple2.Model.Game.HomeLayout(other.Uid, other.Id, other.Name, other.Area, other.Height, other.Timestamp, other.Cubes.ConvertAll(cube => (PlotCube) cube)) { + Background = other.Background, + Lighting = other.Lighting, + Camera = other.Camera, + }; } public static void Configure(EntityTypeBuilder builder) { diff --git a/Maple2.Model/Enum/StringCode.cs b/Maple2.Model/Enum/StringCode.cs index 0d65de012..7c5cb4ba4 100644 --- a/Maple2.Model/Enum/StringCode.cs +++ b/Maple2.Model/Enum/StringCode.cs @@ -1797,6 +1797,7 @@ public enum StringCode { s_ugcmap_height_level_extended_successfully = 1793, s_ugcmap_area_level_shrink_successfully = 1794, s_ugcmap_height_level_shrink_successfully = 1795, + [Description("You can only use a blueprint while in your home.")] s_ugcmap_not_use_blueprint_item = 1796, s_ugc_edit_homeless = 1797, s_ugc_edit_different_indoorsize = 1798, @@ -2984,6 +2985,7 @@ public enum StringCode { s_function_cube_error_invalid_summon_user = 2981, s_err_ugcmap_package_cant_use = 2982, s_err_ugcmap_package_cant_use_in_this_map = 2983, + [Description("Can only be used in the indoor space of the house.")] s_err_ugcmap_package_should_use_in_indoor = 2984, s_err_ugcmap_package_not_a_valid_package_item = 2985, s_err_ugcmap_package_cant_use_in_others_home = 2986, diff --git a/Maple2.Model/Game/User/Home.cs b/Maple2.Model/Game/User/Home.cs index d6f416a0d..2109264e4 100644 --- a/Maple2.Model/Game/User/Home.cs +++ b/Maple2.Model/Game/User/Home.cs @@ -188,6 +188,9 @@ public class HomeLayout : IByteSerializable { public string Name { get; private set; } public byte Area { get; private set; } public byte Height { get; private set; } + public HomeBackground Background { get; init; } + public HomeLighting Lighting { get; init; } + public HomeCamera Camera { get; init; } public DateTimeOffset Timestamp { get; private set; } public List Cubes { get; set; } diff --git a/Maple2.Server.Game/Manager/HousingManager.cs b/Maple2.Server.Game/Manager/HousingManager.cs index f21e6f926..d90f2db09 100644 --- a/Maple2.Server.Game/Manager/HousingManager.cs +++ b/Maple2.Server.Game/Manager/HousingManager.cs @@ -123,6 +123,17 @@ public bool SavePlots() { return plot; } + public Plot? GetIndoorPlot() { + if (session.Field == null) { + return null; + } + + if (session.AccountId != session.Field.OwnerId || session.Field.MapId != Home.Indoor.MapId) return null; + + session.Field.Plots.TryGetValue(Home.Indoor.Number, out Plot? plot); + return plot; + } + public bool SaveFieldPlot(int number) { if (session.Field?.Plots.TryGetValue(number, out Plot? plot) != true) { return false; @@ -481,7 +492,7 @@ public void RequestLayout(HomeLayout layout) { session.Send(CubePacket.BuyCubes(cubeCosts, cubeCount)); } - public void ApplyLayout(Plot plot, HomeLayout layout) { + public void ApplyLayout(Plot plot, HomeLayout layout, bool isBlueprint = false) { if (plot.IsPlanner) { Home.SetPlannerArea(layout.Area); Home.SetPlannerHeight(layout.Height); @@ -491,6 +502,20 @@ public void ApplyLayout(Plot plot, HomeLayout layout) { } session.Field.Broadcast(CubePacket.UpdateHomeAreaAndHeight(Home.Area, Home.Height)); + if (isBlueprint) { + if (session.Player.Value.Home.SetBackground(layout.Background)) { + session.Field.Broadcast(CubePacket.SetBackground(layout.Background)); + } + + if (session.Player.Value.Home.SetLighting(layout.Lighting)) { + session.Field.Broadcast(CubePacket.SetLighting(layout.Lighting)); + } + + if (session.Player.Value.Home.SetCamera(layout.Camera)) { + session.Field.Broadcast(CubePacket.SetCamera(layout.Camera)); + } + } + foreach (PlotCube cube in layout.Cubes) { Item? item = session.Item.Furnishing.GetItem(cube.ItemId); diff --git a/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs b/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs index 9e87068af..f471f1bf9 100644 --- a/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ItemUseHandler.cs @@ -111,8 +111,9 @@ private void HandleBlueprintImport(GameSession session, Item item) { return; } - Plot? plot = session.Housing.GetFieldPlot(); - if (plot == null) { + Plot? plot = session.Housing.GetIndoorPlot(); + if (plot is null) { + session.Send(NoticePacket.Message(StringCode.s_ugcmap_not_use_blueprint_item, NoticePacket.Flags.Message | NoticePacket.Flags.Alert)); return; } diff --git a/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs b/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs index 2b474a796..5ed281cd6 100644 --- a/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/RequestCubeHandler.cs @@ -583,15 +583,22 @@ private void HandleLoadLayout(GameSession session, IByteReader packet) { using GameStorage.Request db = session.GameStorage.Context(); layout = db.GetHomeLayout(session.Housing.StagedItemBlueprint.BlueprintUid); + if (layout is null) { + Logger.Error("Failed to load layout for {AccountId}", session.AccountId); + session.Send(CubePacket.Error(UgcMapError.s_ugcmap_db)); + return; + } } else { layout = home.Layouts.FirstOrDefault(homeLayout => homeLayout.Id == slot); - } - if (layout is null) { - return; + if (layout is null) { + Logger.Error("Failed to find layout {Slot} for {AccountId}", slot, session.AccountId); + session.Send(CubePacket.Error(UgcMapError.s_ugcmap_db)); + return; + } } session.Housing.StagedItemBlueprint = null; - session.Housing.ApplyLayout(plot, layout); + session.Housing.ApplyLayout(plot, layout, isBlueprint: slot is 0); } @@ -649,7 +656,12 @@ private void HandleCreateBlueprint(GameSession session) { byte height = home.PlannerHeight; using GameStorage.Request db = session.GameStorage.Context(); - HomeLayout? layout = db.SaveHomeLayout(new HomeLayout(0, "Blueprint", area, height, DateTimeOffset.Now, plot.Cubes.Values.ToList())); + var homeLayout = new HomeLayout(0, "Blueprint", area, height, DateTimeOffset.Now, plot.Cubes.Values.ToList()) { + Background = home.Background, + Lighting = home.Lighting, + Camera = home.Camera, + }; + HomeLayout? layout = db.SaveHomeLayout(homeLayout); if (layout is null) { Logger.Error("Failed to save layout for {AccountId}", session.AccountId); session.Send(CubePacket.Error(UgcMapError.s_ugcmap_db)); @@ -702,7 +714,12 @@ private void HandleSaveBlueprint(GameSession session, IByteReader packet) { byte area = home.PlannerArea; byte height = home.PlannerHeight; - layout = db.SaveHomeLayout(new HomeLayout(slot, name, area, height, DateTimeOffset.Now, plot.Cubes.Values.ToList())); + var homeLayout = new HomeLayout(slot, name, area, height, DateTimeOffset.Now, plot.Cubes.Values.ToList()) { + Background = home.Background, + Lighting = home.Lighting, + Camera = home.Camera, + }; + layout = db.SaveHomeLayout(homeLayout); if (layout is null) { Logger.Error("Failed to save layout for {AccountId}", session.AccountId); session.Send(CubePacket.Error(UgcMapError.s_ugcmap_db)); @@ -734,6 +751,6 @@ private void HandleLoadBlueprint(GameSession session, IByteReader packet) { return; } - session.Housing.ApplyLayout(plot, layout); + session.Housing.ApplyLayout(plot, layout, isBlueprint: true); } } diff --git a/Maple2.Server.World/Migrations/20240921185307_AddHomePropertiesToLayouts.Designer.cs b/Maple2.Server.World/Migrations/20240921185307_AddHomePropertiesToLayouts.Designer.cs new file mode 100644 index 000000000..472894143 --- /dev/null +++ b/Maple2.Server.World/Migrations/20240921185307_AddHomePropertiesToLayouts.Designer.cs @@ -0,0 +1,1683 @@ +// +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("20240921185307_AddHomePropertiesToLayouts")] + partial class AddHomePropertiesToLayouts + { + /// + 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("HomeLayoutId") + .HasColumnType("bigint"); + + 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.HasKey("ReceiverId", "Id"); + + b.ToTable("mail", (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("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("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.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.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.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/20240921185307_AddHomePropertiesToLayouts.cs b/Maple2.Server.World/Migrations/20240921185307_AddHomePropertiesToLayouts.cs new file mode 100644 index 000000000..565d0bbc9 --- /dev/null +++ b/Maple2.Server.World/Migrations/20240921185307_AddHomePropertiesToLayouts.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Maple2.Server.World.Migrations +{ + /// + public partial class AddHomePropertiesToLayouts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Background", + table: "home-layout", + type: "tinyint unsigned", + nullable: false, + defaultValue: (byte)0); + + migrationBuilder.AddColumn( + name: "Camera", + table: "home-layout", + type: "tinyint unsigned", + nullable: false, + defaultValue: (byte)0); + + migrationBuilder.AddColumn( + name: "Lighting", + table: "home-layout", + type: "tinyint unsigned", + nullable: false, + defaultValue: (byte)0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Background", + table: "home-layout"); + + migrationBuilder.DropColumn( + name: "Camera", + table: "home-layout"); + + migrationBuilder.DropColumn( + name: "Lighting", + table: "home-layout"); + } + } +} diff --git a/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs b/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs index b0f0d9b4d..fcf6fd532 100644 --- a/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs +++ b/Maple2.Server.World/Migrations/Ms2ContextModelSnapshot.cs @@ -712,12 +712,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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"); @@ -1435,7 +1444,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.OwnsOne("Maple2.Database.Model.CharacterConfig.SkillBook#Maple2.Database.Model.SkillBook", "SkillBook", b1 => + b.OwnsOne("Maple2.Database.Model.SkillBook", "SkillBook", b1 => { b1.Property("CharacterConfigCharacterId") .HasColumnType("bigint"); @@ -1453,11 +1462,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasIndex("ActiveSkillTabId") .IsUnique(); - b1.ToTable("character-config", (string)null); + b1.ToTable("character-config"); b1.HasOne("Maple2.Database.Model.SkillTab", null) .WithOne() - .HasForeignKey("Maple2.Database.Model.CharacterConfig.SkillBook#Maple2.Database.Model.CharacterConfig.SkillBook#Maple2.Database.Model.SkillBook", "ActiveSkillTabId") + .HasForeignKey("Maple2.Database.Model.CharacterConfig.SkillBook#Maple2.Database.Model.SkillBook", "ActiveSkillTabId") .HasPrincipalKey("Maple2.Database.Model.SkillTab", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired();