Use EFCore to store preferences (#506)
* Use EFcore to store preferences * Fixed nullabilty warnings
This commit is contained in:
committed by
Pieter-Jan Briers
parent
1856cb079c
commit
c4ea6e53e8
28
Content.Server.Database/Content.Server.Database.csproj
Normal file
28
Content.Server.Database/Content.Server.Database.csproj
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets"/>
|
||||||
|
<PropertyGroup>
|
||||||
|
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
|
||||||
|
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||||
|
<LangVersion>8</LangVersion>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<Platforms>x64</Platforms>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<OutputPath>..\bin\Content.Server.Database\</OutputPath>
|
||||||
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets"/>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Migrations"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
109
Content.Server.Database/Migrations/20200111103836_InitialCreate.Designer.cs
generated
Normal file
109
Content.Server.Database/Migrations/20200111103836_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Content.Server.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace Content.Server.Database.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(PreferencesDbContext))]
|
||||||
|
[Migration("20200111103836_InitialCreate")]
|
||||||
|
partial class InitialCreate
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "3.1.0");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("HumanoidProfileId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Age")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CharacterName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EyeColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("FacialHairColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("FacialHairName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HairColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HairName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("PrefsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Sex")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SkinColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Slot")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SlotName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("HumanoidProfileId");
|
||||||
|
|
||||||
|
b.HasIndex("PrefsId");
|
||||||
|
|
||||||
|
b.ToTable("HumanoidProfile");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Content.Server.Database.Prefs", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("PrefsId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SelectedCharacterSlot")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("PrefsId");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Preferences");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Content.Server.Database.Prefs", null)
|
||||||
|
.WithMany("HumanoidProfiles")
|
||||||
|
.HasForeignKey("PrefsId");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace Content.Server.Database.Migrations
|
||||||
|
{
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
"Preferences",
|
||||||
|
table => new
|
||||||
|
{
|
||||||
|
PrefsId = table.Column<int>()
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Username = table.Column<string>(),
|
||||||
|
SelectedCharacterSlot = table.Column<int>()
|
||||||
|
},
|
||||||
|
constraints: table => { table.PrimaryKey("PK_Preferences", x => x.PrefsId); });
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
"HumanoidProfile",
|
||||||
|
table => new
|
||||||
|
{
|
||||||
|
HumanoidProfileId = table.Column<int>()
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Slot = table.Column<int>(),
|
||||||
|
SlotName = table.Column<string>(),
|
||||||
|
CharacterName = table.Column<string>(),
|
||||||
|
Age = table.Column<int>(),
|
||||||
|
Sex = table.Column<string>(),
|
||||||
|
HairName = table.Column<string>(),
|
||||||
|
HairColor = table.Column<string>(),
|
||||||
|
FacialHairName = table.Column<string>(),
|
||||||
|
FacialHairColor = table.Column<string>(),
|
||||||
|
EyeColor = table.Column<string>(),
|
||||||
|
SkinColor = table.Column<string>(),
|
||||||
|
PrefsId = table.Column<int>(nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_HumanoidProfile", x => x.HumanoidProfileId);
|
||||||
|
table.ForeignKey(
|
||||||
|
"FK_HumanoidProfile_Preferences_PrefsId",
|
||||||
|
x => x.PrefsId,
|
||||||
|
"Preferences",
|
||||||
|
"PrefsId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
"IX_HumanoidProfile_PrefsId",
|
||||||
|
"HumanoidProfile",
|
||||||
|
"PrefsId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
"IX_Preferences_Username",
|
||||||
|
"Preferences",
|
||||||
|
"Username",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
"HumanoidProfile");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
"Preferences");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|
||||||
|
namespace Content.Server.Database.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(PreferencesDbContext))]
|
||||||
|
internal class PreferencesDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "3.1.0");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("HumanoidProfileId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Age")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("CharacterName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EyeColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("FacialHairColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("FacialHairName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HairColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("HairName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("PrefsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Sex")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SkinColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Slot")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SlotName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("HumanoidProfileId");
|
||||||
|
|
||||||
|
b.HasIndex("PrefsId");
|
||||||
|
|
||||||
|
b.ToTable("HumanoidProfile");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Content.Server.Database.Prefs", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("PrefsId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SelectedCharacterSlot")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("PrefsId");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Preferences");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Content.Server.Database.Prefs", null)
|
||||||
|
.WithMany("HumanoidProfiles")
|
||||||
|
.HasForeignKey("PrefsId");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
Content.Server.Database/Model.cs
Normal file
51
Content.Server.Database/Model.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Content.Server.Database
|
||||||
|
{
|
||||||
|
public class PreferencesDbContext : DbContext
|
||||||
|
{
|
||||||
|
// This is used by the "dotnet ef" CLI tool.
|
||||||
|
public PreferencesDbContext() :
|
||||||
|
base(new DbContextOptionsBuilder().UseSqlite("Data Source=:memory:").Options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreferencesDbContext(DbContextOptions<PreferencesDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<Prefs> Preferences { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<Prefs>()
|
||||||
|
.HasIndex(p => p.Username)
|
||||||
|
.IsUnique();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Prefs
|
||||||
|
{
|
||||||
|
public int PrefsId { get; set; }
|
||||||
|
public string Username { get; set; } = null!;
|
||||||
|
public int SelectedCharacterSlot { get; set; }
|
||||||
|
public List<HumanoidProfile> HumanoidProfiles { get; } = new List<HumanoidProfile>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HumanoidProfile
|
||||||
|
{
|
||||||
|
public int HumanoidProfileId { get; set; }
|
||||||
|
public int Slot { get; set; }
|
||||||
|
public string SlotName { get; set; } = null!;
|
||||||
|
public string CharacterName { get; set; } = null!;
|
||||||
|
public int Age { get; set; }
|
||||||
|
public string Sex { get; set; } = null!;
|
||||||
|
public string HairName { get; set; } = null!;
|
||||||
|
public string HairColor { get; set; } = null!;
|
||||||
|
public string FacialHairName { get; set; } = null!;
|
||||||
|
public string FacialHairColor { get; set; } = null!;
|
||||||
|
public string EyeColor { get; set; } = null!;
|
||||||
|
public string SkinColor { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
Content.Server.Database/PrefsDb.cs
Normal file
60
Content.Server.Database/PrefsDb.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Content.Server.Database
|
||||||
|
{
|
||||||
|
public class PrefsDb
|
||||||
|
{
|
||||||
|
private readonly PreferencesDbContext _prefsCtx;
|
||||||
|
|
||||||
|
public PrefsDb(string dbPath)
|
||||||
|
{
|
||||||
|
var optionsBuilder = new DbContextOptionsBuilder<PreferencesDbContext>();
|
||||||
|
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
||||||
|
|
||||||
|
_prefsCtx = new PreferencesDbContext(optionsBuilder.Options);
|
||||||
|
_prefsCtx.Database.Migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Prefs GetPlayerPreferences(string username)
|
||||||
|
{
|
||||||
|
return _prefsCtx.Preferences.SingleOrDefault(p => p.Username == username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveSelectedCharacterIndex(string username, int slot)
|
||||||
|
{
|
||||||
|
var prefs = _prefsCtx.Preferences.SingleOrDefault(p => p.Username == username);
|
||||||
|
if (prefs is null)
|
||||||
|
_prefsCtx.Preferences.Add(new Prefs
|
||||||
|
{
|
||||||
|
Username = username,
|
||||||
|
SelectedCharacterSlot = slot
|
||||||
|
});
|
||||||
|
else
|
||||||
|
prefs.SelectedCharacterSlot = slot;
|
||||||
|
_prefsCtx.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveCharacterSlot(string username, HumanoidProfile newProfile)
|
||||||
|
{
|
||||||
|
var prefs = _prefsCtx
|
||||||
|
.Preferences
|
||||||
|
.Single(p => p.Username == username);
|
||||||
|
var oldProfile = prefs
|
||||||
|
.HumanoidProfiles
|
||||||
|
.SingleOrDefault(h => h.Slot == newProfile.Slot);
|
||||||
|
if (!(oldProfile is null)) prefs.HumanoidProfiles.Remove(oldProfile);
|
||||||
|
prefs.HumanoidProfiles.Add(newProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteCharacterSlot(string username, int slot)
|
||||||
|
{
|
||||||
|
var profile = _prefsCtx
|
||||||
|
.Preferences
|
||||||
|
.Single(p => p.Username == username)
|
||||||
|
.HumanoidProfiles
|
||||||
|
.RemoveAll(h => h.Slot == slot);
|
||||||
|
_prefsCtx.SaveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,12 +12,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
|
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dapper" Version="2.0.30" />
|
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="3.1.0" />
|
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||||
<PackageReference Include="YamlDotNet" Version="6.1.2" />
|
<PackageReference Include="YamlDotNet" Version="6.1.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Content.Server.Database\Content.Server.Database.csproj"/>
|
||||||
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj">
|
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj">
|
||||||
<Private>false</Private>
|
<Private>false</Private>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
@@ -32,7 +31,4 @@
|
|||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
|
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="Preferences\Migrations\000_Initial.sql" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS "HumanoidCharacterProfiles" (
|
|
||||||
"Id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
|
||||||
"Player" INTEGER NOT NULL,
|
|
||||||
"Slot" INTEGER NOT NULL,
|
|
||||||
"Name" TEXT NOT NULL,
|
|
||||||
"Age" INTEGER NOT NULL,
|
|
||||||
"Sex" TEXT NOT NULL,
|
|
||||||
"HairStyleName" TEXT NOT NULL,
|
|
||||||
"HairColor" TEXT NOT NULL,
|
|
||||||
"FacialHairStyleName" TEXT NOT NULL,
|
|
||||||
"FacialHairColor" TEXT NOT NULL,
|
|
||||||
"EyeColor" TEXT NOT NULL,
|
|
||||||
"SkinColor" TEXT NOT NULL,
|
|
||||||
FOREIGN KEY("Player") REFERENCES "PlayerPreferences"("Id")
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS "PlayerPreferences" (
|
|
||||||
"Id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
|
||||||
"Username" TEXT NOT NULL UNIQUE,
|
|
||||||
"SelectedCharacterIndex" INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Data;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Dapper;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using Robust.Shared.Log;
|
|
||||||
|
|
||||||
namespace Content.Server.Preferences.Migrations
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures database schemas are up to date.
|
|
||||||
/// </summary>
|
|
||||||
public static class MigrationManager
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures the database schema for the given connection string is up to date.
|
|
||||||
/// </summary>
|
|
||||||
public static void PerformUpgrade(string connectionString)
|
|
||||||
{
|
|
||||||
using (var connection = new SqliteConnection(connectionString))
|
|
||||||
{
|
|
||||||
EnsureSchemaVersionTableExists(connection);
|
|
||||||
foreach (var migrationToRun in MigrationsToRun(connection))
|
|
||||||
{
|
|
||||||
Logger.InfoS("db", "Running migration {0}", migrationToRun.Id);
|
|
||||||
migrationToRun.Run(connection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generated for each SQL file found.
|
|
||||||
/// </summary>
|
|
||||||
private class Migration
|
|
||||||
{
|
|
||||||
public readonly string Id;
|
|
||||||
private readonly string _sql;
|
|
||||||
|
|
||||||
public Migration(string id, string sql)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
_sql = sql;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Executes the query in <see cref="_sql"/> and logs this in the SchemaVersion table.
|
|
||||||
/// </summary>
|
|
||||||
public void Run(IDbConnection connection)
|
|
||||||
{
|
|
||||||
connection.Execute(_sql);
|
|
||||||
InsertMigrationLog(connection, Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const string InsertMigrationLogQuery =
|
|
||||||
@"INSERT INTO SchemaVersion (Id) VALUES (@Id)";
|
|
||||||
/// <summary>
|
|
||||||
/// Inserts a <see cref="MigrationLog"/> in the SchemaVersion table.
|
|
||||||
/// </summary>
|
|
||||||
private static void InsertMigrationLog(IDbConnection connection, string id)
|
|
||||||
{
|
|
||||||
Logger.InfoS("db", "Completing migration {0}", id);
|
|
||||||
connection.Execute(InsertMigrationLogQuery, new {Id = id});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An entry in the SchemaVersion table.
|
|
||||||
/// </summary>
|
|
||||||
[UsedImplicitly]
|
|
||||||
private class MigrationLog
|
|
||||||
{
|
|
||||||
public string Id;
|
|
||||||
public string Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
private const string GetRanMigrationsQuery =
|
|
||||||
@"SELECT Id, Timestamp FROM SchemaVersion ORDER BY Id COLLATE NOCASE";
|
|
||||||
/// <summary>
|
|
||||||
/// Fetches a collection of <see cref="MigrationLog"/> from the SchemaVersion table and returns it.
|
|
||||||
/// </summary>
|
|
||||||
private static IEnumerable<MigrationLog> RanMigrations(IDbConnection connection)
|
|
||||||
{
|
|
||||||
return connection.Query<MigrationLog>(GetRanMigrationsQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds all available migrations, returns those that haven't been run yet.
|
|
||||||
/// </summary>
|
|
||||||
private static List<Migration> MigrationsToRun(IDbConnection connection)
|
|
||||||
{
|
|
||||||
var discoveredMigrations = DiscoverMigrations(connection);
|
|
||||||
if (discoveredMigrations.Count == 0)
|
|
||||||
{
|
|
||||||
// No migrations found.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ranMigrations = RanMigrations(connection);
|
|
||||||
|
|
||||||
// Filter out migrations that have already been executed
|
|
||||||
discoveredMigrations
|
|
||||||
.RemoveAll(migration => ranMigrations.Any(ranMigration => migration.Id == ranMigration.Id));
|
|
||||||
return discoveredMigrations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Given an embedded resource's full path returns its contents as a string.
|
|
||||||
/// </summary>
|
|
||||||
private static string ResourceAssemblyToString(string resourceName)
|
|
||||||
{
|
|
||||||
using (var stream = Assembly.GetExecutingAssembly()
|
|
||||||
.GetManifestResourceStream(resourceName))
|
|
||||||
using (var reader = new StreamReader(stream))
|
|
||||||
{
|
|
||||||
return reader.ReadToEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Searches the current assembly for SQL migration files.
|
|
||||||
/// TODO: Filter by subfolder so that different databases use different sets of migrations.
|
|
||||||
/// </summary>
|
|
||||||
[NotNull]
|
|
||||||
private static List<Migration> DiscoverMigrations(IDbConnection connection)
|
|
||||||
{
|
|
||||||
var results = new List<Migration>();
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
|
||||||
foreach (var sqlResourceName in assembly
|
|
||||||
.GetManifestResourceNames()
|
|
||||||
.Where(IsValidMigrationFileName))
|
|
||||||
{
|
|
||||||
var splitName = sqlResourceName.Split('.');
|
|
||||||
// The second to last string in the list is the actual file name without the final ".sql"
|
|
||||||
var migrationId = splitName[splitName.Length - 2];
|
|
||||||
var sqlContents = ResourceAssemblyToString(sqlResourceName);
|
|
||||||
results.Add(new Migration(migrationId, sqlContents));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A valid file name is "000_Initial.sql". A dot (from the path, not to be included in the filename itself),
|
|
||||||
/// three digits, a mandatory underscore, any number of characters, a mandatory ".sql".
|
|
||||||
/// </summary>
|
|
||||||
private static bool IsValidMigrationFileName(string name)
|
|
||||||
{
|
|
||||||
return Regex.IsMatch(name, @"\.\d\d\d_[a-zA-Z]+\.sql$");
|
|
||||||
}
|
|
||||||
|
|
||||||
private const string EnsureSchemaVersionTableExistsQuery =
|
|
||||||
@"CREATE TABLE IF NOT EXISTS SchemaVersion (
|
|
||||||
Id TEXT NOT NULL UNIQUE,
|
|
||||||
Timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates the SchemaVersion table if it doesn't exist.
|
|
||||||
/// </summary>
|
|
||||||
private static void EnsureSchemaVersionTableExists(IDbConnection connection)
|
|
||||||
{
|
|
||||||
connection.Execute(EnsureSchemaVersionTableExistsQuery);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +1,63 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Server.Preferences.Migrations;
|
using Content.Server.Database;
|
||||||
using Content.Shared.Preferences;
|
using Content.Shared.Preferences;
|
||||||
using Dapper;
|
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using Robust.Shared.Maths;
|
using Robust.Shared.Maths;
|
||||||
using static Content.Shared.Preferences.Sex;
|
using static Content.Shared.Preferences.Sex;
|
||||||
|
|
||||||
namespace Content.Server.Preferences
|
namespace Content.Server.Preferences
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides methods to retrieve and update character preferences.
|
/// Provides methods to retrieve and update character preferences.
|
||||||
/// Don't use this directly, go through <see cref="ServerPreferencesManager"/> instead.
|
/// Don't use this directly, go through <see cref="ServerPreferencesManager" /> instead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PreferencesDatabase
|
public class PreferencesDatabase
|
||||||
{
|
{
|
||||||
private readonly string _databaseFilePath;
|
|
||||||
private readonly int _maxCharacterSlots;
|
private readonly int _maxCharacterSlots;
|
||||||
|
private readonly PrefsDb _prefsDb;
|
||||||
|
|
||||||
public PreferencesDatabase(string databaseFilePath, int maxCharacterSlots)
|
public PreferencesDatabase(string databaseFilePath, int maxCharacterSlots)
|
||||||
{
|
{
|
||||||
_databaseFilePath = databaseFilePath;
|
|
||||||
_maxCharacterSlots = maxCharacterSlots;
|
_maxCharacterSlots = maxCharacterSlots;
|
||||||
MigrationManager.PerformUpgrade(GetDbConnectionString());
|
_prefsDb = new PrefsDb(databaseFilePath);
|
||||||
}
|
|
||||||
|
|
||||||
private string GetDbConnectionString()
|
|
||||||
{
|
|
||||||
return new SqliteConnectionStringBuilder
|
|
||||||
{
|
|
||||||
DataSource = _databaseFilePath,
|
|
||||||
}.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private SqliteConnection GetDbConnection()
|
|
||||||
{
|
|
||||||
var connectionString = GetDbConnectionString();
|
|
||||||
var conn = new SqliteConnection(connectionString);
|
|
||||||
conn.Open();
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private const string PlayerPreferencesQuery =
|
|
||||||
@"SELECT Id, SelectedCharacterIndex FROM PlayerPreferences WHERE Username=@Username";
|
|
||||||
|
|
||||||
private const string HumanoidCharactersQuery =
|
|
||||||
@"SELECT Slot, Name, Age, Sex, HairStyleName, HairColor, FacialHairStyleName, FacialHairColor, EyeColor, SkinColor
|
|
||||||
FROM HumanoidCharacterProfiles
|
|
||||||
WHERE Player = @Id";
|
|
||||||
|
|
||||||
private sealed class PlayerPreferencesSql
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int SelectedCharacterIndex { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayerPreferences GetPlayerPreferences(string username)
|
public PlayerPreferences GetPlayerPreferences(string username)
|
||||||
{
|
{
|
||||||
using (var connection = GetDbConnection())
|
var prefs = _prefsDb.GetPlayerPreferences(username);
|
||||||
{
|
if (prefs is null) return null;
|
||||||
var prefs = connection.QueryFirstOrDefault<PlayerPreferencesSql>(
|
|
||||||
PlayerPreferencesQuery,
|
|
||||||
new {Username = username});
|
|
||||||
if (prefs is null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using Dapper for ICharacterProfile and ICharacterAppearance is annoying so
|
var profiles = new ICharacterProfile[_maxCharacterSlots];
|
||||||
// we do it manually
|
foreach (var profile in prefs.HumanoidProfiles)
|
||||||
var cmd = new SqliteCommand(HumanoidCharactersQuery, connection);
|
profiles[profile.Slot] = new HumanoidCharacterProfile
|
||||||
cmd.Parameters.AddWithValue("@Id", prefs.Id);
|
|
||||||
cmd.Prepare();
|
|
||||||
|
|
||||||
var reader = cmd.ExecuteReader();
|
|
||||||
var profiles = new ICharacterProfile[_maxCharacterSlots];
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
{
|
||||||
profiles[reader.GetInt32(0)] = new HumanoidCharacterProfile
|
Name = profile.CharacterName,
|
||||||
|
Age = profile.Age,
|
||||||
|
Sex = profile.Sex == "Male" ? Male : Female,
|
||||||
|
CharacterAppearance = new HumanoidCharacterAppearance
|
||||||
{
|
{
|
||||||
Name = reader.GetString(1),
|
HairStyleName = profile.HairName,
|
||||||
Age = reader.GetInt32(2),
|
HairColor = Color.FromHex(profile.HairColor),
|
||||||
Sex = reader.GetString(3) == "Male" ? Male : Female,
|
FacialHairStyleName = profile.FacialHairName,
|
||||||
CharacterAppearance = new HumanoidCharacterAppearance
|
FacialHairColor = Color.FromHex(profile.FacialHairColor),
|
||||||
{
|
EyeColor = Color.FromHex(profile.EyeColor),
|
||||||
HairStyleName = reader.GetString(4),
|
SkinColor = Color.FromHex(profile.SkinColor)
|
||||||
HairColor = Color.FromHex(reader.GetString(5)),
|
}
|
||||||
FacialHairStyleName = reader.GetString(6),
|
|
||||||
FacialHairColor = Color.FromHex(reader.GetString(7)),
|
|
||||||
EyeColor = Color.FromHex(reader.GetString(8)),
|
|
||||||
SkinColor = Color.FromHex(reader.GetString(9))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PlayerPreferences
|
|
||||||
{
|
|
||||||
SelectedCharacterIndex = prefs.SelectedCharacterIndex,
|
|
||||||
Characters = profiles.ToList()
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
return new PlayerPreferences
|
||||||
|
{
|
||||||
|
SelectedCharacterIndex = prefs.SelectedCharacterSlot,
|
||||||
|
Characters = profiles.ToList()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string SaveSelectedCharacterIndexQuery =
|
|
||||||
@"UPDATE PlayerPreferences
|
|
||||||
SET SelectedCharacterIndex = @SelectedCharacterIndex
|
|
||||||
WHERE Username = @Username;
|
|
||||||
|
|
||||||
-- If no update happened (i.e. the row didn't exist) then insert one // https://stackoverflow.com/a/38463024
|
|
||||||
INSERT INTO PlayerPreferences
|
|
||||||
(SelectedCharacterIndex, Username)
|
|
||||||
SELECT
|
|
||||||
@SelectedCharacterIndex,
|
|
||||||
@Username
|
|
||||||
WHERE (SELECT Changes() = 0);";
|
|
||||||
|
|
||||||
public void SaveSelectedCharacterIndex(string username, int index)
|
public void SaveSelectedCharacterIndex(string username, int index)
|
||||||
{
|
{
|
||||||
index = index.Clamp(0, _maxCharacterSlots - 1);
|
index = index.Clamp(0, _maxCharacterSlots - 1);
|
||||||
using (var connection = GetDbConnection())
|
_prefsDb.SaveSelectedCharacterIndex(username, index);
|
||||||
{
|
|
||||||
connection.Execute(SaveSelectedCharacterIndexQuery,
|
|
||||||
new {SelectedCharacterIndex = index, Username = username});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string SaveCharacterSlotQuery =
|
|
||||||
@"UPDATE HumanoidCharacterProfiles
|
|
||||||
SET
|
|
||||||
Name = @Name,
|
|
||||||
Age = @Age,
|
|
||||||
Sex = @Sex,
|
|
||||||
HairStyleName = @HairStyleName,
|
|
||||||
HairColor = @HairColor,
|
|
||||||
FacialHairStyleName = @FacialHairStyleName,
|
|
||||||
FacialHairColor = @FacialHairColor,
|
|
||||||
EyeColor = @EyeColor,
|
|
||||||
SkinColor = @SkinColor
|
|
||||||
WHERE Slot = @Slot AND Player = (SELECT Id FROM PlayerPreferences WHERE Username = @Username);
|
|
||||||
|
|
||||||
-- If no update happened (i.e. the row didn't exist) then insert one // https://stackoverflow.com/a/38463024
|
|
||||||
INSERT INTO HumanoidCharacterProfiles
|
|
||||||
(Slot,
|
|
||||||
Player,
|
|
||||||
Name,
|
|
||||||
Age,
|
|
||||||
Sex,
|
|
||||||
HairStyleName,
|
|
||||||
HairColor,
|
|
||||||
FacialHairStyleName,
|
|
||||||
FacialHairColor,
|
|
||||||
EyeColor,
|
|
||||||
SkinColor)
|
|
||||||
SELECT
|
|
||||||
@Slot,
|
|
||||||
(SELECT Id FROM PlayerPreferences WHERE Username = @Username),
|
|
||||||
@Name,
|
|
||||||
@Age,
|
|
||||||
@Sex,
|
|
||||||
@HairStyleName,
|
|
||||||
@HairColor,
|
|
||||||
@FacialHairStyleName,
|
|
||||||
@FacialHairColor,
|
|
||||||
@EyeColor,
|
|
||||||
@SkinColor
|
|
||||||
WHERE (SELECT Changes() = 0);";
|
|
||||||
|
|
||||||
public void SaveCharacterSlot(string username, ICharacterProfile profile, int slot)
|
public void SaveCharacterSlot(string username, ICharacterProfile profile, int slot)
|
||||||
{
|
{
|
||||||
if (slot < 0 || slot >= _maxCharacterSlots)
|
if (slot < 0 || slot >= _maxCharacterSlots)
|
||||||
@@ -177,44 +69,28 @@ namespace Content.Server.Preferences
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!(profile is HumanoidCharacterProfile humanoid))
|
if (!(profile is HumanoidCharacterProfile humanoid))
|
||||||
{
|
|
||||||
// TODO: Handle other ICharacterProfile implementations properly
|
// TODO: Handle other ICharacterProfile implementations properly
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
|
||||||
|
|
||||||
var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance;
|
var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance;
|
||||||
using (var connection = GetDbConnection())
|
_prefsDb.SaveCharacterSlot(username, new HumanoidProfile
|
||||||
{
|
{
|
||||||
connection.Execute(SaveCharacterSlotQuery, new
|
CharacterName = humanoid.Name,
|
||||||
{
|
Age = humanoid.Age,
|
||||||
Name = humanoid.Name,
|
Sex = humanoid.Sex.ToString(),
|
||||||
Age = humanoid.Age,
|
HairName = appearance.HairStyleName,
|
||||||
Sex = humanoid.Sex.ToString(),
|
HairColor = appearance.HairColor.ToHex(),
|
||||||
HairStyleName = appearance.HairStyleName,
|
FacialHairName = appearance.FacialHairStyleName,
|
||||||
HairColor = appearance.HairColor.ToHex(),
|
FacialHairColor = appearance.FacialHairColor.ToHex(),
|
||||||
FacialHairStyleName = appearance.FacialHairStyleName,
|
EyeColor = appearance.EyeColor.ToHex(),
|
||||||
FacialHairColor = appearance.FacialHairColor.ToHex(),
|
SkinColor = appearance.SkinColor.ToHex(),
|
||||||
EyeColor = appearance.EyeColor.ToHex(),
|
Slot = slot
|
||||||
SkinColor = appearance.SkinColor.ToHex(),
|
});
|
||||||
Slot = slot,
|
|
||||||
Username = username
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string DeleteCharacterSlotQuery =
|
|
||||||
@"DELETE FROM HumanoidCharacterProfiles
|
|
||||||
WHERE
|
|
||||||
Player = (SELECT Id FROM PlayerPreferences WHERE Username = @Username)
|
|
||||||
AND
|
|
||||||
Slot = @Slot";
|
|
||||||
|
|
||||||
private void DeleteCharacterSlot(string username, int slot)
|
private void DeleteCharacterSlot(string username, int slot)
|
||||||
{
|
{
|
||||||
using (var connection = GetDbConnection())
|
_prefsDb.DeleteCharacterSlot(username, slot);
|
||||||
{
|
|
||||||
connection.Execute(DeleteCharacterSlotQuery, new {Username = username, Slot = slot});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Content.Benchmarks", "Conte
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenToolkit.GraphicsLibraryFramework", "RobustToolbox\OpenToolkit.GraphicsLibraryFramework\OpenToolkit.GraphicsLibraryFramework.csproj", "{4809F412-3132-419E-BF9D-CCF7593C3533}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenToolkit.GraphicsLibraryFramework", "RobustToolbox\OpenToolkit.GraphicsLibraryFramework\OpenToolkit.GraphicsLibraryFramework.csproj", "{4809F412-3132-419E-BF9D-CCF7593C3533}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Content.Server.Database", "Content.Server.Database\Content.Server.Database.csproj", "{45C9B43F-305D-4651-9863-F6384CBC847F}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|x64 = Debug|x64
|
Debug|x64 = Debug|x64
|
||||||
@@ -104,6 +106,10 @@ Global
|
|||||||
{4809F412-3132-419E-BF9D-CCF7593C3533}.Debug|x64.Build.0 = Debug|x64
|
{4809F412-3132-419E-BF9D-CCF7593C3533}.Debug|x64.Build.0 = Debug|x64
|
||||||
{4809F412-3132-419E-BF9D-CCF7593C3533}.Release|x64.ActiveCfg = Release|x64
|
{4809F412-3132-419E-BF9D-CCF7593C3533}.Release|x64.ActiveCfg = Release|x64
|
||||||
{4809F412-3132-419E-BF9D-CCF7593C3533}.Release|x64.Build.0 = Release|x64
|
{4809F412-3132-419E-BF9D-CCF7593C3533}.Release|x64.Build.0 = Release|x64
|
||||||
|
{45C9B43F-305D-4651-9863-F6384CBC847F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{45C9B43F-305D-4651-9863-F6384CBC847F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{45C9B43F-305D-4651-9863-F6384CBC847F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{45C9B43F-305D-4651-9863-F6384CBC847F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -21,5 +21,6 @@
|
|||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Lerp/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Lerp/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Noto/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Noto/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=preemptively/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=preemptively/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=prefs/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=soundfonts/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=soundfonts/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=swsl/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=swsl/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||||
|
|||||||
Reference in New Issue
Block a user