diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..acbec22 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,25 @@ +name: ci + +on: + push: + branches: + - 'main' + +jobs: + docker: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Login to Docker Registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERUSER }} + password: ${{ secrets.DOCKERPASSWORD }} + registry: ${{ secrets.DOCKERREGISTRY }} + - name: Restore dependencies + run: dotnet restore + - name: Publish Website + run: dotnet publish Website/Website.csproj --os linux --arch x64 -c Release -p:PublishProfile=DefaultContainer -p:ContainerRegistry=${{ secrets.DOCKERREGISTRY }} -p:ContainerRepository=mecatol_archives_website + - name: Publish API + run: dotnet publish API/API.csproj --os linux --arch x64 -c Release -p:PublishProfile=DefaultContainer -p:ContainerRegistry=${{ secrets.DOCKERREGISTRY }} -p:ContainerRepository=mecatol_archives_api \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97f6a61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,367 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd +*.db +*.db-wal +WebAPI/bookofnuffle.db-shm +*.db-shm diff --git a/.idea/.idea.MecatolArchives/.idea/.gitignore b/.idea/.idea.MecatolArchives/.idea/.gitignore new file mode 100644 index 0000000..2c740ae --- /dev/null +++ b/.idea/.idea.MecatolArchives/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/modules.xml +/projectSettingsUpdater.xml +/.idea.MecatolArchives.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.MecatolArchives/.idea/indexLayout.xml b/.idea/.idea.MecatolArchives/.idea/indexLayout.xml new file mode 100644 index 0000000..1ead36c --- /dev/null +++ b/.idea/.idea.MecatolArchives/.idea/indexLayout.xml @@ -0,0 +1,10 @@ + + + + + .github + + + + + \ No newline at end of file diff --git a/.idea/.idea.MecatolArchives/.idea/vcs.xml b/.idea/.idea.MecatolArchives/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/.idea.MecatolArchives/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/API.Client/API.Client.csproj b/API.Client/API.Client.csproj new file mode 100644 index 0000000..33506a5 --- /dev/null +++ b/API.Client/API.Client.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/API.Client/Auth/BasicAuthHeaderProvider.cs b/API.Client/Auth/BasicAuthHeaderProvider.cs new file mode 100644 index 0000000..d354351 --- /dev/null +++ b/API.Client/Auth/BasicAuthHeaderProvider.cs @@ -0,0 +1,14 @@ +using System.Net.Http.Headers; +using System.Text; + +namespace Hesketh.MecatolArchives.API.Client.Auth; + +public class BasicAuthHeaderProvider(IAdminCredentialStore credentialStore) : IAuthHeaderProvider +{ + public async Task GetHeaderAsync() + { + var details = await credentialStore.GetDetailsAsync(); + return new AuthenticationHeaderValue("Basic", + Convert.ToBase64String(Encoding.ASCII.GetBytes($"{details.Username}:{details.Password}"))); + } +} \ No newline at end of file diff --git a/API.Client/Auth/IAdminCredentialStore.cs b/API.Client/Auth/IAdminCredentialStore.cs new file mode 100644 index 0000000..0843a20 --- /dev/null +++ b/API.Client/Auth/IAdminCredentialStore.cs @@ -0,0 +1,9 @@ +namespace Hesketh.MecatolArchives.API.Client.Auth; + +public interface IAdminCredentialStore +{ + Task SetDetailsAsync(string username, string password); + Task<(string Username, string Password)> GetDetailsAsync(); + Task AreDetailsSet(); + Task ResetAsync(); +} \ No newline at end of file diff --git a/API.Client/Auth/IAuthHeaderProvider.cs b/API.Client/Auth/IAuthHeaderProvider.cs new file mode 100644 index 0000000..389373a --- /dev/null +++ b/API.Client/Auth/IAuthHeaderProvider.cs @@ -0,0 +1,8 @@ +using System.Net.Http.Headers; + +namespace Hesketh.MecatolArchives.API.Client.Auth; + +public interface IAuthHeaderProvider +{ + Task GetHeaderAsync(); +} \ No newline at end of file diff --git a/API.Client/Clients/AdminClient.cs b/API.Client/Clients/AdminClient.cs new file mode 100644 index 0000000..a8fe351 --- /dev/null +++ b/API.Client/Clients/AdminClient.cs @@ -0,0 +1,23 @@ +using System.Net.Http.Json; +using Hesketh.MecatolArchives.API.Client.Auth; + +namespace Hesketh.MecatolArchives.API.Client.Clients; + +public sealed class AdminClient : Client +{ + public AdminClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider) + { + } + + public async Task Confirm() + { + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, "admin"); + requestMessage.Headers.Authorization = await AuthHeaderProvider.GetHeaderAsync(); + + using var response = await HttpClient.SendAsync(requestMessage); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadFromJsonAsync(); + return content; + } +} \ No newline at end of file diff --git a/API.Client/Clients/Client.cs b/API.Client/Clients/Client.cs new file mode 100644 index 0000000..ddeab2f --- /dev/null +++ b/API.Client/Clients/Client.cs @@ -0,0 +1,90 @@ +using System.Net.Http.Json; +using Hesketh.MecatolArchives.API.Client.Auth; +using Hesketh.MecatolArchives.API.Data; + +namespace Hesketh.MecatolArchives.API.Client.Clients; + +public abstract class Client +{ + protected Client(HttpClient client, IAuthHeaderProvider authHeaderProvider) + { + HttpClient = client; + AuthHeaderProvider = authHeaderProvider; + } + + protected HttpClient HttpClient { get; } + public IAuthHeaderProvider AuthHeaderProvider { get; } + + protected async Task PostAsync(string endpoint, TPost entity) where TTransfer : IEntity + { + using var requestMessage = new HttpRequestMessage(HttpMethod.Post, endpoint); + requestMessage.Headers.Authorization = await AuthHeaderProvider.GetHeaderAsync(); + requestMessage.Content = JsonContent.Create(entity); + + using var response = await HttpClient.SendAsync(requestMessage); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadFromJsonAsync() + ?? throw new InvalidOperationException("Expected valid JSON response from request"); + + return content; + } + + protected async Task PostAsync(string endpoint, TPostType content) + { + using var requestMessage = new HttpRequestMessage(HttpMethod.Post, endpoint); + requestMessage.Headers.Authorization = await AuthHeaderProvider.GetHeaderAsync(); + requestMessage.Content = JsonContent.Create(content); + + using var response = await HttpClient.SendAsync(requestMessage); + response.EnsureSuccessStatusCode(); + } + + protected async Task PutAsync(string endpoint, TPut entity) + where TTransfer : IEntity + where TPut : IEntity + { + using var requestMessage = new HttpRequestMessage(HttpMethod.Put, endpoint); + requestMessage.Headers.Authorization = await AuthHeaderProvider.GetHeaderAsync(); + requestMessage.Content = JsonContent.Create(entity); + + using var response = await HttpClient.SendAsync(requestMessage); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadFromJsonAsync() + ?? throw new InvalidOperationException("Expected valid JSON response from request"); + + return content; + } + + protected async Task DeleteAsync(string endpoint) + { + using var requestMessage = new HttpRequestMessage(HttpMethod.Delete, endpoint); + requestMessage.Headers.Authorization = await AuthHeaderProvider.GetHeaderAsync(); + + using var response = await HttpClient.SendAsync(requestMessage); + response.EnsureSuccessStatusCode(); + } + + protected async Task GetAsync(string endpoint) + { + using var response = await HttpClient.GetAsync(endpoint); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadFromJsonAsync() + ?? throw new InvalidOperationException("Expected valid JSON response from request"); + + return content; + } + + protected async Task> GetAllAsync(string endpoint) + { + using var response = await HttpClient.GetAsync(endpoint); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadFromJsonAsync>() + ?? throw new InvalidOperationException("Expected valid JSON response from request"); + + return content; + } +} \ No newline at end of file diff --git a/API.Client/Clients/ColourClient.cs b/API.Client/Clients/ColourClient.cs new file mode 100644 index 0000000..1140a19 --- /dev/null +++ b/API.Client/Clients/ColourClient.cs @@ -0,0 +1,12 @@ +using Hesketh.MecatolArchives.API.Client.Auth; +using Hesketh.MecatolArchives.API.Data; + +namespace Hesketh.MecatolArchives.API.Client.Clients; + +public sealed class ColourClient : EntityClient +{ + public ColourClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider, + "colours") + { + } +} \ No newline at end of file diff --git a/API.Client/Clients/EntityClient.cs b/API.Client/Clients/EntityClient.cs new file mode 100644 index 0000000..d829e4c --- /dev/null +++ b/API.Client/Clients/EntityClient.cs @@ -0,0 +1,47 @@ +using Hesketh.MecatolArchives.API.Client.Auth; +using Hesketh.MecatolArchives.API.Data; + +namespace Hesketh.MecatolArchives.API.Client.Clients; + +public class EntityClient : Client + where TTransfer : IEntity + where TPut : IEntity +{ + private readonly string _endpoint; + + public EntityClient(HttpClient client, IAuthHeaderProvider authHeaderProvider, string endpoint) : base(client, + authHeaderProvider) + { + _endpoint = endpoint; + } + + public async Task GetAsync(Guid identifier) + { + return await GetAsync($"{_endpoint}/{identifier}"); + } + + public async Task> GetAsync() + { + return await GetAllAsync(_endpoint); + } + + public async Task DeleteAsync(Guid identifier) + { + await DeleteAsync($"{_endpoint}/{identifier}"); + } + + public async Task DeleteAsync(TTransfer entity) + { + await DeleteAsync(entity.Identifier); + } + + public async Task UpdateAsync(TPut entity) + { + return await PutAsync(_endpoint, entity); + } + + public async Task CreateAsync(TPost entity) + { + return await PostAsync(_endpoint, entity); + } +} \ No newline at end of file diff --git a/API.Client/Clients/ExpansionClient.cs b/API.Client/Clients/ExpansionClient.cs new file mode 100644 index 0000000..03dd4e4 --- /dev/null +++ b/API.Client/Clients/ExpansionClient.cs @@ -0,0 +1,12 @@ +using Hesketh.MecatolArchives.API.Client.Auth; +using Hesketh.MecatolArchives.API.Data; + +namespace Hesketh.MecatolArchives.API.Client.Clients; + +public sealed class ExpansionClient : EntityClient +{ + public ExpansionClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider, + "expansions") + { + } +} \ No newline at end of file diff --git a/API.Client/Clients/FactionClient.cs b/API.Client/Clients/FactionClient.cs new file mode 100644 index 0000000..11bb900 --- /dev/null +++ b/API.Client/Clients/FactionClient.cs @@ -0,0 +1,12 @@ +using Hesketh.MecatolArchives.API.Client.Auth; +using Hesketh.MecatolArchives.API.Data; + +namespace Hesketh.MecatolArchives.API.Client.Clients; + +public sealed class FactionClient : EntityClient +{ + public FactionClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider, + "factions") + { + } +} \ No newline at end of file diff --git a/API.Client/Clients/PersonClient.cs b/API.Client/Clients/PersonClient.cs new file mode 100644 index 0000000..37b89ab --- /dev/null +++ b/API.Client/Clients/PersonClient.cs @@ -0,0 +1,12 @@ +using Hesketh.MecatolArchives.API.Client.Auth; +using Hesketh.MecatolArchives.API.Data; + +namespace Hesketh.MecatolArchives.API.Client.Clients; + +public sealed class PersonClient : EntityClient +{ + public PersonClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider, + "people") + { + } +} \ No newline at end of file diff --git a/API.Client/Clients/PlayClient.cs b/API.Client/Clients/PlayClient.cs new file mode 100644 index 0000000..d5d9376 --- /dev/null +++ b/API.Client/Clients/PlayClient.cs @@ -0,0 +1,24 @@ +using Hesketh.MecatolArchives.API.Client.Auth; +using Hesketh.MecatolArchives.API.Data; + +namespace Hesketh.MecatolArchives.API.Client.Clients +{ + public sealed class PlayClient : EntityClient + { + private const string Endpoint = "plays"; + + public PlayClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider, Endpoint) + { + } + + public async Task> GetPersonsPlaysAsync(Guid personIdentifier) + { + return await GetAsync>($"{Endpoint}/person={personIdentifier}"); + } + + public async Task> GetFactionsPlaysAsync(Guid factionIdentifier) + { + return await GetAsync>($"{Endpoint}/faction={factionIdentifier}"); + } + } +} diff --git a/API.Client/Clients/StatisticClient.cs b/API.Client/Clients/StatisticClient.cs new file mode 100644 index 0000000..34ee122 --- /dev/null +++ b/API.Client/Clients/StatisticClient.cs @@ -0,0 +1,32 @@ +using Hesketh.MecatolArchives.API.Client.Auth; +using Hesketh.MecatolArchives.API.Data; + +namespace Hesketh.MecatolArchives.API.Client.Clients; + +public sealed class StatisticClient : Client +{ + public StatisticClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider) + { + } + + public async Task GetFactionStatistics() + { + return await GetAsync("stats/factions"); + } + + public async Task GetPersonFactionStatistics(Guid personIdentifier) + { + return await GetAsync($"stats/factions/person={personIdentifier}"); + } + + public async Task GetPeopleStatistics() + { + return await GetAsync("stats/people"); + } + + public async Task GetFactionPeopleStatistics(Guid factionIdentifier) + { + return await GetAsync($"stats/people/faction={factionIdentifier}"); + } + +} \ No newline at end of file diff --git a/API.Client/Clients/VariantClient.cs b/API.Client/Clients/VariantClient.cs new file mode 100644 index 0000000..461d989 --- /dev/null +++ b/API.Client/Clients/VariantClient.cs @@ -0,0 +1,12 @@ +using Hesketh.MecatolArchives.API.Client.Auth; +using Hesketh.MecatolArchives.API.Data; + +namespace Hesketh.MecatolArchives.API.Client.Clients; + +public sealed class VariantClient : EntityClient +{ + public VariantClient(HttpClient client, IAuthHeaderProvider authHeaderProvider) : base(client, authHeaderProvider, + "variants") + { + } +} \ No newline at end of file diff --git a/API.Client/Extensions/ServiceCollectionExtensions.cs b/API.Client/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..f819094 --- /dev/null +++ b/API.Client/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,30 @@ +using Hesketh.MecatolArchives.API.Client.Auth; +using Hesketh.MecatolArchives.API.Client.Clients; +using Microsoft.Extensions.DependencyInjection; + +namespace Hesketh.MecatolArchives.API.Client.Extensions; + +public static class ServiceCollectionExtensions +{ + /// + /// The assumption is that you have already configured a IAdminCredentialStore + /// + /// The application services you are adding too + /// The address of the Mecatol Archives API Server + /// + public static IServiceCollection AddClients(this IServiceCollection serviceCollection, Uri apiAddress) + { + serviceCollection.AddTransient(); + + serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; }); + serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; }); + serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; }); + serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; }); + serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; }); + serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; }); + serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; }); + serviceCollection.AddHttpClient(client => { client.BaseAddress = apiAddress; }); + + return serviceCollection; + } +} \ No newline at end of file diff --git a/API.Client/Global.cs b/API.Client/Global.cs new file mode 100644 index 0000000..4104b20 --- /dev/null +++ b/API.Client/Global.cs @@ -0,0 +1,3 @@ +global using Get = Hesketh.MecatolArchives.API.Data; +global using Post = Hesketh.MecatolArchives.API.Data.Post; +global using Put = Hesketh.MecatolArchives.API.Data.Put; \ No newline at end of file diff --git a/API.Data/API.Data.csproj b/API.Data/API.Data.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/API.Data/API.Data.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/API.Data/Colour.cs b/API.Data/Colour.cs new file mode 100644 index 0000000..a0ee358 --- /dev/null +++ b/API.Data/Colour.cs @@ -0,0 +1,8 @@ +namespace Hesketh.MecatolArchives.API.Data; + +public sealed class Colour : IEntity +{ + public string Name { get; set; } = null!; + public string Hex { get; set; } = null!; + public Guid Identifier { get; set; } +} \ No newline at end of file diff --git a/API.Data/Expansion.cs b/API.Data/Expansion.cs new file mode 100644 index 0000000..64b0a5f --- /dev/null +++ b/API.Data/Expansion.cs @@ -0,0 +1,7 @@ +namespace Hesketh.MecatolArchives.API.Data; + +public sealed class Expansion : IEntity +{ + public string Name { get; set; } = null!; + public Guid Identifier { get; set; } +} \ No newline at end of file diff --git a/API.Data/Faction.cs b/API.Data/Faction.cs new file mode 100644 index 0000000..fb02971 --- /dev/null +++ b/API.Data/Faction.cs @@ -0,0 +1,7 @@ +namespace Hesketh.MecatolArchives.API.Data; + +public sealed class Faction : IEntity +{ + public string Name { get; set; } = null!; + public Guid Identifier { get; set; } +} \ No newline at end of file diff --git a/API.Data/IEntity.cs b/API.Data/IEntity.cs new file mode 100644 index 0000000..0ab935b --- /dev/null +++ b/API.Data/IEntity.cs @@ -0,0 +1,6 @@ +namespace Hesketh.MecatolArchives.API.Data; + +public interface IEntity +{ + public Guid Identifier { get; } +} \ No newline at end of file diff --git a/API.Data/Person.cs b/API.Data/Person.cs new file mode 100644 index 0000000..15d1ca9 --- /dev/null +++ b/API.Data/Person.cs @@ -0,0 +1,29 @@ +using System.Text; + +namespace Hesketh.MecatolArchives.API.Data; + +public sealed class Person : IEntity +{ + public string Name { get; set; } = null!; + public Guid Identifier { get; set; } + + public string Initials + { + get + { + if (string.IsNullOrEmpty(Name)) + return string.Empty; + + var sb = new StringBuilder(); + var split = Name.Split(' '); + + foreach (var item in split) + { + var firstCharacter = item[0]; + sb.Append(firstCharacter); + } + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/API.Data/Play.cs b/API.Data/Play.cs new file mode 100644 index 0000000..ed80c06 --- /dev/null +++ b/API.Data/Play.cs @@ -0,0 +1,14 @@ +namespace Hesketh.MecatolArchives.API.Data; + +public sealed class Play : IEntity +{ + public DateTime UtcDate { get; set; } = DateTime.UtcNow; + public double RulesVersion { get; set; } = 1.0; + public uint PointGoal { get; set; } = 10; + public string? Map { get; set; } = null; + + public ICollection Players { get; set; } = null!; + public ICollection Expansions { get; set; } = null!; + public ICollection Variants { get; set; } = null!; + public Guid Identifier { get; set; } +} \ No newline at end of file diff --git a/API.Data/Player.cs b/API.Data/Player.cs new file mode 100644 index 0000000..78075e5 --- /dev/null +++ b/API.Data/Player.cs @@ -0,0 +1,13 @@ +namespace Hesketh.MecatolArchives.API.Data; + +public sealed class Player : IEntity +{ + public uint Points { get; set; } = 0; + public bool Winner { get; set; } = false; + public bool Eliminated { get; set; } = false; + + public Person Person { get; set; } = null!; + public Faction Faction { get; set; } = null!; + public Colour Colour { get; set; } = null!; + public Guid Identifier { get; set; } +} \ No newline at end of file diff --git a/API.Data/Post/Colour.cs b/API.Data/Post/Colour.cs new file mode 100644 index 0000000..cba864d --- /dev/null +++ b/API.Data/Post/Colour.cs @@ -0,0 +1,7 @@ +namespace Hesketh.MecatolArchives.API.Data.Post; + +public sealed class Colour +{ + public string Name { get; set; } = null!; + public string Hex { get; set; } = null!; +} \ No newline at end of file diff --git a/API.Data/Post/Expansion.cs b/API.Data/Post/Expansion.cs new file mode 100644 index 0000000..c5e056c --- /dev/null +++ b/API.Data/Post/Expansion.cs @@ -0,0 +1,6 @@ +namespace Hesketh.MecatolArchives.API.Data.Post; + +public sealed class Expansion +{ + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/API.Data/Post/Faction.cs b/API.Data/Post/Faction.cs new file mode 100644 index 0000000..6c8d583 --- /dev/null +++ b/API.Data/Post/Faction.cs @@ -0,0 +1,6 @@ +namespace Hesketh.MecatolArchives.API.Data.Post; + +public sealed class Faction +{ + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/API.Data/Post/Person.cs b/API.Data/Post/Person.cs new file mode 100644 index 0000000..d79a92f --- /dev/null +++ b/API.Data/Post/Person.cs @@ -0,0 +1,6 @@ +namespace Hesketh.MecatolArchives.API.Data.Post; + +public sealed class Person +{ + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/API.Data/Post/Play.cs b/API.Data/Post/Play.cs new file mode 100644 index 0000000..d55dfb3 --- /dev/null +++ b/API.Data/Post/Play.cs @@ -0,0 +1,13 @@ +namespace Hesketh.MecatolArchives.API.Data.Post; + +public sealed class Play +{ + public DateTime UtcDate { get; set; } = DateTime.Now; + public double RulesVersion { get; set; } = 1.0; + public uint PointGoal { get; set; } = 10; + public string? Map { get; set; } = null; + + public ICollection VariantIdentifiers { get; set; } = new HashSet(); + public ICollection ExpansionIdentifiers { get; set; } = new HashSet(); + public ICollection Players { get; set; } = new HashSet(); +} \ No newline at end of file diff --git a/API.Data/Post/Player.cs b/API.Data/Post/Player.cs new file mode 100644 index 0000000..b8f3041 --- /dev/null +++ b/API.Data/Post/Player.cs @@ -0,0 +1,24 @@ +namespace Hesketh.MecatolArchives.API.Data.Post; + +public sealed class Player +{ + public Player() {} + public Player(Data.Player model) + { + Points = model.Points; + Winner = model.Winner; + Eliminated = model.Eliminated; + + PersonIdentifier = model.Person.Identifier; + FactionIdentifier = model.Faction.Identifier; + ColourIdentifier = model.Colour.Identifier; + } + + public uint Points { get; set; } + public bool Winner { get; set; } + public bool Eliminated { get; set; } + + public Guid PersonIdentifier { get; set; } + public Guid FactionIdentifier { get; set; } + public Guid ColourIdentifier { get; set; } +} \ No newline at end of file diff --git a/API.Data/Post/Variant.cs b/API.Data/Post/Variant.cs new file mode 100644 index 0000000..49abd7a --- /dev/null +++ b/API.Data/Post/Variant.cs @@ -0,0 +1,6 @@ +namespace Hesketh.MecatolArchives.API.Data.Post; + +public sealed class Variant +{ + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/API.Data/Put/Play.cs b/API.Data/Put/Play.cs new file mode 100644 index 0000000..a402c68 --- /dev/null +++ b/API.Data/Put/Play.cs @@ -0,0 +1,30 @@ +namespace Hesketh.MecatolArchives.API.Data.Put; + +public sealed class Play : IEntity +{ + public Play() {} + public Play(Data.Play model) + { + Identifier = model.Identifier; + + UtcDate = model.UtcDate; + RulesVersion = model.RulesVersion; + PointGoal = model.PointGoal; + Map = model.Map; + + VariantIdentifiers = new List(model.Variants.Select(x => x.Identifier)); + ExpansionIdentifiers = new List(model.Expansions.Select(x => x.Identifier)); + Players = new List(model.Players.Select(x => new Post.Player(x))); + } + + public Guid Identifier { get; set; } + + public DateTime UtcDate { get; set; } = DateTime.Now; + public double RulesVersion { get; set; } = 1.0; + public uint PointGoal { get; set; } = 10; + public string? Map { get; set; } + + public ICollection VariantIdentifiers { get; set; } = new HashSet(); + public ICollection ExpansionIdentifiers { get; set; } = new HashSet(); + public ICollection Players { get; set; } = new HashSet(); +} \ No newline at end of file diff --git a/API.Data/Statistic.cs b/API.Data/Statistic.cs new file mode 100644 index 0000000..26e3936 --- /dev/null +++ b/API.Data/Statistic.cs @@ -0,0 +1,23 @@ +namespace Hesketh.MecatolArchives.API.Data; + +public class Statistic +{ + public Guid? LinkedIdentifier { get; set; } + public string Name { get; } + public int Wins { get; set; } = 0; + public int Plays { get; set; } = 0; + public int Losses => Plays - Wins; + public double WinPercentage => Plays > 0 ? (double)Wins / Plays * 100 : 0; + + // The point percentage is the total percentage of Points earned in each game + // So if you earned 4/5 points in one game and 10/10 in another. That is 180% total + // But it is in the range 0...1 + public double PointPercentSum { get; set; } + public double PointPercentage => Plays > 0 ? (double)PointPercentSum / Plays * 100 : 0; + + public Statistic(string name, Guid? linkedIdentifier) + { + Name = name; + LinkedIdentifier = linkedIdentifier; + } +} \ No newline at end of file diff --git a/API.Data/Statistics.cs b/API.Data/Statistics.cs new file mode 100644 index 0000000..a0f1217 --- /dev/null +++ b/API.Data/Statistics.cs @@ -0,0 +1,16 @@ +namespace Hesketh.MecatolArchives.API.Data; + +public class Statistics +{ + public Statistic Overall { get; set; } = new("Overall", null); + public List Stats { get; set; } = new(); + + public void AddStatistic(Statistic statistic) + { + Stats.Add(statistic); + + Overall.Plays += statistic.Plays; + Overall.Wins += statistic.Wins; + Overall.PointPercentSum += statistic.PointPercentSum; + } +} diff --git a/API.Data/Variant.cs b/API.Data/Variant.cs new file mode 100644 index 0000000..f87a7b5 --- /dev/null +++ b/API.Data/Variant.cs @@ -0,0 +1,7 @@ +namespace Hesketh.MecatolArchives.API.Data; + +public sealed class Variant : IEntity +{ + public string Name { get; set; } = null!; + public Guid Identifier { get; set; } +} \ No newline at end of file diff --git a/API/API.csproj b/API/API.csproj new file mode 100644 index 0000000..39b3c4b --- /dev/null +++ b/API/API.csproj @@ -0,0 +1,44 @@ + + + + net8.0 + enable + enable + false + Linux + f29964b2-e609-453e-8e0c-5cf5fee26a49 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + .dockerignore + + + + diff --git a/API/Auth/AdminAccountOptions.cs b/API/Auth/AdminAccountOptions.cs new file mode 100644 index 0000000..22df083 --- /dev/null +++ b/API/Auth/AdminAccountOptions.cs @@ -0,0 +1,26 @@ +namespace Hesketh.MecatolArchives.API.Auth; + +public class AdminAccountOptions +{ + public const string SectionName = "Admin"; + + public AdminAccount? Account { get; set; } = null; + + public void Validate() + { + if (Account == null) + throw new InvalidOperationException("No admin account details specified"); + + if (string.IsNullOrWhiteSpace(Account.Username)) + throw new InvalidOperationException($"Admin account does not have a valid username"); + + if (string.IsNullOrEmpty(Account.Username)) + throw new InvalidOperationException($"Admin account does not have a valid password"); + } +} + +public sealed class AdminAccount +{ + public string Username { get; set; } = null!; + public string Password { get; set; } = null!; +} \ No newline at end of file diff --git a/API/Auth/BasicAuthenticationHandler.cs b/API/Auth/BasicAuthenticationHandler.cs new file mode 100644 index 0000000..38219e1 --- /dev/null +++ b/API/Auth/BasicAuthenticationHandler.cs @@ -0,0 +1,45 @@ +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; + +namespace Hesketh.MecatolArchives.API.Auth; + +public class BasicAuthenticationHandler : AuthenticationHandler +{ + private readonly IBasicAuthenticationService _authenticationService; + + public BasicAuthenticationHandler( + IBasicAuthenticationService authenticationService, + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) : + base(options, logger, encoder) + { + _authenticationService = authenticationService; + } + + protected override async Task HandleAuthenticateAsync() + { + var authorizationHeader = Request.Headers["Authorization"].ToString(); + if (authorizationHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase)) + { + var token = authorizationHeader.Substring("Basic ".Length).Trim(); + var credentialsAsEncodedString = Encoding.UTF8.GetString(Convert.FromBase64String(token)); + var credentials = credentialsAsEncodedString.Split(':'); + if (_authenticationService.Authenticate(credentials[0], credentials[1])) + { + var claims = new[] { new Claim("name", credentials[0]), new Claim(ClaimTypes.Role, "Admin") }; + var identity = new ClaimsIdentity(claims, "Basic"); + var claimsPrincipal = new ClaimsPrincipal(identity); + return await Task.FromResult( + AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, Scheme.Name))); + } + } + + Response.StatusCode = 401; + Response.Headers.Append("WWW-Authenticate", "Basic realm=\"joydipkanjilal.com\""); + return await Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); + } +} \ No newline at end of file diff --git a/API/Auth/BasicAuthenticationService.cs b/API/Auth/BasicAuthenticationService.cs new file mode 100644 index 0000000..58820c5 --- /dev/null +++ b/API/Auth/BasicAuthenticationService.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Options; + +namespace Hesketh.MecatolArchives.API.Auth; + +public class BasicAuthenticationService : IBasicAuthenticationService +{ + private readonly AdminAccountOptions _adminAccountOptions; + + public BasicAuthenticationService(IOptions adminAccountOptions) + { + _adminAccountOptions = adminAccountOptions.Value; + _adminAccountOptions.Validate(); + } + + public bool Authenticate(string username, string password) + { + if (_adminAccountOptions.Account == null) + return false; + + if (!_adminAccountOptions.Account.Username.Equals(username, StringComparison.InvariantCultureIgnoreCase)) + return false; + + if (!_adminAccountOptions.Account.Password.Equals(password, StringComparison.InvariantCulture)) + return false; + + return true; + } +} \ No newline at end of file diff --git a/API/Auth/IBasicAuthenticationService.cs b/API/Auth/IBasicAuthenticationService.cs new file mode 100644 index 0000000..e3d4a44 --- /dev/null +++ b/API/Auth/IBasicAuthenticationService.cs @@ -0,0 +1,6 @@ +namespace Hesketh.MecatolArchives.API.Auth; + +public interface IBasicAuthenticationService +{ + bool Authenticate(string username, string password); +} \ No newline at end of file diff --git a/API/Controllers/AdminController.cs b/API/Controllers/AdminController.cs new file mode 100644 index 0000000..fe33232 --- /dev/null +++ b/API/Controllers/AdminController.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Hesketh.MecatolArchives.API.Controllers; + +[ApiController] +[Route("admin")] +[Authorize] +public sealed class AdminController : ControllerBase +{ + [HttpGet] + public ActionResult ConfirmAsync() + { + return Ok(true); + } +} \ No newline at end of file diff --git a/API/Controllers/ColourController.cs b/API/Controllers/ColourController.cs new file mode 100644 index 0000000..15841a5 --- /dev/null +++ b/API/Controllers/ColourController.cs @@ -0,0 +1,53 @@ +using AutoMapper; +using Hesketh.MecatolArchives.API.Helpers; +using Hesketh.MecatolArchives.DB; +using Hesketh.MecatolArchives.DB.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Hesketh.MecatolArchives.API.Controllers; + +[ApiController] +[Route("colours")] +[Authorize] +public sealed class ColourController : ControllerBase +{ + private readonly CrudControllerHelper _crud; + + public ColourController(MecatolArchivesDbContext db, IMapper mapper) + { + _crud = new CrudControllerHelper(db, mapper); + } + + [HttpGet("{identifier}")] + [AllowAnonymous] + public async Task> Get(Guid identifier) + { + return await _crud.GetAsync(identifier); + } + + [HttpGet] + [AllowAnonymous] + public async Task>> Get() + { + return await _crud.GetAsync(); + } + + [HttpPost] + public async Task> Post(Data.Post.Colour model) + { + return await _crud.PostAsync(model); + } + + [HttpPut] + public async Task> Put(Data.Colour model) + { + return await _crud.PutAsync(model); + } + + [HttpDelete("{identifier}")] + public async Task Delete(Guid identifier) + { + return await _crud.DeleteAsync(identifier); + } +} \ No newline at end of file diff --git a/API/Controllers/ExpansionController.cs b/API/Controllers/ExpansionController.cs new file mode 100644 index 0000000..61dc4d5 --- /dev/null +++ b/API/Controllers/ExpansionController.cs @@ -0,0 +1,53 @@ +using AutoMapper; +using Hesketh.MecatolArchives.API.Helpers; +using Hesketh.MecatolArchives.DB; +using Hesketh.MecatolArchives.DB.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Hesketh.MecatolArchives.API.Controllers; + +[ApiController] +[Route("expansions")] +[Authorize] +public sealed class ExpansionController : ControllerBase +{ + private readonly CrudControllerHelper _crud; + + public ExpansionController(MecatolArchivesDbContext db, IMapper mapper) + { + _crud = new CrudControllerHelper(db, mapper); + } + + [HttpGet("{identifier}")] + [AllowAnonymous] + public async Task> Get(Guid identifier) + { + return await _crud.GetAsync(identifier); + } + + [HttpGet] + [AllowAnonymous] + public async Task>> Get() + { + return await _crud.GetAsync(); + } + + [HttpPost] + public async Task> Post(Data.Post.Expansion model) + { + return await _crud.PostAsync(model); + } + + [HttpPut] + public async Task> Put(Data.Expansion model) + { + return await _crud.PutAsync(model); + } + + [HttpDelete("{identifier}")] + public async Task Delete(Guid identifier) + { + return await _crud.DeleteAsync(identifier); + } +} \ No newline at end of file diff --git a/API/Controllers/FactionController.cs b/API/Controllers/FactionController.cs new file mode 100644 index 0000000..b26e721 --- /dev/null +++ b/API/Controllers/FactionController.cs @@ -0,0 +1,57 @@ +using AutoMapper; +using Hesketh.MecatolArchives.API.Helpers; +using Hesketh.MecatolArchives.DB; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Faction = Hesketh.MecatolArchives.DB.Models.Faction; + +namespace Hesketh.MecatolArchives.API.Controllers; + +[ApiController] +[Route("factions")] +[Authorize] +public sealed class FactionController : ControllerBase +{ + private readonly MecatolArchivesDbContext _db; + private readonly IMapper _mapper; + private readonly CrudControllerHelper _crud; + + public FactionController(MecatolArchivesDbContext db, IMapper mapper) + { + _db = db; + _mapper = mapper; + _crud = new CrudControllerHelper(db, mapper); + } + + [HttpGet("{identifier}")] + [AllowAnonymous] + public async Task> Get(Guid identifier) + { + return await _crud.GetAsync(identifier); + } + + [HttpGet] + [AllowAnonymous] + public async Task>> Get() + { + return await _crud.GetAsync(); + } + + [HttpPost] + public async Task> Post(Data.Post.Faction model) + { + return await _crud.PostAsync(model); + } + + [HttpPut] + public async Task> Put(Data.Faction model) + { + return await _crud.PutAsync(model); + } + + [HttpDelete("{identifier}")] + public async Task Delete(Guid identifier) + { + return await _crud.DeleteAsync(identifier); + } +} \ No newline at end of file diff --git a/API/Controllers/PersonController.cs b/API/Controllers/PersonController.cs new file mode 100644 index 0000000..c328d6d --- /dev/null +++ b/API/Controllers/PersonController.cs @@ -0,0 +1,53 @@ +using AutoMapper; +using Hesketh.MecatolArchives.API.Helpers; +using Hesketh.MecatolArchives.DB; +using Hesketh.MecatolArchives.DB.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Hesketh.MecatolArchives.API.Controllers; + +[ApiController] +[Route("people")] +[Authorize] +public sealed class PersonController : ControllerBase +{ + private readonly CrudControllerHelper _crud; + + public PersonController(MecatolArchivesDbContext db, IMapper mapper) + { + _crud = new CrudControllerHelper(db, mapper); + } + + [HttpGet("{identifier}")] + [AllowAnonymous] + public async Task> Get(Guid identifier) + { + return await _crud.GetAsync(identifier); + } + + [HttpGet] + [AllowAnonymous] + public async Task>> Get() + { + return await _crud.GetAsync(); + } + + [HttpPost] + public async Task> Post(Data.Post.Person model) + { + return await _crud.PostAsync(model); + } + + [HttpPut] + public async Task> Put(Data.Person model) + { + return await _crud.PutAsync(model); + } + + [HttpDelete("{identifier}")] + public async Task Delete(Guid identifier) + { + return await _crud.DeleteAsync(identifier); + } +} \ No newline at end of file diff --git a/API/Controllers/PlayController.cs b/API/Controllers/PlayController.cs new file mode 100644 index 0000000..008d39a --- /dev/null +++ b/API/Controllers/PlayController.cs @@ -0,0 +1,198 @@ +using AutoMapper; +using Hesketh.MecatolArchives.API.Helpers; +using Hesketh.MecatolArchives.DB; +using Hesketh.MecatolArchives.DB.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Hesketh.MecatolArchives.API.Controllers; + +[ApiController] +[Route("plays")] +[Authorize] +public sealed class PlayController : ControllerBase +{ + private readonly MecatolArchivesDbContext _db; + private readonly IMapper _mapper; + private readonly CrudControllerHelper _crud; + + public PlayController(MecatolArchivesDbContext db, IMapper mapper) + { + _db = db; + _mapper = mapper; + _crud = new CrudControllerHelper(db, mapper); + } + + [HttpGet("{identifier}")] + [AllowAnonymous] + public async Task> Get(Guid identifier) + { + var res = await _db.GetDbSet() + .Include(x => x.Players).ThenInclude(x => x.Person) + .Include(x => x.Players).ThenInclude(x => x.Faction) + .Include(x => x.Players).ThenInclude(x => x.Colour) + .Include(x => x.Variants) + .Include(x => x.Expansions) + .FirstOrDefaultAsync(x => x.Identifier == identifier); + var mapped = _mapper.Map(res); + return Ok(mapped); + } + + [HttpGet] + [AllowAnonymous] + public async Task>> Get() + { + var res = await _db.GetDbSet() + .OrderByDescending(x => x.UtcDate) + .Include(x => x.Players).ThenInclude(x => x.Person) + .Include(x => x.Players).ThenInclude(x => x.Faction) + .Include(x => x.Players).ThenInclude(x => x.Colour) + .Include(x => x.Variants) + .Include(x => x.Expansions) + .ToListAsync(); + var mapped = _mapper.Map>(res); + return Ok(mapped); + } + + [HttpGet("person={personIdentifier}")] + [AllowAnonymous] + public async Task>> GetWithPlayer(Guid personIdentifier) + { + var res = await _db.GetDbSet() + .OrderByDescending(x => x.UtcDate) + .Include(x => x.Players).ThenInclude(x => x.Person) + .Include(x => x.Players).ThenInclude(x => x.Faction) + .Include(x => x.Players).ThenInclude(x => x.Colour) + .Include(x => x.Variants) + .Include(x => x.Expansions) + .Where(x => x.Players.Any(y => y.Person.Identifier == personIdentifier)) + .ToListAsync(); + var mapped = _mapper.Map>(res); + return Ok(mapped); + } + + [HttpGet("faction={factionIdentifier}")] + [AllowAnonymous] + public async Task>> GetWithFaction(Guid factionIdentifier) + { + var res = await _db.GetDbSet() + .OrderByDescending(x => x.UtcDate) + .Include(x => x.Players).ThenInclude(x => x.Person) + .Include(x => x.Players).ThenInclude(x => x.Faction) + .Include(x => x.Players).ThenInclude(x => x.Colour) + .Include(x => x.Variants) + .Include(x => x.Expansions) + .Where(x => x.Players.Any(y => y.Faction.Identifier == factionIdentifier)) + .ToListAsync(); + var mapped = _mapper.Map>(res); + return Ok(mapped); + } + + [HttpPost] + public async Task> Post(Data.Post.Play model) + { + var entity = _mapper.Map(model); + + entity.Variants = await _db.Variants.Where(x => model.VariantIdentifiers.Contains(x.Identifier)).ToListAsync(); + if (entity.Variants.Count != model.VariantIdentifiers.Count) + return NotFound("Variants could not be located"); + + entity.Expansions = await _db.Expansions.Where(x => model.ExpansionIdentifiers.Contains(x.Identifier)).ToListAsync(); + if (entity.Expansions.Count != model.ExpansionIdentifiers.Count) + return NotFound("Expansions could not be located"); + + entity.Players = new List(); + foreach (var playerPost in model.Players) + { + var playerEntity = _mapper.Map(playerPost); + + var person = await _db.People.FirstOrDefaultAsync(x => x.Identifier == playerPost.PersonIdentifier); + if (person == null) + return NotFound($"Player person could not be found"); + + var faction = await _db.Factions.FirstOrDefaultAsync(x => x.Identifier == playerPost.FactionIdentifier); + if (faction == null) + return NotFound($"Player person could not be found"); + + var colour = await _db.Colours.FirstOrDefaultAsync(x => x.Identifier == playerPost.ColourIdentifier); + if (colour == null) + return NotFound($"Player colour could not be found"); + + playerEntity.Person = person; + playerEntity.Faction = faction; + playerEntity.Colour = colour; + + entity.Players.Add(playerEntity); + playerEntity.Play = entity; + await _db.GetDbSet().AddAsync(playerEntity); + } + + await _db.GetDbSet().AddAsync(entity); + await _db.SaveChangesAsync(); + + var mapped = _mapper.Map(entity); + return Ok(mapped); + } + + [HttpPut] + public async Task> Put(Data.Put.Play model) + { + var existingEntity = await _db.GetDbSet() + .Include(x => x.Players).ThenInclude(x => x.Person) + .Include(x => x.Players).ThenInclude(x => x.Faction) + .Include(x => x.Players).ThenInclude(x => x.Colour) + .Include(x => x.Variants) + .Include(x => x.Expansions) + .FirstOrDefaultAsync(x => x.Identifier == model.Identifier); + if (existingEntity == null) + return NotFound("Play could not be located"); + + var entity = _mapper.Map(model, existingEntity); + + entity.Variants = await _db.Variants.Where(x => model.VariantIdentifiers.Contains(x.Identifier)).ToListAsync(); + if (entity.Variants.Count != model.VariantIdentifiers.Count) + return NotFound("Variants could not be located"); + + entity.Expansions = await _db.Expansions.Where(x => model.ExpansionIdentifiers.Contains(x.Identifier)).ToListAsync(); + if (entity.Expansions.Count != model.ExpansionIdentifiers.Count) + return NotFound("Expansions could not be located"); + + entity.Players.Clear(); + foreach (var playerPost in model.Players) + { + var playerEntity = _mapper.Map(playerPost); + + var person = await _db.People.FirstOrDefaultAsync(x => x.Identifier == playerPost.PersonIdentifier); + if (person == null) + return NotFound($"Player person could not be found"); + + var faction = await _db.Factions.FirstOrDefaultAsync(x => x.Identifier == playerPost.FactionIdentifier); + if (faction == null) + return NotFound($"Player person could not be found"); + + var colour = await _db.Colours.FirstOrDefaultAsync(x => x.Identifier == playerPost.ColourIdentifier); + if (colour == null) + return NotFound($"Player colour could not be found"); + + playerEntity.Person = person; + playerEntity.Faction = faction; + playerEntity.Colour = colour; + + entity.Players.Add(playerEntity); + playerEntity.Play = entity; + await _db.Players.AddAsync(playerEntity); + } + + await _db.SaveChangesAsync(); + + var mapped = _mapper.Map(entity); + return Ok(mapped); + } + + [HttpDelete("{identifier}")] + public async Task Delete(Guid identifier) + { + return await _crud.DeleteAsync(identifier); + } +} \ No newline at end of file diff --git a/API/Controllers/StatsController.cs b/API/Controllers/StatsController.cs new file mode 100644 index 0000000..165237b --- /dev/null +++ b/API/Controllers/StatsController.cs @@ -0,0 +1,182 @@ +using Hesketh.MecatolArchives.API.Data; +using Hesketh.MecatolArchives.DB; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Hesketh.MecatolArchives.API.Controllers; + +[ApiController] +[Route("stats")] +public sealed class StatsController : ControllerBase +{ + private readonly MecatolArchivesDbContext _db; + + public StatsController(MecatolArchivesDbContext db) + { + _db = db; + } + + [HttpGet("factions/person={personIdentifier}")] + public ActionResult GetPlayersFactions(Guid personIdentifier) + { + var overall = new Statistics(); + + var statsLookup = new Dictionary(); + foreach (var faction in _db.Factions.Where(x => x.Name != MecatolArchivesDbContext.UnknownName)) + { + statsLookup[faction.Identifier] = new Statistic(faction.Name, faction.Identifier); + } + + foreach (var play in _db.Plays.Include(x => x.Players) + .ThenInclude(x => x.Faction) + .Include(x => x.Players).ThenInclude(x => x.Person) + .Where(x => x.Players.Any(y => y.Winner))) + { + foreach (var player in play.Players.Where(x => x.Person.Identifier == personIdentifier)) + { + if (!statsLookup.TryGetValue(player.Faction.Identifier, out var stats)) + { + stats = new Statistic(player.Faction.Name, player.Faction.Identifier); + statsLookup[player.Faction.Identifier] = stats; + } + + stats.Plays += 1; + + if (player.Winner) + { + stats.Wins += 1; + } + + var pointPercentage = (double)player.Points / play.PointGoal; + stats.PointPercentSum += pointPercentage; + } + } + + foreach (var (id, stat) in statsLookup) + { + overall.AddStatistic(stat); + } + + return Ok(overall); + } + + [HttpGet("factions")] + public ActionResult GetFactionStats() + { + var overall = new Statistics(); + + var statsLookup = new Dictionary(); + foreach (var faction in _db.Factions.Where(x => x.Name != MecatolArchivesDbContext.UnknownName)) + { + statsLookup[faction.Identifier] = new Statistic(faction.Name, faction.Identifier); + } + + foreach (var play in _db.Plays.Include(x => x.Players) + .ThenInclude(x => x.Faction) + .Where(x => x.Players.Any(y => y.Winner))) + { + foreach (var player in play.Players) + { + if (!statsLookup.TryGetValue(player.Faction.Identifier, out var stats)) + { + stats = new Statistic(player.Faction.Name, player.Faction.Identifier); + statsLookup[player.Faction.Identifier] = stats; + } + + stats.Plays += 1; + + if (player.Winner) + { + stats.Wins += 1; + } + + var pointPercentage = (double)player.Points / play.PointGoal; + stats.PointPercentSum += pointPercentage; + } + } + + foreach (var (id, stat) in statsLookup) + { + overall.AddStatistic(stat); + } + + return Ok(overall); + } + + [HttpGet("people")] + public ActionResult GetPeopleStats() + { + var overall = new Statistics(); + + var statsLookup = new Dictionary(); + foreach (var play in _db.Plays.Include(x => x.Players) + .ThenInclude(x => x.Person) + .Where(x => x.Players.Any(y => y.Winner))) + { + foreach (var player in play.Players) + { + if (!statsLookup.TryGetValue(player.Person.Identifier, out var stats)) + { + stats = new Statistic(player.Person.Name, player.Person.Identifier); + statsLookup[player.Person.Identifier] = stats; + } + + stats.Plays += 1; + + if (player.Winner) + { + stats.Wins += 1; + } + + var pointPercentage = (double)player.Points / play.PointGoal; + stats.PointPercentSum += pointPercentage; + } + } + + foreach (var (id, stat) in statsLookup) + { + overall.AddStatistic(stat); + } + + return Ok(overall); + } + + [HttpGet("people/faction={factionIdentifier}")] + public ActionResult GetFactionPeopleStats(Guid factionIdentifier) + { + var overall = new Statistics(); + + var statsLookup = new Dictionary(); + foreach (var play in _db.Plays.Include(x => x.Players) + .ThenInclude(x => x.Person) + .Include(x => x.Players).ThenInclude(x => x.Faction) + .Where(x => x.Players.Any(y => y.Winner))) + { + foreach (var player in play.Players.Where(x => x.Faction.Identifier == factionIdentifier)) + { + if (!statsLookup.TryGetValue(player.Person.Identifier, out var stats)) + { + stats = new Statistic(player.Person.Name, player.Person.Identifier); + statsLookup[player.Person.Identifier] = stats; + } + + stats.Plays += 1; + + if (player.Winner) + { + stats.Wins += 1; + } + + var pointPercentage = (double)player.Points / play.PointGoal; + stats.PointPercentSum += pointPercentage; + } + } + + foreach (var (id, stat) in statsLookup) + { + overall.AddStatistic(stat); + } + + return Ok(overall); + } +} \ No newline at end of file diff --git a/API/Controllers/VariantController.cs b/API/Controllers/VariantController.cs new file mode 100644 index 0000000..d201d79 --- /dev/null +++ b/API/Controllers/VariantController.cs @@ -0,0 +1,53 @@ +using AutoMapper; +using Hesketh.MecatolArchives.API.Helpers; +using Hesketh.MecatolArchives.DB; +using Hesketh.MecatolArchives.DB.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Hesketh.MecatolArchives.API.Controllers; + +[ApiController] +[Route("variants")] +[Authorize] +public sealed class VariantController : ControllerBase +{ + private readonly CrudControllerHelper _crud; + + public VariantController(MecatolArchivesDbContext db, IMapper mapper) + { + _crud = new CrudControllerHelper(db, mapper); + } + + [HttpGet("{identifier}")] + [AllowAnonymous] + public async Task> Get(Guid identifier) + { + return await _crud.GetAsync(identifier); + } + + [HttpGet] + [AllowAnonymous] + public async Task>> Get() + { + return await _crud.GetAsync(); + } + + [HttpPost] + public async Task> Post(Data.Post.Variant model) + { + return await _crud.PostAsync(model); + } + + [HttpPut] + public async Task> Put(Data.Variant model) + { + return await _crud.PutAsync(model); + } + + [HttpDelete("{identifier}")] + public async Task Delete(Guid identifier) + { + return await _crud.DeleteAsync(identifier); + } +} \ No newline at end of file diff --git a/API/Dockerfile b/API/Dockerfile new file mode 100644 index 0000000..8da9a34 --- /dev/null +++ b/API/Dockerfile @@ -0,0 +1,25 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["API/API.csproj", "API/"] +COPY ["API.Data/API.Data.csproj", "API.Data/"] +COPY ["DB/DB.csproj", "DB/"] +RUN dotnet restore "API/API.csproj" +COPY . . +WORKDIR "/src/API" +RUN dotnet build "API.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "API.dll"] diff --git a/API/Global.cs b/API/Global.cs new file mode 100644 index 0000000..c100c60 --- /dev/null +++ b/API/Global.cs @@ -0,0 +1,4 @@ +global using DTM = Hesketh.MecatolArchives.API.Data; +global using DTPost = Hesketh.MecatolArchives.API.Data.Post; +global using DTPut = Hesketh.MecatolArchives.API.Data.Put; +global using DBM = Hesketh.MecatolArchives.DB.Models; \ No newline at end of file diff --git a/API/Helpers/CrudControllerHelper.cs b/API/Helpers/CrudControllerHelper.cs new file mode 100644 index 0000000..213187e --- /dev/null +++ b/API/Helpers/CrudControllerHelper.cs @@ -0,0 +1,79 @@ +using AutoMapper; +using Hesketh.MecatolArchives.DB; +using Hesketh.MecatolArchives.DB.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Hesketh.MecatolArchives.API.Helpers; + +public sealed class CrudControllerHelper : ControllerBase + where TDatabase : class, IEntity, INamed + where TGet : Data.IEntity + where TPut : Data.IEntity +{ + private readonly MecatolArchivesDbContext _db; + private readonly IMapper _mapper; + + public CrudControllerHelper(MecatolArchivesDbContext db, + IMapper mapper) + { + _db = db; + _mapper = mapper; + } + + //[HttpGet("{identifier}")] + public async Task> GetAsync(Guid identifier) + { + var res = await _db.GetDbSet().FirstOrDefaultAsync(x => x.Identifier == identifier); + if (res == null) return NotFound(); + + var mapped = _mapper.Map(res); + return Ok(mapped); + } + + //[HttpGet] + public async Task>> GetAsync() + { + var res = await _db.GetDbSet() + .OrderBy(x => x.Name) + .ToListAsync(); + var mapped = _mapper.Map>(res); + return Ok(mapped); + } + + //[HttpPost] + public async Task> PostAsync(TPost model) + { + var entity = _mapper.Map(model); + await _db.GetDbSet().AddAsync(entity); + await _db.SaveChangesAsync(); + + var mapped = _mapper.Map(entity); + return Ok(mapped); + } + + //[HttpPut] + public async Task> PutAsync(TPut model) + { + var entity = await _db.GetDbSet().FirstOrDefaultAsync(x => x.Identifier == model.Identifier); + if (entity == null) return NotFound(); + + entity = _mapper.Map(model, entity); + await _db.SaveChangesAsync(); + + var mapped = _mapper.Map(entity); + return Ok(mapped); + } + + //[HttpDelete("{identifier}")] + public async Task DeleteAsync(Guid identifier) + { + var entity = await _db.GetDbSet().FirstOrDefaultAsync(x => x.Identifier == identifier); + if (entity == null) return NotFound(); + + _db.GetDbSet().Remove(entity); + await _db.SaveChangesAsync(); + + return Ok(); + } +} \ No newline at end of file diff --git a/API/Helpers/DevelopmentDataSeedingHelper.cs b/API/Helpers/DevelopmentDataSeedingHelper.cs new file mode 100644 index 0000000..5022288 --- /dev/null +++ b/API/Helpers/DevelopmentDataSeedingHelper.cs @@ -0,0 +1,196 @@ +using Hesketh.MecatolArchives.DB; + +namespace Hesketh.MecatolArchives.API.Helpers; + +public static class DevelopmentDataSeedingHelper +{ + private const uint PlaysToGenerate = 25; + private const int MinPlayersInPlay = 3; + private const int MaxPlayersInPlay = 8; + private const int MaxPoints = 10; + private const int MinPoints = 0; + private const int MaxDaysDifferenceInDate = 1000; + + private const uint DataFieldsToGenerate = 10; + + private static readonly Random Random = new Random(404); + private static readonly NounHelper NounHelper = new NounHelper(Random); + + public static async Task Seed(MecatolArchivesDbContext db) + { + await GenerateDataFields(db); + await GeneratePlays(db); + + await db.SaveChangesAsync(); + } + + private static async Task GeneratePlays(MecatolArchivesDbContext db) + { + for (int i = 0; i < PlaysToGenerate; i++) + { + var play = new DBM.Play() + { + Identifier = Guid.NewGuid(), + UtcDate = GetRandomUtcDate(), + PointGoal = 10, + RulesVersion = 2.0, + Map = null, + Expansions = GetRandomExpansions(db), + Variants = GetRandomVariants(db), + }; + play.Players = GetRandomPlayers(db, play); + await db.AddAsync(play); + } + await db.SaveChangesAsync(); + } + + private static DBM.Colour GetRandomColour(MecatolArchivesDbContext db, ICollection invalid) + { + var index = Random.Next(0, db.Colours.Count(x => !invalid.Contains(x.Identifier))); + return db.Colours.Where(x => !invalid.Contains(x.Identifier)).Skip(index - 1).First(); + } + + private static DBM.Faction GetRandomFaction(MecatolArchivesDbContext db, ICollection invalid) + { + var index = Random.Next(0, db.Factions.Count(x => !invalid.Contains(x.Identifier))); + return db.Factions.Where(x => !invalid.Contains(x.Identifier)).Skip(index - 1).First(); + } + + private static DBM.Person GetRandomPerson(MecatolArchivesDbContext db, ICollection invalid) + { + var index = Random.Next(0, db.People.Count(x => !invalid.Contains(x.Identifier))); + return db.People.Where(x => !invalid.Contains(x.Identifier)).Skip(index - 1).First(); + } + + private static ICollection GetRandomPlayers(MecatolArchivesDbContext db, DBM.Play play) + { + var res = new List(); + var playerCount = Random.Next(MinPlayersInPlay, MaxPlayersInPlay + 1); + for (int i = 0; i < playerCount; i++) + { + var player = new DBM.Player + { + Identifier = Guid.NewGuid(), + Person = GetRandomPerson(db, res.Select(x => x.Person.Identifier).ToList()), + Faction = GetRandomFaction(db, res.Select(x => x.Faction.Identifier).ToList()), + Colour = GetRandomColour(db, res.Select(x => x.Colour.Identifier).ToList()), + Points = (uint)Random.Next(MinPoints, MaxPoints + 1), + Eliminated = GetRandomBool(50), + }; + + db.Add(player); + player.Play = play; + res.Add(player); + } + + var winner = res.OrderByDescending(x => x.Points).First(); + winner.Winner = !GetRandomBool(10); + + return res; + } + + private static ICollection GetRandomExpansions(MecatolArchivesDbContext db) + { + var res = new List(); + foreach (var expansion in db.Expansions) + { + if (GetRandomBool(1)) + { + res.Add(expansion); + } + } + return res; + } + + private static ICollection GetRandomVariants(MecatolArchivesDbContext db) + { + var res = new List(); + foreach (var variant in db.Variants) + { + if (GetRandomBool(3)) + { + res.Add(variant); + } + } + return res; + } + private static DateTime GetRandomUtcDate() + { + var modifier = Random.Next(-MaxDaysDifferenceInDate, MaxDaysDifferenceInDate); + return DateTime.UtcNow.AddDays(modifier); + } + + private static bool GetRandomBool(int chance) + { + return Random.Next(0, chance) == 0; + } + + private static async Task GenerateDataFields(MecatolArchivesDbContext db) + { + for (int i = 0; i < DataFieldsToGenerate; i++) + { + var person = GeneratePerson(); + await db.AddAsync(person); + + var colour = GenerateColour(); + await db.AddAsync(colour); + + var faction = GenerateFaction(); + await db.AddAsync(faction); + + var variant = GenerateVariant(); + await db.AddAsync(variant); + + var expansion = GenerateExpansion(); + await db.AddAsync(expansion); + } + + await db.SaveChangesAsync(); + } + + private static DBM.Person GeneratePerson() + { + return new DBM.Person + { + Identifier = Guid.NewGuid(), + Name = NounHelper.GetRandomNoun() + }; + } + + private static DBM.Colour GenerateColour() + { + return new DBM.Colour + { + Identifier = Guid.NewGuid(), + Name = NounHelper.GetRandomNoun(), + Hex = NounHelper.GetRandomColour() + }; + } + + private static DBM.Faction GenerateFaction() + { + return new DBM.Faction + { + Identifier = Guid.NewGuid(), + Name = NounHelper.GetRandomNoun() + }; + } + + private static DBM.Variant GenerateVariant() + { + return new DBM.Variant + { + Identifier = Guid.NewGuid(), + Name = NounHelper.GetRandomNoun() + }; + } + + private static DBM.Expansion GenerateExpansion() + { + return new DBM.Expansion + { + Identifier = Guid.NewGuid(), + Name = NounHelper.GetRandomNoun() + }; + } +} \ No newline at end of file diff --git a/API/Helpers/NounHelper.cs b/API/Helpers/NounHelper.cs new file mode 100644 index 0000000..b6886de --- /dev/null +++ b/API/Helpers/NounHelper.cs @@ -0,0 +1,6846 @@ +namespace Hesketh.MecatolArchives.API.Helpers; + +public class NounHelper +{ + private readonly Random _random; + + #region Data + private static readonly string[] Nouns = new[] + { + "ATM", + "CD", + "SUV", + "TV", + "aardvark", + "abacus", + "abbey", + "abbreviation", + "abdomen", + "ability", + "abnormality", + "abolishment", + "abortion", + "abrogation", + "absence", + "abundance", + "abuse", + "academics", + "academy", + "accelerant", + "accelerator", + "accent", + "acceptance", + "access", + "accessory", + "accident", + "accommodation", + "accompanist", + "accomplishment", + "accord", + "accordance", + "accordion", + "account", + "accountability", + "accountant", + "accounting", + "accuracy", + "accusation", + "acetate", + "achievement", + "achiever", + "acid", + "acknowledgment", + "acorn", + "acoustics", + "acquaintance", + "acquisition", + "acre", + "acrylic", + "act", + "action", + "activation", + "activist", + "activity", + "actor", + "actress", + "acupuncture", + "ad", + "adaptation", + "adapter", + "addiction", + "addition", + "address", + "adjective", + "adjustment", + "admin", + "administration", + "administrator", + "admire", + "admission", + "adobe", + "adoption", + "adrenalin", + "adrenaline", + "adult", + "adulthood", + "advance", + "advancement", + "advantage", + "advent", + "adverb", + "advertisement", + "advertising", + "advice", + "adviser", + "advocacy", + "advocate", + "affair", + "affect", + "affidavit", + "affiliate", + "affinity", + "afoul", + "afterlife", + "aftermath", + "afternoon", + "aftershave", + "aftershock", + "afterthought", + "age", + "agency", + "agenda", + "agent", + "aggradation", + "aggression", + "aglet", + "agony", + "agreement", + "agriculture", + "aid", + "aide", + "aim", + "air", + "airbag", + "airbus", + "aircraft", + "airfare", + "airfield", + "airforce", + "airline", + "airmail", + "airman", + "airplane", + "airport", + "airship", + "airspace", + "alarm", + "alb", + "albatross", + "album", + "alcohol", + "alcove", + "alder", + "ale", + "alert", + "alfalfa", + "algebra", + "algorithm", + "alias", + "alibi", + "alien", + "allegation", + "allergist", + "alley", + "alliance", + "alligator", + "allocation", + "allowance", + "alloy", + "alluvium", + "almanac", + "almighty", + "almond", + "alpaca", + "alpenglow", + "alpenhorn", + "alpha", + "alphabet", + "altar", + "alteration", + "alternative", + "altitude", + "alto", + "aluminium", + "aluminum", + "amazement", + "amazon", + "ambassador", + "amber", + "ambience", + "ambiguity", + "ambition", + "ambulance", + "amendment", + "amenity", + "ammunition", + "amnesty", + "amount", + "amusement", + "anagram", + "analgesia", + "analog", + "analogue", + "analogy", + "analysis", + "analyst", + "analytics", + "anarchist", + "anarchy", + "anatomy", + "ancestor", + "anchovy", + "android", + "anesthesiologist", + "anesthesiology", + "angel", + "anger", + "angina", + "angiosperm", + "angle", + "angora", + "angstrom", + "anguish", + "animal", + "anime", + "anise", + "ankle", + "anklet", + "anniversary", + "announcement", + "annual", + "anorak", + "answer", + "ant", + "anteater", + "antecedent", + "antechamber", + "antelope", + "antennae", + "anterior", + "anthropology", + "antibody", + "anticipation", + "anticodon", + "antigen", + "antique", + "antiquity", + "antler", + "antling", + "anxiety", + "anybody", + "anyone", + "anything", + "anywhere", + "apartment", + "ape", + "aperitif", + "apology", + "app", + "apparatus", + "apparel", + "appeal", + "appearance", + "appellation", + "appendix", + "appetiser", + "appetite", + "appetizer", + "applause", + "apple", + "applewood", + "appliance", + "application", + "appointment", + "appreciation", + "apprehension", + "approach", + "appropriation", + "approval", + "apricot", + "apron", + "apse", + "aquarium", + "aquifer", + "arcade", + "arch", + "arch-rival", + "archaeologist", + "archaeology", + "archeology", + "archer", + "architect", + "architecture", + "archives", + "area", + "arena", + "argument", + "arithmetic", + "ark", + "arm", + "arm-rest", + "armadillo", + "armament", + "armchair", + "armoire", + "armor", + "armour", + "armpit", + "armrest", + "army", + "arrangement", + "array", + "arrest", + "arrival", + "arrogance", + "arrow", + "art", + "artery", + "arthur", + "artichoke", + "article", + "artifact", + "artificer", + "artist", + "ascend", + "ascent", + "ascot", + "ash", + "ashram", + "ashtray", + "aside", + "asparagus", + "aspect", + "asphalt", + "aspic", + "ass", + "assassination", + "assault", + "assembly", + "assertion", + "assessment", + "asset", + "assignment", + "assist", + "assistance", + "assistant", + "associate", + "association", + "assumption", + "assurance", + "asterisk", + "astrakhan", + "astrolabe", + "astrologer", + "astrology", + "astronomy", + "asymmetry", + "atelier", + "atheist", + "athlete", + "athletics", + "atmosphere", + "atom", + "atrium", + "attachment", + "attack", + "attacker", + "attainment", + "attempt", + "attendance", + "attendant", + "attention", + "attenuation", + "attic", + "attitude", + "attorney", + "attraction", + "attribute", + "auction", + "audience", + "audit", + "auditorium", + "aunt", + "authentication", + "authenticity", + "author", + "authorisation", + "authority", + "authorization", + "auto", + "autoimmunity", + "automation", + "automaton", + "autumn", + "availability", + "avalanche", + "avenue", + "average", + "avocado", + "award", + "awareness", + "awe", + "axis", + "azimuth", + "babe", + "baboon", + "babushka", + "baby", + "bachelor", + "back", + "back-up", + "backbone", + "backburn", + "backdrop", + "background", + "backpack", + "backup", + "backyard", + "bacon", + "bacterium", + "badge", + "badger", + "bafflement", + "bag", + "bagel", + "baggage", + "baggie", + "baggy", + "bagpipe", + "bail", + "bait", + "bake", + "baker", + "bakery", + "bakeware", + "balaclava", + "balalaika", + "balance", + "balcony", + "ball", + "ballet", + "balloon", + "balloonist", + "ballot", + "ballpark", + "bamboo", + "ban", + "banana", + "band", + "bandana", + "bandanna", + "bandolier", + "bandwidth", + "bangle", + "banjo", + "bank", + "bankbook", + "banker", + "banking", + "bankruptcy", + "banner", + "banquette", + "banyan", + "baobab", + "bar", + "barbecue", + "barbeque", + "barber", + "barbiturate", + "bargain", + "barge", + "baritone", + "barium", + "bark", + "barley", + "barn", + "barometer", + "barracks", + "barrage", + "barrel", + "barrier", + "barstool", + "bartender", + "base", + "baseball", + "baseboard", + "baseline", + "basement", + "basics", + "basil", + "basin", + "basis", + "basket", + "basketball", + "bass", + "bassinet", + "bassoon", + "bat", + "bath", + "bather", + "bathhouse", + "bathrobe", + "bathroom", + "bathtub", + "battalion", + "batter", + "battery", + "batting", + "battle", + "battleship", + "bay", + "bayou", + "beach", + "bead", + "beak", + "beam", + "bean", + "beancurd", + "beanie", + "beanstalk", + "bear", + "beard", + "beast", + "beastie", + "beat", + "beating", + "beauty", + "beaver", + "beck", + "bed", + "bedrock", + "bedroom", + "bee", + "beech", + "beef", + "beer", + "beet", + "beetle", + "beggar", + "beginner", + "beginning", + "begonia", + "behalf", + "behavior", + "behaviour", + "beheading", + "behest", + "behold", + "being", + "belfry", + "belief", + "believer", + "bell", + "belligerency", + "bellows", + "belly", + "belt", + "bench", + "bend", + "beneficiary", + "benefit", + "beret", + "berry", + "best-seller", + "bestseller", + "bet", + "beverage", + "beyond", + "bias", + "bibliography", + "bicycle", + "bid", + "bidder", + "bidding", + "bidet", + "bifocals", + "bijou", + "bike", + "bikini", + "bill", + "billboard", + "billing", + "billion", + "bin", + "binoculars", + "biology", + "biopsy", + "biosphere", + "biplane", + "birch", + "bird", + "bird-watcher", + "birdbath", + "birdcage", + "birdhouse", + "birth", + "birthday", + "biscuit", + "bit", + "bite", + "bitten", + "bitter", + "black", + "blackberry", + "blackbird", + "blackboard", + "blackfish", + "blackness", + "bladder", + "blade", + "blame", + "blank", + "blanket", + "blast", + "blazer", + "blend", + "blessing", + "blight", + "blind", + "blinker", + "blister", + "blizzard", + "block", + "blocker", + "blog", + "blogger", + "blood", + "bloodflow", + "bloom", + "bloomer", + "blossom", + "blouse", + "blow", + "blowgun", + "blowhole", + "blue", + "blueberry", + "blush", + "boar", + "board", + "boat", + "boatload", + "boatyard", + "bob", + "bobcat", + "body", + "bog", + "bolero", + "bolt", + "bomb", + "bomber", + "bombing", + "bond", + "bonding", + "bondsman", + "bone", + "bonfire", + "bongo", + "bonnet", + "bonsai", + "bonus", + "boogeyman", + "book", + "bookcase", + "bookend", + "booking", + "booklet", + "bookmark", + "boolean", + "boom", + "boon", + "boost", + "booster", + "boot", + "bootee", + "bootie", + "booty", + "border", + "bore", + "borrower", + "borrowing", + "bosom", + "boss", + "botany", + "bother", + "bottle", + "bottling", + "bottom", + "bottom-line", + "boudoir", + "bough", + "boulder", + "boulevard", + "boundary", + "bouquet", + "bourgeoisie", + "bout", + "boutique", + "bow", + "bower", + "bowl", + "bowler", + "bowling", + "bowtie", + "box", + "boxer", + "boxspring", + "boy", + "boycott", + "boyfriend", + "boyhood", + "boysenberry", + "bra", + "brace", + "bracelet", + "bracket", + "brain", + "brake", + "bran", + "branch", + "brand", + "brandy", + "brass", + "brassiere", + "bratwurst", + "bread", + "breadcrumb", + "breadfruit", + "break", + "breakdown", + "breakfast", + "breakpoint", + "breakthrough", + "breast", + "breastplate", + "breath", + "breeze", + "brewer", + "bribery", + "brick", + "bricklaying", + "bride", + "bridge", + "brief", + "briefing", + "briefly", + "briefs", + "brilliant", + "brink", + "brisket", + "broad", + "broadcast", + "broccoli", + "brochure", + "brocolli", + "broiler", + "broker", + "bronchitis", + "bronco", + "bronze", + "brooch", + "brood", + "brook", + "broom", + "brother", + "brother-in-law", + "brow", + "brown", + "brownie", + "browser", + "browsing", + "brunch", + "brush", + "brushfire", + "brushing", + "bubble", + "buck", + "bucket", + "buckle", + "buckwheat", + "bud", + "buddy", + "budget", + "buffalo", + "buffer", + "buffet", + "bug", + "buggy", + "bugle", + "builder", + "building", + "bulb", + "bulk", + "bull", + "bull-fighter", + "bulldozer", + "bullet", + "bump", + "bumper", + "bun", + "bunch", + "bungalow", + "bunghole", + "bunkhouse", + "burden", + "bureau", + "burglar", + "burial", + "burlesque", + "burn", + "burn-out", + "burning", + "burrito", + "burro", + "burrow", + "burst", + "bus", + "bush", + "business", + "businessman", + "bust", + "bustle", + "butane", + "butcher", + "butler", + "butter", + "butterfly", + "button", + "buy", + "buyer", + "buying", + "buzz", + "buzzard", + "c-clamp", + "cabana", + "cabbage", + "cabin", + "cabinet", + "cable", + "caboose", + "cacao", + "cactus", + "caddy", + "cadet", + "cafe", + "caffeine", + "caftan", + "cage", + "cake", + "calcification", + "calculation", + "calculator", + "calculus", + "calendar", + "calf", + "caliber", + "calibre", + "calico", + "call", + "calm", + "calorie", + "camel", + "cameo", + "camera", + "camp", + "campaign", + "campaigning", + "campanile", + "camper", + "campus", + "can", + "canal", + "cancer", + "candelabra", + "candidacy", + "candidate", + "candle", + "candy", + "cane", + "cannibal", + "cannon", + "canoe", + "canon", + "canopy", + "cantaloupe", + "canteen", + "canvas", + "cap", + "capability", + "capacity", + "cape", + "caper", + "capital", + "capitalism", + "capitulation", + "capon", + "cappelletti", + "cappuccino", + "captain", + "caption", + "captor", + "car", + "carabao", + "caramel", + "caravan", + "carbohydrate", + "carbon", + "carboxyl", + "card", + "cardboard", + "cardigan", + "care", + "career", + "cargo", + "caribou", + "carload", + "carnation", + "carnival", + "carol", + "carotene", + "carp", + "carpenter", + "carpet", + "carpeting", + "carport", + "carriage", + "carrier", + "carrot", + "carry", + "cart", + "cartel", + "carter", + "cartilage", + "cartload", + "cartoon", + "cartridge", + "carving", + "cascade", + "case", + "casement", + "cash", + "cashew", + "cashier", + "casino", + "casket", + "cassava", + "casserole", + "cassock", + "cast", + "castanet", + "castle", + "casualty", + "cat", + "catacomb", + "catalogue", + "catalysis", + "catalyst", + "catamaran", + "catastrophe", + "catch", + "catcher", + "category", + "caterpillar", + "cathedral", + "cation", + "catsup", + "cattle", + "cauliflower", + "causal", + "cause", + "causeway", + "caution", + "cave", + "caviar", + "cayenne", + "ceiling", + "celebration", + "celebrity", + "celeriac", + "celery", + "cell", + "cellar", + "cello", + "celsius", + "cement", + "cemetery", + "cenotaph", + "census", + "cent", + "center", + "centimeter", + "centre", + "centurion", + "century", + "cephalopod", + "ceramic", + "ceramics", + "cereal", + "ceremony", + "certainty", + "certificate", + "certification", + "cesspool", + "chafe", + "chain", + "chainstay", + "chair", + "chairlift", + "chairman", + "chairperson", + "chaise", + "chalet", + "chalice", + "chalk", + "challenge", + "chamber", + "champagne", + "champion", + "championship", + "chance", + "chandelier", + "change", + "channel", + "chaos", + "chap", + "chapel", + "chaplain", + "chapter", + "character", + "characteristic", + "characterization", + "chard", + "charge", + "charger", + "charity", + "charlatan", + "charm", + "charset", + "chart", + "charter", + "chasm", + "chassis", + "chastity", + "chasuble", + "chateau", + "chatter", + "chauffeur", + "chauvinist", + "check", + "checkbook", + "checking", + "checkout", + "checkroom", + "cheddar", + "cheek", + "cheer", + "cheese", + "cheesecake", + "cheetah", + "chef", + "chem", + "chemical", + "chemistry", + "chemotaxis", + "cheque", + "cherry", + "chess", + "chest", + "chestnut", + "chick", + "chicken", + "chicory", + "chief", + "chiffonier", + "child", + "childbirth", + "childhood", + "chili", + "chill", + "chime", + "chimpanzee", + "chin", + "chinchilla", + "chino", + "chip", + "chipmunk", + "chit-chat", + "chivalry", + "chive", + "chives", + "chocolate", + "choice", + "choir", + "choker", + "cholesterol", + "choosing", + "chop", + "chops", + "chopstick", + "chopsticks", + "chord", + "chorus", + "chow", + "chowder", + "chrome", + "chromolithograph", + "chronicle", + "chronograph", + "chronometer", + "chrysalis", + "chub", + "chuck", + "chug", + "church", + "churn", + "chutney", + "cicada", + "cigarette", + "cilantro", + "cinder", + "cinema", + "cinnamon", + "circadian", + "circle", + "circuit", + "circulation", + "circumference", + "circumstance", + "cirrhosis", + "cirrus", + "citizen", + "citizenship", + "citron", + "citrus", + "city", + "civilian", + "civilisation", + "civilization", + "claim", + "clam", + "clamp", + "clan", + "clank", + "clapboard", + "clarification", + "clarinet", + "clarity", + "clasp", + "class", + "classic", + "classification", + "classmate", + "classroom", + "clause", + "clave", + "clavicle", + "clavier", + "claw", + "clay", + "cleaner", + "clearance", + "clearing", + "cleat", + "cleavage", + "clef", + "cleft", + "clergyman", + "cleric", + "clerk", + "click", + "client", + "cliff", + "climate", + "climb", + "clinic", + "clip", + "clipboard", + "clipper", + "cloak", + "cloakroom", + "clock", + "clockwork", + "clogs", + "cloister", + "clone", + "close", + "closet", + "closing", + "closure", + "cloth", + "clothes", + "clothing", + "cloud", + "cloudburst", + "clove", + "clover", + "cloves", + "club", + "clue", + "cluster", + "clutch", + "co-producer", + "coach", + "coal", + "coalition", + "coast", + "coaster", + "coat", + "cob", + "cobbler", + "cobweb", + "cock", + "cockpit", + "cockroach", + "cocktail", + "cocoa", + "coconut", + "cod", + "code", + "codepage", + "codling", + "codon", + "codpiece", + "coevolution", + "cofactor", + "coffee", + "coffin", + "cohesion", + "cohort", + "coil", + "coin", + "coincidence", + "coinsurance", + "coke", + "cold", + "coleslaw", + "coliseum", + "collaboration", + "collagen", + "collapse", + "collar", + "collard", + "collateral", + "colleague", + "collection", + "collectivisation", + "collectivization", + "collector", + "college", + "collision", + "colloquy", + "colon", + "colonial", + "colonialism", + "colonisation", + "colonization", + "colony", + "color", + "colorlessness", + "colt", + "column", + "columnist", + "comb", + "combat", + "combination", + "combine", + "comeback", + "comedy", + "comestible", + "comfort", + "comfortable", + "comic", + "comics", + "comma", + "command", + "commander", + "commandment", + "comment", + "commerce", + "commercial", + "commission", + "commitment", + "committee", + "commodity", + "common", + "commonsense", + "commotion", + "communicant", + "communication", + "communion", + "communist", + "community", + "commuter", + "company", + "comparison", + "compass", + "compassion", + "compassionate", + "compensation", + "competence", + "competition", + "competitor", + "complaint", + "complement", + "completion", + "complex", + "complexity", + "compliance", + "complication", + "complicity", + "compliment", + "component", + "comportment", + "composer", + "composite", + "composition", + "compost", + "comprehension", + "compress", + "compromise", + "comptroller", + "compulsion", + "computer", + "comradeship", + "con", + "concentrate", + "concentration", + "concept", + "conception", + "concern", + "concert", + "conclusion", + "concrete", + "condition", + "conditioner", + "condominium", + "condor", + "conduct", + "conductor", + "cone", + "confectionery", + "conference", + "confidence", + "confidentiality", + "configuration", + "confirmation", + "conflict", + "conformation", + "confusion", + "conga", + "congo", + "congregation", + "congress", + "congressman", + "congressperson", + "conifer", + "connection", + "connotation", + "conscience", + "consciousness", + "consensus", + "consent", + "consequence", + "conservation", + "conservative", + "consideration", + "consignment", + "consist", + "consistency", + "console", + "consonant", + "conspiracy", + "conspirator", + "constant", + "constellation", + "constitution", + "constraint", + "construction", + "consul", + "consulate", + "consulting", + "consumer", + "consumption", + "contact", + "contact lens", + "contagion", + "container", + "content", + "contention", + "contest", + "context", + "continent", + "contingency", + "continuity", + "contour", + "contract", + "contractor", + "contrail", + "contrary", + "contrast", + "contribution", + "contributor", + "control", + "controller", + "controversy", + "convection", + "convenience", + "convention", + "conversation", + "conversion", + "convert", + "convertible", + "conviction", + "cook", + "cookbook", + "cookie", + "cooking", + "coonskin", + "cooperation", + "coordination", + "coordinator", + "cop", + "cop-out", + "cope", + "copper", + "copy", + "copying", + "copyright", + "copywriter", + "coral", + "cord", + "corduroy", + "core", + "cork", + "cormorant", + "corn", + "corner", + "cornerstone", + "cornet", + "cornflakes", + "cornmeal", + "corporal", + "corporation", + "corporatism", + "corps", + "corral", + "correspondence", + "correspondent", + "corridor", + "corruption", + "corsage", + "cosset", + "cost", + "costume", + "cot", + "cottage", + "cotton", + "couch", + "cougar", + "cough", + "council", + "councilman", + "councilor", + "councilperson", + "counsel", + "counseling", + "counselling", + "counsellor", + "counselor", + "count", + "counter", + "counter-force", + "counterpart", + "counterterrorism", + "countess", + "country", + "countryside", + "county", + "couple", + "coupon", + "courage", + "course", + "court", + "courthouse", + "courtroom", + "cousin", + "covariate", + "cover", + "coverage", + "coverall", + "cow", + "cowbell", + "cowboy", + "coyote", + "crab", + "crack", + "cracker", + "crackers", + "cradle", + "craft", + "craftsman", + "cranberry", + "crane", + "cranky", + "crap", + "crash", + "crate", + "cravat", + "craw", + "crawdad", + "crayfish", + "crayon", + "crazy", + "cream", + "creation", + "creationism", + "creationist", + "creative", + "creativity", + "creator", + "creature", + "creche", + "credential", + "credenza", + "credibility", + "credit", + "creditor", + "creek", + "creme brulee", + "crepe", + "crest", + "crew", + "crewman", + "crewmate", + "crewmember", + "crewmen", + "cria", + "crib", + "cribbage", + "cricket", + "cricketer", + "crime", + "criminal", + "crinoline", + "crisis", + "crisp", + "criteria", + "criterion", + "critic", + "criticism", + "crocodile", + "crocus", + "croissant", + "crook", + "crop", + "cross", + "cross-contamination", + "cross-stitch", + "crotch", + "croup", + "crow", + "crowd", + "crown", + "crucifixion", + "crude", + "cruelty", + "cruise", + "crumb", + "crunch", + "crusader", + "crush", + "crust", + "cry", + "crystal", + "crystallography", + "cub", + "cube", + "cuckoo", + "cucumber", + "cue", + "cuff-link", + "cuisine", + "cultivar", + "cultivator", + "culture", + "culvert", + "cummerbund", + "cup", + "cupboard", + "cupcake", + "cupola", + "curd", + "cure", + "curio", + "curiosity", + "curl", + "curler", + "currant", + "currency", + "current", + "curriculum", + "curry", + "curse", + "cursor", + "curtailment", + "curtain", + "curve", + "cushion", + "custard", + "custody", + "custom", + "customer", + "cut", + "cuticle", + "cutlet", + "cutover", + "cutting", + "cyclamen", + "cycle", + "cyclone", + "cyclooxygenase", + "cygnet", + "cylinder", + "cymbal", + "cynic", + "cyst", + "cytokine", + "cytoplasm", + "dad", + "daddy", + "daffodil", + "dagger", + "dahlia", + "daikon", + "daily", + "dairy", + "daisy", + "dam", + "damage", + "dame", + "damn", + "dance", + "dancer", + "dancing", + "dandelion", + "danger", + "dare", + "dark", + "darkness", + "darn", + "dart", + "dash", + "dashboard", + "data", + "database", + "date", + "daughter", + "dawn", + "day", + "daybed", + "daylight", + "dead", + "deadline", + "deal", + "dealer", + "dealing", + "dearest", + "death", + "deathwatch", + "debate", + "debris", + "debt", + "debtor", + "decade", + "decadence", + "decency", + "decimal", + "decision", + "decision-making", + "deck", + "declaration", + "declination", + "decline", + "decoder", + "decongestant", + "decoration", + "decrease", + "decryption", + "dedication", + "deduce", + "deduction", + "deed", + "deep", + "deer", + "default", + "defeat", + "defendant", + "defender", + "defense", + "deficit", + "definition", + "deformation", + "degradation", + "degree", + "delay", + "deliberation", + "delight", + "delivery", + "demand", + "democracy", + "democrat", + "demon", + "demur", + "den", + "denim", + "denominator", + "density", + "dentist", + "deodorant", + "department", + "departure", + "dependency", + "dependent", + "deployment", + "deposit", + "deposition", + "depot", + "depression", + "depressive", + "depth", + "deputy", + "derby", + "derivation", + "derivative", + "derrick", + "descendant", + "descent", + "description", + "desert", + "design", + "designation", + "designer", + "desire", + "desk", + "desktop", + "dessert", + "destination", + "destiny", + "destroyer", + "destruction", + "detail", + "detainee", + "detainment", + "detection", + "detective", + "detector", + "detention", + "determination", + "detour", + "devastation", + "developer", + "developing", + "development", + "developmental", + "deviance", + "deviation", + "device", + "devil", + "dew", + "dhow", + "diabetes", + "diadem", + "diagnosis", + "diagram", + "dial", + "dialect", + "dialogue", + "diam", + "diamond", + "diaper", + "diaphragm", + "diarist", + "diary", + "dibble", + "dick", + "dickey", + "dictaphone", + "dictator", + "diction", + "dictionary", + "die", + "diesel", + "diet", + "difference", + "differential", + "difficulty", + "diffuse", + "dig", + "digestion", + "digestive", + "digger", + "digging", + "digit", + "dignity", + "dilapidation", + "dill", + "dilution", + "dime", + "dimension", + "dimple", + "diner", + "dinghy", + "dining", + "dinner", + "dinosaur", + "dioxide", + "dip", + "diploma", + "diplomacy", + "dipstick", + "direction", + "directive", + "director", + "directory", + "dirndl", + "dirt", + "disability", + "disadvantage", + "disagreement", + "disappointment", + "disarmament", + "disaster", + "discharge", + "discipline", + "disclaimer", + "disclosure", + "disco", + "disconnection", + "discount", + "discourse", + "discovery", + "discrepancy", + "discretion", + "discrimination", + "discussion", + "disdain", + "disease", + "disembodiment", + "disengagement", + "disguise", + "disgust", + "dish", + "dishwasher", + "disk", + "disparity", + "dispatch", + "displacement", + "display", + "disposal", + "disposer", + "disposition", + "dispute", + "disregard", + "disruption", + "dissemination", + "dissonance", + "distance", + "distinction", + "distortion", + "distribution", + "distributor", + "district", + "divalent", + "divan", + "diver", + "diversity", + "divide", + "dividend", + "divider", + "divine", + "diving", + "division", + "divorce", + "doc", + "dock", + "doctor", + "doctorate", + "doctrine", + "document", + "documentary", + "documentation", + "doe", + "dog", + "doggie", + "dogsled", + "dogwood", + "doing", + "doll", + "dollar", + "dollop", + "dolman", + "dolor", + "dolphin", + "domain", + "dome", + "domination", + "donation", + "donkey", + "donor", + "donut", + "door", + "doorbell", + "doorknob", + "doorpost", + "doorway", + "dory", + "dose", + "dot", + "double", + "doubling", + "doubt", + "doubter", + "dough", + "doughnut", + "down", + "downfall", + "downforce", + "downgrade", + "download", + "downstairs", + "downtown", + "downturn", + "dozen", + "draft", + "drag", + "dragon", + "dragonfly", + "dragonfruit", + "dragster", + "drain", + "drainage", + "drake", + "drama", + "dramaturge", + "drapes", + "draw", + "drawbridge", + "drawer", + "drawing", + "dream", + "dreamer", + "dredger", + "dress", + "dresser", + "dressing", + "drill", + "drink", + "drinking", + "drive", + "driver", + "driveway", + "driving", + "drizzle", + "dromedary", + "drop", + "drudgery", + "drug", + "drum", + "drummer", + "drunk", + "dryer", + "duck", + "duckling", + "dud", + "dude", + "due", + "duel", + "dueling", + "duffel", + "dugout", + "dulcimer", + "dumbwaiter", + "dump", + "dump truck", + "dune", + "dune buggy", + "dungarees", + "dungeon", + "duplexer", + "duration", + "durian", + "dusk", + "dust", + "dust storm", + "duster", + "duty", + "dwarf", + "dwell", + "dwelling", + "dynamics", + "dynamite", + "dynamo", + "dynasty", + "dysfunction", + "e-book", + "e-mail", + "e-reader", + "eagle", + "eaglet", + "ear", + "eardrum", + "earmuffs", + "earnings", + "earplug", + "earring", + "earrings", + "earth", + "earthquake", + "earthworm", + "ease", + "easel", + "east", + "eating", + "eaves", + "eavesdropper", + "ecclesia", + "echidna", + "eclipse", + "ecliptic", + "ecology", + "economics", + "economy", + "ecosystem", + "ectoderm", + "ectodermal", + "ecumenist", + "eddy", + "edge", + "edger", + "edible", + "editing", + "edition", + "editor", + "editorial", + "education", + "eel", + "effacement", + "effect", + "effective", + "effectiveness", + "effector", + "efficacy", + "efficiency", + "effort", + "egg", + "egghead", + "eggnog", + "eggplant", + "ego", + "eicosanoid", + "ejector", + "elbow", + "elderberry", + "election", + "electricity", + "electrocardiogram", + "electronics", + "element", + "elephant", + "elevation", + "elevator", + "eleventh", + "elf", + "elicit", + "eligibility", + "elimination", + "elite", + "elixir", + "elk", + "ellipse", + "elm", + "elongation", + "elver", + "email", + "emanate", + "embarrassment", + "embassy", + "embellishment", + "embossing", + "embryo", + "emerald", + "emergence", + "emergency", + "emergent", + "emery", + "emission", + "emitter", + "emotion", + "emphasis", + "empire", + "employ", + "employee", + "employer", + "employment", + "empowerment", + "emu", + "enactment", + "encirclement", + "enclave", + "enclosure", + "encounter", + "encouragement", + "encyclopedia", + "end", + "endive", + "endoderm", + "endorsement", + "endothelium", + "endpoint", + "enemy", + "energy", + "enforcement", + "engagement", + "engine", + "engineer", + "engineering", + "enigma", + "enjoyment", + "enquiry", + "enrollment", + "enterprise", + "entertainment", + "enthusiasm", + "entirety", + "entity", + "entrance", + "entree", + "entrepreneur", + "entry", + "envelope", + "environment", + "envy", + "enzyme", + "epauliere", + "epee", + "ephemera", + "ephemeris", + "ephyra", + "epic", + "episode", + "epithelium", + "epoch", + "eponym", + "epoxy", + "equal", + "equality", + "equation", + "equinox", + "equipment", + "equity", + "equivalent", + "era", + "eraser", + "erection", + "erosion", + "error", + "escalator", + "escape", + "escort", + "espadrille", + "espalier", + "essay", + "essence", + "essential", + "establishment", + "estate", + "estimate", + "estrogen", + "estuary", + "eternity", + "ethernet", + "ethics", + "ethnicity", + "ethyl", + "euphonium", + "eurocentrism", + "evaluation", + "evaluator", + "evaporation", + "eve", + "evening", + "evening-wear", + "event", + "everybody", + "everyone", + "everything", + "eviction", + "evidence", + "evil", + "evocation", + "evolution", + "ex-husband", + "ex-wife", + "exaggeration", + "exam", + "examination", + "examiner", + "example", + "exasperation", + "excellence", + "exception", + "excerpt", + "excess", + "exchange", + "excitement", + "exclamation", + "excursion", + "excuse", + "execution", + "executive", + "executor", + "exercise", + "exhaust", + "exhaustion", + "exhibit", + "exhibition", + "exile", + "existence", + "exit", + "exocrine", + "expansion", + "expansionism", + "expectancy", + "expectation", + "expedition", + "expense", + "experience", + "experiment", + "experimentation", + "expert", + "expertise", + "explanation", + "exploration", + "explorer", + "explosion", + "export", + "expose", + "exposition", + "exposure", + "expression", + "extension", + "extent", + "exterior", + "external", + "extinction", + "extreme", + "extremist", + "eye", + "eyeball", + "eyebrow", + "eyebrows", + "eyeglasses", + "eyelash", + "eyelashes", + "eyelid", + "eyelids", + "eyeliner", + "eyestrain", + "eyrie", + "fabric", + "face", + "facelift", + "facet", + "facility", + "facsimile", + "fact", + "factor", + "factory", + "faculty", + "fahrenheit", + "fail", + "failure", + "fairness", + "fairy", + "faith", + "faithful", + "fall", + "fallacy", + "falling-out", + "fame", + "familiar", + "familiarity", + "family", + "fan", + "fang", + "fanlight", + "fanny", + "fanny-pack", + "fantasy", + "farm", + "farmer", + "farming", + "farmland", + "farrow", + "fascia", + "fashion", + "fat", + "fate", + "father", + "father-in-law", + "fatigue", + "fatigues", + "faucet", + "fault", + "fav", + "fava", + "favor", + "favorite", + "fawn", + "fax", + "fear", + "feast", + "feather", + "feature", + "fedelini", + "federation", + "fedora", + "fee", + "feed", + "feedback", + "feeding", + "feel", + "feeling", + "fellow", + "felony", + "female", + "fen", + "fence", + "fencing", + "fender", + "feng", + "fennel", + "ferret", + "ferry", + "ferryboat", + "fertilizer", + "festival", + "fetus", + "few", + "fiber", + "fiberglass", + "fibre", + "fibroblast", + "fibrosis", + "ficlet", + "fiction", + "fiddle", + "field", + "fiery", + "fiesta", + "fifth", + "fig", + "fight", + "fighter", + "figure", + "figurine", + "file", + "filing", + "fill", + "fillet", + "filly", + "film", + "filter", + "filth", + "final", + "finance", + "financing", + "finding", + "fine", + "finer", + "finger", + "fingerling", + "fingernail", + "finish", + "finisher", + "fir", + "fire", + "fireman", + "fireplace", + "firewall", + "firm", + "first", + "fish", + "fishbone", + "fisherman", + "fishery", + "fishing", + "fishmonger", + "fishnet", + "fisting", + "fit", + "fitness", + "fix", + "fixture", + "flag", + "flair", + "flame", + "flan", + "flanker", + "flare", + "flash", + "flat", + "flatboat", + "flavor", + "flax", + "fleck", + "fledgling", + "fleece", + "flesh", + "flexibility", + "flick", + "flicker", + "flight", + "flint", + "flintlock", + "flip-flops", + "flock", + "flood", + "floodplain", + "floor", + "floozie", + "flour", + "flow", + "flower", + "flu", + "flugelhorn", + "fluke", + "flume", + "flung", + "flute", + "fly", + "flytrap", + "foal", + "foam", + "fob", + "focus", + "fog", + "fold", + "folder", + "folk", + "folklore", + "follower", + "following", + "fondue", + "font", + "food", + "foodstuffs", + "fool", + "foot", + "footage", + "football", + "footnote", + "footprint", + "footrest", + "footstep", + "footstool", + "footwear", + "forage", + "forager", + "foray", + "force", + "ford", + "forearm", + "forebear", + "forecast", + "forehead", + "foreigner", + "forelimb", + "forest", + "forestry", + "forever", + "forgery", + "fork", + "form", + "formal", + "formamide", + "format", + "formation", + "former", + "formicarium", + "formula", + "fort", + "forte", + "fortnight", + "fortress", + "fortune", + "forum", + "foundation", + "founder", + "founding", + "fountain", + "fourths", + "fowl", + "fox", + "foxglove", + "fraction", + "fragrance", + "frame", + "framework", + "fratricide", + "fraud", + "fraudster", + "freak", + "freckle", + "freedom", + "freelance", + "freezer", + "freezing", + "freight", + "freighter", + "frenzy", + "freon", + "frequency", + "fresco", + "friction", + "fridge", + "friend", + "friendship", + "fries", + "frigate", + "fright", + "fringe", + "fritter", + "frock", + "frog", + "front", + "frontier", + "frost", + "frosting", + "frown", + "fruit", + "frustration", + "fry", + "fuck", + "fuel", + "fugato", + "fulfillment", + "full", + "fun", + "function", + "functionality", + "fund", + "funding", + "fundraising", + "funeral", + "fur", + "furnace", + "furniture", + "furry", + "fusarium", + "futon", + "future", + "gadget", + "gaffe", + "gaffer", + "gain", + "gaiters", + "gale", + "gall-bladder", + "gallery", + "galley", + "gallon", + "galoshes", + "gambling", + "game", + "gamebird", + "gaming", + "gamma-ray", + "gander", + "gang", + "gap", + "garage", + "garb", + "garbage", + "garden", + "garlic", + "garment", + "garter", + "gas", + "gasket", + "gasoline", + "gasp", + "gastronomy", + "gastropod", + "gate", + "gateway", + "gather", + "gathering", + "gator", + "gauge", + "gauntlet", + "gavel", + "gazebo", + "gazelle", + "gear", + "gearshift", + "geek", + "gel", + "gelatin", + "gelding", + "gem", + "gemsbok", + "gender", + "gene", + "general", + "generation", + "generator", + "generosity", + "genetics", + "genie", + "genius", + "genocide", + "genre", + "gentleman", + "geography", + "geology", + "geometry", + "geranium", + "gerbil", + "gesture", + "geyser", + "gherkin", + "ghost", + "giant", + "gift", + "gig", + "gigantism", + "giggle", + "ginger", + "gingerbread", + "ginseng", + "giraffe", + "girdle", + "girl", + "girlfriend", + "git", + "glacier", + "gladiolus", + "glance", + "gland", + "glass", + "glasses", + "glee", + "glen", + "glider", + "gliding", + "glimpse", + "globe", + "glockenspiel", + "gloom", + "glory", + "glove", + "glow", + "glucose", + "glue", + "glut", + "glutamate", + "gnat", + "gnu", + "go-kart", + "goal", + "goat", + "gobbler", + "god", + "goddess", + "godfather", + "godmother", + "godparent", + "goggles", + "going", + "gold", + "goldfish", + "golf", + "gondola", + "gong", + "good", + "good-bye", + "goodbye", + "goodie", + "goodness", + "goodnight", + "goodwill", + "goose", + "gopher", + "gorilla", + "gosling", + "gossip", + "governance", + "government", + "governor", + "gown", + "grab-bag", + "grace", + "grade", + "gradient", + "graduate", + "graduation", + "graffiti", + "graft", + "grain", + "gram", + "grammar", + "gran", + "grand", + "grandchild", + "granddaughter", + "grandfather", + "grandma", + "grandmom", + "grandmother", + "grandpa", + "grandparent", + "grandson", + "granny", + "granola", + "grant", + "grape", + "grapefruit", + "graph", + "graphic", + "grasp", + "grass", + "grasshopper", + "grassland", + "gratitude", + "gravel", + "gravitas", + "gravity", + "gravy", + "gray", + "grease", + "great-grandfather", + "great-grandmother", + "greatness", + "greed", + "green", + "greenhouse", + "greens", + "grenade", + "grey", + "grid", + "grief", + "grill", + "grin", + "grip", + "gripper", + "grit", + "grocery", + "ground", + "group", + "grouper", + "grouse", + "grove", + "growth", + "grub", + "guacamole", + "guarantee", + "guard", + "guava", + "guerrilla", + "guess", + "guest", + "guestbook", + "guidance", + "guide", + "guideline", + "guilder", + "guilt", + "guilty", + "guinea", + "guitar", + "guitarist", + "gum", + "gumshoe", + "gun", + "gunpowder", + "gutter", + "guy", + "gym", + "gymnast", + "gymnastics", + "gynaecology", + "gyro", + "habit", + "habitat", + "hacienda", + "hacksaw", + "hackwork", + "hail", + "hair", + "haircut", + "hake", + "half", + "half-brother", + "half-sister", + "halibut", + "hall", + "halloween", + "hallway", + "halt", + "ham", + "hamburger", + "hammer", + "hammock", + "hamster", + "hand", + "hand-holding", + "handball", + "handful", + "handgun", + "handicap", + "handle", + "handlebar", + "handmaiden", + "handover", + "handrail", + "handsaw", + "hanger", + "happening", + "happiness", + "harald", + "harbor", + "harbour", + "hard-hat", + "hardboard", + "hardcover", + "hardening", + "hardhat", + "hardship", + "hardware", + "hare", + "harm", + "harmonica", + "harmonise", + "harmonize", + "harmony", + "harp", + "harpooner", + "harpsichord", + "harvest", + "harvester", + "hash", + "hashtag", + "hassock", + "haste", + "hat", + "hatbox", + "hatchet", + "hatchling", + "hate", + "hatred", + "haunt", + "haven", + "haversack", + "havoc", + "hawk", + "hay", + "haze", + "hazel", + "hazelnut", + "head", + "headache", + "headlight", + "headline", + "headphones", + "headquarters", + "headrest", + "health", + "health-care", + "hearing", + "hearsay", + "heart", + "heart-throb", + "heartache", + "heartbeat", + "hearth", + "hearthside", + "heartwood", + "heat", + "heater", + "heating", + "heaven", + "heavy", + "hectare", + "hedge", + "hedgehog", + "heel", + "heifer", + "height", + "heir", + "heirloom", + "helicopter", + "helium", + "hell", + "hellcat", + "hello", + "helmet", + "helo", + "help", + "hemisphere", + "hemp", + "hen", + "hepatitis", + "herb", + "herbs", + "heritage", + "hermit", + "hero", + "heroine", + "heron", + "herring", + "hesitation", + "heterosexual", + "hexagon", + "heyday", + "hiccups", + "hide", + "hierarchy", + "high", + "high-rise", + "highland", + "highlight", + "highway", + "hike", + "hiking", + "hill", + "hint", + "hip", + "hippodrome", + "hippopotamus", + "hire", + "hiring", + "historian", + "history", + "hit", + "hive", + "hobbit", + "hobby", + "hockey", + "hoe", + "hog", + "hold", + "holder", + "hole", + "holiday", + "home", + "homeland", + "homeownership", + "hometown", + "homework", + "homicide", + "homogenate", + "homonym", + "homosexual", + "homosexuality", + "honesty", + "honey", + "honeybee", + "honeydew", + "honor", + "honoree", + "hood", + "hoof", + "hook", + "hop", + "hope", + "hops", + "horde", + "horizon", + "hormone", + "horn", + "hornet", + "horror", + "horse", + "horseradish", + "horst", + "hose", + "hosiery", + "hospice", + "hospital", + "hospitalisation", + "hospitality", + "hospitalization", + "host", + "hostel", + "hostess", + "hotdog", + "hotel", + "hound", + "hour", + "hourglass", + "house", + "houseboat", + "household", + "housewife", + "housework", + "housing", + "hovel", + "hovercraft", + "howard", + "howitzer", + "hub", + "hubcap", + "hubris", + "hug", + "hugger", + "hull", + "human", + "humanity", + "humidity", + "hummus", + "humor", + "humour", + "hunchback", + "hundred", + "hunger", + "hunt", + "hunter", + "hunting", + "hurdle", + "hurdler", + "hurricane", + "hurry", + "hurt", + "husband", + "hut", + "hutch", + "hyacinth", + "hybridisation", + "hybridization", + "hydrant", + "hydraulics", + "hydrocarb", + "hydrocarbon", + "hydrofoil", + "hydrogen", + "hydrolyse", + "hydrolysis", + "hydrolyze", + "hydroxyl", + "hyena", + "hygienic", + "hype", + "hyphenation", + "hypochondria", + "hypothermia", + "hypothesis", + "ice", + "ice-cream", + "iceberg", + "icebreaker", + "icecream", + "icicle", + "icing", + "icon", + "icy", + "id", + "idea", + "ideal", + "identification", + "identity", + "ideology", + "idiom", + "idiot", + "igloo", + "ignorance", + "ignorant", + "ikebana", + "illegal", + "illiteracy", + "illness", + "illusion", + "illustration", + "image", + "imagination", + "imbalance", + "imitation", + "immigrant", + "immigration", + "immortal", + "impact", + "impairment", + "impala", + "impediment", + "implement", + "implementation", + "implication", + "import", + "importance", + "impostor", + "impress", + "impression", + "imprisonment", + "impropriety", + "improvement", + "impudence", + "impulse", + "in-joke", + "in-laws", + "inability", + "inauguration", + "inbox", + "incandescence", + "incarnation", + "incense", + "incentive", + "inch", + "incidence", + "incident", + "incision", + "inclusion", + "income", + "incompetence", + "inconvenience", + "increase", + "incubation", + "independence", + "independent", + "index", + "indication", + "indicator", + "indigence", + "individual", + "industrialisation", + "industrialization", + "industry", + "inequality", + "inevitable", + "infancy", + "infant", + "infarction", + "infection", + "infiltration", + "infinite", + "infix", + "inflammation", + "inflation", + "influence", + "influx", + "info", + "information", + "infrastructure", + "infusion", + "inglenook", + "ingrate", + "ingredient", + "inhabitant", + "inheritance", + "inhibition", + "inhibitor", + "initial", + "initialise", + "initialize", + "initiative", + "injunction", + "injury", + "injustice", + "ink", + "inlay", + "inn", + "innervation", + "innocence", + "innocent", + "innovation", + "input", + "inquiry", + "inscription", + "insect", + "insectarium", + "insert", + "inside", + "insight", + "insolence", + "insomnia", + "inspection", + "inspector", + "inspiration", + "installation", + "instance", + "instant", + "instinct", + "institute", + "institution", + "instruction", + "instructor", + "instrument", + "instrumentalist", + "instrumentation", + "insulation", + "insurance", + "insurgence", + "insurrection", + "integer", + "integral", + "integration", + "integrity", + "intellect", + "intelligence", + "intensity", + "intent", + "intention", + "intentionality", + "interaction", + "interchange", + "interconnection", + "intercourse", + "interest", + "interface", + "interferometer", + "interior", + "interject", + "interloper", + "internet", + "interpretation", + "interpreter", + "interval", + "intervenor", + "intervention", + "interview", + "interviewer", + "intestine", + "introduction", + "intuition", + "invader", + "invasion", + "invention", + "inventor", + "inventory", + "inverse", + "inversion", + "investigation", + "investigator", + "investment", + "investor", + "invitation", + "invite", + "invoice", + "involvement", + "iridescence", + "iris", + "iron", + "ironclad", + "irony", + "irrigation", + "ischemia", + "island", + "isogloss", + "isolation", + "issue", + "item", + "itinerary", + "ivory", + "jack", + "jackal", + "jacket", + "jackfruit", + "jade", + "jaguar", + "jail", + "jailhouse", + "jalapeño", + "jam", + "jar", + "jasmine", + "jaw", + "jazz", + "jealousy", + "jeans", + "jeep", + "jelly", + "jellybeans", + "jellyfish", + "jerk", + "jet", + "jewel", + "jeweller", + "jewellery", + "jewelry", + "jicama", + "jiffy", + "job", + "jockey", + "jodhpurs", + "joey", + "jogging", + "joint", + "joke", + "jot", + "journal", + "journalism", + "journalist", + "journey", + "joy", + "judge", + "judgment", + "judo", + "jug", + "juggernaut", + "juice", + "julienne", + "jumbo", + "jump", + "jumper", + "jumpsuit", + "jungle", + "junior", + "junk", + "junker", + "junket", + "jury", + "justice", + "justification", + "jute", + "kale", + "kamikaze", + "kangaroo", + "karate", + "kayak", + "kazoo", + "kebab", + "keep", + "keeper", + "kendo", + "kennel", + "ketch", + "ketchup", + "kettle", + "kettledrum", + "key", + "keyboard", + "keyboarding", + "keystone", + "kick", + "kick-off", + "kid", + "kidney", + "kielbasa", + "kill", + "killer", + "killing", + "kilogram", + "kilometer", + "kilt", + "kimono", + "kinase", + "kind", + "kindness", + "king", + "kingdom", + "kingfish", + "kiosk", + "kiss", + "kit", + "kitchen", + "kite", + "kitsch", + "kitten", + "kitty", + "kiwi", + "knee", + "kneejerk", + "knickers", + "knife", + "knife-edge", + "knight", + "knitting", + "knock", + "knot", + "know-how", + "knowledge", + "knuckle", + "koala", + "kohlrabi", + "kumquat", + "lab", + "label", + "labor", + "laboratory", + "laborer", + "labour", + "labourer", + "lace", + "lack", + "lacquerware", + "lad", + "ladder", + "ladle", + "lady", + "ladybug", + "lag", + "lake", + "lamb", + "lambkin", + "lament", + "lamp", + "lanai", + "land", + "landform", + "landing", + "landmine", + "landscape", + "lane", + "language", + "lantern", + "lap", + "laparoscope", + "lapdog", + "laptop", + "larch", + "lard", + "larder", + "lark", + "larva", + "laryngitis", + "lasagna", + "lashes", + "last", + "latency", + "latex", + "lathe", + "latitude", + "latte", + "latter", + "laugh", + "laughter", + "laundry", + "lava", + "law", + "lawmaker", + "lawn", + "lawsuit", + "lawyer", + "lay", + "layer", + "layout", + "lead", + "leader", + "leadership", + "leading", + "leaf", + "league", + "leaker", + "leap", + "learning", + "leash", + "leather", + "leave", + "leaver", + "lecture", + "leek", + "leeway", + "left", + "leg", + "legacy", + "legal", + "legend", + "legging", + "legislation", + "legislator", + "legislature", + "legitimacy", + "legume", + "leisure", + "lemon", + "lemonade", + "lemur", + "lender", + "lending", + "length", + "lens", + "lentil", + "leopard", + "leprosy", + "leptocephalus", + "lesbian", + "lesson", + "letter", + "lettuce", + "level", + "lever", + "leverage", + "leveret", + "liability", + "liar", + "liberty", + "libido", + "library", + "licence", + "license", + "licensing", + "licorice", + "lid", + "lie", + "lieu", + "lieutenant", + "life", + "lifestyle", + "lifetime", + "lift", + "ligand", + "light", + "lighting", + "lightning", + "lightscreen", + "ligula", + "likelihood", + "likeness", + "lilac", + "lily", + "limb", + "lime", + "limestone", + "limit", + "limitation", + "limo", + "line", + "linen", + "liner", + "linguist", + "linguistics", + "lining", + "link", + "linkage", + "linseed", + "lion", + "lip", + "lipid", + "lipoprotein", + "lipstick", + "liquid", + "liquidity", + "liquor", + "list", + "listening", + "listing", + "literate", + "literature", + "litigation", + "litmus", + "litter", + "littleneck", + "liver", + "livestock", + "living", + "lizard", + "llama", + "load", + "loading", + "loaf", + "loafer", + "loan", + "lobby", + "lobotomy", + "lobster", + "local", + "locality", + "location", + "lock", + "locker", + "locket", + "locomotive", + "locust", + "lode", + "loft", + "log", + "loggia", + "logic", + "login", + "logistics", + "logo", + "loincloth", + "lollipop", + "loneliness", + "longboat", + "longitude", + "look", + "lookout", + "loop", + "loophole", + "loquat", + "lord", + "loss", + "lot", + "lotion", + "lottery", + "lounge", + "louse", + "lout", + "love", + "lover", + "lox", + "loyalty", + "luck", + "luggage", + "lumber", + "lumberman", + "lunch", + "luncheonette", + "lunchmeat", + "lunchroom", + "lung", + "lunge", + "lust", + "lute", + "luxury", + "lychee", + "lycra", + "lye", + "lymphocyte", + "lynx", + "lyocell", + "lyre", + "lyrics", + "lysine", + "mRNA", + "macadamia", + "macaroni", + "macaroon", + "macaw", + "machine", + "machinery", + "macrame", + "macro", + "macrofauna", + "madam", + "maelstrom", + "maestro", + "magazine", + "maggot", + "magic", + "magnet", + "magnitude", + "maid", + "maiden", + "mail", + "mailbox", + "mailer", + "mailing", + "mailman", + "main", + "mainland", + "mainstream", + "maintainer", + "maintenance", + "maize", + "major", + "major-league", + "majority", + "makeover", + "maker", + "makeup", + "making", + "male", + "malice", + "mall", + "mallard", + "mallet", + "malnutrition", + "mama", + "mambo", + "mammoth", + "man", + "manacle", + "management", + "manager", + "manatee", + "mandarin", + "mandate", + "mandolin", + "mangle", + "mango", + "mangrove", + "manhunt", + "maniac", + "manicure", + "manifestation", + "manipulation", + "mankind", + "manner", + "manor", + "mansard", + "manservant", + "mansion", + "mantel", + "mantle", + "mantua", + "manufacturer", + "manufacturing", + "many", + "map", + "maple", + "mapping", + "maracas", + "marathon", + "marble", + "march", + "mare", + "margarine", + "margin", + "mariachi", + "marimba", + "marines", + "marionberry", + "mark", + "marker", + "market", + "marketer", + "marketing", + "marketplace", + "marksman", + "markup", + "marmalade", + "marriage", + "marsh", + "marshland", + "marshmallow", + "marten", + "marxism", + "mascara", + "mask", + "masonry", + "mass", + "massage", + "mast", + "master", + "masterpiece", + "mastication", + "mastoid", + "mat", + "match", + "matchmaker", + "mate", + "material", + "maternity", + "math", + "mathematics", + "matrix", + "matter", + "mattock", + "mattress", + "max", + "maximum", + "maybe", + "mayonnaise", + "mayor", + "meadow", + "meal", + "mean", + "meander", + "meaning", + "means", + "meantime", + "measles", + "measure", + "measurement", + "meat", + "meatball", + "meatloaf", + "mecca", + "mechanic", + "mechanism", + "med", + "medal", + "media", + "median", + "medication", + "medicine", + "medium", + "meet", + "meeting", + "melatonin", + "melody", + "melon", + "member", + "membership", + "membrane", + "meme", + "memo", + "memorial", + "memory", + "men", + "menopause", + "menorah", + "mention", + "mentor", + "menu", + "merchandise", + "merchant", + "mercury", + "meridian", + "meringue", + "merit", + "mesenchyme", + "mess", + "message", + "messenger", + "messy", + "metabolite", + "metal", + "metallurgist", + "metaphor", + "meteor", + "meteorology", + "meter", + "methane", + "method", + "methodology", + "metric", + "metro", + "metronome", + "mezzanine", + "microlending", + "micronutrient", + "microphone", + "microwave", + "mid-course", + "midden", + "middle", + "middleman", + "midline", + "midnight", + "midwife", + "might", + "migrant", + "migration", + "mile", + "mileage", + "milepost", + "milestone", + "military", + "milk", + "milkshake", + "mill", + "millennium", + "millet", + "millimeter", + "million", + "millisecond", + "millstone", + "mime", + "mimosa", + "min", + "mincemeat", + "mind", + "mine", + "mineral", + "mineshaft", + "mini", + "mini-skirt", + "minibus", + "minimalism", + "minimum", + "mining", + "minion", + "minister", + "mink", + "minnow", + "minor", + "minor-league", + "minority", + "mint", + "minute", + "miracle", + "mirror", + "miscarriage", + "miscommunication", + "misfit", + "misnomer", + "misogyny", + "misplacement", + "misreading", + "misrepresentation", + "miss", + "missile", + "mission", + "missionary", + "mist", + "mistake", + "mister", + "misunderstand", + "miter", + "mitten", + "mix", + "mixer", + "mixture", + "moai", + "moat", + "mob", + "mobile", + "mobility", + "mobster", + "moccasins", + "mocha", + "mochi", + "mode", + "model", + "modeling", + "modem", + "modernist", + "modernity", + "modification", + "molar", + "molasses", + "molding", + "mole", + "molecule", + "mom", + "moment", + "monastery", + "monasticism", + "money", + "monger", + "monitor", + "monitoring", + "monk", + "monkey", + "monocle", + "monopoly", + "monotheism", + "monsoon", + "monster", + "month", + "monument", + "mood", + "moody", + "moon", + "moonlight", + "moonscape", + "moonshine", + "moose", + "mop", + "morale", + "morbid", + "morbidity", + "morning", + "moron", + "morphology", + "morsel", + "mortal", + "mortality", + "mortgage", + "mortise", + "mosque", + "mosquito", + "most", + "motel", + "moth", + "mother", + "mother-in-law", + "motion", + "motivation", + "motive", + "motor", + "motorboat", + "motorcar", + "motorcycle", + "mound", + "mountain", + "mouse", + "mouser", + "mousse", + "moustache", + "mouth", + "mouton", + "movement", + "mover", + "movie", + "mower", + "mozzarella", + "mud", + "muffin", + "mug", + "mukluk", + "mule", + "multimedia", + "murder", + "muscat", + "muscatel", + "muscle", + "musculature", + "museum", + "mushroom", + "music", + "music-box", + "music-making", + "musician", + "muskrat", + "mussel", + "mustache", + "mustard", + "mutation", + "mutt", + "mutton", + "mycoplasma", + "mystery", + "myth", + "mythology", + "nail", + "name", + "naming", + "nanoparticle", + "napkin", + "narrative", + "nasal", + "nation", + "nationality", + "native", + "naturalisation", + "nature", + "navigation", + "necessity", + "neck", + "necklace", + "necktie", + "nectar", + "nectarine", + "need", + "needle", + "neglect", + "negligee", + "negotiation", + "neighbor", + "neighborhood", + "neighbour", + "neighbourhood", + "neologism", + "neon", + "neonate", + "nephew", + "nerve", + "nest", + "nestling", + "nestmate", + "net", + "netball", + "netbook", + "netsuke", + "network", + "networking", + "neurobiologist", + "neuron", + "neuropathologist", + "neuropsychiatry", + "news", + "newsletter", + "newspaper", + "newsprint", + "newsstand", + "nexus", + "nibble", + "nicety", + "niche", + "nick", + "nickel", + "nickname", + "niece", + "night", + "nightclub", + "nightgown", + "nightingale", + "nightlife", + "nightlight", + "nightmare", + "ninja", + "nit", + "nitrogen", + "nobody", + "nod", + "node", + "noir", + "noise", + "nonbeliever", + "nonconformist", + "nondisclosure", + "nonsense", + "noodle", + "noodles", + "noon", + "norm", + "normal", + "normalisation", + "normalization", + "north", + "nose", + "notation", + "note", + "notebook", + "notepad", + "nothing", + "notice", + "notion", + "notoriety", + "nougat", + "noun", + "nourishment", + "novel", + "nucleotidase", + "nucleotide", + "nudge", + "nuke", + "number", + "numeracy", + "numeric", + "numismatist", + "nun", + "nurse", + "nursery", + "nursing", + "nurture", + "nut", + "nutmeg", + "nutrient", + "nutrition", + "nylon", + "nymph", + "oak", + "oar", + "oasis", + "oat", + "oatmeal", + "oats", + "obedience", + "obesity", + "obi", + "object", + "objection", + "objective", + "obligation", + "oboe", + "observation", + "observatory", + "obsession", + "obsidian", + "obstacle", + "occasion", + "occupation", + "occurrence", + "ocean", + "ocelot", + "octagon", + "octave", + "octavo", + "octet", + "octopus", + "odometer", + "odyssey", + "oeuvre", + "off-ramp", + "offence", + "offense", + "offer", + "offering", + "office", + "officer", + "official", + "offset", + "oil", + "okra", + "oldie", + "oleo", + "olive", + "omega", + "omelet", + "omission", + "omnivore", + "oncology", + "onion", + "online", + "onset", + "opening", + "opera", + "operating", + "operation", + "operator", + "ophthalmologist", + "opinion", + "opium", + "opossum", + "opponent", + "opportunist", + "opportunity", + "opposite", + "opposition", + "optimal", + "optimisation", + "optimist", + "optimization", + "option", + "orange", + "orangutan", + "orator", + "orchard", + "orchestra", + "orchid", + "order", + "ordinary", + "ordination", + "ore", + "oregano", + "organ", + "organisation", + "organising", + "organization", + "organizing", + "orient", + "orientation", + "origin", + "original", + "originality", + "ornament", + "osmosis", + "osprey", + "ostrich", + "other", + "otter", + "ottoman", + "ounce", + "outback", + "outcome", + "outfielder", + "outfit", + "outhouse", + "outlaw", + "outlay", + "outlet", + "outline", + "outlook", + "output", + "outrage", + "outrigger", + "outrun", + "outset", + "outside", + "oval", + "ovary", + "oven", + "overcharge", + "overclocking", + "overcoat", + "overexertion", + "overflight", + "overhead", + "overheard", + "overload", + "overnighter", + "overshoot", + "oversight", + "overview", + "overweight", + "owl", + "owner", + "ownership", + "ox", + "oxford", + "oxygen", + "oyster", + "ozone", + "pace", + "pacemaker", + "pack", + "package", + "packaging", + "packet", + "pad", + "paddle", + "paddock", + "pagan", + "page", + "pagoda", + "pail", + "pain", + "paint", + "painter", + "painting", + "paintwork", + "pair", + "pajamas", + "palace", + "palate", + "palm", + "pamphlet", + "pan", + "pancake", + "pancreas", + "panda", + "panel", + "panic", + "pannier", + "panpipe", + "pansy", + "panther", + "panties", + "pantologist", + "pantology", + "pantry", + "pants", + "pantsuit", + "panty", + "pantyhose", + "papa", + "papaya", + "paper", + "paperback", + "paperwork", + "parable", + "parachute", + "parade", + "paradise", + "paragraph", + "parallelogram", + "paramecium", + "paramedic", + "parameter", + "paranoia", + "parcel", + "parchment", + "pard", + "pardon", + "parent", + "parenthesis", + "parenting", + "park", + "parka", + "parking", + "parliament", + "parole", + "parrot", + "parser", + "parsley", + "parsnip", + "part", + "participant", + "participation", + "particle", + "particular", + "partner", + "partnership", + "partridge", + "party", + "pass", + "passage", + "passbook", + "passenger", + "passing", + "passion", + "passive", + "passport", + "password", + "past", + "pasta", + "paste", + "pastor", + "pastoralist", + "pastry", + "pasture", + "pat", + "patch", + "pate", + "patent", + "patentee", + "path", + "pathogenesis", + "pathology", + "pathway", + "patience", + "patient", + "patina", + "patio", + "patriarch", + "patrimony", + "patriot", + "patrol", + "patroller", + "patrolling", + "patron", + "pattern", + "patty", + "pattypan", + "pause", + "pavement", + "pavilion", + "paw", + "pawnshop", + "pay", + "payee", + "payment", + "payoff", + "pea", + "peace", + "peach", + "peacoat", + "peacock", + "peak", + "peanut", + "pear", + "pearl", + "peasant", + "pecan", + "pecker", + "pedal", + "peek", + "peen", + "peer", + "peer-to-peer", + "pegboard", + "pelican", + "pelt", + "pen", + "penalty", + "pence", + "pencil", + "pendant", + "pendulum", + "penguin", + "penicillin", + "peninsula", + "penis", + "pennant", + "penny", + "pension", + "pentagon", + "peony", + "people", + "pepper", + "pepperoni", + "percent", + "percentage", + "perception", + "perch", + "perennial", + "perfection", + "performance", + "perfume", + "period", + "periodical", + "peripheral", + "permafrost", + "permission", + "permit", + "perp", + "perpendicular", + "persimmon", + "person", + "personal", + "personality", + "personnel", + "perspective", + "pest", + "pet", + "petal", + "petition", + "petitioner", + "petticoat", + "pew", + "pharmacist", + "pharmacopoeia", + "phase", + "pheasant", + "phenomenon", + "phenotype", + "pheromone", + "philanthropy", + "philosopher", + "philosophy", + "phone", + "phosphate", + "photo", + "photodiode", + "photograph", + "photographer", + "photography", + "photoreceptor", + "phrase", + "phrasing", + "physical", + "physics", + "physiology", + "pianist", + "piano", + "piccolo", + "pick", + "pickax", + "pickaxe", + "picket", + "pickle", + "pickup", + "picnic", + "picture", + "picturesque", + "pie", + "piece", + "pier", + "piety", + "pig", + "pigeon", + "piglet", + "pigpen", + "pigsty", + "pike", + "pilaf", + "pile", + "pilgrim", + "pilgrimage", + "pill", + "pillar", + "pillbox", + "pillow", + "pilot", + "pimp", + "pimple", + "pin", + "pinafore", + "pince-nez", + "pine", + "pineapple", + "pinecone", + "ping", + "pink", + "pinkie", + "pinot", + "pinstripe", + "pint", + "pinto", + "pinworm", + "pioneer", + "pipe", + "pipeline", + "piracy", + "pirate", + "piss", + "pistol", + "pit", + "pita", + "pitch", + "pitcher", + "pitching", + "pith", + "pizza", + "place", + "placebo", + "placement", + "placode", + "plagiarism", + "plain", + "plaintiff", + "plan", + "plane", + "planet", + "planning", + "plant", + "plantation", + "planter", + "planula", + "plaster", + "plasterboard", + "plastic", + "plate", + "platelet", + "platform", + "platinum", + "platter", + "platypus", + "play", + "player", + "playground", + "playroom", + "playwright", + "plea", + "pleasure", + "pleat", + "pledge", + "plenty", + "plier", + "pliers", + "plight", + "plot", + "plough", + "plover", + "plow", + "plowman", + "plug", + "plugin", + "plum", + "plumber", + "plume", + "plunger", + "plywood", + "pneumonia", + "pocket", + "pocket-watch", + "pocketbook", + "pod", + "podcast", + "poem", + "poet", + "poetry", + "poignance", + "point", + "poison", + "poisoning", + "poker", + "polarisation", + "polarization", + "pole", + "polenta", + "police", + "policeman", + "policy", + "polish", + "politician", + "politics", + "poll", + "polliwog", + "pollutant", + "pollution", + "polo", + "polyester", + "polyp", + "pomegranate", + "pomelo", + "pompom", + "poncho", + "pond", + "pony", + "pool", + "poor", + "pop", + "popcorn", + "poppy", + "popsicle", + "popularity", + "population", + "populist", + "porcelain", + "porch", + "porcupine", + "pork", + "porpoise", + "port", + "porter", + "portfolio", + "porthole", + "portion", + "portrait", + "position", + "possession", + "possibility", + "possible", + "post", + "postage", + "postbox", + "poster", + "posterior", + "postfix", + "pot", + "potato", + "potential", + "pottery", + "potty", + "pouch", + "poultry", + "pound", + "pounding", + "poverty", + "powder", + "power", + "practice", + "practitioner", + "prairie", + "praise", + "pray", + "prayer", + "precedence", + "precedent", + "precipitation", + "precision", + "predecessor", + "preface", + "preference", + "prefix", + "pregnancy", + "prejudice", + "prelude", + "premeditation", + "premier", + "premise", + "premium", + "preoccupation", + "preparation", + "prescription", + "presence", + "present", + "presentation", + "preservation", + "preserves", + "presidency", + "president", + "press", + "pressroom", + "pressure", + "pressurisation", + "pressurization", + "prestige", + "presume", + "pretzel", + "prevalence", + "prevention", + "prey", + "price", + "pricing", + "pride", + "priest", + "priesthood", + "primary", + "primate", + "prince", + "princess", + "principal", + "principle", + "print", + "printer", + "printing", + "prior", + "priority", + "prison", + "prisoner", + "privacy", + "private", + "privilege", + "prize", + "prizefight", + "probability", + "probation", + "probe", + "problem", + "procedure", + "proceedings", + "process", + "processing", + "processor", + "proctor", + "procurement", + "produce", + "producer", + "product", + "production", + "productivity", + "profession", + "professional", + "professor", + "profile", + "profit", + "progenitor", + "program", + "programme", + "programming", + "progress", + "progression", + "prohibition", + "project", + "proliferation", + "promenade", + "promise", + "promotion", + "prompt", + "pronoun", + "pronunciation", + "proof", + "proof-reader", + "propaganda", + "propane", + "property", + "prophet", + "proponent", + "proportion", + "proposal", + "proposition", + "proprietor", + "prose", + "prosecution", + "prosecutor", + "prospect", + "prosperity", + "prostacyclin", + "prostanoid", + "prostrate", + "protection", + "protein", + "protest", + "protocol", + "providence", + "provider", + "province", + "provision", + "prow", + "proximal", + "proximity", + "prune", + "pruner", + "pseudocode", + "pseudoscience", + "psychiatrist", + "psychoanalyst", + "psychologist", + "psychology", + "ptarmigan", + "pub", + "public", + "publication", + "publicity", + "publisher", + "publishing", + "pudding", + "puddle", + "puffin", + "pug", + "puggle", + "pulley", + "pulse", + "puma", + "pump", + "pumpernickel", + "pumpkin", + "pumpkinseed", + "pun", + "punch", + "punctuation", + "punishment", + "pup", + "pupa", + "pupil", + "puppet", + "puppy", + "purchase", + "puritan", + "purity", + "purple", + "purpose", + "purr", + "purse", + "pursuit", + "push", + "pusher", + "put", + "puzzle", + "pyramid", + "pyridine", + "quadrant", + "quail", + "qualification", + "quality", + "quantity", + "quart", + "quarter", + "quartet", + "quartz", + "queen", + "query", + "quest", + "question", + "questioner", + "questionnaire", + "quiche", + "quicksand", + "quiet", + "quill", + "quilt", + "quince", + "quinoa", + "quit", + "quiver", + "quota", + "quotation", + "quote", + "rabbi", + "rabbit", + "raccoon", + "race", + "racer", + "racing", + "racism", + "racist", + "rack", + "radar", + "radiator", + "radio", + "radiosonde", + "radish", + "raffle", + "raft", + "rag", + "rage", + "raid", + "rail", + "railing", + "railroad", + "railway", + "raiment", + "rain", + "rainbow", + "raincoat", + "rainmaker", + "rainstorm", + "rainy", + "raise", + "raisin", + "rake", + "rally", + "ram", + "rambler", + "ramen", + "ramie", + "ranch", + "rancher", + "randomisation", + "randomization", + "range", + "ranger", + "rank", + "rap", + "rape", + "raspberry", + "rat", + "rate", + "ratepayer", + "rating", + "ratio", + "rationale", + "rations", + "raven", + "ravioli", + "rawhide", + "ray", + "rayon", + "razor", + "reach", + "reactant", + "reaction", + "read", + "reader", + "readiness", + "reading", + "real", + "reality", + "realization", + "realm", + "reamer", + "rear", + "reason", + "reasoning", + "rebel", + "rebellion", + "reboot", + "recall", + "recapitulation", + "receipt", + "receiver", + "reception", + "receptor", + "recess", + "recession", + "recipe", + "recipient", + "reciprocity", + "reclamation", + "recliner", + "recognition", + "recollection", + "recommendation", + "reconsideration", + "record", + "recorder", + "recording", + "recovery", + "recreation", + "recruit", + "rectangle", + "red", + "redesign", + "redhead", + "redirect", + "rediscovery", + "reduction", + "reef", + "refectory", + "reference", + "referendum", + "reflection", + "reform", + "refreshments", + "refrigerator", + "refuge", + "refund", + "refusal", + "refuse", + "regard", + "regime", + "region", + "regionalism", + "register", + "registration", + "registry", + "regret", + "regulation", + "regulator", + "rehospitalisation", + "rehospitalization", + "reindeer", + "reinscription", + "reject", + "relation", + "relationship", + "relative", + "relaxation", + "relay", + "release", + "reliability", + "relief", + "religion", + "relish", + "reluctance", + "remains", + "remark", + "reminder", + "remnant", + "remote", + "removal", + "renaissance", + "rent", + "reorganisation", + "reorganization", + "repair", + "reparation", + "repayment", + "repeat", + "replacement", + "replica", + "replication", + "reply", + "report", + "reporter", + "reporting", + "repository", + "representation", + "representative", + "reprocessing", + "republic", + "republican", + "reputation", + "request", + "requirement", + "resale", + "rescue", + "research", + "researcher", + "resemblance", + "reservation", + "reserve", + "reservoir", + "reset", + "residence", + "resident", + "residue", + "resist", + "resistance", + "resolution", + "resolve", + "resort", + "resource", + "respect", + "respite", + "response", + "responsibility", + "rest", + "restaurant", + "restoration", + "restriction", + "restroom", + "restructuring", + "result", + "resume", + "retailer", + "retention", + "rethinking", + "retina", + "retirement", + "retouching", + "retreat", + "retrospect", + "retrospective", + "retrospectivity", + "return", + "reunion", + "revascularisation", + "revascularization", + "reveal", + "revelation", + "revenant", + "revenge", + "revenue", + "reversal", + "reverse", + "review", + "revitalisation", + "revitalization", + "revival", + "revolution", + "revolver", + "reward", + "rhetoric", + "rheumatism", + "rhinoceros", + "rhubarb", + "rhyme", + "rhythm", + "rib", + "ribbon", + "rice", + "riddle", + "ride", + "rider", + "ridge", + "riding", + "rifle", + "right", + "rim", + "ring", + "ringworm", + "riot", + "rip", + "ripple", + "rise", + "riser", + "risk", + "rite", + "ritual", + "river", + "riverbed", + "rivulet", + "road", + "roadway", + "roar", + "roast", + "robe", + "robin", + "robot", + "robotics", + "rock", + "rocker", + "rocket", + "rocket-ship", + "rod", + "role", + "roll", + "roller", + "romaine", + "romance", + "roof", + "room", + "roommate", + "rooster", + "root", + "rope", + "rose", + "rosemary", + "roster", + "rostrum", + "rotation", + "round", + "roundabout", + "route", + "router", + "routine", + "row", + "rowboat", + "rowing", + "rubber", + "rubbish", + "rubric", + "ruby", + "ruckus", + "rudiment", + "ruffle", + "rug", + "rugby", + "ruin", + "rule", + "ruler", + "ruling", + "rum", + "rumor", + "run", + "runaway", + "runner", + "running", + "runway", + "rush", + "rust", + "rutabaga", + "rye", + "sabre", + "sac", + "sack", + "saddle", + "sadness", + "safari", + "safe", + "safeguard", + "safety", + "saffron", + "sage", + "sail", + "sailboat", + "sailing", + "sailor", + "saint", + "sake", + "salad", + "salami", + "salary", + "sale", + "salesman", + "salmon", + "salon", + "saloon", + "salsa", + "salt", + "salute", + "samovar", + "sampan", + "sample", + "samurai", + "sanction", + "sanctity", + "sanctuary", + "sand", + "sandal", + "sandbar", + "sandpaper", + "sandwich", + "sanity", + "sardine", + "sari", + "sarong", + "sash", + "satellite", + "satin", + "satire", + "satisfaction", + "sauce", + "saucer", + "sauerkraut", + "sausage", + "savage", + "savannah", + "saving", + "savings", + "savior", + "saviour", + "savory", + "saw", + "saxophone", + "scaffold", + "scale", + "scallion", + "scallops", + "scalp", + "scam", + "scanner", + "scarecrow", + "scarf", + "scarification", + "scenario", + "scene", + "scenery", + "scent", + "schedule", + "scheduling", + "schema", + "scheme", + "schizophrenic", + "schnitzel", + "scholar", + "scholarship", + "school", + "schoolhouse", + "schooner", + "science", + "scientist", + "scimitar", + "scissors", + "scooter", + "scope", + "score", + "scorn", + "scorpion", + "scotch", + "scout", + "scow", + "scrambled", + "scrap", + "scraper", + "scratch", + "screamer", + "screen", + "screening", + "screenwriting", + "screw", + "screw-up", + "screwdriver", + "scrim", + "scrip", + "script", + "scripture", + "scrutiny", + "sculpting", + "sculptural", + "sculpture", + "sea", + "seabass", + "seafood", + "seagull", + "seal", + "seaplane", + "search", + "seashore", + "seaside", + "season", + "seat", + "seaweed", + "second", + "secrecy", + "secret", + "secretariat", + "secretary", + "secretion", + "section", + "sectional", + "sector", + "security", + "sediment", + "seed", + "seeder", + "seeker", + "seep", + "segment", + "seizure", + "selection", + "self", + "self-confidence", + "self-control", + "self-esteem", + "seller", + "selling", + "semantics", + "semester", + "semicircle", + "semicolon", + "semiconductor", + "seminar", + "senate", + "senator", + "sender", + "senior", + "sense", + "sensibility", + "sensitive", + "sensitivity", + "sensor", + "sentence", + "sentencing", + "sentiment", + "sepal", + "separation", + "septicaemia", + "sequel", + "sequence", + "serial", + "series", + "sermon", + "serum", + "serval", + "servant", + "server", + "service", + "servitude", + "sesame", + "session", + "set", + "setback", + "setting", + "settlement", + "settler", + "severity", + "sewer", + "sex", + "sexuality", + "shack", + "shackle", + "shade", + "shadow", + "shadowbox", + "shakedown", + "shaker", + "shallot", + "shallows", + "shame", + "shampoo", + "shanty", + "shape", + "share", + "shareholder", + "shark", + "shaw", + "shawl", + "shear", + "shearling", + "sheath", + "shed", + "sheep", + "sheet", + "shelf", + "shell", + "shelter", + "sherbet", + "sherry", + "shield", + "shift", + "shin", + "shine", + "shingle", + "ship", + "shipper", + "shipping", + "shipyard", + "shirt", + "shirtdress", + "shit", + "shoat", + "shock", + "shoe", + "shoe-horn", + "shoehorn", + "shoelace", + "shoemaker", + "shoes", + "shoestring", + "shofar", + "shoot", + "shootdown", + "shop", + "shopper", + "shopping", + "shore", + "shoreline", + "short", + "shortage", + "shorts", + "shortwave", + "shot", + "shoulder", + "shout", + "shovel", + "show", + "show-stopper", + "shower", + "shred", + "shrimp", + "shrine", + "shutdown", + "sibling", + "sick", + "sickness", + "side", + "sideboard", + "sideburns", + "sidecar", + "sidestream", + "sidewalk", + "siding", + "siege", + "sigh", + "sight", + "sightseeing", + "sign", + "signal", + "signature", + "signet", + "significance", + "signify", + "signup", + "silence", + "silica", + "silicon", + "silk", + "silkworm", + "sill", + "silly", + "silo", + "silver", + "similarity", + "simple", + "simplicity", + "simplification", + "simvastatin", + "sin", + "singer", + "singing", + "singular", + "sink", + "sinuosity", + "sip", + "sir", + "sister", + "sister-in-law", + "sitar", + "site", + "situation", + "size", + "skate", + "skating", + "skean", + "skeleton", + "ski", + "skiing", + "skill", + "skin", + "skirt", + "skull", + "skullcap", + "skullduggery", + "skunk", + "sky", + "skylight", + "skyline", + "skyscraper", + "skywalk", + "slang", + "slapstick", + "slash", + "slate", + "slave", + "slavery", + "slaw", + "sled", + "sledge", + "sleep", + "sleepiness", + "sleeping", + "sleet", + "sleuth", + "slice", + "slide", + "slider", + "slime", + "slip", + "slipper", + "slippers", + "slope", + "slot", + "sloth", + "slump", + "smell", + "smelting", + "smile", + "smith", + "smock", + "smog", + "smoke", + "smoking", + "smolt", + "smuggling", + "snack", + "snail", + "snake", + "snakebite", + "snap", + "snarl", + "sneaker", + "sneakers", + "sneeze", + "sniffle", + "snob", + "snorer", + "snow", + "snowboarding", + "snowflake", + "snowman", + "snowmobiling", + "snowplow", + "snowstorm", + "snowsuit", + "snuck", + "snug", + "snuggle", + "soap", + "soccer", + "socialism", + "socialist", + "society", + "sociology", + "sock", + "socks", + "soda", + "sofa", + "softball", + "softdrink", + "softening", + "software", + "soil", + "soldier", + "sole", + "solicitation", + "solicitor", + "solidarity", + "solidity", + "soliloquy", + "solitaire", + "solution", + "solvency", + "sombrero", + "somebody", + "someone", + "someplace", + "somersault", + "something", + "somewhere", + "son", + "sonar", + "sonata", + "song", + "songbird", + "sonnet", + "soot", + "sophomore", + "soprano", + "sorbet", + "sorghum", + "sorrel", + "sorrow", + "sort", + "soul", + "soulmate", + "sound", + "soundness", + "soup", + "source", + "sourwood", + "sousaphone", + "south", + "southeast", + "souvenir", + "sovereignty", + "sow", + "soy", + "soybean", + "space", + "spacing", + "spade", + "spaghetti", + "span", + "spandex", + "spank", + "sparerib", + "spark", + "sparrow", + "spasm", + "spat", + "spatula", + "spawn", + "speaker", + "speakerphone", + "speaking", + "spear", + "spec", + "special", + "specialist", + "specialty", + "species", + "specification", + "spectacle", + "spectacles", + "spectrograph", + "spectrum", + "speculation", + "speech", + "speed", + "speedboat", + "spell", + "spelling", + "spelt", + "spending", + "sphere", + "sphynx", + "spice", + "spider", + "spiderling", + "spike", + "spill", + "spinach", + "spine", + "spiral", + "spirit", + "spiritual", + "spirituality", + "spit", + "spite", + "spleen", + "splendor", + "split", + "spokesman", + "spokeswoman", + "sponge", + "sponsor", + "sponsorship", + "spool", + "spoon", + "spork", + "sport", + "sportsman", + "spot", + "spotlight", + "spouse", + "sprag", + "sprat", + "spray", + "spread", + "spreadsheet", + "spree", + "spring", + "sprinkles", + "sprinter", + "sprout", + "spruce", + "spud", + "spume", + "spur", + "spy", + "spyglass", + "square", + "squash", + "squatter", + "squeegee", + "squid", + "squirrel", + "stab", + "stability", + "stable", + "stack", + "stacking", + "stadium", + "staff", + "stag", + "stage", + "stain", + "stair", + "staircase", + "stake", + "stalk", + "stall", + "stallion", + "stamen", + "stamina", + "stamp", + "stance", + "stand", + "standard", + "standardisation", + "standardization", + "standing", + "standoff", + "standpoint", + "star", + "starboard", + "start", + "starter", + "state", + "statement", + "statin", + "station", + "station-wagon", + "statistic", + "statistics", + "statue", + "status", + "statute", + "stay", + "steak", + "stealth", + "steam", + "steamroller", + "steel", + "steeple", + "stem", + "stench", + "stencil", + "step", + "step-aunt", + "step-brother", + "step-daughter", + "step-father", + "step-grandfather", + "step-grandmother", + "step-mother", + "step-sister", + "step-son", + "step-uncle", + "stepdaughter", + "stepmother", + "stepping-stone", + "stepson", + "stereo", + "stew", + "steward", + "stick", + "sticker", + "stiletto", + "still", + "stimulation", + "stimulus", + "sting", + "stinger", + "stir-fry", + "stitch", + "stitcher", + "stock", + "stock-in-trade", + "stockings", + "stole", + "stomach", + "stone", + "stonework", + "stool", + "stop", + "stopsign", + "stopwatch", + "storage", + "store", + "storey", + "storm", + "story", + "story-telling", + "storyboard", + "stot", + "stove", + "strait", + "strand", + "stranger", + "strap", + "strategy", + "straw", + "strawberry", + "strawman", + "stream", + "street", + "streetcar", + "strength", + "stress", + "stretch", + "strife", + "strike", + "string", + "strip", + "stripe", + "strobe", + "stroke", + "structure", + "strudel", + "struggle", + "stucco", + "stud", + "student", + "studio", + "study", + "stuff", + "stumbling", + "stump", + "stupidity", + "sturgeon", + "sty", + "style", + "styling", + "stylus", + "sub", + "subcomponent", + "subconscious", + "subcontractor", + "subexpression", + "subgroup", + "subject", + "submarine", + "submitter", + "subprime", + "subroutine", + "subscription", + "subsection", + "subset", + "subsidence", + "subsidiary", + "subsidy", + "substance", + "substitution", + "subtitle", + "suburb", + "subway", + "success", + "succotash", + "suck", + "sucker", + "suede", + "suet", + "suffocation", + "sugar", + "suggestion", + "suicide", + "suit", + "suitcase", + "suite", + "sulfur", + "sultan", + "sum", + "summary", + "summer", + "summit", + "sun", + "sunbeam", + "sunbonnet", + "sundae", + "sunday", + "sundial", + "sunflower", + "sunglasses", + "sunlamp", + "sunlight", + "sunrise", + "sunroom", + "sunset", + "sunshine", + "superiority", + "supermarket", + "supernatural", + "supervision", + "supervisor", + "supper", + "supplement", + "supplier", + "supply", + "support", + "supporter", + "suppression", + "supreme", + "surface", + "surfboard", + "surge", + "surgeon", + "surgery", + "surname", + "surplus", + "surprise", + "surround", + "surroundings", + "surrounds", + "survey", + "survival", + "survivor", + "sushi", + "suspect", + "suspenders", + "suspension", + "sustainment", + "sustenance", + "swallow", + "swamp", + "swan", + "swanling", + "swath", + "sweat", + "sweater", + "sweatshirt", + "sweatshop", + "sweatsuit", + "sweets", + "swell", + "swim", + "swimming", + "swimsuit", + "swine", + "swing", + "switch", + "switchboard", + "switching", + "swivel", + "sword", + "swordfight", + "swordfish", + "sycamore", + "symbol", + "symmetry", + "sympathy", + "symptom", + "syndicate", + "syndrome", + "synergy", + "synod", + "synonym", + "synthesis", + "syrup", + "system", + "t-shirt", + "tab", + "tabby", + "tabernacle", + "table", + "tablecloth", + "tablet", + "tabletop", + "tachometer", + "tackle", + "taco", + "tactics", + "tactile", + "tadpole", + "tag", + "tail", + "tailbud", + "tailor", + "tailspin", + "take-out", + "takeover", + "tale", + "talent", + "talk", + "talking", + "tam-o'-shanter", + "tamale", + "tambour", + "tambourine", + "tan", + "tandem", + "tangerine", + "tank", + "tank-top", + "tanker", + "tankful", + "tap", + "tape", + "tapioca", + "target", + "taro", + "tarragon", + "tart", + "task", + "tassel", + "taste", + "tatami", + "tattler", + "tattoo", + "tavern", + "tax", + "taxi", + "taxicab", + "taxpayer", + "tea", + "teacher", + "teaching", + "team", + "teammate", + "teapot", + "tear", + "tech", + "technician", + "technique", + "technologist", + "technology", + "tectonics", + "teen", + "teenager", + "teepee", + "telephone", + "telescreen", + "teletype", + "television", + "tell", + "teller", + "temp", + "temper", + "temperature", + "temple", + "tempo", + "temporariness", + "temporary", + "temptation", + "temptress", + "tenant", + "tendency", + "tender", + "tenement", + "tenet", + "tennis", + "tenor", + "tension", + "tensor", + "tent", + "tentacle", + "tenth", + "tepee", + "teriyaki", + "term", + "terminal", + "termination", + "terminology", + "termite", + "terrace", + "terracotta", + "terrapin", + "terrarium", + "territory", + "terror", + "terrorism", + "terrorist", + "test", + "testament", + "testimonial", + "testimony", + "testing", + "text", + "textbook", + "textual", + "texture", + "thanks", + "thaw", + "theater", + "theft", + "theism", + "theme", + "theology", + "theory", + "therapist", + "therapy", + "thermals", + "thermometer", + "thermostat", + "thesis", + "thickness", + "thief", + "thigh", + "thing", + "thinking", + "thirst", + "thistle", + "thong", + "thongs", + "thorn", + "thought", + "thousand", + "thread", + "threat", + "threshold", + "thrift", + "thrill", + "throat", + "throne", + "thrush", + "thrust", + "thug", + "thumb", + "thump", + "thunder", + "thunderbolt", + "thunderhead", + "thunderstorm", + "thyme", + "tiara", + "tic", + "tick", + "ticket", + "tide", + "tie", + "tiger", + "tights", + "tile", + "till", + "tilt", + "timbale", + "timber", + "time", + "timeline", + "timeout", + "timer", + "timetable", + "timing", + "timpani", + "tin", + "tinderbox", + "tinkle", + "tintype", + "tip", + "tire", + "tissue", + "titanium", + "title", + "toad", + "toast", + "toaster", + "tobacco", + "today", + "toe", + "toenail", + "toffee", + "tofu", + "tog", + "toga", + "toilet", + "tolerance", + "tolerant", + "toll", + "tom-tom", + "tomatillo", + "tomato", + "tomb", + "tomography", + "tomorrow", + "ton", + "tonality", + "tone", + "tongue", + "tonic", + "tonight", + "tool", + "toot", + "tooth", + "toothbrush", + "toothpaste", + "toothpick", + "top", + "top-hat", + "topic", + "topsail", + "toque", + "toreador", + "tornado", + "torso", + "torte", + "tortellini", + "tortilla", + "tortoise", + "tosser", + "total", + "tote", + "touch", + "tough-guy", + "tour", + "tourism", + "tourist", + "tournament", + "tow-truck", + "towel", + "tower", + "town", + "townhouse", + "township", + "toy", + "trace", + "trachoma", + "track", + "tracking", + "tracksuit", + "tract", + "tractor", + "trade", + "trader", + "trading", + "tradition", + "traditionalism", + "traffic", + "trafficker", + "tragedy", + "trail", + "trailer", + "trailpatrol", + "train", + "trainer", + "training", + "trait", + "tram", + "tramp", + "trance", + "transaction", + "transcript", + "transfer", + "transformation", + "transit", + "transition", + "translation", + "transmission", + "transom", + "transparency", + "transplantation", + "transport", + "transportation", + "trap", + "trapdoor", + "trapezium", + "trapezoid", + "trash", + "travel", + "traveler", + "tray", + "treasure", + "treasury", + "treat", + "treatment", + "treaty", + "tree", + "trek", + "trellis", + "tremor", + "trench", + "trend", + "triad", + "trial", + "triangle", + "tribe", + "tributary", + "trick", + "trigger", + "trigonometry", + "trillion", + "trim", + "trinket", + "trip", + "tripod", + "tritone", + "triumph", + "trolley", + "trombone", + "troop", + "trooper", + "trophy", + "trouble", + "trousers", + "trout", + "trove", + "trowel", + "truck", + "trumpet", + "trunk", + "trust", + "trustee", + "truth", + "try", + "tsunami", + "tub", + "tuba", + "tube", + "tuber", + "tug", + "tugboat", + "tuition", + "tulip", + "tumbler", + "tummy", + "tuna", + "tune", + "tune-up", + "tunic", + "tunnel", + "turban", + "turf", + "turkey", + "turmeric", + "turn", + "turning", + "turnip", + "turnover", + "turnstile", + "turret", + "turtle", + "tusk", + "tussle", + "tutu", + "tuxedo", + "tweet", + "tweezers", + "twig", + "twilight", + "twine", + "twins", + "twist", + "twister", + "twitter", + "type", + "typeface", + "typewriter", + "typhoon", + "ukulele", + "ultimatum", + "umbrella", + "unblinking", + "uncertainty", + "uncle", + "underclothes", + "underestimate", + "underground", + "underneath", + "underpants", + "underpass", + "undershirt", + "understanding", + "understatement", + "undertaker", + "underwear", + "underweight", + "underwire", + "underwriting", + "unemployment", + "unibody", + "uniform", + "uniformity", + "union", + "unique", + "unit", + "unity", + "universe", + "university", + "update", + "upgrade", + "uplift", + "upper", + "upstairs", + "upward", + "urge", + "urgency", + "urn", + "usage", + "use", + "user", + "usher", + "usual", + "utensil", + "utilisation", + "utility", + "utilization", + "vacation", + "vaccine", + "vacuum", + "vagrant", + "valance", + "valentine", + "validate", + "validity", + "valley", + "valuable", + "value", + "vampire", + "van", + "vanadyl", + "vane", + "vanilla", + "vanity", + "variability", + "variable", + "variant", + "variation", + "variety", + "vascular", + "vase", + "vault", + "vaulting", + "veal", + "vector", + "vegetable", + "vegetarian", + "vegetarianism", + "vegetation", + "vehicle", + "veil", + "vein", + "veldt", + "vellum", + "velocity", + "velodrome", + "velvet", + "vendor", + "veneer", + "vengeance", + "venison", + "venom", + "venti", + "venture", + "venue", + "veranda", + "verb", + "verdict", + "verification", + "vermicelli", + "vernacular", + "verse", + "version", + "vertigo", + "verve", + "vessel", + "vest", + "vestment", + "vet", + "veteran", + "veterinarian", + "veto", + "viability", + "vibe", + "vibraphone", + "vibration", + "vibrissae", + "vice", + "vicinity", + "victim", + "victory", + "video", + "view", + "viewer", + "vignette", + "villa", + "village", + "vine", + "vinegar", + "vineyard", + "vintage", + "vintner", + "vinyl", + "viola", + "violation", + "violence", + "violet", + "violin", + "virginal", + "virtue", + "virus", + "visa", + "viscose", + "vise", + "vision", + "visit", + "visitor", + "visor", + "vista", + "visual", + "vitality", + "vitamin", + "vitro", + "vivo", + "vixen", + "vodka", + "vogue", + "voice", + "void", + "vol", + "volatility", + "volcano", + "volleyball", + "volume", + "volunteer", + "volunteering", + "vomit", + "vote", + "voter", + "voting", + "voyage", + "vulture", + "wad", + "wafer", + "waffle", + "wage", + "wagon", + "waist", + "waistband", + "wait", + "waiter", + "waiting", + "waitress", + "waiver", + "wake", + "walk", + "walker", + "walking", + "walkway", + "wall", + "wallaby", + "wallet", + "walnut", + "walrus", + "wampum", + "wannabe", + "want", + "war", + "warden", + "wardrobe", + "warfare", + "warlock", + "warlord", + "warm-up", + "warming", + "warmth", + "warning", + "warrant", + "warren", + "warrior", + "wasabi", + "wash", + "washbasin", + "washcloth", + "washer", + "washtub", + "wasp", + "waste", + "wastebasket", + "wasting", + "watch", + "watcher", + "watchmaker", + "water", + "waterbed", + "watercress", + "waterfall", + "waterfront", + "watermelon", + "waterskiing", + "waterspout", + "waterwheel", + "wave", + "waveform", + "wax", + "way", + "weakness", + "wealth", + "weapon", + "wear", + "weasel", + "weather", + "web", + "webinar", + "webmail", + "webpage", + "website", + "wedding", + "wedge", + "weed", + "weeder", + "weedkiller", + "week", + "weekend", + "weekender", + "weight", + "weird", + "welcome", + "welfare", + "well", + "well-being", + "west", + "western", + "wet-bar", + "wetland", + "wetsuit", + "whack", + "whale", + "wharf", + "wheat", + "wheel", + "whelp", + "whey", + "whip", + "whirlpool", + "whirlwind", + "whisker", + "whiskey", + "whisper", + "whistle", + "white", + "whole", + "wholesale", + "wholesaler", + "whorl", + "wick", + "widget", + "widow", + "width", + "wife", + "wifi", + "wild", + "wildebeest", + "wilderness", + "wildlife", + "will", + "willingness", + "willow", + "win", + "wind", + "wind-chime", + "windage", + "window", + "windscreen", + "windshield", + "wine", + "winery", + "wing", + "wingman", + "wingtip", + "wink", + "winner", + "winter", + "wire", + "wiretap", + "wiring", + "wisdom", + "wiseguy", + "wish", + "wisteria", + "wit", + "witch", + "witch-hunt", + "withdrawal", + "witness", + "wok", + "wolf", + "woman", + "wombat", + "wonder", + "wont", + "wood", + "woodchuck", + "woodland", + "woodshed", + "woodwind", + "wool", + "woolens", + "word", + "wording", + "work", + "workbench", + "worker", + "workforce", + "workhorse", + "working", + "workout", + "workplace", + "workshop", + "world", + "worm", + "worry", + "worship", + "worshiper", + "worth", + "wound", + "wrap", + "wraparound", + "wrapper", + "wrapping", + "wreck", + "wrecker", + "wren", + "wrench", + "wrestler", + "wriggler", + "wrinkle", + "wrist", + "writer", + "writing", + "wrong", + "xylophone", + "yacht", + "yahoo", + "yak", + "yam", + "yang", + "yard", + "yarmulke", + "yarn", + "yawl", + "year", + "yeast", + "yellow", + "yellowjacket", + "yesterday", + "yew", + "yin", + "yoga", + "yogurt", + "yoke", + "yolk", + "young", + "youngster", + "yourself", + "youth", + "yoyo", + "yurt", + "zampone", + "zebra", + "zebrafish", + "zen", + "zephyr", + "zero", + "ziggurat", + "zinc", + "zipper", + "zither", + "zombie", + "zone", + "zoo", + "zoologist", + "zoology", + "zoot-suit", + "zucchini" + }; + + private static readonly string[] Colours = + { + "red", + "blue", + "green", + "yellow", + "pink", + "orange", + "blue", + "purple", + "white", + "gold", + "silver", + "teal", + }; + #endregion + + public NounHelper(Random random) + { + _random = random; + } + + public string GetRandomNoun() + { + var index = _random.Next(0, Nouns.Length); + return Nouns[index]; + } + + public string GetRandomColour() + { + var index = _random.Next(0, Colours.Length); + return Colours[index]; + } +} \ No newline at end of file diff --git a/API/Mapping/Full.cs b/API/Mapping/Full.cs new file mode 100644 index 0000000..0d14ed9 --- /dev/null +++ b/API/Mapping/Full.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using Hesketh.MecatolArchives.DB.Models; + +namespace Hesketh.MecatolArchives.API.Mapping; + +public sealed class Full : Profile +{ + public Full() + { + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().AfterMap((dbo, dto) => + { + dto.Players = new List(dto.Players.OrderBy(x => x.Eliminated).ThenByDescending(x => x.Points)); + }); + CreateMap(); + CreateMap().ReverseMap(); + } +} \ No newline at end of file diff --git a/API/Mapping/Post.cs b/API/Mapping/Post.cs new file mode 100644 index 0000000..19edaf9 --- /dev/null +++ b/API/Mapping/Post.cs @@ -0,0 +1,18 @@ +using AutoMapper; +using Hesketh.MecatolArchives.API.Data.Post; + +namespace Hesketh.MecatolArchives.API.Mapping; + +public class Post : Profile +{ + public Post() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/API/Mapping/Put.cs b/API/Mapping/Put.cs new file mode 100644 index 0000000..10653a0 --- /dev/null +++ b/API/Mapping/Put.cs @@ -0,0 +1,12 @@ +using AutoMapper; +using Hesketh.MecatolArchives.API.Data.Put; + +namespace Hesketh.MecatolArchives.API.Mapping; + +public class Put : Profile +{ + public Put() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/API/Program.cs b/API/Program.cs new file mode 100644 index 0000000..36ec6d2 --- /dev/null +++ b/API/Program.cs @@ -0,0 +1,81 @@ +using Hesketh.MecatolArchives.API.Auth; +using Hesketh.MecatolArchives.API.Helpers; +using Hesketh.MecatolArchives.DB; +using Microsoft.AspNetCore.Authentication; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => options.CustomSchemaIds(x => +{ + // Could do with something more generic if this becomes an issue in the future + // But this is because two Models used in the API have the same name + // So this quick work around appends .Post to any models in the Post namespace + if (x.FullName!.EndsWith($"Post.{x.Name}")) return $"Post.{x.Name}"; + if (x.FullName!.EndsWith($"Put.{x.Name}")) return $"Put.{x.Name}"; + return x.Name; +})); + +builder.Services.Configure(builder.Configuration.GetSection(AdminAccountOptions.SectionName)); +builder.Services.AddTransient(); +builder.Services.AddAuthentication("BasicAuthentication") + .AddScheme("BasicAuthentication", null); + +builder.Services.AddAutoMapper(typeof(Program).Assembly); + +var databaseMode = builder.Configuration.GetValue("DatabaseMode"); +if (databaseMode == "memory") +{ + builder.Services.AddDbContext(options + => options.UseInMemoryDatabase(nameof(MecatolArchivesDbContext))); +} +else if (databaseMode == "sqlite") +{ + builder.Services.AddDbContext(options + => options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"), b => + { + b.MigrationsAssembly("DB.Migrations.Sqlite"); + })); +} +else if (databaseMode == "mssql") +{ + builder.Services.AddDbContext(options + => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), b => + { + b.MigrationsAssembly("DB.Migrations.Mssql"); + })); +} + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +if (app.Configuration.GetValue("Https", false)) + app.UseHttpsRedirection(); + +app.UseAuthorization(); +app.MapControllers(); + +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + if (db.Database.IsRelational() && !db.Database.IsInMemory()) + { + await db.Database.MigrateAsync(); + } + #if DEBUG + else if (db.Database.IsInMemory()) + { + await DevelopmentDataSeedingHelper.Seed(db); + } + #endif +} + +app.Run(); \ No newline at end of file diff --git a/API/Properties/launchSettings.json b/API/Properties/launchSettings.json new file mode 100644 index 0000000..78d807e --- /dev/null +++ b/API/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:16591", + "sslPort": 44319 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5012", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7174;http://localhost:5012", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/API/appsettings.Development.json b/API/appsettings.Development.json new file mode 100644 index 0000000..f9cd5b9 --- /dev/null +++ b/API/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "DatabaseMode": "memory" +} \ No newline at end of file diff --git a/API/appsettings.json b/API/appsettings.json new file mode 100644 index 0000000..ec5c9df --- /dev/null +++ b/API/appsettings.json @@ -0,0 +1,16 @@ +{ + "Admin": { + "Account": { + "Username": "admin", + "Password": "password123" + } + }, + "Https": false, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/DB.Migrations.Mssql/DB.Migrations.Mssql.csproj b/DB.Migrations.Mssql/DB.Migrations.Mssql.csproj new file mode 100644 index 0000000..e571bca --- /dev/null +++ b/DB.Migrations.Mssql/DB.Migrations.Mssql.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/DB.Migrations.Mssql/Migrations/20240219200002_Initial.Designer.cs b/DB.Migrations.Mssql/Migrations/20240219200002_Initial.Designer.cs new file mode 100644 index 0000000..222b90c --- /dev/null +++ b/DB.Migrations.Mssql/Migrations/20240219200002_Initial.Designer.cs @@ -0,0 +1,470 @@ +// +using System; +using Hesketh.MecatolArchives.DB; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hesketh.MecatolArchives.DB.Migrations.Mssql.Migrations +{ + [DbContext(typeof(MecatolArchivesDbContext))] + [Migration("20240219200002_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Colour", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Hex") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("Colours"); + + b.HasData( + new + { + Identifier = new Guid("cbdfdda9-13bf-4a45-be5d-0882f6dcbad8"), + Hex = "", + Name = "_Unknown_" + }, + new + { + Identifier = new Guid("564f166e-33cb-45cc-bca6-1b2d16b8bf60"), + Hex = "#000000", + Name = "Black" + }, + new + { + Identifier = new Guid("53bf36a1-669c-41ed-9ced-4fa94ba038ee"), + Hex = "#FF0000", + Name = "Red" + }, + new + { + Identifier = new Guid("e8ca7b27-00cd-4a2a-bc7f-17105e690e2d"), + Hex = "#008000", + Name = "Green" + }, + new + { + Identifier = new Guid("51b6cc96-0e35-48f9-8665-b50bbe3fdb44"), + Hex = "#FFFF00", + Name = "Yellow" + }, + new + { + Identifier = new Guid("a9c3b568-d781-452d-91ae-44b0cc8e7020"), + Hex = "#800080", + Name = "Purple" + }, + new + { + Identifier = new Guid("43c078a5-0561-40f0-8adc-92afa32eaeb0"), + Hex = "#FFA500", + Name = "Orange" + }, + new + { + Identifier = new Guid("daceda53-e450-4fce-82d4-ef1cdd312e38"), + Hex = "#FF00FF", + Name = "Magenta" + }, + new + { + Identifier = new Guid("b5616b41-2821-4a27-85dc-fa81b899e578"), + Hex = "#0000FF", + Name = "Blue" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Expansion", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayIdentifier") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Identifier"); + + b.HasIndex("PlayIdentifier"); + + b.ToTable("Expansions"); + + b.HasData( + new + { + Identifier = new Guid("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"), + Name = "Prophecy of Kings" + }, + new + { + Identifier = new Guid("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"), + Name = "Codex I: Ordinian" + }, + new + { + Identifier = new Guid("9420502f-4ef0-4887-add4-3d8a4941016a"), + Name = "Codex II: Affinity" + }, + new + { + Identifier = new Guid("1eb732ba-74ac-4993-943e-cd6f3650d310"), + Name = "Codex III: Vigil" + }, + new + { + Identifier = new Guid("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"), + Name = "Omega Initiative I" + }, + new + { + Identifier = new Guid("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"), + Name = "Omega Initiative II" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Faction", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("Factions"); + + b.HasData( + new + { + Identifier = new Guid("11ee6931-58d5-4808-b2c5-e3d4b7bf4343"), + Name = "The Arborec" + }, + new + { + Identifier = new Guid("e4d8ad5c-cf62-4001-9b81-f2a31bd87b0d"), + Name = "The Barony of Letnev" + }, + new + { + Identifier = new Guid("b01aaa66-1888-4926-803e-95a864057219"), + Name = "The Clan of Saar" + }, + new + { + Identifier = new Guid("fcae96dd-793a-4393-9f5f-f378ff167b12"), + Name = "The Embers of Muaat" + }, + new + { + Identifier = new Guid("c9a5311a-4798-44c5-8b97-3a5bc4eaa01a"), + Name = "The Emirates of Hacan" + }, + new + { + Identifier = new Guid("a386b3e8-683b-4e7e-9fba-3361bdbbeef1"), + Name = "The Federation of Sol" + }, + new + { + Identifier = new Guid("2e017142-848a-4203-a109-1fa905a5db04"), + Name = "The Ghosts of Creuss" + }, + new + { + Identifier = new Guid("4df36a67-6b3d-4966-ae0c-6ebdb6d0a97d"), + Name = "The L1Z1X Mindnet" + }, + new + { + Identifier = new Guid("c765da52-404b-4073-8c0c-003537169b4c"), + Name = "The Mentak Coalition" + }, + new + { + Identifier = new Guid("2002e0b3-603f-4b66-8268-2ca8ab2bfce4"), + Name = "The Naalu Collective" + }, + new + { + Identifier = new Guid("79c4217c-dc20-46fe-9e0b-44e852dfc64b"), + Name = "The Nekro Virus" + }, + new + { + Identifier = new Guid("1a5777dc-4106-46ec-8168-596c7c9edd36"), + Name = "Sardakk N'orr" + }, + new + { + Identifier = new Guid("0cc14756-19f7-42ef-acd0-156fab6bcd2b"), + Name = "The Universities of Jol-Nar" + }, + new + { + Identifier = new Guid("9b8cf62c-9c94-49aa-b55b-11a8f4f9a7c9"), + Name = "The Winnu" + }, + new + { + Identifier = new Guid("279eefc5-4bd3-41be-93d0-b49bd6e9b7d6"), + Name = "The Xxcha Kingdom" + }, + new + { + Identifier = new Guid("2c637584-cdbb-485e-8c3f-bc713eebaa03"), + Name = "The Yin Brotherhood" + }, + new + { + Identifier = new Guid("5265b1f2-4422-4d0b-8711-582330d9afe4"), + Name = "The Yssaril Tribes" + }, + new + { + Identifier = new Guid("be81d0fd-0ba8-4d55-ac65-4eb66d49028a"), + Name = "The Argent Flight" + }, + new + { + Identifier = new Guid("f39815c4-b651-4ed4-86e0-5b4f5f6128ef"), + Name = "The Empyrean" + }, + new + { + Identifier = new Guid("2fffa08d-40ab-4e29-8e4a-0cda97b664be"), + Name = "The Mahact Gene-Sorcerers" + }, + new + { + Identifier = new Guid("26251032-94c9-4331-8ccf-697755213fad"), + Name = "The Naaz-Rokha Alliance" + }, + new + { + Identifier = new Guid("4210c2bb-f773-4721-907b-1c3e1cd11357"), + Name = "The Nomad" + }, + new + { + Identifier = new Guid("c4e77f65-5a61-404f-8793-b672ccdb29a4"), + Name = "The Titans of UL" + }, + new + { + Identifier = new Guid("91da5e70-a3db-4168-b18d-61ae7b899b48"), + Name = "The Vuil'Raith Cabal" + }, + new + { + Identifier = new Guid("51ee1c82-279b-444c-a6aa-a8cd475612fd"), + Name = "The Council Keleres (The Mentak Coalition)" + }, + new + { + Identifier = new Guid("d819ecf0-3ccd-45d1-9f19-ba045d39fd65"), + Name = "The Council Keleres (The Xxcha Kingdoms)" + }, + new + { + Identifier = new Guid("0747201a-f0ae-4c88-840f-00a3d6c618a0"), + Name = "The Council Keleres (The Argent Flight)" + }, + new + { + Identifier = new Guid("609382d1-c969-4144-916a-ad4c13df1352"), + Name = "_Unknown_" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Person", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Map") + .HasColumnType("nvarchar(max)"); + + b.Property("PointGoal") + .HasColumnType("bigint"); + + b.Property("RulesVersion") + .HasColumnType("float"); + + b.Property("UtcDate") + .HasColumnType("datetime2"); + + b.HasKey("Identifier"); + + b.ToTable("Plays"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ColourIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("Eliminated") + .HasColumnType("bit"); + + b.Property("FactionIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("PersonIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("PlayIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("Points") + .HasColumnType("bigint"); + + b.Property("Winner") + .HasColumnType("bit"); + + b.HasKey("Identifier"); + + b.HasIndex("ColourIdentifier"); + + b.HasIndex("FactionIdentifier"); + + b.HasIndex("PersonIdentifier"); + + b.HasIndex("PlayIdentifier"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Variant", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PlayIdentifier") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Identifier"); + + b.HasIndex("PlayIdentifier"); + + b.ToTable("Variants"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Expansion", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null) + .WithMany("Expansions") + .HasForeignKey("PlayIdentifier"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Colour", "Colour") + .WithMany() + .HasForeignKey("ColourIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Faction", "Faction") + .WithMany() + .HasForeignKey("FactionIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Person", "Person") + .WithMany() + .HasForeignKey("PersonIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", "Play") + .WithMany("Players") + .HasForeignKey("PlayIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Colour"); + + b.Navigation("Faction"); + + b.Navigation("Person"); + + b.Navigation("Play"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Variant", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null) + .WithMany("Variants") + .HasForeignKey("PlayIdentifier"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b => + { + b.Navigation("Expansions"); + + b.Navigation("Players"); + + b.Navigation("Variants"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DB.Migrations.Mssql/Migrations/20240219200002_Initial.cs b/DB.Migrations.Mssql/Migrations/20240219200002_Initial.cs new file mode 100644 index 0000000..478f83e --- /dev/null +++ b/DB.Migrations.Mssql/Migrations/20240219200002_Initial.cs @@ -0,0 +1,266 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Hesketh.MecatolArchives.DB.Migrations.Mssql.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Colours", + columns: table => new + { + Identifier = table.Column(type: "uniqueidentifier", nullable: false), + Hex = table.Column(type: "nvarchar(max)", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Colours", x => x.Identifier); + }); + + migrationBuilder.CreateTable( + name: "Factions", + columns: table => new + { + Identifier = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Factions", x => x.Identifier); + }); + + migrationBuilder.CreateTable( + name: "People", + columns: table => new + { + Identifier = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_People", x => x.Identifier); + }); + + migrationBuilder.CreateTable( + name: "Plays", + columns: table => new + { + Identifier = table.Column(type: "uniqueidentifier", nullable: false), + UtcDate = table.Column(type: "datetime2", nullable: false), + RulesVersion = table.Column(type: "float", nullable: false), + PointGoal = table.Column(type: "bigint", nullable: false), + Map = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Plays", x => x.Identifier); + }); + + migrationBuilder.CreateTable( + name: "Expansions", + columns: table => new + { + Identifier = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + PlayIdentifier = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Expansions", x => x.Identifier); + table.ForeignKey( + name: "FK_Expansions_Plays_PlayIdentifier", + column: x => x.PlayIdentifier, + principalTable: "Plays", + principalColumn: "Identifier"); + }); + + migrationBuilder.CreateTable( + name: "Players", + columns: table => new + { + Identifier = table.Column(type: "uniqueidentifier", nullable: false), + Points = table.Column(type: "bigint", nullable: false), + Winner = table.Column(type: "bit", nullable: false), + Eliminated = table.Column(type: "bit", nullable: false), + PersonIdentifier = table.Column(type: "uniqueidentifier", nullable: false), + PlayIdentifier = table.Column(type: "uniqueidentifier", nullable: false), + FactionIdentifier = table.Column(type: "uniqueidentifier", nullable: false), + ColourIdentifier = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Players", x => x.Identifier); + table.ForeignKey( + name: "FK_Players_Colours_ColourIdentifier", + column: x => x.ColourIdentifier, + principalTable: "Colours", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Players_Factions_FactionIdentifier", + column: x => x.FactionIdentifier, + principalTable: "Factions", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Players_People_PersonIdentifier", + column: x => x.PersonIdentifier, + principalTable: "People", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Players_Plays_PlayIdentifier", + column: x => x.PlayIdentifier, + principalTable: "Plays", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Variants", + columns: table => new + { + Identifier = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + PlayIdentifier = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Variants", x => x.Identifier); + table.ForeignKey( + name: "FK_Variants_Plays_PlayIdentifier", + column: x => x.PlayIdentifier, + principalTable: "Plays", + principalColumn: "Identifier"); + }); + + migrationBuilder.InsertData( + table: "Colours", + columns: new[] { "Identifier", "Hex", "Name" }, + values: new object[,] + { + { new Guid("43c078a5-0561-40f0-8adc-92afa32eaeb0"), "#FFA500", "Orange" }, + { new Guid("51b6cc96-0e35-48f9-8665-b50bbe3fdb44"), "#FFFF00", "Yellow" }, + { new Guid("53bf36a1-669c-41ed-9ced-4fa94ba038ee"), "#FF0000", "Red" }, + { new Guid("564f166e-33cb-45cc-bca6-1b2d16b8bf60"), "#000000", "Black" }, + { new Guid("a9c3b568-d781-452d-91ae-44b0cc8e7020"), "#800080", "Purple" }, + { new Guid("b5616b41-2821-4a27-85dc-fa81b899e578"), "#0000FF", "Blue" }, + { new Guid("cbdfdda9-13bf-4a45-be5d-0882f6dcbad8"), "", "_Unknown_" }, + { new Guid("daceda53-e450-4fce-82d4-ef1cdd312e38"), "#FF00FF", "Magenta" }, + { new Guid("e8ca7b27-00cd-4a2a-bc7f-17105e690e2d"), "#008000", "Green" } + }); + + migrationBuilder.InsertData( + table: "Expansions", + columns: new[] { "Identifier", "Name", "PlayIdentifier" }, + values: new object[,] + { + { new Guid("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"), "Omega Initiative II", null }, + { new Guid("1eb732ba-74ac-4993-943e-cd6f3650d310"), "Codex III: Vigil", null }, + { new Guid("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"), "Codex I: Ordinian", null }, + { new Guid("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"), "Omega Initiative I", null }, + { new Guid("9420502f-4ef0-4887-add4-3d8a4941016a"), "Codex II: Affinity", null }, + { new Guid("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"), "Prophecy of Kings", null } + }); + + migrationBuilder.InsertData( + table: "Factions", + columns: new[] { "Identifier", "Name" }, + values: new object[,] + { + { new Guid("0747201a-f0ae-4c88-840f-00a3d6c618a0"), "The Council Keleres (The Argent Flight)" }, + { new Guid("0cc14756-19f7-42ef-acd0-156fab6bcd2b"), "The Universities of Jol-Nar" }, + { new Guid("11ee6931-58d5-4808-b2c5-e3d4b7bf4343"), "The Arborec" }, + { new Guid("1a5777dc-4106-46ec-8168-596c7c9edd36"), "Sardakk N'orr" }, + { new Guid("2002e0b3-603f-4b66-8268-2ca8ab2bfce4"), "The Naalu Collective" }, + { new Guid("26251032-94c9-4331-8ccf-697755213fad"), "The Naaz-Rokha Alliance" }, + { new Guid("279eefc5-4bd3-41be-93d0-b49bd6e9b7d6"), "The Xxcha Kingdom" }, + { new Guid("2c637584-cdbb-485e-8c3f-bc713eebaa03"), "The Yin Brotherhood" }, + { new Guid("2e017142-848a-4203-a109-1fa905a5db04"), "The Ghosts of Creuss" }, + { new Guid("2fffa08d-40ab-4e29-8e4a-0cda97b664be"), "The Mahact Gene-Sorcerers" }, + { new Guid("4210c2bb-f773-4721-907b-1c3e1cd11357"), "The Nomad" }, + { new Guid("4df36a67-6b3d-4966-ae0c-6ebdb6d0a97d"), "The L1Z1X Mindnet" }, + { new Guid("51ee1c82-279b-444c-a6aa-a8cd475612fd"), "The Council Keleres (The Mentak Coalition)" }, + { new Guid("5265b1f2-4422-4d0b-8711-582330d9afe4"), "The Yssaril Tribes" }, + { new Guid("609382d1-c969-4144-916a-ad4c13df1352"), "_Unknown_" }, + { new Guid("79c4217c-dc20-46fe-9e0b-44e852dfc64b"), "The Nekro Virus" }, + { new Guid("91da5e70-a3db-4168-b18d-61ae7b899b48"), "The Vuil'Raith Cabal" }, + { new Guid("9b8cf62c-9c94-49aa-b55b-11a8f4f9a7c9"), "The Winnu" }, + { new Guid("a386b3e8-683b-4e7e-9fba-3361bdbbeef1"), "The Federation of Sol" }, + { new Guid("b01aaa66-1888-4926-803e-95a864057219"), "The Clan of Saar" }, + { new Guid("be81d0fd-0ba8-4d55-ac65-4eb66d49028a"), "The Argent Flight" }, + { new Guid("c4e77f65-5a61-404f-8793-b672ccdb29a4"), "The Titans of UL" }, + { new Guid("c765da52-404b-4073-8c0c-003537169b4c"), "The Mentak Coalition" }, + { new Guid("c9a5311a-4798-44c5-8b97-3a5bc4eaa01a"), "The Emirates of Hacan" }, + { new Guid("d819ecf0-3ccd-45d1-9f19-ba045d39fd65"), "The Council Keleres (The Xxcha Kingdoms)" }, + { new Guid("e4d8ad5c-cf62-4001-9b81-f2a31bd87b0d"), "The Barony of Letnev" }, + { new Guid("f39815c4-b651-4ed4-86e0-5b4f5f6128ef"), "The Empyrean" }, + { new Guid("fcae96dd-793a-4393-9f5f-f378ff167b12"), "The Embers of Muaat" } + }); + + migrationBuilder.CreateIndex( + name: "IX_Expansions_PlayIdentifier", + table: "Expansions", + column: "PlayIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_Players_ColourIdentifier", + table: "Players", + column: "ColourIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_Players_FactionIdentifier", + table: "Players", + column: "FactionIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_Players_PersonIdentifier", + table: "Players", + column: "PersonIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_Players_PlayIdentifier", + table: "Players", + column: "PlayIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_Variants_PlayIdentifier", + table: "Variants", + column: "PlayIdentifier"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Expansions"); + + migrationBuilder.DropTable( + name: "Players"); + + migrationBuilder.DropTable( + name: "Variants"); + + migrationBuilder.DropTable( + name: "Colours"); + + migrationBuilder.DropTable( + name: "Factions"); + + migrationBuilder.DropTable( + name: "People"); + + migrationBuilder.DropTable( + name: "Plays"); + } + } +} diff --git a/DB.Migrations.Mssql/Migrations/20240220210932_VariantsExpansionsManyPlays.Designer.cs b/DB.Migrations.Mssql/Migrations/20240220210932_VariantsExpansionsManyPlays.Designer.cs new file mode 100644 index 0000000..2ce92a9 --- /dev/null +++ b/DB.Migrations.Mssql/Migrations/20240220210932_VariantsExpansionsManyPlays.Designer.cs @@ -0,0 +1,502 @@ +// +using System; +using Hesketh.MecatolArchives.DB; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hesketh.MecatolArchives.DB.Migrations.Mssql.Migrations +{ + [DbContext(typeof(MecatolArchivesDbContext))] + [Migration("20240220210932_VariantsExpansionsManyPlays")] + partial class VariantsExpansionsManyPlays + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ExpansionPlay", b => + { + b.Property("ExpansionsIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("PlaysIdentifier") + .HasColumnType("uniqueidentifier"); + + b.HasKey("ExpansionsIdentifier", "PlaysIdentifier"); + + b.HasIndex("PlaysIdentifier"); + + b.ToTable("ExpansionPlay"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Colour", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Hex") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("Colours"); + + b.HasData( + new + { + Identifier = new Guid("cbdfdda9-13bf-4a45-be5d-0882f6dcbad8"), + Hex = "", + Name = "_Unknown_" + }, + new + { + Identifier = new Guid("564f166e-33cb-45cc-bca6-1b2d16b8bf60"), + Hex = "#000000", + Name = "Black" + }, + new + { + Identifier = new Guid("53bf36a1-669c-41ed-9ced-4fa94ba038ee"), + Hex = "#FF0000", + Name = "Red" + }, + new + { + Identifier = new Guid("e8ca7b27-00cd-4a2a-bc7f-17105e690e2d"), + Hex = "#008000", + Name = "Green" + }, + new + { + Identifier = new Guid("51b6cc96-0e35-48f9-8665-b50bbe3fdb44"), + Hex = "#FFFF00", + Name = "Yellow" + }, + new + { + Identifier = new Guid("a9c3b568-d781-452d-91ae-44b0cc8e7020"), + Hex = "#800080", + Name = "Purple" + }, + new + { + Identifier = new Guid("43c078a5-0561-40f0-8adc-92afa32eaeb0"), + Hex = "#FFA500", + Name = "Orange" + }, + new + { + Identifier = new Guid("daceda53-e450-4fce-82d4-ef1cdd312e38"), + Hex = "#FF00FF", + Name = "Magenta" + }, + new + { + Identifier = new Guid("b5616b41-2821-4a27-85dc-fa81b899e578"), + Hex = "#0000FF", + Name = "Blue" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Expansion", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("Expansions"); + + b.HasData( + new + { + Identifier = new Guid("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"), + Name = "Prophecy of Kings" + }, + new + { + Identifier = new Guid("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"), + Name = "Codex I: Ordinian" + }, + new + { + Identifier = new Guid("9420502f-4ef0-4887-add4-3d8a4941016a"), + Name = "Codex II: Affinity" + }, + new + { + Identifier = new Guid("1eb732ba-74ac-4993-943e-cd6f3650d310"), + Name = "Codex III: Vigil" + }, + new + { + Identifier = new Guid("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"), + Name = "Omega Initiative I" + }, + new + { + Identifier = new Guid("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"), + Name = "Omega Initiative II" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Faction", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("Factions"); + + b.HasData( + new + { + Identifier = new Guid("11ee6931-58d5-4808-b2c5-e3d4b7bf4343"), + Name = "The Arborec" + }, + new + { + Identifier = new Guid("e4d8ad5c-cf62-4001-9b81-f2a31bd87b0d"), + Name = "The Barony of Letnev" + }, + new + { + Identifier = new Guid("b01aaa66-1888-4926-803e-95a864057219"), + Name = "The Clan of Saar" + }, + new + { + Identifier = new Guid("fcae96dd-793a-4393-9f5f-f378ff167b12"), + Name = "The Embers of Muaat" + }, + new + { + Identifier = new Guid("c9a5311a-4798-44c5-8b97-3a5bc4eaa01a"), + Name = "The Emirates of Hacan" + }, + new + { + Identifier = new Guid("a386b3e8-683b-4e7e-9fba-3361bdbbeef1"), + Name = "The Federation of Sol" + }, + new + { + Identifier = new Guid("2e017142-848a-4203-a109-1fa905a5db04"), + Name = "The Ghosts of Creuss" + }, + new + { + Identifier = new Guid("4df36a67-6b3d-4966-ae0c-6ebdb6d0a97d"), + Name = "The L1Z1X Mindnet" + }, + new + { + Identifier = new Guid("c765da52-404b-4073-8c0c-003537169b4c"), + Name = "The Mentak Coalition" + }, + new + { + Identifier = new Guid("2002e0b3-603f-4b66-8268-2ca8ab2bfce4"), + Name = "The Naalu Collective" + }, + new + { + Identifier = new Guid("79c4217c-dc20-46fe-9e0b-44e852dfc64b"), + Name = "The Nekro Virus" + }, + new + { + Identifier = new Guid("1a5777dc-4106-46ec-8168-596c7c9edd36"), + Name = "Sardakk N'orr" + }, + new + { + Identifier = new Guid("0cc14756-19f7-42ef-acd0-156fab6bcd2b"), + Name = "The Universities of Jol-Nar" + }, + new + { + Identifier = new Guid("9b8cf62c-9c94-49aa-b55b-11a8f4f9a7c9"), + Name = "The Winnu" + }, + new + { + Identifier = new Guid("279eefc5-4bd3-41be-93d0-b49bd6e9b7d6"), + Name = "The Xxcha Kingdom" + }, + new + { + Identifier = new Guid("2c637584-cdbb-485e-8c3f-bc713eebaa03"), + Name = "The Yin Brotherhood" + }, + new + { + Identifier = new Guid("5265b1f2-4422-4d0b-8711-582330d9afe4"), + Name = "The Yssaril Tribes" + }, + new + { + Identifier = new Guid("be81d0fd-0ba8-4d55-ac65-4eb66d49028a"), + Name = "The Argent Flight" + }, + new + { + Identifier = new Guid("f39815c4-b651-4ed4-86e0-5b4f5f6128ef"), + Name = "The Empyrean" + }, + new + { + Identifier = new Guid("2fffa08d-40ab-4e29-8e4a-0cda97b664be"), + Name = "The Mahact Gene-Sorcerers" + }, + new + { + Identifier = new Guid("26251032-94c9-4331-8ccf-697755213fad"), + Name = "The Naaz-Rokha Alliance" + }, + new + { + Identifier = new Guid("4210c2bb-f773-4721-907b-1c3e1cd11357"), + Name = "The Nomad" + }, + new + { + Identifier = new Guid("c4e77f65-5a61-404f-8793-b672ccdb29a4"), + Name = "The Titans of UL" + }, + new + { + Identifier = new Guid("91da5e70-a3db-4168-b18d-61ae7b899b48"), + Name = "The Vuil'Raith Cabal" + }, + new + { + Identifier = new Guid("51ee1c82-279b-444c-a6aa-a8cd475612fd"), + Name = "The Council Keleres (The Mentak Coalition)" + }, + new + { + Identifier = new Guid("d819ecf0-3ccd-45d1-9f19-ba045d39fd65"), + Name = "The Council Keleres (The Xxcha Kingdoms)" + }, + new + { + Identifier = new Guid("0747201a-f0ae-4c88-840f-00a3d6c618a0"), + Name = "The Council Keleres (The Argent Flight)" + }, + new + { + Identifier = new Guid("609382d1-c969-4144-916a-ad4c13df1352"), + Name = "_Unknown_" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Person", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Map") + .HasColumnType("nvarchar(max)"); + + b.Property("PointGoal") + .HasColumnType("bigint"); + + b.Property("RulesVersion") + .HasColumnType("float"); + + b.Property("UtcDate") + .HasColumnType("datetime2"); + + b.HasKey("Identifier"); + + b.ToTable("Plays"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ColourIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("Eliminated") + .HasColumnType("bit"); + + b.Property("FactionIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("PersonIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("PlayIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("Points") + .HasColumnType("bigint"); + + b.Property("Winner") + .HasColumnType("bit"); + + b.HasKey("Identifier"); + + b.HasIndex("ColourIdentifier"); + + b.HasIndex("FactionIdentifier"); + + b.HasIndex("PersonIdentifier"); + + b.HasIndex("PlayIdentifier"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Variant", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("Variants"); + }); + + modelBuilder.Entity("PlayVariant", b => + { + b.Property("PlaysIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("VariantsIdentifier") + .HasColumnType("uniqueidentifier"); + + b.HasKey("PlaysIdentifier", "VariantsIdentifier"); + + b.HasIndex("VariantsIdentifier"); + + b.ToTable("PlayVariant"); + }); + + modelBuilder.Entity("ExpansionPlay", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Expansion", null) + .WithMany() + .HasForeignKey("ExpansionsIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null) + .WithMany() + .HasForeignKey("PlaysIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Colour", "Colour") + .WithMany() + .HasForeignKey("ColourIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Faction", "Faction") + .WithMany() + .HasForeignKey("FactionIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Person", "Person") + .WithMany() + .HasForeignKey("PersonIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", "Play") + .WithMany("Players") + .HasForeignKey("PlayIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Colour"); + + b.Navigation("Faction"); + + b.Navigation("Person"); + + b.Navigation("Play"); + }); + + modelBuilder.Entity("PlayVariant", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null) + .WithMany() + .HasForeignKey("PlaysIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Variant", null) + .WithMany() + .HasForeignKey("VariantsIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b => + { + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DB.Migrations.Mssql/Migrations/20240220210932_VariantsExpansionsManyPlays.cs b/DB.Migrations.Mssql/Migrations/20240220210932_VariantsExpansionsManyPlays.cs new file mode 100644 index 0000000..6a1f942 --- /dev/null +++ b/DB.Migrations.Mssql/Migrations/20240220210932_VariantsExpansionsManyPlays.cs @@ -0,0 +1,185 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hesketh.MecatolArchives.DB.Migrations.Mssql.Migrations +{ + /// + public partial class VariantsExpansionsManyPlays : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Expansions_Plays_PlayIdentifier", + table: "Expansions"); + + migrationBuilder.DropForeignKey( + name: "FK_Variants_Plays_PlayIdentifier", + table: "Variants"); + + migrationBuilder.DropIndex( + name: "IX_Variants_PlayIdentifier", + table: "Variants"); + + migrationBuilder.DropIndex( + name: "IX_Expansions_PlayIdentifier", + table: "Expansions"); + + migrationBuilder.DropColumn( + name: "PlayIdentifier", + table: "Variants"); + + migrationBuilder.DropColumn( + name: "PlayIdentifier", + table: "Expansions"); + + migrationBuilder.CreateTable( + name: "ExpansionPlay", + columns: table => new + { + ExpansionsIdentifier = table.Column(type: "uniqueidentifier", nullable: false), + PlaysIdentifier = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExpansionPlay", x => new { x.ExpansionsIdentifier, x.PlaysIdentifier }); + table.ForeignKey( + name: "FK_ExpansionPlay_Expansions_ExpansionsIdentifier", + column: x => x.ExpansionsIdentifier, + principalTable: "Expansions", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ExpansionPlay_Plays_PlaysIdentifier", + column: x => x.PlaysIdentifier, + principalTable: "Plays", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PlayVariant", + columns: table => new + { + PlaysIdentifier = table.Column(type: "uniqueidentifier", nullable: false), + VariantsIdentifier = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PlayVariant", x => new { x.PlaysIdentifier, x.VariantsIdentifier }); + table.ForeignKey( + name: "FK_PlayVariant_Plays_PlaysIdentifier", + column: x => x.PlaysIdentifier, + principalTable: "Plays", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PlayVariant_Variants_VariantsIdentifier", + column: x => x.VariantsIdentifier, + principalTable: "Variants", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ExpansionPlay_PlaysIdentifier", + table: "ExpansionPlay", + column: "PlaysIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_PlayVariant_VariantsIdentifier", + table: "PlayVariant", + column: "VariantsIdentifier"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ExpansionPlay"); + + migrationBuilder.DropTable( + name: "PlayVariant"); + + migrationBuilder.AddColumn( + name: "PlayIdentifier", + table: "Variants", + type: "uniqueidentifier", + nullable: true); + + migrationBuilder.AddColumn( + name: "PlayIdentifier", + table: "Expansions", + type: "uniqueidentifier", + nullable: true); + + migrationBuilder.UpdateData( + table: "Expansions", + keyColumn: "Identifier", + keyValue: new Guid("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"), + column: "PlayIdentifier", + value: null); + + migrationBuilder.UpdateData( + table: "Expansions", + keyColumn: "Identifier", + keyValue: new Guid("1eb732ba-74ac-4993-943e-cd6f3650d310"), + column: "PlayIdentifier", + value: null); + + migrationBuilder.UpdateData( + table: "Expansions", + keyColumn: "Identifier", + keyValue: new Guid("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"), + column: "PlayIdentifier", + value: null); + + migrationBuilder.UpdateData( + table: "Expansions", + keyColumn: "Identifier", + keyValue: new Guid("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"), + column: "PlayIdentifier", + value: null); + + migrationBuilder.UpdateData( + table: "Expansions", + keyColumn: "Identifier", + keyValue: new Guid("9420502f-4ef0-4887-add4-3d8a4941016a"), + column: "PlayIdentifier", + value: null); + + migrationBuilder.UpdateData( + table: "Expansions", + keyColumn: "Identifier", + keyValue: new Guid("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"), + column: "PlayIdentifier", + value: null); + + migrationBuilder.CreateIndex( + name: "IX_Variants_PlayIdentifier", + table: "Variants", + column: "PlayIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_Expansions_PlayIdentifier", + table: "Expansions", + column: "PlayIdentifier"); + + migrationBuilder.AddForeignKey( + name: "FK_Expansions_Plays_PlayIdentifier", + table: "Expansions", + column: "PlayIdentifier", + principalTable: "Plays", + principalColumn: "Identifier"); + + migrationBuilder.AddForeignKey( + name: "FK_Variants_Plays_PlayIdentifier", + table: "Variants", + column: "PlayIdentifier", + principalTable: "Plays", + principalColumn: "Identifier"); + } + } +} diff --git a/DB.Migrations.Mssql/Migrations/MecatolArchivesDbContextModelSnapshot.cs b/DB.Migrations.Mssql/Migrations/MecatolArchivesDbContextModelSnapshot.cs new file mode 100644 index 0000000..a3552d9 --- /dev/null +++ b/DB.Migrations.Mssql/Migrations/MecatolArchivesDbContextModelSnapshot.cs @@ -0,0 +1,499 @@ +// +using System; +using Hesketh.MecatolArchives.DB; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hesketh.MecatolArchives.DB.Migrations.Mssql.Migrations +{ + [DbContext(typeof(MecatolArchivesDbContext))] + partial class MecatolArchivesDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ExpansionPlay", b => + { + b.Property("ExpansionsIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("PlaysIdentifier") + .HasColumnType("uniqueidentifier"); + + b.HasKey("ExpansionsIdentifier", "PlaysIdentifier"); + + b.HasIndex("PlaysIdentifier"); + + b.ToTable("ExpansionPlay"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Colour", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Hex") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("Colours"); + + b.HasData( + new + { + Identifier = new Guid("cbdfdda9-13bf-4a45-be5d-0882f6dcbad8"), + Hex = "", + Name = "_Unknown_" + }, + new + { + Identifier = new Guid("564f166e-33cb-45cc-bca6-1b2d16b8bf60"), + Hex = "#000000", + Name = "Black" + }, + new + { + Identifier = new Guid("53bf36a1-669c-41ed-9ced-4fa94ba038ee"), + Hex = "#FF0000", + Name = "Red" + }, + new + { + Identifier = new Guid("e8ca7b27-00cd-4a2a-bc7f-17105e690e2d"), + Hex = "#008000", + Name = "Green" + }, + new + { + Identifier = new Guid("51b6cc96-0e35-48f9-8665-b50bbe3fdb44"), + Hex = "#FFFF00", + Name = "Yellow" + }, + new + { + Identifier = new Guid("a9c3b568-d781-452d-91ae-44b0cc8e7020"), + Hex = "#800080", + Name = "Purple" + }, + new + { + Identifier = new Guid("43c078a5-0561-40f0-8adc-92afa32eaeb0"), + Hex = "#FFA500", + Name = "Orange" + }, + new + { + Identifier = new Guid("daceda53-e450-4fce-82d4-ef1cdd312e38"), + Hex = "#FF00FF", + Name = "Magenta" + }, + new + { + Identifier = new Guid("b5616b41-2821-4a27-85dc-fa81b899e578"), + Hex = "#0000FF", + Name = "Blue" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Expansion", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("Expansions"); + + b.HasData( + new + { + Identifier = new Guid("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"), + Name = "Prophecy of Kings" + }, + new + { + Identifier = new Guid("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"), + Name = "Codex I: Ordinian" + }, + new + { + Identifier = new Guid("9420502f-4ef0-4887-add4-3d8a4941016a"), + Name = "Codex II: Affinity" + }, + new + { + Identifier = new Guid("1eb732ba-74ac-4993-943e-cd6f3650d310"), + Name = "Codex III: Vigil" + }, + new + { + Identifier = new Guid("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"), + Name = "Omega Initiative I" + }, + new + { + Identifier = new Guid("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"), + Name = "Omega Initiative II" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Faction", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("Factions"); + + b.HasData( + new + { + Identifier = new Guid("11ee6931-58d5-4808-b2c5-e3d4b7bf4343"), + Name = "The Arborec" + }, + new + { + Identifier = new Guid("e4d8ad5c-cf62-4001-9b81-f2a31bd87b0d"), + Name = "The Barony of Letnev" + }, + new + { + Identifier = new Guid("b01aaa66-1888-4926-803e-95a864057219"), + Name = "The Clan of Saar" + }, + new + { + Identifier = new Guid("fcae96dd-793a-4393-9f5f-f378ff167b12"), + Name = "The Embers of Muaat" + }, + new + { + Identifier = new Guid("c9a5311a-4798-44c5-8b97-3a5bc4eaa01a"), + Name = "The Emirates of Hacan" + }, + new + { + Identifier = new Guid("a386b3e8-683b-4e7e-9fba-3361bdbbeef1"), + Name = "The Federation of Sol" + }, + new + { + Identifier = new Guid("2e017142-848a-4203-a109-1fa905a5db04"), + Name = "The Ghosts of Creuss" + }, + new + { + Identifier = new Guid("4df36a67-6b3d-4966-ae0c-6ebdb6d0a97d"), + Name = "The L1Z1X Mindnet" + }, + new + { + Identifier = new Guid("c765da52-404b-4073-8c0c-003537169b4c"), + Name = "The Mentak Coalition" + }, + new + { + Identifier = new Guid("2002e0b3-603f-4b66-8268-2ca8ab2bfce4"), + Name = "The Naalu Collective" + }, + new + { + Identifier = new Guid("79c4217c-dc20-46fe-9e0b-44e852dfc64b"), + Name = "The Nekro Virus" + }, + new + { + Identifier = new Guid("1a5777dc-4106-46ec-8168-596c7c9edd36"), + Name = "Sardakk N'orr" + }, + new + { + Identifier = new Guid("0cc14756-19f7-42ef-acd0-156fab6bcd2b"), + Name = "The Universities of Jol-Nar" + }, + new + { + Identifier = new Guid("9b8cf62c-9c94-49aa-b55b-11a8f4f9a7c9"), + Name = "The Winnu" + }, + new + { + Identifier = new Guid("279eefc5-4bd3-41be-93d0-b49bd6e9b7d6"), + Name = "The Xxcha Kingdom" + }, + new + { + Identifier = new Guid("2c637584-cdbb-485e-8c3f-bc713eebaa03"), + Name = "The Yin Brotherhood" + }, + new + { + Identifier = new Guid("5265b1f2-4422-4d0b-8711-582330d9afe4"), + Name = "The Yssaril Tribes" + }, + new + { + Identifier = new Guid("be81d0fd-0ba8-4d55-ac65-4eb66d49028a"), + Name = "The Argent Flight" + }, + new + { + Identifier = new Guid("f39815c4-b651-4ed4-86e0-5b4f5f6128ef"), + Name = "The Empyrean" + }, + new + { + Identifier = new Guid("2fffa08d-40ab-4e29-8e4a-0cda97b664be"), + Name = "The Mahact Gene-Sorcerers" + }, + new + { + Identifier = new Guid("26251032-94c9-4331-8ccf-697755213fad"), + Name = "The Naaz-Rokha Alliance" + }, + new + { + Identifier = new Guid("4210c2bb-f773-4721-907b-1c3e1cd11357"), + Name = "The Nomad" + }, + new + { + Identifier = new Guid("c4e77f65-5a61-404f-8793-b672ccdb29a4"), + Name = "The Titans of UL" + }, + new + { + Identifier = new Guid("91da5e70-a3db-4168-b18d-61ae7b899b48"), + Name = "The Vuil'Raith Cabal" + }, + new + { + Identifier = new Guid("51ee1c82-279b-444c-a6aa-a8cd475612fd"), + Name = "The Council Keleres (The Mentak Coalition)" + }, + new + { + Identifier = new Guid("d819ecf0-3ccd-45d1-9f19-ba045d39fd65"), + Name = "The Council Keleres (The Xxcha Kingdoms)" + }, + new + { + Identifier = new Guid("0747201a-f0ae-4c88-840f-00a3d6c618a0"), + Name = "The Council Keleres (The Argent Flight)" + }, + new + { + Identifier = new Guid("609382d1-c969-4144-916a-ad4c13df1352"), + Name = "_Unknown_" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Person", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Map") + .HasColumnType("nvarchar(max)"); + + b.Property("PointGoal") + .HasColumnType("bigint"); + + b.Property("RulesVersion") + .HasColumnType("float"); + + b.Property("UtcDate") + .HasColumnType("datetime2"); + + b.HasKey("Identifier"); + + b.ToTable("Plays"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ColourIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("Eliminated") + .HasColumnType("bit"); + + b.Property("FactionIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("PersonIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("PlayIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("Points") + .HasColumnType("bigint"); + + b.Property("Winner") + .HasColumnType("bit"); + + b.HasKey("Identifier"); + + b.HasIndex("ColourIdentifier"); + + b.HasIndex("FactionIdentifier"); + + b.HasIndex("PersonIdentifier"); + + b.HasIndex("PlayIdentifier"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Variant", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Identifier"); + + b.ToTable("Variants"); + }); + + modelBuilder.Entity("PlayVariant", b => + { + b.Property("PlaysIdentifier") + .HasColumnType("uniqueidentifier"); + + b.Property("VariantsIdentifier") + .HasColumnType("uniqueidentifier"); + + b.HasKey("PlaysIdentifier", "VariantsIdentifier"); + + b.HasIndex("VariantsIdentifier"); + + b.ToTable("PlayVariant"); + }); + + modelBuilder.Entity("ExpansionPlay", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Expansion", null) + .WithMany() + .HasForeignKey("ExpansionsIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null) + .WithMany() + .HasForeignKey("PlaysIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Colour", "Colour") + .WithMany() + .HasForeignKey("ColourIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Faction", "Faction") + .WithMany() + .HasForeignKey("FactionIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Person", "Person") + .WithMany() + .HasForeignKey("PersonIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", "Play") + .WithMany("Players") + .HasForeignKey("PlayIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Colour"); + + b.Navigation("Faction"); + + b.Navigation("Person"); + + b.Navigation("Play"); + }); + + modelBuilder.Entity("PlayVariant", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null) + .WithMany() + .HasForeignKey("PlaysIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Variant", null) + .WithMany() + .HasForeignKey("VariantsIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b => + { + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DB.Migrations.Sqlite/DB.Migrations.Sqlite.csproj b/DB.Migrations.Sqlite/DB.Migrations.Sqlite.csproj new file mode 100644 index 0000000..e571bca --- /dev/null +++ b/DB.Migrations.Sqlite/DB.Migrations.Sqlite.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/DB.Migrations.Sqlite/Migrations/20240219152348_Initial.Designer.cs b/DB.Migrations.Sqlite/Migrations/20240219152348_Initial.Designer.cs new file mode 100644 index 0000000..796e6dd --- /dev/null +++ b/DB.Migrations.Sqlite/Migrations/20240219152348_Initial.Designer.cs @@ -0,0 +1,465 @@ +// +using System; +using Hesketh.MecatolArchives.DB; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hesketh.MecatolArchives.DB.Migrations.Sqlite.Migrations +{ + [DbContext(typeof(MecatolArchivesDbContext))] + [Migration("20240219152348_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Colour", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Hex") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Colours"); + + b.HasData( + new + { + Identifier = new Guid("cbdfdda9-13bf-4a45-be5d-0882f6dcbad8"), + Hex = "", + Name = "_Unknown_" + }, + new + { + Identifier = new Guid("564f166e-33cb-45cc-bca6-1b2d16b8bf60"), + Hex = "#000000", + Name = "Black" + }, + new + { + Identifier = new Guid("53bf36a1-669c-41ed-9ced-4fa94ba038ee"), + Hex = "#FF0000", + Name = "Red" + }, + new + { + Identifier = new Guid("e8ca7b27-00cd-4a2a-bc7f-17105e690e2d"), + Hex = "#008000", + Name = "Green" + }, + new + { + Identifier = new Guid("51b6cc96-0e35-48f9-8665-b50bbe3fdb44"), + Hex = "#FFFF00", + Name = "Yellow" + }, + new + { + Identifier = new Guid("a9c3b568-d781-452d-91ae-44b0cc8e7020"), + Hex = "#800080", + Name = "Purple" + }, + new + { + Identifier = new Guid("43c078a5-0561-40f0-8adc-92afa32eaeb0"), + Hex = "#FFA500", + Name = "Orange" + }, + new + { + Identifier = new Guid("daceda53-e450-4fce-82d4-ef1cdd312e38"), + Hex = "#FF00FF", + Name = "Magenta" + }, + new + { + Identifier = new Guid("b5616b41-2821-4a27-85dc-fa81b899e578"), + Hex = "#0000FF", + Name = "Blue" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Expansion", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlayIdentifier") + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.HasIndex("PlayIdentifier"); + + b.ToTable("Expansions"); + + b.HasData( + new + { + Identifier = new Guid("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"), + Name = "Prophecy of Kings" + }, + new + { + Identifier = new Guid("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"), + Name = "Codex I: Ordinian" + }, + new + { + Identifier = new Guid("9420502f-4ef0-4887-add4-3d8a4941016a"), + Name = "Codex II: Affinity" + }, + new + { + Identifier = new Guid("1eb732ba-74ac-4993-943e-cd6f3650d310"), + Name = "Codex III: Vigil" + }, + new + { + Identifier = new Guid("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"), + Name = "Omega Initiative I" + }, + new + { + Identifier = new Guid("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"), + Name = "Omega Initiative II" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Faction", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Factions"); + + b.HasData( + new + { + Identifier = new Guid("11ee6931-58d5-4808-b2c5-e3d4b7bf4343"), + Name = "The Arborec" + }, + new + { + Identifier = new Guid("e4d8ad5c-cf62-4001-9b81-f2a31bd87b0d"), + Name = "The Barony of Letnev" + }, + new + { + Identifier = new Guid("b01aaa66-1888-4926-803e-95a864057219"), + Name = "The Clan of Saar" + }, + new + { + Identifier = new Guid("fcae96dd-793a-4393-9f5f-f378ff167b12"), + Name = "The Embers of Muaat" + }, + new + { + Identifier = new Guid("c9a5311a-4798-44c5-8b97-3a5bc4eaa01a"), + Name = "The Emirates of Hacan" + }, + new + { + Identifier = new Guid("a386b3e8-683b-4e7e-9fba-3361bdbbeef1"), + Name = "The Federation of Sol" + }, + new + { + Identifier = new Guid("2e017142-848a-4203-a109-1fa905a5db04"), + Name = "The Ghosts of Creuss" + }, + new + { + Identifier = new Guid("4df36a67-6b3d-4966-ae0c-6ebdb6d0a97d"), + Name = "The L1Z1X Mindnet" + }, + new + { + Identifier = new Guid("c765da52-404b-4073-8c0c-003537169b4c"), + Name = "The Mentak Coalition" + }, + new + { + Identifier = new Guid("2002e0b3-603f-4b66-8268-2ca8ab2bfce4"), + Name = "The Naalu Collective" + }, + new + { + Identifier = new Guid("79c4217c-dc20-46fe-9e0b-44e852dfc64b"), + Name = "The Nekro Virus" + }, + new + { + Identifier = new Guid("1a5777dc-4106-46ec-8168-596c7c9edd36"), + Name = "Sardakk N'orr" + }, + new + { + Identifier = new Guid("0cc14756-19f7-42ef-acd0-156fab6bcd2b"), + Name = "The Universities of Jol-Nar" + }, + new + { + Identifier = new Guid("9b8cf62c-9c94-49aa-b55b-11a8f4f9a7c9"), + Name = "The Winnu" + }, + new + { + Identifier = new Guid("279eefc5-4bd3-41be-93d0-b49bd6e9b7d6"), + Name = "The Xxcha Kingdom" + }, + new + { + Identifier = new Guid("2c637584-cdbb-485e-8c3f-bc713eebaa03"), + Name = "The Yin Brotherhood" + }, + new + { + Identifier = new Guid("5265b1f2-4422-4d0b-8711-582330d9afe4"), + Name = "The Yssaril Tribes" + }, + new + { + Identifier = new Guid("be81d0fd-0ba8-4d55-ac65-4eb66d49028a"), + Name = "The Argent Flight" + }, + new + { + Identifier = new Guid("f39815c4-b651-4ed4-86e0-5b4f5f6128ef"), + Name = "The Empyrean" + }, + new + { + Identifier = new Guid("2fffa08d-40ab-4e29-8e4a-0cda97b664be"), + Name = "The Mahact Gene-Sorcerers" + }, + new + { + Identifier = new Guid("26251032-94c9-4331-8ccf-697755213fad"), + Name = "The Naaz-Rokha Alliance" + }, + new + { + Identifier = new Guid("4210c2bb-f773-4721-907b-1c3e1cd11357"), + Name = "The Nomad" + }, + new + { + Identifier = new Guid("c4e77f65-5a61-404f-8793-b672ccdb29a4"), + Name = "The Titans of UL" + }, + new + { + Identifier = new Guid("91da5e70-a3db-4168-b18d-61ae7b899b48"), + Name = "The Vuil'Raith Cabal" + }, + new + { + Identifier = new Guid("51ee1c82-279b-444c-a6aa-a8cd475612fd"), + Name = "The Council Keleres (The Mentak Coalition)" + }, + new + { + Identifier = new Guid("d819ecf0-3ccd-45d1-9f19-ba045d39fd65"), + Name = "The Council Keleres (The Xxcha Kingdoms)" + }, + new + { + Identifier = new Guid("0747201a-f0ae-4c88-840f-00a3d6c618a0"), + Name = "The Council Keleres (The Argent Flight)" + }, + new + { + Identifier = new Guid("609382d1-c969-4144-916a-ad4c13df1352"), + Name = "_Unknown_" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Person", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Map") + .HasColumnType("TEXT"); + + b.Property("PointGoal") + .HasColumnType("INTEGER"); + + b.Property("RulesVersion") + .HasColumnType("REAL"); + + b.Property("UtcDate") + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Plays"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ColourIdentifier") + .HasColumnType("TEXT"); + + b.Property("Eliminated") + .HasColumnType("INTEGER"); + + b.Property("FactionIdentifier") + .HasColumnType("TEXT"); + + b.Property("PersonIdentifier") + .HasColumnType("TEXT"); + + b.Property("PlayIdentifier") + .HasColumnType("TEXT"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("Winner") + .HasColumnType("INTEGER"); + + b.HasKey("Identifier"); + + b.HasIndex("ColourIdentifier"); + + b.HasIndex("FactionIdentifier"); + + b.HasIndex("PersonIdentifier"); + + b.HasIndex("PlayIdentifier"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Variant", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlayIdentifier") + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.HasIndex("PlayIdentifier"); + + b.ToTable("Variants"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Expansion", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null) + .WithMany("Expansions") + .HasForeignKey("PlayIdentifier"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Colour", "Colour") + .WithMany() + .HasForeignKey("ColourIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Faction", "Faction") + .WithMany() + .HasForeignKey("FactionIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Person", "Person") + .WithMany() + .HasForeignKey("PersonIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", "Play") + .WithMany("Players") + .HasForeignKey("PlayIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Colour"); + + b.Navigation("Faction"); + + b.Navigation("Person"); + + b.Navigation("Play"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Variant", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null) + .WithMany("Variants") + .HasForeignKey("PlayIdentifier"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b => + { + b.Navigation("Expansions"); + + b.Navigation("Players"); + + b.Navigation("Variants"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DB.Migrations.Sqlite/Migrations/20240219152348_Initial.cs b/DB.Migrations.Sqlite/Migrations/20240219152348_Initial.cs new file mode 100644 index 0000000..c19e265 --- /dev/null +++ b/DB.Migrations.Sqlite/Migrations/20240219152348_Initial.cs @@ -0,0 +1,266 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Hesketh.MecatolArchives.DB.Migrations.Sqlite.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Colours", + columns: table => new + { + Identifier = table.Column(type: "TEXT", nullable: false), + Hex = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Colours", x => x.Identifier); + }); + + migrationBuilder.CreateTable( + name: "Factions", + columns: table => new + { + Identifier = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Factions", x => x.Identifier); + }); + + migrationBuilder.CreateTable( + name: "People", + columns: table => new + { + Identifier = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_People", x => x.Identifier); + }); + + migrationBuilder.CreateTable( + name: "Plays", + columns: table => new + { + Identifier = table.Column(type: "TEXT", nullable: false), + UtcDate = table.Column(type: "TEXT", nullable: false), + RulesVersion = table.Column(type: "REAL", nullable: false), + PointGoal = table.Column(type: "INTEGER", nullable: false), + Map = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Plays", x => x.Identifier); + }); + + migrationBuilder.CreateTable( + name: "Expansions", + columns: table => new + { + Identifier = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + PlayIdentifier = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Expansions", x => x.Identifier); + table.ForeignKey( + name: "FK_Expansions_Plays_PlayIdentifier", + column: x => x.PlayIdentifier, + principalTable: "Plays", + principalColumn: "Identifier"); + }); + + migrationBuilder.CreateTable( + name: "Players", + columns: table => new + { + Identifier = table.Column(type: "TEXT", nullable: false), + Points = table.Column(type: "INTEGER", nullable: false), + Winner = table.Column(type: "INTEGER", nullable: false), + Eliminated = table.Column(type: "INTEGER", nullable: false), + PersonIdentifier = table.Column(type: "TEXT", nullable: false), + PlayIdentifier = table.Column(type: "TEXT", nullable: false), + FactionIdentifier = table.Column(type: "TEXT", nullable: false), + ColourIdentifier = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Players", x => x.Identifier); + table.ForeignKey( + name: "FK_Players_Colours_ColourIdentifier", + column: x => x.ColourIdentifier, + principalTable: "Colours", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Players_Factions_FactionIdentifier", + column: x => x.FactionIdentifier, + principalTable: "Factions", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Players_People_PersonIdentifier", + column: x => x.PersonIdentifier, + principalTable: "People", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Players_Plays_PlayIdentifier", + column: x => x.PlayIdentifier, + principalTable: "Plays", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Variants", + columns: table => new + { + Identifier = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + PlayIdentifier = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Variants", x => x.Identifier); + table.ForeignKey( + name: "FK_Variants_Plays_PlayIdentifier", + column: x => x.PlayIdentifier, + principalTable: "Plays", + principalColumn: "Identifier"); + }); + + migrationBuilder.InsertData( + table: "Colours", + columns: new[] { "Identifier", "Hex", "Name" }, + values: new object[,] + { + { new Guid("43c078a5-0561-40f0-8adc-92afa32eaeb0"), "#FFA500", "Orange" }, + { new Guid("51b6cc96-0e35-48f9-8665-b50bbe3fdb44"), "#FFFF00", "Yellow" }, + { new Guid("53bf36a1-669c-41ed-9ced-4fa94ba038ee"), "#FF0000", "Red" }, + { new Guid("564f166e-33cb-45cc-bca6-1b2d16b8bf60"), "#000000", "Black" }, + { new Guid("a9c3b568-d781-452d-91ae-44b0cc8e7020"), "#800080", "Purple" }, + { new Guid("b5616b41-2821-4a27-85dc-fa81b899e578"), "#0000FF", "Blue" }, + { new Guid("cbdfdda9-13bf-4a45-be5d-0882f6dcbad8"), "", "_Unknown_" }, + { new Guid("daceda53-e450-4fce-82d4-ef1cdd312e38"), "#FF00FF", "Magenta" }, + { new Guid("e8ca7b27-00cd-4a2a-bc7f-17105e690e2d"), "#008000", "Green" } + }); + + migrationBuilder.InsertData( + table: "Expansions", + columns: new[] { "Identifier", "Name", "PlayIdentifier" }, + values: new object[,] + { + { new Guid("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"), "Omega Initiative II", null }, + { new Guid("1eb732ba-74ac-4993-943e-cd6f3650d310"), "Codex III: Vigil", null }, + { new Guid("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"), "Codex I: Ordinian", null }, + { new Guid("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"), "Omega Initiative I", null }, + { new Guid("9420502f-4ef0-4887-add4-3d8a4941016a"), "Codex II: Affinity", null }, + { new Guid("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"), "Prophecy of Kings", null } + }); + + migrationBuilder.InsertData( + table: "Factions", + columns: new[] { "Identifier", "Name" }, + values: new object[,] + { + { new Guid("0747201a-f0ae-4c88-840f-00a3d6c618a0"), "The Council Keleres (The Argent Flight)" }, + { new Guid("0cc14756-19f7-42ef-acd0-156fab6bcd2b"), "The Universities of Jol-Nar" }, + { new Guid("11ee6931-58d5-4808-b2c5-e3d4b7bf4343"), "The Arborec" }, + { new Guid("1a5777dc-4106-46ec-8168-596c7c9edd36"), "Sardakk N'orr" }, + { new Guid("2002e0b3-603f-4b66-8268-2ca8ab2bfce4"), "The Naalu Collective" }, + { new Guid("26251032-94c9-4331-8ccf-697755213fad"), "The Naaz-Rokha Alliance" }, + { new Guid("279eefc5-4bd3-41be-93d0-b49bd6e9b7d6"), "The Xxcha Kingdom" }, + { new Guid("2c637584-cdbb-485e-8c3f-bc713eebaa03"), "The Yin Brotherhood" }, + { new Guid("2e017142-848a-4203-a109-1fa905a5db04"), "The Ghosts of Creuss" }, + { new Guid("2fffa08d-40ab-4e29-8e4a-0cda97b664be"), "The Mahact Gene-Sorcerers" }, + { new Guid("4210c2bb-f773-4721-907b-1c3e1cd11357"), "The Nomad" }, + { new Guid("4df36a67-6b3d-4966-ae0c-6ebdb6d0a97d"), "The L1Z1X Mindnet" }, + { new Guid("51ee1c82-279b-444c-a6aa-a8cd475612fd"), "The Council Keleres (The Mentak Coalition)" }, + { new Guid("5265b1f2-4422-4d0b-8711-582330d9afe4"), "The Yssaril Tribes" }, + { new Guid("609382d1-c969-4144-916a-ad4c13df1352"), "_Unknown_" }, + { new Guid("79c4217c-dc20-46fe-9e0b-44e852dfc64b"), "The Nekro Virus" }, + { new Guid("91da5e70-a3db-4168-b18d-61ae7b899b48"), "The Vuil'Raith Cabal" }, + { new Guid("9b8cf62c-9c94-49aa-b55b-11a8f4f9a7c9"), "The Winnu" }, + { new Guid("a386b3e8-683b-4e7e-9fba-3361bdbbeef1"), "The Federation of Sol" }, + { new Guid("b01aaa66-1888-4926-803e-95a864057219"), "The Clan of Saar" }, + { new Guid("be81d0fd-0ba8-4d55-ac65-4eb66d49028a"), "The Argent Flight" }, + { new Guid("c4e77f65-5a61-404f-8793-b672ccdb29a4"), "The Titans of UL" }, + { new Guid("c765da52-404b-4073-8c0c-003537169b4c"), "The Mentak Coalition" }, + { new Guid("c9a5311a-4798-44c5-8b97-3a5bc4eaa01a"), "The Emirates of Hacan" }, + { new Guid("d819ecf0-3ccd-45d1-9f19-ba045d39fd65"), "The Council Keleres (The Xxcha Kingdoms)" }, + { new Guid("e4d8ad5c-cf62-4001-9b81-f2a31bd87b0d"), "The Barony of Letnev" }, + { new Guid("f39815c4-b651-4ed4-86e0-5b4f5f6128ef"), "The Empyrean" }, + { new Guid("fcae96dd-793a-4393-9f5f-f378ff167b12"), "The Embers of Muaat" } + }); + + migrationBuilder.CreateIndex( + name: "IX_Expansions_PlayIdentifier", + table: "Expansions", + column: "PlayIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_Players_ColourIdentifier", + table: "Players", + column: "ColourIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_Players_FactionIdentifier", + table: "Players", + column: "FactionIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_Players_PersonIdentifier", + table: "Players", + column: "PersonIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_Players_PlayIdentifier", + table: "Players", + column: "PlayIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_Variants_PlayIdentifier", + table: "Variants", + column: "PlayIdentifier"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Expansions"); + + migrationBuilder.DropTable( + name: "Players"); + + migrationBuilder.DropTable( + name: "Variants"); + + migrationBuilder.DropTable( + name: "Colours"); + + migrationBuilder.DropTable( + name: "Factions"); + + migrationBuilder.DropTable( + name: "People"); + + migrationBuilder.DropTable( + name: "Plays"); + } + } +} diff --git a/DB.Migrations.Sqlite/Migrations/20240220210539_VariantsExpansionsManyPlays.Designer.cs b/DB.Migrations.Sqlite/Migrations/20240220210539_VariantsExpansionsManyPlays.Designer.cs new file mode 100644 index 0000000..c4e5ea3 --- /dev/null +++ b/DB.Migrations.Sqlite/Migrations/20240220210539_VariantsExpansionsManyPlays.Designer.cs @@ -0,0 +1,497 @@ +// +using System; +using Hesketh.MecatolArchives.DB; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hesketh.MecatolArchives.DB.Migrations.Sqlite.Migrations +{ + [DbContext(typeof(MecatolArchivesDbContext))] + [Migration("20240220210539_VariantsExpansionsManyPlays")] + partial class VariantsExpansionsManyPlays + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("ExpansionPlay", b => + { + b.Property("ExpansionsIdentifier") + .HasColumnType("TEXT"); + + b.Property("PlaysIdentifier") + .HasColumnType("TEXT"); + + b.HasKey("ExpansionsIdentifier", "PlaysIdentifier"); + + b.HasIndex("PlaysIdentifier"); + + b.ToTable("ExpansionPlay"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Colour", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Hex") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Colours"); + + b.HasData( + new + { + Identifier = new Guid("cbdfdda9-13bf-4a45-be5d-0882f6dcbad8"), + Hex = "", + Name = "_Unknown_" + }, + new + { + Identifier = new Guid("564f166e-33cb-45cc-bca6-1b2d16b8bf60"), + Hex = "#000000", + Name = "Black" + }, + new + { + Identifier = new Guid("53bf36a1-669c-41ed-9ced-4fa94ba038ee"), + Hex = "#FF0000", + Name = "Red" + }, + new + { + Identifier = new Guid("e8ca7b27-00cd-4a2a-bc7f-17105e690e2d"), + Hex = "#008000", + Name = "Green" + }, + new + { + Identifier = new Guid("51b6cc96-0e35-48f9-8665-b50bbe3fdb44"), + Hex = "#FFFF00", + Name = "Yellow" + }, + new + { + Identifier = new Guid("a9c3b568-d781-452d-91ae-44b0cc8e7020"), + Hex = "#800080", + Name = "Purple" + }, + new + { + Identifier = new Guid("43c078a5-0561-40f0-8adc-92afa32eaeb0"), + Hex = "#FFA500", + Name = "Orange" + }, + new + { + Identifier = new Guid("daceda53-e450-4fce-82d4-ef1cdd312e38"), + Hex = "#FF00FF", + Name = "Magenta" + }, + new + { + Identifier = new Guid("b5616b41-2821-4a27-85dc-fa81b899e578"), + Hex = "#0000FF", + Name = "Blue" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Expansion", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Expansions"); + + b.HasData( + new + { + Identifier = new Guid("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"), + Name = "Prophecy of Kings" + }, + new + { + Identifier = new Guid("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"), + Name = "Codex I: Ordinian" + }, + new + { + Identifier = new Guid("9420502f-4ef0-4887-add4-3d8a4941016a"), + Name = "Codex II: Affinity" + }, + new + { + Identifier = new Guid("1eb732ba-74ac-4993-943e-cd6f3650d310"), + Name = "Codex III: Vigil" + }, + new + { + Identifier = new Guid("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"), + Name = "Omega Initiative I" + }, + new + { + Identifier = new Guid("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"), + Name = "Omega Initiative II" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Faction", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Factions"); + + b.HasData( + new + { + Identifier = new Guid("11ee6931-58d5-4808-b2c5-e3d4b7bf4343"), + Name = "The Arborec" + }, + new + { + Identifier = new Guid("e4d8ad5c-cf62-4001-9b81-f2a31bd87b0d"), + Name = "The Barony of Letnev" + }, + new + { + Identifier = new Guid("b01aaa66-1888-4926-803e-95a864057219"), + Name = "The Clan of Saar" + }, + new + { + Identifier = new Guid("fcae96dd-793a-4393-9f5f-f378ff167b12"), + Name = "The Embers of Muaat" + }, + new + { + Identifier = new Guid("c9a5311a-4798-44c5-8b97-3a5bc4eaa01a"), + Name = "The Emirates of Hacan" + }, + new + { + Identifier = new Guid("a386b3e8-683b-4e7e-9fba-3361bdbbeef1"), + Name = "The Federation of Sol" + }, + new + { + Identifier = new Guid("2e017142-848a-4203-a109-1fa905a5db04"), + Name = "The Ghosts of Creuss" + }, + new + { + Identifier = new Guid("4df36a67-6b3d-4966-ae0c-6ebdb6d0a97d"), + Name = "The L1Z1X Mindnet" + }, + new + { + Identifier = new Guid("c765da52-404b-4073-8c0c-003537169b4c"), + Name = "The Mentak Coalition" + }, + new + { + Identifier = new Guid("2002e0b3-603f-4b66-8268-2ca8ab2bfce4"), + Name = "The Naalu Collective" + }, + new + { + Identifier = new Guid("79c4217c-dc20-46fe-9e0b-44e852dfc64b"), + Name = "The Nekro Virus" + }, + new + { + Identifier = new Guid("1a5777dc-4106-46ec-8168-596c7c9edd36"), + Name = "Sardakk N'orr" + }, + new + { + Identifier = new Guid("0cc14756-19f7-42ef-acd0-156fab6bcd2b"), + Name = "The Universities of Jol-Nar" + }, + new + { + Identifier = new Guid("9b8cf62c-9c94-49aa-b55b-11a8f4f9a7c9"), + Name = "The Winnu" + }, + new + { + Identifier = new Guid("279eefc5-4bd3-41be-93d0-b49bd6e9b7d6"), + Name = "The Xxcha Kingdom" + }, + new + { + Identifier = new Guid("2c637584-cdbb-485e-8c3f-bc713eebaa03"), + Name = "The Yin Brotherhood" + }, + new + { + Identifier = new Guid("5265b1f2-4422-4d0b-8711-582330d9afe4"), + Name = "The Yssaril Tribes" + }, + new + { + Identifier = new Guid("be81d0fd-0ba8-4d55-ac65-4eb66d49028a"), + Name = "The Argent Flight" + }, + new + { + Identifier = new Guid("f39815c4-b651-4ed4-86e0-5b4f5f6128ef"), + Name = "The Empyrean" + }, + new + { + Identifier = new Guid("2fffa08d-40ab-4e29-8e4a-0cda97b664be"), + Name = "The Mahact Gene-Sorcerers" + }, + new + { + Identifier = new Guid("26251032-94c9-4331-8ccf-697755213fad"), + Name = "The Naaz-Rokha Alliance" + }, + new + { + Identifier = new Guid("4210c2bb-f773-4721-907b-1c3e1cd11357"), + Name = "The Nomad" + }, + new + { + Identifier = new Guid("c4e77f65-5a61-404f-8793-b672ccdb29a4"), + Name = "The Titans of UL" + }, + new + { + Identifier = new Guid("91da5e70-a3db-4168-b18d-61ae7b899b48"), + Name = "The Vuil'Raith Cabal" + }, + new + { + Identifier = new Guid("51ee1c82-279b-444c-a6aa-a8cd475612fd"), + Name = "The Council Keleres (The Mentak Coalition)" + }, + new + { + Identifier = new Guid("d819ecf0-3ccd-45d1-9f19-ba045d39fd65"), + Name = "The Council Keleres (The Xxcha Kingdoms)" + }, + new + { + Identifier = new Guid("0747201a-f0ae-4c88-840f-00a3d6c618a0"), + Name = "The Council Keleres (The Argent Flight)" + }, + new + { + Identifier = new Guid("609382d1-c969-4144-916a-ad4c13df1352"), + Name = "_Unknown_" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Person", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Map") + .HasColumnType("TEXT"); + + b.Property("PointGoal") + .HasColumnType("INTEGER"); + + b.Property("RulesVersion") + .HasColumnType("REAL"); + + b.Property("UtcDate") + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Plays"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ColourIdentifier") + .HasColumnType("TEXT"); + + b.Property("Eliminated") + .HasColumnType("INTEGER"); + + b.Property("FactionIdentifier") + .HasColumnType("TEXT"); + + b.Property("PersonIdentifier") + .HasColumnType("TEXT"); + + b.Property("PlayIdentifier") + .HasColumnType("TEXT"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("Winner") + .HasColumnType("INTEGER"); + + b.HasKey("Identifier"); + + b.HasIndex("ColourIdentifier"); + + b.HasIndex("FactionIdentifier"); + + b.HasIndex("PersonIdentifier"); + + b.HasIndex("PlayIdentifier"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Variant", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Variants"); + }); + + modelBuilder.Entity("PlayVariant", b => + { + b.Property("PlaysIdentifier") + .HasColumnType("TEXT"); + + b.Property("VariantsIdentifier") + .HasColumnType("TEXT"); + + b.HasKey("PlaysIdentifier", "VariantsIdentifier"); + + b.HasIndex("VariantsIdentifier"); + + b.ToTable("PlayVariant"); + }); + + modelBuilder.Entity("ExpansionPlay", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Expansion", null) + .WithMany() + .HasForeignKey("ExpansionsIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null) + .WithMany() + .HasForeignKey("PlaysIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Colour", "Colour") + .WithMany() + .HasForeignKey("ColourIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Faction", "Faction") + .WithMany() + .HasForeignKey("FactionIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Person", "Person") + .WithMany() + .HasForeignKey("PersonIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", "Play") + .WithMany("Players") + .HasForeignKey("PlayIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Colour"); + + b.Navigation("Faction"); + + b.Navigation("Person"); + + b.Navigation("Play"); + }); + + modelBuilder.Entity("PlayVariant", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null) + .WithMany() + .HasForeignKey("PlaysIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Variant", null) + .WithMany() + .HasForeignKey("VariantsIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b => + { + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DB.Migrations.Sqlite/Migrations/20240220210539_VariantsExpansionsManyPlays.cs b/DB.Migrations.Sqlite/Migrations/20240220210539_VariantsExpansionsManyPlays.cs new file mode 100644 index 0000000..ddbb28f --- /dev/null +++ b/DB.Migrations.Sqlite/Migrations/20240220210539_VariantsExpansionsManyPlays.cs @@ -0,0 +1,185 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hesketh.MecatolArchives.DB.Migrations.Sqlite.Migrations +{ + /// + public partial class VariantsExpansionsManyPlays : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Expansions_Plays_PlayIdentifier", + table: "Expansions"); + + migrationBuilder.DropForeignKey( + name: "FK_Variants_Plays_PlayIdentifier", + table: "Variants"); + + migrationBuilder.DropIndex( + name: "IX_Variants_PlayIdentifier", + table: "Variants"); + + migrationBuilder.DropIndex( + name: "IX_Expansions_PlayIdentifier", + table: "Expansions"); + + migrationBuilder.DropColumn( + name: "PlayIdentifier", + table: "Variants"); + + migrationBuilder.DropColumn( + name: "PlayIdentifier", + table: "Expansions"); + + migrationBuilder.CreateTable( + name: "ExpansionPlay", + columns: table => new + { + ExpansionsIdentifier = table.Column(type: "TEXT", nullable: false), + PlaysIdentifier = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExpansionPlay", x => new { x.ExpansionsIdentifier, x.PlaysIdentifier }); + table.ForeignKey( + name: "FK_ExpansionPlay_Expansions_ExpansionsIdentifier", + column: x => x.ExpansionsIdentifier, + principalTable: "Expansions", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ExpansionPlay_Plays_PlaysIdentifier", + column: x => x.PlaysIdentifier, + principalTable: "Plays", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PlayVariant", + columns: table => new + { + PlaysIdentifier = table.Column(type: "TEXT", nullable: false), + VariantsIdentifier = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PlayVariant", x => new { x.PlaysIdentifier, x.VariantsIdentifier }); + table.ForeignKey( + name: "FK_PlayVariant_Plays_PlaysIdentifier", + column: x => x.PlaysIdentifier, + principalTable: "Plays", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PlayVariant_Variants_VariantsIdentifier", + column: x => x.VariantsIdentifier, + principalTable: "Variants", + principalColumn: "Identifier", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ExpansionPlay_PlaysIdentifier", + table: "ExpansionPlay", + column: "PlaysIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_PlayVariant_VariantsIdentifier", + table: "PlayVariant", + column: "VariantsIdentifier"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ExpansionPlay"); + + migrationBuilder.DropTable( + name: "PlayVariant"); + + migrationBuilder.AddColumn( + name: "PlayIdentifier", + table: "Variants", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "PlayIdentifier", + table: "Expansions", + type: "TEXT", + nullable: true); + + migrationBuilder.UpdateData( + table: "Expansions", + keyColumn: "Identifier", + keyValue: new Guid("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"), + column: "PlayIdentifier", + value: null); + + migrationBuilder.UpdateData( + table: "Expansions", + keyColumn: "Identifier", + keyValue: new Guid("1eb732ba-74ac-4993-943e-cd6f3650d310"), + column: "PlayIdentifier", + value: null); + + migrationBuilder.UpdateData( + table: "Expansions", + keyColumn: "Identifier", + keyValue: new Guid("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"), + column: "PlayIdentifier", + value: null); + + migrationBuilder.UpdateData( + table: "Expansions", + keyColumn: "Identifier", + keyValue: new Guid("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"), + column: "PlayIdentifier", + value: null); + + migrationBuilder.UpdateData( + table: "Expansions", + keyColumn: "Identifier", + keyValue: new Guid("9420502f-4ef0-4887-add4-3d8a4941016a"), + column: "PlayIdentifier", + value: null); + + migrationBuilder.UpdateData( + table: "Expansions", + keyColumn: "Identifier", + keyValue: new Guid("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"), + column: "PlayIdentifier", + value: null); + + migrationBuilder.CreateIndex( + name: "IX_Variants_PlayIdentifier", + table: "Variants", + column: "PlayIdentifier"); + + migrationBuilder.CreateIndex( + name: "IX_Expansions_PlayIdentifier", + table: "Expansions", + column: "PlayIdentifier"); + + migrationBuilder.AddForeignKey( + name: "FK_Expansions_Plays_PlayIdentifier", + table: "Expansions", + column: "PlayIdentifier", + principalTable: "Plays", + principalColumn: "Identifier"); + + migrationBuilder.AddForeignKey( + name: "FK_Variants_Plays_PlayIdentifier", + table: "Variants", + column: "PlayIdentifier", + principalTable: "Plays", + principalColumn: "Identifier"); + } + } +} diff --git a/DB.Migrations.Sqlite/Migrations/MecatolArchivesDbContextModelSnapshot.cs b/DB.Migrations.Sqlite/Migrations/MecatolArchivesDbContextModelSnapshot.cs new file mode 100644 index 0000000..527ec87 --- /dev/null +++ b/DB.Migrations.Sqlite/Migrations/MecatolArchivesDbContextModelSnapshot.cs @@ -0,0 +1,494 @@ +// +using System; +using Hesketh.MecatolArchives.DB; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hesketh.MecatolArchives.DB.Migrations.Sqlite.Migrations +{ + [DbContext(typeof(MecatolArchivesDbContext))] + partial class MecatolArchivesDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("ExpansionPlay", b => + { + b.Property("ExpansionsIdentifier") + .HasColumnType("TEXT"); + + b.Property("PlaysIdentifier") + .HasColumnType("TEXT"); + + b.HasKey("ExpansionsIdentifier", "PlaysIdentifier"); + + b.HasIndex("PlaysIdentifier"); + + b.ToTable("ExpansionPlay"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Colour", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Hex") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Colours"); + + b.HasData( + new + { + Identifier = new Guid("cbdfdda9-13bf-4a45-be5d-0882f6dcbad8"), + Hex = "", + Name = "_Unknown_" + }, + new + { + Identifier = new Guid("564f166e-33cb-45cc-bca6-1b2d16b8bf60"), + Hex = "#000000", + Name = "Black" + }, + new + { + Identifier = new Guid("53bf36a1-669c-41ed-9ced-4fa94ba038ee"), + Hex = "#FF0000", + Name = "Red" + }, + new + { + Identifier = new Guid("e8ca7b27-00cd-4a2a-bc7f-17105e690e2d"), + Hex = "#008000", + Name = "Green" + }, + new + { + Identifier = new Guid("51b6cc96-0e35-48f9-8665-b50bbe3fdb44"), + Hex = "#FFFF00", + Name = "Yellow" + }, + new + { + Identifier = new Guid("a9c3b568-d781-452d-91ae-44b0cc8e7020"), + Hex = "#800080", + Name = "Purple" + }, + new + { + Identifier = new Guid("43c078a5-0561-40f0-8adc-92afa32eaeb0"), + Hex = "#FFA500", + Name = "Orange" + }, + new + { + Identifier = new Guid("daceda53-e450-4fce-82d4-ef1cdd312e38"), + Hex = "#FF00FF", + Name = "Magenta" + }, + new + { + Identifier = new Guid("b5616b41-2821-4a27-85dc-fa81b899e578"), + Hex = "#0000FF", + Name = "Blue" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Expansion", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Expansions"); + + b.HasData( + new + { + Identifier = new Guid("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"), + Name = "Prophecy of Kings" + }, + new + { + Identifier = new Guid("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"), + Name = "Codex I: Ordinian" + }, + new + { + Identifier = new Guid("9420502f-4ef0-4887-add4-3d8a4941016a"), + Name = "Codex II: Affinity" + }, + new + { + Identifier = new Guid("1eb732ba-74ac-4993-943e-cd6f3650d310"), + Name = "Codex III: Vigil" + }, + new + { + Identifier = new Guid("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"), + Name = "Omega Initiative I" + }, + new + { + Identifier = new Guid("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"), + Name = "Omega Initiative II" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Faction", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Factions"); + + b.HasData( + new + { + Identifier = new Guid("11ee6931-58d5-4808-b2c5-e3d4b7bf4343"), + Name = "The Arborec" + }, + new + { + Identifier = new Guid("e4d8ad5c-cf62-4001-9b81-f2a31bd87b0d"), + Name = "The Barony of Letnev" + }, + new + { + Identifier = new Guid("b01aaa66-1888-4926-803e-95a864057219"), + Name = "The Clan of Saar" + }, + new + { + Identifier = new Guid("fcae96dd-793a-4393-9f5f-f378ff167b12"), + Name = "The Embers of Muaat" + }, + new + { + Identifier = new Guid("c9a5311a-4798-44c5-8b97-3a5bc4eaa01a"), + Name = "The Emirates of Hacan" + }, + new + { + Identifier = new Guid("a386b3e8-683b-4e7e-9fba-3361bdbbeef1"), + Name = "The Federation of Sol" + }, + new + { + Identifier = new Guid("2e017142-848a-4203-a109-1fa905a5db04"), + Name = "The Ghosts of Creuss" + }, + new + { + Identifier = new Guid("4df36a67-6b3d-4966-ae0c-6ebdb6d0a97d"), + Name = "The L1Z1X Mindnet" + }, + new + { + Identifier = new Guid("c765da52-404b-4073-8c0c-003537169b4c"), + Name = "The Mentak Coalition" + }, + new + { + Identifier = new Guid("2002e0b3-603f-4b66-8268-2ca8ab2bfce4"), + Name = "The Naalu Collective" + }, + new + { + Identifier = new Guid("79c4217c-dc20-46fe-9e0b-44e852dfc64b"), + Name = "The Nekro Virus" + }, + new + { + Identifier = new Guid("1a5777dc-4106-46ec-8168-596c7c9edd36"), + Name = "Sardakk N'orr" + }, + new + { + Identifier = new Guid("0cc14756-19f7-42ef-acd0-156fab6bcd2b"), + Name = "The Universities of Jol-Nar" + }, + new + { + Identifier = new Guid("9b8cf62c-9c94-49aa-b55b-11a8f4f9a7c9"), + Name = "The Winnu" + }, + new + { + Identifier = new Guid("279eefc5-4bd3-41be-93d0-b49bd6e9b7d6"), + Name = "The Xxcha Kingdom" + }, + new + { + Identifier = new Guid("2c637584-cdbb-485e-8c3f-bc713eebaa03"), + Name = "The Yin Brotherhood" + }, + new + { + Identifier = new Guid("5265b1f2-4422-4d0b-8711-582330d9afe4"), + Name = "The Yssaril Tribes" + }, + new + { + Identifier = new Guid("be81d0fd-0ba8-4d55-ac65-4eb66d49028a"), + Name = "The Argent Flight" + }, + new + { + Identifier = new Guid("f39815c4-b651-4ed4-86e0-5b4f5f6128ef"), + Name = "The Empyrean" + }, + new + { + Identifier = new Guid("2fffa08d-40ab-4e29-8e4a-0cda97b664be"), + Name = "The Mahact Gene-Sorcerers" + }, + new + { + Identifier = new Guid("26251032-94c9-4331-8ccf-697755213fad"), + Name = "The Naaz-Rokha Alliance" + }, + new + { + Identifier = new Guid("4210c2bb-f773-4721-907b-1c3e1cd11357"), + Name = "The Nomad" + }, + new + { + Identifier = new Guid("c4e77f65-5a61-404f-8793-b672ccdb29a4"), + Name = "The Titans of UL" + }, + new + { + Identifier = new Guid("91da5e70-a3db-4168-b18d-61ae7b899b48"), + Name = "The Vuil'Raith Cabal" + }, + new + { + Identifier = new Guid("51ee1c82-279b-444c-a6aa-a8cd475612fd"), + Name = "The Council Keleres (The Mentak Coalition)" + }, + new + { + Identifier = new Guid("d819ecf0-3ccd-45d1-9f19-ba045d39fd65"), + Name = "The Council Keleres (The Xxcha Kingdoms)" + }, + new + { + Identifier = new Guid("0747201a-f0ae-4c88-840f-00a3d6c618a0"), + Name = "The Council Keleres (The Argent Flight)" + }, + new + { + Identifier = new Guid("609382d1-c969-4144-916a-ad4c13df1352"), + Name = "_Unknown_" + }); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Person", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Map") + .HasColumnType("TEXT"); + + b.Property("PointGoal") + .HasColumnType("INTEGER"); + + b.Property("RulesVersion") + .HasColumnType("REAL"); + + b.Property("UtcDate") + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Plays"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ColourIdentifier") + .HasColumnType("TEXT"); + + b.Property("Eliminated") + .HasColumnType("INTEGER"); + + b.Property("FactionIdentifier") + .HasColumnType("TEXT"); + + b.Property("PersonIdentifier") + .HasColumnType("TEXT"); + + b.Property("PlayIdentifier") + .HasColumnType("TEXT"); + + b.Property("Points") + .HasColumnType("INTEGER"); + + b.Property("Winner") + .HasColumnType("INTEGER"); + + b.HasKey("Identifier"); + + b.HasIndex("ColourIdentifier"); + + b.HasIndex("FactionIdentifier"); + + b.HasIndex("PersonIdentifier"); + + b.HasIndex("PlayIdentifier"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Variant", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Variants"); + }); + + modelBuilder.Entity("PlayVariant", b => + { + b.Property("PlaysIdentifier") + .HasColumnType("TEXT"); + + b.Property("VariantsIdentifier") + .HasColumnType("TEXT"); + + b.HasKey("PlaysIdentifier", "VariantsIdentifier"); + + b.HasIndex("VariantsIdentifier"); + + b.ToTable("PlayVariant"); + }); + + modelBuilder.Entity("ExpansionPlay", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Expansion", null) + .WithMany() + .HasForeignKey("ExpansionsIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null) + .WithMany() + .HasForeignKey("PlaysIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Player", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Colour", "Colour") + .WithMany() + .HasForeignKey("ColourIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Faction", "Faction") + .WithMany() + .HasForeignKey("FactionIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Person", "Person") + .WithMany() + .HasForeignKey("PersonIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", "Play") + .WithMany("Players") + .HasForeignKey("PlayIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Colour"); + + b.Navigation("Faction"); + + b.Navigation("Person"); + + b.Navigation("Play"); + }); + + modelBuilder.Entity("PlayVariant", b => + { + b.HasOne("Hesketh.MecatolArchives.DB.Models.Play", null) + .WithMany() + .HasForeignKey("PlaysIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hesketh.MecatolArchives.DB.Models.Variant", null) + .WithMany() + .HasForeignKey("VariantsIdentifier") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hesketh.MecatolArchives.DB.Models.Play", b => + { + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DB/DB.csproj b/DB/DB.csproj new file mode 100644 index 0000000..e87764d --- /dev/null +++ b/DB/DB.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/DB/MecatolArchivesDbContext.cs b/DB/MecatolArchivesDbContext.cs new file mode 100644 index 0000000..1af96dc --- /dev/null +++ b/DB/MecatolArchivesDbContext.cs @@ -0,0 +1,264 @@ +using Hesketh.MecatolArchives.DB.Models; +using Microsoft.EntityFrameworkCore; + +namespace Hesketh.MecatolArchives.DB; + +public sealed class MecatolArchivesDbContext : DbContext +{ + public const string UnknownName = "_Unknown_"; + + public MecatolArchivesDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Colours { get; set; } = null!; + public DbSet Expansions { get; set; } = null!; + public DbSet Factions { get; set; } = null!; + public DbSet People { get; set; } = null!; + public DbSet Plays { get; set; } = null!; + public DbSet Players { get; set; } = null!; + public DbSet Variants { get; set; } = null!; + + public DbSet GetDbSet() where TEntity : class, IEntity + { + if (typeof(TEntity) == typeof(Colour)) return (Colours as DbSet)!; + if (typeof(TEntity) == typeof(Expansion)) return (Expansions as DbSet)!; + if (typeof(TEntity) == typeof(Faction)) return (Factions as DbSet)!; + if (typeof(TEntity) == typeof(Person)) return (People as DbSet)!; + if (typeof(TEntity) == typeof(Play)) return (Plays as DbSet)!; + if (typeof(TEntity) == typeof(Player)) return (Players as DbSet)!; + if (typeof(TEntity) == typeof(Variant)) return (Variants as DbSet)!; + + throw new InvalidOperationException($"No DbSet is registered for type {typeof(TEntity).FullName}"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + SeedExpansions(modelBuilder); + SeedColours(modelBuilder); + SeedFactions(modelBuilder); + } + + private void SeedVariants(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasData(new Variant + { + Identifier = Guid.Parse("4082a883-4c5f-44c7-abf6-0d4823698cb0"), + Name = "Standard" + }, new Variant + { + Identifier = Guid.Parse("f018f38e-85fd-4c5b-9036-d9c071aae506"), + Name = "Competitive" + }, new Variant + { + Identifier = Guid.Parse("c0db28a6-29e1-4463-a187-dd49d5416c0a"), + Name = "Milty" + }, new Variant + { + Identifier = Guid.Parse("b6125086-d6ae-4f4a-b981-5f4e63b15cc0"), + Name = "Alliance" + }, new Variant + { + Identifier = Guid.Parse("5b8c8301-b5e9-4c72-b6fa-b07cb30005be"), + Name = "Pax Magnifica" + }); + } + + private void SeedExpansions(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasData(new Expansion + { + Identifier = Guid.Parse("fb08d4e6-5ac1-4cbf-8eb9-166f6c5e41f0"), + Name = "Prophecy of Kings" + }, new Expansion + { + Identifier = Guid.Parse("21fbcaf7-ae17-4db7-851a-dc65eb3ba60f"), + Name = "Codex I: Ordinian" + }, new Expansion + { + Identifier = Guid.Parse("9420502f-4ef0-4887-add4-3d8a4941016a"), + Name = "Codex II: Affinity" + }, new Expansion + { + Identifier = Guid.Parse("1eb732ba-74ac-4993-943e-cd6f3650d310"), + Name = "Codex III: Vigil" + }, new Expansion + { + Identifier = Guid.Parse("2b7c9cd6-a7d8-40f1-843d-8f10c7c45fb3"), + Name = "Omega Initiative I" + }, new Expansion + { + Identifier = Guid.Parse("0e886b80-9ef3-48c0-bfc5-8107ee183c1b"), + Name = "Omega Initiative II" + }); + } + + private void SeedFactions(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasData(new Faction + { + Identifier = Guid.Parse("11ee6931-58d5-4808-b2c5-e3d4b7bf4343"), + Name = "The Arborec" + }, new Faction + { + Identifier = Guid.Parse("e4d8ad5c-cf62-4001-9b81-f2a31bd87b0d"), + Name = "The Barony of Letnev" + }, new Faction + { + Identifier = Guid.Parse("b01aaa66-1888-4926-803e-95a864057219"), + Name = "The Clan of Saar" + }, new Faction + { + Identifier = Guid.Parse("fcae96dd-793a-4393-9f5f-f378ff167b12"), + Name = "The Embers of Muaat" + }, new Faction + { + Identifier = Guid.Parse("c9a5311a-4798-44c5-8b97-3a5bc4eaa01a"), + Name = "The Emirates of Hacan" + }, new Faction + { + Identifier = Guid.Parse("a386b3e8-683b-4e7e-9fba-3361bdbbeef1"), + Name = "The Federation of Sol" + }, new Faction + { + Identifier = Guid.Parse("2e017142-848a-4203-a109-1fa905a5db04"), + Name = "The Ghosts of Creuss" + }, new Faction + { + Identifier = Guid.Parse("4df36a67-6b3d-4966-ae0c-6ebdb6d0a97d"), + Name = "The L1Z1X Mindnet" + }, new Faction + { + Identifier = Guid.Parse("c765da52-404b-4073-8c0c-003537169b4c"), + Name = "The Mentak Coalition" + }, new Faction + { + Identifier = Guid.Parse("2002e0b3-603f-4b66-8268-2ca8ab2bfce4"), + Name = "The Naalu Collective" + }, new Faction + { + Identifier = Guid.Parse("79c4217c-dc20-46fe-9e0b-44e852dfc64b"), + Name = "The Nekro Virus" + }, new Faction + { + Identifier = Guid.Parse("1a5777dc-4106-46ec-8168-596c7c9edd36"), + Name = "Sardakk N'orr" + }, new Faction + { + Identifier = Guid.Parse("0cc14756-19f7-42ef-acd0-156fab6bcd2b"), + Name = "The Universities of Jol-Nar" + }, new Faction + { + Identifier = Guid.Parse("9b8cf62c-9c94-49aa-b55b-11a8f4f9a7c9"), + Name = "The Winnu" + }, new Faction + { + Identifier = Guid.Parse("279eefc5-4bd3-41be-93d0-b49bd6e9b7d6"), + Name = "The Xxcha Kingdom" + }, new Faction + { + Identifier = Guid.Parse("2c637584-cdbb-485e-8c3f-bc713eebaa03"), + Name = "The Yin Brotherhood" + }, new Faction + { + Identifier = Guid.Parse("5265b1f2-4422-4d0b-8711-582330d9afe4"), + Name = "The Yssaril Tribes" + }, new Faction + { + Identifier = Guid.Parse("be81d0fd-0ba8-4d55-ac65-4eb66d49028a"), + Name = "The Argent Flight" + }, new Faction + { + Identifier = Guid.Parse("f39815c4-b651-4ed4-86e0-5b4f5f6128ef"), + Name = "The Empyrean" + }, new Faction + { + Identifier = Guid.Parse("2fffa08d-40ab-4e29-8e4a-0cda97b664be"), + Name = "The Mahact Gene-Sorcerers" + }, new Faction + { + Identifier = Guid.Parse("26251032-94c9-4331-8ccf-697755213fad"), + Name = "The Naaz-Rokha Alliance" + }, new Faction + { + Identifier = Guid.Parse("4210c2bb-f773-4721-907b-1c3e1cd11357"), + Name = "The Nomad" + }, new Faction + { + Identifier = Guid.Parse("c4e77f65-5a61-404f-8793-b672ccdb29a4"), + Name = "The Titans of UL" + }, new Faction + { + Identifier = Guid.Parse("91da5e70-a3db-4168-b18d-61ae7b899b48"), + Name = "The Vuil'Raith Cabal" + }, new Faction + { + Identifier = Guid.Parse("51ee1c82-279b-444c-a6aa-a8cd475612fd"), + Name = "The Council Keleres (The Mentak Coalition)" + }, new Faction + { + Identifier = Guid.Parse("d819ecf0-3ccd-45d1-9f19-ba045d39fd65"), + Name = "The Council Keleres (The Xxcha Kingdoms)" + }, new Faction + { + Identifier = Guid.Parse("0747201a-f0ae-4c88-840f-00a3d6c618a0"), + Name = "The Council Keleres (The Argent Flight)" + }, new Faction + { + Identifier = Guid.Parse("609382d1-c969-4144-916a-ad4c13df1352"), + Name = UnknownName + }); + } + + private void SeedColours(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasData(new Colour + { + Identifier = Guid.Parse("CBDFDDA9-13BF-4A45-BE5D-0882F6DCBAD8"), + Hex = string.Empty, + Name = UnknownName + }, new Colour + { + Identifier = Guid.Parse("564F166E-33CB-45CC-BCA6-1B2D16B8BF60"), + Hex = "#000000", + Name = "Black" + }, new Colour + { + Identifier = Guid.Parse("53BF36A1-669C-41ED-9CED-4FA94BA038EE"), + Hex = "#FF0000", + Name = "Red" + }, new Colour + { + Identifier = Guid.Parse("E8CA7B27-00CD-4A2A-BC7F-17105E690E2D"), + Hex = "#008000", + Name = "Green" + }, new Colour + { + Identifier = Guid.Parse("51B6CC96-0E35-48F9-8665-B50BBE3FDB44"), + Hex = "#FFFF00", + Name = "Yellow" + }, new Colour + { + Identifier = Guid.Parse("A9C3B568-D781-452D-91AE-44B0CC8E7020"), + Hex = "#800080", + Name = "Purple" + }, new Colour + { + Identifier = Guid.Parse("43C078A5-0561-40F0-8ADC-92AFA32EAEB0"), + Hex = "#FFA500", + Name = "Orange" + }, new Colour + { + Identifier = Guid.Parse("DACEDA53-E450-4FCE-82D4-EF1CDD312E38"), + Hex = "#FF00FF", + Name = "Magenta" + }, new Colour + { + Identifier = Guid.Parse("B5616B41-2821-4A27-85DC-FA81B899E578"), + Hex = "#0000FF", + Name = "Blue" + }); + } +} \ No newline at end of file diff --git a/DB/Models/Colour.cs b/DB/Models/Colour.cs new file mode 100644 index 0000000..0d211b9 --- /dev/null +++ b/DB/Models/Colour.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Hesketh.MecatolArchives.DB.Models; + +public class Colour : IEntity, INamed +{ + public string Hex { get; set; } = null!; + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Identifier { get; set; } + + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/DB/Models/Expansion.cs b/DB/Models/Expansion.cs new file mode 100644 index 0000000..f6a37e1 --- /dev/null +++ b/DB/Models/Expansion.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Hesketh.MecatolArchives.DB.Models; + +public class Expansion : IEntity, INamed +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Identifier { get; set; } + + public string Name { get; set; } = null!; + + public virtual ICollection Plays { get; set; } = null!; +} \ No newline at end of file diff --git a/DB/Models/Faction.cs b/DB/Models/Faction.cs new file mode 100644 index 0000000..15e2f8e --- /dev/null +++ b/DB/Models/Faction.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Hesketh.MecatolArchives.DB.Models; + +public class Faction : IEntity, INamed +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Identifier { get; set; } + + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/DB/Models/IEntity.cs b/DB/Models/IEntity.cs new file mode 100644 index 0000000..8ee39bd --- /dev/null +++ b/DB/Models/IEntity.cs @@ -0,0 +1,6 @@ +namespace Hesketh.MecatolArchives.DB.Models; + +public interface IEntity +{ + public Guid Identifier { get; } +} \ No newline at end of file diff --git a/DB/Models/INamed.cs b/DB/Models/INamed.cs new file mode 100644 index 0000000..6b58614 --- /dev/null +++ b/DB/Models/INamed.cs @@ -0,0 +1,6 @@ +namespace Hesketh.MecatolArchives.DB.Models; + +public interface INamed +{ + public string Name { get; } +} \ No newline at end of file diff --git a/DB/Models/Person.cs b/DB/Models/Person.cs new file mode 100644 index 0000000..8bb667a --- /dev/null +++ b/DB/Models/Person.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Hesketh.MecatolArchives.DB.Models; + +public class Person : IEntity, INamed +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Identifier { get; set; } + + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/DB/Models/Play.cs b/DB/Models/Play.cs new file mode 100644 index 0000000..454c397 --- /dev/null +++ b/DB/Models/Play.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Hesketh.MecatolArchives.DB.Models; + +public class Play : IEntity, INamed +{ + public DateTime UtcDate { get; set; } = DateTime.UtcNow; + public double RulesVersion { get; set; } = 1.0; + public uint PointGoal { get; set; } = 10; + public string? Map { get; set; } = null; + + public virtual ICollection Players { get; set; } = null!; + public virtual ICollection Expansions { get; set; } = null!; + public virtual ICollection Variants { get; set; } = null!; + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Identifier { get; set; } + + [NotMapped] public string Name => UtcDate.ToString("yyyy-MM-dd"); +} \ No newline at end of file diff --git a/DB/Models/Player.cs b/DB/Models/Player.cs new file mode 100644 index 0000000..8298a85 --- /dev/null +++ b/DB/Models/Player.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Hesketh.MecatolArchives.DB.Models; + +public class Player : IEntity +{ + public uint Points { get; set; } = 0; + public bool Winner { get; set; } = false; + public bool Eliminated { get; set; } = false; + + public virtual Person Person { get; set; } = null!; + public virtual Play Play { get; set; } = null!; + public virtual Faction Faction { get; set; } = null!; + public virtual Colour Colour { get; set; } = null!; + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Identifier { get; set; } +} \ No newline at end of file diff --git a/DB/Models/Variant.cs b/DB/Models/Variant.cs new file mode 100644 index 0000000..8b3314a --- /dev/null +++ b/DB/Models/Variant.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Hesketh.MecatolArchives.DB.Models; + +public class Variant : IEntity, INamed +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Identifier { get; set; } + + public string Name { get; set; } = null!; + + public virtual ICollection Plays { get; set; } = null!; +} \ No newline at end of file diff --git a/Directory.build.targets b/Directory.build.targets new file mode 100644 index 0000000..6f49b04 --- /dev/null +++ b/Directory.build.targets @@ -0,0 +1,9 @@ + + + + Alex Hesketh + Hesketh + 0.1.1.0 + Hesketh.MecatolArchives.$(MSBuildProjectName.Replace(" ", "_")) + + \ No newline at end of file diff --git a/MecatolArchives.sln b/MecatolArchives.sln new file mode 100644 index 0000000..6462e7e --- /dev/null +++ b/MecatolArchives.sln @@ -0,0 +1,61 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34330.188 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "API", "API\API.csproj", "{EE248FC8-B54A-464A-A498-A3C88CE7B5CA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DB", "DB\DB.csproj", "{3B146EFC-992B-4B1A-B11F-3B3581E04F5D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "API.Data", "API.Data\API.Data.csproj", "{2D042396-502E-4C5C-A259-1C6EF2772C33}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "API.Client", "API.Client\API.Client.csproj", "{CCA02CC2-B8BE-4020-92C2-F503E0C8740A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Website", "Website\Website.csproj", "{AAFD2307-F2BE-4E14-8D5A-231D65C9DF87}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DB.Migrations.Sqlite", "DB.Migrations.Sqlite\DB.Migrations.Sqlite.csproj", "{610EB756-1BDB-4CBE-98BA-06FC31E59271}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DB.Migrations.Mssql", "DB.Migrations.Mssql\DB.Migrations.Mssql.csproj", "{C8A9F219-2852-42F6-8C49-6133798BDECA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EE248FC8-B54A-464A-A498-A3C88CE7B5CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE248FC8-B54A-464A-A498-A3C88CE7B5CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE248FC8-B54A-464A-A498-A3C88CE7B5CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE248FC8-B54A-464A-A498-A3C88CE7B5CA}.Release|Any CPU.Build.0 = Release|Any CPU + {3B146EFC-992B-4B1A-B11F-3B3581E04F5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B146EFC-992B-4B1A-B11F-3B3581E04F5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B146EFC-992B-4B1A-B11F-3B3581E04F5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B146EFC-992B-4B1A-B11F-3B3581E04F5D}.Release|Any CPU.Build.0 = Release|Any CPU + {2D042396-502E-4C5C-A259-1C6EF2772C33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D042396-502E-4C5C-A259-1C6EF2772C33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D042396-502E-4C5C-A259-1C6EF2772C33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D042396-502E-4C5C-A259-1C6EF2772C33}.Release|Any CPU.Build.0 = Release|Any CPU + {CCA02CC2-B8BE-4020-92C2-F503E0C8740A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCA02CC2-B8BE-4020-92C2-F503E0C8740A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCA02CC2-B8BE-4020-92C2-F503E0C8740A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCA02CC2-B8BE-4020-92C2-F503E0C8740A}.Release|Any CPU.Build.0 = Release|Any CPU + {AAFD2307-F2BE-4E14-8D5A-231D65C9DF87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAFD2307-F2BE-4E14-8D5A-231D65C9DF87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAFD2307-F2BE-4E14-8D5A-231D65C9DF87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAFD2307-F2BE-4E14-8D5A-231D65C9DF87}.Release|Any CPU.Build.0 = Release|Any CPU + {610EB756-1BDB-4CBE-98BA-06FC31E59271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {610EB756-1BDB-4CBE-98BA-06FC31E59271}.Debug|Any CPU.Build.0 = Debug|Any CPU + {610EB756-1BDB-4CBE-98BA-06FC31E59271}.Release|Any CPU.ActiveCfg = Release|Any CPU + {610EB756-1BDB-4CBE-98BA-06FC31E59271}.Release|Any CPU.Build.0 = Release|Any CPU + {C8A9F219-2852-42F6-8C49-6133798BDECA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8A9F219-2852-42F6-8C49-6133798BDECA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8A9F219-2852-42F6-8C49-6133798BDECA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8A9F219-2852-42F6-8C49-6133798BDECA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A66971D1-B6B9-4C3B-9C61-66E053B9C4A7} + EndGlobalSection +EndGlobal diff --git a/Website/Components/App.razor b/Website/Components/App.razor new file mode 100644 index 0000000..53a8903 --- /dev/null +++ b/Website/Components/App.razor @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Website/Components/Controls/PlayHistory.razor b/Website/Components/Controls/PlayHistory.razor new file mode 100644 index 0000000..06c06b3 --- /dev/null +++ b/Website/Components/Controls/PlayHistory.razor @@ -0,0 +1,91 @@ +@using Bromix.MudBlazor.MaterialDesignIcons +@using Hesketh.MecatolArchives.API.Client.Auth +@using Hesketh.MecatolArchives.API.Data +@using Hesketh.MecatolArchives.Website.Helpers + + + @foreach (var play in Plays) + { + + +
+ @play.UtcDate.ToLocalTime().ToLongDateString() + + @if (!ReadOnly && EditPlayPressedAction != null) + { +
+ + +
+ } +
+
+ + + + Player + Faction + Victory Points + + + +
+ @context.Person.Initials + @context.Person.Name +
+
+ +
+ + @context.Faction.Name +
+
+ + @if (context.Eliminated) + { + + } + else + { + @if (context.Winner) + { + + } + @context.Points + } + +
+
+
+
+ } +
+ +@code +{ + [Parameter] public ICollection Plays { get; set; } = new List(); + [Parameter] public bool ReadOnly { get; set; } = false; + [Parameter] public Action? EditPlayPressedAction { get; set; } = null; + [Parameter] public Action? DeletePlayPressedAction { get; set; } = null; + + private string GetFactionLink(Guid factionId) + { + return $"stats/factions/{factionId}"; + } + + private string GetPersonLink(Guid personId) + { + return $"stats/people/{personId}"; + } + + private bool ShouldInitialExpand(Play play) + { + return Plays.FirstOrDefault() == play; + } +} diff --git a/Website/Components/Controls/StatisticTable.razor b/Website/Components/Controls/StatisticTable.razor new file mode 100644 index 0000000..fad0bb3 --- /dev/null +++ b/Website/Components/Controls/StatisticTable.razor @@ -0,0 +1,25 @@ +@using Hesketh.MecatolArchives.API.Data + + + + Name + Played + Wins + Losses + Win % + VP % + + + + + + +@code +{ + [Parameter] public IEnumerable Statistics { get; set; } = Enumerable.Empty(); + [Parameter] public string? NavigationFormat { get; set; } = null; + [Parameter] public Func? GetCustomNamePrefixFragment { get; set; } = null; +} diff --git a/Website/Components/Controls/StatisticTableRow.razor b/Website/Components/Controls/StatisticTableRow.razor new file mode 100644 index 0000000..0db1ede --- /dev/null +++ b/Website/Components/Controls/StatisticTableRow.razor @@ -0,0 +1,43 @@ +@using Hesketh.MecatolArchives.API.Data +@using Hesketh.MecatolArchives.Website.Helpers + +@if (Context != null) +{ + + @if (CanNavigate(Context.LinkedIdentifier)) + { +
+ @GetCustomNamePrefixFragment?.Invoke(Context) + @Context.Name +
+ } + else + { + @Context.Name + } +
+ @Context.Plays + @Context.Wins + @Context.Losses + @(Context.WinPercentage.ToString("0.0"))% + @(Context.PointPercentage.ToString("0.0"))% +} + +@code +{ + [Parameter] public Statistic? Context { get; set; } = null; + [Parameter] public string? NavigationFormat { get; set; } = null; + [Parameter] public Func? GetCustomNamePrefixFragment { get; set; } = null; + + public string GetNavigationUrl(Guid linkedIdentifier) + { + return string.Format(NavigationFormat!, linkedIdentifier); + } + + public bool CanNavigate(Guid? linkedIdentifier) + { + return linkedIdentifier != null && !string.IsNullOrEmpty(NavigationFormat); + } +} \ No newline at end of file diff --git a/Website/Components/Dialogs/AuthDialog.razor b/Website/Components/Dialogs/AuthDialog.razor new file mode 100644 index 0000000..160e7c6 --- /dev/null +++ b/Website/Components/Dialogs/AuthDialog.razor @@ -0,0 +1,90 @@ +@using Hesketh.MecatolArchives.API.Client.Auth +@using Hesketh.MecatolArchives.API.Client.Clients +@using System.ComponentModel.DataAnnotations +@inject IAdminCredentialStore Credentials +@inject IDialogService DialogService +@inject AdminClient AdminClient + + + + + + Authentication + + + + + + + + Cancel + Confirm + + + + +@code +{ + [CascadingParameter] + private MudDialogInstance MudDialog { get; set; } = null!; + + private UserModel User { get; } = new(); + private MudTextField UsernameTextField { get; set; } = null!; + + protected override async void OnAfterRender(bool firstRender) + { + if (firstRender) + { + var details = await Credentials.GetDetailsAsync(); + User.Name = details.Username; + User.Password = details.Password; + + await UsernameTextField.FocusAsync(); + } + } + + private async void Submit() + { + await Credentials.SetDetailsAsync(User.Name, User.Password); + + var valid = false; + try + { + valid = await AdminClient.Confirm(); + } + catch + { + valid = false; + } + + if (valid) + { + MudDialog.Close(DialogResult.Ok(User.Name)); + } + else + { + await DialogService.ShowMessageBox("Invalid Login Attempt", "The credentials provided are not valid"); + } + } + + private void Cancel() + { + MudDialog.Cancel(); + } + + private class UserModel + { + [Required] + public string Name { get; set; } = string.Empty; + + [Required] + public string Password { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Website/Components/Dialogs/EditPlayDialog.razor b/Website/Components/Dialogs/EditPlayDialog.razor new file mode 100644 index 0000000..c05e27f --- /dev/null +++ b/Website/Components/Dialogs/EditPlayDialog.razor @@ -0,0 +1,342 @@ +@using Hesketh.MecatolArchives.API.Client.Clients +@using Hesketh.MecatolArchives.API.Data.Post +@using Hesketh.MecatolArchives.Website.Helpers +@using Variant = Hesketh.MecatolArchives.API.Data.Variant +@using Expansion = Hesketh.MecatolArchives.API.Data.Expansion +@using Faction = Hesketh.MecatolArchives.API.Data.Faction +@using Person = Hesketh.MecatolArchives.API.Data.Person +@using Play = Hesketh.MecatolArchives.API.Data.Play +@using Colour = Hesketh.MecatolArchives.API.Data.Colour +@inject PlayClient PlayClient +@inject VariantClient VariantClient +@inject ColourClient ColourClient +@inject FactionClient FactionClient +@inject PersonClient PersonClient +@inject ExpansionClient ExpansionClient +@inject IDialogService DialogService + + + + + + Play + + + + + + + + + + + + + + + + + + + @foreach (var variant in Variants) + { + @variant.Name + } + + + + + + @foreach (var expansion in Expansions) + { + @expansion.Name + } + + + + + + + + + + + Players + + + + + Player + Colour + Faction + Eliminated + Points + Winner + + + + + + @foreach (var person in People) + { + @person.Name + } + + + + + @foreach (var colour in Colours) + { + @colour.Name + } + + + + + @foreach (var faction in Factions) + { + +
+ + @faction.Name +
+
+ } +
+
+ + + + + + + + + + + + +
+
+
+
+
+ + Cancel + Confirm + +
+
+ +@code +{ + [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = null!; + [Parameter] public Play? ExistingPlay { get; set; } + + private PlayModel _play = new(); + + private ICollection Variants { get; set; } = new List(); + private ICollection Expansions { get; set; } = new List(); + private ICollection People { get; set; } = new List(); + private ICollection Factions { get; set; } = new List(); + private ICollection Colours { get; set; } = new List(); + + private void SetPlayModelFromTransfer(Play play) + { + _play.Identifier = play.Identifier; + _play.PointGoal = play.PointGoal; + _play.Map = play.Map; + _play.RulesVersion = play.RulesVersion; + _play.UtcDate = play.UtcDate; + _play.Variants = new List(Variants.Where(x => play.Variants.Any(y => y.Identifier == x.Identifier))); + _play.Expansions = new List(Expansions.Where(x => play.Expansions.Any(y => y.Identifier == x.Identifier))); + _play.Players = new List(); + + foreach (var player in play.Players) + { + _play.Players.Add(new PlayerModel + { + Colour = player.Colour, + Eliminated = player.Eliminated, + Faction = player.Faction, + Person = player.Person, + Points = player.Points, + Winner = player.Winner + }); + } + + StateHasChanged(); + } + + protected override async Task OnInitializedAsync() + { + Variants = new List(await VariantClient.GetAsync()); + Expansions = new List(await ExpansionClient.GetAsync()); + Factions = new List(await FactionClient.GetAsync()); + People = new List(await PersonClient.GetAsync()); + Colours = new List(await ColourClient.GetAsync()); + } + + protected override void OnParametersSet() + { + _play = new(); + if (ExistingPlay != null) + { + SetPlayModelFromTransfer(ExistingPlay); + } + } + + private async void Submit() + { + if (!_play.IsValid()) + { + await DialogService.ShowMessageBox("Play not Valid!", "The Play or Players do not contain valid data"); + return; + } + + object? created = null; + + if (_play.Identifier.HasValue) + { + created = await PlayClient.UpdateAsync(new API.Data.Put.Play + { + Identifier = _play.Identifier.Value, + ExpansionIdentifiers = _play.Expansions.Select(x => x.Identifier).ToList(), + Map = _play.Map, + Players = _play.Players.Select(x => new Player + { + ColourIdentifier = x.Colour!.Identifier, + Eliminated = x.Eliminated, + FactionIdentifier = x.Faction!.Identifier, + PersonIdentifier = x.Person!.Identifier, + Points = x.Points, + Winner = x.Winner + }).ToList(), + PointGoal = _play.PointGoal, + RulesVersion = _play.RulesVersion, + UtcDate = _play.UtcDate!.Value, + VariantIdentifiers = _play.Variants.Select(x => x.Identifier).ToList() + }); + } + else + { + created = await PlayClient.CreateAsync(new API.Data.Post.Play + { + ExpansionIdentifiers = _play.Expansions.Select(x => x.Identifier).ToList(), + Map = _play.Map, + Players = _play.Players.Select(x => new Player + { + ColourIdentifier = x.Colour!.Identifier, + Eliminated = x.Eliminated, + FactionIdentifier = x.Faction!.Identifier, + PersonIdentifier = x.Person!.Identifier, + Points = x.Points, + Winner = x.Winner + }).ToList(), + PointGoal = _play.PointGoal, + RulesVersion = _play.RulesVersion, + UtcDate = _play.UtcDate!.Value, + VariantIdentifiers = _play.Variants.Select(x => x.Identifier).ToList() + }); + } + + MudDialog.Close(DialogResult.Ok(created)); + } + + private void Cancel() + { + MudDialog.Cancel(); + } + + private async void DeletePlayer(PlayerModel player) + { + var delete = await DialogService.ShowMessageBox("DeletePlayer", $"Are you sure you wish to remove this row?", "Yes", "No"); + if (delete != true) + return; + + _play.Players.Remove(player); + + StateHasChanged(); + } + + private void CreateNewPlayer() + { + _play.Players.Add(new PlayerModel + { + Colour = Colours.FirstOrDefault(), + Faction = Factions.FirstOrDefault(), + Person = People.FirstOrDefault(x => _play.Players.All(y => x.Identifier != y.Person?.Identifier)) + }); + StateHasChanged(); + } + + private string ToStringFunc(Variant? arg) + { + return arg?.Name ?? string.Empty; + } + + private string ToStringFunc(Expansion? arg) + { + return arg?.Name ?? string.Empty; + } + + private string ToStringFunc(Faction? arg) + { + return arg?.Name ?? string.Empty; + } + + private string ToStringFunc(Colour? arg) + { + return arg?.Name ?? string.Empty; + } + + private string ToStringFunc(Person? arg) + { + return arg?.Name ?? string.Empty; + } + + private sealed class PlayModel + { + public Guid? Identifier { get; set; } // Null if new entity + public DateTime? UtcDate { get; set; } = DateTime.UtcNow; + public double RulesVersion { get; set; } = 1.0; + public uint PointGoal { get; set; } = 10; + public string? Map { get; set; } + + public IEnumerable Variants { get; set; } = new HashSet(); + public IEnumerable Expansions { get; set; } = new HashSet(); + public ICollection Players { get; set; } = new HashSet(); + + public bool IsValid() + { + foreach (var player in Players) + { + if (!player.IsValid()) + return false; + } + + return true; + } + } + + private sealed class PlayerModel + { + public uint Points { get; set; } + public bool Winner { get; set; } + public bool Eliminated { get; set; } + + public Person? Person { get; set; } + public Faction? Faction { get; set; } + public Colour? Colour { get; set; } + + public bool IsValid() + { + return Person != null && Faction != null && Colour != null; + } + } +} \ No newline at end of file diff --git a/Website/Components/Layout/MainLayout.razor b/Website/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..1cabddf --- /dev/null +++ b/Website/Components/Layout/MainLayout.razor @@ -0,0 +1,136 @@ +@using Hesketh.MecatolArchives.Website.Services +@using System.Runtime.CompilerServices +@using Hesketh.MecatolArchives.API.Client.Auth +@using Hesketh.MecatolArchives.Website.Components.Dialogs +@inherits LayoutComponentBase +@inject IPreferenceStore Preferences +@inject IDialogService DialogService +@inject IAdminCredentialStore Admin +@inject NavigationManager NavigationManager + + + +@* Nebula Background *@ + +
+
+
+ + + + +Mecatol Archives + + + + + MECATOL ARCHIVES + + + @if (Authenticated) + { + + } + else + { + + } + + + + + MECATOL ARCHIVES + + + + + + + @Body + + + + + +@code { + private bool _drawerOpen; + private bool _darkMode = true; + public bool Authenticated { get; set; } = false; + + public bool DrawerOpen + { + get => _drawerOpen; + set + { + _drawerOpen = value; + UpdateBoolPreference(value); + } + } + + public bool DarkMode + { + get => _darkMode; + set + { + _darkMode = value; + UpdateBoolPreference(value); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + DrawerOpen = await Preferences.GetPreferenceAsync(nameof(DrawerOpen), DrawerOpen); + DarkMode = await Preferences.GetPreferenceAsync(nameof(DarkMode), DarkMode); + Authenticated = await Admin.AreDetailsSet(); + + StateHasChanged(); + } + } + + private async void UpdateBoolPreference(bool value, [CallerMemberName] string propertyName = "") + { + await Preferences.SetPreferenceAsync(propertyName, value); + } + + private async void OnAuthenticateClicked() + { + var dialog = await DialogService.ShowAsync(); + var result = await dialog.Result; + if (result.Canceled) + return; + + Authenticated = await Admin.AreDetailsSet(); + StateHasChanged(); + } + + private async void OnLogoutClicked() + { + var dialog = await DialogService.ShowMessageBox("Logout", "Are you sure you want to logout?", "Yes", "No"); + if (dialog != true) + return; + + await Admin.ResetAsync(); + Authenticated = false; + + StateHasChanged(); + } +} \ No newline at end of file diff --git a/Website/Components/Layout/NavMenu.razor b/Website/Components/Layout/NavMenu.razor new file mode 100644 index 0000000..0f1b156 --- /dev/null +++ b/Website/Components/Layout/NavMenu.razor @@ -0,0 +1,13 @@ + + Home + Games + Players + Factions + + Colours + Expansions + Factions + Variants + People + + diff --git a/Website/Components/MudProviders.razor b/Website/Components/MudProviders.razor new file mode 100644 index 0000000..9864585 --- /dev/null +++ b/Website/Components/MudProviders.razor @@ -0,0 +1,17 @@ +@inject ISnackbar Snackbar + + + + + +@code +{ + protected override void OnInitialized() + { + Snackbar.Clear(); + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopCenter; + Snackbar.Configuration.MaxDisplayedSnackbars = 5; + Snackbar.Configuration.SnackbarVariant = Variant.Filled; + Snackbar.Configuration.PreventDuplicates = true; + } +} \ No newline at end of file diff --git a/Website/Components/Pages/Data/ColoursPage.razor b/Website/Components/Pages/Data/ColoursPage.razor new file mode 100644 index 0000000..cde2a59 --- /dev/null +++ b/Website/Components/Pages/Data/ColoursPage.razor @@ -0,0 +1,102 @@ +@page "/data/colours" +@using Hesketh.MecatolArchives.API.Client.Clients +@using Hesketh.MecatolArchives.API.Data +@using Hesketh.MecatolArchives.API.Client.Auth +@using Hesketh.MecatolArchives.Website.Helpers +@inject ColourClient ColourClient; +@inject IDialogService DialogService + +Colours + + + + + + @if (!_dataGrid.ReadOnly) + { + + } + + + COLOURS + + @if (!_dataGrid.ReadOnly) + { + + } + + + + + @context.Item.Name + + + + + + + + + + +@code +{ + private MudDataGrid _dataGrid = null!; + private List _colours = new(); + + [CascadingParameter] public bool Authenticated { get; set; } = false; + + protected override async Task OnParametersSetAsync() + { + _colours = new List(await ColourClient.GetAsync()); + } + + private async void Delete(Colour colour) + { + var delete = await DialogService.ShowMessageBox("Delete", $"Are you sure you wish to delete {colour.Name}?", "Yes", "No"); + if (delete != true) + return; + + await ColourClient.DeleteAsync(colour.Identifier); + _colours.Remove(colour); + + StateHasChanged(); + } + + private async void CreateNew() + { + await _dataGrid.SetEditingItemAsync(new Colour()); + StateHasChanged(); + } + + private async void CommitItemChanges(Colour colour) + { + if (colour.Identifier != Guid.Empty) + { + await ColourClient.UpdateAsync(colour); + } + else + { + var result = await ColourClient.CreateAsync(new API.Data.Post.Colour + { + Hex = colour.Hex, + Name = colour.Name + }); + colour.Identifier = result.Identifier; + + _colours.Add(colour); + } + + StateHasChanged(); + } +} diff --git a/Website/Components/Pages/Data/ExpansionPage.razor b/Website/Components/Pages/Data/ExpansionPage.razor new file mode 100644 index 0000000..4f77d7e --- /dev/null +++ b/Website/Components/Pages/Data/ExpansionPage.razor @@ -0,0 +1,89 @@ +@page "/data/expansions" +@using Hesketh.MecatolArchives.API.Client.Clients +@using Hesketh.MecatolArchives.API.Data +@using Hesketh.MecatolArchives.API.Client.Auth +@inject ExpansionClient ExpansionClient; +@inject IDialogService DialogService + +Expansions + + + + + @if (!_dataGrid.ReadOnly) + { + + } + + + EXPANSIONS + + @if (!_dataGrid.ReadOnly) + { + + } + + + + + + + +@code +{ + private MudDataGrid _dataGrid = null!; + private List _expansions = new(); + [CascadingParameter] public bool Authenticated { get; set; } = false; + + protected override async Task OnParametersSetAsync() + { + _expansions = new List(await ExpansionClient.GetAsync()); + } + + private async void Delete(Expansion expansion) + { + var delete = await DialogService.ShowMessageBox("Delete", $"Are you sure you wish to delete {expansion.Name}?", "Yes", "No"); + if (delete != true) + return; + + await ExpansionClient.DeleteAsync(expansion.Identifier); + _expansions.Remove(expansion); + + StateHasChanged(); + } + + private async void CreateNew() + { + await _dataGrid.SetEditingItemAsync(new Expansion()); + StateHasChanged(); + } + + private async void CommitItemChanges(Expansion expansion) + { + if (expansion.Identifier != Guid.Empty) + { + await ExpansionClient.UpdateAsync(expansion); + } + else + { + var result = await ExpansionClient.CreateAsync(new API.Data.Post.Expansion + { + Name = expansion.Name + }); + expansion.Identifier = result.Identifier; + _expansions.Add(expansion); + } + + StateHasChanged(); + } +} \ No newline at end of file diff --git a/Website/Components/Pages/Data/FactionsPage.razor b/Website/Components/Pages/Data/FactionsPage.razor new file mode 100644 index 0000000..f33a4d0 --- /dev/null +++ b/Website/Components/Pages/Data/FactionsPage.razor @@ -0,0 +1,106 @@ +@page "/data/factions" +@using Hesketh.MecatolArchives.API.Client.Clients +@using Hesketh.MecatolArchives.API.Data +@using Hesketh.MecatolArchives.API.Client.Auth +@using Hesketh.MecatolArchives.Website.Helpers +@inject FactionClient FactionClient +@inject IDialogService DialogService + +Factions + + + + @if (_dataGrid.ReadOnly) + { + + } + + @if (!_dataGrid.ReadOnly) + { + + } + + + FACTIONS + + @if (!_dataGrid.ReadOnly) + { + + } + + + + +
+ + + @context.Item.Name + +
+
+ + + +
+ +
+
+ +@code +{ + private MudDataGrid _dataGrid = null!; + private List _factions = new(); + [CascadingParameter] public bool Authenticated { get; set; } = false; + + protected override async Task OnParametersSetAsync() + { + _factions = new List(await FactionClient.GetAsync()); + } + + private async void Delete(Faction faction) + { + var delete = await DialogService.ShowMessageBox("Delete", $"Are you sure you wish to delete {faction.Name}?", "Yes", "No"); + if (delete != true) + return; + + await FactionClient.DeleteAsync(faction.Identifier); + _factions.Remove(faction); + + StateHasChanged(); + } + + private async void CreateNew() + { + await _dataGrid.SetEditingItemAsync(new Faction()); + StateHasChanged(); + } + + private async void CommitItemChanges(Faction faction) + { + if (faction.Identifier != Guid.Empty) + { + await FactionClient.UpdateAsync(faction); + } + else + { + var result = await FactionClient.CreateAsync(new API.Data.Post.Faction + { + Name = faction.Name + }); + faction.Identifier = result.Identifier; + _factions.Add(faction); + } + + StateHasChanged(); + } +} \ No newline at end of file diff --git a/Website/Components/Pages/Data/PeoplePage.razor b/Website/Components/Pages/Data/PeoplePage.razor new file mode 100644 index 0000000..0e7b947 --- /dev/null +++ b/Website/Components/Pages/Data/PeoplePage.razor @@ -0,0 +1,89 @@ +@page "/data/people" +@using Hesketh.MecatolArchives.API.Client.Clients +@using Hesketh.MecatolArchives.API.Data +@using Hesketh.MecatolArchives.API.Client.Auth +@inject PersonClient PersonClient; +@inject IDialogService DialogService + +People + + + + + @if (!_dataGrid.ReadOnly) + { + + } + + + PEOPLE + + @if (!_dataGrid.ReadOnly) + { + + } + + + + + + + +@code +{ + private MudDataGrid _dataGrid = null!; + private List _people = new(); + [CascadingParameter] public bool Authenticated { get; set; } = false; + + protected override async Task OnParametersSetAsync() + { + _people = new List(await PersonClient.GetAsync()); + } + + private async void Delete(Person person) + { + var delete = await DialogService.ShowMessageBox("Delete", $"Are you sure you wish to delete {person.Name}?", "Yes", "No"); + if (delete != true) + return; + + await PersonClient.DeleteAsync(person.Identifier); + _people.Remove(person); + + StateHasChanged(); + } + + private async void CreateNew() + { + await _dataGrid.SetEditingItemAsync(new Person()); + StateHasChanged(); + } + + private async void CommitItemChanges(Person person) + { + if (person.Identifier != Guid.Empty) + { + await PersonClient.UpdateAsync(person); + } + else + { + var result = await PersonClient.CreateAsync(new API.Data.Post.Person + { + Name = person.Name + }); + person.Identifier = result.Identifier; + _people.Add(person); + } + + StateHasChanged(); + } +} \ No newline at end of file diff --git a/Website/Components/Pages/Data/VariantsPage.razor b/Website/Components/Pages/Data/VariantsPage.razor new file mode 100644 index 0000000..aa966c2 --- /dev/null +++ b/Website/Components/Pages/Data/VariantsPage.razor @@ -0,0 +1,89 @@ +@page "/data/variants" +@using Hesketh.MecatolArchives.API.Client.Clients +@using Hesketh.MecatolArchives.API.Client.Auth +@using Variant = Hesketh.MecatolArchives.API.Data.Variant +@inject VariantClient VariantClient; +@inject IDialogService DialogService + +Variants + + + + + @if (!_dataGrid.ReadOnly) + { + + } + + + VARIANTS + + @if (!_dataGrid.ReadOnly) + { + + } + + + + + + + +@code +{ + private MudDataGrid _dataGrid = null!; + private List _variants = new(); + [CascadingParameter] public bool Authenticated { get; set; } = false; + + protected override async Task OnParametersSetAsync() + { + _variants = new List(await VariantClient.GetAsync()); + } + + private async void Delete(Variant variant) + { + var delete = await DialogService.ShowMessageBox("Delete", $"Are you sure you wish to delete {variant.Name}?", "Yes", "No"); + if (delete != true) + return; + + await VariantClient.DeleteAsync(variant.Identifier); + _variants.Remove(variant); + + StateHasChanged(); + } + + private async void CreateNew() + { + await _dataGrid.SetEditingItemAsync(new Variant()); + StateHasChanged(); + } + + private async void CommitItemChanges(Variant variant) + { + if (variant.Identifier != Guid.Empty) + { + await VariantClient.UpdateAsync(variant); + } + else + { + var result = await VariantClient.CreateAsync(new API.Data.Post.Variant + { + Name = variant.Name + }); + variant.Identifier = result.Identifier; + _variants.Add(variant); + } + + StateHasChanged(); + } +} \ No newline at end of file diff --git a/Website/Components/Pages/ErrorPage.razor b/Website/Components/Pages/ErrorPage.razor new file mode 100644 index 0000000..abd2871 --- /dev/null +++ b/Website/Components/Pages/ErrorPage.razor @@ -0,0 +1,40 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() + { + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; + } + +} \ No newline at end of file diff --git a/Website/Components/Pages/Games/HistoryPage.razor b/Website/Components/Pages/Games/HistoryPage.razor new file mode 100644 index 0000000..8527855 --- /dev/null +++ b/Website/Components/Pages/Games/HistoryPage.razor @@ -0,0 +1,82 @@ +@page "/games/history" +@using Hesketh.MecatolArchives.API.Client.Clients +@using Hesketh.MecatolArchives.API.Data +@using Hesketh.MecatolArchives.API.Client.Auth +@using Hesketh.MecatolArchives.Website.Components.Dialogs +@using Hesketh.MecatolArchives.Website.Components.Controls +@inject PlayClient PlayClient +@inject IDialogService DialogService + +Games + + + + Games + + @if (Authenticated) + { + + } + + + + + + +@code +{ + [CascadingParameter] public bool Authenticated { get; set; } = false; + private List _plays = new(); + + protected override async Task OnParametersSetAsync() + { + await RefreshPlays(); + } + + private async Task RefreshPlays() + { + _plays = new List(await PlayClient.GetAsync()); + StateHasChanged(); + } + + private async void CreateGameButtonClicked() + { + var res = await DialogService.ShowAsync("New Play", new DialogOptions + { + FullScreen = true, + CloseButton = true + }); + await res.Result; + + await RefreshPlays(); + } + + private async void DeleteGameButtonClicked(Play play) + { + var result = await DialogService.ShowMessageBox("Delete Game?", + $"Are you sure you want to delete the game from {play.UtcDate.ToLocalTime().ToLongDateString()}?", + "Delete", + "Cancel"); + + if (result == true) + { + await PlayClient.DeleteAsync(play.Identifier); + await RefreshPlays(); + } + } + + private async void EditGameButtonClicked(Play play) + { + var parameters = new DialogParameters { { x => x.ExistingPlay, play } }; + var res = await DialogService.ShowAsync("Edit Play", parameters, new DialogOptions + { + FullScreen = true, + CloseButton = true, + }); + await res.Result; + + await RefreshPlays(); + } +} diff --git a/Website/Components/Pages/HomePage.razor b/Website/Components/Pages/HomePage.razor new file mode 100644 index 0000000..d268e36 --- /dev/null +++ b/Website/Components/Pages/HomePage.razor @@ -0,0 +1,12 @@ +@page "/" +@inject NavigationManager NavigationManager + +@code +{ + protected override void OnAfterRender(bool firstRender) + { + // The homepage is just a placeholder until we actually have something to display on it + // i.e. most recent games, current champion, stuff like that. + NavigationManager.NavigateTo("games/history"); + } +} diff --git a/Website/Components/Pages/Stats/FactionPage.razor b/Website/Components/Pages/Stats/FactionPage.razor new file mode 100644 index 0000000..11e1e76 --- /dev/null +++ b/Website/Components/Pages/Stats/FactionPage.razor @@ -0,0 +1,63 @@ +@page "/stats/factions/{FactionIdentifier:guid}" +@using Hesketh.MecatolArchives.API.Data +@using Hesketh.MecatolArchives.API.Client.Clients +@using Hesketh.MecatolArchives.Website.Components.Controls; +@using Hesketh.MecatolArchives.Website.Helpers +@inject StatisticClient StatisticClient +@inject FactionClient FactionClient +@inject PlayClient PlayClient; + +@_title + + + +
+ + @_title +
+
+ + + +
+ + + + Players + + + + + + + + + Play History + + + + + + +@code +{ + [Parameter] public Guid FactionIdentifier { get; set; } + private Statistics _statistics = new(); + private ICollection _playHistory = new List(); + private string _title = "Faction"; + + protected override async Task OnParametersSetAsync() + { + _statistics = await StatisticClient.GetFactionPeopleStatistics(FactionIdentifier); + _playHistory = await PlayClient.GetFactionsPlaysAsync(FactionIdentifier); + + var faction = await FactionClient.GetAsync(FactionIdentifier); + _title = faction.Name; + + StateHasChanged(); + } +} \ No newline at end of file diff --git a/Website/Components/Pages/Stats/FactionsPage.razor b/Website/Components/Pages/Stats/FactionsPage.razor new file mode 100644 index 0000000..94fde56 --- /dev/null +++ b/Website/Components/Pages/Stats/FactionsPage.razor @@ -0,0 +1,39 @@ +@page "/stats/factions" +@using Hesketh.MecatolArchives.API.Data +@using Hesketh.MecatolArchives.API.Client.Clients +@using Hesketh.MecatolArchives.Website.Components.Controls; +@using Hesketh.MecatolArchives.Website.Helpers +@inject StatisticClient StatisticClient + +Factions + + + + Factions + + + + + + +@code +{ + private Statistics _statistics = new(); + + protected override async Task OnParametersSetAsync() + { + _statistics = await StatisticClient.GetFactionStatistics(); + StateHasChanged(); + } + + private RenderFragment? GetFactionIcon(object o) + { + if (o is Statistic statistic) + { + return @; + } + return null; + } +} diff --git a/Website/Components/Pages/Stats/PeoplePage.razor b/Website/Components/Pages/Stats/PeoplePage.razor new file mode 100644 index 0000000..c2014ca --- /dev/null +++ b/Website/Components/Pages/Stats/PeoplePage.razor @@ -0,0 +1,27 @@ +@page "/stats/people" +@using Hesketh.MecatolArchives.API.Data +@using Hesketh.MecatolArchives.API.Client.Clients +@using Hesketh.MecatolArchives.Website.Components.Controls; +@inject StatisticClient StatisticClient + +People + + + + People + + + + + + +@code +{ + private Statistics _statistics = new(); + + protected override async Task OnParametersSetAsync() + { + _statistics = await StatisticClient.GetPeopleStatistics(); + StateHasChanged(); + } +} diff --git a/Website/Components/Pages/Stats/PersonPage.razor b/Website/Components/Pages/Stats/PersonPage.razor new file mode 100644 index 0000000..2dff1e9 --- /dev/null +++ b/Website/Components/Pages/Stats/PersonPage.razor @@ -0,0 +1,67 @@ +@page "/stats/people/{PersonIdentifier:guid}" +@using Hesketh.MecatolArchives.API.Data +@using Hesketh.MecatolArchives.API.Client.Clients +@using Hesketh.MecatolArchives.Website.Components.Controls; +@using Hesketh.MecatolArchives.Website.Helpers +@inject StatisticClient StatisticClient +@inject PersonClient PersonClient +@inject PlayClient PlayClient; + +@_title + + + + @_title + + + + + + + + + Factions + + + + + + + + + Play History + + + + + + +@code +{ + [Parameter] public Guid PersonIdentifier { get; set; } + private Statistics _statistics = new(); + private ICollection _playHistory = new List(); + private string _title = "Player"; + + protected override async Task OnParametersSetAsync() + { + _statistics = await StatisticClient.GetPersonFactionStatistics(PersonIdentifier); + _playHistory = await PlayClient.GetPersonsPlaysAsync(PersonIdentifier); + + var person = await PersonClient.GetAsync(PersonIdentifier); + _title = person.Name; + + StateHasChanged(); + } + + private RenderFragment? GetFactionIcon(object o) + { + if (o is Statistic statistic) + { + return @; + } + return null; + } +} \ No newline at end of file diff --git a/Website/Components/Routes.razor b/Website/Components/Routes.razor new file mode 100644 index 0000000..1bc1b3b --- /dev/null +++ b/Website/Components/Routes.razor @@ -0,0 +1,7 @@ +@using Hesketh.MecatolArchives.Website.Components.Layout + + + + + + \ No newline at end of file diff --git a/Website/Components/ThemeProvider.razor b/Website/Components/ThemeProvider.razor new file mode 100644 index 0000000..20a061c --- /dev/null +++ b/Website/Components/ThemeProvider.razor @@ -0,0 +1,58 @@ + + + + +@code +{ + [Parameter] + public bool DarkMode { get; set; } = false; + + public static MudTheme Default => new() + { + Typography = new Typography + { + Default = new Default + { + FontFamily = new[] { "MyriadPro" }, + }, + Body1 = new Body1 + { + FontFamily = new[] { "MyriadPro" }, + }, + Body2 = new Body2 + { + FontFamily = new[] { "MyriadPro" }, + }, + H1 = new H1 + { + FontFamily = new[] { "AmbroiseFirmin" }, + FontWeight = 700 + }, + H2 = new H2 + { + FontFamily = new[] { "AmbroiseFirmin" }, + FontWeight = 700 + }, + H3 = new H3 + { + FontFamily = new[] { "AmbroiseFirmin" }, + FontWeight = 700 + }, + H4 = new H4 + { + FontFamily = new[] { "AmbroiseFirmin" }, + FontWeight = 700 + }, + H5 = new H5 + { + FontFamily = new[] { "AmbroiseFirmin" }, + FontWeight = 700 + }, + H6 = new H6 + { + FontFamily = new[] { "AmbroiseFirmin" }, + FontWeight = 700 + } + } + }; +} \ No newline at end of file diff --git a/Website/Components/_Imports.razor b/Website/Components/_Imports.razor new file mode 100644 index 0000000..ef0ba18 --- /dev/null +++ b/Website/Components/_Imports.razor @@ -0,0 +1,12 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Hesketh.MecatolArchives.Website +@using Hesketh.MecatolArchives.Website.Components +@using MudBlazor +@using MudBlazor.Services \ No newline at end of file diff --git a/Website/Dockerfile b/Website/Dockerfile new file mode 100644 index 0000000..3118645 --- /dev/null +++ b/Website/Dockerfile @@ -0,0 +1,25 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Website/Website.csproj", "Website/"] +COPY ["API.Client/API.Client.csproj", "API.Client/"] +COPY ["API.Data/API.Data.csproj", "API.Data/"] +RUN dotnet restore "Website/Website.csproj" +COPY . . +WORKDIR "/src/Website" +RUN dotnet build "Website.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "Website.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Website.dll"] diff --git a/Website/Helpers/ColourStyleHelper.cs b/Website/Helpers/ColourStyleHelper.cs new file mode 100644 index 0000000..915acd5 --- /dev/null +++ b/Website/Helpers/ColourStyleHelper.cs @@ -0,0 +1,15 @@ +namespace Hesketh.MecatolArchives.Website.Helpers +{ + public static class ColourStyleHelper + { + public static string GetUnderlineStyle(string hex) + { + return $"text-decoration: underline; text-decoration-color: {hex}; text-underline-offset: 5px;"; + } + + public static string GetBackgroundStyle(string hex) + { + return $"background: {hex};"; + } + } +} diff --git a/Website/Helpers/FactionIconHelper.cs b/Website/Helpers/FactionIconHelper.cs new file mode 100644 index 0000000..531f1c2 --- /dev/null +++ b/Website/Helpers/FactionIconHelper.cs @@ -0,0 +1,66 @@ +namespace Hesketh.MecatolArchives.Website.Helpers +{ + public static class FactionIconHelper + { + public static string GetFactionIcon(string factionName) + { + switch (factionName) + { + case "The Arborec": + return "factions/Arborec.png"; + case "The Argent Flight": + return "factions/Argent.png"; + case "The Ghosts of Creuss": + return "factions/Creuss.png"; + case "The Empyrean": + return "factions/Empyrean.png"; + case "The Emirates of Hacan": + return "factions/Hacan.png"; + case "The Universities of Jol-Nar": + return "factions/Jol Nar.png"; + case "The Council Keleres (The Argent Flight)": + case "The Council Keleres (The Mentak Coalition)": + case "The Council Keleres (The Xxcha Kingdoms)": + return "factions/Keleres.png"; + case "The L1Z1X Mindnet": + return "factions/L1Z1X.png"; + case "The Barony of Letnev": + return "factions/Letnev.png"; + case "The Mahact Gene-Sorcerers": + return "factions/Mahact.png"; + case "The Mentak Coalition": + return "factions/Mentak.png"; + case "The Embers of Muaat": + return "factions/Muaat.png"; + case "The Naalu Collective": + return "factions/Naalu.png"; + case "The Naaz-Rokha Alliance": + return "factions/Naaz-Rokha.png"; + case "The Nekro Virus": + return "factions/Nekro.png"; + case "The Nomad": + return "factions/Nomad.png"; + case "The Clan of Saar": + return "factions/Saar.png"; + case "Sardakk N'orr": + return "factions/Sardakk.png"; + case "The Federation of Sol": + return "factions/Sol.png"; + case "The Titans of UL": + return "factions/Titans.png"; + case "The Vuil'Raith Cabal": + return "factions/Vuil'Raith.png"; + case "The Winnu": + return "factions/Winnu.png"; + case "The Yin Brotherhood": + return "factions/Yin.png"; + case "The Yssaril Tribes": + return "factions/Yssaril.png"; + case "The Xxcha Kingdom": + return "factions/Xxcha.png"; + default: + return "factions/Unknown.png"; + } + } + } +} diff --git a/Website/Program.cs b/Website/Program.cs new file mode 100644 index 0000000..3bf38ae --- /dev/null +++ b/Website/Program.cs @@ -0,0 +1,40 @@ +using Blazored.LocalStorage; +using Hesketh.MecatolArchives.API.Client.Auth; +using Hesketh.MecatolArchives.API.Client.Extensions; +using Hesketh.MecatolArchives.Website.Components; +using Hesketh.MecatolArchives.Website.Services; +using MudBlazor.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + +builder.Services.AddMudServices(); +builder.Services.AddBlazoredLocalStorage(); +builder.Services.AddTransient(); + +var apiUrl = builder.Configuration.GetValue("APIUrl"); +if (string.IsNullOrEmpty(apiUrl)) + throw new InvalidOperationException("No APIUrl was specified"); +builder.Services.AddTransient(); +builder.Services.AddClients(new Uri(apiUrl)); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", true); + app.UseHsts(); +} + +if (app.Configuration.GetValue("Https", false)) + app.UseHttpsRedirection(); + +app.UseStaticFiles(); +app.UseAntiforgery(); + +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); \ No newline at end of file diff --git a/Website/Properties/launchSettings.json b/Website/Properties/launchSettings.json new file mode 100644 index 0000000..8be0c5b --- /dev/null +++ b/Website/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:58388", + "sslPort": 44336 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5020", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7019;http://localhost:5020", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/Website/Services/CookiePreferenceStore.cs b/Website/Services/CookiePreferenceStore.cs new file mode 100644 index 0000000..0413471 --- /dev/null +++ b/Website/Services/CookiePreferenceStore.cs @@ -0,0 +1,25 @@ +using Blazored.LocalStorage; + +namespace Hesketh.MecatolArchives.Website.Services; + +public class CookiePreferenceStore : IPreferenceStore +{ + private readonly ILocalStorageService _localStorageService; + + public CookiePreferenceStore(ILocalStorageService localStorageService) + { + _localStorageService = localStorageService; + } + + public async Task GetPreferenceAsync(string key, T defaultValue) + { + return await _localStorageService.ContainKeyAsync(key).ConfigureAwait(false) + ? await _localStorageService.GetItemAsync(key).ConfigureAwait(false) ?? defaultValue + : defaultValue; + } + + public async Task SetPreferenceAsync(string key, T value) + { + await _localStorageService.SetItemAsync(key, value).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/Website/Services/IPreferenceStore.cs b/Website/Services/IPreferenceStore.cs new file mode 100644 index 0000000..930f68f --- /dev/null +++ b/Website/Services/IPreferenceStore.cs @@ -0,0 +1,7 @@ +namespace Hesketh.MecatolArchives.Website.Services; + +public interface IPreferenceStore +{ + Task GetPreferenceAsync(string key, T defaultValue); + Task SetPreferenceAsync(string key, T value); +} \ No newline at end of file diff --git a/Website/Services/PreferenceCredentialStore.cs b/Website/Services/PreferenceCredentialStore.cs new file mode 100644 index 0000000..b9482cb --- /dev/null +++ b/Website/Services/PreferenceCredentialStore.cs @@ -0,0 +1,45 @@ +using Hesketh.MecatolArchives.API.Client.Auth; + +namespace Hesketh.MecatolArchives.Website.Services; + +public class PreferenceCredentialStore(IPreferenceStore preferenceStore) : IAdminCredentialStore +{ + private const string UsernamePreference = "Username"; + private const string PasswordPreference = "Password"; + + public async Task SetDetailsAsync(string username, string password) + { + await preferenceStore.SetPreferenceAsync(UsernamePreference, username).ConfigureAwait(false); + await preferenceStore.SetPreferenceAsync(PasswordPreference, password).ConfigureAwait(false); + } + + public async Task<(string Username, string Password)> GetDetailsAsync() + { + var username = await preferenceStore.GetPreferenceAsync(UsernamePreference, string.Empty).ConfigureAwait(false); + var password = await preferenceStore.GetPreferenceAsync(PasswordPreference, string.Empty).ConfigureAwait(false); + + return (username, password); + } + + public async Task AreDetailsSet() + { + var details = await GetDetailsAsync(); + return !string.IsNullOrEmpty(details.Password) && !string.IsNullOrEmpty(details.Username); + } + + public async Task ResetAsync() + { + await SetDetailsAsync(string.Empty, string.Empty); + } + + public bool IsSet + { + get + { + var task = Task.Run(() => preferenceStore.GetPreferenceAsync(UsernamePreference, string.Empty)); + task.ConfigureAwait(false); + task.RunSynchronously(); + return !string.IsNullOrEmpty(task.Result); + } + } +} \ No newline at end of file diff --git a/Website/Website.csproj b/Website/Website.csproj new file mode 100644 index 0000000..1c2491d --- /dev/null +++ b/Website/Website.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + Linux + false + b080f38d-ce08-4f08-98a8-5eec169a092c + + + + + + + + + + + + + + + + + + + .dockerignore + + + + diff --git a/Website/appsettings.Development.json b/Website/appsettings.Development.json new file mode 100644 index 0000000..1b2d3ba --- /dev/null +++ b/Website/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/Website/appsettings.json b/Website/appsettings.json new file mode 100644 index 0000000..98b4dd8 --- /dev/null +++ b/Website/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Https": false, + "AllowedHosts": "*", + "APIUrl": "http://localhost:5012/" +} \ No newline at end of file diff --git a/Website/web.config b/Website/web.config new file mode 100644 index 0000000..8959207 --- /dev/null +++ b/Website/web.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Website/wwwroot/app.css b/Website/wwwroot/app.css new file mode 100644 index 0000000..0e5f3b3 --- /dev/null +++ b/Website/wwwroot/app.css @@ -0,0 +1,85 @@ +/* + This CSS file matches the color scheme from MudBlazor to Bootstrap when utilized for authentication. + The file remains available at all times for demonstration purposes, + but it is exclusively employed in the 'App.razor' component when authentication is enabled. +*/ + +.ti-logo-text { + background: -webkit-linear-gradient(orange, yellow); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-decoration: underline overline; + position: relative; + text-decoration: none; +} + +.ti-logo-text::after { + background: yellow; + content: ""; + position: absolute; + bottom: 0; + left: 0; + height: 1px; + width: 100%; +} + +.ti-logo-text::before { + background: orange; + content: ""; + position: absolute; + top: 0; + left: 0; + height: 1px; + width: 100%; +} + +@font-face { + font-family: MyriadPro; + src: url("Fonts/Myriad Pro/Myriad Pro Regular.ttf"); +} + +@font-face { + font-family: MyriadPro; + src: url("Fonts/Myriad Pro/Myriad Pro Bold.ttf"); + font-weight: bold; +} + +@font-face { + font-family: MyriadPro; + src: url("Fonts/Myriad Pro/Myriad Pro Bold.ttf"); + font-weight: bold; + font-style: italic; +} + +@font-face { + font-family: MyriadPro; + src: url("Fonts/Myriad Pro/Myriad Pro Bold.ttf"); + font-style: italic; +} + +@font-face { + font-family: MyriadPro; + src: url("Fonts/Myriad Pro/Myriad Pro Bold.ttf"); + font-weight: 600; +} + +@font-face { + font-family: AmbroiseFirmin; + src: url("Fonts/Ambroise Firmin Bold/Ambroise Firmin Bold.otf"); + font-weight: bold; +} + +.btn-primary { + text-transform: uppercase; + --bs-btn-bg: var(--mud-palette-primary) !important; + --bs-btn-hover-bg: var(--mud-palette-primary-darken) !important; +} + +.nav-pills { + --bs-nav-pills-link-active-bg: var(--mud-palette-primary) !important; +} + +.nav { + --bs-nav-link-color: var(--mud-palette-primary) !important; + --bs-nav-link-hover-color: var(--mud-palette-primary-darken) !important; +} diff --git a/Website/wwwroot/factions/Arborec.png b/Website/wwwroot/factions/Arborec.png new file mode 100644 index 0000000..ac99b24 Binary files /dev/null and b/Website/wwwroot/factions/Arborec.png differ diff --git a/Website/wwwroot/factions/Argent.png b/Website/wwwroot/factions/Argent.png new file mode 100644 index 0000000..1dc0834 Binary files /dev/null and b/Website/wwwroot/factions/Argent.png differ diff --git a/Website/wwwroot/factions/Creuss.png b/Website/wwwroot/factions/Creuss.png new file mode 100644 index 0000000..9e6940d Binary files /dev/null and b/Website/wwwroot/factions/Creuss.png differ diff --git a/Website/wwwroot/factions/Empyrean.png b/Website/wwwroot/factions/Empyrean.png new file mode 100644 index 0000000..239a1c2 Binary files /dev/null and b/Website/wwwroot/factions/Empyrean.png differ diff --git a/Website/wwwroot/factions/Hacan.png b/Website/wwwroot/factions/Hacan.png new file mode 100644 index 0000000..89d4cbc Binary files /dev/null and b/Website/wwwroot/factions/Hacan.png differ diff --git a/Website/wwwroot/factions/Jol Nar.png b/Website/wwwroot/factions/Jol Nar.png new file mode 100644 index 0000000..ffe5664 Binary files /dev/null and b/Website/wwwroot/factions/Jol Nar.png differ diff --git a/Website/wwwroot/factions/Keleres.png b/Website/wwwroot/factions/Keleres.png new file mode 100644 index 0000000..817934a Binary files /dev/null and b/Website/wwwroot/factions/Keleres.png differ diff --git a/Website/wwwroot/factions/L1Z1X.png b/Website/wwwroot/factions/L1Z1X.png new file mode 100644 index 0000000..9d3a9d0 Binary files /dev/null and b/Website/wwwroot/factions/L1Z1X.png differ diff --git a/Website/wwwroot/factions/Letnev.png b/Website/wwwroot/factions/Letnev.png new file mode 100644 index 0000000..792a54d Binary files /dev/null and b/Website/wwwroot/factions/Letnev.png differ diff --git a/Website/wwwroot/factions/Mahact.png b/Website/wwwroot/factions/Mahact.png new file mode 100644 index 0000000..e3f5f3a Binary files /dev/null and b/Website/wwwroot/factions/Mahact.png differ diff --git a/Website/wwwroot/factions/Mentak.png b/Website/wwwroot/factions/Mentak.png new file mode 100644 index 0000000..d0286da Binary files /dev/null and b/Website/wwwroot/factions/Mentak.png differ diff --git a/Website/wwwroot/factions/Muaat.png b/Website/wwwroot/factions/Muaat.png new file mode 100644 index 0000000..a8c9312 Binary files /dev/null and b/Website/wwwroot/factions/Muaat.png differ diff --git a/Website/wwwroot/factions/Naalu.png b/Website/wwwroot/factions/Naalu.png new file mode 100644 index 0000000..114de53 Binary files /dev/null and b/Website/wwwroot/factions/Naalu.png differ diff --git a/Website/wwwroot/factions/Naaz-Rokha.png b/Website/wwwroot/factions/Naaz-Rokha.png new file mode 100644 index 0000000..bfd30b0 Binary files /dev/null and b/Website/wwwroot/factions/Naaz-Rokha.png differ diff --git a/Website/wwwroot/factions/Nekro.png b/Website/wwwroot/factions/Nekro.png new file mode 100644 index 0000000..a16792d Binary files /dev/null and b/Website/wwwroot/factions/Nekro.png differ diff --git a/Website/wwwroot/factions/Nomad.png b/Website/wwwroot/factions/Nomad.png new file mode 100644 index 0000000..4f029b2 Binary files /dev/null and b/Website/wwwroot/factions/Nomad.png differ diff --git a/Website/wwwroot/factions/Saar.png b/Website/wwwroot/factions/Saar.png new file mode 100644 index 0000000..657af98 Binary files /dev/null and b/Website/wwwroot/factions/Saar.png differ diff --git a/Website/wwwroot/factions/Sardakk.png b/Website/wwwroot/factions/Sardakk.png new file mode 100644 index 0000000..f182e84 Binary files /dev/null and b/Website/wwwroot/factions/Sardakk.png differ diff --git a/Website/wwwroot/factions/Sol.png b/Website/wwwroot/factions/Sol.png new file mode 100644 index 0000000..03d2402 Binary files /dev/null and b/Website/wwwroot/factions/Sol.png differ diff --git a/Website/wwwroot/factions/Titans.png b/Website/wwwroot/factions/Titans.png new file mode 100644 index 0000000..7c95c4c Binary files /dev/null and b/Website/wwwroot/factions/Titans.png differ diff --git a/Website/wwwroot/factions/Unknown.png b/Website/wwwroot/factions/Unknown.png new file mode 100644 index 0000000..652d17e Binary files /dev/null and b/Website/wwwroot/factions/Unknown.png differ diff --git a/Website/wwwroot/factions/Vuil'Raith.png b/Website/wwwroot/factions/Vuil'Raith.png new file mode 100644 index 0000000..92b7ecd Binary files /dev/null and b/Website/wwwroot/factions/Vuil'Raith.png differ diff --git a/Website/wwwroot/factions/Winnu.png b/Website/wwwroot/factions/Winnu.png new file mode 100644 index 0000000..f949ed0 Binary files /dev/null and b/Website/wwwroot/factions/Winnu.png differ diff --git a/Website/wwwroot/factions/Xxcha.png b/Website/wwwroot/factions/Xxcha.png new file mode 100644 index 0000000..57a12b9 Binary files /dev/null and b/Website/wwwroot/factions/Xxcha.png differ diff --git a/Website/wwwroot/factions/Yin.png b/Website/wwwroot/factions/Yin.png new file mode 100644 index 0000000..01cbb6f Binary files /dev/null and b/Website/wwwroot/factions/Yin.png differ diff --git a/Website/wwwroot/factions/Yssaril.png b/Website/wwwroot/factions/Yssaril.png new file mode 100644 index 0000000..862d326 Binary files /dev/null and b/Website/wwwroot/factions/Yssaril.png differ diff --git a/Website/wwwroot/fonts/Ambroise Firmin Bold/Ambroise Firmin Bold.otf b/Website/wwwroot/fonts/Ambroise Firmin Bold/Ambroise Firmin Bold.otf new file mode 100644 index 0000000..a3268b1 Binary files /dev/null and b/Website/wwwroot/fonts/Ambroise Firmin Bold/Ambroise Firmin Bold.otf differ diff --git a/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Bold Italic.ttf b/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Bold Italic.ttf new file mode 100644 index 0000000..79d2aab Binary files /dev/null and b/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Bold Italic.ttf differ diff --git a/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Bold.ttf b/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Bold.ttf new file mode 100644 index 0000000..a4c78b0 Binary files /dev/null and b/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Bold.ttf differ diff --git a/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Italic.ttf b/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Italic.ttf new file mode 100644 index 0000000..700c9de Binary files /dev/null and b/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Italic.ttf differ diff --git a/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Regular.ttf b/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Regular.ttf new file mode 100644 index 0000000..57a953b Binary files /dev/null and b/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Regular.ttf differ diff --git a/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Semibold.ttf b/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Semibold.ttf new file mode 100644 index 0000000..4dd3d97 Binary files /dev/null and b/Website/wwwroot/fonts/Myriad Pro/Myriad Pro Semibold.ttf differ diff --git a/Website/wwwroot/fonts/Source.txt b/Website/wwwroot/fonts/Source.txt new file mode 100644 index 0000000..7a18873 --- /dev/null +++ b/Website/wwwroot/fonts/Source.txt @@ -0,0 +1,9 @@ +These fonts are for: +card names/subtitles/rulebook headings/unit abilities like BOMBARDMENT etc (SLIDER TI4, all caps) +the word ACTION: (Myriad pro bold italic) +card text triggers with a colon, eg “At the start of combat:” (Myriad pro semibold) +strategy card/game card body text and abilities (Myriad pro regular/semibold depending on taste) +flavour text (Myriad pro regular italic) +Box/rules cover art (remember not to say Twilight Imperium to remain in FFG fan content guidelines) (Ambroise Firmin Bold) + +Sourced from the TI4 Homebrew Hub Discord server. Uploaded by cacotopos \ No newline at end of file diff --git a/Website/wwwroot/images/favicon.ico b/Website/wwwroot/images/favicon.ico new file mode 100644 index 0000000..8cbc8df Binary files /dev/null and b/Website/wwwroot/images/favicon.ico differ diff --git a/Website/wwwroot/images/nebula/bg.png b/Website/wwwroot/images/nebula/bg.png new file mode 100644 index 0000000..9a28874 Binary files /dev/null and b/Website/wwwroot/images/nebula/bg.png differ diff --git a/Website/wwwroot/images/nebula/clouds.png b/Website/wwwroot/images/nebula/clouds.png new file mode 100644 index 0000000..498850f Binary files /dev/null and b/Website/wwwroot/images/nebula/clouds.png differ diff --git a/Website/wwwroot/images/nebula/nebula.png b/Website/wwwroot/images/nebula/nebula.png new file mode 100644 index 0000000..c286d0c Binary files /dev/null and b/Website/wwwroot/images/nebula/nebula.png differ diff --git a/Website/wwwroot/images/nebula/stars.png b/Website/wwwroot/images/nebula/stars.png new file mode 100644 index 0000000..100185c Binary files /dev/null and b/Website/wwwroot/images/nebula/stars.png differ diff --git a/Website/wwwroot/nebula.css b/Website/wwwroot/nebula.css new file mode 100644 index 0000000..386390f --- /dev/null +++ b/Website/wwwroot/nebula.css @@ -0,0 +1,97 @@ +/* Stolen from https://codepen.io/matuzyaka/pen/VwZaORe */ + +.o { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.o1 { + background: url(images/nebula/bg.png); + z-index: -10; +} + +.o2 { + background: url(images/nebula/stars.png); + z-index: -9; + animation: 800s scroll infinite linear; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + filter: alpha(opacity=50); + -moz-opacity: 0.5; + -khtml-opacity: 0.5; + opacity: 0.5; +} + +.o3 { + background: url(images/nebula/nebula.png); + z-index: -8; + animation: 800s scroll2 infinite linear; +} + +.o4 { + background: url(images/nebula/clouds.png); + z-index: -8; + animation: 1600s scroll infinite linear; +} + +@-webkit-keyframes scroll { + 100% { + background-position: -3000px 0px; + } +} + +@-moz-keyframes scroll { + 100% { + background-position: -3000px 0px; + } +} + +@-o-keyframes scroll { + 100% { + background-position: -3000px 0px; + } +} + +@-ms-keyframes scroll { + 100% { + background-position: -3000px 0px; + } +} + +@keyframes scroll { + 100% { + background-position: -3000px 0px; + } +} + +@-webkit-keyframes scroll2 { + 100% { + background-position: -3000px 0px; + } +} + +@-moz-keyframes scroll2 { + 100% { + background-position: -3000px 0px; + } +} + +@-o-keyframes scroll2 { + 100% { + background-position: -3000px 0px; + } +} + +@-ms-keyframes scroll2 { + 100% { + background-position: -3000px 0px; + } +} + +@keyframes scroll2 { + 100% { + background-position: 3000px 0px; + } +} \ No newline at end of file