From f3750c93050b5554e64e02027da475ab90e43188 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Sat, 26 Sep 2026 16:33:38 +0200 Subject: [PATCH 1/7] feat(items): add item rule flags to the item definition Adds IsTradable, IsDroppable, IsStorable, IsSellableToNpc, IsPersonalStoreSellable and IsRepairable to ItemDefinition. They are true unless set otherwise, and the migration adds them with the default true, so existing items keep allowing everything. The admin panel shows them as checkboxes on the item pages. They match the rule flags of the MuMain client item data (tradable, droppable, storable, sellable, personalShopSellable, repairable), so both sides can allow the same actions. --- .../Configuration/Items/ItemDefinition.cs | 30 + ...0260926142218_AddItemRuleFlags.Designer.cs | 5944 +++++++++++++++++ .../20260926142218_AddItemRuleFlags.cs | 99 + .../EntityDataContextModelSnapshot.cs | 18 + 4 files changed, 6091 insertions(+) create mode 100644 src/Persistence/EntityFramework/Migrations/20260926142218_AddItemRuleFlags.Designer.cs create mode 100644 src/Persistence/EntityFramework/Migrations/20260926142218_AddItemRuleFlags.cs diff --git a/src/DataModel/Configuration/Items/ItemDefinition.cs b/src/DataModel/Configuration/Items/ItemDefinition.cs index 79a8c7af92..7bb8e56e05 100644 --- a/src/DataModel/Configuration/Items/ItemDefinition.cs +++ b/src/DataModel/Configuration/Items/ItemDefinition.cs @@ -66,6 +66,36 @@ public partial class ItemDefinition /// public int StorageLimitPerCharacter { get; set; } + /// + /// Gets or sets a value indicating whether items of this kind can be put into a trade with another player. + /// + public bool IsTradable { get; set; } = true; + + /// + /// Gets or sets a value indicating whether items of this kind can be dropped to the ground. + /// + public bool IsDroppable { get; set; } = true; + + /// + /// Gets or sets a value indicating whether items of this kind can be stored in the vault. + /// + public bool IsStorable { get; set; } = true; + + /// + /// Gets or sets a value indicating whether items of this kind can be sold to an NPC merchant. + /// + public bool IsSellableToNpc { get; set; } = true; + + /// + /// Gets or sets a value indicating whether items of this kind can be offered in a personal store. + /// + public bool IsPersonalStoreSellable { get; set; } = true; + + /// + /// Gets or sets a value indicating whether items of this kind can be repaired. + /// + public bool IsRepairable { get; set; } = true; + /// /// Gets or sets the name of the item. /// diff --git a/src/Persistence/EntityFramework/Migrations/20260926142218_AddItemRuleFlags.Designer.cs b/src/Persistence/EntityFramework/Migrations/20260926142218_AddItemRuleFlags.Designer.cs new file mode 100644 index 0000000000..ba0a41d65e --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20260926142218_AddItemRuleFlags.Designer.cs @@ -0,0 +1,5944 @@ +// +using System; +using MUnique.OpenMU.Persistence.EntityFramework; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations +{ + [DbContext(typeof(EntityDataContext))] + [Migration("20260926142218_AddItemRuleFlags")] + partial class AddItemRuleFlags + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatBanUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("EMail") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsBot") + .HasColumnType("boolean"); + + b.Property("IsNetworkObservationActive") + .HasColumnType("boolean"); + + b.Property("IsTemplate") + .HasColumnType("boolean"); + + b.Property("IsVaultExtended") + .HasColumnType("boolean"); + + b.Property("LanguageIsoCode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("character varying(3)") + .HasDefaultValue("en"); + + b.Property("LoginName") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegistrationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("TimeZone") + .HasColumnType("smallint"); + + b.Property("VaultId") + .HasColumnType("uuid"); + + b.Property("VaultPassword") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("LoginName") + .IsUnique(); + + b.HasIndex("VaultId") + .IsUnique(); + + b.ToTable("Account", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AccountCharacterClass", b => + { + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.HasKey("AccountId", "CharacterClassId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("AccountCharacterClass", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("FullAncientSetEquipped") + .HasColumnType("boolean"); + + b.Property("Pose") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("AppearanceData", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AreaSkillSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DelayBetweenHits") + .HasColumnType("interval"); + + b.Property("DelayPerOneDistance") + .HasColumnType("interval"); + + b.Property("EffectRange") + .HasColumnType("integer"); + + b.Property("FrustumDistance") + .HasColumnType("real"); + + b.Property("FrustumEndWidth") + .HasColumnType("real"); + + b.Property("FrustumStartWidth") + .HasColumnType("real"); + + b.Property("HitChancePerDistanceMultiplier") + .HasColumnType("real"); + + b.Property("MaximumNumberOfHitsPerAttack") + .HasColumnType("integer"); + + b.Property("MaximumNumberOfHitsPerTarget") + .HasColumnType("integer"); + + b.Property("MinimumNumberOfHitsPerAttack") + .HasColumnType("integer"); + + b.Property("MinimumNumberOfHitsPerTarget") + .HasColumnType("integer"); + + b.Property("ProjectileCount") + .HasColumnType("integer"); + + b.Property("TargetAreaDiameter") + .HasColumnType("real"); + + b.Property("UseDeferredHits") + .HasColumnType("boolean"); + + b.Property("UseFrustumFilter") + .HasColumnType("boolean"); + + b.Property("UseTargetAreaFilter") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("AreaSkillSettings", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Designation") + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MaximumValue") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("AttributeDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRelationship", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("InputAttributeId") + .HasColumnType("uuid"); + + b.Property("InputOperand") + .HasColumnType("real"); + + b.Property("InputOperator") + .HasColumnType("integer"); + + b.Property("OperandAttributeId") + .HasColumnType("uuid"); + + b.Property("PowerUpDefinitionValueId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterClassId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("InputAttributeId"); + + b.HasIndex("OperandAttributeId"); + + b.HasIndex("PowerUpDefinitionValueId"); + + b.HasIndex("SkillId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("AttributeRelationship", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeId") + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("MinimumValue") + .HasColumnType("integer"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("SkillId1") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AttributeId"); + + b.HasIndex("GameMapDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("SkillId"); + + b.HasIndex("SkillId1"); + + b.ToTable("AttributeRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GroundId") + .HasColumnType("uuid"); + + b.Property("LeftGoalId") + .HasColumnType("uuid"); + + b.Property("LeftTeamSpawnPointX") + .HasColumnType("smallint"); + + b.Property("LeftTeamSpawnPointY") + .HasColumnType("smallint"); + + b.Property("RightGoalId") + .HasColumnType("uuid"); + + b.Property("RightTeamSpawnPointX") + .HasColumnType("smallint"); + + b.Property("RightTeamSpawnPointY") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GroundId") + .IsUnique(); + + b.HasIndex("LeftGoalId") + .IsUnique(); + + b.HasIndex("RightGoalId") + .IsUnique(); + + b.ToTable("BattleZoneDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MagicEffectDefinitionId") + .HasColumnType("uuid"); + + b.Property("MaximumLevel") + .HasColumnType("integer"); + + b.Property("MinimumLevel") + .HasColumnType("integer"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MagicEffectDefinitionId") + .IsUnique(); + + b.HasIndex("MonsterDefinitionId"); + + b.ToTable("Buff", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttackRespawnAreaId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("CrownHoldTimeSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30); + + b.Property("DefenseRespawnAreaId") + .HasColumnType("uuid"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("GateBuyPrice") + .HasColumnType("integer"); + + b.Property("GateRepairCostPerHealthPoint") + .HasColumnType("integer"); + + b.Property("GuildScoreCastleSiege") + .HasColumnType("integer"); + + b.Property("GuildScoreCastleSiegeMembers") + .HasColumnType("integer"); + + b.Property("LandOfTrialsMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("MaxAttackingGuilds") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(3); + + b.Property("ParticipantRewardMinSeconds") + .HasColumnType("integer"); + + b.Property("RegisterMinLevel") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(200); + + b.Property("RegisterMinMembers") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(20); + + b.Property("RepairCostPerUpgradeLevel") + .HasColumnType("integer"); + + b.Property("RewardItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("SignOfLordItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("SignOfLordItemLevel") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)3); + + b.Property("StatueBuyPrice") + .HasColumnType("integer"); + + b.Property("StatueRepairCostPerHealthPoint") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AttackRespawnAreaId") + .IsUnique(); + + b.HasIndex("CastleSiegeMapDefinitionId"); + + b.HasIndex("DefenseRespawnAreaId") + .IsUnique(); + + b.HasIndex("LandOfTrialsMapDefinitionId"); + + b.HasIndex("RewardItemDefinitionId"); + + b.HasIndex("SignOfLordItemDefinitionId"); + + b.ToTable("CastleSiegeConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsHuntZoneEnabled") + .HasColumnType("boolean"); + + b.Property("IsOccupied") + .HasColumnType("boolean"); + + b.Property("OwnerGuildId") + .HasColumnType("uuid"); + + b.Property("TaxChaos") + .HasColumnType("smallint"); + + b.Property("TaxHunt") + .HasColumnType("integer"); + + b.Property("TaxStore") + .HasColumnType("smallint"); + + b.Property("TributeMoney") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OwnerGuildId"); + + b.ToTable("CastleSiegeData", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeDataId") + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("GuildName") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("IsAllianceMaster") + .HasColumnType("boolean"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("Side") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeDataId"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("CastleSiegeGuild", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuildRegistration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("GuildName") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("Marks") + .HasColumnType("integer"); + + b.Property("RegistrationOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("CastleSiegeGuildRegistration", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("DefaultSide") + .HasColumnType("smallint"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("smallint"); + + b.Property("IsPersistedToDatabase") + .HasColumnType("boolean"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("SpawnX") + .HasColumnType("smallint"); + + b.Property("SpawnY") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.HasIndex("MonsterDefinitionId", "InstanceId"); + + b.ToTable("CastleSiegeNpcDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeDataId") + .HasColumnType("uuid"); + + b.Property("CurrentHp") + .HasColumnType("integer"); + + b.Property("DefenseLevel") + .HasColumnType("smallint"); + + b.Property("InstanceId") + .HasColumnType("smallint"); + + b.Property("LifeLevel") + .HasColumnType("smallint"); + + b.Property("MonsterNumber") + .HasColumnType("smallint"); + + b.Property("RegenLevel") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeDataId"); + + b.HasIndex("MonsterNumber", "InstanceId") + .IsUnique(); + + b.ToTable("CastleSiegeNpcState", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegePendingReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("CastleSiegePendingReward", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeStateScheduleEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("Hour") + .HasColumnType("smallint"); + + b.Property("Minute") + .HasColumnType("smallint"); + + b.Property("State") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.ToTable("CastleSiegeStateScheduleEntry", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeUpgradeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId1") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId2") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId3") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId4") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("RequiredJewelOfGuardianCount") + .HasColumnType("integer"); + + b.Property("RequiredZen") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.HasIndex("CastleSiegeConfigurationId1"); + + b.HasIndex("CastleSiegeConfigurationId2"); + + b.HasIndex("CastleSiegeConfigurationId3"); + + b.HasIndex("CastleSiegeConfigurationId4"); + + b.ToTable("CastleSiegeUpgradeDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("CastleSiegeConfigurationId1") + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId"); + + b.HasIndex("CastleSiegeConfigurationId1"); + + b.ToTable("CastleSiegeZoneDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("CharacterSlot") + .HasColumnType("smallint"); + + b.Property("CharacterStatus") + .HasColumnType("integer"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentMapId") + .HasColumnType("uuid"); + + b.Property("Experience") + .HasColumnType("bigint"); + + b.Property("InventoryExtensions") + .HasColumnType("integer"); + + b.Property("InventoryId") + .HasColumnType("uuid"); + + b.Property("IsStoreOpened") + .HasColumnType("boolean"); + + b.Property("KeyConfiguration") + .HasColumnType("bytea"); + + b.Property("LevelUpPoints") + .HasColumnType("integer"); + + b.Property("MasterExperience") + .HasColumnType("bigint"); + + b.Property("MasterLevelUpPoints") + .HasColumnType("integer"); + + b.Property("MuHelperConfiguration") + .HasColumnType("bytea"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("PlayerKillCount") + .HasColumnType("integer"); + + b.Property("Pose") + .HasColumnType("smallint"); + + b.Property("PositionX") + .HasColumnType("smallint"); + + b.Property("PositionY") + .HasColumnType("smallint"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("StateRemainingSeconds") + .HasColumnType("integer"); + + b.Property("StoreName") + .HasColumnType("text"); + + b.Property("UsedFruitPoints") + .HasColumnType("integer"); + + b.Property("UsedNegFruitPoints") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CharacterClassId"); + + b.HasIndex("CurrentMapId"); + + b.HasIndex("InventoryId") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Character", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanGetCreated") + .HasColumnType("boolean"); + + b.Property("ComboDefinitionId") + .HasColumnType("uuid"); + + b.Property("CreationAllowedFlag") + .HasColumnType("smallint"); + + b.Property("FruitCalculation") + .HasColumnType("integer"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("HomeMapId") + .HasColumnType("uuid"); + + b.Property("IsMasterClass") + .HasColumnType("boolean"); + + b.Property("LevelRequirementByCreation") + .HasColumnType("smallint"); + + b.Property("LevelWarpRequirementReductionPercent") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextGenerationClassId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ComboDefinitionId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("HomeMapId"); + + b.HasIndex("NextGenerationClassId"); + + b.ToTable("CharacterClass", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterDropItemGroup", b => + { + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.HasKey("CharacterId", "DropItemGroupId"); + + b.HasIndex("DropItemGroupId"); + + b.ToTable("CharacterDropItemGroup", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActiveQuestId") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("ClientActionPerformed") + .HasColumnType("boolean"); + + b.Property("Group") + .HasColumnType("smallint"); + + b.Property("LastFinishedQuestId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ActiveQuestId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("LastFinishedQuestId"); + + b.ToTable("CharacterQuestState", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientCleanUpInterval") + .HasColumnType("interval"); + + b.Property("ClientTimeout") + .HasColumnType("interval"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaximumConnections") + .HasColumnType("integer"); + + b.Property("RoomCleanUpInterval") + .HasColumnType("interval"); + + b.Property("ServerId") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("ChatServerDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatServerDefinitionId") + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("NetworkPort") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ChatServerDefinitionId"); + + b.HasIndex("ClientId"); + + b.ToTable("ChatServerEndpoint", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CombinationBonusRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemOptionCombinationBonusId") + .HasColumnType("uuid"); + + b.Property("MinimumCount") + .HasColumnType("integer"); + + b.Property("OptionTypeId") + .HasColumnType("uuid"); + + b.Property("SubOptionType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemOptionCombinationBonusId"); + + b.HasIndex("OptionTypeId"); + + b.ToTable("CombinationBonusRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConfigurationUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstalledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ConfigurationUpdate", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConfigurationUpdateState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CurrentInstalledVersion") + .HasColumnType("integer"); + + b.Property("InitializationKey") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("ConfigurationUpdateState", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConnectServerDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CheckMaxConnectionsPerAddress") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientListenerPort") + .HasColumnType("integer"); + + b.Property("CurrentPatchVersion") + .HasColumnType("bytea"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DisconnectOnUnknownPacket") + .HasColumnType("boolean"); + + b.Property("ListenerBacklog") + .HasColumnType("integer"); + + b.Property("MaxConnections") + .HasColumnType("integer"); + + b.Property("MaxConnectionsPerAddress") + .HasColumnType("integer"); + + b.Property("MaxFtpRequests") + .HasColumnType("integer"); + + b.Property("MaxIpRequests") + .HasColumnType("integer"); + + b.Property("MaxServerListRequests") + .HasColumnType("integer"); + + b.Property("MaximumReceiveSize") + .HasColumnType("smallint"); + + b.Property("PatchAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("smallint"); + + b.Property("Timeout") + .HasColumnType("interval"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ConnectServerDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConstValueAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("CharacterClassId"); + + b.HasIndex("DefinitionId"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ConstValueAttribute", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Chance") + .HasColumnType("double precision"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("ItemLevel") + .HasColumnType("smallint"); + + b.Property("ItemType") + .HasColumnType("integer"); + + b.Property("MaximumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MinimumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MonsterId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MonsterId"); + + b.ToTable("DropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroupItemDefinition", b => + { + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("DropItemGroupId", "ItemDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("DropItemGroupItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DuelConfigurationId") + .HasColumnType("uuid"); + + b.Property("FirstPlayerGateId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("smallint"); + + b.Property("SecondPlayerGateId") + .HasColumnType("uuid"); + + b.Property("SpectatorsGateId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DuelConfigurationId"); + + b.HasIndex("FirstPlayerGateId"); + + b.HasIndex("SecondPlayerGateId"); + + b.HasIndex("SpectatorsGateId"); + + b.ToTable("DuelArea", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntranceFee") + .HasColumnType("integer"); + + b.Property("ExitId") + .HasColumnType("uuid"); + + b.Property("MaximumScore") + .HasColumnType("integer"); + + b.Property("MaximumSpectatorsPerDuelRoom") + .HasColumnType("integer"); + + b.Property("MinimumCharacterLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExitId"); + + b.ToTable("DuelConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.EnterGate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("LevelRequirement") + .HasColumnType("smallint"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("TargetGateId") + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GameMapDefinitionId"); + + b.HasIndex("TargetGateId"); + + b.ToTable("EnterGate", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("IsSpawnGate") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("MapId"); + + b.ToTable("ExitGate", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Friend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Accepted") + .HasColumnType("boolean"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("FriendId") + .HasColumnType("uuid"); + + b.Property("RequestOpen") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasAlternateKey("CharacterId", "FriendId"); + + b.ToTable("Friend", "friend"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Episode") + .HasColumnType("smallint"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("Season") + .HasColumnType("smallint"); + + b.Property("Serial") + .HasColumnType("bytea"); + + b.Property("Version") + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.ToTable("GameClientDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AreaSkillHitsPlayer") + .HasColumnType("boolean"); + + b.Property("CastleSiegeConfigurationId") + .HasColumnType("uuid"); + + b.Property("CharacterNameRegex") + .HasColumnType("text"); + + b.Property("ClampMoneyOnPickup") + .HasColumnType("boolean"); + + b.Property("DamagePerOneItemDurability") + .HasColumnType("double precision"); + + b.Property("DamagePerOnePetDurability") + .HasColumnType("double precision"); + + b.Property("DuelConfigurationId") + .HasColumnType("uuid"); + + b.Property("ExcellentItemDropLevelDelta") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)25); + + b.Property("ExperienceFormula") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("if(level == 0, 0, if(level < 256, 10 * (level + 8) * (level - 1) * (level - 1), (10 * (level + 8) * (level - 1) * (level - 1)) + (1000 * (level - 247) * (level - 256) * (level - 256))))"); + + b.Property("ExperienceRate") + .HasColumnType("real"); + + b.Property("HitsPerOneItemDurability") + .HasColumnType("double precision"); + + b.Property("InfoRange") + .HasColumnType("smallint"); + + b.Property("ItemDropDuration") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 1, 0, 0)); + + b.Property("LetterSendPrice") + .HasColumnType("integer"); + + b.Property("MasterExperienceFormula") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("(505 * level * level * level) + (35278500 * level) + (228045 * level * level)"); + + b.Property("MasterExperienceRate") + .HasColumnType("real"); + + b.Property("MaximumCharactersPerAccount") + .HasColumnType("smallint"); + + b.Property("MaximumInventoryMoney") + .HasColumnType("integer"); + + b.Property("MaximumItemOptionLevelDrop") + .HasColumnType("smallint"); + + b.Property("MaximumLetters") + .HasColumnType("integer"); + + b.Property("MaximumLevel") + .HasColumnType("smallint"); + + b.Property("MaximumMasterLevel") + .HasColumnType("smallint"); + + b.Property("MaximumPartySize") + .HasColumnType("smallint"); + + b.Property("MaximumPasswordLength") + .HasColumnType("integer"); + + b.Property("MaximumVaultMoney") + .HasColumnType("integer"); + + b.Property("MinimumMonsterLevelForMasterExperience") + .HasColumnType("smallint"); + + b.Property("PreventExperienceOverflow") + .HasColumnType("boolean"); + + b.Property("RecoveryInterval") + .HasColumnType("integer"); + + b.Property("ShouldDropMoney") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CastleSiegeConfigurationId") + .IsUnique(); + + b.HasIndex("DuelConfigurationId") + .IsUnique(); + + b.ToTable("GameConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BattleZoneId") + .HasColumnType("uuid"); + + b.Property("Discriminator") + .HasColumnType("integer"); + + b.Property("ExpMultiplier") + .HasColumnType("double precision"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("SafezoneMapId") + .HasColumnType("uuid"); + + b.Property("TerrainData") + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("BattleZoneId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("SafezoneMapId"); + + b.ToTable("GameMapDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinitionDropItemGroup", b => + { + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.HasKey("GameMapDefinitionId", "DropItemGroupId"); + + b.HasIndex("DropItemGroupId"); + + b.ToTable("GameMapDefinitionDropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MaximumPlayers") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("GameServerConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfigurationGameMapDefinition", b => + { + b.Property("GameServerConfigurationId") + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("GameServerConfigurationId", "GameMapDefinitionId"); + + b.HasIndex("GameMapDefinitionId"); + + b.ToTable("GameServerConfigurationGameMapDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExperienceRate") + .HasColumnType("real"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("PvpEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ServerConfigurationId") + .HasColumnType("uuid"); + + b.Property("ServerID") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("ServerConfigurationId"); + + b.ToTable("GameServerDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AlternativePublishedPort") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("GameServerDefinitionId") + .HasColumnType("uuid"); + + b.Property("NetworkPort") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("GameServerDefinitionId"); + + b.ToTable("GameServerEndpoint", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllianceGuildId") + .HasColumnType("uuid"); + + b.Property("HostilityId") + .HasColumnType("uuid"); + + b.Property("Logo") + .HasColumnType("bytea"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("Notice") + .HasColumnType("text"); + + b.Property("Score") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AllianceGuildId"); + + b.HasIndex("HostilityId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Guild", "guild"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GuildMember", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.ToTable("GuildMember", "guild"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemOptionDefinitionId") + .HasColumnType("uuid"); + + b.Property("LevelType") + .HasColumnType("integer"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OptionTypeId") + .HasColumnType("uuid"); + + b.Property("PowerUpDefinitionId") + .HasColumnType("uuid"); + + b.Property("SubOptionType") + .HasColumnType("integer"); + + b.Property("Weight") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ItemOptionDefinitionId"); + + b.HasIndex("OptionTypeId"); + + b.HasIndex("PowerUpDefinitionId") + .IsUnique(); + + b.ToTable("IncreasableItemOption", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("Durability") + .HasColumnType("double precision"); + + b.Property("HasSkill") + .HasColumnType("boolean"); + + b.Property("ItemSlot") + .HasColumnType("smallint"); + + b.Property("ItemStorageId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("PetExperience") + .HasColumnType("integer"); + + b.Property("SocketCount") + .HasColumnType("integer"); + + b.Property("StorePrice") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DefinitionId"); + + b.HasIndex("ItemStorageId"); + + b.ToTable("Item", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppearanceDataId") + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemSlot") + .HasColumnType("smallint"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AppearanceDataId"); + + b.HasIndex("DefinitionId"); + + b.ToTable("ItemAppearance", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearanceItemOptionType", b => + { + b.Property("ItemAppearanceId") + .HasColumnType("uuid"); + + b.Property("ItemOptionTypeId") + .HasColumnType("uuid"); + + b.HasKey("ItemAppearanceId", "ItemOptionTypeId"); + + b.HasIndex("ItemOptionTypeId"); + + b.ToTable("ItemAppearanceItemOptionType", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemBasePowerUpDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("BaseValue") + .HasColumnType("real"); + + b.Property("BonusPerLevelTableId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BonusPerLevelTableId"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("ItemBasePowerUpDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCrafting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemCraftingHandlerClassName") + .IsRequired() + .HasColumnType("text"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("SimpleCraftingSettingsId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MonsterDefinitionId"); + + b.HasIndex("SimpleCraftingSettingsId") + .IsUnique(); + + b.ToTable("ItemCrafting", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddPercentage") + .HasColumnType("smallint"); + + b.Property("FailResult") + .HasColumnType("integer"); + + b.Property("MaximumAmount") + .HasColumnType("smallint"); + + b.Property("MaximumItemLevel") + .HasColumnType("smallint"); + + b.Property("MinimumAmount") + .HasColumnType("smallint"); + + b.Property("MinimumItemLevel") + .HasColumnType("smallint"); + + b.Property("NpcPriceDivisor") + .HasColumnType("integer"); + + b.Property("Reference") + .HasColumnType("smallint"); + + b.Property("SimpleCraftingSettingsId") + .HasColumnType("uuid"); + + b.Property("SuccessResult") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SimpleCraftingSettingsId"); + + b.ToTable("ItemCraftingRequiredItem", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemDefinition", b => + { + b.Property("ItemCraftingRequiredItemId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("ItemCraftingRequiredItemId", "ItemDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("ItemCraftingRequiredItemItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemOptionType", b => + { + b.Property("ItemCraftingRequiredItemId") + .HasColumnType("uuid"); + + b.Property("ItemOptionTypeId") + .HasColumnType("uuid"); + + b.HasKey("ItemCraftingRequiredItemId", "ItemOptionTypeId"); + + b.HasIndex("ItemOptionTypeId"); + + b.ToTable("ItemCraftingRequiredItemItemOptionType", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingResultItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddLevel") + .HasColumnType("smallint"); + + b.Property("Durability") + .HasColumnType("smallint"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("RandomMaximumLevel") + .HasColumnType("smallint"); + + b.Property("RandomMinimumLevel") + .HasColumnType("smallint"); + + b.Property("Reference") + .HasColumnType("smallint"); + + b.Property("SimpleCraftingSettingsId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("SimpleCraftingSettingsId"); + + b.ToTable("ItemCraftingResultItem", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConsumeEffectId") + .HasColumnType("uuid"); + + b.Property("DropLevel") + .HasColumnType("smallint"); + + b.Property("DropsFromMonsters") + .HasColumnType("boolean"); + + b.Property("Durability") + .HasColumnType("smallint"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Group") + .HasColumnType("smallint"); + + b.Property("Height") + .HasColumnType("smallint"); + + b.Property("IsAmmunition") + .HasColumnType("boolean"); + + b.Property("IsBoundToCharacter") + .HasColumnType("boolean"); + + b.Property("IsDroppable") + .HasColumnType("boolean"); + + b.Property("IsPersonalStoreSellable") + .HasColumnType("boolean"); + + b.Property("IsQuestItem") + .HasColumnType("boolean"); + + b.Property("IsRepairable") + .HasColumnType("boolean"); + + b.Property("IsSellableToNpc") + .HasColumnType("boolean"); + + b.Property("IsStorable") + .HasColumnType("boolean"); + + b.Property("IsTradable") + .HasColumnType("boolean"); + + b.Property("ItemSlotId") + .HasColumnType("uuid"); + + b.Property("MaximumDropLevel") + .HasColumnType("smallint"); + + b.Property("MaximumItemLevel") + .HasColumnType("smallint"); + + b.Property("MaximumSockets") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("PetExperienceFormula") + .HasColumnType("text"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("StorageLimitPerCharacter") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("integer"); + + b.Property("Width") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ConsumeEffectId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("ItemSlotId"); + + b.HasIndex("SkillId"); + + b.ToTable("ItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionCharacterClass", b => + { + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.HasKey("ItemDefinitionId", "CharacterClassId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("ItemDefinitionCharacterClass", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemOptionDefinition", b => + { + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemOptionDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("ItemDefinitionId", "ItemOptionDefinitionId"); + + b.HasIndex("ItemOptionDefinitionId"); + + b.ToTable("ItemDefinitionItemOptionDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemSetGroup", b => + { + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemSetGroupId") + .HasColumnType("uuid"); + + b.HasKey("ItemDefinitionId", "ItemSetGroupId"); + + b.HasIndex("ItemSetGroupId"); + + b.ToTable("ItemDefinitionItemSetGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Chance") + .HasColumnType("double precision"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DropEffect") + .HasColumnType("integer"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemLevel") + .HasColumnType("smallint"); + + b.Property("ItemType") + .HasColumnType("integer"); + + b.Property("MaximumLevel") + .HasColumnType("smallint"); + + b.Property("MaximumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MinimumLevel") + .HasColumnType("smallint"); + + b.Property("MinimumMonsterLevel") + .HasColumnType("smallint"); + + b.Property("MoneyAmount") + .HasColumnType("integer"); + + b.Property("MonsterId") + .HasColumnType("uuid"); + + b.Property("RequiredCharacterLevel") + .HasColumnType("smallint"); + + b.Property("SourceItemLevel") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("MonsterId"); + + b.ToTable("ItemDropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroupItemDefinition", b => + { + b.Property("ItemDropItemGroupId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("ItemDropItemGroupId", "ItemDefinitionId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("ItemDropItemGroupItemDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemItemOfItemSet", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ItemOfItemSetId") + .HasColumnType("uuid"); + + b.HasKey("ItemId", "ItemOfItemSetId"); + + b.HasIndex("ItemOfItemSetId"); + + b.ToTable("ItemItemOfItemSet", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemLevelBonusTable", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOfItemSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AncientSetDiscriminator") + .HasColumnType("integer"); + + b.Property("BonusOptionId") + .HasColumnType("uuid"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemSetGroupId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BonusOptionId"); + + b.HasIndex("ItemDefinitionId"); + + b.HasIndex("ItemSetGroupId"); + + b.ToTable("ItemOfItemSet", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OptionTypeId") + .HasColumnType("uuid"); + + b.Property("PowerUpDefinitionId") + .HasColumnType("uuid"); + + b.Property("SubOptionType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OptionTypeId"); + + b.HasIndex("PowerUpDefinitionId") + .IsUnique(); + + b.ToTable("ItemOption", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppliesMultipleTimes") + .HasColumnType("boolean"); + + b.Property("BonusId") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BonusId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemOptionCombinationBonus", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddChance") + .HasColumnType("real"); + + b.Property("AddsRandomly") + .HasColumnType("boolean"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MaximumOptionsPerItem") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemOptionDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ItemOptionId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.HasIndex("ItemOptionId"); + + b.ToTable("ItemOptionLink", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionOfLevel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IncreasableItemOptionId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("PowerUpDefinitionId") + .HasColumnType("uuid"); + + b.Property("RequiredItemLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IncreasableItemOptionId"); + + b.HasIndex("PowerUpDefinitionId") + .IsUnique(); + + b.ToTable("ItemOptionOfLevel", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("IsVisible") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemOptionType", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AlwaysApplies") + .HasColumnType("boolean"); + + b.Property("CountDistinct") + .HasColumnType("boolean"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MinimumItemCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OptionsId") + .HasColumnType("uuid"); + + b.Property("SetLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("OptionsId"); + + b.ToTable("ItemSetGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSlotType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("RawItemSlots") + .HasColumnType("text") + .HasColumnName("ItemSlots") + .HasJsonPropertyName("itemSlots"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("ItemSlotType", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Money") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ItemStorage", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.JewelMix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("MixedJewelId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("SingleJewelId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MixedJewelId"); + + b.HasIndex("SingleJewelId"); + + b.ToTable("JewelMix", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterBody", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Animation") + .HasColumnType("smallint"); + + b.Property("HeaderId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rotation") + .HasColumnType("smallint"); + + b.Property("SenderAppearanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("HeaderId"); + + b.HasIndex("SenderAppearanceId") + .IsUnique(); + + b.ToTable("LetterBody", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterHeader", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("LetterDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadFlag") + .HasColumnType("boolean"); + + b.Property("ReceiverId") + .HasColumnType("uuid"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ReceiverId"); + + b.ToTable("LetterHeader", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LevelBonus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdditionalValue") + .HasColumnType("real"); + + b.Property("ItemLevelBonusTableId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemLevelBonusTableId"); + + b.ToTable("LevelBonus", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChanceId") + .HasColumnType("uuid"); + + b.Property("ChancePvpId") + .HasColumnType("uuid"); + + b.Property("DurationDependsOnTargetLevel") + .HasColumnType("boolean"); + + b.Property("DurationId") + .HasColumnType("uuid"); + + b.Property("DurationPvpId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("InformObservers") + .HasColumnType("boolean"); + + b.Property("MonsterTargetLevelDivisor") + .HasColumnType("real"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("PlayerTargetLevelDivisor") + .HasColumnType("real"); + + b.Property("SendDuration") + .HasColumnType("boolean"); + + b.Property("StopByDeath") + .HasColumnType("boolean"); + + b.Property("SubType") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ChanceId") + .IsUnique(); + + b.HasIndex("ChancePvpId") + .IsUnique(); + + b.HasIndex("DurationId") + .IsUnique(); + + b.HasIndex("DurationPvpId") + .IsUnique(); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("MagicEffectDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Aggregation") + .HasColumnType("integer"); + + b.Property("DisplayValueFormula") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtendsDuration") + .HasColumnType("boolean"); + + b.Property("MaximumLevel") + .HasColumnType("smallint"); + + b.Property("MinimumLevel") + .HasColumnType("smallint"); + + b.Property("Rank") + .HasColumnType("smallint"); + + b.Property("ReplacedSkillId") + .HasColumnType("uuid"); + + b.Property("RootId") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.Property("ValueFormula") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ReplacedSkillId"); + + b.HasIndex("RootId"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("MasterSkillDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinitionSkill", b => + { + b.Property("MasterSkillDefinitionId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("MasterSkillDefinitionId", "SkillId"); + + b.HasIndex("SkillId"); + + b.ToTable("MasterSkillDefinitionSkill", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("MasterSkillRoot", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("MiniGameDefinitionId") + .HasColumnType("uuid"); + + b.Property("MinimumTargetLevel") + .HasColumnType("smallint"); + + b.Property("MultiplyKillsByPlayers") + .HasColumnType("boolean"); + + b.Property("NumberOfKills") + .HasColumnType("smallint"); + + b.Property("SpawnAreaId") + .HasColumnType("uuid"); + + b.Property("Target") + .HasColumnType("integer"); + + b.Property("TargetDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MiniGameDefinitionId"); + + b.HasIndex("SpawnAreaId") + .IsUnique(); + + b.HasIndex("TargetDefinitionId"); + + b.ToTable("MiniGameChangeEvent", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowParty") + .HasColumnType("boolean"); + + b.Property("ArePlayerKillersAllowedToEnter") + .HasColumnType("boolean"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EnterDuration") + .HasColumnType("interval"); + + b.Property("EntranceFee") + .HasColumnType("integer"); + + b.Property("EntranceId") + .HasColumnType("uuid"); + + b.Property("ExitDuration") + .HasColumnType("interval"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("GameDuration") + .HasColumnType("interval"); + + b.Property("GameLevel") + .HasColumnType("smallint"); + + b.Property("MapCreationPolicy") + .HasColumnType("integer"); + + b.Property("MaximumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MaximumPlayerCount") + .HasColumnType("integer"); + + b.Property("MaximumSpecialCharacterLevel") + .HasColumnType("integer"); + + b.Property("MinimumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MinimumSpecialCharacterLevel") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequiresMasterClass") + .HasColumnType("boolean"); + + b.Property("SaveRankingStatistics") + .HasColumnType("boolean"); + + b.Property("TicketItemId") + .HasColumnType("uuid"); + + b.Property("TicketItemLevel") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EntranceId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("TicketItemId"); + + b.ToTable("MiniGameDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameRankingEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("GameInstanceId") + .HasColumnType("uuid"); + + b.Property("MiniGameId") + .HasColumnType("uuid"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("MiniGameId"); + + b.ToTable("MiniGameRankingEntry", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ItemRewardId") + .HasColumnType("uuid"); + + b.Property("MiniGameDefinitionId") + .HasColumnType("uuid"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("RequiredKillId") + .HasColumnType("uuid"); + + b.Property("RequiredSuccess") + .HasColumnType("integer"); + + b.Property("RewardAmount") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemRewardId"); + + b.HasIndex("MiniGameDefinitionId"); + + b.HasIndex("RequiredKillId"); + + b.ToTable("MiniGameReward", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameSpawnWave", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("interval"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("MiniGameDefinitionId") + .HasColumnType("uuid"); + + b.Property("StartTime") + .HasColumnType("interval"); + + b.Property("WaveNumber") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("MiniGameDefinitionId"); + + b.ToTable("MiniGameSpawnWave", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameTerrainChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EndX") + .HasColumnType("smallint"); + + b.Property("EndY") + .HasColumnType("smallint"); + + b.Property("IsClientUpdateRequired") + .HasColumnType("boolean"); + + b.Property("MiniGameChangeEventId") + .HasColumnType("uuid"); + + b.Property("SetTerrainAttribute") + .HasColumnType("boolean"); + + b.Property("StartX") + .HasColumnType("smallint"); + + b.Property("StartY") + .HasColumnType("smallint"); + + b.Property("TerrainAttribute") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("MiniGameChangeEventId"); + + b.ToTable("MiniGameTerrainChange", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeDefinitionId") + .HasColumnType("uuid"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("AttributeDefinitionId"); + + b.HasIndex("MonsterDefinitionId"); + + b.ToTable("MonsterAttribute", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttackDelay") + .HasColumnType("interval"); + + b.Property("AttackRange") + .HasColumnType("smallint"); + + b.Property("AttackSkillId") + .HasColumnType("uuid"); + + b.Property("Attribute") + .HasColumnType("smallint"); + + b.Property("Designation") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("IntelligenceTypeName") + .HasColumnType("text"); + + b.Property("MerchantStoreId") + .HasColumnType("uuid"); + + b.Property("MoveDelay") + .HasColumnType("interval"); + + b.Property("MoveRange") + .HasColumnType("smallint"); + + b.Property("NpcWindow") + .HasColumnType("integer"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("NumberOfMaximumItemDrops") + .HasColumnType("integer"); + + b.Property("ObjectKind") + .HasColumnType("integer"); + + b.Property("RespawnDelay") + .HasColumnType("interval"); + + b.Property("ViewRange") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AttackSkillId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MerchantStoreId") + .IsUnique(); + + b.ToTable("MonsterDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinitionDropItemGroup", b => + { + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.HasKey("MonsterDefinitionId", "DropItemGroupId"); + + b.HasIndex("DropItemGroupId"); + + b.ToTable("MonsterDefinitionDropItemGroup", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterSpawnArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("GameMapId") + .HasColumnType("uuid"); + + b.Property("MaximumHealthOverride") + .HasColumnType("integer"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("smallint"); + + b.Property("SpawnTrigger") + .HasColumnType("integer"); + + b.Property("WaveNumber") + .HasColumnType("smallint"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("GameMapId"); + + b.HasIndex("MonsterDefinitionId"); + + b.ToTable("MonsterSpawnArea", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PlugInConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CustomConfiguration") + .HasColumnType("text"); + + b.Property("CustomPlugInSource") + .HasColumnType("text"); + + b.Property("ExternalAssemblyName") + .HasColumnType("text"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("TypeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.ToTable("PlugInConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BoostId") + .HasColumnType("uuid"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("MagicEffectDefinitionId") + .HasColumnType("uuid"); + + b.Property("MagicEffectDefinitionId1") + .HasColumnType("uuid"); + + b.Property("TargetAttributeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BoostId") + .IsUnique(); + + b.HasIndex("GameMapDefinitionId"); + + b.HasIndex("MagicEffectDefinitionId"); + + b.HasIndex("MagicEffectDefinitionId1"); + + b.HasIndex("TargetAttributeId"); + + b.ToTable("PowerUpDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AggregateType") + .HasColumnType("integer"); + + b.Property("MaximumValue") + .HasColumnType("real"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.ToTable("PowerUpDefinitionValue", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Group") + .HasColumnType("smallint"); + + b.Property("MaximumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MinimumCharacterLevel") + .HasColumnType("integer"); + + b.Property("MonsterDefinitionId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("QualifiedCharacterId") + .HasColumnType("uuid"); + + b.Property("QuestGiverId") + .HasColumnType("uuid"); + + b.Property("RefuseNumber") + .HasColumnType("smallint"); + + b.Property("Repeatable") + .HasColumnType("boolean"); + + b.Property("RequiredStartMoney") + .HasColumnType("integer"); + + b.Property("RequiresClientAction") + .HasColumnType("boolean"); + + b.Property("StartingNumber") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("MonsterDefinitionId"); + + b.HasIndex("QualifiedCharacterId"); + + b.HasIndex("QuestGiverId"); + + b.ToTable("QuestDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestItemRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DropItemGroupId") + .HasColumnType("uuid"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("MinimumNumber") + .HasColumnType("integer"); + + b.Property("QuestDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DropItemGroupId"); + + b.HasIndex("ItemId"); + + b.HasIndex("QuestDefinitionId"); + + b.ToTable("QuestItemRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MinimumNumber") + .HasColumnType("integer"); + + b.Property("MonsterId") + .HasColumnType("uuid"); + + b.Property("QuestDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MonsterId"); + + b.HasIndex("QuestDefinitionId"); + + b.ToTable("QuestMonsterKillRequirement", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirementState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterQuestStateId") + .HasColumnType("uuid"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("RequirementId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterQuestStateId"); + + b.HasIndex("RequirementId"); + + b.ToTable("QuestMonsterKillRequirementState", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeRewardId") + .HasColumnType("uuid"); + + b.Property("ItemRewardId") + .HasColumnType("uuid"); + + b.Property("QuestDefinitionId") + .HasColumnType("uuid"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("SkillRewardId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AttributeRewardId"); + + b.HasIndex("ItemRewardId") + .IsUnique(); + + b.HasIndex("QuestDefinitionId"); + + b.HasIndex("SkillRewardId"); + + b.ToTable("QuestReward", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("X1") + .HasColumnType("smallint"); + + b.Property("X2") + .HasColumnType("smallint"); + + b.Property("Y1") + .HasColumnType("smallint"); + + b.Property("Y2") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Rectangle", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MaximumSuccessPercent") + .HasColumnType("smallint"); + + b.Property("Money") + .HasColumnType("integer"); + + b.Property("MoneyPerFinalSuccessPercentage") + .HasColumnType("integer"); + + b.Property("MultipleAllowed") + .HasColumnType("boolean"); + + b.Property("NpcPriceDivisor") + .HasColumnType("integer"); + + b.Property("ResultItemExcellentOptionChance") + .HasColumnType("smallint"); + + b.Property("ResultItemLuckOptionChance") + .HasColumnType("smallint"); + + b.Property("ResultItemMaxExcOptionCount") + .HasColumnType("smallint"); + + b.Property("ResultItemSelect") + .HasColumnType("integer"); + + b.Property("ResultItemSkillChance") + .HasColumnType("smallint"); + + b.Property("SuccessPercent") + .HasColumnType("smallint"); + + b.Property("SuccessPercentageAdditionForAncientItem") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForExcellentItem") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForGuardianItem") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForLuck") + .HasColumnType("integer"); + + b.Property("SuccessPercentageAdditionForSocketItem") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SimpleCraftingSettings", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AreaSkillSettingsId") + .HasColumnType("uuid"); + + b.Property("AttackDamage") + .HasColumnType("integer"); + + b.Property("DamageType") + .HasColumnType("integer"); + + b.Property("ElementalModifierTargetId") + .HasColumnType("uuid"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("ImplicitTargetRange") + .HasColumnType("smallint"); + + b.Property("MagicEffectDefId") + .HasColumnType("uuid"); + + b.Property("MasterDefinitionId") + .HasColumnType("uuid"); + + b.Property("MovesTarget") + .HasColumnType("boolean"); + + b.Property("MovesToTarget") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("NumberOfHitsPerAttack") + .HasColumnType("smallint"); + + b.Property("Range") + .HasColumnType("smallint"); + + b.Property("SkillType") + .HasColumnType("integer"); + + b.Property("SkipElementalModifier") + .HasColumnType("boolean"); + + b.Property("Target") + .HasColumnType("integer"); + + b.Property("TargetRestriction") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AreaSkillSettingsId") + .IsUnique(); + + b.HasIndex("ElementalModifierTargetId"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("MagicEffectDefId"); + + b.HasIndex("MasterDefinitionId") + .IsUnique(); + + b.ToTable("Skill", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillCharacterClass", b => + { + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.HasKey("SkillId", "CharacterClassId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("SkillCharacterClass", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MaximumCompletionTime") + .HasColumnType("interval"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SkillComboDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsFinalStep") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("SkillComboDefinitionId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SkillComboDefinitionId"); + + b.HasIndex("SkillId"); + + b.ToTable("SkillComboStep", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("SkillId"); + + b.ToTable("SkillEntry", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttribute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("DefinitionId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("DefinitionId"); + + b.ToTable("StatAttribute", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttributeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttributeId") + .HasColumnType("uuid"); + + b.Property("BaseValue") + .HasColumnType("real"); + + b.Property("CharacterClassId") + .HasColumnType("uuid"); + + b.Property("IncreasableByPlayer") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AttributeId"); + + b.HasIndex("CharacterClassId"); + + b.ToTable("StatAttributeDefinition", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SystemConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AutoStart") + .HasColumnType("boolean"); + + b.Property("AutoUpdateSchema") + .HasColumnType("boolean"); + + b.Property("IpResolver") + .HasColumnType("integer"); + + b.Property("IpResolverParameter") + .HasColumnType("text"); + + b.Property("NetworkAnalyzerLiveBufferSize") + .HasColumnType("integer"); + + b.Property("NetworkObservationArchivePath") + .HasColumnType("text"); + + b.Property("NetworkObservationMaxSessionSizeMb") + .HasColumnType("integer"); + + b.Property("NetworkObservationMaxTotalSizeMb") + .HasColumnType("integer"); + + b.Property("NetworkObservationRetentionDays") + .HasColumnType("integer"); + + b.Property("ReadConsoleInput") + .HasColumnType("boolean"); + + b.Property("TimeZoneId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SystemConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.WarpInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Costs") + .HasColumnType("integer"); + + b.Property("GameConfigurationId") + .HasColumnType("uuid"); + + b.Property("GateId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("LevelRequirement") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameConfigurationId"); + + b.HasIndex("GateId"); + + b.ToTable("WarpInfo", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawVault") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", "VaultId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawVault"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AccountCharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", "Account") + .WithMany("JoinedUnlockedCharacterClasses") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + + b.Navigation("CharacterClass"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawCharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId"); + + b.Navigation("RawCharacterClass"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawAttributes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRelationship", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", null) + .WithMany("RawAttributeCombinations") + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawGlobalAttributeCombinations") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawInputAttribute") + .WithMany() + .HasForeignKey("InputAttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawOperandAttribute") + .WithMany() + .HasForeignKey("OperandAttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", null) + .WithMany("RawRelatedValues") + .HasForeignKey("PowerUpDefinitionValueId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", null) + .WithMany("RawAttributeRelationships") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawInputAttribute"); + + b.Navigation("RawOperandAttribute"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttribute") + .WithMany() + .HasForeignKey("AttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawMapRequirements") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany("RawRequirements") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", null) + .WithMany("RawConsumeRequirements") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", null) + .WithMany("RawRequirements") + .HasForeignKey("SkillId1") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", "RawGround") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "GroundId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", "RawLeftGoal") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "LeftGoalId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Rectangle", "RawRightGoal") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "RightGoalId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawGround"); + + b.Navigation("RawLeftGoal"); + + b.Navigation("RawRightGoal"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "RawMagicEffectDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Buff", "MagicEffectDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawBuffs") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawMagicEffectDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", "RawAttackRespawnArea") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", "AttackRespawnAreaId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawCastleSiegeMapDefinition") + .WithMany() + .HasForeignKey("CastleSiegeMapDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", "RawDefenseRespawnArea") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", "DefenseRespawnAreaId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawLandOfTrialsMapDefinition") + .WithMany() + .HasForeignKey("LandOfTrialsMapDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawRewardItemDefinition") + .WithMany() + .HasForeignKey("RewardItemDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawSignOfLordItemDefinition") + .WithMany() + .HasForeignKey("SignOfLordItemDefinitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("RawAttackRespawnArea"); + + b.Navigation("RawCastleSiegeMapDefinition"); + + b.Navigation("RawDefenseRespawnArea"); + + b.Navigation("RawLandOfTrialsMapDefinition"); + + b.Navigation("RawRewardItemDefinition"); + + b.Navigation("RawSignOfLordItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("OwnerGuildId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuild", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", null) + .WithMany("RawGuilds") + .HasForeignKey("CastleSiegeDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeGuildRegistration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawNpcDefinitions") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonsterDefinition") + .WithMany() + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("RawMonsterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeNpcState", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", null) + .WithMany("RawNpcStates") + .HasForeignKey("CastleSiegeDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegePendingReward", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeStateScheduleEntry", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStateSchedule") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeUpgradeDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawGateDefenseUpgrades") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawGateLifeUpgrades") + .HasForeignKey("CastleSiegeConfigurationId1") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~1"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStatueDefenseUpgrades") + .HasForeignKey("CastleSiegeConfigurationId2") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~2"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStatueLifeUpgrades") + .HasForeignKey("CastleSiegeConfigurationId3") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~3"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawStatueRegenUpgrades") + .HasForeignKey("CastleSiegeConfigurationId4") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeUpgradeDefinition_CastleSiegeConfiguration_Cast~4"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeZoneDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawAttackMachineZones") + .HasForeignKey("CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", null) + .WithMany("RawDefenseMachineZones") + .HasForeignKey("CastleSiegeConfigurationId1") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_CastleSiegeZoneDefinition_CastleSiegeConfiguration_CastleS~1"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany("RawCharacters") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawCharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawCurrentMap") + .WithMany() + .HasForeignKey("CurrentMapId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawInventory") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "InventoryId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawCharacterClass"); + + b.Navigation("RawCurrentMap"); + + b.Navigation("RawInventory"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", "RawComboDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "ComboDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawCharacterClasses") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawHomeMap") + .WithMany() + .HasForeignKey("HomeMapId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawNextGenerationClass") + .WithMany() + .HasForeignKey("NextGenerationClassId"); + + b.Navigation("RawComboDefinition"); + + b.Navigation("RawHomeMap"); + + b.Navigation("RawNextGenerationClass"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "Character") + .WithMany("JoinedDropItemGroups") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + + b.Navigation("DropItemGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", "RawActiveQuest") + .WithMany() + .HasForeignKey("ActiveQuestId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany("RawQuestStates") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", "RawLastFinishedQuest") + .WithMany() + .HasForeignKey("LastFinishedQuestId"); + + b.Navigation("RawActiveQuest"); + + b.Navigation("RawLastFinishedQuest"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerEndpoint", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", null) + .WithMany("RawEndpoints") + .HasForeignKey("ChatServerDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", "RawClient") + .WithMany() + .HasForeignKey("ClientId"); + + b.Navigation("RawClient"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CombinationBonusRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", null) + .WithMany("RawRequirements") + .HasForeignKey("ItemOptionCombinationBonusId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "RawOptionType") + .WithMany() + .HasForeignKey("OptionTypeId"); + + b.Navigation("RawOptionType"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConnectServerDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", "RawClient") + .WithMany() + .HasForeignKey("ClientId"); + + b.Navigation("RawClient"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ConstValueAttribute", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany("RawBaseAttributeValues") + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "GameConfiguration") + .WithMany("RawGlobalBaseAttributeValues") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("CharacterClass"); + + b.Navigation("GameConfiguration"); + + b.Navigation("RawDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawDropItemGroups") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonster") + .WithMany() + .HasForeignKey("MonsterId"); + + b.Navigation("RawMonster"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroupItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany("JoinedPossibleItems") + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DropItemGroup"); + + b.Navigation("ItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelArea", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", null) + .WithMany("RawDuelAreas") + .HasForeignKey("DuelConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawFirstPlayerGate") + .WithMany() + .HasForeignKey("FirstPlayerGateId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawSecondPlayerGate") + .WithMany() + .HasForeignKey("SecondPlayerGateId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawSpectatorsGate") + .WithMany() + .HasForeignKey("SpectatorsGateId"); + + b.Navigation("RawFirstPlayerGate"); + + b.Navigation("RawSecondPlayerGate"); + + b.Navigation("RawSpectatorsGate"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawExit") + .WithMany() + .HasForeignKey("ExitId"); + + b.Navigation("RawExit"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.EnterGate", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawEnterGates") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawTargetGate") + .WithMany() + .HasForeignKey("TargetGateId"); + + b.Navigation("RawTargetGate"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawMap") + .WithMany("RawExitGates") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawMap"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", "RawCastleSiegeConfiguration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "CastleSiegeConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", "RawDuelConfiguration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "DuelConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawCastleSiegeConfiguration"); + + b.Navigation("RawDuelConfiguration"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.BattleZoneDefinition", "RawBattleZone") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "BattleZoneId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMaps") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawSafezoneMap") + .WithMany() + .HasForeignKey("SafezoneMapId"); + + b.Navigation("RawBattleZone"); + + b.Navigation("RawSafezoneMap"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinitionDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "GameMapDefinition") + .WithMany("JoinedDropItemGroups") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DropItemGroup"); + + b.Navigation("GameMapDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfigurationGameMapDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "GameMapDefinition") + .WithMany() + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", "GameServerConfiguration") + .WithMany("JoinedMaps") + .HasForeignKey("GameServerConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GameMapDefinition"); + + b.Navigation("GameServerConfiguration"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "RawGameConfiguration") + .WithMany() + .HasForeignKey("GameConfigurationId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", "RawServerConfiguration") + .WithMany() + .HasForeignKey("ServerConfigurationId"); + + b.Navigation("RawGameConfiguration"); + + b.Navigation("RawServerConfiguration"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerEndpoint", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameClientDefinition", "RawClient") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", null) + .WithMany("RawEndpoints") + .HasForeignKey("GameServerDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawClient"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", "RawAllianceGuild") + .WithMany() + .HasForeignKey("AllianceGuildId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", "RawHostility") + .WithMany() + .HasForeignKey("HostilityId"); + + b.Navigation("RawAllianceGuild"); + + b.Navigation("RawHostility"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GuildMember", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany("RawMembers") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "Character") + .WithMany() + .HasForeignKey("Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", null) + .WithMany("RawPossibleOptions") + .HasForeignKey("ItemOptionDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "RawOptionType") + .WithMany() + .HasForeignKey("OptionTypeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawPowerUpDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", "PowerUpDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawOptionType"); + + b.Navigation("RawPowerUpDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawItemStorage") + .WithMany("RawItems") + .HasForeignKey("ItemStorageId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawDefinition"); + + b.Navigation("RawItemStorage"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", null) + .WithMany("RawEquippedItems") + .HasForeignKey("AppearanceDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.Navigation("RawDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearanceItemOptionType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", "ItemAppearance") + .WithMany("JoinedVisibleOptions") + .HasForeignKey("ItemAppearanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "ItemOptionType") + .WithMany() + .HasForeignKey("ItemOptionTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemAppearance"); + + b.Navigation("ItemOptionType"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemBasePowerUpDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", "RawBonusPerLevelTable") + .WithMany() + .HasForeignKey("BonusPerLevelTableId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany("RawBasePowerUpAttributes") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawBonusPerLevelTable"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCrafting", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawItemCraftings") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", "RawSimpleCraftingSettings") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCrafting", "SimpleCraftingSettingsId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawSimpleCraftingSettings"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", null) + .WithMany("RawRequiredItems") + .HasForeignKey("SimpleCraftingSettingsId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", "ItemCraftingRequiredItem") + .WithMany("JoinedPossibleItems") + .HasForeignKey("ItemCraftingRequiredItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemCraftingRequiredItem"); + + b.Navigation("ItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItemItemOptionType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", "ItemCraftingRequiredItem") + .WithMany("JoinedRequiredItemOptions") + .HasForeignKey("ItemCraftingRequiredItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "ItemOptionType") + .WithMany() + .HasForeignKey("ItemOptionTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemCraftingRequiredItem"); + + b.Navigation("ItemOptionType"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingResultItem", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", null) + .WithMany("RawResultItems") + .HasForeignKey("SimpleCraftingSettingsId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "RawConsumeEffect") + .WithMany() + .HasForeignKey("ConsumeEffectId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItems") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSlotType", "RawItemSlot") + .WithMany() + .HasForeignKey("ItemSlotId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkill") + .WithMany() + .HasForeignKey("SkillId"); + + b.Navigation("RawConsumeEffect"); + + b.Navigation("RawItemSlot"); + + b.Navigation("RawSkill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionCharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany("JoinedQualifiedCharacters") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CharacterClass"); + + b.Navigation("ItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemOptionDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany("JoinedPossibleItemOptions") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", "ItemOptionDefinition") + .WithMany() + .HasForeignKey("ItemOptionDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemDefinition"); + + b.Navigation("ItemOptionDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinitionItemSetGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany("JoinedPossibleItemSetGroups") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", "ItemSetGroup") + .WithMany() + .HasForeignKey("ItemSetGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemDefinition"); + + b.Navigation("ItemSetGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", null) + .WithMany("RawDropItems") + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonster") + .WithMany() + .HasForeignKey("MonsterId"); + + b.Navigation("RawMonster"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroupItemDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "ItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", "ItemDropItemGroup") + .WithMany("JoinedPossibleItems") + .HasForeignKey("ItemDropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ItemDefinition"); + + b.Navigation("ItemDropItemGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemItemOfItemSet", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", "Item") + .WithMany("JoinedItemSetGroups") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOfItemSet", "ItemOfItemSet") + .WithMany() + .HasForeignKey("ItemOfItemSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemOfItemSet"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemLevelBonusTables") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOfItemSet", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", "RawBonusOption") + .WithMany() + .HasForeignKey("BonusOptionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", "RawItemSetGroup") + .WithMany("RawItems") + .HasForeignKey("ItemSetGroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawBonusOption"); + + b.Navigation("RawItemDefinition"); + + b.Navigation("RawItemSetGroup"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOption", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", "RawOptionType") + .WithMany() + .HasForeignKey("OptionTypeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawPowerUpDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOption", "PowerUpDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawOptionType"); + + b.Navigation("RawPowerUpDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawBonus") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", "BonusId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemOptionCombinationBonuses") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawBonus"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemOptions") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionLink", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", null) + .WithMany("RawItemOptions") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", "RawItemOption") + .WithMany() + .HasForeignKey("ItemOptionId"); + + b.Navigation("RawItemOption"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionOfLevel", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", null) + .WithMany("RawLevelDependentOptions") + .HasForeignKey("IncreasableItemOptionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "RawPowerUpDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionOfLevel", "PowerUpDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawPowerUpDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemOptionTypes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemSetGroups") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", "RawOptions") + .WithMany() + .HasForeignKey("OptionsId"); + + b.Navigation("RawOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSlotType", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawItemSlotTypes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.JewelMix", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawJewelMixes") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawMixedJewel") + .WithMany() + .HasForeignKey("MixedJewelId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawSingleJewel") + .WithMany() + .HasForeignKey("SingleJewelId"); + + b.Navigation("RawMixedJewel"); + + b.Navigation("RawSingleJewel"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterBody", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterHeader", "RawHeader") + .WithMany() + .HasForeignKey("HeaderId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", "RawSenderAppearance") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterBody", "SenderAppearanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawHeader"); + + b.Navigation("RawSenderAppearance"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LetterHeader", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "Receiver") + .WithMany("RawLetters") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Receiver"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.LevelBonus", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", null) + .WithMany("RawBonusPerLevel") + .HasForeignKey("ItemLevelBonusTableId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawChance") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "ChanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawChancePvp") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "ChancePvpId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawDuration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "DurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawDurationPvp") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "DurationPvpId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMagicEffects") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawChance"); + + b.Navigation("RawChancePvp"); + + b.Navigation("RawDuration"); + + b.Navigation("RawDurationPvp"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawReplacedSkill") + .WithMany() + .HasForeignKey("ReplacedSkillId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillRoot", "RawRoot") + .WithMany() + .HasForeignKey("RootId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawReplacedSkill"); + + b.Navigation("RawRoot"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinitionSkill", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", "MasterSkillDefinition") + .WithMany("JoinedRequiredMasterSkills") + .HasForeignKey("MasterSkillDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "Skill") + .WithMany() + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MasterSkillDefinition"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillRoot", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMasterSkillRoots") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", null) + .WithMany("RawChangeEvents") + .HasForeignKey("MiniGameDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterSpawnArea", "RawSpawnArea") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", "SpawnAreaId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawTargetDefinition") + .WithMany() + .HasForeignKey("TargetDefinitionId"); + + b.Navigation("RawSpawnArea"); + + b.Navigation("RawTargetDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawEntrance") + .WithMany() + .HasForeignKey("EntranceId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMiniGameDefinitions") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawTicketItem") + .WithMany() + .HasForeignKey("TicketItemId"); + + b.Navigation("RawEntrance"); + + b.Navigation("RawTicketItem"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameRankingEntry", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", "RawCharacter") + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", "RawMiniGame") + .WithMany() + .HasForeignKey("MiniGameId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawCharacter"); + + b.Navigation("RawMiniGame"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameReward", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "RawItemReward") + .WithMany() + .HasForeignKey("ItemRewardId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", null) + .WithMany("RawRewards") + .HasForeignKey("MiniGameDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawRequiredKill") + .WithMany() + .HasForeignKey("RequiredKillId"); + + b.Navigation("RawItemReward"); + + b.Navigation("RawRequiredKill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameSpawnWave", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", null) + .WithMany("RawSpawnWaves") + .HasForeignKey("MiniGameDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameTerrainChange", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", null) + .WithMany("RawTerrainChanges") + .HasForeignKey("MiniGameChangeEventId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterAttribute", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttributeDefinition") + .WithMany() + .HasForeignKey("AttributeDefinitionId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawAttributes") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttributeDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawAttackSkill") + .WithMany() + .HasForeignKey("AttackSkillId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawMonsters") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", "RawMerchantStore") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "MerchantStoreId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttackSkill"); + + b.Navigation("RawMerchantStore"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinitionDropItemGroup", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "DropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "MonsterDefinition") + .WithMany("JoinedDropItemGroups") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DropItemGroup"); + + b.Navigation("MonsterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterSpawnArea", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", "RawGameMap") + .WithMany("RawMonsterSpawns") + .HasForeignKey("GameMapId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonsterDefinition") + .WithMany() + .HasForeignKey("MonsterDefinitionId"); + + b.Navigation("RawGameMap"); + + b.Navigation("RawMonsterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PlugInConfiguration", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawPlugInConfigurations") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", "RawBoost") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinition", "BoostId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawCharacterPowerUpDefinitions") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", null) + .WithMany("RawPowerUpDefinitions") + .HasForeignKey("MagicEffectDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", null) + .WithMany("RawPowerUpDefinitionsPvp") + .HasForeignKey("MagicEffectDefinitionId1") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_PowerUpDefinition_MagicEffectDefinition_MagicEffectDefinit~1"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawTargetAttribute") + .WithMany() + .HasForeignKey("TargetAttributeId"); + + b.Navigation("RawBoost"); + + b.Navigation("RawTargetAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", null) + .WithMany("RawQuests") + .HasForeignKey("MonsterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawQualifiedCharacter") + .WithMany() + .HasForeignKey("QualifiedCharacterId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawQuestGiver") + .WithMany() + .HasForeignKey("QuestGiverId"); + + b.Navigation("RawQualifiedCharacter"); + + b.Navigation("RawQuestGiver"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestItemRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", "RawDropItemGroup") + .WithMany() + .HasForeignKey("DropItemGroupId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItem") + .WithMany() + .HasForeignKey("ItemId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", null) + .WithMany("RawRequiredItems") + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawDropItemGroup"); + + b.Navigation("RawItem"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirement", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", "RawMonster") + .WithMany() + .HasForeignKey("MonsterId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", null) + .WithMany("RawRequiredMonsterKills") + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawMonster"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirementState", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", null) + .WithMany("RawRequirementStates") + .HasForeignKey("CharacterQuestStateId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestMonsterKillRequirement", "RawRequirement") + .WithMany() + .HasForeignKey("RequirementId"); + + b.Navigation("RawRequirement"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestReward", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttributeReward") + .WithMany() + .HasForeignKey("AttributeRewardId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", "RawItemReward") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestReward", "ItemRewardId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", null) + .WithMany("RawRewards") + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkillReward") + .WithMany() + .HasForeignKey("SkillRewardId"); + + b.Navigation("RawAttributeReward"); + + b.Navigation("RawItemReward"); + + b.Navigation("RawSkillReward"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AreaSkillSettings", "RawAreaSkillSettings") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "AreaSkillSettingsId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawElementalModifierTarget") + .WithMany() + .HasForeignKey("ElementalModifierTargetId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawSkills") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", "RawMagicEffectDef") + .WithMany() + .HasForeignKey("MagicEffectDefId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", "RawMasterDefinition") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "MasterDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAreaSkillSettings"); + + b.Navigation("RawElementalModifierTarget"); + + b.Navigation("RawMagicEffectDef"); + + b.Navigation("RawMasterDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillCharacterClass", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "CharacterClass") + .WithMany() + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "Skill") + .WithMany("JoinedQualifiedCharacters") + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CharacterClass"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboStep", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", null) + .WithMany("RawSteps") + .HasForeignKey("SkillComboDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkill") + .WithMany() + .HasForeignKey("SkillId"); + + b.Navigation("RawSkill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillEntry", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany("RawLearnedSkills") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", "RawSkill") + .WithMany() + .HasForeignKey("SkillId"); + + b.Navigation("RawSkill"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttribute", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany("RawAttributes") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany("RawAttributes") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawDefinition") + .WithMany() + .HasForeignKey("DefinitionId"); + + b.Navigation("RawDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.StatAttributeDefinition", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.AttributeDefinition", "RawAttribute") + .WithMany() + .HasForeignKey("AttributeId"); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", null) + .WithMany("RawStatAttributes") + .HasForeignKey("CharacterClassId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RawAttribute"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.WarpInfo", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", null) + .WithMany("RawWarpList") + .HasForeignKey("GameConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ExitGate", "RawGate") + .WithMany() + .HasForeignKey("GateId"); + + b.Navigation("RawGate"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", b => + { + b.Navigation("JoinedUnlockedCharacterClasses"); + + b.Navigation("RawAttributes"); + + b.Navigation("RawCharacters"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => + { + b.Navigation("RawEquippedItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeConfiguration", b => + { + b.Navigation("RawAttackMachineZones"); + + b.Navigation("RawDefenseMachineZones"); + + b.Navigation("RawGateDefenseUpgrades"); + + b.Navigation("RawGateLifeUpgrades"); + + b.Navigation("RawNpcDefinitions"); + + b.Navigation("RawStateSchedule"); + + b.Navigation("RawStatueDefenseUpgrades"); + + b.Navigation("RawStatueLifeUpgrades"); + + b.Navigation("RawStatueRegenUpgrades"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CastleSiegeData", b => + { + b.Navigation("RawGuilds"); + + b.Navigation("RawNpcStates"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", b => + { + b.Navigation("JoinedDropItemGroups"); + + b.Navigation("RawAttributes"); + + b.Navigation("RawLearnedSkills"); + + b.Navigation("RawLetters"); + + b.Navigation("RawQuestStates"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", b => + { + b.Navigation("RawAttributeCombinations"); + + b.Navigation("RawBaseAttributeValues"); + + b.Navigation("RawStatAttributes"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterQuestState", b => + { + b.Navigation("RawRequirementStates"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ChatServerDefinition", b => + { + b.Navigation("RawEndpoints"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DropItemGroup", b => + { + b.Navigation("JoinedPossibleItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.DuelConfiguration", b => + { + b.Navigation("RawDuelAreas"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", b => + { + b.Navigation("RawAttributes"); + + b.Navigation("RawCharacterClasses"); + + b.Navigation("RawDropItemGroups"); + + b.Navigation("RawGlobalAttributeCombinations"); + + b.Navigation("RawGlobalBaseAttributeValues"); + + b.Navigation("RawItemLevelBonusTables"); + + b.Navigation("RawItemOptionCombinationBonuses"); + + b.Navigation("RawItemOptionTypes"); + + b.Navigation("RawItemOptions"); + + b.Navigation("RawItemSetGroups"); + + b.Navigation("RawItemSlotTypes"); + + b.Navigation("RawItems"); + + b.Navigation("RawJewelMixes"); + + b.Navigation("RawMagicEffects"); + + b.Navigation("RawMaps"); + + b.Navigation("RawMasterSkillRoots"); + + b.Navigation("RawMiniGameDefinitions"); + + b.Navigation("RawMonsters"); + + b.Navigation("RawPlugInConfigurations"); + + b.Navigation("RawSkills"); + + b.Navigation("RawWarpList"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", b => + { + b.Navigation("JoinedDropItemGroups"); + + b.Navigation("RawCharacterPowerUpDefinitions"); + + b.Navigation("RawEnterGates"); + + b.Navigation("RawExitGates"); + + b.Navigation("RawMapRequirements"); + + b.Navigation("RawMonsterSpawns"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerConfiguration", b => + { + b.Navigation("JoinedMaps"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GameServerDefinition", b => + { + b.Navigation("RawEndpoints"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", b => + { + b.Navigation("RawMembers"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.IncreasableItemOption", b => + { + b.Navigation("RawLevelDependentOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Item", b => + { + b.Navigation("JoinedItemSetGroups"); + + b.Navigation("RawItemOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemAppearance", b => + { + b.Navigation("JoinedVisibleOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemCraftingRequiredItem", b => + { + b.Navigation("JoinedPossibleItems"); + + b.Navigation("JoinedRequiredItemOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", b => + { + b.Navigation("JoinedPossibleItemOptions"); + + b.Navigation("JoinedPossibleItemSetGroups"); + + b.Navigation("JoinedQualifiedCharacters"); + + b.Navigation("RawBasePowerUpAttributes"); + + b.Navigation("RawDropItems"); + + b.Navigation("RawRequirements"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDropItemGroup", b => + { + b.Navigation("JoinedPossibleItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemLevelBonusTable", b => + { + b.Navigation("RawBonusPerLevel"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionCombinationBonus", b => + { + b.Navigation("RawRequirements"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemOptionDefinition", b => + { + b.Navigation("RawPossibleOptions"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemSetGroup", b => + { + b.Navigation("RawItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemStorage", b => + { + b.Navigation("RawItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MagicEffectDefinition", b => + { + b.Navigation("RawPowerUpDefinitions"); + + b.Navigation("RawPowerUpDefinitionsPvp"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MasterSkillDefinition", b => + { + b.Navigation("JoinedRequiredMasterSkills"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameChangeEvent", b => + { + b.Navigation("RawTerrainChanges"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MiniGameDefinition", b => + { + b.Navigation("RawChangeEvents"); + + b.Navigation("RawRewards"); + + b.Navigation("RawSpawnWaves"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.MonsterDefinition", b => + { + b.Navigation("JoinedDropItemGroups"); + + b.Navigation("RawAttributes"); + + b.Navigation("RawBuffs"); + + b.Navigation("RawItemCraftings"); + + b.Navigation("RawQuests"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.PowerUpDefinitionValue", b => + { + b.Navigation("RawRelatedValues"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.QuestDefinition", b => + { + b.Navigation("RawRequiredItems"); + + b.Navigation("RawRequiredMonsterKills"); + + b.Navigation("RawRewards"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SimpleCraftingSettings", b => + { + b.Navigation("RawRequiredItems"); + + b.Navigation("RawResultItems"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.Skill", b => + { + b.Navigation("JoinedQualifiedCharacters"); + + b.Navigation("RawAttributeRelationships"); + + b.Navigation("RawConsumeRequirements"); + + b.Navigation("RawRequirements"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.SkillComboDefinition", b => + { + b.Navigation("RawSteps"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/EntityFramework/Migrations/20260926142218_AddItemRuleFlags.cs b/src/Persistence/EntityFramework/Migrations/20260926142218_AddItemRuleFlags.cs new file mode 100644 index 0000000000..b199035159 --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20260926142218_AddItemRuleFlags.cs @@ -0,0 +1,99 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations +{ + /// + /// Adds the item rule flags. Existing items allow everything; the + /// AddItemRuleFlagsPlugIn update sets the Season 6 values. + /// + public partial class AddItemRuleFlags : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsDroppable", + schema: "config", + table: "ItemDefinition", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "IsPersonalStoreSellable", + schema: "config", + table: "ItemDefinition", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "IsRepairable", + schema: "config", + table: "ItemDefinition", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "IsSellableToNpc", + schema: "config", + table: "ItemDefinition", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "IsStorable", + schema: "config", + table: "ItemDefinition", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "IsTradable", + schema: "config", + table: "ItemDefinition", + type: "boolean", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsDroppable", + schema: "config", + table: "ItemDefinition"); + + migrationBuilder.DropColumn( + name: "IsPersonalStoreSellable", + schema: "config", + table: "ItemDefinition"); + + migrationBuilder.DropColumn( + name: "IsRepairable", + schema: "config", + table: "ItemDefinition"); + + migrationBuilder.DropColumn( + name: "IsSellableToNpc", + schema: "config", + table: "ItemDefinition"); + + migrationBuilder.DropColumn( + name: "IsStorable", + schema: "config", + table: "ItemDefinition"); + + migrationBuilder.DropColumn( + name: "IsTradable", + schema: "config", + table: "ItemDefinition"); + } + } +} diff --git a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs index 517184d7a1..da079b1429 100644 --- a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs +++ b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs @@ -2098,9 +2098,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsBoundToCharacter") .HasColumnType("boolean"); + b.Property("IsDroppable") + .HasColumnType("boolean"); + + b.Property("IsPersonalStoreSellable") + .HasColumnType("boolean"); + b.Property("IsQuestItem") .HasColumnType("boolean"); + b.Property("IsRepairable") + .HasColumnType("boolean"); + + b.Property("IsSellableToNpc") + .HasColumnType("boolean"); + + b.Property("IsStorable") + .HasColumnType("boolean"); + + b.Property("IsTradable") + .HasColumnType("boolean"); + b.Property("ItemSlotId") .HasColumnType("uuid"); From 8d2ad09957e9c8912056500f7d849f849cd4de56 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Sat, 26 Sep 2026 16:33:38 +0200 Subject: [PATCH 2/7] feat(items): check the item rule flags in the item actions - MoveItemAction: an item can't be moved into a trade, the vault or the personal store when IsTradable, IsStorable or IsPersonalStoreSellable is false. - SellItemToNpcAction: IsSellableToNpc. - DropItemAction: IsDroppable. - ItemRepairAction: IsRepairable; repair all skips such items. A refused action shows a message to the player and logs a warning: the client should not have sent it, so its item data may differ from the server (or the client is modified). IsBoundToCharacter keeps working as before. --- .../PlayerActions/Items/DropItemAction.cs | 11 +++ .../PlayerActions/Items/ItemRepairAction.cs | 12 ++- .../PlayerActions/Items/MoveItemAction.cs | 33 +++++++ .../Items/SellItemToNpcAction.cs | 11 +++ .../Properties/PlayerMessage.Designer.cs | 54 +++++++++++ src/GameLogic/Properties/PlayerMessage.resx | 18 ++++ .../ItemRuleFlagsTests.cs | 92 +++++++++++++++++++ .../MoveItemActionTests.cs | 65 +++++++++++++ 8 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 tests/MUnique.OpenMU.Tests/ItemRuleFlagsTests.cs diff --git a/src/GameLogic/PlayerActions/Items/DropItemAction.cs b/src/GameLogic/PlayerActions/Items/DropItemAction.cs index 7dd55977c9..5d96add9ec 100644 --- a/src/GameLogic/PlayerActions/Items/DropItemAction.cs +++ b/src/GameLogic/PlayerActions/Items/DropItemAction.cs @@ -38,6 +38,17 @@ public async ValueTask DropItemAsync(Player player, byte slot, Point target) return; } + if (item.Definition is { IsDroppable: false }) + { + player.Logger.LogWarning( + "Player {0} tried to drop {1}, which its item definition doesn't allow. The client item data may differ from the server.", + player, + item); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ItemCannotBeDropped)).ConfigureAwait(false); + await player.InvokeViewPlugInAsync(p => p.ItemDropResultAsync(slot, false)).ConfigureAwait(false); + return; + } + if (player.GameContext.PlugInManager.GetPlugInPoint() is { } plugInPoint) { var dropArguments = new ItemDropArguments(); diff --git a/src/GameLogic/PlayerActions/Items/ItemRepairAction.cs b/src/GameLogic/PlayerActions/Items/ItemRepairAction.cs index 9d520d45eb..2b208276be 100644 --- a/src/GameLogic/PlayerActions/Items/ItemRepairAction.cs +++ b/src/GameLogic/PlayerActions/Items/ItemRepairAction.cs @@ -35,6 +35,16 @@ public async ValueTask RepairItemAsync(Player player, byte slot) return; } + if (item.Definition is { IsRepairable: false }) + { + player.Logger.LogWarning( + "Player {0} tried to repair {1}, which its item definition doesn't allow. The client item data may differ from the server.", + player, + item); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ItemCannotBeRepaired)).ConfigureAwait(false); + return; + } + if ((byte)item.Durability == item.GetMaximumDurabilityOfOnePiece()) { return; @@ -77,7 +87,7 @@ public async ValueTask RepairAllItemsAsync(Player player) } var item = player.Inventory?.GetItem(i); - if (item is null) + if (item is null || item.Definition is { IsRepairable: false }) { continue; } diff --git a/src/GameLogic/PlayerActions/Items/MoveItemAction.cs b/src/GameLogic/PlayerActions/Items/MoveItemAction.cs index a7b5b9851e..1a9f78be06 100644 --- a/src/GameLogic/PlayerActions/Items/MoveItemAction.cs +++ b/src/GameLogic/PlayerActions/Items/MoveItemAction.cs @@ -56,6 +56,18 @@ public async ValueTask MoveItemAsync(Player player, byte fromSlot, Storages from return; } + if (fromStorage != toStorage && GetRuleBlockingMove(item, toStorage) is { } blockedMessage) + { + player.Logger.LogWarning( + "Player {0} tried to move {1} into the {2}, which its item definition doesn't allow. The client item data may differ from the server.", + player, + item, + toStorage); + await player.ShowLocalizedBlueMessageAsync(blockedMessage).ConfigureAwait(false); + await player.InvokeViewPlugInAsync(p => p.ItemMoveFailedAsync(item)).ConfigureAwait(false); + return; + } + var toStorageInfo = this.GetStorageInfo(player, toStorage); var toItemStorage = toStorageInfo?.Storage; @@ -100,6 +112,27 @@ public async ValueTask MoveItemAsync(Player player, byte fromSlot, Storages from } } + /// + /// Gets the message to show when the item rules of the item definition don't allow + /// moving it into the target storage, or null when they do. + /// + private static string? GetRuleBlockingMove(Item item, Storages toStorage) + { + var definition = item.Definition; + if (definition is null) + { + return null; + } + + return toStorage switch + { + Storages.Trade when !definition.IsTradable => nameof(PlayerMessage.ItemCannotBeTraded), + Storages.Vault when !definition.IsStorable => nameof(PlayerMessage.ItemCannotBeStored), + Storages.PersonalStore when !definition.IsPersonalStoreSellable => nameof(PlayerMessage.ItemCannotBeSoldInPersonalStore), + _ => null, + }; + } + private async ValueTask FullStackAsync(Player player, IStorage sourceStorage, Item sourceItem, Item targetItem) { targetItem.Durability += sourceItem.Durability; diff --git a/src/GameLogic/PlayerActions/Items/SellItemToNpcAction.cs b/src/GameLogic/PlayerActions/Items/SellItemToNpcAction.cs index 6b9c2a538a..85db709869 100644 --- a/src/GameLogic/PlayerActions/Items/SellItemToNpcAction.cs +++ b/src/GameLogic/PlayerActions/Items/SellItemToNpcAction.cs @@ -47,6 +47,17 @@ public async ValueTask SellItemAsync(Player player, byte slot) return false; } + if (item.Definition is { IsSellableToNpc: false }) + { + player.Logger.LogWarning( + "Player {0} tried to sell {1} to an npc, which its item definition doesn't allow. The client item data may differ from the server.", + player, + item); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ItemCannotBeSoldToNpc)).ConfigureAwait(false); + await player.InvokeViewPlugInAsync(p => p.ItemSoldToNpcAsync(false)).ConfigureAwait(false); + return false; + } + if (item.Definition is null || (item.Definition.IsBoundToCharacter && (item.Definition.Durability == 0 || item.Durability > 0))) { await player.InvokeViewPlugInAsync(p => p.ItemSoldToNpcAsync(false)).ConfigureAwait(false); diff --git a/src/GameLogic/Properties/PlayerMessage.Designer.cs b/src/GameLogic/Properties/PlayerMessage.Designer.cs index 49588da0bd..b1b9722d2b 100644 --- a/src/GameLogic/Properties/PlayerMessage.Designer.cs +++ b/src/GameLogic/Properties/PlayerMessage.Designer.cs @@ -843,6 +843,60 @@ public static string ItemGroupNumberNotExists { } } + /// + /// Looks up a localized string similar to This item can't be dropped.. + /// + public static string ItemCannotBeDropped { + get { + return ResourceManager.GetString("ItemCannotBeDropped", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This item can't be repaired.. + /// + public static string ItemCannotBeRepaired { + get { + return ResourceManager.GetString("ItemCannotBeRepaired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This item can't be sold in a personal store.. + /// + public static string ItemCannotBeSoldInPersonalStore { + get { + return ResourceManager.GetString("ItemCannotBeSoldInPersonalStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This item can't be sold.. + /// + public static string ItemCannotBeSoldToNpc { + get { + return ResourceManager.GetString("ItemCannotBeSoldToNpc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This item can't be stored in the vault.. + /// + public static string ItemCannotBeStored { + get { + return ResourceManager.GetString("ItemCannotBeStored", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This item can't be traded.. + /// + public static string ItemCannotBeTraded { + get { + return ResourceManager.GetString("ItemCannotBeTraded", resourceCulture); + } + } + /// /// Looks up a localized string similar to This item is bound to the inventory of this character.. /// diff --git a/src/GameLogic/Properties/PlayerMessage.resx b/src/GameLogic/Properties/PlayerMessage.resx index d04c613c4e..b588f4c94f 100644 --- a/src/GameLogic/Properties/PlayerMessage.resx +++ b/src/GameLogic/Properties/PlayerMessage.resx @@ -579,6 +579,24 @@ This item is bound to the inventory of this character. + + This item can't be dropped. + + + This item can't be repaired. + + + This item can't be sold in a personal store. + + + This item can't be sold. + + + This item can't be stored in the vault. + + + This item can't be traded. + Not enough Zen to send a letter. diff --git a/tests/MUnique.OpenMU.Tests/ItemRuleFlagsTests.cs b/tests/MUnique.OpenMU.Tests/ItemRuleFlagsTests.cs new file mode 100644 index 0000000000..1c9d224394 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/ItemRuleFlagsTests.cs @@ -0,0 +1,92 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using Moq; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.NPC; +using MUnique.OpenMU.GameLogic.PlayerActions.Items; +using MUnique.OpenMU.Pathfinding; + +/// +/// Tests that the item rule flags of are enforced by the +/// drop, sell and repair actions. The storage moves are tested in . +/// +[TestFixture] +public class ItemRuleFlagsTests +{ + private const byte InventorySlot = 20; + + /// + /// Verifies that an item which isn't droppable stays in the inventory, while a droppable one is dropped. + /// + /// If set to true, the item is droppable. + [TestCase(true)] + [TestCase(false)] + public async ValueTask ItemIsOnlyDroppedWhenDroppableAsync(bool isDroppable) + { + var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + var item = CreateItem(definition => definition.IsDroppable = isDroppable); + await player.Inventory!.AddItemAsync(InventorySlot, item).ConfigureAwait(false); + + await new DropItemAction().DropItemAsync(player, InventorySlot, new Point(10, 10)).ConfigureAwait(false); + + Assert.That(player.Inventory.GetItem(InventorySlot), isDroppable ? Is.Null : Is.SameAs(item)); + } + + /// + /// Verifies that an item which isn't sellable to an NPC isn't sold. + /// + [Test] + public async ValueTask ItemWhichIsNotSellableToNpcIsNotSoldAsync() + { + var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + player.OpenedNpc = new NonPlayerCharacter(null!, new MonsterDefinition { MerchantStore = new Mock().Object }, null!); + var item = CreateItem(definition => definition.IsSellableToNpc = false); + await player.Inventory!.AddItemAsync(InventorySlot, item).ConfigureAwait(false); + + var sold = await new SellItemToNpcAction().SellItemAsync(player, InventorySlot).ConfigureAwait(false); + + Assert.That(sold, Is.False); + Assert.That(player.Inventory.GetItem(InventorySlot), Is.SameAs(item)); + } + + /// + /// Verifies that an item which isn't repairable isn't repaired, while a repairable one is. + /// The item can't be worn, so its durability is the number of pieces, and repairing would reduce it. + /// + /// If set to true, the item is repairable. + [TestCase(true)] + [TestCase(false)] + public async ValueTask ItemIsOnlyRepairedWhenRepairableAsync(bool isRepairable) + { + var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + player.Money = 1_000_000; + var item = CreateItem(definition => definition.IsRepairable = isRepairable); + item.Durability = 5; + await player.Inventory!.AddItemAsync(InventorySlot, item).ConfigureAwait(false); + + await new ItemRepairAction().RepairItemAsync(player, InventorySlot).ConfigureAwait(false); + + Assert.That(item.Durability, isRepairable ? Is.Not.EqualTo(5) : Is.EqualTo(5)); + } + + private static Item CreateItem(Action setRule) + { + var definition = new ItemDefinition { Width = 1, Height = 1, Durability = 10 }; + setRule(definition); + + var item = new Mock(); + item.SetupAllProperties(); + item.Setup(i => i.ItemOptions).Returns(new List()); + item.Setup(i => i.ItemSetGroups).Returns(new List()); + item.Object.Definition = definition; + item.Object.Durability = 1; + return item.Object; + } +} diff --git a/tests/MUnique.OpenMU.Tests/MoveItemActionTests.cs b/tests/MUnique.OpenMU.Tests/MoveItemActionTests.cs index 0475a19501..3599e2f1c1 100644 --- a/tests/MUnique.OpenMU.Tests/MoveItemActionTests.cs +++ b/tests/MUnique.OpenMU.Tests/MoveItemActionTests.cs @@ -239,6 +239,71 @@ public async ValueTask LogoutAndRelogAfterInventoryToVaultMoveKeepsSinglePersist Assert.That(vaultStorage.Items.Count(i => ReferenceEquals(i, movedItem)), Is.EqualTo(1)); } + /// + /// Verifies that an item which isn't tradable can't be put into a trade, while a tradable one can. + /// + [Test] + public async ValueTask ItemWhichIsNotTradableIsNotMovedIntoTradeAsync() + { + var trader1 = await CreateTestPlayerAsync().ConfigureAwait(false); + var trader2 = await CreateTestPlayerAsync().ConfigureAwait(false); + var notTradable = CreateItem(CreateDefinition(), 1); + notTradable.Definition!.IsTradable = false; + var tradable = CreateItem(CreateDefinition(), 1); + await trader1.Inventory!.AddItemAsync(20, notTradable).ConfigureAwait(false); + await trader1.Inventory.AddItemAsync(21, tradable).ConfigureAwait(false); + + await new TradeRequestAction().RequestTradeAsync(trader1, trader2).ConfigureAwait(false); + await new TradeAcceptAction().HandleTradeAcceptAsync(trader2, true).ConfigureAwait(false); + + var action = new MoveItemAction(); + await action.MoveItemAsync(trader1, 20, Storages.Inventory, 0, Storages.Trade).ConfigureAwait(false); + await action.MoveItemAsync(trader1, 21, Storages.Inventory, 1, Storages.Trade).ConfigureAwait(false); + + Assert.That(trader1.Inventory.GetItem(20), Is.SameAs(notTradable)); + Assert.That(trader1.TemporaryStorage!.GetItem(0), Is.Null); + Assert.That(trader1.TemporaryStorage.GetItem(1), Is.SameAs(tradable)); + } + + /// + /// Verifies that an item which isn't storable can't be moved into the vault. + /// + [Test] + public async ValueTask ItemWhichIsNotStorableIsNotMovedIntoVaultAsync() + { + var player = await CreateTestPlayerAsync().ConfigureAwait(false); + var vaultStorage = CreateVaultStorage(); + player.Vault = vaultStorage; + player.OpenedNpc = new NonPlayerCharacter(null!, new MonsterDefinition { NpcWindow = NpcWindow.VaultStorage }, null!); + Assert.That(await player.PlayerState.TryAdvanceToAsync(PlayerState.NpcDialogOpened).ConfigureAwait(false), Is.True); + + var item = CreateItem(CreateDefinition(), 1); + item.Definition!.IsStorable = false; + await player.Inventory!.AddItemAsync(20, item).ConfigureAwait(false); + + await new MoveItemAction().MoveItemAsync(player, 20, Storages.Inventory, 0, Storages.Vault).ConfigureAwait(false); + + Assert.That(player.Inventory.GetItem(20), Is.SameAs(item)); + Assert.That(vaultStorage.GetItem(0), Is.Null); + } + + /// + /// Verifies that an item which isn't sellable in a personal store can't be moved into it. + /// + [Test] + public async ValueTask ItemWhichIsNotPersonalStoreSellableIsNotMovedIntoPersonalStoreAsync() + { + var player = await CreateTestPlayerAsync().ConfigureAwait(false); + var item = CreateItem(CreateDefinition(), 1); + item.Definition!.IsPersonalStoreSellable = false; + await player.Inventory!.AddItemAsync(20, item).ConfigureAwait(false); + + await new MoveItemAction().MoveItemAsync(player, 20, Storages.Inventory, InventoryConstants.FirstStoreItemSlotIndex, Storages.PersonalStore).ConfigureAwait(false); + + Assert.That(player.Inventory.GetItem(20), Is.SameAs(item)); + Assert.That(player.ShopStorage!.GetItem(InventoryConstants.FirstStoreItemSlotIndex), Is.Null); + } + private static Storage CreateVaultStorage() { var itemStorage = new Mock(); From 1ac2de57e9fa93df55b86ad4729b279e41bd1f5b Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Sat, 26 Sep 2026 16:33:38 +0200 Subject: [PATCH 3/7] feat(items): set the item rules of the Season 6 items ItemRules lists the Season 6 items which don't allow every action, with the values of the MuMain client item data (src/bin/Data/Items). It is applied by the Season 6 initialization and, for existing databases, by the mandatory AddItemRuleFlagsPlugIn update. Two differences to the client data: - The Wizards Ring is bound to the character here, so it is not tradable, storable or sellable in a personal store (the client allows that at +0; its data will be adjusted). - The Dark Horse and Dark Raven stay repairable, because the server repairs trainable pets. --- .../Updates/AddItemRuleFlagsPlugIn.cs | 59 ++++ .../Initialization/Updates/UpdateVersion.cs | 5 + .../GameConfigurationInitializer.cs | 3 + .../VersionSeasonSix/Items/ItemRules.cs | 268 ++++++++++++++++++ .../ItemRulesDataTest.cs | 117 ++++++++ 5 files changed, 452 insertions(+) create mode 100644 src/Persistence/Initialization/Updates/AddItemRuleFlagsPlugIn.cs create mode 100644 src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs create mode 100644 tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemRulesDataTest.cs diff --git a/src/Persistence/Initialization/Updates/AddItemRuleFlagsPlugIn.cs b/src/Persistence/Initialization/Updates/AddItemRuleFlagsPlugIn.cs new file mode 100644 index 0000000000..bfe1d1badd --- /dev/null +++ b/src/Persistence/Initialization/Updates/AddItemRuleFlagsPlugIn.cs @@ -0,0 +1,59 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Updates; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Items; +using MUnique.OpenMU.PlugIns; + +/// +/// This update sets the new item rule flags (, +/// , , +/// , +/// and ) on the existing Season 6 items, see . +/// The database migration adds them allowing everything. +/// +[PlugIn] +[Display(Name = PlugInName, Description = PlugInDescription)] +[Guid("C0F83AEA-C8E1-4E40-9B85-D344B7F93238")] +public class AddItemRuleFlagsPlugIn : UpdatePlugInBase +{ + /// + /// The plug in name. + /// + internal const string PlugInName = "Add item rule flags"; + + /// + /// The plug in description. + /// + internal const string PlugInDescription = "This update sets which items can't be traded, dropped, stored, sold to NPCs, offered in personal stores or repaired, matching the client item data."; + + /// + public override string Name => PlugInName; + + /// + public override string Description => PlugInDescription; + + /// + public override UpdateVersion Version => UpdateVersion.AddItemRuleFlags; + + /// + public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id; + + /// + public override bool IsMandatory => true; + + /// + public override DateTime CreatedAt => new(2026, 09, 26, 16, 0, 0, DateTimeKind.Utc); + + /// + protected override ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) + { + ItemRules.Apply(gameConfiguration); + return ValueTask.CompletedTask; + } +} diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs index a946148541..badc93514d 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -584,4 +584,9 @@ public enum UpdateVersion /// The version of the . /// AddDoppelgangerData = 115, + + /// + /// The version of the . + /// + AddItemRuleFlags = 116, } diff --git a/src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs b/src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs index a7a0ad04a5..97a3998fc8 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/GameConfigurationInitializer.cs @@ -92,6 +92,9 @@ public override void Initialize() new CastleSiegeInitializer(this.Context, this.GameConfiguration).Initialize(); new KanturuInitializer(this.Context, this.GameConfiguration).Initialize(); new DoppelgangerInitializer(this.Context, this.GameConfiguration).Initialize(); + + // After all items exist. + new ItemRules(this.Context, this.GameConfiguration).Initialize(); } /// diff --git a/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs b/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs new file mode 100644 index 0000000000..6226068b8e --- /dev/null +++ b/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs @@ -0,0 +1,268 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Items; + +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; + +/// +/// Sets the item rules (, , +/// , , +/// and ) +/// of the Season 6 items. +/// +/// +/// The values are the ones of the MuMain client item data (src/bin/Data/Items), so the client and the +/// server allow the same actions. Items which are not listed allow everything. +/// Two exceptions: +/// +/// The Wizards Ring: the client allows trading and storing it at level 0, but here it is +/// , so its flags follow that. +/// The Dark Horse and the Dark Raven stay repairable: the server repairs trainable pets +/// (with their own repair price), while the client has no way to repair them. +/// +/// +public class ItemRules : InitializerBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// The game configuration. + public ItemRules(IContext context, GameConfiguration gameConfiguration) + : base(context, gameConfiguration) + { + } + + /// + /// The actions an item rule blocks. + /// + [Flags] + public enum Blocked + { + /// + /// The item can't be traded. + /// + Trade = 1, + + /// + /// The item can't be dropped. + /// + Drop = 2, + + /// + /// The item can't be stored in the vault. + /// + Store = 4, + + /// + /// The item can't be sold to an NPC. + /// + SellToNpc = 8, + + /// + /// The item can't be offered in a personal store. + /// + PersonalStore = 16, + + /// + /// The item can't be repaired. + /// + Repair = 32, + } + + /// + /// Gets the items which don't allow every action, with the actions they block. + /// + public static IReadOnlyList<(byte Group, short Number, Blocked Blocked)> Rules { get; } = new (byte, short, Blocked)[] + { + (4, 7, Blocked.Repair), // Bolt + (4, 15, Blocked.Repair), // Arrow + (12, 7, Blocked.Repair), // Orb of Twisting Slash + (12, 8, Blocked.Repair), // Healing Orb + (12, 9, Blocked.Repair), // Orb of Greater Fortitude + (12, 10, Blocked.Repair), // Orb of Greater Damage + (12, 11, Blocked.Repair), // Orb of Summoning + (12, 12, Blocked.Repair), // Orb of Rageful Blow + (12, 13, Blocked.Repair), // Orb of Impale + (12, 14, Blocked.Repair), // Orb of Greater Fortitude + (12, 15, Blocked.Repair), // Jewel of Chaos + (12, 16, Blocked.Repair), // Orb of Fire Slash + (12, 17, Blocked.Repair), // Orb of Penetration + (12, 18, Blocked.Repair), // Orb of Ice Arrow + (12, 19, Blocked.Repair), // Orb of Death Stab + (13, 0, Blocked.Repair), // Guardian Angel + (13, 1, Blocked.Repair), // Imp + (13, 2, Blocked.Repair), // Horn of Uniria + (13, 3, Blocked.Repair), // Horn of Dinorant + (13, 10, Blocked.Repair), // Transformation Ring + (13, 11, Blocked.Repair), // Order (Guardian/Life Stone) + (13, 14, Blocked.Repair), // Loch's Feather + (13, 15, Blocked.Repair), // Fruits + (13, 16, Blocked.Repair), // Scroll of Archangel + (13, 17, Blocked.Repair), // Blood Bone + (13, 18, Blocked.Repair), // Invisibility Cloak + (13, 19, Blocked.Trade | Blocked.Store | Blocked.SellToNpc | Blocked.PersonalStore | Blocked.Repair), // Absolute Weapon of Archangel + (13, 20, Blocked.Trade | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Wizards Ring (bound to the character, see the class remarks) + (13, 29, Blocked.Repair), // Armor of Guardsman + (13, 32, Blocked.Repair), // Splinter of Armor + (13, 33, Blocked.Repair), // Bless of Guardian + (13, 34, Blocked.Repair), // Claw of Beast + (13, 35, Blocked.Repair), // Fragment of Horn + (13, 36, Blocked.Repair), // Broken Horn + (13, 37, Blocked.Repair), // Horn of Fenrir + (13, 38, Blocked.Trade | Blocked.PersonalStore | Blocked.Repair), // Moonstone Pendant + (13, 39, Blocked.Trade | Blocked.PersonalStore | Blocked.Repair), // Eilte Transfer Skeleton Ring + (13, 40, Blocked.Repair), // Jack Olantern Ring + (13, 41, Blocked.Repair), // Transfer Christmas Ring + (13, 42, Blocked.Repair), // Ring of GM + (13, 49, Blocked.Repair), // Old Scroll + (13, 50, Blocked.Repair), // Illusion Sorcerer Covenant + (13, 51, Blocked.Repair), // Scroll of Blood + (13, 64, Blocked.Trade | Blocked.SellToNpc | Blocked.Repair), // Demon + (13, 65, Blocked.Trade | Blocked.SellToNpc | Blocked.Repair), // Spirit of Guardian + (13, 67, Blocked.Repair), // Pet Rudolf + (13, 68, Blocked.Repair), // Snowman Transformation Ring + (13, 76, Blocked.Trade | Blocked.SellToNpc | Blocked.Repair), // Panda Ring + (13, 80, Blocked.Trade | Blocked.SellToNpc | Blocked.Repair), // Pet Panda + (13, 106, Blocked.Trade | Blocked.Drop | Blocked.SellToNpc | Blocked.PersonalStore | Blocked.Repair), // Pet Unicorn + (13, 122, Blocked.Trade | Blocked.SellToNpc | Blocked.Repair), // Skeleton Transformation Ring + (13, 123, Blocked.Trade | Blocked.SellToNpc | Blocked.Repair), // Pet Skeleton + (14, 0, Blocked.Repair), // Apple + (14, 1, Blocked.Repair), // Small Healing Potion + (14, 2, Blocked.Repair), // Healing Potion + (14, 3, Blocked.Repair), // Large Healing Potion + (14, 4, Blocked.Repair), // Small Mana Potion + (14, 5, Blocked.Repair), // Mana Potion + (14, 6, Blocked.Repair), // Large Mana Potion + (14, 7, Blocked.Repair), // Siege Potion + (14, 8, Blocked.Repair), // Antidote + (14, 9, Blocked.Repair), // Ale + (14, 10, Blocked.Repair), // Town Portal Scroll + (14, 11, Blocked.SellToNpc | Blocked.Repair), // Box of Luck + (14, 13, Blocked.Repair), // Jewel of Bless + (14, 14, Blocked.Repair), // Jewel of Soul + (14, 16, Blocked.Repair), // Jewel of Life + (14, 17, Blocked.Repair), // Devil's Eye + (14, 18, Blocked.Repair), // Devil's Key + (14, 19, Blocked.Repair), // Devil's Invitation + (14, 21, Blocked.Trade | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Rena + (14, 22, Blocked.Repair), // Jewel of Creation + (14, 23, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Scroll of the Emperor + (14, 24, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Broken Sword + (14, 25, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Tear of Elf + (14, 26, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Soul Shard of Wizard + (14, 28, Blocked.Repair), // Lost Map + (14, 29, Blocked.Repair), // Symbol of Kundun + (14, 31, Blocked.Repair), // Jewel of Guardian + (14, 32, Blocked.Repair), // Pink Chocolate Box + (14, 33, Blocked.Repair), // Red Chocolate Box + (14, 34, Blocked.Repair), // Blue Chocolate Box + (14, 35, Blocked.Repair), // Small SD Potion + (14, 36, Blocked.Repair), // SD Potion + (14, 37, Blocked.Repair), // Large SD Potion + (14, 38, Blocked.Repair), // Small Complex Potion + (14, 39, Blocked.Repair), // Complex Potion + (14, 40, Blocked.Repair), // Large Complex Potion + (14, 41, Blocked.Repair), // Gemstone + (14, 42, Blocked.Repair), // Jewel of Harmony + (14, 43, Blocked.Repair), // Lower refining stone + (14, 44, Blocked.Repair), // Higher refining stone + (14, 45, Blocked.Repair), // Pumpkin of Luck + (14, 46, Blocked.Repair), // Jack O'Lantern Blessings + (14, 47, Blocked.Repair), // Jack O'Lantern Wrath + (14, 48, Blocked.Repair), // Jack O'Lantern Cry + (14, 49, Blocked.Repair), // Jack O'Lantern Food + (14, 50, Blocked.Repair), // Jack O'Lantern Drink + (14, 51, Blocked.Repair), // Chistmas Star + (14, 52, Blocked.Trade | Blocked.Repair), // GM Gift + (14, 63, Blocked.Repair), // Firecracker + (14, 65, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Death-beam Knight Flame + (14, 66, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Hell-Miner Horn + (14, 67, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Dark Phoenix Feather + (14, 68, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Abyssal Eye + (14, 84, Blocked.Repair), // Cherry Blossom Play-Box + (14, 85, Blocked.Repair), // Cherry Blossom Wine + (14, 86, Blocked.Repair), // Cherry Blossom Rice Cake + (14, 87, Blocked.Repair), // Cherry Blossom Flower Petal + (14, 90, Blocked.Repair), // Golden Cherry Blossom Branch + (14, 99, Blocked.Repair), // Christmas Firecracker + (14, 101, Blocked.Repair), // Suspicious Scrap of Paper + (14, 102, Blocked.Repair), // Gaion's Order + (14, 103, Blocked.Repair), // First Secromicon Fragment + (14, 104, Blocked.Repair), // Second Secromicon Fragment + (14, 105, Blocked.Repair), // Third Secromicon Fragment + (14, 106, Blocked.Repair), // Fourth Secromicon Fragment + (14, 107, Blocked.Repair), // Fifth Secromicon Fragment + (14, 108, Blocked.Repair), // Sixth Secromicon Fragment + (14, 109, Blocked.Repair), // Complete Secromicon + (15, 0, Blocked.Repair), // Scroll of Poison + (15, 1, Blocked.Repair), // Scroll of Meteorite + (15, 2, Blocked.Repair), // Scroll of Lighting + (15, 3, Blocked.Repair), // Scroll of Fire Ball + (15, 4, Blocked.Repair), // Scroll of Flame + (15, 5, Blocked.Repair), // Scroll of Teleport + (15, 6, Blocked.Repair), // Scroll of Ice + (15, 7, Blocked.Repair), // Scroll of Twister + (15, 8, Blocked.Repair), // Scroll of Evil Spirit + (15, 9, Blocked.Repair), // Scroll of Hellfire + (15, 10, Blocked.Repair), // Scroll of Power Wave + (15, 11, Blocked.Repair), // Scroll of Aqua Beam + (15, 12, Blocked.Repair), // Scroll of Cometfall + (15, 13, Blocked.Repair), // Scroll of Inferno + (15, 14, Blocked.Repair), // Scroll of Teleport Ally + (15, 15, Blocked.Repair), // Scroll of Soul Barrier + (15, 16, Blocked.Repair), // Scroll of Decay + (15, 17, Blocked.Repair), // Scroll of Ice Storm + (15, 18, Blocked.Repair), // Scroll of Nova + (15, 19, Blocked.Repair), // Chain Lightning Parchment + (15, 20, Blocked.Repair), // Drain Life Parchment + (15, 21, Blocked.Repair), // Lightning Shock Parchment + (15, 22, Blocked.Repair), // Damage Reflection Parchment + (15, 23, Blocked.Repair), // Berserker Parchment + (15, 24, Blocked.Repair), // Sleep Parchment + (15, 26, Blocked.Repair), // Weakness Parchment + (15, 27, Blocked.Repair), // Innovation Parchment + (15, 28, Blocked.Repair), // Scroll of Wizardry Enhance + (15, 29, Blocked.Repair), // Scroll of Gigantic Storm + (15, 30, Blocked.Repair), // Chain Drive Parchment + (15, 31, Blocked.Repair), // Dark Side Parchment + (15, 32, Blocked.Repair), // Dragon Roar Parchment + (15, 33, Blocked.Repair), // Dragon Slasher Parchment + (15, 34, Blocked.Repair), // Ignore Defense Parchment + (15, 35, Blocked.Repair), // Increase Health Parchment + (15, 36, Blocked.Repair), // Increase Block Parchment + }; + + /// + public override void Initialize() + { + Apply(this.GameConfiguration); + } + + /// + /// Applies the to the items of the game configuration. + /// Items which are not part of the configuration are skipped. + /// + /// The game configuration. + internal static void Apply(GameConfiguration gameConfiguration) + { + var items = gameConfiguration.Items.ToDictionary(item => (item.Group, item.Number)); + foreach (var (group, number, blocked) in Rules) + { + if (!items.TryGetValue((group, number), out var item)) + { + continue; + } + + item.IsTradable = !blocked.HasFlag(Blocked.Trade); + item.IsDroppable = !blocked.HasFlag(Blocked.Drop); + item.IsStorable = !blocked.HasFlag(Blocked.Store); + item.IsSellableToNpc = !blocked.HasFlag(Blocked.SellToNpc); + item.IsPersonalStoreSellable = !blocked.HasFlag(Blocked.PersonalStore); + item.IsRepairable = !blocked.HasFlag(Blocked.Repair); + } + } +} diff --git a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemRulesDataTest.cs b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemRulesDataTest.cs new file mode 100644 index 0000000000..1691185dea --- /dev/null +++ b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemRulesDataTest.cs @@ -0,0 +1,117 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Tests; + +using Microsoft.Extensions.Logging.Abstractions; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.Persistence.Initialization.Updates; +using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Items; +using MUnique.OpenMU.Persistence.InMemory; + +/// +/// Tests the item rule flags of the season 6 data (). +/// +[TestFixture] +internal class ItemRulesDataTest +{ + /// + /// Tests that every listed item exists once in the season 6 data. + /// + [Test] + public async Task EveryListedItemExistsAsync() + { + var gameConfiguration = await CreateSeason6ConfigurationAsync().ConfigureAwait(false); + + Assert.That(ItemRules.Rules.Select(rule => (rule.Group, rule.Number)), Is.Unique); + foreach (var (group, number, _) in ItemRules.Rules) + { + Assert.That(gameConfiguration.Items.Count(item => item.Group == group && item.Number == number), Is.EqualTo(1), $"Item ({group},{number})"); + } + } + + /// + /// Tests that a new season 6 database contains the item rules. + /// + [Test] + public async Task NewDatabaseContainsItemRulesAsync() + { + var gameConfiguration = await CreateSeason6ConfigurationAsync().ConfigureAwait(false); + + // Kris: a normal weapon allows everything. + var kris = GetItem(gameConfiguration, 0, 0); + Assert.That(new[] { kris.IsTradable, kris.IsDroppable, kris.IsStorable, kris.IsSellableToNpc, kris.IsPersonalStoreSellable, kris.IsRepairable }, Is.All.True); + + // Scroll of the Emperor: a quest item. + var scroll = GetItem(gameConfiguration, 14, 23); + Assert.That(scroll.IsTradable, Is.False); + Assert.That(scroll.IsDroppable, Is.False); + Assert.That(scroll.IsStorable, Is.False); + Assert.That(scroll.IsPersonalStoreSellable, Is.False); + Assert.That(scroll.IsRepairable, Is.False); + + // Potions can't be repaired: their durability is the number of pieces. + Assert.That(GetItem(gameConfiguration, 14, 3).IsRepairable, Is.False); + + // The Wizards Ring is bound to the character, and its flags follow that. + var wizardsRing = GetItem(gameConfiguration, 13, 20); + Assert.That(wizardsRing.IsTradable, Is.False); + Assert.That(wizardsRing.IsStorable, Is.False); + Assert.That(wizardsRing.IsPersonalStoreSellable, Is.False); + + // The trainable pets stay repairable on the server. + Assert.That(GetItem(gameConfiguration, 13, 4).IsRepairable, Is.True); + Assert.That(GetItem(gameConfiguration, 13, 5).IsRepairable, Is.True); + } + + /// + /// Tests that the update sets the same rules on a database which was created before they existed. + /// + [Test] + public async Task UpdateSetsItemRulesOnExistingDatabaseAsync() + { + var expected = await CreateSeason6ConfigurationAsync().ConfigureAwait(false); + + var contextProvider = new InMemoryPersistenceContextProvider(); + var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory()); + await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false); + using var context = contextProvider.CreateNewContext(); + var gameConfiguration = (await context.GetAsync().ConfigureAwait(false)).First(); + + // The state after the database migration: every item allows everything. + foreach (var item in gameConfiguration.Items) + { + item.IsTradable = item.IsDroppable = item.IsStorable = item.IsSellableToNpc = item.IsPersonalStoreSellable = item.IsRepairable = true; + } + + await new AddItemRuleFlagsPlugIn().ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false); + + foreach (var item in gameConfiguration.Items) + { + var expectedItem = GetItem(expected, item.Group, item.Number); + Assert.That(Flags(item), Is.EqualTo(Flags(expectedItem)), $"Item ({item.Group},{item.Number})"); + } + } + + private static (bool, bool, bool, bool, bool, bool) Flags(ItemDefinition item) + { + return (item.IsTradable, item.IsDroppable, item.IsStorable, item.IsSellableToNpc, item.IsPersonalStoreSellable, item.IsRepairable); + } + + private static ItemDefinition GetItem(GameConfiguration gameConfiguration, byte group, short number) + { + return gameConfiguration.Items.Single(item => item.Group == group && item.Number == number); + } + + private static async Task CreateSeason6ConfigurationAsync() + { + var contextProvider = new InMemoryPersistenceContextProvider(); + var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory()); + await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false); + + using var context = contextProvider.CreateNewContext(); + return (await context.GetAsync().ConfigureAwait(false)).First(); + } +} From 141a2f4f1163a1127ac85b27d8e453fa192631e6 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Sat, 26 Sep 2026 17:00:40 +0200 Subject: [PATCH 4/7] fix(items): address the review of the item rule flags - Trade: the rules are also checked when the trade completes. Items which another window (e.g. the chaos machine) left in the temporary storage go back to the inventory when a trade opens; before, they could be traded, or were lost when the trade was cancelled. - Personal store: an item which isn't sellable there can't be bought, also when it was put into the store before the rules existed. - Repair: new Item.CanBeRepaired() (wearable and IsRepairable). Items which can't be worn are no longer repaired in any version: their durability is the stack size, and a repair reduced it to one. Offline auto repair and the bots' merchant repair skip items which can't be repaired instead of logging a warning on every attempt. - Bots keep items which can't be sold to NPCs instead of trying to sell and then destroying them. - ItemRules.Apply no longer fails on duplicate item definitions; every copy gets the rule. - The remarks of ItemRules say that the pets and transformation rings follow the client and can't be repaired. - Tests: the repair test uses a wearable item and checks the full durability; new tests for stacks, leftover items when a trade opens, the trade completion and personal store purchases. The data test builds the Season 6 data once for its read-only tests. --- src/DataModel/ItemExtensions.cs | 11 ++ src/GameLogic/Bots/BotShoppingHandler.cs | 6 + src/GameLogic/Offline/RepairHandler.cs | 2 +- .../PlayerActions/Items/ItemRepairAction.cs | 4 +- .../PlayerStore/BuyRequestAction.cs | 14 +++ .../PlayerActions/Trade/TradeAcceptAction.cs | 10 ++ .../PlayerActions/Trade/TradeButtonAction.cs | 24 ++++ .../VersionSeasonSix/Items/ItemRules.cs | 14 ++- .../ItemRulesDataTest.cs | 21 +++- .../ItemRuleFlagsTests.cs | 106 ++++++++++++++++-- 10 files changed, 192 insertions(+), 20 deletions(-) diff --git a/src/DataModel/ItemExtensions.cs b/src/DataModel/ItemExtensions.cs index 2e78d4a85a..e703d8a9cd 100644 --- a/src/DataModel/ItemExtensions.cs +++ b/src/DataModel/ItemExtensions.cs @@ -100,6 +100,17 @@ public static int GetDarkRavenLeadershipRequirement(this Item item, int petLevel /// public static bool IsWearable(this Item item) => item.Definition?.ItemSlot != null; + /// + /// Determines whether this item can be repaired: it must be wearable, and its definition must allow it + /// (). The durability of an item which can't be worn is its + /// number of pieces, so repairing it would reduce a stack to one piece. + /// + /// The item. + /// + /// true if the specified item can be repaired; otherwise, false. + /// + public static bool CanBeRepaired(this Item item) => item.IsWearable() && item.Definition!.IsRepairable; + /// /// Determines whether this item is stackable. /// diff --git a/src/GameLogic/Bots/BotShoppingHandler.cs b/src/GameLogic/Bots/BotShoppingHandler.cs index 93fa777d0f..e063e264c7 100644 --- a/src/GameLogic/Bots/BotShoppingHandler.cs +++ b/src/GameLogic/Bots/BotShoppingHandler.cs @@ -214,6 +214,11 @@ private static List GetSellableJunk(OfflinePlayer player, IStorage invento continue; // equipped, or an archer's arrows - selling those would disarm the bow. } + if (!definition.IsSellableToNpc) + { + continue; // the merchant refuses it, and as unsold junk it would be destroyed under slot pressure. + } + var identifier = new ItemIdentifier(definition.Number, definition.Group); if (HealingHandler.HealthPotionPriority.Contains(identifier) || HealingHandler.ManaPotionPriority.Contains(identifier)) @@ -360,6 +365,7 @@ private static async ValueTask RepairGearAsync(OfflinePlayer player) } if (inventory.GetItem(slot) is { } item + && item.CanBeRepaired() && item.Durability() < item.GetMaximumDurabilityOfOnePiece()) { damaged.Add((slot, PriceCalculator.CalculateRepairPrice(item, true))); diff --git a/src/GameLogic/Offline/RepairHandler.cs b/src/GameLogic/Offline/RepairHandler.cs index f7859af6a2..aedda8d020 100644 --- a/src/GameLogic/Offline/RepairHandler.cs +++ b/src/GameLogic/Offline/RepairHandler.cs @@ -63,7 +63,7 @@ public async ValueTask PerformRepairsAsync() } var item = this._player.Inventory?.GetItem(i); - if (item is null) + if (item is null || !item.CanBeRepaired()) { continue; } diff --git a/src/GameLogic/PlayerActions/Items/ItemRepairAction.cs b/src/GameLogic/PlayerActions/Items/ItemRepairAction.cs index 2b208276be..313aa80bc2 100644 --- a/src/GameLogic/PlayerActions/Items/ItemRepairAction.cs +++ b/src/GameLogic/PlayerActions/Items/ItemRepairAction.cs @@ -35,7 +35,7 @@ public async ValueTask RepairItemAsync(Player player, byte slot) return; } - if (item.Definition is { IsRepairable: false }) + if (!item.CanBeRepaired()) { player.Logger.LogWarning( "Player {0} tried to repair {1}, which its item definition doesn't allow. The client item data may differ from the server.", @@ -87,7 +87,7 @@ public async ValueTask RepairAllItemsAsync(Player player) } var item = player.Inventory?.GetItem(i); - if (item is null || item.Definition is { IsRepairable: false }) + if (item is null || !item.CanBeRepaired()) { continue; } diff --git a/src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs b/src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs index 0cbf0cc548..c03eaeae23 100644 --- a/src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs +++ b/src/GameLogic/PlayerActions/PlayerStore/BuyRequestAction.cs @@ -53,6 +53,20 @@ public async ValueTask BuyItemAsync(Player player, Player requestedPlayer, byte return; } + // Checked here, and not only when the item is moved into the store: the store may + // contain it since before the item rules existed. + if (item.Definition is { IsPersonalStoreSellable: false }) + { + player.Logger.LogWarning( + "Player {0} tried to buy {1} from the store of {2}, which its item definition doesn't allow to sell in a personal store.", + player, + item, + requestedPlayer); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ItemCannotBeSoldInPersonalStore)).ConfigureAwait(false); + await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(requestedPlayer, ItemBuyResult.ItemBlock, null)).ConfigureAwait(false); + return; + } + var itemPrice = item.StorePrice.Value; if (player.Money < itemPrice) diff --git a/src/GameLogic/PlayerActions/Trade/TradeAcceptAction.cs b/src/GameLogic/PlayerActions/Trade/TradeAcceptAction.cs index 472cf518f6..057e4c6557 100644 --- a/src/GameLogic/PlayerActions/Trade/TradeAcceptAction.cs +++ b/src/GameLogic/PlayerActions/Trade/TradeAcceptAction.cs @@ -66,6 +66,16 @@ internal async ValueTask OpenTradeAsync(ITrader trader) // first make sure that all items which could be transferred are already present in the database await trader.SaveProgressAsync().ConfigureAwait(false); + // Items which another window (e.g. the chaos machine) left in the temporary storage go back to + // the inventory first. The trade uses the same storage, so they would be offered in the trade, + // and lost when it is cancelled, because a cancel restores the inventory backup made below. + if (trader.TemporaryStorage is { } temporaryStorage + && temporaryStorage.Items.Any() + && !await trader.Inventory!.TryTakeAllAsync(temporaryStorage).ConfigureAwait(false)) + { + trader.Logger.LogWarning("Could not return the items of the temporary storage of {trader} to the inventory before opening a trade.", trader); + } + trader.BackupInventory = new BackupItemStorage(trader.Inventory!.ItemStorage); trader.TradingMoney = 0; await trader.InvokeViewPlugInAsync(p => p.ShowTradeRequestAnswerAsync(true)).ConfigureAwait(false); diff --git a/src/GameLogic/PlayerActions/Trade/TradeButtonAction.cs b/src/GameLogic/PlayerActions/Trade/TradeButtonAction.cs index 24bc0213cc..03a1ba73a2 100644 --- a/src/GameLogic/PlayerActions/Trade/TradeButtonAction.cs +++ b/src/GameLogic/PlayerActions/Trade/TradeButtonAction.cs @@ -80,6 +80,14 @@ private async ValueTask InternalFinishTradeAsync(ITrader trader, IT using var itemContext = trader.GameContext.PersistenceContextProvider.CreateNewTradeContext(); var traderItems = trader.TemporaryStorage!.Items.ToList(); var tradePartnerItems = tradingPartner.TemporaryStorage!.Items.ToList(); + + // Checked here, and not only when an item is moved into the trade: the temporary storage + // is shared with other windows (e.g. the chaos machine), so it may already contain items. + if (!await this.CheckItemsAreTradableAsync(trader, traderItems).ConfigureAwait(false) + | !await this.CheckItemsAreTradableAsync(tradingPartner, tradePartnerItems).ConfigureAwait(false)) + { + return TradeResult.Cancelled; + } this.AttachItemsToPersistenceContext(traderItems, itemContext); this.AttachItemsToPersistenceContext(tradePartnerItems, itemContext); @@ -119,6 +127,22 @@ private async ValueTask InternalFinishTradeAsync(ITrader trader, IT } } + private async ValueTask CheckItemsAreTradableAsync(ITrader trader, IEnumerable items) + { + var notTradable = items.FirstOrDefault(item => item.Definition is { IsTradable: false }); + if (notTradable is null) + { + return true; + } + + (trader as Player)?.Logger.LogWarning( + "Trader {0} offered {1}, which its item definition doesn't allow to trade. The trade is cancelled.", + trader.Name, + notTradable); + await this.SendMessageAsync(trader, nameof(PlayerMessage.ItemCannotBeTraded)).ConfigureAwait(false); + return false; + } + private void CallPlugIn(IEnumerable items, ITrader source, ITrader target) { var point = target.GameContext.PlugInManager.GetPlugInPoint(); diff --git a/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs b/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs index 6226068b8e..873a1a0023 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs @@ -23,6 +23,9 @@ namespace MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Items; /// The Dark Horse and the Dark Raven stay repairable: the server repairs trainable pets /// (with their own repair price), while the client has no way to repair them. /// +/// Like in the client, the other pets (Guardian Angel, Imp, Horn of Uniria, Horn of Dinorant, +/// Horn of Fenrir) and the transformation rings can't be repaired, although they wear out: +/// a pet which can't be trained is destroyed when its durability reaches zero. /// public class ItemRules : InitializerBase { @@ -243,16 +246,17 @@ public override void Initialize() } /// - /// Applies the to the items of the game configuration. - /// Items which are not part of the configuration are skipped. + /// Applies the to the items of the game configuration. Listed items which are + /// not part of the configuration are skipped; an item which exists more than once (e.g. copied in + /// the admin panel) gets the rule on every copy. /// /// The game configuration. internal static void Apply(GameConfiguration gameConfiguration) { - var items = gameConfiguration.Items.ToDictionary(item => (item.Group, item.Number)); - foreach (var (group, number, blocked) in Rules) + var rules = Rules.ToDictionary(rule => (rule.Group, rule.Number), rule => rule.Blocked); + foreach (var item in gameConfiguration.Items) { - if (!items.TryGetValue((group, number), out var item)) + if (!rules.TryGetValue((item.Group, item.Number), out var blocked)) { continue; } diff --git a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemRulesDataTest.cs b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemRulesDataTest.cs index 1691185dea..bc554c14ef 100644 --- a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemRulesDataTest.cs +++ b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemRulesDataTest.cs @@ -17,13 +17,24 @@ namespace MUnique.OpenMU.Persistence.Initialization.Tests; [TestFixture] internal class ItemRulesDataTest { + private GameConfiguration _season6 = null!; + + /// + /// Creates the season 6 data once for the tests which only read it. + /// + [OneTimeSetUp] + public async Task CreateSeason6DataAsync() + { + this._season6 = await CreateSeason6ConfigurationAsync().ConfigureAwait(false); + } + /// /// Tests that every listed item exists once in the season 6 data. /// [Test] - public async Task EveryListedItemExistsAsync() + public void EveryListedItemExists() { - var gameConfiguration = await CreateSeason6ConfigurationAsync().ConfigureAwait(false); + var gameConfiguration = this._season6; Assert.That(ItemRules.Rules.Select(rule => (rule.Group, rule.Number)), Is.Unique); foreach (var (group, number, _) in ItemRules.Rules) @@ -36,9 +47,9 @@ public async Task EveryListedItemExistsAsync() /// Tests that a new season 6 database contains the item rules. /// [Test] - public async Task NewDatabaseContainsItemRulesAsync() + public void NewDatabaseContainsItemRules() { - var gameConfiguration = await CreateSeason6ConfigurationAsync().ConfigureAwait(false); + var gameConfiguration = this._season6; // Kris: a normal weapon allows everything. var kris = GetItem(gameConfiguration, 0, 0); @@ -72,7 +83,7 @@ public async Task NewDatabaseContainsItemRulesAsync() [Test] public async Task UpdateSetsItemRulesOnExistingDatabaseAsync() { - var expected = await CreateSeason6ConfigurationAsync().ConfigureAwait(false); + var expected = this._season6; var contextProvider = new InMemoryPersistenceContextProvider(); var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory()); diff --git a/tests/MUnique.OpenMU.Tests/ItemRuleFlagsTests.cs b/tests/MUnique.OpenMU.Tests/ItemRuleFlagsTests.cs index 1c9d224394..5bad47b1ad 100644 --- a/tests/MUnique.OpenMU.Tests/ItemRuleFlagsTests.cs +++ b/tests/MUnique.OpenMU.Tests/ItemRuleFlagsTests.cs @@ -5,17 +5,21 @@ namespace MUnique.OpenMU.Tests; using Moq; +using MUnique.OpenMU.DataModel; using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.DataModel.Configuration.Items; using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.NPC; using MUnique.OpenMU.GameLogic.PlayerActions.Items; +using MUnique.OpenMU.GameLogic.PlayerActions.PlayerStore; +using MUnique.OpenMU.GameLogic.PlayerActions.Trade; +using MUnique.OpenMU.GameLogic.Views.Trade; using MUnique.OpenMU.Pathfinding; /// -/// Tests that the item rule flags of are enforced by the -/// drop, sell and repair actions. The storage moves are tested in . +/// Tests that the item rule flags of are enforced by the item actions. +/// The checks when an item is moved into a storage are tested in . /// [TestFixture] public class ItemRuleFlagsTests @@ -57,23 +61,111 @@ public async ValueTask ItemWhichIsNotSellableToNpcIsNotSoldAsync() } /// - /// Verifies that an item which isn't repairable isn't repaired, while a repairable one is. - /// The item can't be worn, so its durability is the number of pieces, and repairing would reduce it. + /// Verifies that a wearable item is repaired to its maximum durability when it is repairable, and not at all when it isn't. /// /// If set to true, the item is repairable. [TestCase(true)] [TestCase(false)] - public async ValueTask ItemIsOnlyRepairedWhenRepairableAsync(bool isRepairable) + public async ValueTask WearableItemIsOnlyRepairedWhenRepairableAsync(bool isRepairable) { var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); player.Money = 1_000_000; - var item = CreateItem(definition => definition.IsRepairable = isRepairable); + var item = CreateItem(definition => + { + definition.ItemSlot = new ItemSlotType(); + definition.IsRepairable = isRepairable; + }); item.Durability = 5; await player.Inventory!.AddItemAsync(InventorySlot, item).ConfigureAwait(false); await new ItemRepairAction().RepairItemAsync(player, InventorySlot).ConfigureAwait(false); - Assert.That(item.Durability, isRepairable ? Is.Not.EqualTo(5) : Is.EqualTo(5)); + Assert.That(item.Durability, Is.EqualTo(isRepairable ? item.Definition!.Durability : 5)); + } + + /// + /// Verifies that an item which can't be worn isn't repaired: its durability is the number of pieces, + /// and a repair would reduce the stack to one piece. + /// + [Test] + public async ValueTask StackIsNotRepairedAsync() + { + var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + player.Money = 1_000_000; + var stack = CreateItem(_ => { }); + stack.Durability = 5; + await player.Inventory!.AddItemAsync(InventorySlot, stack).ConfigureAwait(false); + + await new ItemRepairAction().RepairItemAsync(player, InventorySlot).ConfigureAwait(false); + + Assert.That(stack.Durability, Is.EqualTo(5)); + } + + /// + /// Verifies that items which another window (e.g. the chaos machine) left in the temporary storage go + /// back to the inventory when a trade opens, so they are neither offered in the trade nor lost when + /// it is cancelled. + /// + [Test] + public async ValueTask LeftoverItemsReturnToInventoryWhenTradeOpensAsync() + { + var trader1 = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + var trader2 = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + var leftover = CreateItem(definition => definition.IsTradable = false); + await trader1.TemporaryStorage!.AddItemAsync(0, leftover).ConfigureAwait(false); + + await new TradeRequestAction().RequestTradeAsync(trader1, trader2).ConfigureAwait(false); + await new TradeAcceptAction().HandleTradeAcceptAsync(trader2, true).ConfigureAwait(false); + + Assert.That(trader1.Inventory!.Items, Does.Contain(leftover)); + Assert.That(trader1.TemporaryStorage.Items, Is.Empty); + } + + /// + /// Verifies that a trade is cancelled when it contains an item which isn't tradable, however it got there. + /// + [Test] + public async ValueTask TradeWithItemWhichIsNotTradableIsCancelledAsync() + { + var trader1 = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + var trader2 = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + + await new TradeRequestAction().RequestTradeAsync(trader1, trader2).ConfigureAwait(false); + await new TradeAcceptAction().HandleTradeAcceptAsync(trader2, true).ConfigureAwait(false); + + // Not moved in by MoveItemAction, which would refuse it. + var notTradable = CreateItem(definition => definition.IsTradable = false); + await trader1.TemporaryStorage!.AddItemAsync(0, notTradable).ConfigureAwait(false); + + var buttonAction = new TradeButtonAction(); + await buttonAction.TradeButtonChangedAsync(trader1, TradeButtonState.Checked).ConfigureAwait(false); + await buttonAction.TradeButtonChangedAsync(trader2, TradeButtonState.Checked).ConfigureAwait(false); + + Assert.That(trader2.Inventory!.Items, Does.Not.Contain(notTradable)); + Assert.That(trader1.PlayerState.CurrentState, Is.EqualTo(PlayerState.EnteredWorld)); + Assert.That(trader2.PlayerState.CurrentState, Is.EqualTo(PlayerState.EnteredWorld)); + } + + /// + /// Verifies that an item which isn't sellable in a personal store can't be bought from one, even when it + /// is in the store already (e.g. since before the item rules existed). + /// + [Test] + public async ValueTask ItemWhichIsNotPersonalStoreSellableCanNotBeBoughtAsync() + { + var seller = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + var buyer = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + buyer.Money = 1_000_000; + var item = CreateItem(definition => definition.IsPersonalStoreSellable = false); + item.StorePrice = 100; + await seller.ShopStorage!.AddItemAsync(InventoryConstants.FirstStoreItemSlotIndex, item).ConfigureAwait(false); + seller.ShopStorage.StoreOpen = true; + + await new BuyRequestAction().BuyItemAsync(buyer, seller, InventoryConstants.FirstStoreItemSlotIndex).ConfigureAwait(false); + + Assert.That(seller.ShopStorage.GetItem(InventoryConstants.FirstStoreItemSlotIndex), Is.SameAs(item)); + Assert.That(buyer.Inventory!.Items, Does.Not.Contain(item)); + Assert.That(buyer.Money, Is.EqualTo(1_000_000)); } private static Item CreateItem(Action setRule) From 319bca6b6f41c1be6b0165f06b05e3b7f1bc32bf Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Sat, 26 Sep 2026 21:28:58 +0200 Subject: [PATCH 5/7] docs(items): IsRepairable only covers the normal repair IsRepairable and Item.CanBeRepaired() are about the repair by an NPC or the inventory repair. Other ways to restore the durability don't use them: the Horn of Fenrir, which can't be repaired normally, is still repaired with a Jewel of Bless (the repair target items of the jewel of bless consume handler). A test covers that. --- .../Configuration/Items/ItemDefinition.cs | 5 ++- src/DataModel/ItemExtensions.cs | 8 ++-- .../ItemConsumptionTest.cs | 38 +++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/DataModel/Configuration/Items/ItemDefinition.cs b/src/DataModel/Configuration/Items/ItemDefinition.cs index 7bb8e56e05..d486f79106 100644 --- a/src/DataModel/Configuration/Items/ItemDefinition.cs +++ b/src/DataModel/Configuration/Items/ItemDefinition.cs @@ -92,7 +92,10 @@ public partial class ItemDefinition public bool IsPersonalStoreSellable { get; set; } = true; /// - /// Gets or sets a value indicating whether items of this kind can be repaired. + /// Gets or sets a value indicating whether items of this kind can be repaired the normal way: + /// by an NPC, or with the repair of the inventory. + /// Other ways to restore the durability don't depend on it, e.g. repairing a Horn of Fenrir + /// with a Jewel of Bless (see the repair target items of the jewel of bless consume handler). /// public bool IsRepairable { get; set; } = true; diff --git a/src/DataModel/ItemExtensions.cs b/src/DataModel/ItemExtensions.cs index e703d8a9cd..71feee60c5 100644 --- a/src/DataModel/ItemExtensions.cs +++ b/src/DataModel/ItemExtensions.cs @@ -101,9 +101,11 @@ public static int GetDarkRavenLeadershipRequirement(this Item item, int petLevel public static bool IsWearable(this Item item) => item.Definition?.ItemSlot != null; /// - /// Determines whether this item can be repaired: it must be wearable, and its definition must allow it - /// (). The durability of an item which can't be worn is its - /// number of pieces, so repairing it would reduce a stack to one piece. + /// Determines whether this item can be repaired the normal way (by an NPC, or with the repair of the + /// inventory): it must be wearable, and its definition must allow it (). + /// The durability of an item which can't be worn is its number of pieces, so repairing it would reduce a + /// stack to one piece. Other ways to restore the durability, like a Jewel of Bless on a Horn of Fenrir, + /// don't use this check. /// /// The item. /// diff --git a/tests/MUnique.OpenMU.Tests/ItemConsumptionTest.cs b/tests/MUnique.OpenMU.Tests/ItemConsumptionTest.cs index c89174a5d8..038aa1d9f4 100644 --- a/tests/MUnique.OpenMU.Tests/ItemConsumptionTest.cs +++ b/tests/MUnique.OpenMU.Tests/ItemConsumptionTest.cs @@ -54,6 +54,44 @@ public async ValueTask JewelOfBlessAsync(byte itemLevel, bool consumptionExpecta Assert.That(upgradeableItem.Level, consumed ? Is.EqualTo(itemLevel + 1) : Is.EqualTo(itemLevel)); } + /// + /// Tests that the jewel of bless repairs its repair target items (the Horn of Fenrir in season 6), + /// even when they can't be repaired the normal way (). + /// + [Test] + public async ValueTask JewelOfBlessRepairsItemWhichIsNotNormallyRepairableAsync() + { + var player = await this.GetPlayerAsync().ConfigureAwait(false); + var fenrirMock = new Mock(); + fenrirMock.SetupAllProperties(); + fenrirMock.Setup(i => i.ItemOptions).Returns(new List()); + fenrirMock.Setup(i => i.ItemSetGroups).Returns(new List()); + var fenrir = fenrirMock.Object; + fenrir.Definition = new DataModel.Configuration.Items.ItemDefinition + { + Width = 1, + Height = 1, + Durability = 255, + ItemSlot = new DataModel.Configuration.Items.ItemSlotType(), + IsRepairable = false, + }; + fenrir.Durability = 100; + var fenrirSlot = (byte)(ItemSlot + 1); + await player.Inventory!.AddItemAsync(fenrirSlot, fenrir).ConfigureAwait(false); + var bless = this.GetItem(); + await player.Inventory.AddItemAsync(ItemSlot, bless).ConfigureAwait(false); + + var consumeHandler = new BlessJewelConsumeHandlerPlugIn(); + var configuration = (BlessJewelConsumeHandlerPlugInConfiguration)consumeHandler.CreateDefaultConfig(); + configuration.RepairTargetItems.Add(fenrir.Definition); + consumeHandler.Configuration = configuration; + + var consumed = await consumeHandler.ConsumeItemAsync(player, bless, fenrir, FruitUsage.Undefined).ConfigureAwait(false); + + Assert.That(consumed, Is.True); + Assert.That(fenrir.Durability, Is.EqualTo(255)); + } + /// /// Tests the jewel of soul consumption. /// From d009ebcee2b8a953bd1a73ae5b738aa1621e92a3 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Sat, 26 Sep 2026 22:13:53 +0200 Subject: [PATCH 6/7] style(items): address the static code analysis findings - ItemRules lists the blocked actions of each item instead of combining them with bitwise operations: Codacy's Sonar analysis didn't see the Flags attribute of the nested enum and flagged every combination. - TradeButtonAction checks both traders' items with two variables instead of a non-short-circuit operator, and gets the missing blank line after the block. --- .../PlayerActions/Trade/TradeButtonAction.cs | 7 +- .../VersionSeasonSix/Items/ItemRules.cs | 356 +++++++++--------- 2 files changed, 189 insertions(+), 174 deletions(-) diff --git a/src/GameLogic/PlayerActions/Trade/TradeButtonAction.cs b/src/GameLogic/PlayerActions/Trade/TradeButtonAction.cs index 03a1ba73a2..0b9930cf42 100644 --- a/src/GameLogic/PlayerActions/Trade/TradeButtonAction.cs +++ b/src/GameLogic/PlayerActions/Trade/TradeButtonAction.cs @@ -83,11 +83,14 @@ private async ValueTask InternalFinishTradeAsync(ITrader trader, IT // Checked here, and not only when an item is moved into the trade: the temporary storage // is shared with other windows (e.g. the chaos machine), so it may already contain items. - if (!await this.CheckItemsAreTradableAsync(trader, traderItems).ConfigureAwait(false) - | !await this.CheckItemsAreTradableAsync(tradingPartner, tradePartnerItems).ConfigureAwait(false)) + // Both are checked, so that each trader gets the message about their own items. + var traderItemsAreTradable = await this.CheckItemsAreTradableAsync(trader, traderItems).ConfigureAwait(false); + var partnerItemsAreTradable = await this.CheckItemsAreTradableAsync(tradingPartner, tradePartnerItems).ConfigureAwait(false); + if (!traderItemsAreTradable || !partnerItemsAreTradable) { return TradeResult.Cancelled; } + this.AttachItemsToPersistenceContext(traderItems, itemContext); this.AttachItemsToPersistenceContext(tradePartnerItems, itemContext); diff --git a/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs b/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs index 873a1a0023..c5595aa5df 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs @@ -40,203 +40,202 @@ public ItemRules(IContext context, GameConfiguration gameConfiguration) } /// - /// The actions an item rule blocks. + /// An action which an item rule blocks. /// - [Flags] public enum Blocked { /// /// The item can't be traded. /// - Trade = 1, + Trade, /// /// The item can't be dropped. /// - Drop = 2, + Drop, /// /// The item can't be stored in the vault. /// - Store = 4, + Store, /// /// The item can't be sold to an NPC. /// - SellToNpc = 8, + SellToNpc, /// /// The item can't be offered in a personal store. /// - PersonalStore = 16, + PersonalStore, /// /// The item can't be repaired. /// - Repair = 32, + Repair, } /// /// Gets the items which don't allow every action, with the actions they block. /// - public static IReadOnlyList<(byte Group, short Number, Blocked Blocked)> Rules { get; } = new (byte, short, Blocked)[] + public static IReadOnlyList Rules { get; } = new[] { - (4, 7, Blocked.Repair), // Bolt - (4, 15, Blocked.Repair), // Arrow - (12, 7, Blocked.Repair), // Orb of Twisting Slash - (12, 8, Blocked.Repair), // Healing Orb - (12, 9, Blocked.Repair), // Orb of Greater Fortitude - (12, 10, Blocked.Repair), // Orb of Greater Damage - (12, 11, Blocked.Repair), // Orb of Summoning - (12, 12, Blocked.Repair), // Orb of Rageful Blow - (12, 13, Blocked.Repair), // Orb of Impale - (12, 14, Blocked.Repair), // Orb of Greater Fortitude - (12, 15, Blocked.Repair), // Jewel of Chaos - (12, 16, Blocked.Repair), // Orb of Fire Slash - (12, 17, Blocked.Repair), // Orb of Penetration - (12, 18, Blocked.Repair), // Orb of Ice Arrow - (12, 19, Blocked.Repair), // Orb of Death Stab - (13, 0, Blocked.Repair), // Guardian Angel - (13, 1, Blocked.Repair), // Imp - (13, 2, Blocked.Repair), // Horn of Uniria - (13, 3, Blocked.Repair), // Horn of Dinorant - (13, 10, Blocked.Repair), // Transformation Ring - (13, 11, Blocked.Repair), // Order (Guardian/Life Stone) - (13, 14, Blocked.Repair), // Loch's Feather - (13, 15, Blocked.Repair), // Fruits - (13, 16, Blocked.Repair), // Scroll of Archangel - (13, 17, Blocked.Repair), // Blood Bone - (13, 18, Blocked.Repair), // Invisibility Cloak - (13, 19, Blocked.Trade | Blocked.Store | Blocked.SellToNpc | Blocked.PersonalStore | Blocked.Repair), // Absolute Weapon of Archangel - (13, 20, Blocked.Trade | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Wizards Ring (bound to the character, see the class remarks) - (13, 29, Blocked.Repair), // Armor of Guardsman - (13, 32, Blocked.Repair), // Splinter of Armor - (13, 33, Blocked.Repair), // Bless of Guardian - (13, 34, Blocked.Repair), // Claw of Beast - (13, 35, Blocked.Repair), // Fragment of Horn - (13, 36, Blocked.Repair), // Broken Horn - (13, 37, Blocked.Repair), // Horn of Fenrir - (13, 38, Blocked.Trade | Blocked.PersonalStore | Blocked.Repair), // Moonstone Pendant - (13, 39, Blocked.Trade | Blocked.PersonalStore | Blocked.Repair), // Eilte Transfer Skeleton Ring - (13, 40, Blocked.Repair), // Jack Olantern Ring - (13, 41, Blocked.Repair), // Transfer Christmas Ring - (13, 42, Blocked.Repair), // Ring of GM - (13, 49, Blocked.Repair), // Old Scroll - (13, 50, Blocked.Repair), // Illusion Sorcerer Covenant - (13, 51, Blocked.Repair), // Scroll of Blood - (13, 64, Blocked.Trade | Blocked.SellToNpc | Blocked.Repair), // Demon - (13, 65, Blocked.Trade | Blocked.SellToNpc | Blocked.Repair), // Spirit of Guardian - (13, 67, Blocked.Repair), // Pet Rudolf - (13, 68, Blocked.Repair), // Snowman Transformation Ring - (13, 76, Blocked.Trade | Blocked.SellToNpc | Blocked.Repair), // Panda Ring - (13, 80, Blocked.Trade | Blocked.SellToNpc | Blocked.Repair), // Pet Panda - (13, 106, Blocked.Trade | Blocked.Drop | Blocked.SellToNpc | Blocked.PersonalStore | Blocked.Repair), // Pet Unicorn - (13, 122, Blocked.Trade | Blocked.SellToNpc | Blocked.Repair), // Skeleton Transformation Ring - (13, 123, Blocked.Trade | Blocked.SellToNpc | Blocked.Repair), // Pet Skeleton - (14, 0, Blocked.Repair), // Apple - (14, 1, Blocked.Repair), // Small Healing Potion - (14, 2, Blocked.Repair), // Healing Potion - (14, 3, Blocked.Repair), // Large Healing Potion - (14, 4, Blocked.Repair), // Small Mana Potion - (14, 5, Blocked.Repair), // Mana Potion - (14, 6, Blocked.Repair), // Large Mana Potion - (14, 7, Blocked.Repair), // Siege Potion - (14, 8, Blocked.Repair), // Antidote - (14, 9, Blocked.Repair), // Ale - (14, 10, Blocked.Repair), // Town Portal Scroll - (14, 11, Blocked.SellToNpc | Blocked.Repair), // Box of Luck - (14, 13, Blocked.Repair), // Jewel of Bless - (14, 14, Blocked.Repair), // Jewel of Soul - (14, 16, Blocked.Repair), // Jewel of Life - (14, 17, Blocked.Repair), // Devil's Eye - (14, 18, Blocked.Repair), // Devil's Key - (14, 19, Blocked.Repair), // Devil's Invitation - (14, 21, Blocked.Trade | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Rena - (14, 22, Blocked.Repair), // Jewel of Creation - (14, 23, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Scroll of the Emperor - (14, 24, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Broken Sword - (14, 25, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Tear of Elf - (14, 26, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Soul Shard of Wizard - (14, 28, Blocked.Repair), // Lost Map - (14, 29, Blocked.Repair), // Symbol of Kundun - (14, 31, Blocked.Repair), // Jewel of Guardian - (14, 32, Blocked.Repair), // Pink Chocolate Box - (14, 33, Blocked.Repair), // Red Chocolate Box - (14, 34, Blocked.Repair), // Blue Chocolate Box - (14, 35, Blocked.Repair), // Small SD Potion - (14, 36, Blocked.Repair), // SD Potion - (14, 37, Blocked.Repair), // Large SD Potion - (14, 38, Blocked.Repair), // Small Complex Potion - (14, 39, Blocked.Repair), // Complex Potion - (14, 40, Blocked.Repair), // Large Complex Potion - (14, 41, Blocked.Repair), // Gemstone - (14, 42, Blocked.Repair), // Jewel of Harmony - (14, 43, Blocked.Repair), // Lower refining stone - (14, 44, Blocked.Repair), // Higher refining stone - (14, 45, Blocked.Repair), // Pumpkin of Luck - (14, 46, Blocked.Repair), // Jack O'Lantern Blessings - (14, 47, Blocked.Repair), // Jack O'Lantern Wrath - (14, 48, Blocked.Repair), // Jack O'Lantern Cry - (14, 49, Blocked.Repair), // Jack O'Lantern Food - (14, 50, Blocked.Repair), // Jack O'Lantern Drink - (14, 51, Blocked.Repair), // Chistmas Star - (14, 52, Blocked.Trade | Blocked.Repair), // GM Gift - (14, 63, Blocked.Repair), // Firecracker - (14, 65, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Death-beam Knight Flame - (14, 66, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Hell-Miner Horn - (14, 67, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Dark Phoenix Feather - (14, 68, Blocked.Trade | Blocked.Drop | Blocked.Store | Blocked.PersonalStore | Blocked.Repair), // Abyssal Eye - (14, 84, Blocked.Repair), // Cherry Blossom Play-Box - (14, 85, Blocked.Repair), // Cherry Blossom Wine - (14, 86, Blocked.Repair), // Cherry Blossom Rice Cake - (14, 87, Blocked.Repair), // Cherry Blossom Flower Petal - (14, 90, Blocked.Repair), // Golden Cherry Blossom Branch - (14, 99, Blocked.Repair), // Christmas Firecracker - (14, 101, Blocked.Repair), // Suspicious Scrap of Paper - (14, 102, Blocked.Repair), // Gaion's Order - (14, 103, Blocked.Repair), // First Secromicon Fragment - (14, 104, Blocked.Repair), // Second Secromicon Fragment - (14, 105, Blocked.Repair), // Third Secromicon Fragment - (14, 106, Blocked.Repair), // Fourth Secromicon Fragment - (14, 107, Blocked.Repair), // Fifth Secromicon Fragment - (14, 108, Blocked.Repair), // Sixth Secromicon Fragment - (14, 109, Blocked.Repair), // Complete Secromicon - (15, 0, Blocked.Repair), // Scroll of Poison - (15, 1, Blocked.Repair), // Scroll of Meteorite - (15, 2, Blocked.Repair), // Scroll of Lighting - (15, 3, Blocked.Repair), // Scroll of Fire Ball - (15, 4, Blocked.Repair), // Scroll of Flame - (15, 5, Blocked.Repair), // Scroll of Teleport - (15, 6, Blocked.Repair), // Scroll of Ice - (15, 7, Blocked.Repair), // Scroll of Twister - (15, 8, Blocked.Repair), // Scroll of Evil Spirit - (15, 9, Blocked.Repair), // Scroll of Hellfire - (15, 10, Blocked.Repair), // Scroll of Power Wave - (15, 11, Blocked.Repair), // Scroll of Aqua Beam - (15, 12, Blocked.Repair), // Scroll of Cometfall - (15, 13, Blocked.Repair), // Scroll of Inferno - (15, 14, Blocked.Repair), // Scroll of Teleport Ally - (15, 15, Blocked.Repair), // Scroll of Soul Barrier - (15, 16, Blocked.Repair), // Scroll of Decay - (15, 17, Blocked.Repair), // Scroll of Ice Storm - (15, 18, Blocked.Repair), // Scroll of Nova - (15, 19, Blocked.Repair), // Chain Lightning Parchment - (15, 20, Blocked.Repair), // Drain Life Parchment - (15, 21, Blocked.Repair), // Lightning Shock Parchment - (15, 22, Blocked.Repair), // Damage Reflection Parchment - (15, 23, Blocked.Repair), // Berserker Parchment - (15, 24, Blocked.Repair), // Sleep Parchment - (15, 26, Blocked.Repair), // Weakness Parchment - (15, 27, Blocked.Repair), // Innovation Parchment - (15, 28, Blocked.Repair), // Scroll of Wizardry Enhance - (15, 29, Blocked.Repair), // Scroll of Gigantic Storm - (15, 30, Blocked.Repair), // Chain Drive Parchment - (15, 31, Blocked.Repair), // Dark Side Parchment - (15, 32, Blocked.Repair), // Dragon Roar Parchment - (15, 33, Blocked.Repair), // Dragon Slasher Parchment - (15, 34, Blocked.Repair), // Ignore Defense Parchment - (15, 35, Blocked.Repair), // Increase Health Parchment - (15, 36, Blocked.Repair), // Increase Block Parchment + Rule(4, 7, Blocked.Repair), // Bolt + Rule(4, 15, Blocked.Repair), // Arrow + Rule(12, 7, Blocked.Repair), // Orb of Twisting Slash + Rule(12, 8, Blocked.Repair), // Healing Orb + Rule(12, 9, Blocked.Repair), // Orb of Greater Fortitude + Rule(12, 10, Blocked.Repair), // Orb of Greater Damage + Rule(12, 11, Blocked.Repair), // Orb of Summoning + Rule(12, 12, Blocked.Repair), // Orb of Rageful Blow + Rule(12, 13, Blocked.Repair), // Orb of Impale + Rule(12, 14, Blocked.Repair), // Orb of Greater Fortitude + Rule(12, 15, Blocked.Repair), // Jewel of Chaos + Rule(12, 16, Blocked.Repair), // Orb of Fire Slash + Rule(12, 17, Blocked.Repair), // Orb of Penetration + Rule(12, 18, Blocked.Repair), // Orb of Ice Arrow + Rule(12, 19, Blocked.Repair), // Orb of Death Stab + Rule(13, 0, Blocked.Repair), // Guardian Angel + Rule(13, 1, Blocked.Repair), // Imp + Rule(13, 2, Blocked.Repair), // Horn of Uniria + Rule(13, 3, Blocked.Repair), // Horn of Dinorant + Rule(13, 10, Blocked.Repair), // Transformation Ring + Rule(13, 11, Blocked.Repair), // Order (Guardian/Life Stone) + Rule(13, 14, Blocked.Repair), // Loch's Feather + Rule(13, 15, Blocked.Repair), // Fruits + Rule(13, 16, Blocked.Repair), // Scroll of Archangel + Rule(13, 17, Blocked.Repair), // Blood Bone + Rule(13, 18, Blocked.Repair), // Invisibility Cloak + Rule(13, 19, Blocked.Trade, Blocked.Store, Blocked.SellToNpc, Blocked.PersonalStore, Blocked.Repair), // Absolute Weapon of Archangel + Rule(13, 20, Blocked.Trade, Blocked.Store, Blocked.PersonalStore, Blocked.Repair), // Wizards Ring (bound to the character, see the class remarks) + Rule(13, 29, Blocked.Repair), // Armor of Guardsman + Rule(13, 32, Blocked.Repair), // Splinter of Armor + Rule(13, 33, Blocked.Repair), // Bless of Guardian + Rule(13, 34, Blocked.Repair), // Claw of Beast + Rule(13, 35, Blocked.Repair), // Fragment of Horn + Rule(13, 36, Blocked.Repair), // Broken Horn + Rule(13, 37, Blocked.Repair), // Horn of Fenrir + Rule(13, 38, Blocked.Trade, Blocked.PersonalStore, Blocked.Repair), // Moonstone Pendant + Rule(13, 39, Blocked.Trade, Blocked.PersonalStore, Blocked.Repair), // Eilte Transfer Skeleton Ring + Rule(13, 40, Blocked.Repair), // Jack Olantern Ring + Rule(13, 41, Blocked.Repair), // Transfer Christmas Ring + Rule(13, 42, Blocked.Repair), // Ring of GM + Rule(13, 49, Blocked.Repair), // Old Scroll + Rule(13, 50, Blocked.Repair), // Illusion Sorcerer Covenant + Rule(13, 51, Blocked.Repair), // Scroll of Blood + Rule(13, 64, Blocked.Trade, Blocked.SellToNpc, Blocked.Repair), // Demon + Rule(13, 65, Blocked.Trade, Blocked.SellToNpc, Blocked.Repair), // Spirit of Guardian + Rule(13, 67, Blocked.Repair), // Pet Rudolf + Rule(13, 68, Blocked.Repair), // Snowman Transformation Ring + Rule(13, 76, Blocked.Trade, Blocked.SellToNpc, Blocked.Repair), // Panda Ring + Rule(13, 80, Blocked.Trade, Blocked.SellToNpc, Blocked.Repair), // Pet Panda + Rule(13, 106, Blocked.Trade, Blocked.Drop, Blocked.SellToNpc, Blocked.PersonalStore, Blocked.Repair), // Pet Unicorn + Rule(13, 122, Blocked.Trade, Blocked.SellToNpc, Blocked.Repair), // Skeleton Transformation Ring + Rule(13, 123, Blocked.Trade, Blocked.SellToNpc, Blocked.Repair), // Pet Skeleton + Rule(14, 0, Blocked.Repair), // Apple + Rule(14, 1, Blocked.Repair), // Small Healing Potion + Rule(14, 2, Blocked.Repair), // Healing Potion + Rule(14, 3, Blocked.Repair), // Large Healing Potion + Rule(14, 4, Blocked.Repair), // Small Mana Potion + Rule(14, 5, Blocked.Repair), // Mana Potion + Rule(14, 6, Blocked.Repair), // Large Mana Potion + Rule(14, 7, Blocked.Repair), // Siege Potion + Rule(14, 8, Blocked.Repair), // Antidote + Rule(14, 9, Blocked.Repair), // Ale + Rule(14, 10, Blocked.Repair), // Town Portal Scroll + Rule(14, 11, Blocked.SellToNpc, Blocked.Repair), // Box of Luck + Rule(14, 13, Blocked.Repair), // Jewel of Bless + Rule(14, 14, Blocked.Repair), // Jewel of Soul + Rule(14, 16, Blocked.Repair), // Jewel of Life + Rule(14, 17, Blocked.Repair), // Devil's Eye + Rule(14, 18, Blocked.Repair), // Devil's Key + Rule(14, 19, Blocked.Repair), // Devil's Invitation + Rule(14, 21, Blocked.Trade, Blocked.Store, Blocked.PersonalStore, Blocked.Repair), // Rena + Rule(14, 22, Blocked.Repair), // Jewel of Creation + Rule(14, 23, Blocked.Trade, Blocked.Drop, Blocked.Store, Blocked.PersonalStore, Blocked.Repair), // Scroll of the Emperor + Rule(14, 24, Blocked.Trade, Blocked.Drop, Blocked.Store, Blocked.PersonalStore, Blocked.Repair), // Broken Sword + Rule(14, 25, Blocked.Trade, Blocked.Drop, Blocked.Store, Blocked.PersonalStore, Blocked.Repair), // Tear of Elf + Rule(14, 26, Blocked.Trade, Blocked.Drop, Blocked.Store, Blocked.PersonalStore, Blocked.Repair), // Soul Shard of Wizard + Rule(14, 28, Blocked.Repair), // Lost Map + Rule(14, 29, Blocked.Repair), // Symbol of Kundun + Rule(14, 31, Blocked.Repair), // Jewel of Guardian + Rule(14, 32, Blocked.Repair), // Pink Chocolate Box + Rule(14, 33, Blocked.Repair), // Red Chocolate Box + Rule(14, 34, Blocked.Repair), // Blue Chocolate Box + Rule(14, 35, Blocked.Repair), // Small SD Potion + Rule(14, 36, Blocked.Repair), // SD Potion + Rule(14, 37, Blocked.Repair), // Large SD Potion + Rule(14, 38, Blocked.Repair), // Small Complex Potion + Rule(14, 39, Blocked.Repair), // Complex Potion + Rule(14, 40, Blocked.Repair), // Large Complex Potion + Rule(14, 41, Blocked.Repair), // Gemstone + Rule(14, 42, Blocked.Repair), // Jewel of Harmony + Rule(14, 43, Blocked.Repair), // Lower refining stone + Rule(14, 44, Blocked.Repair), // Higher refining stone + Rule(14, 45, Blocked.Repair), // Pumpkin of Luck + Rule(14, 46, Blocked.Repair), // Jack O'Lantern Blessings + Rule(14, 47, Blocked.Repair), // Jack O'Lantern Wrath + Rule(14, 48, Blocked.Repair), // Jack O'Lantern Cry + Rule(14, 49, Blocked.Repair), // Jack O'Lantern Food + Rule(14, 50, Blocked.Repair), // Jack O'Lantern Drink + Rule(14, 51, Blocked.Repair), // Chistmas Star + Rule(14, 52, Blocked.Trade, Blocked.Repair), // GM Gift + Rule(14, 63, Blocked.Repair), // Firecracker + Rule(14, 65, Blocked.Trade, Blocked.Drop, Blocked.Store, Blocked.PersonalStore, Blocked.Repair), // Death-beam Knight Flame + Rule(14, 66, Blocked.Trade, Blocked.Drop, Blocked.Store, Blocked.PersonalStore, Blocked.Repair), // Hell-Miner Horn + Rule(14, 67, Blocked.Trade, Blocked.Drop, Blocked.Store, Blocked.PersonalStore, Blocked.Repair), // Dark Phoenix Feather + Rule(14, 68, Blocked.Trade, Blocked.Drop, Blocked.Store, Blocked.PersonalStore, Blocked.Repair), // Abyssal Eye + Rule(14, 84, Blocked.Repair), // Cherry Blossom Play-Box + Rule(14, 85, Blocked.Repair), // Cherry Blossom Wine + Rule(14, 86, Blocked.Repair), // Cherry Blossom Rice Cake + Rule(14, 87, Blocked.Repair), // Cherry Blossom Flower Petal + Rule(14, 90, Blocked.Repair), // Golden Cherry Blossom Branch + Rule(14, 99, Blocked.Repair), // Christmas Firecracker + Rule(14, 101, Blocked.Repair), // Suspicious Scrap of Paper + Rule(14, 102, Blocked.Repair), // Gaion's Order + Rule(14, 103, Blocked.Repair), // First Secromicon Fragment + Rule(14, 104, Blocked.Repair), // Second Secromicon Fragment + Rule(14, 105, Blocked.Repair), // Third Secromicon Fragment + Rule(14, 106, Blocked.Repair), // Fourth Secromicon Fragment + Rule(14, 107, Blocked.Repair), // Fifth Secromicon Fragment + Rule(14, 108, Blocked.Repair), // Sixth Secromicon Fragment + Rule(14, 109, Blocked.Repair), // Complete Secromicon + Rule(15, 0, Blocked.Repair), // Scroll of Poison + Rule(15, 1, Blocked.Repair), // Scroll of Meteorite + Rule(15, 2, Blocked.Repair), // Scroll of Lighting + Rule(15, 3, Blocked.Repair), // Scroll of Fire Ball + Rule(15, 4, Blocked.Repair), // Scroll of Flame + Rule(15, 5, Blocked.Repair), // Scroll of Teleport + Rule(15, 6, Blocked.Repair), // Scroll of Ice + Rule(15, 7, Blocked.Repair), // Scroll of Twister + Rule(15, 8, Blocked.Repair), // Scroll of Evil Spirit + Rule(15, 9, Blocked.Repair), // Scroll of Hellfire + Rule(15, 10, Blocked.Repair), // Scroll of Power Wave + Rule(15, 11, Blocked.Repair), // Scroll of Aqua Beam + Rule(15, 12, Blocked.Repair), // Scroll of Cometfall + Rule(15, 13, Blocked.Repair), // Scroll of Inferno + Rule(15, 14, Blocked.Repair), // Scroll of Teleport Ally + Rule(15, 15, Blocked.Repair), // Scroll of Soul Barrier + Rule(15, 16, Blocked.Repair), // Scroll of Decay + Rule(15, 17, Blocked.Repair), // Scroll of Ice Storm + Rule(15, 18, Blocked.Repair), // Scroll of Nova + Rule(15, 19, Blocked.Repair), // Chain Lightning Parchment + Rule(15, 20, Blocked.Repair), // Drain Life Parchment + Rule(15, 21, Blocked.Repair), // Lightning Shock Parchment + Rule(15, 22, Blocked.Repair), // Damage Reflection Parchment + Rule(15, 23, Blocked.Repair), // Berserker Parchment + Rule(15, 24, Blocked.Repair), // Sleep Parchment + Rule(15, 26, Blocked.Repair), // Weakness Parchment + Rule(15, 27, Blocked.Repair), // Innovation Parchment + Rule(15, 28, Blocked.Repair), // Scroll of Wizardry Enhance + Rule(15, 29, Blocked.Repair), // Scroll of Gigantic Storm + Rule(15, 30, Blocked.Repair), // Chain Drive Parchment + Rule(15, 31, Blocked.Repair), // Dark Side Parchment + Rule(15, 32, Blocked.Repair), // Dragon Roar Parchment + Rule(15, 33, Blocked.Repair), // Dragon Slasher Parchment + Rule(15, 34, Blocked.Repair), // Ignore Defense Parchment + Rule(15, 35, Blocked.Repair), // Increase Health Parchment + Rule(15, 36, Blocked.Repair), // Increase Block Parchment }; /// @@ -253,7 +252,7 @@ public override void Initialize() /// The game configuration. internal static void Apply(GameConfiguration gameConfiguration) { - var rules = Rules.ToDictionary(rule => (rule.Group, rule.Number), rule => rule.Blocked); + var rules = Rules.ToDictionary(rule => (rule.Group, rule.Number), rule => rule.BlockedActions); foreach (var item in gameConfiguration.Items) { if (!rules.TryGetValue((item.Group, item.Number), out var blocked)) @@ -261,12 +260,25 @@ internal static void Apply(GameConfiguration gameConfiguration) continue; } - item.IsTradable = !blocked.HasFlag(Blocked.Trade); - item.IsDroppable = !blocked.HasFlag(Blocked.Drop); - item.IsStorable = !blocked.HasFlag(Blocked.Store); - item.IsSellableToNpc = !blocked.HasFlag(Blocked.SellToNpc); - item.IsPersonalStoreSellable = !blocked.HasFlag(Blocked.PersonalStore); - item.IsRepairable = !blocked.HasFlag(Blocked.Repair); + item.IsTradable = !blocked.Contains(Blocked.Trade); + item.IsDroppable = !blocked.Contains(Blocked.Drop); + item.IsStorable = !blocked.Contains(Blocked.Store); + item.IsSellableToNpc = !blocked.Contains(Blocked.SellToNpc); + item.IsPersonalStoreSellable = !blocked.Contains(Blocked.PersonalStore); + item.IsRepairable = !blocked.Contains(Blocked.Repair); } } + + private static ItemRule Rule(byte group, short number, params Blocked[] blockedActions) + { + return new ItemRule(group, number, blockedActions); + } + + /// + /// The actions which an item doesn't allow. + /// + /// The group of the item. + /// The number of the item. + /// The actions which the item doesn't allow. + public sealed record ItemRule(byte Group, short Number, IReadOnlyCollection BlockedActions); } From 90b255b9e58f0eb250c074c4583f64c5e36b55b7 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Sat, 26 Sep 2026 22:32:36 +0200 Subject: [PATCH 7/7] feat(items): let the Dark Horse fly The Icarus map requires the CanFly attribute, which the wings, the Horn of Dinorant and the Horn of Fenrir give. The Dark Horse didn't, while the client counts it as a flying mount (its data tags it as flying), so client and server disagreed about entering and staying in Icarus. The Season 6 initialization now gives the Dark Horse the CanFly power-up, and the mandatory AddDarkHorseCanFlyPlugIn update adds it to existing databases (only once). --- .../Updates/AddDarkHorseCanFlyPlugIn.cs | 75 +++++++++++++++++++ .../Initialization/Updates/UpdateVersion.cs | 5 ++ .../VersionSeasonSix/Items/Pets.cs | 1 + .../DarkHorseCanFlyTest.cs | 73 ++++++++++++++++++ 4 files changed, 154 insertions(+) create mode 100644 src/Persistence/Initialization/Updates/AddDarkHorseCanFlyPlugIn.cs create mode 100644 tests/MUnique.OpenMU.Persistence.Initialization.Tests/DarkHorseCanFlyTest.cs diff --git a/src/Persistence/Initialization/Updates/AddDarkHorseCanFlyPlugIn.cs b/src/Persistence/Initialization/Updates/AddDarkHorseCanFlyPlugIn.cs new file mode 100644 index 0000000000..71d876fad7 --- /dev/null +++ b/src/Persistence/Initialization/Updates/AddDarkHorseCanFlyPlugIn.cs @@ -0,0 +1,75 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Updates; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.AttributeSystem; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.PlugIns; + +/// +/// This update lets the Dark Horse fly: it gets the power-up, which the +/// Icarus map requires, like the wings and the Horn of Fenrir. The client counts it as a flying mount. +/// +[PlugIn] +[Display(Name = PlugInName, Description = PlugInDescription)] +[Guid("B2A18B26-5BF6-4EBC-80D9-421CB8B6BFCE")] +public class AddDarkHorseCanFlyPlugIn : UpdatePlugInBase +{ + /// + /// The plug in name. + /// + internal const string PlugInName = "Add CanFly to the Dark Horse"; + + /// + /// The plug in description. + /// + internal const string PlugInDescription = "This update lets characters on a Dark Horse enter Icarus, like with wings, a Horn of Dinorant or a Horn of Fenrir."; + + private const byte DarkHorseGroup = 13; + + private const short DarkHorseNumber = 4; + + /// + public override string Name => PlugInName; + + /// + public override string Description => PlugInDescription; + + /// + public override UpdateVersion Version => UpdateVersion.AddDarkHorseCanFly; + + /// + public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id; + + /// + public override bool IsMandatory => true; + + /// + public override DateTime CreatedAt => new(2026, 09, 26, 22, 0, 0, DateTimeKind.Utc); + + /// + protected override ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) + { + var canFly = Stats.CanFly.GetPersistent(gameConfiguration); + foreach (var darkHorse in gameConfiguration.Items.Where(item => item.Group == DarkHorseGroup && item.Number == DarkHorseNumber)) + { + if (darkHorse.BasePowerUpAttributes.Any(powerUp => powerUp.TargetAttribute == canFly)) + { + continue; + } + + var powerUp = context.CreateNew(); + powerUp.TargetAttribute = canFly; + powerUp.BaseValue = 1; + powerUp.AggregateType = AggregateType.AddRaw; + darkHorse.BasePowerUpAttributes.Add(powerUp); + } + + return ValueTask.CompletedTask; + } +} diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs index badc93514d..67f7ec0753 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -589,4 +589,9 @@ public enum UpdateVersion /// The version of the . /// AddItemRuleFlags = 116, + + /// + /// The version of the . + /// + AddDarkHorseCanFly = 117, } diff --git a/src/Persistence/Initialization/VersionSeasonSix/Items/Pets.cs b/src/Persistence/Initialization/VersionSeasonSix/Items/Pets.cs index d25609db4c..d76310dd72 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Items/Pets.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Items/Pets.cs @@ -65,6 +65,7 @@ public override void Initialize() var darkHorse = this.CreatePet(4, SkillNumber.Earthshake, 1, 1, "Dark Horse", 218, false, false, (Stats.IsHorseEquipped, 1, AggregateType.AddRaw), + (Stats.CanFly, 1.0f, AggregateType.AddRaw), (Stats.MovementSpeed, MovementSpeedConstants.HorseOrFenrirMovementSpeed, AggregateType.Maximum), (Stats.MovementSpeedUnderwater, MovementSpeedConstants.HorseOrFenrirMovementSpeed, AggregateType.Maximum)); this.AddDarkHorseOptions(darkHorse); diff --git a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/DarkHorseCanFlyTest.cs b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/DarkHorseCanFlyTest.cs new file mode 100644 index 0000000000..79599d9cea --- /dev/null +++ b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/DarkHorseCanFlyTest.cs @@ -0,0 +1,73 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Tests; + +using Microsoft.Extensions.Logging.Abstractions; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.Persistence.Initialization.Updates; +using MUnique.OpenMU.Persistence.InMemory; + +/// +/// Tests that the Dark Horse can fly () in the season 6 data. +/// +[TestFixture] +internal class DarkHorseCanFlyTest +{ + /// + /// Tests that a new season 6 database lets the Dark Horse fly. + /// + [Test] + public async Task NewDatabaseLetsDarkHorseFlyAsync() + { + var (_, gameConfiguration) = await CreateSeason6ConfigurationAsync().ConfigureAwait(false); + + Assert.That(CountCanFly(GetDarkHorse(gameConfiguration)), Is.EqualTo(1)); + } + + /// + /// Tests that the update adds the power-up to a database which was created before it existed, + /// and that applying it twice doesn't add it twice. + /// + [Test] + public async Task UpdateLetsDarkHorseFlyOnExistingDatabaseAsync() + { + var (context, gameConfiguration) = await CreateSeason6ConfigurationAsync().ConfigureAwait(false); + using var _ = context; + var darkHorse = GetDarkHorse(gameConfiguration); + foreach (var powerUp in darkHorse.BasePowerUpAttributes.Where(p => p.TargetAttribute == Stats.CanFly).ToList()) + { + darkHorse.BasePowerUpAttributes.Remove(powerUp); + } + + var update = new AddDarkHorseCanFlyPlugIn(); + await update.ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false); + await update.ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false); + + Assert.That(CountCanFly(darkHorse), Is.EqualTo(1)); + } + + private static int CountCanFly(ItemDefinition item) + { + return item.BasePowerUpAttributes.Count(powerUp => powerUp.TargetAttribute == Stats.CanFly && powerUp.BaseValue > 0); + } + + private static ItemDefinition GetDarkHorse(GameConfiguration gameConfiguration) + { + return gameConfiguration.Items.Single(item => item is { Group: 13, Number: 4 }); + } + + private static async Task<(IContext Context, GameConfiguration GameConfiguration)> CreateSeason6ConfigurationAsync() + { + var contextProvider = new InMemoryPersistenceContextProvider(); + var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory()); + await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false); + + var context = contextProvider.CreateNewContext(); + var gameConfiguration = (await context.GetAsync().ConfigureAwait(false)).First(); + return (context, gameConfiguration); + } +}