From 5a7d749b3336b1bb91ae9042e560020e7265bd5f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 07:34:39 +0000 Subject: [PATCH 1/6] feat(persistence): add links of accounts to users of external services An AccountExternalLink links an account to a user of an external service like Discord, with a pending one-time code for the linking. Like the cash shop grants, it's a separate aggregate which refers to its account by id, so the external service can change it while a game server holds the account. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ --- src/DataModel/Entities/AccountExternalLink.cs | 67 + .../AccountExternalLink.Generated.cs | 46 + .../AccountContextModelBuilder.cs | 5 +- .../AccountExternalLinkEntityType.cs | 147 + .../AccountExternalLinkEntityType.cs | 147 + .../EntityDataContextModelBuilder.cs | 5 +- .../AccountExternalLinkEntityType.cs | 147 + .../TradeContext/TradeContextModelBuilder.cs | 5 +- .../EntityFramework/EntityDataContext.cs | 6 + .../AccountExternalLinkExtensions.cs | 31 + ...9073339_AddAccountExternalLink.Designer.cs | 6351 +++++++++++++++++ .../20261009073339_AddAccountExternalLink.cs | 70 + .../EntityDataContextModelSnapshot.cs | 53 + .../Model/AccountExternalLink.Generated.cs | 47 + .../Model/BasicModelConverter.Generated.cs | 22 + .../Model/ExtendedTypeContext.Generated.cs | 1 + .../EntityFramework/PlayerContext.cs | 61 + src/Persistence/IPlayerContext.cs | 46 + .../InMemory/PlayerInMemoryContext.cs | 47 + 19 files changed, 7301 insertions(+), 3 deletions(-) create mode 100644 src/DataModel/Entities/AccountExternalLink.cs create mode 100644 src/Persistence/BasicModel/AccountExternalLink.Generated.cs create mode 100644 src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountExternalLinkEntityType.cs create mode 100644 src/Persistence/EntityFramework/CompiledModels/EntityDataContext/AccountExternalLinkEntityType.cs create mode 100644 src/Persistence/EntityFramework/CompiledModels/TradeContext/AccountExternalLinkEntityType.cs create mode 100644 src/Persistence/EntityFramework/Extensions/ModelBuilder/AccountExternalLinkExtensions.cs create mode 100644 src/Persistence/EntityFramework/Migrations/20261009073339_AddAccountExternalLink.Designer.cs create mode 100644 src/Persistence/EntityFramework/Migrations/20261009073339_AddAccountExternalLink.cs create mode 100644 src/Persistence/EntityFramework/Model/AccountExternalLink.Generated.cs diff --git a/src/DataModel/Entities/AccountExternalLink.cs b/src/DataModel/Entities/AccountExternalLink.cs new file mode 100644 index 000000000..945f979d3 --- /dev/null +++ b/src/DataModel/Entities/AccountExternalLink.cs @@ -0,0 +1,67 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.DataModel.Entities; + +/// +/// A link of an to a user of an external service, e.g. Discord. +/// +/// +/// The link is created in two steps: In the game, the player requests a one-time code, which +/// is stored as hash with an expiration. In the external service, the user enters the code, +/// which links the user to the account. +/// It's not part of the aggregate, because the external service links it +/// while a game server may hold the account in memory. That's why it refers to its account by identifier. +/// +[AggregateRoot] +public class AccountExternalLink +{ + /// + /// Gets or sets the identifier. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets the identifier of the linked account. + /// + public Guid AccountId { get; set; } + + /// + /// Gets or sets the name of the external service, e.g. discord. + /// An account can be linked to one user per service. + /// + public string Provider { get; set; } = string.Empty; + + /// + /// Gets or sets the identifier of the user in the external service; + /// null, if the link isn't confirmed yet. + /// A user can be linked to one account. + /// + public string? ExternalUserId { get; set; } + + /// + /// Gets or sets the name of the user in the external service, at the time of linking. + /// + public string? ExternalUserName { get; set; } + + /// + /// Gets or sets the name of the character, as which the user appears, e.g. when chatting from the external service. + /// + public string? CharacterName { get; set; } + + /// + /// Gets or sets the timestamp at which the user was linked. + /// + public DateTime? LinkedAt { get; set; } + + /// + /// Gets or sets the hash of the pending one-time code; null, if there is none. + /// + public string? CodeHash { get; set; } + + /// + /// Gets or sets the timestamp until which the pending code is valid. + /// + public DateTime? CodeExpiresAt { get; set; } +} diff --git a/src/Persistence/BasicModel/AccountExternalLink.Generated.cs b/src/Persistence/BasicModel/AccountExternalLink.Generated.cs new file mode 100644 index 000000000..be53fa065 --- /dev/null +++ b/src/Persistence/BasicModel/AccountExternalLink.Generated.cs @@ -0,0 +1,46 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +//------------------------------------------------------------------------------ +// +// This source code was auto-generated by a roslyn code generator. +// +//------------------------------------------------------------------------------ + +// ReSharper disable All + +namespace MUnique.OpenMU.Persistence.BasicModel; + +using MUnique.OpenMU.Persistence.Json; + +/// +/// A plain implementation of . +/// +public partial class AccountExternalLink : MUnique.OpenMU.DataModel.Entities.AccountExternalLink, IIdentifiable, IConvertibleTo +{ + + + + + /// + public override bool Equals(object obj) + { + var baseObject = obj as IIdentifiable; + if (baseObject != null) + { + return baseObject.Id == this.Id; + } + + return base.Equals(obj); + } + + /// + public override int GetHashCode() + { + return this.Id.GetHashCode(); + } + + /// + public AccountExternalLink Convert() => this; +} diff --git a/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountContextModelBuilder.cs b/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountContextModelBuilder.cs index fb2a4fdf8..a8c8c4295 100644 --- a/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountContextModelBuilder.cs +++ b/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountContextModelBuilder.cs @@ -12,7 +12,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.CompiledModels.ForAccountCo public partial class AccountContextModel { private AccountContextModel() - : base(skipDetectChanges: false, modelId: new Guid("8b99359b-a111-4b0d-b8f0-fca6f18076e4"), entityTypeCount: 45) + : base(skipDetectChanges: false, modelId: new Guid("febbaf8d-9359-42d0-9873-d91e2db419ad"), entityTypeCount: 46) { } @@ -20,6 +20,7 @@ partial void Initialize() { var account = AccountEntityType.Create(this); var accountCharacterClass = AccountCharacterClassEntityType.Create(this); + var accountExternalLink = AccountExternalLinkEntityType.Create(this); var appearanceData = AppearanceDataEntityType.Create(this); var attributeRelationship = AttributeRelationshipEntityType.Create(this); var cashShopCoinGrant = CashShopCoinGrantEntityType.Create(this); @@ -66,6 +67,7 @@ partial void Initialize() AccountEntityType.CreateForeignKey1(account, itemStorage); AccountCharacterClassEntityType.CreateForeignKey1(accountCharacterClass, account); + AccountExternalLinkEntityType.CreateForeignKey1(accountExternalLink, account); AttributeRelationshipEntityType.CreateForeignKey1(attributeRelationship, powerUpDefinitionValue); CashShopCoinGrantEntityType.CreateForeignKey1(cashShopCoinGrant, account); CashShopStorageItemEntityType.CreateForeignKey1(cashShopStorageItem, account); @@ -96,6 +98,7 @@ partial void Initialize() AccountEntityType.CreateAnnotations(account); AccountCharacterClassEntityType.CreateAnnotations(accountCharacterClass); + AccountExternalLinkEntityType.CreateAnnotations(accountExternalLink); AppearanceDataEntityType.CreateAnnotations(appearanceData); AttributeRelationshipEntityType.CreateAnnotations(attributeRelationship); CashShopCoinGrantEntityType.CreateAnnotations(cashShopCoinGrant); diff --git a/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountExternalLinkEntityType.cs b/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountExternalLinkEntityType.cs new file mode 100644 index 000000000..5966cac86 --- /dev/null +++ b/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountExternalLinkEntityType.cs @@ -0,0 +1,147 @@ +// +using System; +using System.Reflection; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.Persistence.EntityFramework.Model; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#pragma warning disable 219, 612, 618 +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.CompiledModels.ForAccountContext +{ + [EntityFrameworkInternal] + public partial class AccountExternalLinkEntityType + { + public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null) + { + var runtimeEntityType = model.AddEntityType( + "MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink", + typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink), + baseEntityType, + propertyCount: 9, + foreignKeyCount: 1, + unnamedIndexCount: 3, + keyCount: 1); + + var id = runtimeEntityType.AddProperty( + "Id", + typeof(Guid), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + valueGenerated: ValueGenerated.OnAdd, + afterSaveBehavior: PropertySaveBehavior.Throw, + valueGeneratorFactory: new MUnique.OpenMU.Persistence.EntityFramework.GuidV7ValueGeneratorFactory().Create, + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); + id.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var accountId = runtimeEntityType.AddProperty( + "AccountId", + typeof(Guid), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("AccountId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); + accountId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var characterName = runtimeEntityType.AddProperty( + "CharacterName", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("CharacterName", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + characterName.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var codeExpiresAt = runtimeEntityType.AddProperty( + "CodeExpiresAt", + typeof(DateTime?), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("CodeExpiresAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + codeExpiresAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var codeHash = runtimeEntityType.AddProperty( + "CodeHash", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("CodeHash", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + codeHash.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var externalUserId = runtimeEntityType.AddProperty( + "ExternalUserId", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("ExternalUserId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + externalUserId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var externalUserName = runtimeEntityType.AddProperty( + "ExternalUserName", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("ExternalUserName", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + externalUserName.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var linkedAt = runtimeEntityType.AddProperty( + "LinkedAt", + typeof(DateTime?), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("LinkedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + linkedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var provider = runtimeEntityType.AddProperty( + "Provider", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("Provider", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + provider.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var key = runtimeEntityType.AddKey( + new[] { id }); + runtimeEntityType.SetPrimaryKey(key); + + var index = runtimeEntityType.AddIndex( + new[] { codeHash }); + + var index0 = runtimeEntityType.AddIndex( + new[] { accountId, provider }, + unique: true); + + var index1 = runtimeEntityType.AddIndex( + new[] { provider, externalUserId }, + unique: true); + + return runtimeEntityType; + } + + public static RuntimeForeignKey CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType) + { + var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty("AccountId") }, + principalEntityType.FindKey(new[] { principalEntityType.FindProperty("Id") }), + principalEntityType, + deleteBehavior: DeleteBehavior.Cascade, + required: true); + + return runtimeForeignKey; + } + + public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) + { + runtimeEntityType.AddAnnotation("Relational:FunctionName", null); + runtimeEntityType.AddAnnotation("Relational:Schema", "data"); + runtimeEntityType.AddAnnotation("Relational:SqlQuery", null); + runtimeEntityType.AddAnnotation("Relational:TableName", "AccountExternalLink"); + runtimeEntityType.AddAnnotation("Relational:ViewName", null); + runtimeEntityType.AddAnnotation("Relational:ViewSchema", null); + + Customize(runtimeEntityType); + } + + static partial void Customize(RuntimeEntityType runtimeEntityType); + } +} diff --git a/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/AccountExternalLinkEntityType.cs b/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/AccountExternalLinkEntityType.cs new file mode 100644 index 000000000..b210dc2eb --- /dev/null +++ b/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/AccountExternalLinkEntityType.cs @@ -0,0 +1,147 @@ +// +using System; +using System.Reflection; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.Persistence.EntityFramework.Model; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#pragma warning disable 219, 612, 618 +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.CompiledModels.ForEntityDataContext +{ + [EntityFrameworkInternal] + public partial class AccountExternalLinkEntityType + { + public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null) + { + var runtimeEntityType = model.AddEntityType( + "MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink", + typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink), + baseEntityType, + propertyCount: 9, + foreignKeyCount: 1, + unnamedIndexCount: 3, + keyCount: 1); + + var id = runtimeEntityType.AddProperty( + "Id", + typeof(Guid), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + valueGenerated: ValueGenerated.OnAdd, + afterSaveBehavior: PropertySaveBehavior.Throw, + valueGeneratorFactory: new MUnique.OpenMU.Persistence.EntityFramework.GuidV7ValueGeneratorFactory().Create, + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); + id.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var accountId = runtimeEntityType.AddProperty( + "AccountId", + typeof(Guid), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("AccountId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); + accountId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var characterName = runtimeEntityType.AddProperty( + "CharacterName", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("CharacterName", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + characterName.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var codeExpiresAt = runtimeEntityType.AddProperty( + "CodeExpiresAt", + typeof(DateTime?), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("CodeExpiresAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + codeExpiresAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var codeHash = runtimeEntityType.AddProperty( + "CodeHash", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("CodeHash", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + codeHash.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var externalUserId = runtimeEntityType.AddProperty( + "ExternalUserId", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("ExternalUserId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + externalUserId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var externalUserName = runtimeEntityType.AddProperty( + "ExternalUserName", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("ExternalUserName", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + externalUserName.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var linkedAt = runtimeEntityType.AddProperty( + "LinkedAt", + typeof(DateTime?), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("LinkedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + linkedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var provider = runtimeEntityType.AddProperty( + "Provider", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("Provider", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + provider.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var key = runtimeEntityType.AddKey( + new[] { id }); + runtimeEntityType.SetPrimaryKey(key); + + var index = runtimeEntityType.AddIndex( + new[] { codeHash }); + + var index0 = runtimeEntityType.AddIndex( + new[] { accountId, provider }, + unique: true); + + var index1 = runtimeEntityType.AddIndex( + new[] { provider, externalUserId }, + unique: true); + + return runtimeEntityType; + } + + public static RuntimeForeignKey CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType) + { + var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty("AccountId") }, + principalEntityType.FindKey(new[] { principalEntityType.FindProperty("Id") }), + principalEntityType, + deleteBehavior: DeleteBehavior.Cascade, + required: true); + + return runtimeForeignKey; + } + + public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) + { + runtimeEntityType.AddAnnotation("Relational:FunctionName", null); + runtimeEntityType.AddAnnotation("Relational:Schema", "data"); + runtimeEntityType.AddAnnotation("Relational:SqlQuery", null); + runtimeEntityType.AddAnnotation("Relational:TableName", "AccountExternalLink"); + runtimeEntityType.AddAnnotation("Relational:ViewName", null); + runtimeEntityType.AddAnnotation("Relational:ViewSchema", null); + + Customize(runtimeEntityType); + } + + static partial void Customize(RuntimeEntityType runtimeEntityType); + } +} diff --git a/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/EntityDataContextModelBuilder.cs b/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/EntityDataContextModelBuilder.cs index ffaa85fe9..7fa80bdab 100644 --- a/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/EntityDataContextModelBuilder.cs +++ b/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/EntityDataContextModelBuilder.cs @@ -12,7 +12,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.CompiledModels.ForEntityDat public partial class EntityDataContextModel { private EntityDataContextModel() - : base(skipDetectChanges: false, modelId: new Guid("4a60a1d5-f676-4193-90ba-ccdedaa4b496"), entityTypeCount: 119) + : base(skipDetectChanges: false, modelId: new Guid("241b86a9-af29-4213-80a7-d679277bb936"), entityTypeCount: 120) { } @@ -20,6 +20,7 @@ partial void Initialize() { var account = AccountEntityType.Create(this); var accountCharacterClass = AccountCharacterClassEntityType.Create(this); + var accountExternalLink = AccountExternalLinkEntityType.Create(this); var appearanceData = AppearanceDataEntityType.Create(this); var areaSkillSettings = AreaSkillSettingsEntityType.Create(this); var attributeDefinition = AttributeDefinitionEntityType.Create(this); @@ -141,6 +142,7 @@ partial void Initialize() AccountEntityType.CreateForeignKey1(account, itemStorage); AccountCharacterClassEntityType.CreateForeignKey1(accountCharacterClass, account); AccountCharacterClassEntityType.CreateForeignKey2(accountCharacterClass, characterClass); + AccountExternalLinkEntityType.CreateForeignKey1(accountExternalLink, account); AppearanceDataEntityType.CreateForeignKey1(appearanceData, characterClass); AttributeDefinitionEntityType.CreateForeignKey1(attributeDefinition, gameConfiguration); AttributeRelationshipEntityType.CreateForeignKey1(attributeRelationship, characterClass); @@ -378,6 +380,7 @@ partial void Initialize() AccountEntityType.CreateAnnotations(account); AccountCharacterClassEntityType.CreateAnnotations(accountCharacterClass); + AccountExternalLinkEntityType.CreateAnnotations(accountExternalLink); AppearanceDataEntityType.CreateAnnotations(appearanceData); AreaSkillSettingsEntityType.CreateAnnotations(areaSkillSettings); AttributeDefinitionEntityType.CreateAnnotations(attributeDefinition); diff --git a/src/Persistence/EntityFramework/CompiledModels/TradeContext/AccountExternalLinkEntityType.cs b/src/Persistence/EntityFramework/CompiledModels/TradeContext/AccountExternalLinkEntityType.cs new file mode 100644 index 000000000..636c7fe82 --- /dev/null +++ b/src/Persistence/EntityFramework/CompiledModels/TradeContext/AccountExternalLinkEntityType.cs @@ -0,0 +1,147 @@ +// +using System; +using System.Reflection; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.Persistence.EntityFramework.Model; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#pragma warning disable 219, 612, 618 +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.CompiledModels.ForTradeContext +{ + [EntityFrameworkInternal] + public partial class AccountExternalLinkEntityType + { + public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null) + { + var runtimeEntityType = model.AddEntityType( + "MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink", + typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink), + baseEntityType, + propertyCount: 9, + foreignKeyCount: 1, + unnamedIndexCount: 3, + keyCount: 1); + + var id = runtimeEntityType.AddProperty( + "Id", + typeof(Guid), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + valueGenerated: ValueGenerated.OnAdd, + afterSaveBehavior: PropertySaveBehavior.Throw, + valueGeneratorFactory: new MUnique.OpenMU.Persistence.EntityFramework.GuidV7ValueGeneratorFactory().Create, + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); + id.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var accountId = runtimeEntityType.AddProperty( + "AccountId", + typeof(Guid), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("AccountId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + sentinel: new Guid("00000000-0000-0000-0000-000000000000")); + accountId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var characterName = runtimeEntityType.AddProperty( + "CharacterName", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("CharacterName", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + characterName.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var codeExpiresAt = runtimeEntityType.AddProperty( + "CodeExpiresAt", + typeof(DateTime?), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("CodeExpiresAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + codeExpiresAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var codeHash = runtimeEntityType.AddProperty( + "CodeHash", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("CodeHash", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + codeHash.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var externalUserId = runtimeEntityType.AddProperty( + "ExternalUserId", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("ExternalUserId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + externalUserId.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var externalUserName = runtimeEntityType.AddProperty( + "ExternalUserName", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("ExternalUserName", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + externalUserName.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var linkedAt = runtimeEntityType.AddProperty( + "LinkedAt", + typeof(DateTime?), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("LinkedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + linkedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var provider = runtimeEntityType.AddProperty( + "Provider", + typeof(string), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("Provider", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + provider.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + + var key = runtimeEntityType.AddKey( + new[] { id }); + runtimeEntityType.SetPrimaryKey(key); + + var index = runtimeEntityType.AddIndex( + new[] { codeHash }); + + var index0 = runtimeEntityType.AddIndex( + new[] { accountId, provider }, + unique: true); + + var index1 = runtimeEntityType.AddIndex( + new[] { provider, externalUserId }, + unique: true); + + return runtimeEntityType; + } + + public static RuntimeForeignKey CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType) + { + var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty("AccountId") }, + principalEntityType.FindKey(new[] { principalEntityType.FindProperty("Id") }), + principalEntityType, + deleteBehavior: DeleteBehavior.Cascade, + required: true); + + return runtimeForeignKey; + } + + public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) + { + runtimeEntityType.AddAnnotation("Relational:FunctionName", null); + runtimeEntityType.AddAnnotation("Relational:Schema", "data"); + runtimeEntityType.AddAnnotation("Relational:SqlQuery", null); + runtimeEntityType.AddAnnotation("Relational:TableName", "AccountExternalLink"); + runtimeEntityType.AddAnnotation("Relational:ViewName", null); + runtimeEntityType.AddAnnotation("Relational:ViewSchema", null); + + Customize(runtimeEntityType); + } + + static partial void Customize(RuntimeEntityType runtimeEntityType); + } +} diff --git a/src/Persistence/EntityFramework/CompiledModels/TradeContext/TradeContextModelBuilder.cs b/src/Persistence/EntityFramework/CompiledModels/TradeContext/TradeContextModelBuilder.cs index dd749cdc1..8325349a9 100644 --- a/src/Persistence/EntityFramework/CompiledModels/TradeContext/TradeContextModelBuilder.cs +++ b/src/Persistence/EntityFramework/CompiledModels/TradeContext/TradeContextModelBuilder.cs @@ -12,7 +12,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.CompiledModels.ForTradeCont public partial class TradeContextModel { private TradeContextModel() - : base(skipDetectChanges: false, modelId: new Guid("e017e9d8-60cc-47ee-be81-ee7a7352c760"), entityTypeCount: 44) + : base(skipDetectChanges: false, modelId: new Guid("785b9a5a-7254-4952-be76-b1673c6069b4"), entityTypeCount: 45) { } @@ -20,6 +20,7 @@ partial void Initialize() { var account = AccountEntityType.Create(this); var accountCharacterClass = AccountCharacterClassEntityType.Create(this); + var accountExternalLink = AccountExternalLinkEntityType.Create(this); var appearanceData = AppearanceDataEntityType.Create(this); var attributeRelationship = AttributeRelationshipEntityType.Create(this); var cashShopCoinGrant = CashShopCoinGrantEntityType.Create(this); @@ -64,6 +65,7 @@ partial void Initialize() var statAttribute = StatAttributeEntityType.Create(this); AccountCharacterClassEntityType.CreateForeignKey1(accountCharacterClass, account); + AccountExternalLinkEntityType.CreateForeignKey1(accountExternalLink, account); AttributeRelationshipEntityType.CreateForeignKey1(attributeRelationship, powerUpDefinitionValue); CashShopCoinGrantEntityType.CreateForeignKey1(cashShopCoinGrant, account); CashShopStorageItemEntityType.CreateForeignKey1(cashShopStorageItem, account); @@ -92,6 +94,7 @@ partial void Initialize() AccountEntityType.CreateAnnotations(account); AccountCharacterClassEntityType.CreateAnnotations(accountCharacterClass); + AccountExternalLinkEntityType.CreateAnnotations(accountExternalLink); AppearanceDataEntityType.CreateAnnotations(appearanceData); AttributeRelationshipEntityType.CreateAnnotations(attributeRelationship); CashShopCoinGrantEntityType.CreateAnnotations(cashShopCoinGrant); diff --git a/src/Persistence/EntityFramework/EntityDataContext.cs b/src/Persistence/EntityFramework/EntityDataContext.cs index 992bad643..8ae00c4ef 100644 --- a/src/Persistence/EntityFramework/EntityDataContext.cs +++ b/src/Persistence/EntityFramework/EntityDataContext.cs @@ -70,6 +70,11 @@ public class EntityDataContext : ExtendedTypeContext /// internal DbSet CashShopCoinGrants => this.Set(); + /// + /// Gets the links of the accounts to users of external services. + /// + internal DbSet AccountExternalLinks => this.Set(); + /// /// Gets the gens memberships of the characters. /// @@ -126,6 +131,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().Apply(); modelBuilder.Entity().Apply(); modelBuilder.Entity().Apply(); + modelBuilder.Entity().Apply(); modelBuilder.Entity().Apply(); modelBuilder.Entity().Apply(); modelBuilder.Entity().Apply(); diff --git a/src/Persistence/EntityFramework/Extensions/ModelBuilder/AccountExternalLinkExtensions.cs b/src/Persistence/EntityFramework/Extensions/ModelBuilder/AccountExternalLinkExtensions.cs new file mode 100644 index 000000000..093365aa2 --- /dev/null +++ b/src/Persistence/EntityFramework/Extensions/ModelBuilder/AccountExternalLinkExtensions.cs @@ -0,0 +1,31 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.EntityFramework.Extensions.ModelBuilder; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using MUnique.OpenMU.Persistence.EntityFramework.Model; + +/// +/// Extensions for the of the . +/// +internal static class AccountExternalLinkExtensions +{ + /// + /// Applies the settings for the entity. + /// + /// The builder. + public static void Apply(this EntityTypeBuilder builder) + { + builder.Property(link => link.Provider).IsRequired(); + builder.HasIndex(link => new { link.AccountId, link.Provider }).IsUnique(); + builder.HasIndex(link => new { link.Provider, link.ExternalUserId }).IsUnique(); + builder.HasIndex(link => link.CodeHash); + builder.HasOne() + .WithMany() + .HasForeignKey(link => link.AccountId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/src/Persistence/EntityFramework/Migrations/20261009073339_AddAccountExternalLink.Designer.cs b/src/Persistence/EntityFramework/Migrations/20261009073339_AddAccountExternalLink.Designer.cs new file mode 100644 index 000000000..ea6bd8423 --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20261009073339_AddAccountExternalLink.Designer.cs @@ -0,0 +1,6351 @@ +// +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("20261009073339_AddAccountExternalLink")] + partial class AddAccountExternalLink + { + /// + 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("GoblinPoints") + .HasColumnType("integer"); + + 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.Property("WCoinC") + .HasColumnType("integer"); + + b.Property("WCoinP") + .HasColumnType("integer"); + + 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.AccountExternalLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterName") + .HasColumnType("text"); + + b.Property("CodeExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CodeHash") + .HasColumnType("text"); + + b.Property("ExternalUserId") + .HasColumnType("text"); + + b.Property("ExternalUserName") + .HasColumnType("text"); + + b.Property("LinkedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CodeHash"); + + b.HasIndex("AccountId", "Provider") + .IsUnique(); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("AccountExternalLink", "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.CashShopCoinGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("integer"); + + b.Property("AppliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CoinType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GrantedBy") + .HasColumnType("text"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Reference") + .IsUnique(); + + b.HasIndex("AccountId", "AppliedAt"); + + b.ToTable("CashShopCoinGrant", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BannerSaleZone") + .HasColumnType("smallint"); + + b.Property("BannerYear") + .HasColumnType("smallint"); + + b.Property("BannerYearId") + .HasColumnType("smallint"); + + b.Property("ScriptSaleZone") + .HasColumnType("smallint"); + + b.Property("ScriptYear") + .HasColumnType("smallint"); + + b.Property("ScriptYearId") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("CashShopConfiguration", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CashShopConfigurationId") + .HasColumnType("uuid"); + + b.Property("CoinType") + .HasColumnType("integer"); + + b.Property("IsBundle") + .HasColumnType("boolean"); + + b.Property("IsForSale") + .HasColumnType("boolean"); + + b.Property("IsGiftable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PackageSequence") + .HasColumnType("integer"); + + b.Property("Price") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CashShopConfigurationId"); + + b.ToTable("CashShopPackage", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopProduct", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CashShopPackageId") + .HasColumnType("uuid"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("ItemDefinitionId") + .HasColumnType("uuid"); + + b.Property("ItemLevel") + .HasColumnType("smallint"); + + b.Property("Price") + .HasColumnType("integer"); + + b.Property("PriceSequence") + .HasColumnType("integer"); + + b.Property("ProductSequence") + .HasColumnType("integer"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CashShopPackageId"); + + b.HasIndex("ItemDefinitionId"); + + b.ToTable("CashShopProduct", "config"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopStorageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GiftMessage") + .HasColumnType("text"); + + b.Property("GiftSenderName") + .HasColumnType("text"); + + b.Property("IsGift") + .HasColumnType("boolean"); + + b.Property("PriceSequence") + .HasColumnType("integer"); + + b.Property("ProductSequence") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("CashShopStorageItem", "data"); + }); + + 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("Key") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + 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("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("CashShopConfigurationId") + .HasColumnType("uuid"); + + 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("CashShopConfigurationId") + .IsUnique(); + + 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.GensAbuse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("KillerId") + .HasColumnType("uuid"); + + b.Property("LastKillAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VictimId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("VictimId"); + + b.HasIndex("KillerId", "VictimId") + .IsUnique(); + + b.ToTable("GensAbuse", "data"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GensMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("Contribution") + .HasColumnType("integer"); + + b.Property("Gens") + .HasColumnType("integer"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LeftAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Rank") + .HasColumnType("smallint"); + + b.Property("RankingPosition") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId") + .IsUnique(); + + b.ToTable("GensMember", "data"); + }); + + 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("MasterSkillDefinitionId") + .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("MasterSkillDefinitionId"); + + 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.AccountExternalLink", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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.CashShopCoinGrant", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopPackage", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopConfiguration", null) + .WithMany("RawPackages") + .HasForeignKey("CashShopConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopProduct", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopPackage", null) + .WithMany("RawProducts") + .HasForeignKey("CashShopPackageId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawItemDefinition") + .WithMany() + .HasForeignKey("ItemDefinitionId"); + + b.Navigation("RawItemDefinition"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopStorageItem", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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.CashShopConfiguration", "RawCashShopConfiguration") + .WithOne() + .HasForeignKey("MUnique.OpenMU.Persistence.EntityFramework.Model.GameConfiguration", "CashShopConfigurationId") + .OnDelete(DeleteBehavior.Cascade); + + 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("RawCashShopConfiguration"); + + 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.GensAbuse", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany() + .HasForeignKey("KillerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.GensMember", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Character", null) + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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.MasterSkillDefinition", null) + .WithMany("RawPassivePowerUps") + .HasForeignKey("MasterSkillDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + + 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.CashShopConfiguration", b => + { + b.Navigation("RawPackages"); + }); + + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.CashShopPackage", b => + { + b.Navigation("RawProducts"); + }); + + 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"); + + b.Navigation("RawPassivePowerUps"); + }); + + 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/20261009073339_AddAccountExternalLink.cs b/src/Persistence/EntityFramework/Migrations/20261009073339_AddAccountExternalLink.cs new file mode 100644 index 000000000..57f4432cd --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20261009073339_AddAccountExternalLink.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations +{ + /// + public partial class AddAccountExternalLink : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AccountExternalLink", + schema: "data", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + AccountId = table.Column(type: "uuid", nullable: false), + Provider = table.Column(type: "text", nullable: false), + ExternalUserId = table.Column(type: "text", nullable: true), + ExternalUserName = table.Column(type: "text", nullable: true), + CharacterName = table.Column(type: "text", nullable: true), + LinkedAt = table.Column(type: "timestamp with time zone", nullable: true), + CodeHash = table.Column(type: "text", nullable: true), + CodeExpiresAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountExternalLink", x => x.Id); + table.ForeignKey( + name: "FK_AccountExternalLink_Account_AccountId", + column: x => x.AccountId, + principalSchema: "data", + principalTable: "Account", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AccountExternalLink_AccountId_Provider", + schema: "data", + table: "AccountExternalLink", + columns: new[] { "AccountId", "Provider" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AccountExternalLink_CodeHash", + schema: "data", + table: "AccountExternalLink", + column: "CodeHash"); + + migrationBuilder.CreateIndex( + name: "IX_AccountExternalLink_Provider_ExternalUserId", + schema: "data", + table: "AccountExternalLink", + columns: new[] { "Provider", "ExternalUserId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AccountExternalLink", + schema: "data"); + } + } +} diff --git a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs index 82860a33a..56ec09451 100644 --- a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs +++ b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs @@ -118,6 +118,50 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AccountCharacterClass", "data"); }); + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CharacterName") + .HasColumnType("text"); + + b.Property("CodeExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CodeHash") + .HasColumnType("text"); + + b.Property("ExternalUserId") + .HasColumnType("text"); + + b.Property("ExternalUserName") + .HasColumnType("text"); + + b.Property("LinkedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CodeHash"); + + b.HasIndex("AccountId", "Provider") + .IsUnique(); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("AccountExternalLink", "data"); + }); + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => { b.Property("Id") @@ -4225,6 +4269,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("CharacterClass"); }); + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("MUnique.OpenMU.Persistence.EntityFramework.Model.AppearanceData", b => { b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.CharacterClass", "RawCharacterClass") diff --git a/src/Persistence/EntityFramework/Model/AccountExternalLink.Generated.cs b/src/Persistence/EntityFramework/Model/AccountExternalLink.Generated.cs new file mode 100644 index 000000000..4b3874fff --- /dev/null +++ b/src/Persistence/EntityFramework/Model/AccountExternalLink.Generated.cs @@ -0,0 +1,47 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +//------------------------------------------------------------------------------ +// +// This source code was auto-generated by a roslyn code generator. +// +//------------------------------------------------------------------------------ + +// ReSharper disable All + +namespace MUnique.OpenMU.Persistence.EntityFramework.Model; + +using System.ComponentModel.DataAnnotations.Schema; +using MUnique.OpenMU.Persistence; + +/// +/// The Entity Framework Core implementation of . +/// +[Table(nameof(AccountExternalLink), Schema = SchemaNames.AccountData)] +internal partial class AccountExternalLink : MUnique.OpenMU.DataModel.Entities.AccountExternalLink, IIdentifiable +{ + + + + + /// + public override bool Equals(object obj) + { + var baseObject = obj as IIdentifiable; + if (baseObject != null) + { + return baseObject.Id == this.Id; + } + + return base.Equals(obj); + } + + /// + public override int GetHashCode() + { + return this.Id.GetHashCode(); + } + + +} diff --git a/src/Persistence/EntityFramework/Model/BasicModelConverter.Generated.cs b/src/Persistence/EntityFramework/Model/BasicModelConverter.Generated.cs index aba30ffdc..e2adfcc8a 100644 --- a/src/Persistence/EntityFramework/Model/BasicModelConverter.Generated.cs +++ b/src/Persistence/EntityFramework/Model/BasicModelConverter.Generated.cs @@ -50,6 +50,7 @@ public static TBasic Convert(object source) IncreasableItemOption value => this.Convert(value), ItemDropItemGroup value => this.Convert(value), Account value => this.Convert(value), + AccountExternalLink value => this.Convert(value), AppearanceData value => this.Convert(value), AreaSkillSettings value => this.Convert(value), AttributeDefinition value => this.Convert(value), @@ -205,6 +206,27 @@ private BasicModel.Account Convert(Account source) return target; } + private BasicModel.AccountExternalLink Convert(AccountExternalLink source) + { + if (this._converted.TryGetValue(source, out var converted)) + { + return (BasicModel.AccountExternalLink)converted; + } + + var target = new BasicModel.AccountExternalLink(); + this._converted.Add(source, target); + target.Id = source.Id; + target.AccountId = source.AccountId; + target.Provider = source.Provider; + target.ExternalUserId = source.ExternalUserId; + target.ExternalUserName = source.ExternalUserName; + target.CharacterName = source.CharacterName; + target.LinkedAt = source.LinkedAt; + target.CodeHash = source.CodeHash; + target.CodeExpiresAt = source.CodeExpiresAt; + return target; + } + private BasicModel.AppearanceData Convert(AppearanceData source) { if (this._converted.TryGetValue(source, out var converted)) diff --git a/src/Persistence/EntityFramework/Model/ExtendedTypeContext.Generated.cs b/src/Persistence/EntityFramework/Model/ExtendedTypeContext.Generated.cs index 372be9a72..45af47337 100644 --- a/src/Persistence/EntityFramework/Model/ExtendedTypeContext.Generated.cs +++ b/src/Persistence/EntityFramework/Model/ExtendedTypeContext.Generated.cs @@ -26,6 +26,7 @@ protected override void OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuild { modelBuilder.Ignore(); modelBuilder.Ignore(); + modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); diff --git a/src/Persistence/EntityFramework/PlayerContext.cs b/src/Persistence/EntityFramework/PlayerContext.cs index 123f8fd75..bb7756d64 100644 --- a/src/Persistence/EntityFramework/PlayerContext.cs +++ b/src/Persistence/EntityFramework/PlayerContext.cs @@ -247,6 +247,67 @@ public async ValueTask CanSaveLetterAsync(Interfaces.LetterHeader letterHe } } + /// + public async ValueTask GetAccountExternalLinkAsync( + Guid accountId, + string provider, + CancellationToken cancellationToken = default) + { + using var l = await this.LockAsync(cancellationToken).ConfigureAwait(false); + using (this.RepositoryProvider.ContextStack.UseContext(this)) + { + return await this.Context.Set() + .FirstOrDefaultAsync(link => link.AccountId == accountId && link.Provider == provider, cancellationToken) + .ConfigureAwait(false); + } + } + + /// + public async ValueTask GetAccountExternalLinkByUserAsync( + string provider, + string externalUserId, + CancellationToken cancellationToken = default) + { + using var l = await this.LockAsync(cancellationToken).ConfigureAwait(false); + using (this.RepositoryProvider.ContextStack.UseContext(this)) + { + return await this.Context.Set() + .FirstOrDefaultAsync(link => link.Provider == provider && link.ExternalUserId == externalUserId, cancellationToken) + .ConfigureAwait(false); + } + } + + /// + public async ValueTask GetAccountExternalLinkByCodeAsync( + string provider, + string codeHash, + CancellationToken cancellationToken = default) + { + using var l = await this.LockAsync(cancellationToken).ConfigureAwait(false); + using (this.RepositoryProvider.ContextStack.UseContext(this)) + { + return await this.Context.Set() + .FirstOrDefaultAsync(link => link.Provider == provider && link.CodeHash == codeHash, cancellationToken) + .ConfigureAwait(false); + } + } + + /// + public async ValueTask> GetAccountExternalLinksAsync( + Guid accountId, + CancellationToken cancellationToken = default) + { + using var l = await this.LockAsync(cancellationToken).ConfigureAwait(false); + using (this.RepositoryProvider.ContextStack.UseContext(this)) + { + return await this.Context.Set() + .Where(link => link.AccountId == accountId) + .OrderBy(link => link.Provider) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + } + } + /// public async ValueTask GetGensMemberAsync( Guid characterId, diff --git a/src/Persistence/IPlayerContext.cs b/src/Persistence/IPlayerContext.cs index 01d1a7aef..262403236 100644 --- a/src/Persistence/IPlayerContext.cs +++ b/src/Persistence/IPlayerContext.cs @@ -161,6 +161,52 @@ ValueTask> GetLatestCashShopCoinGrantsAsync( string reference, CancellationToken cancellationToken = default); + /// + /// Gets the link of an account to a user of an external service. + /// + /// The persistent account identifier. + /// The name of the external service, e.g. discord. + /// The cancellation token. + /// The link; Otherwise, null, if the account has no link to the service. + ValueTask GetAccountExternalLinkAsync( + Guid accountId, + string provider, + CancellationToken cancellationToken = default); + + /// + /// Gets the link of a user of an external service to an account. + /// + /// The name of the external service, e.g. discord. + /// The identifier of the user in the external service. + /// The cancellation token. + /// The link; Otherwise, null, if the user isn't linked. + ValueTask GetAccountExternalLinkByUserAsync( + string provider, + string externalUserId, + CancellationToken cancellationToken = default); + + /// + /// Gets the link with a pending one-time code. + /// + /// The name of the external service, e.g. discord. + /// The hash of the code. + /// The cancellation token. + /// The link; Otherwise, null, if no link has the code. + ValueTask GetAccountExternalLinkByCodeAsync( + string provider, + string codeHash, + CancellationToken cancellationToken = default); + + /// + /// Gets the links of an account to users of external services. + /// + /// The persistent account identifier. + /// The cancellation token. + /// The links. + ValueTask> GetAccountExternalLinksAsync( + Guid accountId, + CancellationToken cancellationToken = default); + /// /// Gets the gens membership of a character. /// diff --git a/src/Persistence/InMemory/PlayerInMemoryContext.cs b/src/Persistence/InMemory/PlayerInMemoryContext.cs index e61577f74..a4c0f9220 100644 --- a/src/Persistence/InMemory/PlayerInMemoryContext.cs +++ b/src/Persistence/InMemory/PlayerInMemoryContext.cs @@ -156,6 +156,53 @@ public async ValueTask CanSaveLetterAsync(Interfaces.LetterHeader letterHe return grants.FirstOrDefault(grant => grant.Reference == reference); } + /// + public async ValueTask GetAccountExternalLinkAsync( + Guid accountId, + string provider, + CancellationToken cancellationToken = default) + { + var links = await this.Provider.GetRepository() + .GetAllAsync(cancellationToken) + .ConfigureAwait(false); + return links.FirstOrDefault(link => link.AccountId == accountId && link.Provider == provider); + } + + /// + public async ValueTask GetAccountExternalLinkByUserAsync( + string provider, + string externalUserId, + CancellationToken cancellationToken = default) + { + var links = await this.Provider.GetRepository() + .GetAllAsync(cancellationToken) + .ConfigureAwait(false); + return links.FirstOrDefault(link => link.Provider == provider && link.ExternalUserId == externalUserId); + } + + /// + public async ValueTask GetAccountExternalLinkByCodeAsync( + string provider, + string codeHash, + CancellationToken cancellationToken = default) + { + var links = await this.Provider.GetRepository() + .GetAllAsync(cancellationToken) + .ConfigureAwait(false); + return links.FirstOrDefault(link => link.Provider == provider && link.CodeHash == codeHash); + } + + /// + public async ValueTask> GetAccountExternalLinksAsync( + Guid accountId, + CancellationToken cancellationToken = default) + { + var links = await this.Provider.GetRepository() + .GetAllAsync(cancellationToken) + .ConfigureAwait(false); + return links.Where(link => link.AccountId == accountId).OrderBy(link => link.Provider).ToList(); + } + /// public async ValueTask GetGensMemberAsync( Guid characterId, From 10de9e5b266bef7b565fcfc6a96f3440e49ef69b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 07:47:56 +0000 Subject: [PATCH 2/6] feat(game-logic): link accounts to Discord with /discord link The chat command creates a one-time code, which the player enters in Discord. /discord unlink removes the link and publishes an AccountUnlinkedEvent, so that the Discord bot can remove the role of the user. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ --- .../AccountLinking/AccountLinkResult.cs | 13 + .../AccountLinking/AccountLinkService.cs | 230 ++++++++++++++++++ .../CharacterSelectionResult.cs | 26 ++ .../Arguments/DiscordChatCommandArgs.cs | 28 +++ .../ChatCommands/DiscordChatCommandPlugIn.cs | 74 ++++++ .../Properties/PlayerMessage.Designer.cs | 36 +++ .../Properties/PlayerMessage.de.resx | 12 + src/GameLogic/Properties/PlayerMessage.resx | 12 + .../Properties/PlugInResources.Designer.cs | 18 ++ .../Properties/PlugInResources.de.resx | 6 + src/GameLogic/Properties/PlugInResources.resx | 6 + src/Interfaces/AccountUnlinkedEvent.cs | 15 ++ src/Interfaces/GameEvent.cs | 1 + .../AccountLinkServiceTest.cs | 175 +++++++++++++ .../DiscordChatCommandPlugInTest.cs | 45 ++++ 15 files changed, 697 insertions(+) create mode 100644 src/GameLogic/AccountLinking/AccountLinkResult.cs create mode 100644 src/GameLogic/AccountLinking/AccountLinkService.cs create mode 100644 src/GameLogic/AccountLinking/CharacterSelectionResult.cs create mode 100644 src/GameLogic/PlugIns/ChatCommands/Arguments/DiscordChatCommandArgs.cs create mode 100644 src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs create mode 100644 src/Interfaces/AccountUnlinkedEvent.cs create mode 100644 tests/MUnique.OpenMU.Tests/AccountLinkServiceTest.cs create mode 100644 tests/MUnique.OpenMU.Tests/DiscordChatCommandPlugInTest.cs diff --git a/src/GameLogic/AccountLinking/AccountLinkResult.cs b/src/GameLogic/AccountLinking/AccountLinkResult.cs new file mode 100644 index 000000000..87815648c --- /dev/null +++ b/src/GameLogic/AccountLinking/AccountLinkResult.cs @@ -0,0 +1,13 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.AccountLinking; + +/// +/// The result of linking a user of an external service to an account. +/// +/// The identifier of the linked account. +/// The name of the character as which the user appears. +/// The identifier of the user which was linked to the account before, if it's another one. +public sealed record AccountLinkResult(Guid AccountId, string? CharacterName, string? ReplacedExternalUserId); diff --git a/src/GameLogic/AccountLinking/AccountLinkService.cs b/src/GameLogic/AccountLinking/AccountLinkService.cs new file mode 100644 index 000000000..94466ec35 --- /dev/null +++ b/src/GameLogic/AccountLinking/AccountLinkService.cs @@ -0,0 +1,230 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.AccountLinking; + +using System.Security.Cryptography; +using System.Text; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.Persistence; + +/// +/// Links accounts to users of external services, e.g. Discord. +/// +/// +/// In the game, a player requests a one-time code with . +/// The user enters it in the external service, which calls . +/// A user is linked to one account, and an account to one user per service. +/// Each method uses its own short-lived persistence context, so that it doesn't interfere +/// with the context of a player which holds the account. +/// +public sealed class AccountLinkService +{ + /// + /// The name of Discord as external service. + /// + public const string DiscordProvider = "discord"; + + /// + /// The characters of a code. Characters which are easily confused, like 0 and O, are left out. + /// + private const string CodeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + + private const int CodeLength = 8; + + private readonly Func _createContext; + private readonly TimeProvider _timeProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The function which creates a new persistence context. + /// The time provider. + public AccountLinkService(Func createContext, TimeProvider? timeProvider = null) + { + this._createContext = createContext; + this._timeProvider = timeProvider ?? TimeProvider.System; + } + + /// + /// Gets the time in which a code has to be used. + /// + public static TimeSpan CodeValidity { get; } = TimeSpan.FromMinutes(10); + + /// + /// Creates a one-time code, with which a user of the external service links itself to the account. + /// A previously created code becomes invalid. An existing link stays until the code is used. + /// + /// The identifier of the account. + /// The name of the external service. + /// The name of the character as which the user should appear, e.g. the one which requested the code. + /// The code, e.g. ABCD-EFGH. + public async ValueTask CreateCodeAsync(Guid accountId, string provider, string? characterName) + { + var code = string.Create(CodeLength, 0, static (span, _) => + { + for (var i = 0; i < span.Length; i++) + { + span[i] = CodeAlphabet[RandomNumberGenerator.GetInt32(CodeAlphabet.Length)]; + } + }); + + using var context = this._createContext(); + var link = await context.GetAccountExternalLinkAsync(accountId, provider).ConfigureAwait(false); + if (link is null) + { + link = context.CreateNew(); + link.AccountId = accountId; + link.Provider = provider; + } + + link.CodeHash = HashCode(code); + link.CodeExpiresAt = this._timeProvider.GetUtcNow().UtcDateTime.Add(CodeValidity); + link.CharacterName = characterName; + await context.SaveChangesAsync().ConfigureAwait(false); + return $"{code[..(CodeLength / 2)]}-{code[(CodeLength / 2)..]}"; + } + + /// + /// Links a user of the external service to the account of the code. + /// When the user was linked to another account before, that link is removed. + /// + /// The name of the external service. + /// The code which the player got in the game. + /// The identifier of the user in the external service. + /// The name of the user in the external service. + /// The result; or null, if the code is unknown, expired or already used. + public async ValueTask LinkAsync(string provider, string code, string externalUserId, string externalUserName) + { + var codeHash = HashCode(NormalizeCode(code)); + using var context = this._createContext(); + if (await context.GetAccountExternalLinkByCodeAsync(provider, codeHash).ConfigureAwait(false) is not { } link + || link.CodeExpiresAt is not { } expiresAt + || expiresAt < this._timeProvider.GetUtcNow().UtcDateTime) + { + return null; + } + + var previousUserId = link.ExternalUserId; + if (previousUserId != externalUserId + && await context.GetAccountExternalLinkByUserAsync(provider, externalUserId).ConfigureAwait(false) is { } otherLink) + { + // The user is linked to another account, which is replaced. It's saved first, because a user can only be linked once. + await context.DeleteAsync(otherLink).ConfigureAwait(false); + await context.SaveChangesAsync().ConfigureAwait(false); + } + + link.ExternalUserId = externalUserId; + link.ExternalUserName = externalUserName; + link.LinkedAt = this._timeProvider.GetUtcNow().UtcDateTime; + link.CodeHash = null; + link.CodeExpiresAt = null; + await context.SaveChangesAsync().ConfigureAwait(false); + return new AccountLinkResult(link.AccountId, link.CharacterName, previousUserId == externalUserId ? null : previousUserId); + } + + /// + /// Gets the confirmed link of an account. + /// + /// The identifier of the account. + /// The name of the external service. + /// The link; or null, if the account isn't linked. + public async ValueTask GetLinkAsync(Guid accountId, string provider) + { + using var context = this._createContext(); + var link = await context.GetAccountExternalLinkAsync(accountId, provider).ConfigureAwait(false); + return link?.ExternalUserId is null ? null : link; + } + + /// + /// Gets the link of a user of an external service. + /// + /// The name of the external service. + /// The identifier of the user in the external service. + /// The link; or null, if the user isn't linked. + public async ValueTask GetLinkByUserAsync(string provider, string externalUserId) + { + using var context = this._createContext(); + return await context.GetAccountExternalLinkByUserAsync(provider, externalUserId).ConfigureAwait(false); + } + + /// + /// Removes the link of an account, including a pending code. + /// + /// The identifier of the account. + /// The name of the external service. + /// The identifier of the user which was linked; or null, if the account wasn't linked. + public async ValueTask UnlinkAccountAsync(Guid accountId, string provider) + { + using var context = this._createContext(); + if (await context.GetAccountExternalLinkAsync(accountId, provider).ConfigureAwait(false) is not { } link) + { + return null; + } + + await context.DeleteAsync(link).ConfigureAwait(false); + await context.SaveChangesAsync().ConfigureAwait(false); + return link.ExternalUserId; + } + + /// + /// Removes the link of a user of an external service. + /// + /// The name of the external service. + /// The identifier of the user in the external service. + /// true, if the user was linked; otherwise, false. + public async ValueTask UnlinkUserAsync(string provider, string externalUserId) + { + using var context = this._createContext(); + if (await context.GetAccountExternalLinkByUserAsync(provider, externalUserId).ConfigureAwait(false) is not { } link) + { + return false; + } + + await context.DeleteAsync(link).ConfigureAwait(false); + await context.SaveChangesAsync().ConfigureAwait(false); + return true; + } + + /// + /// Selects the character of the account, as which a linked user appears. + /// + /// The name of the external service. + /// The identifier of the user in the external service. + /// The name of the character. + /// The result. + public async ValueTask SelectCharacterAsync(string provider, string externalUserId, string characterName) + { + using var context = this._createContext(); + if (await context.GetAccountExternalLinkByUserAsync(provider, externalUserId).ConfigureAwait(false) is not { } link) + { + return CharacterSelectionResult.NotLinked; + } + + if (await context.GetAccountIdByCharacterNameAsync(characterName).ConfigureAwait(false) != link.AccountId) + { + return CharacterSelectionResult.CharacterNotFound; + } + + link.CharacterName = characterName; + await context.SaveChangesAsync().ConfigureAwait(false); + return CharacterSelectionResult.Selected; + } + + /// + /// Normalizes an entered code, so that it doesn't matter if it's entered in lower case or with separators. + /// + private static string NormalizeCode(string code) + { + return new string(code.Where(char.IsLetterOrDigit).Select(char.ToUpperInvariant).ToArray()); + } + + /// + /// Hashes a code, so that the stored codes can't be used by someone who can read the database. + /// + private static string HashCode(string code) + { + return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(code))); + } +} diff --git a/src/GameLogic/AccountLinking/CharacterSelectionResult.cs b/src/GameLogic/AccountLinking/CharacterSelectionResult.cs new file mode 100644 index 000000000..37f0c5b96 --- /dev/null +++ b/src/GameLogic/AccountLinking/CharacterSelectionResult.cs @@ -0,0 +1,26 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.AccountLinking; + +/// +/// The result of selecting the character, as which a linked user appears. +/// +public enum CharacterSelectionResult +{ + /// + /// The character was selected. + /// + Selected, + + /// + /// The user isn't linked to an account. + /// + NotLinked, + + /// + /// The account has no character with the name. + /// + CharacterNotFound, +} diff --git a/src/GameLogic/PlugIns/ChatCommands/Arguments/DiscordChatCommandArgs.cs b/src/GameLogic/PlugIns/ChatCommands/Arguments/DiscordChatCommandArgs.cs new file mode 100644 index 000000000..dd4131d76 --- /dev/null +++ b/src/GameLogic/PlugIns/ChatCommands/Arguments/DiscordChatCommandArgs.cs @@ -0,0 +1,28 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands.Arguments; + +/// +/// Arguments for the discord chat command. +/// +public class DiscordChatCommandArgs : ArgumentsBase +{ + /// + /// The action which creates a code for linking. + /// + public const string LinkAction = "link"; + + /// + /// The action which removes the link. + /// + public const string UnlinkAction = "unlink"; + + /// + /// Gets or sets the action. Without action, the state of the link is shown. + /// + [Argument("action", false)] + [ValidValues(LinkAction, UnlinkAction)] + public string? Action { get; set; } +} diff --git a/src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs b/src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs new file mode 100644 index 000000000..0623d4630 --- /dev/null +++ b/src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs @@ -0,0 +1,74 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.GameLogic.PlugIns.ChatCommands.Arguments; +using MUnique.OpenMU.Interfaces; +using MUnique.OpenMU.Persistence; +using MUnique.OpenMU.PlugIns; + +/// +/// A chat command plugin which links the account of the player to a Discord user. +/// /discord link creates a one-time code, which the player enters in Discord with /link. +/// /discord unlink removes the link, and /discord shows it. +/// +[Guid("6694D50F-49B1-481D-8F65-B0D7DCCA77C3")] +[PlugIn] +[Display(Name = nameof(PlugInResources.DiscordChatCommandPlugIn_Name), Description = nameof(PlugInResources.DiscordChatCommandPlugIn_Description), ResourceType = typeof(PlugInResources))] +[ChatCommandHelp(Command, typeof(DiscordChatCommandArgs), CharacterStatus.Normal)] +public class DiscordChatCommandPlugIn : ChatCommandPlugInBase +{ + private const string Command = "/discord"; + + /// + public override string Key => Command; + + /// + public override CharacterStatus MinCharacterStatusRequirement => CharacterStatus.Normal; + + /// + protected override async ValueTask DoHandleCommandAsync(Player player, DiscordChatCommandArgs arguments) + { + if (player.Account is not { } account) + { + return; + } + + var service = new AccountLinkService(() => player.GameContext.PersistenceContextProvider.CreateNewPlayerContext(player.GameContext.Configuration)); + var accountId = account.GetId(); + switch (arguments.Action?.ToLowerInvariant()) + { + case DiscordChatCommandArgs.LinkAction: + var code = await service.CreateCodeAsync(accountId, AccountLinkService.DiscordProvider, player.SelectedCharacter?.Name).ConfigureAwait(false); + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.DiscordLinkCode), code, (int)AccountLinkService.CodeValidity.TotalMinutes).ConfigureAwait(false); + break; + case DiscordChatCommandArgs.UnlinkAction: + if (await service.UnlinkAccountAsync(accountId, AccountLinkService.DiscordProvider).ConfigureAwait(false) is { } userId + && player.GameContext is IGameServerContext gameServerContext) + { + // The Discord bot removes the roles of the user. + await gameServerContext.EventPublisher.GameEventAsync( + new AccountUnlinkedEvent(gameServerContext.Id, DateTime.UtcNow, AccountLinkService.DiscordProvider, userId)).ConfigureAwait(false); + } + + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.DiscordUnlinked)).ConfigureAwait(false); + break; + default: + var link = await service.GetLinkAsync(accountId, AccountLinkService.DiscordProvider).ConfigureAwait(false); + if (link is null) + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.DiscordNotLinked)).ConfigureAwait(false); + } + else + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.DiscordLinked), link.ExternalUserName ?? link.ExternalUserId ?? string.Empty, link.CharacterName ?? "-").ConfigureAwait(false); + } + + break; + } + } +} diff --git a/src/GameLogic/Properties/PlayerMessage.Designer.cs b/src/GameLogic/Properties/PlayerMessage.Designer.cs index 4cceb8887..07ea7680d 100644 --- a/src/GameLogic/Properties/PlayerMessage.Designer.cs +++ b/src/GameLogic/Properties/PlayerMessage.Designer.cs @@ -238,6 +238,42 @@ public static string CharacterInfoFormat { } } + /// + /// Looks up a localized string similar to Your code to link Discord is {0}. Enter /link {0} in Discord within {1} minutes.. + /// + public static string DiscordLinkCode { + get { + return ResourceManager.GetString("DiscordLinkCode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your account is linked to the Discord user {0}, who appears as {1}. Use /discord unlink to remove the link.. + /// + public static string DiscordLinked { + get { + return ResourceManager.GetString("DiscordLinked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your account isn't linked to Discord. Use /discord link to get a code for linking.. + /// + public static string DiscordNotLinked { + get { + return ResourceManager.GetString("DiscordNotLinked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your account isn't linked to Discord anymore.. + /// + public static string DiscordUnlinked { + get { + return ResourceManager.GetString("DiscordUnlinked", resourceCulture); + } + } + /// /// Looks up a localized string similar to The Ice Walker appeared! Kill it within one minute, or the following monsters become stronger.. /// diff --git a/src/GameLogic/Properties/PlayerMessage.de.resx b/src/GameLogic/Properties/PlayerMessage.de.resx index b243f2268..1c987cb79 100644 --- a/src/GameLogic/Properties/PlayerMessage.de.resx +++ b/src/GameLogic/Properties/PlayerMessage.de.resx @@ -910,4 +910,16 @@ Verwendete Fruchtpunkte: {16} Verwendete negative Fruchtpunkte: {17} Inventarerweiterungen: {18} + + Dein Code zum Verknüpfen mit Discord lautet {0}. Gib innerhalb von {1} Minuten /link {0} in Discord ein. + + + Dein Konto ist mit dem Discord-Benutzer {0} verknüpft, der als {1} erscheint. Mit /discord unlink entfernst du die Verknüpfung. + + + Dein Konto ist nicht mit Discord verknüpft. Mit /discord link erhältst du einen Code zum Verknüpfen. + + + Dein Konto ist nicht mehr mit Discord verknüpft. + diff --git a/src/GameLogic/Properties/PlayerMessage.resx b/src/GameLogic/Properties/PlayerMessage.resx index 8e84c6384..938981d68 100644 --- a/src/GameLogic/Properties/PlayerMessage.resx +++ b/src/GameLogic/Properties/PlayerMessage.resx @@ -922,4 +922,16 @@ Inventory Extensions: {18} You can now create a character of the class {0}. + + Your code to link Discord is {0}. Enter /link {0} in Discord within {1} minutes. + + + Your account is linked to the Discord user {0}, who appears as {1}. Use /discord unlink to remove the link. + + + Your account isn't linked to Discord. Use /discord link to get a code for linking. + + + Your account isn't linked to Discord anymore. + diff --git a/src/GameLogic/Properties/PlugInResources.Designer.cs b/src/GameLogic/Properties/PlugInResources.Designer.cs index ac15068dd..132bc1316 100644 --- a/src/GameLogic/Properties/PlugInResources.Designer.cs +++ b/src/GameLogic/Properties/PlugInResources.Designer.cs @@ -645,6 +645,24 @@ public static string DisconnectChatCommandPlugIn_Name { } } + /// + /// Looks up a localized string similar to Links the account to a Discord user: '/discord link' creates a code which is entered in Discord, '/discord unlink' removes the link.. + /// + public static string DiscordChatCommandPlugIn_Description { + get { + return ResourceManager.GetString("DiscordChatCommandPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Discord command. + /// + public static string DiscordChatCommandPlugIn_Name { + get { + return ResourceManager.GetString("DiscordChatCommandPlugIn_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Configures the run of the doppelganger event, e.g. its monsters, their paths and multipliers.. /// diff --git a/src/GameLogic/Properties/PlugInResources.de.resx b/src/GameLogic/Properties/PlugInResources.de.resx index 20be88ed3..119433540 100644 --- a/src/GameLogic/Properties/PlugInResources.de.resx +++ b/src/GameLogic/Properties/PlugInResources.de.resx @@ -1767,4 +1767,10 @@ Meisterlevel-Meilensteine + + Befehl Discord + + + Verknüpft das Konto mit einem Discord-Benutzer: '/discord link' erstellt einen Code, der in Discord eingegeben wird, '/discord unlink' entfernt die Verknüpfung. + diff --git a/src/GameLogic/Properties/PlugInResources.resx b/src/GameLogic/Properties/PlugInResources.resx index 904eaad66..fc5e424f1 100644 --- a/src/GameLogic/Properties/PlugInResources.resx +++ b/src/GameLogic/Properties/PlugInResources.resx @@ -1610,4 +1610,10 @@ Master level milestones + + Discord command + + + Links the account to a Discord user: '/discord link' creates a code which is entered in Discord, '/discord unlink' removes the link. + diff --git a/src/Interfaces/AccountUnlinkedEvent.cs b/src/Interfaces/AccountUnlinkedEvent.cs new file mode 100644 index 000000000..3d574bf87 --- /dev/null +++ b/src/Interfaces/AccountUnlinkedEvent.cs @@ -0,0 +1,15 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Interfaces; + +/// +/// The link of an account to a user of an external service, e.g. Discord, was removed in the game. +/// +/// The identifier of the game server. +/// The timestamp, in UTC. +/// The name of the external service, e.g. discord. +/// The identifier of the user in the external service. +public sealed record AccountUnlinkedEvent(byte ServerId, DateTime TimestampUtc, string Provider, string ExternalUserId) + : GameEvent(ServerId, TimestampUtc); diff --git a/src/Interfaces/GameEvent.cs b/src/Interfaces/GameEvent.cs index b2c258675..3bfbf6333 100644 --- a/src/Interfaces/GameEvent.cs +++ b/src/Interfaces/GameEvent.cs @@ -28,4 +28,5 @@ namespace MUnique.OpenMU.Interfaces; [JsonDerivedType(typeof(GlobalNoticeEvent), nameof(GlobalNoticeEvent))] [JsonDerivedType(typeof(BossKilledEvent), nameof(BossKilledEvent))] [JsonDerivedType(typeof(CharacterLevelMilestoneEvent), nameof(CharacterLevelMilestoneEvent))] +[JsonDerivedType(typeof(AccountUnlinkedEvent), nameof(AccountUnlinkedEvent))] public abstract record GameEvent(byte ServerId, DateTime TimestampUtc); diff --git a/tests/MUnique.OpenMU.Tests/AccountLinkServiceTest.cs b/tests/MUnique.OpenMU.Tests/AccountLinkServiceTest.cs new file mode 100644 index 000000000..2a09898f4 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/AccountLinkServiceTest.cs @@ -0,0 +1,175 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.Persistence; +using MUnique.OpenMU.Persistence.InMemory; + +/// +/// Tests for the . +/// +[TestFixture] +public class AccountLinkServiceTest +{ + private const string Provider = AccountLinkService.DiscordProvider; + + private InMemoryPersistenceContextProvider _persistence = null!; + private ManualTimeProvider _time = null!; + private AccountLinkService _service = null!; + private Guid _accountId; + private Guid _otherAccountId; + + /// + /// Sets up two accounts. + /// + [SetUp] + public async Task SetUpAsync() + { + this._persistence = new InMemoryPersistenceContextProvider(); + this._time = new ManualTimeProvider(); + this._service = new AccountLinkService(this.CreateContext, this._time); + this._accountId = await this.CreateAccountAsync("hero", "Hero").ConfigureAwait(false); + this._otherAccountId = await this.CreateAccountAsync("other", "Other").ConfigureAwait(false); + } + + /// + /// Tests that a user is linked to the account with the code, and appears as the character which requested it. + /// + [Test] + public async Task UserIsLinkedWithCodeAsync() + { + var code = await this._service.CreateCodeAsync(this._accountId, Provider, "Hero").ConfigureAwait(false); + + var result = await this._service.LinkAsync(Provider, code.ToLowerInvariant(), "42", "hero#1").ConfigureAwait(false); + + Assert.That(code, Does.Match("^[A-Z2-9]{4}-[A-Z2-9]{4}$")); + Assert.That(result, Is.EqualTo(new AccountLinkResult(this._accountId, "Hero", null))); + var link = await this._service.GetLinkAsync(this._accountId, Provider).ConfigureAwait(false); + Assert.That(link?.ExternalUserId, Is.EqualTo("42")); + Assert.That(link?.ExternalUserName, Is.EqualTo("hero#1")); + Assert.That(link?.CodeHash, Is.Null); + } + + /// + /// Tests that a code can only be used once. + /// + [Test] + public async Task CodeCanBeUsedOnceAsync() + { + var code = await this._service.CreateCodeAsync(this._accountId, Provider, "Hero").ConfigureAwait(false); + await this._service.LinkAsync(Provider, code, "42", "hero").ConfigureAwait(false); + + var secondResult = await this._service.LinkAsync(Provider, code, "43", "thief").ConfigureAwait(false); + + Assert.That(secondResult, Is.Null); + Assert.That((await this._service.GetLinkAsync(this._accountId, Provider).ConfigureAwait(false))?.ExternalUserId, Is.EqualTo("42")); + } + + /// + /// Tests that an expired code can't be used, and that a new code replaces the previous one. + /// + [Test] + public async Task ExpiredAndReplacedCodesAreRejectedAsync() + { + var expiredCode = await this._service.CreateCodeAsync(this._accountId, Provider, "Hero").ConfigureAwait(false); + this._time.Advance(AccountLinkService.CodeValidity + TimeSpan.FromSeconds(1)); + Assert.That(await this._service.LinkAsync(Provider, expiredCode, "42", "hero").ConfigureAwait(false), Is.Null); + + var replacedCode = await this._service.CreateCodeAsync(this._accountId, Provider, "Hero").ConfigureAwait(false); + var newCode = await this._service.CreateCodeAsync(this._accountId, Provider, "Hero").ConfigureAwait(false); + Assert.That(await this._service.LinkAsync(Provider, replacedCode, "42", "hero").ConfigureAwait(false), Is.Null); + Assert.That(await this._service.LinkAsync(Provider, newCode, "42", "hero").ConfigureAwait(false), Is.Not.Null); + } + + /// + /// Tests that a user who links another account is unlinked from the previous one, + /// and that the previous user of the account is reported. + /// + [Test] + public async Task RelinkingReplacesPreviousLinksAsync() + { + await this.LinkAsync(this._accountId, "42").ConfigureAwait(false); + await this.LinkAsync(this._otherAccountId, "43").ConfigureAwait(false); + + // User 43 links the first account, which was linked to user 42. + var code = await this._service.CreateCodeAsync(this._accountId, Provider, "Hero").ConfigureAwait(false); + var result = await this._service.LinkAsync(Provider, code, "43", "other").ConfigureAwait(false); + + Assert.That(result?.ReplacedExternalUserId, Is.EqualTo("42")); + Assert.That((await this._service.GetLinkByUserAsync(Provider, "43").ConfigureAwait(false))?.AccountId, Is.EqualTo(this._accountId)); + Assert.That(await this._service.GetLinkByUserAsync(Provider, "42").ConfigureAwait(false), Is.Null); + Assert.That(await this._service.GetLinkAsync(this._otherAccountId, Provider).ConfigureAwait(false), Is.Null); + } + + /// + /// Tests that the link can be removed from both sides. + /// + [Test] + public async Task UnlinkingWorksFromBothSidesAsync() + { + await this.LinkAsync(this._accountId, "42").ConfigureAwait(false); + await this.LinkAsync(this._otherAccountId, "43").ConfigureAwait(false); + + Assert.That(await this._service.UnlinkAccountAsync(this._accountId, Provider).ConfigureAwait(false), Is.EqualTo("42")); + Assert.That(await this._service.UnlinkUserAsync(Provider, "43").ConfigureAwait(false), Is.True); + + Assert.That(await this._service.GetLinkAsync(this._accountId, Provider).ConfigureAwait(false), Is.Null); + Assert.That(await this._service.GetLinkAsync(this._otherAccountId, Provider).ConfigureAwait(false), Is.Null); + Assert.That(await this._service.UnlinkUserAsync(Provider, "43").ConfigureAwait(false), Is.False); + } + + /// + /// Tests that only a character of the linked account can be selected. + /// + [Test] + public async Task OnlyCharacterOfAccountCanBeSelectedAsync() + { + await this.LinkAsync(this._accountId, "42").ConfigureAwait(false); + + Assert.That(await this._service.SelectCharacterAsync(Provider, "42", "Other").ConfigureAwait(false), Is.EqualTo(CharacterSelectionResult.CharacterNotFound)); + Assert.That(await this._service.SelectCharacterAsync(Provider, "43", "Hero").ConfigureAwait(false), Is.EqualTo(CharacterSelectionResult.NotLinked)); + Assert.That(await this._service.SelectCharacterAsync(Provider, "42", "Hero2").ConfigureAwait(false), Is.EqualTo(CharacterSelectionResult.Selected)); + Assert.That((await this._service.GetLinkAsync(this._accountId, Provider).ConfigureAwait(false))?.CharacterName, Is.EqualTo("Hero2")); + } + + private IPlayerContext CreateContext() => this._persistence.CreateNewPlayerContext(new GameConfiguration()); + + private async Task LinkAsync(Guid accountId, string userId) + { + var code = await this._service.CreateCodeAsync(accountId, Provider, null).ConfigureAwait(false); + Assert.That(await this._service.LinkAsync(Provider, code, userId, userId).ConfigureAwait(false), Is.Not.Null); + } + + private async Task CreateAccountAsync(string loginName, string characterName) + { + using var context = this.CreateContext(); + var account = context.CreateNew(); + account.LoginName = loginName; + foreach (var name in new[] { characterName, characterName + "2" }) + { + var character = context.CreateNew(); + character.Name = name; + account.Characters.Add(character); + } + + await context.SaveChangesAsync().ConfigureAwait(false); + return account.GetId(); + } + + /// + /// A time provider whose time is advanced by the test. + /// + private sealed class ManualTimeProvider : TimeProvider + { + private DateTimeOffset _now = new(2026, 10, 9, 12, 0, 0, TimeSpan.Zero); + + public override DateTimeOffset GetUtcNow() => this._now; + + public void Advance(TimeSpan timeSpan) => this._now += timeSpan; + } +} diff --git a/tests/MUnique.OpenMU.Tests/DiscordChatCommandPlugInTest.cs b/tests/MUnique.OpenMU.Tests/DiscordChatCommandPlugInTest.cs new file mode 100644 index 000000000..837d954ef --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/DiscordChatCommandPlugInTest.cs @@ -0,0 +1,45 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; +using MUnique.OpenMU.Persistence; + +/// +/// Tests for the . +/// +[TestFixture] +public class DiscordChatCommandPlugInTest +{ + /// + /// Tests that /discord link creates a pending link with the selected character, + /// and that /discord unlink removes it. + /// + [Test] + public async Task LinkCreatesCodeAndUnlinkRemovesItAsync() + { + var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + player.SelectedCharacter!.Name = "Hero"; + var plugIn = new DiscordChatCommandPlugIn(); + + await plugIn.HandleCommandAsync(player, "/discord link").ConfigureAwait(false); + var pendingLink = await this.GetLinkAsync(player).ConfigureAwait(false); + await plugIn.HandleCommandAsync(player, "/discord unlink").ConfigureAwait(false); + + Assert.That(pendingLink, Is.Not.Null); + Assert.That(pendingLink!.CodeHash, Is.Not.Null); + Assert.That(pendingLink.ExternalUserId, Is.Null); + Assert.That(pendingLink.CharacterName, Is.EqualTo("Hero")); + Assert.That(await this.GetLinkAsync(player).ConfigureAwait(false), Is.Null); + } + + private async Task GetLinkAsync(Player player) + { + using var context = player.GameContext.PersistenceContextProvider.CreateNewPlayerContext(player.GameContext.Configuration); + return await context.GetAccountExternalLinkAsync(player.Account!.GetId(), AccountLinkService.DiscordProvider).ConfigureAwait(false); + } +} From 586114113e3632c53466b52ba002d3ae379ed5e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 07:47:56 +0000 Subject: [PATCH 3/6] feat(discord): add /link, /unlink and /character, and the Linked Player role Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ --- src/Discord/DiscordAccountCommands.cs | 126 ++++++++++++++++++ src/Discord/DiscordBot.cs | 87 +++++++++++- src/Discord/DiscordCommandAnswer.cs | 12 ++ src/Discord/DiscordCommands.cs | 17 ++- src/Discord/DiscordRoleChange.cs | 12 ++ .../DiscordServiceCollectionExtensions.cs | 10 +- src/Discord/Properties/Resources.Designer.cs | 108 +++++++++++++++ src/Discord/Properties/Resources.de.resx | 36 +++++ src/Discord/Properties/Resources.resx | 36 +++++ .../Provisioning/DiscordServerLayout.cs | 5 + .../Discord/DiscordAccountCommandsTest.cs | 98 ++++++++++++++ .../DiscordServiceCollectionExtensionsTest.cs | 7 +- 12 files changed, 544 insertions(+), 10 deletions(-) create mode 100644 src/Discord/DiscordAccountCommands.cs create mode 100644 src/Discord/DiscordCommandAnswer.cs create mode 100644 src/Discord/DiscordRoleChange.cs create mode 100644 tests/MUnique.OpenMU.Tests/Discord/DiscordAccountCommandsTest.cs diff --git a/src/Discord/DiscordAccountCommands.cs b/src/Discord/DiscordAccountCommands.cs new file mode 100644 index 000000000..839b62caa --- /dev/null +++ b/src/Discord/DiscordAccountCommands.cs @@ -0,0 +1,126 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Discord; + +using System.Globalization; +using Microsoft.Extensions.Logging; +using MUnique.OpenMU.Discord.Properties; +using MUnique.OpenMU.GameLogic.AccountLinking; + +/// +/// The slash commands with which Discord users link themselves to their game account. +/// Their answers are only shown to the user who used them. +/// +public sealed class DiscordAccountCommands +{ + private const string Provider = AccountLinkService.DiscordProvider; + + private readonly AccountLinkService _linkService; + private readonly CultureInfo _culture; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The service which links the accounts. + /// The culture of the answers. + /// The logger. + public DiscordAccountCommands(AccountLinkService linkService, CultureInfo culture, ILogger logger) + { + this._linkService = linkService; + this._culture = culture; + this._logger = logger; + } + + /// + /// Gets the definitions of the commands. + /// + public static IReadOnlyList Definitions { get; } = + [ + new("link", nameof(Resources.Command_Link_Description), "code", nameof(Resources.Command_Link_CodeOption)), + new("unlink", nameof(Resources.Command_Unlink_Description)), + new("character", nameof(Resources.Command_Character_Description), "name", nameof(Resources.Command_Character_NameOption)), + ]; + + /// + /// Determines whether the command is one of these commands. + /// + /// The name of the command. + /// true, if the command is one of these commands. + public static bool IsAccountCommand(string commandName) => Definitions.Any(definition => definition.Name == commandName); + + /// + /// Executes a command. + /// + /// The name of the command. + /// The value of the option of the command, if it has one. + /// The identifier of the Discord user who used the command. + /// The name of the Discord user who used the command. + /// The answer. + public async ValueTask ExecuteAsync(string commandName, string? argument, ulong userId, string userName) + { + try + { + return commandName switch + { + "link" => await this.LinkAsync(argument ?? string.Empty, userId, userName).ConfigureAwait(false), + "unlink" => await this.UnlinkAsync(userId).ConfigureAwait(false), + "character" => await this.SelectCharacterAsync(argument ?? string.Empty, userId).ConfigureAwait(false), + _ => this.Answer(nameof(Resources.Command_Failed)), + }; + } + catch (Exception ex) + { + this._logger.LogError(ex, "Error when executing the Discord command {command}.", commandName); + return this.Answer(nameof(Resources.Command_Failed)); + } + } + + private async ValueTask LinkAsync(string code, ulong userId, string userName) + { + var userIdText = userId.ToString(CultureInfo.InvariantCulture); + if (await this._linkService.LinkAsync(Provider, code, userIdText, userName).ConfigureAwait(false) is not { } result) + { + return this.Answer(nameof(Resources.Link_InvalidCode)); + } + + var roleChanges = new List { new(userId, true) }; + if (ulong.TryParse(result.ReplacedExternalUserId, NumberStyles.None, CultureInfo.InvariantCulture, out var replacedUserId)) + { + roleChanges.Add(new(replacedUserId, false)); + } + + return this.Answer(nameof(Resources.Link_Linked), roleChanges, DiscordMessageFormatter.Escape(result.CharacterName ?? "-")); + } + + private async ValueTask UnlinkAsync(ulong userId) + { + var wasLinked = await this._linkService.UnlinkUserAsync(Provider, userId.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); + return wasLinked + ? this.Answer(nameof(Resources.Link_Unlinked), [new(userId, false)]) + : this.Answer(nameof(Resources.Link_NotLinked)); + } + + private async ValueTask SelectCharacterAsync(string characterName, ulong userId) + { + var result = await this._linkService.SelectCharacterAsync(Provider, userId.ToString(CultureInfo.InvariantCulture), characterName.Trim()).ConfigureAwait(false); + return result switch + { + CharacterSelectionResult.Selected => this.Answer(nameof(Resources.Character_Selected), [], DiscordMessageFormatter.Escape(characterName.Trim())), + CharacterSelectionResult.CharacterNotFound => this.Answer(nameof(Resources.Character_NotOfAccount), [], DiscordMessageFormatter.Escape(characterName.Trim())), + _ => this.Answer(nameof(Resources.Link_NotLinked)), + }; + } + + private DiscordCommandAnswer Answer(string resourceKey) => this.Answer(resourceKey, []); + + private DiscordCommandAnswer Answer(string resourceKey, IReadOnlyList roleChanges, params object[] args) + { + var text = DiscordCommands.GetText(this._culture, resourceKey, args); + return new DiscordCommandAnswer( + new DiscordEmbed(DiscordCommands.GetText(this._culture, nameof(Resources.Link_Title)), text, DiscordCommands.AnswerColor, DateTime.UtcNow, null), + roleChanges); + } +} diff --git a/src/Discord/DiscordBot.cs b/src/Discord/DiscordBot.cs index 8c33878d7..5d560c2ad 100644 --- a/src/Discord/DiscordBot.cs +++ b/src/Discord/DiscordBot.cs @@ -5,6 +5,7 @@ namespace MUnique.OpenMU.Discord; using System.ComponentModel; +using System.Globalization; using System.Runtime.CompilerServices; using System.Threading; using global::Discord; @@ -12,6 +13,7 @@ namespace MUnique.OpenMU.Discord; using Microsoft.Extensions.Logging; using MUnique.OpenMU.Discord.Properties; using MUnique.OpenMU.Discord.Provisioning; +using MUnique.OpenMU.GameLogic.AccountLinking; using MUnique.OpenMU.Interfaces; /// @@ -22,7 +24,7 @@ namespace MUnique.OpenMU.Discord; /// It only needs the intent, and no privileged one, /// because it reacts on slash commands instead of reading messages. /// -public sealed class DiscordBot : IManageableServer, IDiscordMessenger, IAsyncDisposable +public sealed class DiscordBot : IManageableServer, IDiscordMessenger, IGameEventListener, IAsyncDisposable { /// /// The name of the command which administrates the integration. @@ -36,6 +38,7 @@ public sealed class DiscordBot : IManageableServer, IDiscordMessenger, IAsyncDis private readonly DiscordBotSettings _settings; private readonly DiscordCommands _commands; + private readonly DiscordAccountCommands _accountCommands; private readonly DiscordStatusFormatter _statusFormatter; private readonly IDiscordGameDataProvider _data; private readonly DiscordServerLayout _layout; @@ -56,15 +59,17 @@ public sealed class DiscordBot : IManageableServer, IDiscordMessenger, IAsyncDis /// /// The settings of the bot. /// The slash commands. + /// The slash commands with which users link themselves to their game account. /// The formatter of the status. /// The provider of the data of the game. /// The layout of the Discord server. /// The provisioner, which sets up the Discord server. /// The logger. - public DiscordBot(DiscordBotSettings settings, DiscordCommands commands, DiscordStatusFormatter statusFormatter, IDiscordGameDataProvider data, DiscordServerLayout layout, DiscordServerProvisioner provisioner, ILogger logger) + public DiscordBot(DiscordBotSettings settings, DiscordCommands commands, DiscordAccountCommands accountCommands, DiscordStatusFormatter statusFormatter, IDiscordGameDataProvider data, DiscordServerLayout layout, DiscordServerProvisioner provisioner, ILogger logger) { this._settings = settings; this._commands = commands; + this._accountCommands = accountCommands; this._statusFormatter = statusFormatter; this._data = data; this._layout = layout; @@ -183,6 +188,19 @@ public async Task SendAsync(ulong channelId, IReadOnlyList embeds, await channel.SendMessageAsync(embeds: embeds.Select(ToDiscordEmbed).ToArray(), allowedMentions: AllowedMentions.None).ConfigureAwait(false); } + /// + /// + /// When an account is unlinked in the game, the role of linked players is removed from its Discord user. + /// + public async ValueTask OnGameEventAsync(GameEvent gameEvent) + { + if (gameEvent is AccountUnlinkedEvent { Provider: AccountLinkService.DiscordProvider } unlinked + && ulong.TryParse(unlinked.ExternalUserId, NumberStyles.None, CultureInfo.InvariantCulture, out var userId)) + { + await this.UpdateLinkedRoleAsync(new DiscordRoleChange(userId, false)).ConfigureAwait(false); + } + } + /// /// /// It may be called more than once, e.g. by a dependency injection container which knows the bot as singleton and as hosted service. @@ -264,7 +282,7 @@ private async Task OnReadyAsync(DiscordSocketClient client) private async Task RegisterCommandsAsync(DiscordSocketClient client) { - var commands = DiscordCommands.Definitions.Select(definition => + var commands = DiscordCommands.Definitions.Concat(DiscordAccountCommands.Definitions).Select(definition => { var builder = new SlashCommandBuilder() .WithName(definition.Name) @@ -309,6 +327,12 @@ private async Task OnSlashCommandExecutedAsync(SocketSlashCommand command) return; } + if (DiscordAccountCommands.IsAccountCommand(command.Data.Name)) + { + await this.ExecuteAccountCommandAsync(command).ConfigureAwait(false); + return; + } + try { // The data might come from the database, which can take longer than Discord waits for an answer. @@ -323,6 +347,63 @@ private async Task OnSlashCommandExecutedAsync(SocketSlashCommand command) } } + private async Task ExecuteAccountCommandAsync(SocketSlashCommand command) + { + try + { + // The answers are personal, e.g. the codes shouldn't be seen by others. + await command.DeferAsync(ephemeral: true).ConfigureAwait(false); + var argument = command.Data.Options.FirstOrDefault()?.Value as string; + var answer = await this._accountCommands.ExecuteAsync(command.Data.Name, argument, command.User.Id, command.User.Username).ConfigureAwait(false); + foreach (var roleChange in answer.RoleChanges) + { + await this.UpdateLinkedRoleAsync(roleChange).ConfigureAwait(false); + } + + await command.FollowupAsync(embed: ToDiscordEmbed(answer.Embed), ephemeral: true, allowedMentions: AllowedMentions.None).ConfigureAwait(false); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Error when answering the Discord command {command}.", command.Data.Name); + } + } + + /// + /// Adds or removes the role of linked players of the layout, in the Discord server of the game server. + /// + private async Task UpdateLinkedRoleAsync(DiscordRoleChange change) + { + if (this._client is not { } client + || this.GetRoutingGuild(client) is not { } guild + || this._layout.Roles.FirstOrDefault(role => role.Key == DiscordServerLayout.LinkedRoleKey) is not { } roleLayout + || guild.Roles.FirstOrDefault(role => string.Equals(role.Name, roleLayout.Name, StringComparison.OrdinalIgnoreCase)) is not { } role) + { + return; + } + + try + { + // The members aren't cached, because the bot doesn't use the privileged intent for them. + if (await client.Rest.GetGuildUserAsync(guild.Id, change.UserId).ConfigureAwait(false) is not { } user) + { + return; + } + + if (change.IsLinked) + { + await user.AddRoleAsync(role.Id).ConfigureAwait(false); + } + else + { + await user.RemoveRoleAsync(role.Id).ConfigureAwait(false); + } + } + catch (Exception ex) + { + this._logger.LogWarning(ex, "The role {role} of the Discord user {userId} couldn't be updated. Has the bot the permission to manage roles?", role.Name, change.UserId); + } + } + private async Task SetUpDiscordServerAsync(SocketSlashCommand command) { try diff --git a/src/Discord/DiscordCommandAnswer.cs b/src/Discord/DiscordCommandAnswer.cs new file mode 100644 index 000000000..d31c62aa1 --- /dev/null +++ b/src/Discord/DiscordCommandAnswer.cs @@ -0,0 +1,12 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Discord; + +/// +/// The answer of a personal command, which is only shown to the user who used it. +/// +/// The answer. +/// The changes of the links, after which the bot updates the roles. +public sealed record DiscordCommandAnswer(DiscordEmbed Embed, IReadOnlyList RoleChanges); diff --git a/src/Discord/DiscordCommands.cs b/src/Discord/DiscordCommands.cs index 925e55d09..66273e4f0 100644 --- a/src/Discord/DiscordCommands.cs +++ b/src/Discord/DiscordCommands.cs @@ -96,17 +96,26 @@ public async ValueTask ExecuteAsync(string commandName, string? ar } /// - /// Gets the text of a resource in the culture of the answers. + /// Gets the text of a resource in a culture. /// + /// The culture. /// The resource key. /// The format arguments. /// The text. - internal string Text(string resourceKey, params object[] args) + internal static string GetText(CultureInfo culture, string resourceKey, params object[] args) { - var text = Resources.ResourceManager.GetString(resourceKey, this._culture) ?? resourceKey; - return args.Length == 0 ? text : string.Format(this._culture, text, args); + var text = Resources.ResourceManager.GetString(resourceKey, culture) ?? resourceKey; + return args.Length == 0 ? text : string.Format(culture, text, args); } + /// + /// Gets the text of a resource in the culture of the answers. + /// + /// The resource key. + /// The format arguments. + /// The text. + internal string Text(string resourceKey, params object[] args) => GetText(this._culture, resourceKey, args); + /// /// Creates the answer to the setup of the Discord server. /// diff --git a/src/Discord/DiscordRoleChange.cs b/src/Discord/DiscordRoleChange.cs new file mode 100644 index 000000000..83199c436 --- /dev/null +++ b/src/Discord/DiscordRoleChange.cs @@ -0,0 +1,12 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Discord; + +/// +/// A change of the link of a Discord user, after which the bot updates the role of linked players. +/// +/// The identifier of the Discord user. +/// A value indicating whether the user is linked now. +public sealed record DiscordRoleChange(ulong UserId, bool IsLinked); diff --git a/src/Discord/DiscordServiceCollectionExtensions.cs b/src/Discord/DiscordServiceCollectionExtensions.cs index b3744e1f7..1c5af3690 100644 --- a/src/Discord/DiscordServiceCollectionExtensions.cs +++ b/src/Discord/DiscordServiceCollectionExtensions.cs @@ -7,7 +7,9 @@ namespace MUnique.OpenMU.Discord; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.Discord.Provisioning; +using MUnique.OpenMU.GameLogic.AccountLinking; using MUnique.OpenMU.Interfaces; using MUnique.OpenMU.Persistence; using MUnique.OpenMU.PlugIns; @@ -43,18 +45,24 @@ public static IServiceCollection AddDiscord(this IServiceCollection services, Di provider.GetRequiredService(), getServerTimeZone)) .AddSingleton(provider => new DiscordCommands(provider.GetRequiredService(), culture, provider.GetRequiredService>())) + .AddSingleton(provider => new DiscordAccountCommands( + new AccountLinkService(() => provider.GetRequiredService().CreateNewPlayerContext(new GameConfiguration())), + culture, + provider.GetRequiredService>())) .AddSingleton(new DiscordStatusFormatter(culture)) .AddSingleton(settings.Bot.LayoutFile is { Length: > 0 } layoutFile ? DiscordServerLayout.Load(layoutFile) : DiscordServerLayout.LoadDefault()) .AddSingleton(provider => new DiscordServerProvisioner(provider.GetRequiredService>())) .AddSingleton(provider => new DiscordBot( settings.Bot, provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService>())) - .AddHostedService(provider => provider.GetRequiredService()); + .AddHostedService(provider => provider.GetRequiredService()) + .AddSingleton(provider => provider.GetRequiredService()); } if (settings.IsNotificationEnabled) diff --git a/src/Discord/Properties/Resources.Designer.cs b/src/Discord/Properties/Resources.Designer.cs index 1d18aea8f..ab1868ab0 100644 --- a/src/Discord/Properties/Resources.Designer.cs +++ b/src/Discord/Properties/Resources.Designer.cs @@ -159,6 +159,15 @@ public static string Character_NotFound { } } + /// + /// Looks up a localized string similar to Your account has no character named {0}.. + /// + public static string Character_NotOfAccount { + get { + return ResourceManager.GetString("Character_NotOfAccount", resourceCulture); + } + } + /// /// Looks up a localized string similar to Offline. /// @@ -186,6 +195,33 @@ public static string Character_Resets { } } + /// + /// Looks up a localized string similar to You appear as **{0}** now.. + /// + public static string Character_Selected { + get { + return ResourceManager.GetString("Character_Selected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Selects the character of your account, as which you appear.. + /// + public static string Command_Character_Description { + get { + return ResourceManager.GetString("Command_Character_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The name of the character.. + /// + public static string Command_Character_NameOption { + get { + return ResourceManager.GetString("Command_Character_NameOption", resourceCulture); + } + } + /// /// Looks up a localized string similar to Shows the events of the next 24 hours.. /// @@ -222,6 +258,24 @@ public static string Command_Guild_GuildOption { } } + /// + /// Looks up a localized string similar to The code which you got in the game with /discord link.. + /// + public static string Command_Link_CodeOption { + get { + return ResourceManager.GetString("Command_Link_CodeOption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Links your Discord user to your game account.. + /// + public static string Command_Link_Description { + get { + return ResourceManager.GetString("Command_Link_Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Shows how many players are online.. /// @@ -258,6 +312,15 @@ public static string Command_Setup_Description { } } + /// + /// Looks up a localized string similar to Removes the link to your game account.. + /// + public static string Command_Unlink_Description { + get { + return ResourceManager.GetString("Command_Unlink_Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to The name of the character.. /// @@ -411,6 +474,51 @@ public static string LevelMilestone_Title { } } + /// + /// Looks up a localized string similar to The code is invalid or expired. Get a new one in the game with /discord link.. + /// + public static string Link_InvalidCode { + get { + return ResourceManager.GetString("Link_InvalidCode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your Discord user is linked to your game account now. You appear as **{0}**; choose another character with /character.. + /// + public static string Link_Linked { + get { + return ResourceManager.GetString("Link_Linked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your Discord user isn't linked to a game account. Get a code in the game with /discord link, and enter it here with /link.. + /// + public static string Link_NotLinked { + get { + return ResourceManager.GetString("Link_NotLinked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Game account. + /// + public static string Link_Title { + get { + return ResourceManager.GetString("Link_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your Discord user isn't linked to a game account anymore.. + /// + public static string Link_Unlinked { + get { + return ResourceManager.GetString("Link_Unlinked", resourceCulture); + } + } + /// /// Looks up a localized string similar to **{0}** ({1}) reached master level {2}!. /// diff --git a/src/Discord/Properties/Resources.de.resx b/src/Discord/Properties/Resources.de.resx index 5d31355cd..30f50ce05 100644 --- a/src/Discord/Properties/Resources.de.resx +++ b/src/Discord/Properties/Resources.de.resx @@ -131,6 +131,9 @@ Es gibt keinen Charakter mit dem Namen **{0}**. + + Dein Konto hat keinen Charakter namens {0}. + Offline @@ -140,6 +143,15 @@ Resets {0} + + Du erscheinst jetzt als **{0}**. + + + Wählt den Charakter deines Kontos, als der du erscheinst. + + + Der Name des Charakters. + Zeigt die Ereignisse der nächsten 24 Stunden. @@ -152,6 +164,12 @@ Der Name der Gilde. + + Der Code, den du im Spiel mit /discord link erhalten hast. + + + Verknüpft deinen Discord-Benutzer mit deinem Spielkonto. + Zeigt, wie viele Spieler online sind. @@ -164,6 +182,9 @@ Erstellt die fehlenden Kanäle und Rollen dieses Discord-Servers. + + Entfernt die Verknüpfung mit deinem Spielkonto. + Der Name des Charakters. @@ -215,6 +236,21 @@ Meilenstein + + Der Code ist ungültig oder abgelaufen. Im Spiel erhältst du mit /discord link einen neuen. + + + Dein Discord-Benutzer ist jetzt mit deinem Spielkonto verknüpft. Du erscheinst als **{0}**; einen anderen Charakter wählst du mit /character. + + + Dein Discord-Benutzer ist nicht mit einem Spielkonto verknüpft. Im Spiel erhältst du mit /discord link einen Code, den du hier mit /link eingibst. + + + Spielkonto + + + Dein Discord-Benutzer ist nicht mehr mit einem Spielkonto verknüpft. + **{0}** ({1}) hat Meisterlevel {2} erreicht! diff --git a/src/Discord/Properties/Resources.resx b/src/Discord/Properties/Resources.resx index 76ccca36a..457419dad 100644 --- a/src/Discord/Properties/Resources.resx +++ b/src/Discord/Properties/Resources.resx @@ -131,6 +131,9 @@ There is no character with the name **{0}**. + + Your account has no character named {0}. + Offline @@ -140,6 +143,15 @@ Resets {0} + + You appear as **{0}** now. + + + Selects the character of your account, as which you appear. + + + The name of the character. + Shows the events of the next 24 hours. @@ -152,6 +164,12 @@ The name of the guild. + + The code which you got in the game with /discord link. + + + Links your Discord user to your game account. + Shows how many players are online. @@ -164,6 +182,9 @@ Creates the missing channels and roles of this Discord server. + + Removes the link to your game account. + The name of the character. @@ -215,6 +236,21 @@ Milestone + + The code is invalid or expired. Get a new one in the game with /discord link. + + + Your Discord user is linked to your game account now. You appear as **{0}**; choose another character with /character. + + + Your Discord user isn't linked to a game account. Get a code in the game with /discord link, and enter it here with /link. + + + Game account + + + Your Discord user isn't linked to a game account anymore. + **{0}** ({1}) reached master level {2}! diff --git a/src/Discord/Provisioning/DiscordServerLayout.cs b/src/Discord/Provisioning/DiscordServerLayout.cs index 4e6e487be..b10c3bdc8 100644 --- a/src/Discord/Provisioning/DiscordServerLayout.cs +++ b/src/Discord/Provisioning/DiscordServerLayout.cs @@ -15,6 +15,11 @@ namespace MUnique.OpenMU.Discord.Provisioning; /// public sealed class DiscordServerLayout { + /// + /// The key of the role which the bot gives to the Discord users who are linked to a game account. + /// + public const string LinkedRoleKey = "linked"; + private const string DefaultLayoutResourceName = "MUnique.OpenMU.Discord.Provisioning.DefaultLayout.json"; private static readonly JsonSerializerOptions SerializerOptions = new() diff --git a/tests/MUnique.OpenMU.Tests/Discord/DiscordAccountCommandsTest.cs b/tests/MUnique.OpenMU.Tests/Discord/DiscordAccountCommandsTest.cs new file mode 100644 index 000000000..be7e86587 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/Discord/DiscordAccountCommandsTest.cs @@ -0,0 +1,98 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests.Discord; + +using System.Globalization; +using Microsoft.Extensions.Logging.Abstractions; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.Discord; +using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.Persistence; +using MUnique.OpenMU.Persistence.InMemory; + +/// +/// Tests for the . +/// +[TestFixture] +public class DiscordAccountCommandsTest +{ + private AccountLinkService _service = null!; + private DiscordAccountCommands _commands = null!; + private Guid _accountId; + + /// + /// Sets up an account. + /// + [SetUp] + public async Task SetUpAsync() + { + var persistence = new InMemoryPersistenceContextProvider(); + this._service = new AccountLinkService(() => persistence.CreateNewPlayerContext(new GameConfiguration())); + this._commands = new DiscordAccountCommands(this._service, CultureInfo.GetCultureInfo("en"), NullLogger.Instance); + using var context = persistence.CreateNewPlayerContext(new GameConfiguration()); + var account = context.CreateNew(); + account.LoginName = "hero"; + await context.SaveChangesAsync().ConfigureAwait(false); + this._accountId = account.GetId(); + } + + /// + /// Tests that linking with a code links the user and gives it the role, and that unlinking removes it again. + /// + [Test] + public async Task LinkAndUnlinkAsync() + { + var code = await this._service.CreateCodeAsync(this._accountId, AccountLinkService.DiscordProvider, "Hero").ConfigureAwait(false); + + var linkAnswer = await this._commands.ExecuteAsync("link", code, 42, "hero").ConfigureAwait(false); + var unlinkAnswer = await this._commands.ExecuteAsync("unlink", null, 42, "hero").ConfigureAwait(false); + + Assert.That(linkAnswer.Embed.Description, Does.StartWith("Your Discord user is linked to your game account now. You appear as **Hero**")); + Assert.That(linkAnswer.RoleChanges, Is.EqualTo(new[] { new DiscordRoleChange(42, true) })); + Assert.That(unlinkAnswer.Embed.Description, Is.EqualTo("Your Discord user isn't linked to a game account anymore.")); + Assert.That(unlinkAnswer.RoleChanges, Is.EqualTo(new[] { new DiscordRoleChange(42, false) })); + } + + /// + /// Tests the answer to an invalid code. + /// + [Test] + public async Task InvalidCodeIsRejectedAsync() + { + var answer = await this._commands.ExecuteAsync("link", "WRONG-CODE", 42, "hero").ConfigureAwait(false); + + Assert.That(answer.Embed.Description, Does.StartWith("The code is invalid or expired.")); + Assert.That(answer.RoleChanges, Is.Empty); + } + + /// + /// Tests that the previous user of the account loses the role, when another user links the account. + /// + [Test] + public async Task PreviousUserLosesRoleAsync() + { + var firstCode = await this._service.CreateCodeAsync(this._accountId, AccountLinkService.DiscordProvider, "Hero").ConfigureAwait(false); + await this._commands.ExecuteAsync("link", firstCode, 42, "hero").ConfigureAwait(false); + var secondCode = await this._service.CreateCodeAsync(this._accountId, AccountLinkService.DiscordProvider, "Hero").ConfigureAwait(false); + + var answer = await this._commands.ExecuteAsync("link", secondCode, 43, "new").ConfigureAwait(false); + + Assert.That(answer.RoleChanges, Is.EqualTo(new[] { new DiscordRoleChange(43, true), new DiscordRoleChange(42, false) })); + } + + /// + /// Tests the answers for a user who isn't linked. + /// + [Test] + public async Task NotLinkedUserAsync() + { + var unlinkAnswer = await this._commands.ExecuteAsync("unlink", null, 42, "hero").ConfigureAwait(false); + var characterAnswer = await this._commands.ExecuteAsync("character", "Hero", 42, "hero").ConfigureAwait(false); + + Assert.That(unlinkAnswer.Embed.Description, Does.StartWith("Your Discord user isn't linked to a game account.")); + Assert.That(characterAnswer.Embed.Description, Is.EqualTo(unlinkAnswer.Embed.Description)); + } +} diff --git a/tests/MUnique.OpenMU.Tests/Discord/DiscordServiceCollectionExtensionsTest.cs b/tests/MUnique.OpenMU.Tests/Discord/DiscordServiceCollectionExtensionsTest.cs index 9a3bedf0d..bae08ba1d 100644 --- a/tests/MUnique.OpenMU.Tests/Discord/DiscordServiceCollectionExtensionsTest.cs +++ b/tests/MUnique.OpenMU.Tests/Discord/DiscordServiceCollectionExtensionsTest.cs @@ -48,7 +48,7 @@ public async Task NotifierIsAddedForWebhooksAsync() } /// - /// Tests that the bot is added as hosted service, and that the notifier posts through it. + /// Tests that the bot is added as hosted service and as listener of the game events, and that the notifier is added, too. /// [Test] public async Task BotIsAddedWithTokenAsync() @@ -60,7 +60,10 @@ public async Task BotIsAddedWithTokenAsync() var bot = provider.GetRequiredService(); Assert.That(provider.GetServices(), Does.Contain(bot)); - Assert.That(provider.GetServices().Single(), Is.InstanceOf()); + var listeners = provider.GetServices().ToList(); + Assert.That(listeners, Has.Count.EqualTo(2)); + Assert.That(listeners, Has.One.InstanceOf()); + Assert.That(listeners, Does.Contain(bot)); Assert.That(bot.ServerState, Is.EqualTo(ServerState.Stopped)); } From 0ccc18d070aa81939f48b957aa2e766b0ffa0249 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 07:47:56 +0000 Subject: [PATCH 4/6] feat(admin-panel): show and remove the links of an account Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ --- src/Web/AdminPanel/Pages/AccountLinks.razor | 65 +++++++++++ .../AdminPanel/Pages/AccountLinks.razor.cs | 102 ++++++++++++++++++ src/Web/AdminPanel/Pages/Accounts.razor | 5 +- .../Properties/Resources.Designer.cs | 99 +++++++++++++++++ .../AdminPanel/Properties/Resources.de.resx | 33 ++++++ src/Web/AdminPanel/Properties/Resources.resx | 33 ++++++ 6 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 src/Web/AdminPanel/Pages/AccountLinks.razor create mode 100644 src/Web/AdminPanel/Pages/AccountLinks.razor.cs diff --git a/src/Web/AdminPanel/Pages/AccountLinks.razor b/src/Web/AdminPanel/Pages/AccountLinks.razor new file mode 100644 index 000000000..408a4d8f0 --- /dev/null +++ b/src/Web/AdminPanel/Pages/AccountLinks.razor @@ -0,0 +1,65 @@ +@page "/accounts/{LoginName}/links" +@attribute [Authorize(Policy = AdminPolicies.Operator)] + +@using MUnique.OpenMU.Web.AdminPanel.Properties + +@{ + var title = string.Format(Resources.ExternalLinksOf, this.LoginName); +} +OpenMU: @title + +

