diff --git a/docs-website/docs/server-features/discord.md b/docs-website/docs/server-features/discord.md index 5aa4c08d21..71125e96a3 100644 --- a/docs-website/docs/server-features/discord.md +++ b/docs-website/docs/server-features/discord.md @@ -25,6 +25,7 @@ combined: | `CastleSiege` | Guild registration, the start and the end of the battle, and the new owner of the castle | | `WorldNews` | Boss kills, excellent and ancient items which monsters dropped, characters which reach a level milestone | | `Notices` | Global notices which game masters send with the `!` chat prefix | +| `StaffAlerts` | The commands which game masters used in Discord, see [Commands for game masters](#commands-for-game-masters) | Each category is posted into its own channel. A category without channel isn't posted. @@ -104,6 +105,8 @@ In the distributed deployment, the bot runs in its own container, see | `/character ` | Selects the character of the account, as which the player appears | | `/say ` | Sends a message to the chat of the game which is bound to the channel, see [Chat bridge](#chat-bridge) | | `/guildchat bind`, `create`, `unbind` | Binds the chat of a guild or alliance to a channel, see [Chat bridge](#chat-bridge) | +| `/notify ` | Turns [direct messages](#direct-messages) on or off | +| `/announce `, `/kick `, `/ban ` | [Commands for game masters](#commands-for-game-masters) | The commands only show information; they don't change anything in the game. Additionally, players can [link their account](#linking-accounts) with @@ -273,6 +276,45 @@ Without the intent, players use `/say `. Discord only approves the intent for verified bots, which are in more than 100 Discord servers, with a reason. +## Commands for game masters + +Game masters can use these commands in Discord: + +| Command | Effect | +|---|---| +| `/announce ` | Sends the text as golden notice to all players of all game servers, and posts it into the channel of the `Notices` category, e.g. `#announcements`. | +| `/kick ` | Disconnects the character from the game. | +| `/ban ` | Bans the account of the character, which has to be online, and disconnects it. | + +They need the role *GM* of the [layout](#setting-up-your-discord-server) on your +Discord server, **and** a [linked account](#linking-accounts) whose selected +character (`/character`) is a game master. So a Discord role alone isn't +enough. Every use is logged and posted into `#staff-alerts`, or the channel of +the `StaffAlerts` category. + +## Direct messages + +Linked players can get direct messages from the bot. They are off by default, +and each player turns them on with `/notify` in Discord, or with +`/discord notify ` in the game, which turns a type on or off: + +| Type | Direct message | +|---|---| +| `login` | Somebody tried to log into the account with the correct password, while it was logged in. | +| `letter` | A character of the account received a letter. | +| `friend` | A friend entered the game. Friends who are invisible in the game don't cause a message. | + +The bot can only send direct messages to users who share a Discord server with +it, and who allow direct messages from its members. + +## Scheduled events + +With `Bot__CreateScheduledEvents` set to `true`, the bot creates a *scheduled +event* on your Discord server for the next castle siege, so that players can +mark it as interesting and get reminded by Discord. It's updated when the castle +siege changes its state, which requires the *Game Event Publisher* plugin with +castle siege events. The bot needs the permission to create events. + ## Settings All settings are part of the `Discord` section of the configuration, so they can @@ -290,6 +332,7 @@ the parts are separated by two underscores. | `Bot__StatusChannelId` | the layout | The identifier of the channel with the status message. | | `Bot__Channels__` | the layout | The identifier of the channel per category, into which the bot posts the notifications. | | `Bot__LayoutFile` | the default | The path of a JSON file with your own [layout](#your-own-layout). | +| `Bot__CreateScheduledEvents` | `false` | Creates a [scheduled event](#scheduled-events) for the next castle siege. | | `Bot__StatusUpdateInterval` | `00:01:00` | How often the status message and the presence are updated. | | `Bot__ChatBridge__BindingMode` | `Both` | Where the chats of guilds can be bound: `Both`, `HostedOnly` (only channels on your Discord server) or `GuildOwnedOnly` (only on Discord servers of guilds). | | `Bot__ChatBridge__AllowedDiscordServerIds__0`, `__1`, … | all | The Discord servers of guilds, on which chats can be bound. | @@ -326,3 +369,7 @@ Commands are registered globally so guild-owned Discord servers can use `/link`, guild-scoped registration for immediate availability. Binding and unbinding as a guild master require that the selected character still belongs to the linked account. + +Direct messages and scheduled-event updates use a bounded queue. Stopping or +reconfiguring the bot cancels pending work, including notifications waiting for +a gateway connection. diff --git a/src/DataModel/Entities/AccountExternalLink.cs b/src/DataModel/Entities/AccountExternalLink.cs index 945f979d3c..c9d72f333a 100644 --- a/src/DataModel/Entities/AccountExternalLink.cs +++ b/src/DataModel/Entities/AccountExternalLink.cs @@ -55,6 +55,12 @@ public class AccountExternalLink /// public DateTime? LinkedAt { get; set; } + /// + /// Gets or sets the types of notifications which the user receives in the external service. + /// They are opt-in, so there are none by default. + /// + public AccountNotificationTypes Notifications { get; set; } + /// /// Gets or sets the hash of the pending one-time code; null, if there is none. /// diff --git a/src/DataModel/Entities/AccountNotificationTypes.cs b/src/DataModel/Entities/AccountNotificationTypes.cs new file mode 100644 index 0000000000..1e87995aba --- /dev/null +++ b/src/DataModel/Entities/AccountNotificationTypes.cs @@ -0,0 +1,32 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.DataModel.Entities; + +/// +/// The types of notifications which a player receives in an external service, e.g. as direct message in Discord. +/// +[Flags] +public enum AccountNotificationTypes +{ + /// + /// No notifications. + /// + None = 0, + + /// + /// Somebody tried to log into the account while it was logged in. + /// + LoginAttempt = 1, + + /// + /// A character of the account received a letter. + /// + LetterReceived = 2, + + /// + /// A friend of a character of the account entered the game. + /// + FriendOnline = 4, +} diff --git a/src/Discord/DiscordAccountCommands.cs b/src/Discord/DiscordAccountCommands.cs index 839b62caa4..8f6105ecd2 100644 --- a/src/Discord/DiscordAccountCommands.cs +++ b/src/Discord/DiscordAccountCommands.cs @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.Discord; using System.Globalization; using Microsoft.Extensions.Logging; +using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.Discord.Properties; using MUnique.OpenMU.GameLogic.AccountLinking; @@ -78,6 +79,49 @@ public async ValueTask ExecuteAsync(string commandName, st } } + /// + /// Turns a type of direct messages on or off. + /// + /// The identifier of the Discord user. + /// The keyword of the type, see . + /// A value indicating whether the direct messages are turned on. + /// The answer. + public async ValueTask SetNotificationAsync(ulong userId, string? typeKeyword, bool enabled) + { + try + { + var notifications = await this._linkService.SetNotificationByUserAsync(Provider, userId.ToString(CultureInfo.InvariantCulture), AccountNotificationKeywords.Parse(typeKeyword), enabled).ConfigureAwait(false); + if (notifications is not { } types) + { + return this.Answer(nameof(Resources.Link_NotLinked)).Embed; + } + + var names = new List(); + if (types.HasFlag(AccountNotificationTypes.LoginAttempt)) + { + names.Add(DiscordCommands.GetText(this._culture, nameof(Resources.Notify_Type_Login))); + } + + if (types.HasFlag(AccountNotificationTypes.LetterReceived)) + { + names.Add(DiscordCommands.GetText(this._culture, nameof(Resources.Notify_Type_Letter))); + } + + if (types.HasFlag(AccountNotificationTypes.FriendOnline)) + { + names.Add(DiscordCommands.GetText(this._culture, nameof(Resources.Notify_Type_Friend))); + } + + var list = names.Count > 0 ? string.Join(", ", names) : DiscordCommands.GetText(this._culture, nameof(Resources.Notify_None)); + return this.Answer(nameof(Resources.Notify_Changed), [], list).Embed; + } + catch (Exception ex) + { + this._logger.LogError(ex, "Error when changing the direct messages of the Discord user {userId}.", userId); + return this.Answer(nameof(Resources.Command_Failed)).Embed; + } + } + private async ValueTask LinkAsync(string code, ulong userId, string userName) { var userIdText = userId.ToString(CultureInfo.InvariantCulture); diff --git a/src/Discord/DiscordBackgroundQueue.cs b/src/Discord/DiscordBackgroundQueue.cs new file mode 100644 index 0000000000..a7a2a2bd71 --- /dev/null +++ b/src/Discord/DiscordBackgroundQueue.cs @@ -0,0 +1,104 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Discord; + +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +/// +/// Runs bounded background work for one connection lifetime of the bot. +/// +internal sealed class DiscordBackgroundQueue : IAsyncDisposable +{ + private readonly Channel> _queue; + private readonly CancellationTokenSource _cancellation = new(); + private readonly ILogger _logger; + private readonly Task _worker; + private int _isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The maximum number of pending operations. + internal DiscordBackgroundQueue(ILogger logger, int capacity = 100) + { + this._logger = logger; + this._queue = Channel.CreateBounded>(new BoundedChannelOptions(capacity) { SingleReader = true }); + this._worker = Task.Run(this.RunAsync); + } + + /// + ValueTask IAsyncDisposable.DisposeAsync() => this.DisposeAsync(); + + /// + /// Queues an operation, or rejects it when the queue is full or stopped. + /// + /// The operation. + /// Whether the operation was queued. + internal bool Enqueue(Func operation) + { + if (this._queue.Writer.TryWrite(operation)) + { + return true; + } + + this._logger.LogWarning("A Discord background notification was dropped because the queue is full or stopped."); + return false; + } + + /// + /// Cancels the connection's operations and waits for the worker to stop. + /// + /// The asynchronous shutdown operation. + internal async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref this._isDisposed, 1) != 0) + { + return; + } + + this._queue.Writer.TryComplete(); + await this._cancellation.CancelAsync().ConfigureAwait(false); + try + { + await this._worker.WaitAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(false); + } + catch (TimeoutException) + { + this._logger.LogWarning("A Discord background operation did not finish within the shutdown timeout."); + } + finally + { + this._cancellation.Dispose(); + } + } + + private async Task RunAsync() + { + var cancellationToken = this._cancellation.Token; + try + { + await foreach (var operation in this._queue.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + await operation(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) + { + this._logger.LogWarning(ex, "A Discord background notification failed."); + } + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Shutdown cancels waits for Ready as well as requests to Discord. + } + } +} diff --git a/src/Discord/DiscordBot.Extras.cs b/src/Discord/DiscordBot.Extras.cs new file mode 100644 index 0000000000..9f867c368b --- /dev/null +++ b/src/Discord/DiscordBot.Extras.cs @@ -0,0 +1,201 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Discord; + +using System.Threading; +using System.Threading.Tasks; +using global::Discord; +using global::Discord.WebSocket; +using Microsoft.Extensions.Logging; +using MUnique.OpenMU.Discord.Properties; +using MUnique.OpenMU.Discord.Provisioning; +using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.Interfaces; + +/// +/// The part of the with the commands for game masters, the direct messages +/// and the scheduled events. +/// +public sealed partial class DiscordBot +{ + /// + /// The name of the command which turns direct messages on or off. + /// + private const string NotifyCommandName = "notify"; + + private const string NotifyTypeOptionName = "type"; + + private const string NotifyEnabledOptionName = "enabled"; + + private IEnumerable CreateExtraCommands() + { + foreach (var definition in DiscordGameMasterCommands.Definitions) + { + yield return new SlashCommandBuilder() + .WithName(definition.Name) + .WithDescription(this._commands.Text(definition.DescriptionKey)) + .WithContextTypes(InteractionContextType.Guild) + .AddOption(definition.OptionName, ApplicationCommandOptionType.String, this._commands.Text(definition.OptionDescriptionKey!), isRequired: true) + .Build(); + } + + yield return new SlashCommandBuilder() + .WithName(NotifyCommandName) + .WithDescription(this._commands.Text(nameof(Resources.Command_Notify_Description))) + .AddOption(new SlashCommandOptionBuilder() + .WithName(NotifyTypeOptionName) + .WithDescription(this._commands.Text(nameof(Resources.Command_Notify_TypeOption))) + .WithType(ApplicationCommandOptionType.String) + .WithRequired(true) + .AddChoice(this._commands.Text(nameof(Resources.Notify_Type_Login)), AccountNotificationKeywords.LoginAttempt) + .AddChoice(this._commands.Text(nameof(Resources.Notify_Type_Letter)), AccountNotificationKeywords.LetterReceived) + .AddChoice(this._commands.Text(nameof(Resources.Notify_Type_Friend)), AccountNotificationKeywords.FriendOnline)) + .AddOption(NotifyEnabledOptionName, ApplicationCommandOptionType.Boolean, this._commands.Text(nameof(Resources.Command_Notify_EnabledOption)), isRequired: true) + .Build(); + } + + private async Task ExecuteGameMasterCommandAsync(SocketSlashCommand command) + { + try + { + await command.DeferAsync(ephemeral: true).ConfigureAwait(false); + var argument = command.Data.Options.FirstOrDefault()?.Value as string ?? string.Empty; + var answer = await this._gameMasterCommands.ExecuteAsync(command.Data.Name, argument, command.User.Id, command.User.Username, this.HasGameMasterRole(command)).ConfigureAwait(false); + if (answer.StaffAlert is { } alert && this._routing.GetChannelId(DiscordChannelCategory.StaffAlerts) is { } channelId) + { + await this.SendAsync(channelId, [alert], CancellationToken.None).ConfigureAwait(false); + } + + if (answer.Announcement is { } announcement && this._routing.GetChannelId(DiscordChannelCategory.Notices) is { } noticesChannelId) + { + await this.SendAsync(noticesChannelId, [announcement], CancellationToken.None).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); + } + } + + /// + /// Determines whether the user has the GM role of the layout on the Discord server of the game server. + /// + private bool HasGameMasterRole(SocketSlashCommand command) + { + var roleName = this._layout.Roles.FirstOrDefault(role => role.Key == DiscordServerLayout.GameMasterRoleKey)?.Name; + return this._client is { } client + && command.User is SocketGuildUser user + && user.Guild.Id == this.GetRoutingGuild(client)?.Id + && user.Roles.Any(role => string.Equals(role.Name, roleName, StringComparison.OrdinalIgnoreCase)); + } + + private async Task ExecuteNotifyCommandAsync(SocketSlashCommand command) + { + try + { + await command.DeferAsync(ephemeral: true).ConfigureAwait(false); + var type = command.Data.Options.FirstOrDefault(option => option.Name == NotifyTypeOptionName)?.Value as string; + var enabled = command.Data.Options.FirstOrDefault(option => option.Name == NotifyEnabledOptionName)?.Value is true; + var answer = await this._accountCommands.SetNotificationAsync(command.User.Id, type, enabled).ConfigureAwait(false); + await command.FollowupAsync(embed: ToDiscordEmbed(answer), 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); + } + } + + /// + /// Sends the direct messages for a game event in the background, so that the game doesn't wait for the database or Discord. + /// + private void SendDirectMessages(GameEvent gameEvent) + { + this._backgroundQueue?.Enqueue(async cancellationToken => + { + try + { + foreach (var (userId, embed) in await this._directMessages.GetMessagesAsync(gameEvent).ConfigureAwait(false)) + { + await this._ready.Task.WaitAsync(cancellationToken).ConfigureAwait(false); + if (this._client is { } client + && await client.Rest.GetUserAsync(userId, options: new RequestOptions { CancelToken = cancellationToken }).ConfigureAwait(false) is { } user) + { + var channel = await user.CreateDMChannelAsync(new RequestOptions { CancelToken = cancellationToken }).ConfigureAwait(false); + await channel.SendMessageAsync( + embed: ToDiscordEmbed(embed), + allowedMentions: AllowedMentions.None, + options: new RequestOptions { CancelToken = cancellationToken }).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // The connection lifetime ended. + } + catch (Exception ex) + { + // E.g. the user doesn't accept direct messages. + this._logger.LogWarning(ex, "A direct message for the game event {gameEvent} couldn't be sent.", gameEvent.GetType().Name); + } + }); + } + + /// + /// Creates or updates the scheduled event of the next castle siege on the Discord server of the game server. + /// + private void UpdateCastleSiegeEvent(CastleSiegeStateChangedEvent stateChanged) + { + if (!this._settings.CreateScheduledEvents + || stateChanged.NextBattleStartUtc is not { } startUtc + || startUtc <= DateTime.UtcNow.AddMinutes(1)) + { + return; + } + + var endUtc = stateChanged.NextBattleEndUtc is { } end && end > startUtc ? end : startUtc.AddHours(2); + this._backgroundQueue?.Enqueue(cancellationToken => this.UpdateCastleSiegeEventAsync(startUtc, endUtc, cancellationToken)); + } + + private async Task UpdateCastleSiegeEventAsync(DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken) + { + await this._ready.Task.WaitAsync(cancellationToken).ConfigureAwait(false); + if (this._client is not { } client || this.GetRoutingGuild(client) is not { } guild) + { + return; + } + + var name = this._commands.Text(nameof(Resources.Event_CastleSiege_Name)); + var start = new DateTimeOffset(DateTime.SpecifyKind(startUtc, DateTimeKind.Utc)); + var endTime = new DateTimeOffset(DateTime.SpecifyKind(endUtc, DateTimeKind.Utc)); + var events = await guild.GetEventsAsync(options: new RequestOptions { CancelToken = cancellationToken }).ConfigureAwait(false); + var existing = events.FirstOrDefault(e => e.Name == name && e.Type == GuildScheduledEventType.External && e.Status == GuildScheduledEventStatus.Scheduled); + if (existing is null) + { + await guild.CreateEventAsync( + name, + start, + GuildScheduledEventType.External, + GuildScheduledEventPrivacyLevel.Private, + this._commands.Text(nameof(Resources.Event_CastleSiege_Description)), + endTime, + location: this._commands.Text(nameof(Resources.Event_Location)), + options: new RequestOptions { CancelToken = cancellationToken }).ConfigureAwait(false); + } + else if (existing.StartTime != start || existing.EndTime != endTime) + { + await existing.ModifyAsync( + properties => + { + properties.StartTime = start; + properties.EndTime = endTime; + }, + options: new RequestOptions { CancelToken = cancellationToken }).ConfigureAwait(false); + } + } +} diff --git a/src/Discord/DiscordBot.cs b/src/Discord/DiscordBot.cs index 643c5698e7..a5b1e9fe7d 100644 --- a/src/Discord/DiscordBot.cs +++ b/src/Discord/DiscordBot.cs @@ -48,10 +48,13 @@ public sealed partial class DiscordBot : IManageableServer, IDiscordMessenger, I private readonly DiscordServerProvisioner _provisioner; private readonly DiscordChatBridge _chatBridge; private readonly DiscordChatCommands _chatCommands; + private readonly DiscordGameMasterCommands _gameMasterCommands; + private readonly DiscordDirectMessages _directMessages; private readonly ILogger _logger; private readonly SemaphoreSlim _lifecycleLock = new(1, 1); private DiscordSocketClient? _client; + private DiscordBackgroundQueue? _backgroundQueue; private TaskCompletionSource _ready = new(TaskCreationOptions.RunContinuationsAsynchronously); private CancellationTokenSource? _statusLoopCancellation; private DiscordChannelRouting _routing; @@ -71,8 +74,10 @@ public sealed partial class DiscordBot : IManageableServer, IDiscordMessenger, I /// The provisioner, which sets up the Discord server. /// The chat bridge between the game and Discord. /// The commands of the chat bridge. + /// The commands for game masters. + /// The direct messages to linked users. /// The logger. - public DiscordBot(DiscordBotSettings settings, DiscordCommands commands, DiscordAccountCommands accountCommands, DiscordStatusFormatter statusFormatter, IDiscordGameDataProvider data, DiscordServerLayout layout, DiscordServerProvisioner provisioner, DiscordChatBridge chatBridge, DiscordChatCommands chatCommands, ILogger logger) + public DiscordBot(DiscordBotSettings settings, DiscordCommands commands, DiscordAccountCommands accountCommands, DiscordStatusFormatter statusFormatter, IDiscordGameDataProvider data, DiscordServerLayout layout, DiscordServerProvisioner provisioner, DiscordChatBridge chatBridge, DiscordChatCommands chatCommands, DiscordGameMasterCommands gameMasterCommands, DiscordDirectMessages directMessages, ILogger logger) { this._settings = settings; this._commands = commands; @@ -83,6 +88,8 @@ public DiscordBot(DiscordBotSettings settings, DiscordCommands commands, Discord this._provisioner = provisioner; this._chatBridge = chatBridge; this._chatCommands = chatCommands; + this._gameMasterCommands = gameMasterCommands; + this._directMessages = directMessages; this._logger = logger; this._routing = DiscordChannelRouting.Create(settings, layout, new Dictionary()); } @@ -142,6 +149,7 @@ public async ValueTask StartAsync() } this._ready = new(TaskCreationOptions.RunContinuationsAsynchronously); + this._backgroundQueue = new DiscordBackgroundQueue(this._logger); var client = new DiscordSocketClient(new DiscordSocketConfig { GatewayIntents = this.GetGatewayIntents() }); client.Log += this.OnLogAsync; client.Ready += () => this.OnReadyAsync(client); @@ -206,6 +214,7 @@ public async Task SendAsync(ulong channelId, IReadOnlyList embeds, /// /// /// Chat messages are mirrored to the bound channels. + /// Linked users get direct messages, if they turned them on, and the next castle siege becomes a scheduled event. /// 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) @@ -214,6 +223,14 @@ public async ValueTask OnGameEventAsync(GameEvent gameEvent) { await this.OnChatMessageAsync(chatMessage).ConfigureAwait(false); } + else if (gameEvent is AccountLoginBlockedEvent or LetterReceivedEvent or PlayerEnteredGameEvent) + { + this.SendDirectMessages(gameEvent); + } + else if (gameEvent is CastleSiegeStateChangedEvent castleSiegeStateChanged) + { + this.UpdateCastleSiegeEvent(castleSiegeStateChanged); + } else if (gameEvent is AccountUnlinkedEvent { Provider: AccountLinkService.DiscordProvider } unlinked && ulong.TryParse(unlinked.ExternalUserId, NumberStyles.None, CultureInfo.InvariantCulture, out var userId)) { @@ -251,6 +268,12 @@ private static Embed ToDiscordEmbed(DiscordEmbed embed) private async ValueTask StopClientAsync() { + if (this._backgroundQueue is { } backgroundQueue) + { + this._backgroundQueue = null; + await backgroundQueue.DisposeAsync().ConfigureAwait(false); + } + if (this._statusLoopCancellation is { } cancellation) { await cancellation.CancelAsync().ConfigureAwait(false); @@ -315,7 +338,7 @@ private async Task RegisterCommandsAsync(DiscordSocketClient client) } return (ApplicationCommandProperties)builder.Build(); - }).Append(this.CreateAdministrationCommand()).Concat(this.CreateChatCommands()).ToArray(); + }).Append(this.CreateAdministrationCommand()).Concat(this.CreateChatCommands()).Concat(this.CreateExtraCommands()).ToArray(); // Guild-owned chat channels need /link, /guildchat and /say outside the routing guild, too. await ((IDiscordClient)client).BulkOverwriteGlobalApplicationCommand(commands).ConfigureAwait(false); @@ -360,6 +383,18 @@ private async Task OnSlashCommandExecutedAsync(SocketSlashCommand command) return; } + if (DiscordGameMasterCommands.IsGameMasterCommand(command.Data.Name)) + { + await this.ExecuteGameMasterCommandAsync(command).ConfigureAwait(false); + return; + } + + if (command.Data.Name == NotifyCommandName) + { + await this.ExecuteNotifyCommandAsync(command).ConfigureAwait(false); + return; + } + try { // The data might come from the database, which can take longer than Discord waits for an answer. diff --git a/src/Discord/DiscordBotSettings.cs b/src/Discord/DiscordBotSettings.cs index ed256ea71e..7690a4f6bf 100644 --- a/src/Discord/DiscordBotSettings.cs +++ b/src/Discord/DiscordBotSettings.cs @@ -48,6 +48,12 @@ public class DiscordBotSettings /// public DiscordChatBridgeSettings ChatBridge { get; set; } = new(); + /// + /// Gets or sets a value indicating whether the bot creates a scheduled event on the Discord server for the next castle siege. + /// It requires the permission to create events. + /// + public bool CreateScheduledEvents { get; set; } + /// /// Gets or sets the interval in which the status message and the presence of the bot are updated. /// diff --git a/src/Discord/DiscordChannelCategory.cs b/src/Discord/DiscordChannelCategory.cs index 8e021a4356..de2e737127 100644 --- a/src/Discord/DiscordChannelCategory.cs +++ b/src/Discord/DiscordChannelCategory.cs @@ -28,4 +28,9 @@ public enum DiscordChannelCategory /// Notices of game masters. /// Notices, + + /// + /// Alerts for the staff, e.g. the commands which game masters used in Discord. + /// + StaffAlerts, } diff --git a/src/Discord/DiscordDirectMessages.cs b/src/Discord/DiscordDirectMessages.cs new file mode 100644 index 0000000000..b76517452d --- /dev/null +++ b/src/Discord/DiscordDirectMessages.cs @@ -0,0 +1,138 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Discord; + +using System.Globalization; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.Discord.Properties; +using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.Interfaces; +using MUnique.OpenMU.Persistence; + +/// +/// Determines the direct messages which the bot sends to linked users because of game events, if they turned them on. +/// +public sealed class DiscordDirectMessages +{ + /// + /// The color of the direct messages (blue). + /// + private const int Color = DiscordCommands.AnswerColor; + + private readonly AccountLinkService _linkService; + private readonly IFriendServer _friendServer; + private readonly Func _createContext; + private readonly Func _createFriendContext; + private readonly Func _getServerName; + private readonly CultureInfo _culture; + + /// + /// Initializes a new instance of the class. + /// + /// The service of the account links. + /// The friend server. + /// The function which creates a player persistence context. + /// The function which creates a friend persistence context. + /// The function which gets the name of a game server. + /// The culture of the messages. + public DiscordDirectMessages(AccountLinkService linkService, IFriendServer friendServer, Func createContext, Func createFriendContext, Func getServerName, CultureInfo culture) + { + this._linkService = linkService; + this._friendServer = friendServer; + this._createContext = createContext; + this._createFriendContext = createFriendContext; + this._getServerName = getServerName; + this._culture = culture; + } + + /// + /// Gets the direct messages for a game event. + /// + /// The game event. + /// The identifiers of the Discord users and their messages. + public async ValueTask> GetMessagesAsync(GameEvent gameEvent) + { + switch (gameEvent) + { + case AccountLoginBlockedEvent loginBlocked: + { + using var context = this._createContext(); + var accountId = await context.GetAccountIdByLoginNameAsync(loginBlocked.LoginName).ConfigureAwait(false); + return await this.CreateAsync(accountId, AccountNotificationTypes.LoginAttempt, nameof(Resources.Dm_LoginAttempt), this._getServerName(loginBlocked.ServerId) ?? loginBlocked.ServerId.ToString(this._culture)).ConfigureAwait(false); + } + + case LetterReceivedEvent letter: + { + using var context = this._createContext(); + var accountId = await context.GetAccountIdByCharacterNameAsync(letter.ReceiverName).ConfigureAwait(false); + return await this.CreateAsync(accountId, AccountNotificationTypes.LetterReceived, nameof(Resources.Dm_LetterReceived), DiscordMessageFormatter.Escape(letter.ReceiverName), DiscordMessageFormatter.Escape(letter.SenderName), DiscordMessageFormatter.Escape(letter.Subject)).ConfigureAwait(false); + } + + case PlayerEnteredGameEvent entered: + return await this.GetFriendOnlineMessagesAsync(entered).ConfigureAwait(false); + default: + return []; + } + } + + private async ValueTask> GetFriendOnlineMessagesAsync(PlayerEnteredGameEvent entered) + { + // Invisible players don't want their friends to know that they are online. + if (await this._friendServer.GetOnlineServerIdAsync(entered.CharacterName).ConfigureAwait(false) is null) + { + return []; + } + + IReadOnlyList friendNames; + using (var friendContext = this._createFriendContext()) + { + friendNames = (await friendContext.GetFriendNamesAsync(entered.CharacterId).ConfigureAwait(false)).ToList(); + } + + if (friendNames.Count == 0) + { + return []; + } + + // Two queries for all friends, because this runs whenever a character enters the game. + using var context = this._createContext(); + var accountIds = (await context.GetAccountIdsByCharacterNamesAsync(friendNames).ConfigureAwait(false)).Values.Distinct().ToList(); + var links = await context.GetAccountExternalLinksAsync(AccountLinkService.DiscordProvider, accountIds).ConfigureAwait(false); + return links + .Where(link => link.Notifications.HasFlag(AccountNotificationTypes.FriendOnline)) + .Select(link => this.Create(link, nameof(Resources.Dm_FriendOnline), DiscordMessageFormatter.Escape(entered.CharacterName))) + .OfType<(ulong UserId, DiscordEmbed Embed)>() + .ToList(); + } + + private async ValueTask> CreateAsync(Guid? accountId, AccountNotificationTypes type, string resourceKey, params object[] args) + { + if (accountId is not { } id + || await this._linkService.GetLinkAsync(id, AccountLinkService.DiscordProvider).ConfigureAwait(false) is not { } link + || !link.Notifications.HasFlag(type) + || this.Create(link, resourceKey, args) is not { } message) + { + return []; + } + + return [message]; + } + + private (ulong UserId, DiscordEmbed Embed)? Create(AccountExternalLink link, string resourceKey, params object[] args) + { + if (!ulong.TryParse(link.ExternalUserId, NumberStyles.None, CultureInfo.InvariantCulture, out var userId)) + { + return null; + } + + var embed = new DiscordEmbed( + DiscordCommands.GetText(this._culture, nameof(Resources.Link_Title)), + DiscordCommands.GetText(this._culture, resourceKey, args), + Color, + DateTime.UtcNow, + null); + return (userId, embed); + } +} diff --git a/src/Discord/DiscordGameMasterAnswer.cs b/src/Discord/DiscordGameMasterAnswer.cs new file mode 100644 index 0000000000..bd59f1b5b4 --- /dev/null +++ b/src/Discord/DiscordGameMasterAnswer.cs @@ -0,0 +1,13 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Discord; + +/// +/// The answer of a game master command. +/// +/// The answer for the game master. +/// The alert for the staff about the use of the command; or null, if the command wasn't executed. +/// The announcement for the channel of the notices, which /announce sent to the game; or null. +public sealed record DiscordGameMasterAnswer(DiscordEmbed Embed, DiscordEmbed? StaffAlert, DiscordEmbed? Announcement = null); diff --git a/src/Discord/DiscordGameMasterCommands.cs b/src/Discord/DiscordGameMasterCommands.cs new file mode 100644 index 0000000000..637c5d3cdc --- /dev/null +++ b/src/Discord/DiscordGameMasterCommands.cs @@ -0,0 +1,211 @@ +// +// 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.DataModel.Entities; +using MUnique.OpenMU.Discord.Properties; +using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.Interfaces; +using MUnique.OpenMU.Persistence; + +/// +/// The slash commands for game masters, which change something in the game. +/// +/// +/// They are allowed for Discord users who have the GM role of the layout, and who are linked to an account +/// whose selected character is a game master. Every use is reported to the staff. +/// The role is matched by its name, so it's only a convenience filter, e.g. to hide the commands from others: +/// The game master status of the character is the actual check, because only the game can grant it. +/// +public sealed class DiscordGameMasterCommands +{ + /// + /// The color of the alerts for the staff (orange). + /// + private const int StaffAlertColor = 0xE67E22; + + /// + /// The maximum length of an announcement. Longer texts would be shown as several golden messages. + /// + private const int MaximumAnnouncementLength = 200; + + private readonly IServerProvider _serverProvider; + private readonly AccountLinkService _linkService; + private readonly Func _createContext; + private readonly CultureInfo _culture; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The provider of the game servers. + /// The service of the account links. + /// The function which creates a persistence context. + /// The culture of the answers. + /// The logger. + public DiscordGameMasterCommands(IServerProvider serverProvider, AccountLinkService linkService, Func createContext, CultureInfo culture, ILogger logger) + { + this._serverProvider = serverProvider; + this._linkService = linkService; + this._createContext = createContext; + this._culture = culture; + this._logger = logger; + } + + /// + /// Gets the definitions of the commands. + /// + public static IReadOnlyList Definitions { get; } = + [ + new("announce", nameof(Resources.Command_Announce_Description), "text", nameof(Resources.Command_Announce_TextOption)), + new("kick", nameof(Resources.Command_Kick_Description), "character", nameof(Resources.Command_Who_CharacterOption)), + new("ban", nameof(Resources.Command_Ban_Description), "character", nameof(Resources.Command_Who_CharacterOption)), + ]; + + /// + /// 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 IsGameMasterCommand(string commandName) => Definitions.Any(definition => definition.Name == commandName); + + /// + /// Executes a command, if the user is allowed to. + /// + /// The name of the command. + /// The value of the option of the command. + /// The identifier of the Discord user. + /// The name of the Discord user. + /// A value indicating whether the user has the GM role. + /// The answer. + public async ValueTask ExecuteAsync(string commandName, string argument, ulong userId, string userName, bool hasGameMasterRole) + { + if (!hasGameMasterRole || await this.GetGameMasterCharacterAsync(userId).ConfigureAwait(false) is not { } characterName) + { + return new(this.Answer(nameof(Resources.Gm_NotAllowed)), null); + } + + argument = argument.Trim(); + if (commandName == "announce" && argument.Length > MaximumAnnouncementLength) + { + return new(this.Answer(nameof(Resources.Gm_AnnouncementTooLong), MaximumAnnouncementLength), null); + } + + var gameServers = this._serverProvider.Servers.OfType().ToList(); + string resultKey; + object[] resultArgs = [DiscordMessageFormatter.Escape(argument)]; + DiscordEmbed? announcement = null; + switch (commandName) + { + case "announce": + // A failing game server doesn't stop the others, and the game master learns about it. + var announced = 0; + foreach (var gameServer in gameServers) + { + if (await this.TryAsync(gameServer, server => server.SendGlobalMessageAsync(argument, MessageType.GoldenCenter)).ConfigureAwait(false)) + { + announced++; + } + } + + resultKey = announced == gameServers.Count ? nameof(Resources.Gm_Announced) : nameof(Resources.Gm_AnnouncedPartially); + resultArgs = [DiscordMessageFormatter.Escape(argument), announced, gameServers.Count]; + if (announced > 0) + { + // Posted like a notice which a game master sends in the game, but without a game server in the footer. + announcement = new DiscordEmbed( + DiscordCommands.GetText(this._culture, nameof(Resources.GlobalNotice_Title)), + DiscordMessageFormatter.Escape(argument), + DiscordMessageFormatter.NoticeColor, + DateTime.UtcNow, + null); + } + + break; + case "kick": + resultKey = await this.AnyAsync(gameServers, server => server.DisconnectPlayerAsync(argument)).ConfigureAwait(false) + ? nameof(Resources.Gm_Kicked) + : nameof(Resources.Gm_NotOnline); + break; + default: + resultKey = await this.AnyAsync(gameServers, server => server.BanPlayerAsync(argument)).ConfigureAwait(false) + ? nameof(Resources.Gm_Banned) + : nameof(Resources.Gm_NotOnline); + break; + } + + this._logger.LogInformation("The Discord user {user} ({userId}) as {character} used the command {command} {argument}.", userName, userId, characterName, commandName, argument); + var alert = new DiscordEmbed( + DiscordCommands.GetText(this._culture, nameof(Resources.StaffAlert_Title)), + DiscordCommands.GetText(this._culture, nameof(Resources.StaffAlert_Text), DiscordMessageFormatter.Escape(userName), DiscordMessageFormatter.Escape(characterName), commandName, DiscordMessageFormatter.Escape(argument)), + StaffAlertColor, + DateTime.UtcNow, + null); + return new(this.Answer(resultKey, resultArgs), alert, announcement); + } + + private async ValueTask AnyAsync(IEnumerable gameServers, Func> action) + { + foreach (var gameServer in gameServers) + { + try + { + if (await action(gameServer).ConfigureAwait(false)) + { + return true; + } + } + catch (Exception ex) + { + this._logger.LogError(ex, "Error when executing a game master command on game server {server}.", gameServer.Id); + } + } + + return false; + } + + private async ValueTask TryAsync(IGameServer gameServer, Func action) + { + try + { + await action(gameServer).ConfigureAwait(false); + return true; + } + catch (Exception ex) + { + this._logger.LogError(ex, "Error when executing a game master command on game server {server}.", gameServer.Id); + return false; + } + } + + /// + /// Gets the name of the selected character of the linked account, if it's a game master. + /// + private async ValueTask GetGameMasterCharacterAsync(ulong userId) + { + if (await this._linkService.GetLinkByUserAsync(AccountLinkService.DiscordProvider, userId.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false) is not { ExternalUserId: not null, CharacterName: { } characterName } link) + { + return null; + } + + using var context = this._createContext(); + return await context.GetAccountIdByCharacterNameAsync(characterName).ConfigureAwait(false) == link.AccountId + && await context.GetCharacterStatusAsync(characterName).ConfigureAwait(false) == CharacterStatus.GameMaster + ? characterName + : null; + } + + private DiscordEmbed Answer(string resourceKey, params object[] args) + { + return new DiscordEmbed( + DiscordCommands.GetText(this._culture, nameof(Resources.Gm_Title)), + DiscordCommands.GetText(this._culture, resourceKey, args), + DiscordCommands.AnswerColor, + DateTime.UtcNow, + null); + } +} diff --git a/src/Discord/DiscordServiceCollectionExtensions.cs b/src/Discord/DiscordServiceCollectionExtensions.cs index 094a7a9408..e2c74b352b 100644 --- a/src/Discord/DiscordServiceCollectionExtensions.cs +++ b/src/Discord/DiscordServiceCollectionExtensions.cs @@ -63,6 +63,19 @@ public static IServiceCollection AddDiscord(this IServiceCollection services, Di provider.GetRequiredService>(), provider.GetService())) .AddSingleton(new DiscordChatCommands(culture)) + .AddSingleton(provider => new DiscordGameMasterCommands( + provider.GetRequiredService(), + new AccountLinkService(() => provider.GetRequiredService().CreateNewPlayerContext(new GameConfiguration())), + () => provider.GetRequiredService().CreateNewPlayerContext(new GameConfiguration()), + culture, + provider.GetRequiredService>())) + .AddSingleton(provider => new DiscordDirectMessages( + new AccountLinkService(() => provider.GetRequiredService().CreateNewPlayerContext(new GameConfiguration())), + provider.GetRequiredService(), + () => provider.GetRequiredService().CreateNewPlayerContext(new GameConfiguration()), + () => provider.GetRequiredService().CreateNewFriendServerContext(), + serverId => provider.GetRequiredService().Servers.OfType().FirstOrDefault(server => server.Id == serverId)?.Description, + culture)) .AddSingleton(new DiscordStatusFormatter(culture)) .AddSingleton(settings.Bot.LayoutFile is { Length: > 0 } layoutFile ? DiscordServerLayout.Load(layoutFile) : DiscordServerLayout.LoadDefault()) .AddSingleton(provider => new DiscordServerProvisioner(provider.GetRequiredService>())) @@ -76,6 +89,8 @@ public static IServiceCollection AddDiscord(this IServiceCollection services, Di provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService>())) .AddHostedService(provider => provider.GetRequiredService()) .AddSingleton(provider => provider.GetRequiredService()); diff --git a/src/Discord/Properties/Resources.Designer.cs b/src/Discord/Properties/Resources.Designer.cs index 5ccc1e8b72..dfff232f21 100644 --- a/src/Discord/Properties/Resources.Designer.cs +++ b/src/Discord/Properties/Resources.Designer.cs @@ -429,6 +429,33 @@ public static string Chat_TooFast { } } + /// + /// Looks up a localized string similar to Sends a notice to all players in the game and into the announcements channel. For game masters.. + /// + public static string Command_Announce_Description { + get { + return ResourceManager.GetString("Command_Announce_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The text of the notice.. + /// + public static string Command_Announce_TextOption { + get { + return ResourceManager.GetString("Command_Announce_TextOption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bans the account of a character, which is online. For game masters.. + /// + public static string Command_Ban_Description { + get { + return ResourceManager.GetString("Command_Ban_Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Selects the character of your account, as which you appear.. /// @@ -546,6 +573,15 @@ public static string Command_GuildChat_Unbind_Description { } } + /// + /// Looks up a localized string similar to Disconnects a character from the game. For game masters.. + /// + public static string Command_Kick_Description { + get { + return ResourceManager.GetString("Command_Kick_Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to The code which you got in the game with /discord link.. /// @@ -564,6 +600,33 @@ public static string Command_Link_Description { } } + /// + /// Looks up a localized string similar to Turns direct messages about your game account on or off.. + /// + public static string Command_Notify_Description { + get { + return ResourceManager.GetString("Command_Notify_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to On or off.. + /// + public static string Command_Notify_EnabledOption { + get { + return ResourceManager.GetString("Command_Notify_EnabledOption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The kind of direct messages.. + /// + public static string Command_Notify_TypeOption { + get { + return ResourceManager.GetString("Command_Notify_TypeOption", resourceCulture); + } + } + /// /// Looks up a localized string similar to Shows how many players are online.. /// @@ -645,6 +708,60 @@ public static string Command_Who_Description { } } + /// + /// Looks up a localized string similar to Your friend {0} is online.. + /// + public static string Dm_FriendOnline { + get { + return ResourceManager.GetString("Dm_FriendOnline", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} received a letter from {1}: {2}. + /// + public static string Dm_LetterReceived { + get { + return ResourceManager.GetString("Dm_LetterReceived", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Somebody tried to log into your account on {0}, while you were logged in. If it wasn't you, change your password.. + /// + public static string Dm_LoginAttempt { + get { + return ResourceManager.GetString("Dm_LoginAttempt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The battle for the castle in the game. Guilds register for it in the game.. + /// + public static string Event_CastleSiege_Description { + get { + return ResourceManager.GetString("Event_CastleSiege_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Castle Siege. + /// + public static string Event_CastleSiege_Name { + get { + return ResourceManager.GetString("Event_CastleSiege_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to In the game. + /// + public static string Event_Location { + get { + return ResourceManager.GetString("Event_Location", resourceCulture); + } + } + /// /// Looks up a localized string similar to There are no events in the next 24 hours.. /// @@ -672,6 +789,78 @@ public static string GlobalNotice_Title { } } + /// + /// Looks up a localized string similar to The notice was sent: {0}. + /// + public static string Gm_Announced { + get { + return ResourceManager.GetString("Gm_Announced", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The notice was sent to {1} of {2} game servers: {0}. + /// + public static string Gm_AnnouncedPartially { + get { + return ResourceManager.GetString("Gm_AnnouncedPartially", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The notice can have at most {0} characters.. + /// + public static string Gm_AnnouncementTooLong { + get { + return ResourceManager.GetString("Gm_AnnouncementTooLong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The account of {0} was banned.. + /// + public static string Gm_Banned { + get { + return ResourceManager.GetString("Gm_Banned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} was disconnected.. + /// + public static string Gm_Kicked { + get { + return ResourceManager.GetString("Gm_Kicked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only game masters can use this command. You need the role GM, and a game master character of your linked account selected with /character.. + /// + public static string Gm_NotAllowed { + get { + return ResourceManager.GetString("Gm_NotAllowed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} isn't online.. + /// + public static string Gm_NotOnline { + get { + return ResourceManager.GetString("Gm_NotOnline", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Game master. + /// + public static string Gm_Title { + get { + return ResourceManager.GetString("Gm_Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to Guild master: {0}. /// @@ -861,6 +1050,51 @@ public static string MiniGameWon_Description { } } + /// + /// Looks up a localized string similar to You get direct messages about: {0}. + /// + public static string Notify_Changed { + get { + return ResourceManager.GetString("Notify_Changed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to nothing. + /// + public static string Notify_None { + get { + return ResourceManager.GetString("Notify_None", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Friends online. + /// + public static string Notify_Type_Friend { + get { + return ResourceManager.GetString("Notify_Type_Friend", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Letters. + /// + public static string Notify_Type_Letter { + get { + return ResourceManager.GetString("Notify_Type_Letter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login attempts. + /// + public static string Notify_Type_Login { + get { + return ResourceManager.GetString("Notify_Type_Login", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0}: **{1}** / {2}. /// @@ -996,6 +1230,24 @@ public static string Setup_Title { } } + /// + /// Looks up a localized string similar to {0} ({1}) used /{2} {3}. + /// + public static string StaffAlert_Text { + get { + return ResourceManager.GetString("StaffAlert_Text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Game master command. + /// + public static string StaffAlert_Title { + get { + return ResourceManager.GetString("StaffAlert_Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to No game server is running.. /// diff --git a/src/Discord/Properties/Resources.de.resx b/src/Discord/Properties/Resources.de.resx index 0c60795c4f..2825fc09ec 100644 --- a/src/Discord/Properties/Resources.de.resx +++ b/src/Discord/Properties/Resources.de.resx @@ -221,6 +221,15 @@ Du sendest zu schnell Nachrichten. Bitte warte einen Moment. + + Sendet eine Nachricht an alle Spieler im Spiel und in den Ankündigungskanal. Für Spielleiter. + + + Der Text der Nachricht. + + + Sperrt das Konto eines Charakters, der online ist. Für Spielleiter. + Wählt den Charakter deines Kontos, als der du erscheinst. @@ -260,12 +269,24 @@ Entfernt die Verbindung dieses Kanals mit dem Chat des Spiels. + + Trennt einen Charakter vom Spiel. Für Spielleiter. + Der Code, den du im Spiel mit /discord link erhalten hast. Verknüpft deinen Discord-Benutzer mit deinem Spielkonto. + + Schaltet Direktnachrichten über dein Spielkonto ein oder aus. + + + Ein oder aus. + + + Die Art der Direktnachrichten. + Zeigt, wie viele Spieler online sind. @@ -293,6 +314,24 @@ Zeigt Informationen über einen Charakter. + + Dein Freund {0} ist online. + + + {0} hat einen Brief von {1} erhalten: {2} + + + Jemand hat versucht, sich auf {0} in dein Konto einzuloggen, während du eingeloggt warst. Wenn du es nicht warst, ändere dein Passwort. + + + Der Kampf um die Burg im Spiel. Gilden melden sich im Spiel dafür an. + + + Burgbelagerung + + + Im Spiel + In den nächsten 24 Stunden gibt es keine Ereignisse. @@ -302,6 +341,30 @@ Ankündigung + + Die Nachricht wurde gesendet: {0} + + + Die Nachricht wurde an {1} von {2} Spielservern gesendet: {0} + + + Die Nachricht darf höchstens {0} Zeichen haben. + + + Das Konto von {0} wurde gesperrt. + + + {0} wurde getrennt. + + + Nur Spielleiter können diesen Befehl verwenden. Du brauchst die Rolle GM und einen mit /character gewählten Spielleiter-Charakter deines verknüpften Kontos. + + + {0} ist nicht online. + + + Spielleiter + Gildenmeister: {0} @@ -365,6 +428,21 @@ **{0}** hat {1} gewonnen! + + Du erhältst Direktnachrichten über: {0} + + + nichts + + + Freunde online + + + Briefe + + + Anmeldeversuche + {0}: **{1}** / {2} @@ -410,6 +488,12 @@ Einrichtung des Discord-Servers + + {0} ({1}) verwendete /{2} {3} + + + Spielleiter-Befehl + Es läuft kein Spielserver. diff --git a/src/Discord/Properties/Resources.resx b/src/Discord/Properties/Resources.resx index d52f7803ac..928b723ed1 100644 --- a/src/Discord/Properties/Resources.resx +++ b/src/Discord/Properties/Resources.resx @@ -221,6 +221,15 @@ You're sending messages too fast. Please wait a moment. + + Sends a notice to all players in the game and into the announcements channel. For game masters. + + + The text of the notice. + + + Bans the account of a character, which is online. For game masters. + Selects the character of your account, as which you appear. @@ -260,12 +269,24 @@ Removes the binding of this channel to the chat of the game. + + Disconnects a character from the game. For game masters. + The code which you got in the game with /discord link. Links your Discord user to your game account. + + Turns direct messages about your game account on or off. + + + On or off. + + + The kind of direct messages. + Shows how many players are online. @@ -293,6 +314,24 @@ Shows information about a character. + + Your friend {0} is online. + + + {0} received a letter from {1}: {2} + + + Somebody tried to log into your account on {0}, while you were logged in. If it wasn't you, change your password. + + + The battle for the castle in the game. Guilds register for it in the game. + + + Castle Siege + + + In the game + There are no events in the next 24 hours. @@ -302,6 +341,30 @@ Notice + + The notice was sent: {0} + + + The notice was sent to {1} of {2} game servers: {0} + + + The notice can have at most {0} characters. + + + The account of {0} was banned. + + + {0} was disconnected. + + + Only game masters can use this command. You need the role GM, and a game master character of your linked account selected with /character. + + + {0} isn't online. + + + Game master + Guild master: {0} @@ -365,6 +428,21 @@ **{0}** won {1}! + + You get direct messages about: {0} + + + nothing + + + Friends online + + + Letters + + + Login attempts + {0}: **{1}** / {2} @@ -410,6 +488,12 @@ Setup of the Discord server + + {0} ({1}) used /{2} {3} + + + Game master command + No game server is running. diff --git a/src/Discord/Provisioning/DefaultLayout.json b/src/Discord/Provisioning/DefaultLayout.json index 47db6bef63..b511d5f58a 100644 --- a/src/Discord/Provisioning/DefaultLayout.json +++ b/src/Discord/Provisioning/DefaultLayout.json @@ -35,7 +35,7 @@ "name": "Staff", "visibleTo": [ "gm" ], "channels": [ - { "key": "staff-alerts", "name": "staff-alerts", "isReadOnly": true }, + { "key": "staff-alerts", "name": "staff-alerts", "isReadOnly": true, "notifications": [ "StaffAlerts" ] }, { "key": "gm-commands", "name": "gm-commands" } ] } diff --git a/src/Discord/Provisioning/DiscordServerLayout.cs b/src/Discord/Provisioning/DiscordServerLayout.cs index 703c3fde05..9c9fef1b40 100644 --- a/src/Discord/Provisioning/DiscordServerLayout.cs +++ b/src/Discord/Provisioning/DiscordServerLayout.cs @@ -20,6 +20,11 @@ public sealed class DiscordServerLayout /// public const string LinkedRoleKey = "linked"; + /// + /// The key of the role of the game masters, which can use the commands for game masters. + /// + public const string GameMasterRoleKey = "gm"; + /// /// The key of the category in which the bot creates the channels for the chats of guilds. /// diff --git a/src/GameLogic/AccountLinking/AccountLinkService.cs b/src/GameLogic/AccountLinking/AccountLinkService.cs index 8525bf8557..0b321ab731 100644 --- a/src/GameLogic/AccountLinking/AccountLinkService.cs +++ b/src/GameLogic/AccountLinking/AccountLinkService.cs @@ -215,6 +215,50 @@ public async ValueTask SelectCharacterAsync(string pro return CharacterSelectionResult.Selected; } + /// + /// Turns a type of notifications on or off for a linked account. + /// + /// The identifier of the account. + /// The name of the external service. + /// The type of notifications. + /// The new state; or null, to toggle the current state. + /// The types of notifications which are on now; or null, if the account isn't linked. + public ValueTask SetNotificationAsync(Guid accountId, string provider, AccountNotificationTypes type, bool? enabled) + { + return this.SetNotificationAsync(context => context.GetAccountExternalLinkAsync(accountId, provider), type, enabled); + } + + /// + /// Turns a type of notifications on or off for a linked user. + /// + /// The name of the external service. + /// The identifier of the user in the external service. + /// The type of notifications. + /// The new state; or null, to toggle the current state. + /// The types of notifications which are on now; or null, if the user isn't linked. + public ValueTask SetNotificationByUserAsync(string provider, string externalUserId, AccountNotificationTypes type, bool? enabled) + { + return this.SetNotificationAsync(context => context.GetAccountExternalLinkByUserAsync(provider, externalUserId), type, enabled); + } + + private async ValueTask SetNotificationAsync(Func> getLink, AccountNotificationTypes type, bool? enabled) + { + using var context = this._createContext(); + if (await getLink(context).ConfigureAwait(false) is not { ExternalUserId: not null } link) + { + return null; + } + + if (type != AccountNotificationTypes.None) + { + var isEnabled = enabled ?? !link.Notifications.HasFlag(type); + link.Notifications = isEnabled ? link.Notifications | type : link.Notifications & ~type; + await context.SaveChangesAsync().ConfigureAwait(false); + } + + return link.Notifications; + } + /// /// Normalizes an entered code, so that it doesn't matter if it's entered in lower case or with separators. /// diff --git a/src/GameLogic/AccountLinking/AccountNotificationKeywords.cs b/src/GameLogic/AccountLinking/AccountNotificationKeywords.cs new file mode 100644 index 0000000000..204c0bc00b --- /dev/null +++ b/src/GameLogic/AccountLinking/AccountNotificationKeywords.cs @@ -0,0 +1,67 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.AccountLinking; + +using MUnique.OpenMU.DataModel.Entities; + +/// +/// The keywords of the , which are used in commands, e.g. /discord notify letter. +/// +public static class AccountNotificationKeywords +{ + /// + /// The keyword of . + /// + public const string LoginAttempt = "login"; + + /// + /// The keyword of . + /// + public const string LetterReceived = "letter"; + + /// + /// The keyword of . + /// + public const string FriendOnline = "friend"; + + /// + /// Gets the type of a keyword. + /// + /// The keyword. + /// The type; or , if the keyword is unknown. + public static AccountNotificationTypes Parse(string? keyword) => keyword?.ToLowerInvariant() switch + { + LoginAttempt => AccountNotificationTypes.LoginAttempt, + LetterReceived => AccountNotificationTypes.LetterReceived, + FriendOnline => AccountNotificationTypes.FriendOnline, + _ => AccountNotificationTypes.None, + }; + + /// + /// Formats the types as keywords, e.g. login, letter. + /// + /// The types. + /// The keywords; or -, if there are none. + public static string Format(AccountNotificationTypes types) + { + var keywords = new List(); + if (types.HasFlag(AccountNotificationTypes.LoginAttempt)) + { + keywords.Add(LoginAttempt); + } + + if (types.HasFlag(AccountNotificationTypes.LetterReceived)) + { + keywords.Add(LetterReceived); + } + + if (types.HasFlag(AccountNotificationTypes.FriendOnline)) + { + keywords.Add(FriendOnline); + } + + return keywords.Count > 0 ? string.Join(", ", keywords) : "-"; + } +} diff --git a/src/GameLogic/CastleSiege/CastleSiegeSchedule.cs b/src/GameLogic/CastleSiege/CastleSiegeSchedule.cs index 4dacee1cfe..0f216d22ad 100644 --- a/src/GameLogic/CastleSiege/CastleSiegeSchedule.cs +++ b/src/GameLogic/CastleSiege/CastleSiegeSchedule.cs @@ -1,4 +1,4 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // @@ -145,6 +145,32 @@ public CastleSiegeStatePeriod CreateEarlyStartPeriod(CastleSiegeState state, Dat /// /// The current state. /// The next configured state. + /// + /// Gets the next period of a state which starts now or later, e.g. the next battle. + /// + /// The state. + /// The current time, in UTC. + /// The next period; or null, if the schedule doesn't contain the state. + public CastleSiegeStatePeriod? GetNextPeriod(CastleSiegeState state, DateTime utcNow) + { + if (!this.ContainsState(state)) + { + return null; + } + + // Calculated in the week of the server time zone, so that a change of the daylight saving time doesn't shift it. + utcNow = EnsureUtc(utcNow); + var scheduledState = this.GetScheduledState(state); + var weekStart = this.GetLocalWeekStart(utcNow); + var startUtc = this.GetStartUtc(weekStart, scheduledState); + if (startUtc < utcNow) + { + startUtc = this.GetStartUtc(weekStart.AddDays(7), scheduledState); + } + + return this.CreatePeriod(state, startUtc); + } + public CastleSiegeState GetNextState(CastleSiegeState state) { var index = this.GetStateIndex(state); diff --git a/src/GameLogic/PlayerActions/LoginAction.cs b/src/GameLogic/PlayerActions/LoginAction.cs index c02dbfdf69..504f487c5f 100644 --- a/src/GameLogic/PlayerActions/LoginAction.cs +++ b/src/GameLogic/PlayerActions/LoginAction.cs @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions; using System.Threading; using MUnique.OpenMU.GameLogic.Views.Login; +using MUnique.OpenMU.Interfaces; /// /// Action to log in a player to the game. @@ -134,7 +135,7 @@ private async ValueTask ValidateAccountStateAsync(Player player, AccountSt { context.Allowed = false; await player.InvokeViewPlugInAsync(p => p.ShowLoginResultAsync(LoginResult.AccountAlreadyConnected)).ConfigureAwait(false); - await gameServerContext.EventPublisher.PlayerAlreadyLoggedInAsync(gameServerContext.Id, username).ConfigureAwait(false); + await NotifyAlreadyLoggedInAsync(gameServerContext, username).ConfigureAwait(false); return (false, null); } @@ -189,7 +190,7 @@ private async ValueTask ValidateAccountStateAsync(Player player, AccountSt { context.Allowed = false; await player.InvokeViewPlugInAsync(p => p.ShowLoginResultAsync(LoginResult.AccountAlreadyConnected)).ConfigureAwait(false); - await gameServerContext.EventPublisher.PlayerAlreadyLoggedInAsync(gameServerContext.Id, username).ConfigureAwait(false); + await NotifyAlreadyLoggedInAsync(gameServerContext, username).ConfigureAwait(false); return (false, null); } @@ -228,6 +229,15 @@ private async ValueTask ValidateAccountStateAsync(Player player, AccountSt } } + /// + /// Notifies the game servers and external systems, that somebody tried to log into an account which is already logged in. + /// + private static async ValueTask NotifyAlreadyLoggedInAsync(IGameServerContext gameServerContext, string username) + { + await gameServerContext.EventPublisher.PlayerAlreadyLoggedInAsync(gameServerContext.Id, username).ConfigureAwait(false); + await gameServerContext.EventPublisher.GameEventAsync(new AccountLoginBlockedEvent(gameServerContext.Id, DateTime.UtcNow, username)).ConfigureAwait(false); + } + private async ValueTask HandleAlreadyConnectedAsync(Player player, string username) { var result = player.LoginResultOverride ?? LoginResult.AccountAlreadyConnected; @@ -235,7 +245,7 @@ private async ValueTask HandleAlreadyConnectedAsync(Player player, string userna await player.InvokeViewPlugInAsync(p => p.ShowLoginResultAsync(result)).ConfigureAwait(false); if (player.GameContext is IGameServerContext gameServerContext) { - await gameServerContext.EventPublisher.PlayerAlreadyLoggedInAsync(gameServerContext.Id, username).ConfigureAwait(false); + await NotifyAlreadyLoggedInAsync(gameServerContext, username).ConfigureAwait(false); } } diff --git a/src/GameLogic/PlayerActions/Messenger/LetterSendAction.cs b/src/GameLogic/PlayerActions/Messenger/LetterSendAction.cs index 0b932f0880..964cf009ab 100644 --- a/src/GameLogic/PlayerActions/Messenger/LetterSendAction.cs +++ b/src/GameLogic/PlayerActions/Messenger/LetterSendAction.cs @@ -68,9 +68,12 @@ public async ValueTask SendLetterAsync(Player player, string receiver, string me } // Try to forward it to the player, if he is online - if ((player.GameContext as IGameServerContext)?.FriendServer is { } friendServer) + if (player.GameContext is IGameServerContext gameServerContext) { - await friendServer.ForwardLetterAsync(letter).ConfigureAwait(false); + await gameServerContext.FriendServer.ForwardLetterAsync(letter).ConfigureAwait(false); + + // E.g. the Discord bot can notify the receiver. + await gameServerContext.EventPublisher.GameEventAsync(new LetterReceivedEvent(gameServerContext.Id, DateTime.UtcNow, letter.ReceiverName ?? receiver, letter.SenderName ?? player.Name, letter.Subject ?? string.Empty)).ConfigureAwait(false); } } diff --git a/src/GameLogic/PlugIns/ChatCommands/Arguments/DiscordChatCommandArgs.cs b/src/GameLogic/PlugIns/ChatCommands/Arguments/DiscordChatCommandArgs.cs index dd4131d767..d3f894c8b2 100644 --- a/src/GameLogic/PlugIns/ChatCommands/Arguments/DiscordChatCommandArgs.cs +++ b/src/GameLogic/PlugIns/ChatCommands/Arguments/DiscordChatCommandArgs.cs @@ -4,6 +4,8 @@ namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands.Arguments; +using MUnique.OpenMU.GameLogic.AccountLinking; + /// /// Arguments for the discord chat command. /// @@ -19,10 +21,22 @@ public class DiscordChatCommandArgs : ArgumentsBase /// public const string UnlinkAction = "unlink"; + /// + /// The action which turns direct messages in Discord on or off. + /// + public const string NotifyAction = "notify"; + /// /// Gets or sets the action. Without action, the state of the link is shown. /// [Argument("action", false)] - [ValidValues(LinkAction, UnlinkAction)] + [ValidValues(LinkAction, UnlinkAction, NotifyAction)] public string? Action { get; set; } + + /// + /// Gets or sets the type of the direct messages, which is turned on or off by the . + /// + [Argument("type", false)] + [ValidValues(AccountNotificationKeywords.LoginAttempt, AccountNotificationKeywords.LetterReceived, AccountNotificationKeywords.FriendOnline)] + public string? Type { get; set; } } diff --git a/src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs b/src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs index a56e570406..bd9683cddc 100644 --- a/src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs +++ b/src/GameLogic/PlugIns/ChatCommands/DiscordChatCommandPlugIn.cs @@ -15,6 +15,7 @@ namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; /// 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. +/// /discord notify letter turns the direct messages about letters on or off; login and friend are the other types. /// [Guid("6694D50F-49B1-481D-8F65-B0D7DCCA77C3")] [PlugIn] @@ -50,6 +51,18 @@ protected override async ValueTask DoHandleCommandAsync(Player player, DiscordCh 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; + case DiscordChatCommandArgs.NotifyAction: + var type = AccountNotificationKeywords.Parse(arguments.Type); + if (await service.SetNotificationAsync(accountId, AccountLinkService.DiscordProvider, type, null).ConfigureAwait(false) is { } notifications) + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.DiscordNotifications), AccountNotificationKeywords.Format(notifications)).ConfigureAwait(false); + } + else + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.DiscordNotLinked)).ConfigureAwait(false); + } + break; default: var link = await service.GetLinkAsync(accountId, AccountLinkService.DiscordProvider).ConfigureAwait(false); diff --git a/src/GameLogic/PlugIns/GameEvents/GameEventPublisherPlugIn.cs b/src/GameLogic/PlugIns/GameEvents/GameEventPublisherPlugIn.cs index 9dd271d4b5..699596a90b 100644 --- a/src/GameLogic/PlugIns/GameEvents/GameEventPublisherPlugIn.cs +++ b/src/GameLogic/PlugIns/GameEvents/GameEventPublisherPlugIn.cs @@ -139,13 +139,17 @@ public ValueTask CastleSiegeStateChangedAsync(IGameContext gameContext, CastleSi return ValueTask.CompletedTask; } + var now = DateTime.UtcNow; + var nextBattle = castleSiege.Schedule.GetNextPeriod(CastleSiegeState.Start, now); return context.EventPublisher.GameEventAsync(new CastleSiegeStateChangedEvent( context.Id, - DateTime.UtcNow, + now, previousState.ToString(), castleSiege.CurrentState.ToString(), castleSiege.StateEndTimeUtc, - castleSiege.SiegeData?.OwnerGuildId)); + castleSiege.SiegeData?.OwnerGuildId, + nextBattle?.StartUtc, + nextBattle?.EndUtc)); } /// diff --git a/src/GameLogic/Properties/PlayerMessage.Designer.cs b/src/GameLogic/Properties/PlayerMessage.Designer.cs index b3473a6d36..82d56ac8c5 100644 --- a/src/GameLogic/Properties/PlayerMessage.Designer.cs +++ b/src/GameLogic/Properties/PlayerMessage.Designer.cs @@ -589,6 +589,15 @@ public static string DiscordLinked { } } + /// + /// Looks up a localized string similar to Direct messages in Discord about: {0}. Turn them on or off with /discord notify login, letter or friend.. + /// + public static string DiscordNotifications { + get { + return ResourceManager.GetString("DiscordNotifications", resourceCulture); + } + } + /// /// Looks up a localized string similar to Your account isn't linked to Discord. Use /discord link to get a code for linking.. /// diff --git a/src/GameLogic/Properties/PlayerMessage.de.resx b/src/GameLogic/Properties/PlayerMessage.de.resx index 1416822272..a2856d4458 100644 --- a/src/GameLogic/Properties/PlayerMessage.de.resx +++ b/src/GameLogic/Properties/PlayerMessage.de.resx @@ -1009,4 +1009,7 @@ Inventarerweiterungen: {18} Der Chat deiner Allianz wird nach Discord gespiegelt. Mitglieder, die ihren Discord-Benutzer verknüpft haben, können ihn dort lesen und schreiben. + + Direktnachrichten in Discord über: {0}. Mit /discord notify login, letter oder friend schaltest du sie ein oder aus. + diff --git a/src/GameLogic/Properties/PlayerMessage.resx b/src/GameLogic/Properties/PlayerMessage.resx index 2b12e6d7cf..3ff48001c2 100644 --- a/src/GameLogic/Properties/PlayerMessage.resx +++ b/src/GameLogic/Properties/PlayerMessage.resx @@ -1120,4 +1120,7 @@ Inventory Extensions: {18} The chat of your alliance is mirrored to Discord. Members who linked their Discord user can read and write it there. + + Direct messages in Discord about: {0}. Turn them on or off with /discord notify login, letter or friend. + diff --git a/src/GameLogic/Properties/PlugInResources.Designer.cs b/src/GameLogic/Properties/PlugInResources.Designer.cs index d2723e39c5..a63643e79c 100644 --- a/src/GameLogic/Properties/PlugInResources.Designer.cs +++ b/src/GameLogic/Properties/PlugInResources.Designer.cs @@ -700,7 +700,7 @@ 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.. + /// 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, '/discord notify letter' turns direct messages about letters on or off (also 'login' and 'friend').. /// public static string DiscordChatCommandPlugIn_Description { get { diff --git a/src/GameLogic/Properties/PlugInResources.de.resx b/src/GameLogic/Properties/PlugInResources.de.resx index 5954b89a3f..87a87fc472 100644 --- a/src/GameLogic/Properties/PlugInResources.de.resx +++ b/src/GameLogic/Properties/PlugInResources.de.resx @@ -1891,7 +1891,7 @@ 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. + Verknüpft das Konto mit einem Discord-Benutzer: '/discord link' erstellt einen Code, der in Discord eingegeben wird, '/discord unlink' entfernt die Verknüpfung, '/discord notify letter' schaltet Direktnachrichten über Briefe ein oder aus (auch 'login' und 'friend'). Befehl Weltchat diff --git a/src/GameLogic/Properties/PlugInResources.resx b/src/GameLogic/Properties/PlugInResources.resx index 0de4a9a374..51c05f6d7b 100644 --- a/src/GameLogic/Properties/PlugInResources.resx +++ b/src/GameLogic/Properties/PlugInResources.resx @@ -1698,7 +1698,7 @@ Discord command - Links the account to a Discord user: '/discord link' creates a code which is entered in Discord, '/discord unlink' removes the link. + Links the account to a Discord user: '/discord link' creates a code which is entered in Discord, '/discord unlink' removes the link, '/discord notify letter' turns direct messages about letters on or off (also 'login' and 'friend'). World chat command diff --git a/src/GameServer/GameServerContext.cs b/src/GameServer/GameServerContext.cs index a0104279d3..ddd01f9b75 100644 --- a/src/GameServer/GameServerContext.cs +++ b/src/GameServer/GameServerContext.cs @@ -265,6 +265,9 @@ private async ValueTask PlayerEnteredWorldAsync(Player player) } await this.EventPublisher.PlayerEnteredGameAsync(this.Id, selectedCharacter.Id, selectedCharacter.Name).ConfigureAwait(false); + + // E.g. the Discord bot can notify the friends of the player. + await this.EventPublisher.GameEventAsync(new PlayerEnteredGameEvent(this.Id, DateTime.UtcNow, selectedCharacter.Id, selectedCharacter.Name)).ConfigureAwait(false); } catch (Exception ex) { diff --git a/src/Interfaces/AccountLoginBlockedEvent.cs b/src/Interfaces/AccountLoginBlockedEvent.cs new file mode 100644 index 0000000000..3aa771fb2e --- /dev/null +++ b/src/Interfaces/AccountLoginBlockedEvent.cs @@ -0,0 +1,17 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Interfaces; + +/// +/// Somebody tried to log into an account with the correct password, while the account was already logged in. +/// +/// +/// Logins with a wrong password don't cause it; they don't reach the point where the account is known to be logged in. +/// +/// The identifier of the game server. +/// The timestamp, in UTC. +/// The login name of the account. +public sealed record AccountLoginBlockedEvent(byte ServerId, DateTime TimestampUtc, string LoginName) + : GameEvent(ServerId, TimestampUtc); diff --git a/src/Interfaces/CastleSiegeStateChangedEvent.cs b/src/Interfaces/CastleSiegeStateChangedEvent.cs index ab22d21782..fa28bf888d 100644 --- a/src/Interfaces/CastleSiegeStateChangedEvent.cs +++ b/src/Interfaces/CastleSiegeStateChangedEvent.cs @@ -16,5 +16,7 @@ namespace MUnique.OpenMU.Interfaces; /// The persistent identifier of the guild which owns the castle, if any. After the battle, it's the winner. /// The name can be retrieved with . /// -public sealed record CastleSiegeStateChangedEvent(byte ServerId, DateTime TimestampUtc, string PreviousState, string State, DateTime StateEndsAtUtc, Guid? OwnerGuildId) +/// The time, in UTC, when the next battle starts, if it's known. +/// The time, in UTC, when the next battle ends, if it's known. +public sealed record CastleSiegeStateChangedEvent(byte ServerId, DateTime TimestampUtc, string PreviousState, string State, DateTime StateEndsAtUtc, Guid? OwnerGuildId, DateTime? NextBattleStartUtc = null, DateTime? NextBattleEndUtc = null) : GameEvent(ServerId, TimestampUtc); diff --git a/src/Interfaces/GameEvent.cs b/src/Interfaces/GameEvent.cs index 92b78e195a..3569adda75 100644 --- a/src/Interfaces/GameEvent.cs +++ b/src/Interfaces/GameEvent.cs @@ -30,4 +30,7 @@ namespace MUnique.OpenMU.Interfaces; [JsonDerivedType(typeof(CharacterLevelMilestoneEvent), nameof(CharacterLevelMilestoneEvent))] [JsonDerivedType(typeof(AccountUnlinkedEvent), nameof(AccountUnlinkedEvent))] [JsonDerivedType(typeof(ChatMessageEvent), nameof(ChatMessageEvent))] +[JsonDerivedType(typeof(AccountLoginBlockedEvent), nameof(AccountLoginBlockedEvent))] +[JsonDerivedType(typeof(LetterReceivedEvent), nameof(LetterReceivedEvent))] +[JsonDerivedType(typeof(PlayerEnteredGameEvent), nameof(PlayerEnteredGameEvent))] public abstract record GameEvent(byte ServerId, DateTime TimestampUtc); diff --git a/src/Interfaces/LetterReceivedEvent.cs b/src/Interfaces/LetterReceivedEvent.cs new file mode 100644 index 0000000000..b13a700404 --- /dev/null +++ b/src/Interfaces/LetterReceivedEvent.cs @@ -0,0 +1,16 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Interfaces; + +/// +/// A character received a letter. +/// +/// The identifier of the game server. +/// The timestamp, in UTC. +/// The name of the character which received the letter. +/// The name of the character which sent the letter. +/// The subject of the letter. +public sealed record LetterReceivedEvent(byte ServerId, DateTime TimestampUtc, string ReceiverName, string SenderName, string Subject) + : GameEvent(ServerId, TimestampUtc); diff --git a/src/Interfaces/PlayerEnteredGameEvent.cs b/src/Interfaces/PlayerEnteredGameEvent.cs new file mode 100644 index 0000000000..1692fcb1a7 --- /dev/null +++ b/src/Interfaces/PlayerEnteredGameEvent.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; + +/// +/// A character entered the game. +/// +/// The identifier of the game server. +/// The timestamp, in UTC. +/// The identifier of the character. +/// The name of the character. +public sealed record PlayerEnteredGameEvent(byte ServerId, DateTime TimestampUtc, Guid CharacterId, string CharacterName) + : GameEvent(ServerId, TimestampUtc); diff --git a/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountContextModelBuilder.cs b/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountContextModelBuilder.cs index d63116b099..613f325ef3 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("0f82ffdc-4633-4fdb-b68c-5f1a7436f138"), entityTypeCount: 48) + : base(skipDetectChanges: false, modelId: new Guid("7a84b3b3-fd04-4359-b130-08013b01bc5b"), entityTypeCount: 48) { } diff --git a/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountExternalLinkEntityType.cs b/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountExternalLinkEntityType.cs index 5966cac863..0bd94dcee3 100644 --- a/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountExternalLinkEntityType.cs +++ b/src/Persistence/EntityFramework/CompiledModels/AccountContext/AccountExternalLinkEntityType.cs @@ -22,7 +22,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink", typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink), baseEntityType, - propertyCount: 9, + propertyCount: 10, foreignKeyCount: 1, unnamedIndexCount: 3, keyCount: 1); @@ -94,6 +94,14 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas nullable: true); linkedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + var notifications = runtimeEntityType.AddProperty( + "Notifications", + typeof(MUnique.OpenMU.DataModel.Entities.AccountNotificationTypes), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("Notifications", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + notifications.SetSentinelFromProviderValue(0); + notifications.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + var provider = runtimeEntityType.AddProperty( "Provider", typeof(string), diff --git a/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/AccountExternalLinkEntityType.cs b/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/AccountExternalLinkEntityType.cs index b210dc2ebb..173c2e9eba 100644 --- a/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/AccountExternalLinkEntityType.cs +++ b/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/AccountExternalLinkEntityType.cs @@ -22,7 +22,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink", typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink), baseEntityType, - propertyCount: 9, + propertyCount: 10, foreignKeyCount: 1, unnamedIndexCount: 3, keyCount: 1); @@ -94,6 +94,14 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas nullable: true); linkedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + var notifications = runtimeEntityType.AddProperty( + "Notifications", + typeof(MUnique.OpenMU.DataModel.Entities.AccountNotificationTypes), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("Notifications", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + notifications.SetSentinelFromProviderValue(0); + notifications.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + var provider = runtimeEntityType.AddProperty( "Provider", typeof(string), diff --git a/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/EntityDataContextModelBuilder.cs b/src/Persistence/EntityFramework/CompiledModels/EntityDataContext/EntityDataContextModelBuilder.cs index 159f190093..42a4453cce 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("e2795425-f3fb-4ca3-af47-e5d48685e5dc"), entityTypeCount: 123) + : base(skipDetectChanges: false, modelId: new Guid("3c3d6acf-c70b-44c8-b4f1-cf06d8de08b8"), entityTypeCount: 123) { } diff --git a/src/Persistence/EntityFramework/CompiledModels/TradeContext/AccountExternalLinkEntityType.cs b/src/Persistence/EntityFramework/CompiledModels/TradeContext/AccountExternalLinkEntityType.cs index 636c7fe828..d0b1a8d1e9 100644 --- a/src/Persistence/EntityFramework/CompiledModels/TradeContext/AccountExternalLinkEntityType.cs +++ b/src/Persistence/EntityFramework/CompiledModels/TradeContext/AccountExternalLinkEntityType.cs @@ -22,7 +22,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas "MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink", typeof(MUnique.OpenMU.Persistence.EntityFramework.Model.AccountExternalLink), baseEntityType, - propertyCount: 9, + propertyCount: 10, foreignKeyCount: 1, unnamedIndexCount: 3, keyCount: 1); @@ -94,6 +94,14 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas nullable: true); linkedAt.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + var notifications = runtimeEntityType.AddProperty( + "Notifications", + typeof(MUnique.OpenMU.DataModel.Entities.AccountNotificationTypes), + propertyInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetProperty("Notifications", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(MUnique.OpenMU.DataModel.Entities.AccountExternalLink).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + notifications.SetSentinelFromProviderValue(0); + notifications.AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.None); + var provider = runtimeEntityType.AddProperty( "Provider", typeof(string), diff --git a/src/Persistence/EntityFramework/CompiledModels/TradeContext/TradeContextModelBuilder.cs b/src/Persistence/EntityFramework/CompiledModels/TradeContext/TradeContextModelBuilder.cs index cb65bdcacc..f8e190649c 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("39bbdc13-ccd0-4c76-8fea-9289097beff5"), entityTypeCount: 47) + : base(skipDetectChanges: false, modelId: new Guid("0270667c-2566-4156-944b-07ee53f56a8b"), entityTypeCount: 47) { } diff --git a/src/Persistence/EntityFramework/Migrations/20261018143945_AddAccountNotifications.Designer.cs b/src/Persistence/EntityFramework/Migrations/20261018143945_AddAccountNotifications.Designer.cs new file mode 100644 index 0000000000..30dbfe1238 --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20261018143945_AddAccountNotifications.Designer.cs @@ -0,0 +1,6486 @@ +// +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("20261018143945_AddAccountNotifications")] + partial class AddAccountNotifications + { + /// + 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("Notifications") + .HasColumnType("integer"); + + 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.GuildChatBinding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("BoundBy") + .HasColumnType("text"); + + b.Property("ExternalChannelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalServerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("IsHosted") + .HasColumnType("boolean"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("Scope") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Provider", "ExternalChannelId") + .IsUnique(); + + b.HasIndex("GuildId", "Scope", "Provider") + .IsUnique(); + + b.ToTable("GuildChatBinding", "data"); + }); + + 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.GuildChatBinding", b => + { + b.HasOne("MUnique.OpenMU.Persistence.EntityFramework.Model.Guild", null) + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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/20261018143945_AddAccountNotifications.cs b/src/Persistence/EntityFramework/Migrations/20261018143945_AddAccountNotifications.cs new file mode 100644 index 0000000000..8fdab71a07 --- /dev/null +++ b/src/Persistence/EntityFramework/Migrations/20261018143945_AddAccountNotifications.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations +{ + /// + public partial class AddAccountNotifications : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Notifications", + schema: "data", + table: "AccountExternalLink", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Notifications", + schema: "data", + table: "AccountExternalLink"); + } + } +} diff --git a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs index 3e83c6b242..be7a29e7aa 100644 --- a/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs +++ b/src/Persistence/EntityFramework/Migrations/EntityDataContextModelSnapshot.cs @@ -145,6 +145,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LinkedAt") .HasColumnType("timestamp with time zone"); + b.Property("Notifications") + .HasColumnType("integer"); + b.Property("Provider") .IsRequired() .HasColumnType("text"); diff --git a/src/Persistence/EntityFramework/Model/BasicModelConverter.Generated.cs b/src/Persistence/EntityFramework/Model/BasicModelConverter.Generated.cs index a1be9309cd..87644df0b9 100644 --- a/src/Persistence/EntityFramework/Model/BasicModelConverter.Generated.cs +++ b/src/Persistence/EntityFramework/Model/BasicModelConverter.Generated.cs @@ -225,6 +225,7 @@ private BasicModel.AccountExternalLink Convert(AccountExternalLink source) target.ExternalUserName = source.ExternalUserName; target.CharacterName = source.CharacterName; target.LinkedAt = source.LinkedAt; + target.Notifications = source.Notifications; target.CodeHash = source.CodeHash; target.CodeExpiresAt = source.CodeExpiresAt; return target; diff --git a/src/Persistence/EntityFramework/PlayerContext.cs b/src/Persistence/EntityFramework/PlayerContext.cs index 970421936b..05ed34b8ab 100644 --- a/src/Persistence/EntityFramework/PlayerContext.cs +++ b/src/Persistence/EntityFramework/PlayerContext.cs @@ -307,6 +307,34 @@ public async ValueTask GetAccountStatisticsAsync(DateTime reg } } + /// + public async ValueTask GetAccountIdByLoginNameAsync(string loginName, CancellationToken cancellationToken = default) + { + using var l = await this.LockAsync(cancellationToken).ConfigureAwait(false); + using (this.RepositoryProvider.ContextStack.UseContext(this)) + { + return await this.Context.Set() + .Where(account => account.LoginName == loginName) + .Select(account => (Guid?)account.Id) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + } + } + + /// + public async ValueTask GetCharacterStatusAsync(string characterName, CancellationToken cancellationToken = default) + { + using var l = await this.LockAsync(cancellationToken).ConfigureAwait(false); + using (this.RepositoryProvider.ContextStack.UseContext(this)) + { + return await this.Context.Set() + .Where(character => character.Name == characterName) + .Select(character => (DataModel.Entities.CharacterStatus?)character.CharacterStatus) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + } + } + /// public async ValueTask GetAccountChatBanUntilAsync( Guid accountId, diff --git a/src/Persistence/IPlayerContext.cs b/src/Persistence/IPlayerContext.cs index d48099db8a..2ff9a2e596 100644 --- a/src/Persistence/IPlayerContext.cs +++ b/src/Persistence/IPlayerContext.cs @@ -170,6 +170,22 @@ ValueTask> GetLatestCashShopCoinGrantsAsync( string reference, CancellationToken cancellationToken = default); + /// + /// Gets the identifier of an account by its login name, without loading the account. + /// + /// The login name. + /// The cancellation token. + /// The identifier of the account; Otherwise, null, if the account doesn't exist. + ValueTask GetAccountIdByLoginNameAsync(string loginName, CancellationToken cancellationToken = default); + + /// + /// Gets the status of a character, e.g. if it's a game master, without loading the account. + /// + /// The name of the character. + /// The cancellation token. + /// The status; Otherwise, null, if the character doesn't exist. + ValueTask GetCharacterStatusAsync(string characterName, CancellationToken cancellationToken = default); + /// /// Gets the time until which the chat of an account is banned, without loading the account. /// diff --git a/src/Persistence/InMemory/PlayerInMemoryContext.cs b/src/Persistence/InMemory/PlayerInMemoryContext.cs index 4db1facda9..6993ea4e82 100644 --- a/src/Persistence/InMemory/PlayerInMemoryContext.cs +++ b/src/Persistence/InMemory/PlayerInMemoryContext.cs @@ -174,6 +174,24 @@ public async ValueTask CanSaveLetterAsync(Interfaces.LetterHeader letterHe return grants.FirstOrDefault(grant => grant.Reference == reference); } + /// + public async ValueTask GetAccountIdByLoginNameAsync(string loginName, CancellationToken cancellationToken = default) + { + var accounts = await this.Provider.GetRepository() + .GetAllAsync(cancellationToken) + .ConfigureAwait(false); + return accounts.FirstOrDefault(account => account.LoginName == loginName)?.Id; + } + + /// + public async ValueTask GetCharacterStatusAsync(string characterName, CancellationToken cancellationToken = default) + { + var characters = await this.Provider.GetRepository() + .GetAllAsync(cancellationToken) + .ConfigureAwait(false); + return characters.FirstOrDefault(character => character.Name == characterName)?.CharacterStatus; + } + /// public async ValueTask GetAccountChatBanUntilAsync( Guid accountId, diff --git a/tests/MUnique.OpenMU.Tests/CastleSiegeNextBattleTest.cs b/tests/MUnique.OpenMU.Tests/CastleSiegeNextBattleTest.cs new file mode 100644 index 0000000000..718f12cc90 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/CastleSiegeNextBattleTest.cs @@ -0,0 +1,76 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.GameLogic.CastleSiege; + +/// +/// Tests for , which tells when the next battle takes place. +/// +[TestFixture] +public class CastleSiegeNextBattleTest +{ + private static readonly CastleSiegeSchedule Schedule = new(new[] + { + new CastleSiegeStateScheduleEntry { State = CastleSiegeState.RegisterGuild, DayOfWeek = DayOfWeek.Monday, Hour = 0 }, + new CastleSiegeStateScheduleEntry { State = CastleSiegeState.Start, DayOfWeek = DayOfWeek.Saturday, Hour = 20 }, + new CastleSiegeStateScheduleEntry { State = CastleSiegeState.End, DayOfWeek = DayOfWeek.Saturday, Hour = 22 }, + }, TimeZoneInfo.Utc); + + /// + /// Tests that the battle of this week is the next one before it starts. + /// + [Test] + public void BattleOfThisWeekIsNext() + { + // Wednesday, 2026-10-07 + var period = Schedule.GetNextPeriod(CastleSiegeState.Start, new DateTime(2026, 10, 7, 12, 0, 0, DateTimeKind.Utc)); + + Assert.That(period?.StartUtc, Is.EqualTo(new DateTime(2026, 10, 10, 20, 0, 0, DateTimeKind.Utc))); + Assert.That(period?.EndUtc, Is.EqualTo(new DateTime(2026, 10, 10, 22, 0, 0, DateTimeKind.Utc))); + } + + /// + /// Tests that the battle of the next week is the next one, when the battle of this week already started. + /// + [Test] + public void BattleOfNextWeekIsNextAfterTheStart() + { + var period = Schedule.GetNextPeriod(CastleSiegeState.Start, new DateTime(2026, 10, 10, 21, 0, 0, DateTimeKind.Utc)); + + Assert.That(period?.StartUtc, Is.EqualTo(new DateTime(2026, 10, 17, 20, 0, 0, DateTimeKind.Utc))); + } + + /// + /// Tests that the schedule is in the server time zone. + /// + [Test] + public void BattleIsInServerTimeZone() + { + var berlin = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin"); + var schedule = new CastleSiegeSchedule( + new[] + { + new CastleSiegeStateScheduleEntry { State = CastleSiegeState.RegisterGuild, DayOfWeek = DayOfWeek.Monday, Hour = 0 }, + new CastleSiegeStateScheduleEntry { State = CastleSiegeState.Start, DayOfWeek = DayOfWeek.Saturday, Hour = 20 }, + new CastleSiegeStateScheduleEntry { State = CastleSiegeState.End, DayOfWeek = DayOfWeek.Saturday, Hour = 22 }, + }, + berlin); + + // Summer time (UTC+2) on 2026-10-10, winter time (UTC+1) on 2026-10-31. + Assert.That(schedule.GetNextPeriod(CastleSiegeState.Start, new DateTime(2026, 10, 7, 12, 0, 0, DateTimeKind.Utc))?.StartUtc, Is.EqualTo(new DateTime(2026, 10, 10, 18, 0, 0, DateTimeKind.Utc))); + Assert.That(schedule.GetNextPeriod(CastleSiegeState.Start, new DateTime(2026, 10, 28, 12, 0, 0, DateTimeKind.Utc))?.StartUtc, Is.EqualTo(new DateTime(2026, 10, 31, 19, 0, 0, DateTimeKind.Utc))); + } + + /// + /// Tests that there is no next period of a state which isn't scheduled. + /// + [Test] + public void UnscheduledStateHasNoPeriod() + { + Assert.That(Schedule.GetNextPeriod(CastleSiegeState.Notify, DateTime.UtcNow), Is.Null); + } +} diff --git a/tests/MUnique.OpenMU.Tests/Discord/DiscordBackgroundQueueTest.cs b/tests/MUnique.OpenMU.Tests/Discord/DiscordBackgroundQueueTest.cs new file mode 100644 index 0000000000..04e0c52f78 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/Discord/DiscordBackgroundQueueTest.cs @@ -0,0 +1,60 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests.Discord; + +using Microsoft.Extensions.Logging.Abstractions; +using MUnique.OpenMU.Discord; + +/// +/// Tests the bounded lifecycle of background notifications. +/// +[TestFixture] +public class DiscordBackgroundQueueTest +{ + /// + /// Tests shutdown before Ready, including pending work and a full queue. + /// + [Test] + public async Task ShutdownCancelsReadyWaitAndRejectsPendingWorkAsync() + { + var queue = new DiscordBackgroundQueue(NullLogger.Instance, 1); + var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var pendingRan = false; + Assert.That(queue.Enqueue(async cancellationToken => + { + started.SetResult(); + await ready.Task.WaitAsync(cancellationToken).ConfigureAwait(false); + }), Is.True); + await started.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); + Assert.That(queue.Enqueue(_ => + { + pendingRan = true; + return Task.CompletedTask; + }), Is.True); + Assert.That(queue.Enqueue(_ => Task.CompletedTask), Is.False); + await queue.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); + Assert.That(pendingRan, Is.False); + Assert.That(queue.Enqueue(_ => Task.CompletedTask), Is.False); + } + + /// + /// Tests that a failed notification does not stop later work. + /// + [TestCase(false)] + [TestCase(true)] + public async Task FailureDoesNotStopQueueAsync(bool operationTimedOut) + { + await using var queue = new DiscordBackgroundQueue(NullLogger.Instance); + var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + queue.Enqueue(_ => Task.FromException(operationTimedOut ? new OperationCanceledException("request timed out") : new InvalidOperationException("failed"))); + queue.Enqueue(_ => + { + completed.SetResult(); + return Task.CompletedTask; + }); + await completed.Task.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); + } +} diff --git a/tests/MUnique.OpenMU.Tests/Discord/DiscordDirectMessagesTest.cs b/tests/MUnique.OpenMU.Tests/Discord/DiscordDirectMessagesTest.cs new file mode 100644 index 0000000000..a6d4d0f6e0 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/Discord/DiscordDirectMessagesTest.cs @@ -0,0 +1,131 @@ +// +// 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 Moq; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.Discord; +using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.Interfaces; +using MUnique.OpenMU.Persistence; +using MUnique.OpenMU.Persistence.InMemory; + +/// +/// Tests for the . +/// +[TestFixture] +public class DiscordDirectMessagesTest +{ + private const ulong HeroUser = 42; + + private InMemoryPersistenceContextProvider _persistence = null!; + private AccountLinkService _linkService = null!; + private Mock _friendServer = null!; + private DiscordDirectMessages _directMessages = null!; + private Guid _elfId; + + /// + /// Sets up the linked account of Hero, who is a friend of Elf. + /// + [SetUp] + public async Task SetUpAsync() + { + this._persistence = new InMemoryPersistenceContextProvider(); + this._linkService = new AccountLinkService(this.CreateContext); + var heroAccountId = await this.CreateAccountAsync("hero", "Hero").ConfigureAwait(false); + await this.CreateAccountAsync("elf", "Elf").ConfigureAwait(false); + using (var context = this.CreateContext()) + { + this._elfId = (await context.GetAccountByCharacterNameAsync("Elf").ConfigureAwait(false))!.Characters.Single().GetId(); + } + + using (var friendContext = this._persistence.CreateNewFriendServerContext()) + { + await friendContext.CreateNewFriendAsync("Elf", "Hero").ConfigureAwait(false); + await friendContext.SaveChangesAsync().ConfigureAwait(false); + } + + var code = await this._linkService.CreateCodeAsync(heroAccountId, AccountLinkService.DiscordProvider, "Hero").ConfigureAwait(false); + await this._linkService.LinkAsync(AccountLinkService.DiscordProvider, code, HeroUser.ToString(CultureInfo.InvariantCulture), "hero").ConfigureAwait(false); + + this._friendServer = new Mock(); + this._friendServer.Setup(s => s.GetOnlineServerIdAsync("Elf")).ReturnsAsync((byte)0); + this._directMessages = new DiscordDirectMessages( + this._linkService, + this._friendServer.Object, + this.CreateContext, + this._persistence.CreateNewFriendServerContext, + _ => "Server 1", + CultureInfo.GetCultureInfo("en")); + } + + /// + /// Tests that there are no direct messages, until the user turns them on. + /// + [Test] + public async Task DirectMessagesAreOptInAsync() + { + var letter = new LetterReceivedEvent(0, DateTime.UtcNow, "Hero", "Elf", "Hi"); + Assert.That(await this._directMessages.GetMessagesAsync(letter).ConfigureAwait(false), Is.Empty); + + await this.TurnOnAsync(AccountNotificationTypes.LetterReceived).ConfigureAwait(false); + var messages = await this._directMessages.GetMessagesAsync(letter).ConfigureAwait(false); + + Assert.That(messages, Has.Count.EqualTo(1)); + Assert.That(messages[0].UserId, Is.EqualTo(HeroUser)); + Assert.That(messages[0].Embed.Description, Is.EqualTo("Hero received a letter from Elf: Hi")); + } + + /// + /// Tests the direct message about a blocked login. + /// + [Test] + public async Task LoginAttemptIsReportedAsync() + { + await this.TurnOnAsync(AccountNotificationTypes.LoginAttempt).ConfigureAwait(false); + + var messages = await this._directMessages.GetMessagesAsync(new AccountLoginBlockedEvent(0, DateTime.UtcNow, "hero")).ConfigureAwait(false); + + Assert.That(messages.Single().Embed.Description, Does.StartWith("Somebody tried to log into your account on Server 1")); + } + + /// + /// Tests that the friends of a character get a direct message when it enters the game, unless it's invisible. + /// + [Test] + public async Task FriendsAreNotifiedUnlessInvisibleAsync() + { + await this.TurnOnAsync(AccountNotificationTypes.FriendOnline).ConfigureAwait(false); + var entered = new PlayerEnteredGameEvent(0, DateTime.UtcNow, this._elfId, "Elf"); + + var messages = await this._directMessages.GetMessagesAsync(entered).ConfigureAwait(false); + this._friendServer.Setup(s => s.GetOnlineServerIdAsync("Elf")).ReturnsAsync((byte?)null); + var invisibleMessages = await this._directMessages.GetMessagesAsync(entered).ConfigureAwait(false); + + Assert.That(messages.Single().Embed.Description, Is.EqualTo("Your friend Elf is online.")); + Assert.That(invisibleMessages, Is.Empty); + } + + private Task TurnOnAsync(AccountNotificationTypes type) + { + return this._linkService.SetNotificationByUserAsync(AccountLinkService.DiscordProvider, HeroUser.ToString(CultureInfo.InvariantCulture), type, true).AsTask(); + } + + private IPlayerContext CreateContext() => this._persistence.CreateNewPlayerContext(new GameConfiguration()); + + private async Task CreateAccountAsync(string loginName, string characterName) + { + using var context = this.CreateContext(); + var account = context.CreateNew(); + account.LoginName = loginName; + var character = context.CreateNew(); + character.Name = characterName; + account.Characters.Add(character); + await context.SaveChangesAsync().ConfigureAwait(false); + return account.GetId(); + } +} diff --git a/tests/MUnique.OpenMU.Tests/Discord/DiscordGameMasterCommandsTest.cs b/tests/MUnique.OpenMU.Tests/Discord/DiscordGameMasterCommandsTest.cs new file mode 100644 index 0000000000..883f7eeca7 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/Discord/DiscordGameMasterCommandsTest.cs @@ -0,0 +1,161 @@ +// +// 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 Moq; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.Discord; +using MUnique.OpenMU.GameLogic.AccountLinking; +using MUnique.OpenMU.Interfaces; +using MUnique.OpenMU.Persistence; +using MUnique.OpenMU.Persistence.InMemory; + +/// +/// Tests for the . +/// +[TestFixture] +public class DiscordGameMasterCommandsTest +{ + private const ulong GameMasterUser = 42; + private const ulong PlayerUser = 43; + + private InMemoryPersistenceContextProvider _persistence = null!; + private Mock _server1 = null!; + private Mock _server2 = null!; + private DiscordGameMasterCommands _commands = null!; + + /// + /// Sets up a linked game master and a linked player, and two game servers. + /// + [SetUp] + public async Task SetUpAsync() + { + this._persistence = new InMemoryPersistenceContextProvider(); + var linkService = new AccountLinkService(this.CreateContext); + await this.CreateLinkedAccountAsync(linkService, "admin", "Admin", CharacterStatus.GameMaster, GameMasterUser).ConfigureAwait(false); + await this.CreateLinkedAccountAsync(linkService, "hero", "Hero", CharacterStatus.Normal, PlayerUser).ConfigureAwait(false); + + this._server1 = new Mock(); + this._server2 = new Mock(); + var serverProvider = new Mock(); + serverProvider.SetupGet(p => p.Servers).Returns(new List { this._server1.Object, this._server2.Object }); + this._commands = new DiscordGameMasterCommands(serverProvider.Object, linkService, this.CreateContext, CultureInfo.GetCultureInfo("en"), NullLogger.Instance); + } + + /// + /// Tests that a command needs the GM role and a game master character. + /// + [Test] + public async Task OnlyGameMastersCanUseCommandsAsync() + { + var withoutRole = await this._commands.ExecuteAsync("announce", "Hello", GameMasterUser, "admin", false).ConfigureAwait(false); + var withoutGameMaster = await this._commands.ExecuteAsync("announce", "Hello", PlayerUser, "hero", true).ConfigureAwait(false); + + Assert.That(withoutRole.StaffAlert, Is.Null); + Assert.That(withoutRole.Announcement, Is.Null); + Assert.That(withoutGameMaster.StaffAlert, Is.Null); + Assert.That(withoutGameMaster.Announcement, Is.Null); + Assert.That(withoutGameMaster.Embed.Description, Does.StartWith("Only game masters can use this command.")); + this._server1.Verify(s => s.SendGlobalMessageAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + /// + /// Tests that a notice is sent to all game servers and announced in Discord, and that the staff is alerted. + /// + [Test] + public async Task AnnounceSendsToAllGameServersAsync() + { + var answer = await this._commands.ExecuteAsync("announce", "Hello", GameMasterUser, "admin", true).ConfigureAwait(false); + + this._server1.Verify(s => s.SendGlobalMessageAsync("Hello", MessageType.GoldenCenter), Times.Once); + this._server2.Verify(s => s.SendGlobalMessageAsync("Hello", MessageType.GoldenCenter), Times.Once); + Assert.That(answer.StaffAlert?.Description, Is.EqualTo("admin (Admin) used /announce Hello")); + Assert.That(answer.Announcement?.Title, Is.EqualTo("Notice")); + Assert.That(answer.Announcement?.Description, Is.EqualTo("Hello")); + Assert.That(answer.Announcement?.Footer, Is.Null); + } + + /// + /// Tests that nothing is announced in Discord, when no game server got the notice. + /// + [Test] + public async Task NothingIsAnnouncedWhenNoGameServerGotTheNoticeAsync() + { + this._server1.Setup(s => s.SendGlobalMessageAsync(It.IsAny(), It.IsAny())).ThrowsAsync(new InvalidOperationException("Test")); + this._server2.Setup(s => s.SendGlobalMessageAsync(It.IsAny(), It.IsAny())).ThrowsAsync(new InvalidOperationException("Test")); + + var answer = await this._commands.ExecuteAsync("announce", "Hello", GameMasterUser, "admin", true).ConfigureAwait(false); + + Assert.That(answer.Announcement, Is.Null); + Assert.That(answer.Embed.Description, Is.EqualTo("The notice was sent to 0 of 2 game servers: Hello")); + } + + /// + /// Tests that a failing game server doesn't stop the announcement on the others, and that the game master learns about it. + /// + [Test] + public async Task FailingGameServerDoesNotStopAnnouncementAsync() + { + this._server1.Setup(s => s.SendGlobalMessageAsync(It.IsAny(), It.IsAny())).ThrowsAsync(new InvalidOperationException("Test")); + + var answer = await this._commands.ExecuteAsync("announce", "Hello", GameMasterUser, "admin", true).ConfigureAwait(false); + + this._server2.Verify(s => s.SendGlobalMessageAsync("Hello", MessageType.GoldenCenter), Times.Once); + Assert.That(answer.Embed.Description, Is.EqualTo("The notice was sent to 1 of 2 game servers: Hello")); + } + + /// + /// Tests that a too long announcement is rejected. + /// + [Test] + public async Task TooLongAnnouncementIsRejectedAsync() + { + var answer = await this._commands.ExecuteAsync("announce", new string('x', 201), GameMasterUser, "admin", true).ConfigureAwait(false); + + Assert.That(answer.StaffAlert, Is.Null); + Assert.That(answer.Embed.Description, Is.EqualTo("The notice can have at most 200 characters.")); + this._server1.Verify(s => s.SendGlobalMessageAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + /// + /// Tests that a character is disconnected from the game server on which it's online. + /// + [Test] + public async Task KickDisconnectsFromTheGameServerOfTheCharacterAsync() + { + this._server2.Setup(s => s.DisconnectPlayerAsync("Hero")).ReturnsAsync(true); + + var answer = await this._commands.ExecuteAsync("kick", "Hero", GameMasterUser, "admin", true).ConfigureAwait(false); + var offlineAnswer = await this._commands.ExecuteAsync("ban", "Nobody", GameMasterUser, "admin", true).ConfigureAwait(false); + + Assert.That(answer.Embed.Description, Is.EqualTo("Hero was disconnected.")); + Assert.That(offlineAnswer.Embed.Description, Is.EqualTo("Nobody isn't online.")); + Assert.That(offlineAnswer.StaffAlert, Is.Not.Null); + } + + private IPlayerContext CreateContext() => this._persistence.CreateNewPlayerContext(new GameConfiguration()); + + private async Task CreateLinkedAccountAsync(AccountLinkService linkService, string loginName, string characterName, CharacterStatus status, ulong userId) + { + Guid accountId; + using (var context = this.CreateContext()) + { + var account = context.CreateNew(); + account.LoginName = loginName; + var character = context.CreateNew(); + character.Name = characterName; + character.CharacterStatus = status; + account.Characters.Add(character); + await context.SaveChangesAsync().ConfigureAwait(false); + accountId = account.GetId(); + } + + var code = await linkService.CreateCodeAsync(accountId, AccountLinkService.DiscordProvider, characterName).ConfigureAwait(false); + await linkService.LinkAsync(AccountLinkService.DiscordProvider, code, userId.ToString(CultureInfo.InvariantCulture), loginName).ConfigureAwait(false); + } +} diff --git a/tests/MUnique.OpenMU.Tests/DiscordChatCommandPlugInTest.cs b/tests/MUnique.OpenMU.Tests/DiscordChatCommandPlugInTest.cs index 837d954ef4..f12f5f5224 100644 --- a/tests/MUnique.OpenMU.Tests/DiscordChatCommandPlugInTest.cs +++ b/tests/MUnique.OpenMU.Tests/DiscordChatCommandPlugInTest.cs @@ -37,6 +37,25 @@ public async Task LinkCreatesCodeAndUnlinkRemovesItAsync() Assert.That(await this.GetLinkAsync(player).ConfigureAwait(false), Is.Null); } + /// + /// Tests that /discord notify toggles the direct messages of a linked account. + /// + [Test] + public async Task NotifyTogglesDirectMessagesAsync() + { + var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + var plugIn = new DiscordChatCommandPlugIn(); + var service = new AccountLinkService(() => player.GameContext.PersistenceContextProvider.CreateNewPlayerContext(player.GameContext.Configuration)); + var code = await service.CreateCodeAsync(player.Account!.GetId(), AccountLinkService.DiscordProvider, null).ConfigureAwait(false); + await service.LinkAsync(AccountLinkService.DiscordProvider, code, "42", "hero").ConfigureAwait(false); + + await plugIn.HandleCommandAsync(player, "/discord notify letter").ConfigureAwait(false); + await plugIn.HandleCommandAsync(player, "/discord notify friend").ConfigureAwait(false); + await plugIn.HandleCommandAsync(player, "/discord notify letter").ConfigureAwait(false); + + Assert.That((await this.GetLinkAsync(player).ConfigureAwait(false))?.Notifications, Is.EqualTo(DataModel.Entities.AccountNotificationTypes.FriendOnline)); + } + private async Task GetLinkAsync(Player player) { using var context = player.GameContext.PersistenceContextProvider.CreateNewPlayerContext(player.GameContext.Configuration); diff --git a/tests/MUnique.OpenMU.Tests/GameEventPublisherPlugInTest.cs b/tests/MUnique.OpenMU.Tests/GameEventPublisherPlugInTest.cs index fed81453a7..6b82919f07 100644 --- a/tests/MUnique.OpenMU.Tests/GameEventPublisherPlugInTest.cs +++ b/tests/MUnique.OpenMU.Tests/GameEventPublisherPlugInTest.cs @@ -54,6 +54,10 @@ public class GameEventPublisherPlugInTest new CharacterLevelMilestoneEvent(1, DateTime.UtcNow, "Hero", "Blade Knight", 400, false), new AccountUnlinkedEvent(1, DateTime.UtcNow, "discord", "42"), new ChatMessageEvent(1, DateTime.UtcNow, GameChatChannel.Alliance, 7, "Hero", "Hello"), + new AccountLoginBlockedEvent(1, DateTime.UtcNow, "hero"), + new LetterReceivedEvent(1, DateTime.UtcNow, "Hero", "Elf", "Hello"), + new PlayerEnteredGameEvent(1, DateTime.UtcNow, Guid.NewGuid(), "Hero"), + new CastleSiegeStateChangedEvent(1, DateTime.UtcNow, "Notify", "Ready", DateTime.UtcNow.AddHours(1), null, DateTime.UtcNow.AddHours(1), DateTime.UtcNow.AddHours(3)), }; /// diff --git a/tests/MUnique.OpenMU.Web.Tests/Pages/LoggingConfigurationPageTests.cs b/tests/MUnique.OpenMU.Web.Tests/Pages/LoggingConfigurationPageTests.cs index 247d39e1b9..49f2bf8099 100644 --- a/tests/MUnique.OpenMU.Web.Tests/Pages/LoggingConfigurationPageTests.cs +++ b/tests/MUnique.OpenMU.Web.Tests/Pages/LoggingConfigurationPageTests.cs @@ -125,7 +125,8 @@ public void DoesNotSaveInvalidJson() cut.WaitForState(() => cut.FindAll("button.nav-link").Count == 2); cut.FindAll("button.nav-link")[1].Click(); - cut.WaitForElement("textarea").Change("{ \"MinimumLevel\": "); + cut.WaitForElement("textarea"); + cut.Find("textarea").Change("{ \"MinimumLevel\": "); cut.WaitForState(() => cut.Find("textarea").GetAttribute("value") == "{ \"MinimumLevel\": "); cut.FindAll("button").First(b => b.TextContent.Contains(Resources.Save)).Click();