diff --git a/docs-website/docs/server-features/discord.md b/docs-website/docs/server-features/discord.md index 400bdfbd4..006d3b903 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 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 +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 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/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 417c86916..0decaefcc 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; @@ -188,6 +193,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. @@ -269,7 +287,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) @@ -319,6 +337,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. @@ -334,6 +358,64 @@ 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); + await this.AnswerFailureAsync(command, true).ConfigureAwait(false); + } + } + + /// + /// 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 ee1bd0ae4..947163946 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 335c15eab..7e3313e72 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 17b034028..321c9aee9 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 25f814b82..d823be71d 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 c9fe740d0..fc5dbd357 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 ac0ae3af5..f13a316d3 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/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..f0de66d82 --- /dev/null +++ b/src/GameLogic/AccountLinking/AccountLinkService.cs @@ -0,0 +1,244 @@ +// +// 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.Interfaces; +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 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, IEventPublisher? eventPublisher = null, byte serverId = 0) + { + string? userId; + 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); + userId = 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; + } + + /// + /// 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..a56e57040 --- /dev/null +++ b/src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs @@ -0,0 +1,68 @@ +// +// 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: + 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); + 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 84e124234..f726c74ab 100644 --- a/src/GameLogic/Properties/PlayerMessage.Designer.cs +++ b/src/GameLogic/Properties/PlayerMessage.Designer.cs @@ -562,6 +562,42 @@ public static string CrywolfStatueAttack { } } + /// + /// 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 acb5f53f8..d076801b9 100644 --- a/src/GameLogic/Properties/PlayerMessage.de.resx +++ b/src/GameLogic/Properties/PlayerMessage.de.resx @@ -988,4 +988,16 @@ Inventarerweiterungen: {18} Du musst Level {0} sein, um diese Box zu öffnen. + + 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 7a593a6ee..84fee6745 100644 --- a/src/GameLogic/Properties/PlayerMessage.resx +++ b/src/GameLogic/Properties/PlayerMessage.resx @@ -1099,4 +1099,16 @@ Inventory Extensions: {18} You need to be level {0} to open this box. + + 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 5e5ae54d3..1a050ec62 100644 --- a/src/GameLogic/Properties/PlugInResources.Designer.cs +++ b/src/GameLogic/Properties/PlugInResources.Designer.cs @@ -681,6 +681,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 a3ac7e186..26044dacf 100644 --- a/src/GameLogic/Properties/PlugInResources.de.resx +++ b/src/GameLogic/Properties/PlugInResources.de.resx @@ -1875,4 +1875,10 @@ Wie die Ankündigung gezeigt wird, z. B. als goldene Nachricht in der Bildschirmmitte oder als blaue Systemnachricht. + + 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 fd2a006a9..86cc70f43 100644 --- a/src/GameLogic/Properties/PlugInResources.resx +++ b/src/GameLogic/Properties/PlugInResources.resx @@ -1682,4 +1682,10 @@ How the announcement is shown, e.g. as golden message in the center of the screen or as blue system message. + + 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..724de6bfa --- /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 or in the admin panel. +/// +/// 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. +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/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 bba10c66c..7c92bfb74 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("750f48d1-2329-4b96-b749-a5acd51a1bc1"), entityTypeCount: 46) + : base(skipDetectChanges: false, modelId: new Guid("e53209e9-b4b9-474d-9c80-88038379d7dc"), entityTypeCount: 47) { } @@ -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); @@ -67,6 +68,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); @@ -97,6 +99,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 76609aa43..4c907747b 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("edc7bef9-dc99-4de0-a4d1-866509c3e38e"), entityTypeCount: 121) + : base(skipDetectChanges: false, modelId: new Guid("b9d48a9a-c807-40a2-9b41-a5bfa0fe2167"), entityTypeCount: 122) { } @@ -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); @@ -143,6 +144,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); @@ -382,6 +384,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 a26e5346c..abe608af6 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("f25c8777-f9a6-4963-84ed-22c901158903"), entityTypeCount: 45) + : base(skipDetectChanges: false, modelId: new Guid("474248ae-e21e-4a0b-88b4-317ac8d5ca08"), 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); @@ -65,6 +66,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); @@ -93,6 +95,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 7d2e2c599..7690b7cbc 100644 --- a/src/Persistence/EntityFramework/EntityDataContext.cs +++ b/src/Persistence/EntityFramework/EntityDataContext.cs @@ -75,6 +75,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. /// @@ -131,6 +136,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/20261018101832_AddAccountExternalLink.Designer.cs b/src/Persistence/EntityFramework/Migrations/20261018101832_AddAccountExternalLink.Designer.cs new file mode 100644 index 000000000..26c22e14b --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20261018101832_AddAccountExternalLink.Designer.cs @@ -0,0 +1,6430 @@ +// +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("20261018101832_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("MinimumItemLevel") + .HasColumnType("smallint"); + + 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.CrywolfData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsOccupied") + .HasColumnType("boolean"); + + b.Property("IsWarRunning") + .HasColumnType("boolean"); + + b.Property("LastBattleEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("WarStart") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("CrywolfData", "data"); + }); + + 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.GameMapTerrainVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GameMapDefinitionId") + .HasColumnType("uuid"); + + b.Property("Number") + .HasColumnType("smallint"); + + b.Property("TerrainData") + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("GameMapDefinitionId"); + + b.ToTable("GameMapTerrainVariant", "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.Property("RewardClaimedAt") + .HasColumnType("timestamp with time zone"); + + 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.Property("StorePriceCoinType") + .HasColumnType("integer"); + + b.Property("StorePriceItemDefinitionId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DefinitionId"); + + b.HasIndex("ItemStorageId"); + + b.HasIndex("StorePriceItemDefinitionId"); + + 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.GameMapTerrainVariant", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.GameMapDefinition", null) + .WithMany("RawTerrainVariants") + .HasForeignKey("GameMapDefinitionId") + .OnDelete(DeleteBehavior.Cascade); + }); + + 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.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.ItemDefinition", "RawStorePriceItemDefinition") + .WithMany() + .HasForeignKey("StorePriceItemDefinitionId"); + + b.Navigation("RawDefinition"); + + b.Navigation("RawItemStorage"); + + b.Navigation("RawStorePriceItemDefinition"); + }); + + 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"); + + b.Navigation("RawTerrainVariants"); + }); + + 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/20261018101832_AddAccountExternalLink.cs b/src/Persistence/EntityFramework/Migrations/20261018101832_AddAccountExternalLink.cs new file mode 100644 index 000000000..57f4432cd --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20261018101832_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 3d105bec0..80d4fe097 100644 --- a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs +++ b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs @@ -10,7 +10,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations { - [DbContext(typeof(MUnique.OpenMU.Persistence.EntityFramework.EntityDataContext))] + [DbContext(typeof(EntityDataContext))] partial class EntityDataContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) @@ -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") @@ -4288,6 +4332,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 29f055608..483bbb648 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), @@ -207,6 +208,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 5edf98131..85b7b5ed1 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, 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 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..564b68d28 --- /dev/null +++ b/src/Web/AdminPanel/Pages/AccountLinks.razor.cs @@ -0,0 +1,109 @@ +// +// 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.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; + +/// +/// 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 IServiceProvider ServiceProvider { 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 + { + // 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); + } + 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 54b17c530..e466fefe4 100644 --- a/src/Web/AdminPanel/Properties/Resources.Designer.cs +++ b/src/Web/AdminPanel/Properties/Resources.Designer.cs @@ -1683,6 +1683,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.. + /// + 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 1a4747c13..39dc6abe6 100644 --- a/src/Web/AdminPanel/Properties/Resources.de.resx +++ b/src/Web/AdminPanel/Properties/Resources.de.resx @@ -1923,4 +1923,37 @@ Jede Quelle benötigt einen Namen, und jeder Name darf nur einmal verwendet werden. + + 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. + + + 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 19928b606..96fed0760 100644 --- a/src/Web/AdminPanel/Properties/Resources.resx +++ b/src/Web/AdminPanel/Properties/Resources.resx @@ -2014,4 +2014,37 @@ Every source needs a name, and each name may only be used once. + + 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. + + + The account wasn't found. + + + The account isn't linked. + + + Service + + + User + + + Appears as + + + Linked at + + + Code valid until {0} + + + The link was removed. + diff --git a/tests/MUnique.OpenMU.Tests/AccountLinkServiceTest.cs b/tests/MUnique.OpenMU.Tests/AccountLinkServiceTest.cs new file mode 100644 index 000000000..d13eac7ac --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/AccountLinkServiceTest.cs @@ -0,0 +1,192 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using Moq; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.Interfaces; +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 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. + /// + [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/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)); } 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); + } +}