@title

+ +

@Resources.ExternalLinksDescription

+ +@if (this._isLoading) +{ +

@Resources.Loading

+} +else if (this._links is not { } links) +{ +

@Resources.ExternalLinksAccountNotFound

+} +else +{ +
+ + + + + + + + + + + + @foreach (var link in links) + { + + + + + + + + } + @if (links.Count == 0) + { + + + + } + +
@Resources.ExternalLinkService@Resources.ExternalLinkUser@Resources.ExternalLinkCharacter@Resources.ExternalLinkLinkedAt
@link.Provider + @if (link.ExternalUserId is { } userId) + { + @link.ExternalUserName @userId + } + else if (link.CodeExpiresAt is { } expiresAt) + { + @string.Format(Resources.ExternalLinkPending, expiresAt.ToLocalTime()) + } + @link.CharacterName@link.LinkedAt?.ToLocalTime()
@Resources.ExternalLinksNone
+
+} diff --git a/src/Web/AdminPanel/Pages/AccountLinks.razor.cs b/src/Web/AdminPanel/Pages/AccountLinks.razor.cs new file mode 100644 index 000000000..5fe144d13 --- /dev/null +++ b/src/Web/AdminPanel/Pages/AccountLinks.razor.cs @@ -0,0 +1,102 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Web.AdminPanel.Pages; + +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Logging; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.Persistence; +using MUnique.OpenMU.Web.AdminPanel.Properties; +using MUnique.OpenMU.Web.Shared.Components.Toast; + +/// +/// The page which shows and removes the links of an account to users of external services, e.g. Discord. +/// +public partial class AccountLinks : ComponentBase +{ + private IReadOnlyList? _links; + private Guid _accountId; + private GameConfiguration? _gameConfiguration; + private bool _isLoading = true; + private bool _isRemoving; + + /// + /// Gets or sets the login name of the account. + /// + [Parameter] + public string LoginName { get; set; } = string.Empty; + + [Inject] + private IPersistenceContextProvider PersistenceContextProvider { get; set; } = null!; + + [Inject] + private IDataSource GameConfigurationSource { get; set; } = null!; + + [Inject] + private IToastService ToastService { get; set; } = null!; + + [Inject] + private ILogger Logger { get; set; } = null!; + + /// + protected override async Task OnParametersSetAsync() + { + await this.LoadAsync().ConfigureAwait(true); + await base.OnParametersSetAsync().ConfigureAwait(true); + } + + private IPlayerContext CreateContext() => this.PersistenceContextProvider.CreateNewPlayerContext(this._gameConfiguration!); + + private async Task LoadAsync() + { + this._isLoading = true; + try + { + this._gameConfiguration ??= await this.GameConfigurationSource.GetOwnerAsync(Guid.Empty).ConfigureAwait(true); + using var context = this.CreateContext(); + if (await context.GetAccountByLoginNameAsync(this.LoginName).ConfigureAwait(true) is { } account) + { + this._accountId = account.GetId(); + this._links = await context.GetAccountExternalLinksAsync(this._accountId).ConfigureAwait(true); + } + else + { + this._links = null; + } + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Error loading the links of account {LoginName}.", this.LoginName); + this.ToastService.ShowError(string.Format(Resources.UnexpectedErrorOccurred, ex.Message)); + } + finally + { + this._isLoading = false; + } + } + + private async Task OnRemoveAsync(string provider) + { + this._isRemoving = true; + try + { + await new AccountLinkService(this.CreateContext).UnlinkAccountAsync(this._accountId, provider).ConfigureAwait(true); + this.Logger.LogInformation("The {Provider} link of account {LoginName} was removed.", provider, this.LoginName); + this.ToastService.ShowSuccess(Resources.ExternalLinkRemoved); + await this.LoadAsync().ConfigureAwait(true); + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Error removing the {Provider} link of account {LoginName}.", provider, this.LoginName); + this.ToastService.ShowError(string.Format(Resources.UnexpectedErrorOccurred, ex.Message)); + } + finally + { + this._isRemoving = false; + } + } +} diff --git a/src/Web/AdminPanel/Pages/Accounts.razor b/src/Web/AdminPanel/Pages/Accounts.razor index 4e42a934b..f65790e73 100644 --- a/src/Web/AdminPanel/Pages/Accounts.razor +++ b/src/Web/AdminPanel/Pages/Accounts.razor @@ -1,4 +1,4 @@ -@page "/accounts" +@page "/accounts" @using MUnique.OpenMU.DataModel @using MUnique.OpenMU.DataModel.Entities; @using MUnique.OpenMU.Persistence; @@ -35,6 +35,9 @@ @item.EMail @Resources.Edit + + @Resources.ExternalLinks + @Resources.CashShopCoins diff --git a/src/Web/AdminPanel/Properties/Resources.Designer.cs b/src/Web/AdminPanel/Properties/Resources.Designer.cs index 66028b420..5fa37cba5 100644 --- a/src/Web/AdminPanel/Properties/Resources.Designer.cs +++ b/src/Web/AdminPanel/Properties/Resources.Designer.cs @@ -1656,6 +1656,105 @@ public static string ExtensionPoint { } } + /// + /// Looks up a localized string similar to Appears as. + /// + public static string ExternalLinkCharacter { + get { + return ResourceManager.GetString("ExternalLinkCharacter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Linked at. + /// + public static string ExternalLinkLinkedAt { + get { + return ResourceManager.GetString("ExternalLinkLinkedAt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code valid until {0}. + /// + public static string ExternalLinkPending { + get { + return ResourceManager.GetString("ExternalLinkPending", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The link was removed.. + /// + public static string ExternalLinkRemoved { + get { + return ResourceManager.GetString("ExternalLinkRemoved", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Links. + /// + public static string ExternalLinks { + get { + return ResourceManager.GetString("ExternalLinks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The account wasn't found.. + /// + public static string ExternalLinksAccountNotFound { + get { + return ResourceManager.GetString("ExternalLinksAccountNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The users of external services, like Discord, which are linked to the account. Players link their account in the game with /discord link. When you remove a Discord link here, remove the role of linked players in Discord yourself.. + /// + public static string ExternalLinksDescription { + get { + return ResourceManager.GetString("ExternalLinksDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Service. + /// + public static string ExternalLinkService { + get { + return ResourceManager.GetString("ExternalLinkService", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The account isn't linked.. + /// + public static string ExternalLinksNone { + get { + return ResourceManager.GetString("ExternalLinksNone", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Links of {0}. + /// + public static string ExternalLinksOf { + get { + return ResourceManager.GetString("ExternalLinksOf", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User. + /// + public static string ExternalLinkUser { + get { + return ResourceManager.GetString("ExternalLinkUser", resourceCulture); + } + } + /// /// Looks up a localized string similar to Extracted Information. /// diff --git a/src/Web/AdminPanel/Properties/Resources.de.resx b/src/Web/AdminPanel/Properties/Resources.de.resx index b81247b47..192ce59bb 100644 --- a/src/Web/AdminPanel/Properties/Resources.de.resx +++ b/src/Web/AdminPanel/Properties/Resources.de.resx @@ -1689,4 +1689,37 @@ Es wird geprüft, ob integrierte Beschriftungen noch nicht verknüpft sind ... + + Verknüpfungen + + + Verknüpfungen von {0} + + + Die Benutzer externer Dienste, wie Discord, die mit dem Konto verknüpft sind. Spieler verknüpfen ihr Konto im Spiel mit /discord link. Wenn du hier eine Discord-Verknüpfung entfernst, entferne die Rolle für verknüpfte Spieler in Discord selbst. + + + Das Konto wurde nicht gefunden. + + + Das Konto ist nicht verknüpft. + + + Dienst + + + Benutzer + + + Erscheint als + + + Verknüpft am + + + Code gültig bis {0} + + + Die Verknüpfung wurde entfernt. + diff --git a/src/Web/AdminPanel/Properties/Resources.resx b/src/Web/AdminPanel/Properties/Resources.resx index 95528b36a..7962617ce 100644 --- a/src/Web/AdminPanel/Properties/Resources.resx +++ b/src/Web/AdminPanel/Properties/Resources.resx @@ -1954,4 +1954,37 @@ Product {0} has a quantity below 1. + + Links + + + Links of {0} + + + The users of external services, like Discord, which are linked to the account. Players link their account in the game with /discord link. When you remove a Discord link here, remove the role of linked players in Discord yourself. + + + The account wasn't found. + + + The account isn't linked. + + + Service + + + User + + + Appears as + + + Linked at + + + Code valid until {0} + + + The link was removed. + From 9cffa9a05983cd6f828867c254422a4ee09aba4c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 07:47:56 +0000 Subject: [PATCH 5/6] docs(discord): document the linking of accounts Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ --- docs-website/docs/server-features/discord.md | 38 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/docs-website/docs/server-features/discord.md b/docs-website/docs/server-features/discord.md index 400bdfbd4..7e0fa4a8e 100644 --- a/docs-website/docs/server-features/discord.md +++ b/docs-website/docs/server-features/discord.md @@ -98,10 +98,44 @@ In the distributed deployment, the bot runs in its own container, see | `/guild ` | The guild master, the number of members and the members which are online | | `/events` | The events of the next 24 hours, like mini games and invasions | | `/rank` | The ten best characters by resets, master level and level | +| `/link ` | Links the Discord user to the game account, see [Linking accounts](#linking-accounts) | +| `/unlink` | Removes the link to the game account | +| `/character ` | Selects the character of the account, as which the player appears | The commands only show information; they don't change anything in the game. -Additionally, administrators of the Discord server can use -[`/openmu setup`](#setting-up-your-discord-server). +Additionally, players can [link their account](#linking-accounts) with +`/link`, `/unlink` and `/character`, and administrators of the Discord server +can use [`/openmu setup`](#setting-up-your-discord-server). + +### Linking accounts + +Players can link their Discord user to their game account: + +1. In the game, the player enters `/discord link`. The game shows a one-time + code like `ABCD-EFGH`, which is valid for 10 minutes. +2. In Discord, the player enters `/link ABCD-EFGH`. + +The link is to the account, not to a character. The player appears as the +character which requested the code, and chooses another character of the account +with `/character ` in Discord. A Discord user can be linked to one account, +and an account to one Discord user; linking again replaces the previous link. +Because Discord users are the same on every Discord server, the link counts on +every Discord server the bot is in. + +`/discord` in the game shows the link, and `/discord unlink` in the game or +`/unlink` in Discord removes it. In the admin panel, the *Links* button in the +account list shows the links of an account and can remove them. + +Linked users get the role *Linked Player* of the +[layout](#setting-up-your-discord-server) on your Discord server, so you can, +for example, open channels only for players. The bot needs the *Manage Roles* +permission for it, and its own role has to be above *Linked Player* in the role +list. When an administrator removes a link in the admin panel, the role isn't +removed automatically. + +The answers to these commands are only visible to the player who used them. +The codes are stored as hash in the database, and every code can only be used +once. When you configure the identifier of your Discord server (`Discord__Bot__GuildId`), the commands are available immediately. Otherwise, Discord may take up to an hour From d486b121f4137d51492da59a0d8076557b02b749 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 10:27:32 +0000 Subject: [PATCH 6/6] fix(discord): remove the roles when an administrator unlinks an account AccountLinkService.UnlinkAccountAsync publishes the AccountUnlinkedEvent, so the admin panel and the chat command both make the bot remove the roles. The all-in-one deployment registers its event publisher for it. /discord unlink tells when there was no link, and a failed account command gets an answer. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01USFZ9xErMB1EwkpfdUHzoZ --- docs-website/docs/server-features/discord.md | 4 +-- src/Discord/DiscordBot.cs | 1 + .../AccountLinking/AccountLinkService.cs | 28 ++++++++++++++----- .../ChatCommands/DiscordChatCommandPlugIn.cs | 12 ++------ src/Interfaces/AccountUnlinkedEvent.cs | 4 +-- src/Startup/GameServerContainer.cs | 5 ++++ src/Startup/Program.cs | 1 + .../AdminPanel/Pages/AccountLinks.razor.cs | 9 +++++- .../Properties/Resources.Designer.cs | 2 +- .../AdminPanel/Properties/Resources.de.resx | 2 +- src/Web/AdminPanel/Properties/Resources.resx | 2 +- .../AccountLinkServiceTest.cs | 17 +++++++++++ 12 files changed, 63 insertions(+), 24 deletions(-) diff --git a/docs-website/docs/server-features/discord.md b/docs-website/docs/server-features/discord.md index 7e0fa4a8e..006d3b903 100644 --- a/docs-website/docs/server-features/discord.md +++ b/docs-website/docs/server-features/discord.md @@ -130,8 +130,8 @@ Linked users get the role *Linked Player* of the [layout](#setting-up-your-discord-server) on your Discord server, so you can, for example, open channels only for players. The bot needs the *Manage Roles* permission for it, and its own role has to be above *Linked Player* in the role -list. When an administrator removes a link in the admin panel, the role isn't -removed automatically. +list. When a link is removed, also in the admin panel of the all-in-one +deployment, the role is removed again. The answers to these commands are only visible to the player who used them. The codes are stored as hash in the database, and every code can only be used diff --git a/src/Discord/DiscordBot.cs b/src/Discord/DiscordBot.cs index 12d6a54df..0decaefcc 100644 --- a/src/Discord/DiscordBot.cs +++ b/src/Discord/DiscordBot.cs @@ -376,6 +376,7 @@ private async Task ExecuteAccountCommandAsync(SocketSlashCommand command) catch (Exception ex) { this._logger.LogError(ex, "Error when answering the Discord command {command}.", command.Data.Name); + await this.AnswerFailureAsync(command, true).ConfigureAwait(false); } } diff --git a/src/GameLogic/AccountLinking/AccountLinkService.cs b/src/GameLogic/AccountLinking/AccountLinkService.cs index 94466ec35..f0de66d82 100644 --- a/src/GameLogic/AccountLinking/AccountLinkService.cs +++ b/src/GameLogic/AccountLinking/AccountLinkService.cs @@ -7,6 +7,7 @@ namespace MUnique.OpenMU.GameLogic.AccountLinking; using System.Security.Cryptography; using System.Text; using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.Interfaces; using MUnique.OpenMU.Persistence; /// @@ -151,21 +152,34 @@ public async ValueTask CreateCodeAsync(Guid accountId, string provider, /// /// Removes the link of an account, including a pending code. + /// The is published, so that e.g. the Discord bot removes the roles of the user. /// /// The identifier of the account. /// The name of the external service. + /// The publisher of the , if one is available. + /// The identifier of the game server which removes the link, or 0. /// The identifier of the user which was linked; or null, if the account wasn't linked. - public async ValueTask UnlinkAccountAsync(Guid accountId, string provider) + public async ValueTask UnlinkAccountAsync(Guid accountId, string provider, IEventPublisher? eventPublisher = null, byte serverId = 0) { - using var context = this._createContext(); - if (await context.GetAccountExternalLinkAsync(accountId, provider).ConfigureAwait(false) is not { } link) + string? userId; + using (var context = this._createContext()) { - return null; + if (await context.GetAccountExternalLinkAsync(accountId, provider).ConfigureAwait(false) is not { } link) + { + return null; + } + + await context.DeleteAsync(link).ConfigureAwait(false); + await context.SaveChangesAsync().ConfigureAwait(false); + userId = link.ExternalUserId; } - await context.DeleteAsync(link).ConfigureAwait(false); - await context.SaveChangesAsync().ConfigureAwait(false); - return link.ExternalUserId; + if (userId is not null && eventPublisher is not null) + { + await eventPublisher.GameEventAsync(new AccountUnlinkedEvent(serverId, this._timeProvider.GetUtcNow().UtcDateTime, provider, userId)).ConfigureAwait(false); + } + + return userId; } /// diff --git a/src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs b/src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs index 0623d4630..a56e57040 100644 --- a/src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs +++ b/src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs @@ -47,15 +47,9 @@ protected override async ValueTask DoHandleCommandAsync(Player player, DiscordCh await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.DiscordLinkCode), code, (int)AccountLinkService.CodeValidity.TotalMinutes).ConfigureAwait(false); break; case DiscordChatCommandArgs.UnlinkAction: - if (await service.UnlinkAccountAsync(accountId, AccountLinkService.DiscordProvider).ConfigureAwait(false) is { } userId - && player.GameContext is IGameServerContext gameServerContext) - { - // The Discord bot removes the roles of the user. - await gameServerContext.EventPublisher.GameEventAsync( - new AccountUnlinkedEvent(gameServerContext.Id, DateTime.UtcNow, AccountLinkService.DiscordProvider, userId)).ConfigureAwait(false); - } - - await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.DiscordUnlinked)).ConfigureAwait(false); + var gameServerContext = player.GameContext as IGameServerContext; + var unlinkedUserId = await service.UnlinkAccountAsync(accountId, AccountLinkService.DiscordProvider, gameServerContext?.EventPublisher, gameServerContext?.Id ?? 0).ConfigureAwait(false); + await player.ShowLocalizedBlueMessageAsync(unlinkedUserId is null ? nameof(PlayerMessage.DiscordNotLinked) : nameof(PlayerMessage.DiscordUnlinked)).ConfigureAwait(false); break; default: var link = await service.GetLinkAsync(accountId, AccountLinkService.DiscordProvider).ConfigureAwait(false); diff --git a/src/Interfaces/AccountUnlinkedEvent.cs b/src/Interfaces/AccountUnlinkedEvent.cs index 3d574bf87..724de6bfa 100644 --- a/src/Interfaces/AccountUnlinkedEvent.cs +++ b/src/Interfaces/AccountUnlinkedEvent.cs @@ -5,9 +5,9 @@ namespace MUnique.OpenMU.Interfaces; /// -/// The link of an account to a user of an external service, e.g. Discord, was removed in the game. +/// The link of an account to a user of an external service, e.g. Discord, was removed in the game or in the admin panel. /// -/// The identifier of the game server. +/// The identifier of the game server; or 0, if the link was removed in the admin panel. /// The timestamp, in UTC. /// The name of the external service, e.g. discord. /// The identifier of the user in the external service. diff --git a/src/Startup/GameServerContainer.cs b/src/Startup/GameServerContainer.cs index 597b2a321..3fa06c824 100644 --- a/src/Startup/GameServerContainer.cs +++ b/src/Startup/GameServerContainer.cs @@ -92,6 +92,11 @@ public GameServerContainer( this._packetArchive = packetArchive; } + /// + /// Gets the publisher of the events between the servers of this application. + /// + public IEventPublisher EventPublisher => this._eventPublisher; + /// public void Dispose() { diff --git a/src/Startup/Program.cs b/src/Startup/Program.cs index 181c2ef74..1a4c47f7e 100644 --- a/src/Startup/Program.cs +++ b/src/Startup/Program.cs @@ -353,6 +353,7 @@ private async Task CreateHostAsync(string[] args) .AddSingleton(provider => provider.GetService()!) .AddSingleton() .AddSingleton(provider => provider.GetService()!) + .AddSingleton(provider => provider.GetRequiredService().EventPublisher) .AddScoped() .AddCircuitConnectionState() .AddSingleton() diff --git a/src/Web/AdminPanel/Pages/AccountLinks.razor.cs b/src/Web/AdminPanel/Pages/AccountLinks.razor.cs index 5fe144d13..564b68d28 100644 --- a/src/Web/AdminPanel/Pages/AccountLinks.razor.cs +++ b/src/Web/AdminPanel/Pages/AccountLinks.razor.cs @@ -5,10 +5,12 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages; using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.Interfaces; using MUnique.OpenMU.Persistence; using MUnique.OpenMU.Web.AdminPanel.Properties; using MUnique.OpenMU.Web.Shared.Components.Toast; @@ -39,6 +41,9 @@ public partial class AccountLinks : ComponentBase [Inject] private IToastService ToastService { get; set; } = null!; + [Inject] + private IServiceProvider ServiceProvider { get; set; } = null!; + [Inject] private ILogger Logger { get; set; } = null!; @@ -84,7 +89,9 @@ private async Task OnRemoveAsync(string provider) this._isRemoving = true; try { - await new AccountLinkService(this.CreateContext).UnlinkAccountAsync(this._accountId, provider).ConfigureAwait(true); + // With the event, the Discord bot removes the roles of the user. + var eventPublisher = this.ServiceProvider.GetService(); + await new AccountLinkService(this.CreateContext).UnlinkAccountAsync(this._accountId, provider, eventPublisher).ConfigureAwait(true); this.Logger.LogInformation("The {Provider} link of account {LoginName} was removed.", provider, this.LoginName); this.ToastService.ShowSuccess(Resources.ExternalLinkRemoved); await this.LoadAsync().ConfigureAwait(true); diff --git a/src/Web/AdminPanel/Properties/Resources.Designer.cs b/src/Web/AdminPanel/Properties/Resources.Designer.cs index e58cd8afa..e466fefe4 100644 --- a/src/Web/AdminPanel/Properties/Resources.Designer.cs +++ b/src/Web/AdminPanel/Properties/Resources.Designer.cs @@ -1738,7 +1738,7 @@ public static string ExternalLinksAccountNotFound { } /// - /// Looks up a localized string similar to The users of external services, like Discord, which are linked to the account. Players link their account in the game with /discord link. When you remove a Discord link here, remove the role of linked players in Discord yourself.. + /// Looks up a localized string similar to The users of external services, like Discord, which are linked to the account. Players link their account in the game with /discord link.. /// public static string ExternalLinksDescription { get { diff --git a/src/Web/AdminPanel/Properties/Resources.de.resx b/src/Web/AdminPanel/Properties/Resources.de.resx index e88dfcc34..39dc6abe6 100644 --- a/src/Web/AdminPanel/Properties/Resources.de.resx +++ b/src/Web/AdminPanel/Properties/Resources.de.resx @@ -1930,7 +1930,7 @@ Verknüpfungen von {0} - Die Benutzer externer Dienste, wie Discord, die mit dem Konto verknüpft sind. Spieler verknüpfen ihr Konto im Spiel mit /discord link. Wenn du hier eine Discord-Verknüpfung entfernst, entferne die Rolle für verknüpfte Spieler in Discord selbst. + Die Benutzer externer Dienste, wie Discord, die mit dem Konto verknüpft sind. Spieler verknüpfen ihr Konto im Spiel mit /discord link. Das Konto wurde nicht gefunden. diff --git a/src/Web/AdminPanel/Properties/Resources.resx b/src/Web/AdminPanel/Properties/Resources.resx index b9eedd44c..96fed0760 100644 --- a/src/Web/AdminPanel/Properties/Resources.resx +++ b/src/Web/AdminPanel/Properties/Resources.resx @@ -2021,7 +2021,7 @@ Links of {0} - The users of external services, like Discord, which are linked to the account. Players link their account in the game with /discord link. When you remove a Discord link here, remove the role of linked players in Discord yourself. + The users of external services, like Discord, which are linked to the account. Players link their account in the game with /discord link. The account wasn't found. diff --git a/tests/MUnique.OpenMU.Tests/AccountLinkServiceTest.cs b/tests/MUnique.OpenMU.Tests/AccountLinkServiceTest.cs index 2a09898f4..d13eac7ac 100644 --- a/tests/MUnique.OpenMU.Tests/AccountLinkServiceTest.cs +++ b/tests/MUnique.OpenMU.Tests/AccountLinkServiceTest.cs @@ -4,9 +4,11 @@ namespace MUnique.OpenMU.Tests; +using Moq; using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.Interfaces; using MUnique.OpenMU.Persistence; using MUnique.OpenMU.Persistence.InMemory; @@ -123,6 +125,21 @@ public async Task UnlinkingWorksFromBothSidesAsync() Assert.That(await this._service.UnlinkUserAsync(Provider, "43").ConfigureAwait(false), Is.False); } + /// + /// Tests that unlinking an account publishes the event, so that the Discord bot removes the roles of the user. + /// + [Test] + public async Task UnlinkingAccountPublishesEventAsync() + { + await this.LinkAsync(this._accountId, "42").ConfigureAwait(false); + var publisher = new Mock(); + + await this._service.UnlinkAccountAsync(this._accountId, Provider, publisher.Object, 3).ConfigureAwait(false); + await this._service.UnlinkAccountAsync(this._accountId, Provider, publisher.Object, 3).ConfigureAwait(false); + + publisher.Verify(p => p.GameEventAsync(It.Is(e => e.ServerId == 3 && e.Provider == Provider && e.ExternalUserId == "42")), Times.Once); + } + /// /// Tests that only a character of the linked account can be selected. ///