Added preferences backend (#465)
* Added preferences backend * Gender -> Sex * ClientPreferencesManager properties * Username validation * Fixed client init * WIP db * Actually working sqlite db * Dropped shitty sqlite libraries, dropped DbUp, added MigrationManager * Added profile deletion, test * Docs, sanity, tests, cleanup * Cleaned up profile and appearance, fixed running on .net core Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
This commit is contained in:
committed by
Pieter-Jan Briers
parent
c1b9bb348d
commit
f19795edaf
@@ -25,6 +25,7 @@ namespace Content.Client
|
|||||||
IoCManager.Register<IEscapeMenuOwner, EscapeMenuOwner>();
|
IoCManager.Register<IEscapeMenuOwner, EscapeMenuOwner>();
|
||||||
IoCManager.Register<ISandboxManager, SandboxManager>();
|
IoCManager.Register<ISandboxManager, SandboxManager>();
|
||||||
IoCManager.Register<IModuleManager, ClientModuleManager>();
|
IoCManager.Register<IModuleManager, ClientModuleManager>();
|
||||||
|
IoCManager.Register<IClientPreferencesManager, ClientPreferencesManager>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
Content.Client/ClientPreferencesManager.cs
Normal file
48
Content.Client/ClientPreferencesManager.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using Content.Client.Interfaces;
|
||||||
|
using Content.Shared.Preferences;
|
||||||
|
using Robust.Shared.Interfaces.Network;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
|
||||||
|
namespace Content.Client
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Receives <see cref="PlayerPreferences"/> and <see cref="GameSettings"/> from the server during the initial connection.
|
||||||
|
/// Stores preferences on the server through <see cref="SelectCharacter"/> and <see cref="UpdateCharacter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class ClientPreferencesManager : SharedPreferencesManager, IClientPreferencesManager
|
||||||
|
{
|
||||||
|
#pragma warning disable 649
|
||||||
|
[Dependency] private readonly IClientNetManager _netManager;
|
||||||
|
#pragma warning restore 649
|
||||||
|
|
||||||
|
public GameSettings Settings { get; private set; }
|
||||||
|
public PlayerPreferences Preferences { get; private set; }
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_netManager.RegisterNetMessage<MsgPreferencesAndSettings>(nameof(MsgPreferencesAndSettings),
|
||||||
|
HandlePreferencesAndSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandlePreferencesAndSettings(MsgPreferencesAndSettings message)
|
||||||
|
{
|
||||||
|
Preferences = message.Preferences;
|
||||||
|
Settings = message.Settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SelectCharacter(int slot)
|
||||||
|
{
|
||||||
|
var msg = _netManager.CreateNetMessage<MsgSelectCharacter>();
|
||||||
|
msg.SelectedCharacterIndex = slot;
|
||||||
|
_netManager.ClientSendMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateCharacter(ICharacterProfile profile, int slot)
|
||||||
|
{
|
||||||
|
var msg = _netManager.CreateNetMessage<MsgUpdateCharacter>();
|
||||||
|
msg.Profile = profile;
|
||||||
|
msg.Slot = slot;
|
||||||
|
_netManager.ClientSendMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -218,6 +218,7 @@ namespace Content.Client
|
|||||||
IoCManager.Resolve<IOverlayManager>().AddOverlay(new ParallaxOverlay());
|
IoCManager.Resolve<IOverlayManager>().AddOverlay(new ParallaxOverlay());
|
||||||
IoCManager.Resolve<IChatManager>().Initialize();
|
IoCManager.Resolve<IChatManager>().Initialize();
|
||||||
IoCManager.Resolve<ISandboxManager>().Initialize();
|
IoCManager.Resolve<ISandboxManager>().Initialize();
|
||||||
|
IoCManager.Resolve<IClientPreferencesManager>().Initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs)
|
public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs)
|
||||||
|
|||||||
13
Content.Client/Interfaces/IClientPreferencesManager.cs
Normal file
13
Content.Client/Interfaces/IClientPreferencesManager.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Content.Shared.Preferences;
|
||||||
|
|
||||||
|
namespace Content.Client.Interfaces
|
||||||
|
{
|
||||||
|
public interface IClientPreferencesManager
|
||||||
|
{
|
||||||
|
void Initialize();
|
||||||
|
GameSettings Settings { get; }
|
||||||
|
PlayerPreferences Preferences { get; }
|
||||||
|
void SelectCharacter(int slot);
|
||||||
|
void UpdateCharacter(ICharacterProfile profile, int slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,9 +8,12 @@
|
|||||||
<Platforms>x64</Platforms>
|
<Platforms>x64</Platforms>
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
<OutputPath>..\bin\Content.Server\</OutputPath>
|
<OutputPath>..\bin\Content.Server\</OutputPath>
|
||||||
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
</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>
|
||||||
@@ -29,4 +32,7 @@
|
|||||||
</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>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using Content.Server.Interfaces;
|
using Content.Server.Interfaces;
|
||||||
using Content.Server.Interfaces.Chat;
|
using Content.Server.Interfaces.Chat;
|
||||||
using Content.Server.Interfaces.GameTicking;
|
using Content.Server.Interfaces.GameTicking;
|
||||||
|
using Content.Server.Preferences;
|
||||||
using Content.Server.Sandbox;
|
using Content.Server.Sandbox;
|
||||||
using Robust.Server.Interfaces.Player;
|
using Robust.Server.Interfaces.Player;
|
||||||
using Robust.Shared.ContentPack;
|
using Robust.Shared.ContentPack;
|
||||||
@@ -74,6 +75,7 @@ namespace Content.Server
|
|||||||
|
|
||||||
_gameTicker.Initialize();
|
_gameTicker.Initialize();
|
||||||
IoCManager.Resolve<ISandboxManager>().Initialize();
|
IoCManager.Resolve<ISandboxManager>().Initialize();
|
||||||
|
IoCManager.Resolve<IServerPreferencesManager>().Initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs)
|
public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using Content.Server.GameObjects;
|
using Content.Server.GameObjects;
|
||||||
using Content.Server.GameObjects.Components.Markers;
|
using Content.Server.GameObjects.Components.Markers;
|
||||||
using Content.Server.GameTicking.GamePresets;
|
using Content.Server.GameTicking.GamePresets;
|
||||||
|
using Content.Server.Interfaces;
|
||||||
using Content.Server.Interfaces.Chat;
|
using Content.Server.Interfaces.Chat;
|
||||||
using Content.Server.Interfaces.GameTicking;
|
using Content.Server.Interfaces.GameTicking;
|
||||||
using Content.Server.Mobs;
|
using Content.Server.Mobs;
|
||||||
@@ -97,6 +98,7 @@ namespace Content.Server.GameTicking
|
|||||||
[Dependency] private IPrototypeManager _prototypeManager;
|
[Dependency] private IPrototypeManager _prototypeManager;
|
||||||
[Dependency] private readonly ILocalizationManager _localization;
|
[Dependency] private readonly ILocalizationManager _localization;
|
||||||
[Dependency] private readonly IRobustRandom _robustRandom;
|
[Dependency] private readonly IRobustRandom _robustRandom;
|
||||||
|
[Dependency] private readonly IServerPreferencesManager _prefsManager;
|
||||||
#pragma warning restore 649
|
#pragma warning restore 649
|
||||||
|
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
@@ -475,6 +477,7 @@ namespace Content.Server.GameTicking
|
|||||||
{
|
{
|
||||||
_playersInLobby.Add(session, false);
|
_playersInLobby.Add(session, false);
|
||||||
|
|
||||||
|
_prefsManager.OnClientConnected(session);
|
||||||
_netManager.ServerSendMessage(_netManager.CreateNetMessage<MsgTickerJoinLobby>(), session.ConnectedClient);
|
_netManager.ServerSendMessage(_netManager.CreateNetMessage<MsgTickerJoinLobby>(), session.ConnectedClient);
|
||||||
_netManager.ServerSendMessage(_getStatusMsg(session), session.ConnectedClient);
|
_netManager.ServerSendMessage(_getStatusMsg(session), session.ConnectedClient);
|
||||||
_netManager.ServerSendMessage(GetInfoMsg(), session.ConnectedClient);
|
_netManager.ServerSendMessage(GetInfoMsg(), session.ConnectedClient);
|
||||||
|
|||||||
13
Content.Server/Interfaces/IServerPreferencesManager.cs
Normal file
13
Content.Server/Interfaces/IServerPreferencesManager.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Content.Shared.Preferences;
|
||||||
|
using Robust.Server.Interfaces.Player;
|
||||||
|
|
||||||
|
namespace Content.Server.Interfaces
|
||||||
|
{
|
||||||
|
public interface IServerPreferencesManager
|
||||||
|
{
|
||||||
|
void Initialize();
|
||||||
|
void OnClientConnected(IPlayerSession session);
|
||||||
|
PlayerPreferences GetPreferences(string username);
|
||||||
|
void SavePreferences(PlayerPreferences prefs, string username);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Content.Server/Preferences/Migrations/000_Initial.sql
Normal file
20
Content.Server/Preferences/Migrations/000_Initial.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
169
Content.Server/Preferences/Migrations/MigrationManager.cs
Normal file
169
Content.Server/Preferences/Migrations/MigrationManager.cs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
220
Content.Server/Preferences/PreferencesDatabase.cs
Normal file
220
Content.Server/Preferences/PreferencesDatabase.cs
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Content.Server.Preferences.Migrations;
|
||||||
|
using Content.Shared.Preferences;
|
||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using static Content.Shared.Preferences.Sex;
|
||||||
|
|
||||||
|
namespace Content.Server.Preferences
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides methods to retrieve and update character preferences.
|
||||||
|
/// Don't use this directly, go through <see cref="ServerPreferencesManager"/> instead.
|
||||||
|
/// </summary>
|
||||||
|
public class PreferencesDatabase
|
||||||
|
{
|
||||||
|
private readonly string _databaseFilePath;
|
||||||
|
private readonly int _maxCharacterSlots;
|
||||||
|
|
||||||
|
public PreferencesDatabase(string databaseFilePath, int maxCharacterSlots)
|
||||||
|
{
|
||||||
|
_databaseFilePath = databaseFilePath;
|
||||||
|
_maxCharacterSlots = maxCharacterSlots;
|
||||||
|
MigrationManager.PerformUpgrade(GetDbConnectionString());
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
using (var connection = GetDbConnection())
|
||||||
|
{
|
||||||
|
var prefs = connection.QueryFirstOrDefault<PlayerPreferencesSql>(
|
||||||
|
PlayerPreferencesQuery,
|
||||||
|
new {Username = username});
|
||||||
|
if (prefs is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using Dapper for ICharacterProfile and ICharacterAppearance is annoying so
|
||||||
|
// we do it manually
|
||||||
|
var cmd = new SqliteCommand(HumanoidCharactersQuery, connection);
|
||||||
|
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 = reader.GetString(1),
|
||||||
|
Age = reader.GetInt32(2),
|
||||||
|
Sex = reader.GetString(3) == "Male" ? Male : Female,
|
||||||
|
CharacterAppearance = new HumanoidCharacterAppearance
|
||||||
|
{
|
||||||
|
HairStyleName = reader.GetString(4),
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
index = index.Clamp(0, _maxCharacterSlots - 1);
|
||||||
|
using (var connection = GetDbConnection())
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (slot < 0 || slot >= _maxCharacterSlots)
|
||||||
|
return;
|
||||||
|
if (profile is null)
|
||||||
|
{
|
||||||
|
DeleteCharacterSlot(username, slot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(profile is HumanoidCharacterProfile humanoid))
|
||||||
|
{
|
||||||
|
// TODO: Handle other ICharacterProfile implementations properly
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance;
|
||||||
|
using (var connection = GetDbConnection())
|
||||||
|
{
|
||||||
|
connection.Execute(SaveCharacterSlotQuery, new
|
||||||
|
{
|
||||||
|
Name = humanoid.Name,
|
||||||
|
Age = humanoid.Age,
|
||||||
|
Sex = humanoid.Sex.ToString(),
|
||||||
|
HairStyleName = appearance.HairStyleName,
|
||||||
|
HairColor = appearance.HairColor.ToHex(),
|
||||||
|
FacialHairStyleName = appearance.FacialHairStyleName,
|
||||||
|
FacialHairColor = appearance.FacialHairColor.ToHex(),
|
||||||
|
EyeColor = appearance.EyeColor.ToHex(),
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
using (var connection = GetDbConnection())
|
||||||
|
{
|
||||||
|
connection.Execute(DeleteCharacterSlotQuery, new {Username = username, Slot = slot});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
Content.Server/Preferences/ServerPreferencesManager.cs
Normal file
103
Content.Server/Preferences/ServerPreferencesManager.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Content.Server.Interfaces;
|
||||||
|
using Content.Shared.Preferences;
|
||||||
|
using Robust.Server.Interfaces.Player;
|
||||||
|
using Robust.Shared.Interfaces.Configuration;
|
||||||
|
using Robust.Shared.Interfaces.Network;
|
||||||
|
using Robust.Shared.Interfaces.Resources;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
|
||||||
|
namespace Content.Server.Preferences
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sends <see cref="SharedPreferencesManager.MsgPreferencesAndSettings"/> before the client joins the lobby.
|
||||||
|
/// Receives <see cref="SharedPreferencesManager.MsgSelectCharacter"/> and <see cref="SharedPreferencesManager.MsgUpdateCharacter"/> at any time.
|
||||||
|
/// </summary>
|
||||||
|
public class ServerPreferencesManager : SharedPreferencesManager, IServerPreferencesManager
|
||||||
|
{
|
||||||
|
#pragma warning disable 649
|
||||||
|
[Dependency] private readonly IServerNetManager _netManager;
|
||||||
|
[Dependency] private readonly IConfigurationManager _configuration;
|
||||||
|
[Dependency] private readonly IResourceManager _resourceManager;
|
||||||
|
#pragma warning restore 649
|
||||||
|
private PreferencesDatabase _preferencesDb;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_netManager.RegisterNetMessage<MsgPreferencesAndSettings>(nameof(MsgPreferencesAndSettings));
|
||||||
|
_netManager.RegisterNetMessage<MsgSelectCharacter>(nameof(MsgSelectCharacter),
|
||||||
|
HandleSelectCharacterMessage);
|
||||||
|
_netManager.RegisterNetMessage<MsgUpdateCharacter>(nameof(MsgUpdateCharacter),
|
||||||
|
HandleUpdateCharacterMessage);
|
||||||
|
|
||||||
|
_configuration.RegisterCVar("game.maxcharacterslots", 10);
|
||||||
|
_configuration.RegisterCVar("game.preferencesdbpath", "preferences.db");
|
||||||
|
|
||||||
|
var configPreferencesDbPath = _configuration.GetCVar<string>("game.preferencesdbpath");
|
||||||
|
var finalPreferencesDbPath = Path.Combine(_resourceManager.UserData.RootDir, configPreferencesDbPath);
|
||||||
|
|
||||||
|
var maxCharacterSlots = _configuration.GetCVar<int>("game.maxcharacterslots");
|
||||||
|
|
||||||
|
_preferencesDb = new PreferencesDatabase(finalPreferencesDbPath, maxCharacterSlots);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleSelectCharacterMessage(MsgSelectCharacter message)
|
||||||
|
{
|
||||||
|
_preferencesDb.SaveSelectedCharacterIndex(message.MsgChannel.SessionId.Username, message.SelectedCharacterIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleUpdateCharacterMessage(MsgUpdateCharacter message)
|
||||||
|
{
|
||||||
|
_preferencesDb.SaveCharacterSlot(message.MsgChannel.SessionId.Username, message.Profile, message.Slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnClientConnected(IPlayerSession session)
|
||||||
|
{
|
||||||
|
var msg = _netManager.CreateNetMessage<MsgPreferencesAndSettings>();
|
||||||
|
msg.Preferences = GetPreferences(session.SessionId.Username);
|
||||||
|
msg.Settings = new GameSettings
|
||||||
|
{
|
||||||
|
MaxCharacterSlots = _configuration.GetCVar<int>("game.maxcharacterslots")
|
||||||
|
};
|
||||||
|
_netManager.ServerSendMessage(msg, session.ConnectedClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the requested <see cref="PlayerPreferences"/> or null if not found.
|
||||||
|
/// </summary>
|
||||||
|
private PlayerPreferences GetFromSql(string username)
|
||||||
|
{
|
||||||
|
return _preferencesDb.GetPlayerPreferences(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves preferences for the given username from storage.
|
||||||
|
/// Creates and saves default preferences if they are not found, then returns them.
|
||||||
|
/// </summary>
|
||||||
|
public PlayerPreferences GetPreferences(string username)
|
||||||
|
{
|
||||||
|
var prefs = GetFromSql(username);
|
||||||
|
if (prefs is null)
|
||||||
|
{
|
||||||
|
prefs = PlayerPreferences.Default(); // TODO: Create random character instead
|
||||||
|
SavePreferences(prefs, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the given preferences to storage.
|
||||||
|
/// </summary>
|
||||||
|
public void SavePreferences(PlayerPreferences prefs, string username)
|
||||||
|
{
|
||||||
|
_preferencesDb.SaveSelectedCharacterIndex(username, prefs.SelectedCharacterIndex);
|
||||||
|
var index = 0;
|
||||||
|
foreach (var character in prefs.Characters)
|
||||||
|
{
|
||||||
|
_preferencesDb.SaveCharacterSlot(username, character, index);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ using Content.Server.GameTicking;
|
|||||||
using Content.Server.Interfaces;
|
using Content.Server.Interfaces;
|
||||||
using Content.Server.Interfaces.Chat;
|
using Content.Server.Interfaces.Chat;
|
||||||
using Content.Server.Interfaces.GameTicking;
|
using Content.Server.Interfaces.GameTicking;
|
||||||
|
using Content.Server.Preferences;
|
||||||
using Content.Server.Sandbox;
|
using Content.Server.Sandbox;
|
||||||
using Content.Server.Utility;
|
using Content.Server.Utility;
|
||||||
using Content.Shared.Interfaces;
|
using Content.Shared.Interfaces;
|
||||||
@@ -24,6 +25,7 @@ namespace Content.Server
|
|||||||
IoCManager.Register<IGalacticBankManager, GalacticBankManager>();
|
IoCManager.Register<IGalacticBankManager, GalacticBankManager>();
|
||||||
IoCManager.Register<ICargoOrderDataManager, CargoOrderDataManager>();
|
IoCManager.Register<ICargoOrderDataManager, CargoOrderDataManager>();
|
||||||
IoCManager.Register<IModuleManager, ServerModuleManager>();
|
IoCManager.Register<IModuleManager, ServerModuleManager>();
|
||||||
|
IoCManager.Register<IServerPreferencesManager, ServerPreferencesManager>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
Content.Shared/Preferences/GameSettings.cs
Normal file
20
Content.Shared/Preferences/GameSettings.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Preferences
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Information needed for character setup.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class GameSettings
|
||||||
|
{
|
||||||
|
private int _maxCharacterSlots;
|
||||||
|
|
||||||
|
public int MaxCharacterSlots
|
||||||
|
{
|
||||||
|
get => _maxCharacterSlots;
|
||||||
|
set => _maxCharacterSlots = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Content.Shared/Preferences/HumanoidCharacterAppearance.cs
Normal file
42
Content.Shared/Preferences/HumanoidCharacterAppearance.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Preferences
|
||||||
|
{
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class HumanoidCharacterAppearance : ICharacterAppearance
|
||||||
|
{
|
||||||
|
public string HairStyleName { get; set; }
|
||||||
|
public Color HairColor { get; set; }
|
||||||
|
public string FacialHairStyleName { get; set; }
|
||||||
|
public Color FacialHairColor { get; set; }
|
||||||
|
public Color EyeColor { get; set; }
|
||||||
|
public Color SkinColor { get; set; }
|
||||||
|
|
||||||
|
public static HumanoidCharacterAppearance Default()
|
||||||
|
{
|
||||||
|
return new HumanoidCharacterAppearance
|
||||||
|
{
|
||||||
|
HairStyleName = "Bald",
|
||||||
|
HairColor = Color.Black,
|
||||||
|
FacialHairStyleName = "Shaved",
|
||||||
|
FacialHairColor = Color.Black,
|
||||||
|
EyeColor = Color.Black,
|
||||||
|
SkinColor = Color.Black
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MemberwiseEquals(ICharacterAppearance maybeOther)
|
||||||
|
{
|
||||||
|
if (!(maybeOther is HumanoidCharacterAppearance other)) return false;
|
||||||
|
if (HairStyleName != other.HairStyleName) return false;
|
||||||
|
if (HairColor != other.HairColor) return false;
|
||||||
|
if (FacialHairStyleName != other.FacialHairStyleName) return false;
|
||||||
|
if (FacialHairColor != other.FacialHairColor) return false;
|
||||||
|
if (EyeColor != other.EyeColor) return false;
|
||||||
|
if (SkinColor != other.SkinColor) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Content.Shared/Preferences/HumanoidCharacterProfile.cs
Normal file
36
Content.Shared/Preferences/HumanoidCharacterProfile.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Preferences
|
||||||
|
{
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class HumanoidCharacterProfile : ICharacterProfile
|
||||||
|
{
|
||||||
|
public static HumanoidCharacterProfile Default()
|
||||||
|
{
|
||||||
|
return new HumanoidCharacterProfile
|
||||||
|
{
|
||||||
|
Name = "John Doe",
|
||||||
|
Age = 18,
|
||||||
|
Sex = Sex.Male,
|
||||||
|
CharacterAppearance = HumanoidCharacterAppearance.Default()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Age { get; set; }
|
||||||
|
public Sex Sex { get; set; }
|
||||||
|
public ICharacterAppearance CharacterAppearance { get; set; }
|
||||||
|
|
||||||
|
public bool MemberwiseEquals(ICharacterProfile maybeOther)
|
||||||
|
{
|
||||||
|
if (!(maybeOther is HumanoidCharacterProfile other)) return false;
|
||||||
|
if (Name != other.Name) return false;
|
||||||
|
if (Age != other.Age) return false;
|
||||||
|
if (Sex != other.Sex) return false;
|
||||||
|
if (CharacterAppearance is null)
|
||||||
|
return other.CharacterAppearance is null;
|
||||||
|
return CharacterAppearance.MemberwiseEquals(other.CharacterAppearance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Content.Shared/Preferences/ICharacterAppearance.cs
Normal file
7
Content.Shared/Preferences/ICharacterAppearance.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Content.Shared.Preferences
|
||||||
|
{
|
||||||
|
public interface ICharacterAppearance
|
||||||
|
{
|
||||||
|
bool MemberwiseEquals(ICharacterAppearance other);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Content.Shared/Preferences/ICharacterProfile.cs
Normal file
7
Content.Shared/Preferences/ICharacterProfile.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Content.Shared.Preferences
|
||||||
|
{
|
||||||
|
public interface ICharacterProfile
|
||||||
|
{
|
||||||
|
bool MemberwiseEquals(ICharacterProfile other);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Content.Shared/Preferences/PlayerPreferences.cs
Normal file
54
Content.Shared/Preferences/PlayerPreferences.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Robust.Shared.Interfaces.Serialization;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Preferences
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains all player characters and the index of the currently selected character.
|
||||||
|
/// Serialized both over the network and to disk.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public class PlayerPreferences
|
||||||
|
{
|
||||||
|
public static PlayerPreferences Default()
|
||||||
|
{
|
||||||
|
return new PlayerPreferences
|
||||||
|
{
|
||||||
|
Characters = new List<ICharacterProfile>
|
||||||
|
{
|
||||||
|
HumanoidCharacterProfile.Default()
|
||||||
|
},
|
||||||
|
SelectedCharacterIndex = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ICharacterProfile> _characters;
|
||||||
|
private int _selectedCharacterIndex;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All player characters.
|
||||||
|
/// </summary>
|
||||||
|
public List<ICharacterProfile> Characters
|
||||||
|
{
|
||||||
|
get => _characters;
|
||||||
|
set => _characters = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Index of the currently selected character.
|
||||||
|
/// </summary>
|
||||||
|
public int SelectedCharacterIndex
|
||||||
|
{
|
||||||
|
get => _selectedCharacterIndex;
|
||||||
|
set => _selectedCharacterIndex = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the currently selected character.
|
||||||
|
/// </summary>
|
||||||
|
public ICharacterProfile SelectedCharacter => Characters.ElementAtOrDefault(SelectedCharacterIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Content.Shared/Preferences/Sex.cs
Normal file
8
Content.Shared/Preferences/Sex.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Content.Shared.Preferences
|
||||||
|
{
|
||||||
|
public enum Sex
|
||||||
|
{
|
||||||
|
Male,
|
||||||
|
Female
|
||||||
|
}
|
||||||
|
}
|
||||||
133
Content.Shared/Preferences/SharedPreferencesManager.cs
Normal file
133
Content.Shared/Preferences/SharedPreferencesManager.cs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Lidgren.Network;
|
||||||
|
using Robust.Shared.Interfaces.Network;
|
||||||
|
using Robust.Shared.Interfaces.Serialization;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
|
||||||
|
namespace Content.Shared.Preferences
|
||||||
|
{
|
||||||
|
public abstract class SharedPreferencesManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The server sends this before the client joins the lobby.
|
||||||
|
/// </summary>
|
||||||
|
protected class MsgPreferencesAndSettings : NetMessage
|
||||||
|
{
|
||||||
|
#region REQUIRED
|
||||||
|
|
||||||
|
public const MsgGroups GROUP = MsgGroups.Command;
|
||||||
|
public const string NAME = nameof(MsgPreferencesAndSettings);
|
||||||
|
|
||||||
|
public MsgPreferencesAndSettings(INetChannel channel) : base(NAME, GROUP) { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public PlayerPreferences Preferences;
|
||||||
|
public GameSettings Settings;
|
||||||
|
|
||||||
|
public override void ReadFromBuffer(NetIncomingMessage buffer)
|
||||||
|
{
|
||||||
|
var serializer = IoCManager.Resolve<IRobustSerializer>();
|
||||||
|
var length = buffer.ReadInt32();
|
||||||
|
var bytes = buffer.ReadBytes(length);
|
||||||
|
using (var stream = new MemoryStream(bytes))
|
||||||
|
{
|
||||||
|
Preferences = serializer.Deserialize<PlayerPreferences>(stream);
|
||||||
|
}
|
||||||
|
length = buffer.ReadInt32();
|
||||||
|
bytes = buffer.ReadBytes(length);
|
||||||
|
using (var stream = new MemoryStream(bytes))
|
||||||
|
{
|
||||||
|
Settings = serializer.Deserialize<GameSettings>(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteToBuffer(NetOutgoingMessage buffer)
|
||||||
|
{
|
||||||
|
var serializer = IoCManager.Resolve<IRobustSerializer>();
|
||||||
|
using (var stream = new MemoryStream())
|
||||||
|
{
|
||||||
|
serializer.Serialize(stream, Preferences);
|
||||||
|
buffer.Write((int)stream.Length);
|
||||||
|
buffer.Write(stream.ToArray());
|
||||||
|
}
|
||||||
|
using (var stream = new MemoryStream())
|
||||||
|
{
|
||||||
|
serializer.Serialize(stream, Settings);
|
||||||
|
buffer.Write((int)stream.Length);
|
||||||
|
buffer.Write(stream.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client sends this to select a character slot.
|
||||||
|
/// </summary>
|
||||||
|
protected class MsgSelectCharacter : NetMessage
|
||||||
|
{
|
||||||
|
#region REQUIRED
|
||||||
|
|
||||||
|
public const MsgGroups GROUP = MsgGroups.Command;
|
||||||
|
public const string NAME = nameof(MsgSelectCharacter);
|
||||||
|
|
||||||
|
public MsgSelectCharacter(INetChannel channel) : base(NAME, GROUP) { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public int SelectedCharacterIndex;
|
||||||
|
|
||||||
|
public override void ReadFromBuffer(NetIncomingMessage buffer)
|
||||||
|
{
|
||||||
|
SelectedCharacterIndex = buffer.ReadInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteToBuffer(NetOutgoingMessage buffer)
|
||||||
|
{
|
||||||
|
buffer.Write(SelectedCharacterIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client sends this to update a character profile.
|
||||||
|
/// </summary>
|
||||||
|
protected class MsgUpdateCharacter : NetMessage
|
||||||
|
{
|
||||||
|
#region REQUIRED
|
||||||
|
|
||||||
|
public const MsgGroups GROUP = MsgGroups.Command;
|
||||||
|
public const string NAME = nameof(MsgUpdateCharacter);
|
||||||
|
|
||||||
|
public MsgUpdateCharacter(INetChannel channel) : base(NAME, GROUP) { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public int Slot;
|
||||||
|
public ICharacterProfile Profile;
|
||||||
|
|
||||||
|
public override void ReadFromBuffer(NetIncomingMessage buffer)
|
||||||
|
{
|
||||||
|
Slot = buffer.ReadInt32();
|
||||||
|
var serializer = IoCManager.Resolve<IRobustSerializer>();
|
||||||
|
var length = buffer.ReadInt32();
|
||||||
|
var bytes = buffer.ReadBytes(length);
|
||||||
|
using (var stream = new MemoryStream(bytes))
|
||||||
|
{
|
||||||
|
Profile = serializer.Deserialize<ICharacterProfile>(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteToBuffer(NetOutgoingMessage buffer)
|
||||||
|
{
|
||||||
|
buffer.Write(Slot);
|
||||||
|
var serializer = IoCManager.Resolve<IRobustSerializer>();
|
||||||
|
using (var stream = new MemoryStream())
|
||||||
|
{
|
||||||
|
serializer.Serialize(stream, Profile);
|
||||||
|
buffer.Write((int)stream.Length);
|
||||||
|
buffer.Write(stream.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
Content.Tests/Server/Preferences/PreferencesDatabaseTests.cs
Normal file
101
Content.Tests/Server/Preferences/PreferencesDatabaseTests.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Content.Server.Preferences;
|
||||||
|
using Content.Shared.Preferences;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.UnitTesting;
|
||||||
|
|
||||||
|
namespace Content.Tests.Server.Preferences
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class PreferencesDatabaseTests : RobustUnitTest
|
||||||
|
{
|
||||||
|
private const int MaxCharacterSlots = 10;
|
||||||
|
|
||||||
|
private static ICharacterProfile CharlieCharlieson()
|
||||||
|
{
|
||||||
|
return new HumanoidCharacterProfile
|
||||||
|
{
|
||||||
|
Name = "Charlie Charlieson",
|
||||||
|
Age = 21,
|
||||||
|
Sex = Sex.Male,
|
||||||
|
CharacterAppearance = new HumanoidCharacterAppearance()
|
||||||
|
{
|
||||||
|
HairStyleName = "Afro",
|
||||||
|
HairColor = Color.Aqua,
|
||||||
|
FacialHairStyleName = "Shaved",
|
||||||
|
FacialHairColor = Color.Aquamarine,
|
||||||
|
EyeColor = Color.Azure,
|
||||||
|
SkinColor = Color.Beige
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PreferencesDatabase GetDb()
|
||||||
|
{
|
||||||
|
return new PreferencesDatabase(Path.GetTempFileName(), MaxCharacterSlots);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUserDoesNotExist()
|
||||||
|
{
|
||||||
|
var db = GetDb();
|
||||||
|
Assert.Null(db.GetPlayerPreferences("[The database should be empty so any string should do]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUserDoesExist()
|
||||||
|
{
|
||||||
|
var db = GetDb();
|
||||||
|
const string username = "bobby";
|
||||||
|
db.SaveSelectedCharacterIndex(username, 0);
|
||||||
|
var prefs = db.GetPlayerPreferences(username);
|
||||||
|
Assert.NotNull(prefs);
|
||||||
|
Assert.Zero(prefs.SelectedCharacterIndex);
|
||||||
|
Assert.That(prefs.Characters.TrueForAll(character => character is null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUpdateCharacter()
|
||||||
|
{
|
||||||
|
var db = GetDb();
|
||||||
|
const string username = "charlie";
|
||||||
|
const int slot = 0;
|
||||||
|
var originalProfile = CharlieCharlieson();
|
||||||
|
db.SaveSelectedCharacterIndex(username, slot);
|
||||||
|
db.SaveCharacterSlot(username, originalProfile, slot);
|
||||||
|
var prefs = db.GetPlayerPreferences(username);
|
||||||
|
Assert.That(prefs.Characters[slot].MemberwiseEquals(originalProfile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDeleteCharacter()
|
||||||
|
{
|
||||||
|
var db = GetDb();
|
||||||
|
const string username = "charlie";
|
||||||
|
const int slot = 0;
|
||||||
|
db.SaveSelectedCharacterIndex(username, slot);
|
||||||
|
db.SaveCharacterSlot(username, CharlieCharlieson(), slot);
|
||||||
|
db.SaveCharacterSlot(username, null, slot);
|
||||||
|
var prefs = db.GetPlayerPreferences(username);
|
||||||
|
Assert.That(prefs.Characters.TrueForAll(character => character is null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInvalidSlot()
|
||||||
|
{
|
||||||
|
var db = GetDb();
|
||||||
|
const string username = "charlie";
|
||||||
|
const int slot = -1;
|
||||||
|
|
||||||
|
db.SaveSelectedCharacterIndex(username, slot);
|
||||||
|
db.SaveCharacterSlot(username, CharlieCharlieson(), slot);
|
||||||
|
var prefs = db.GetPlayerPreferences(username);
|
||||||
|
Assert.AreEqual(prefs.SelectedCharacterIndex, 0);
|
||||||
|
|
||||||
|
db.SaveSelectedCharacterIndex(username, MaxCharacterSlots);
|
||||||
|
prefs = db.GetPlayerPreferences(username);
|
||||||
|
Assert.AreEqual(prefs.SelectedCharacterIndex, MaxCharacterSlots-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user