Use async DB queries.

This commit is contained in:
Pieter-Jan Briers
2020-06-26 03:46:08 +02:00
parent 357c110535
commit 579ff6bb26
20 changed files with 656 additions and 148 deletions

View File

@@ -1,3 +1,4 @@
using System;
using System.Linq; using System.Linq;
using Content.Client.Interfaces; using Content.Client.Interfaces;
using Content.Shared.Preferences; using Content.Shared.Preferences;
@@ -17,6 +18,7 @@ namespace Content.Client
[Dependency] private readonly IClientNetManager _netManager; [Dependency] private readonly IClientNetManager _netManager;
#pragma warning restore 649 #pragma warning restore 649
public event Action OnServerDataLoaded;
public GameSettings Settings { get; private set; } public GameSettings Settings { get; private set; }
public PlayerPreferences Preferences { get; private set; } public PlayerPreferences Preferences { get; private set; }
@@ -69,6 +71,8 @@ namespace Content.Client
{ {
Preferences = message.Preferences; Preferences = message.Preferences;
Settings = message.Settings; Settings = message.Settings;
OnServerDataLoaded?.Invoke();
} }
} }
} }

View File

@@ -1,9 +1,14 @@
using System;
using Content.Shared.Preferences; using Content.Shared.Preferences;
namespace Content.Client.Interfaces namespace Content.Client.Interfaces
{ {
public interface IClientPreferencesManager public interface IClientPreferencesManager
{ {
event Action OnServerDataLoaded;
bool ServerDataLoaded => Settings != null;
GameSettings Settings { get; } GameSettings Settings { get; }
PlayerPreferences Preferences { get; } PlayerPreferences Preferences { get; }
void Initialize(); void Initialize();

View File

@@ -136,7 +136,6 @@ namespace Content.Client.UserInterface
_createNewCharacterButton = new Button _createNewCharacterButton = new Button
{ {
Text = "Create new slot...", Text = "Create new slot...",
ToolTip = $"A maximum of {preferencesManager.Settings.MaxCharacterSlots} characters are allowed."
}; };
_createNewCharacterButton.OnPressed += args => _createNewCharacterButton.OnPressed += args =>
{ {
@@ -155,6 +154,8 @@ namespace Content.Client.UserInterface
hBox.AddChild(_humanoidProfileEditor); hBox.AddChild(_humanoidProfileEditor);
UpdateUI(); UpdateUI();
preferencesManager.OnServerDataLoaded += UpdateUI;
} }
public void Save() => _humanoidProfileEditor.Save(); public void Save() => _humanoidProfileEditor.Save();
@@ -164,6 +165,15 @@ namespace Content.Client.UserInterface
var numberOfFullSlots = 0; var numberOfFullSlots = 0;
var characterButtonsGroup = new ButtonGroup(); var characterButtonsGroup = new ButtonGroup();
_charactersVBox.RemoveAllChildren(); _charactersVBox.RemoveAllChildren();
if (!_preferencesManager.ServerDataLoaded)
{
return;
}
_createNewCharacterButton.ToolTip =
$"A maximum of {_preferencesManager.Settings.MaxCharacterSlots} characters are allowed.";
var characterIndex = 0; var characterIndex = 0;
foreach (var character in _preferencesManager.Preferences.Characters) foreach (var character in _preferencesManager.Preferences.Characters)
{ {

View File

@@ -51,8 +51,7 @@ namespace Content.Client.UserInterface
public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager) public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager)
{ {
_random = IoCManager.Resolve<IRobustRandom>(); _random = IoCManager.Resolve<IRobustRandom>();
Profile = (HumanoidCharacterProfile) preferencesManager.Preferences.SelectedCharacter;
CharacterSlot = preferencesManager.Preferences.SelectedCharacterIndex;
_preferencesManager = preferencesManager; _preferencesManager = preferencesManager;
var margin = new MarginContainer var margin = new MarginContainer
@@ -365,11 +364,23 @@ namespace Content.Client.UserInterface
#endregion Save #endregion Save
UpdateControls(); if (preferencesManager.ServerDataLoaded)
{
LoadServerData();
}
preferencesManager.OnServerDataLoaded += LoadServerData;
IsDirty = false; IsDirty = false;
} }
private void LoadServerData()
{
Profile = (HumanoidCharacterProfile) _preferencesManager.Preferences.SelectedCharacter;
CharacterSlot = _preferencesManager.Preferences.SelectedCharacterIndex;
UpdateControls();
}
private void SetAge(int newAge) private void SetAge(int newAge)
{ {
Profile = Profile?.WithAge(newAge); Profile = Profile?.WithAge(newAge);

View File

@@ -22,6 +22,8 @@ namespace Content.Client.UserInterface
private readonly IClientPreferencesManager _preferencesManager; private readonly IClientPreferencesManager _preferencesManager;
private IEntity _previewDummy; private IEntity _previewDummy;
private readonly Label _summaryLabel; private readonly Label _summaryLabel;
private VBoxContainer _loaded;
private Label _unloaded;
public LobbyCharacterPreviewPanel(IEntityManager entityManager, public LobbyCharacterPreviewPanel(IEntityManager entityManager,
IClientPreferencesManager preferencesManager) IClientPreferencesManager preferencesManager)
@@ -50,9 +52,13 @@ namespace Content.Client.UserInterface
var vBox = new VBoxContainer(); var vBox = new VBoxContainer();
vBox.AddChild(header); vBox.AddChild(header);
vBox.AddChild(CharacterSetupButton);
vBox.AddChild(_summaryLabel); _unloaded = new Label {Text = "Your character preferences have not yet loaded, please stand by."};
_loaded = new VBoxContainer {Visible = false};
_loaded.AddChild(CharacterSetupButton);
_loaded.AddChild(_summaryLabel);
var hBox = new HBoxContainer(); var hBox = new HBoxContainer();
hBox.AddChild(viewSouth); hBox.AddChild(viewSouth);
@@ -60,11 +66,15 @@ namespace Content.Client.UserInterface
hBox.AddChild(viewWest); hBox.AddChild(viewWest);
hBox.AddChild(viewEast); hBox.AddChild(viewEast);
vBox.AddChild(hBox); _loaded.AddChild(hBox);
vBox.AddChild(_loaded);
vBox.AddChild(_unloaded);
AddChild(vBox); AddChild(vBox);
UpdateUI(); UpdateUI();
_preferencesManager.OnServerDataLoaded += UpdateUI;
} }
public Button CharacterSetupButton { get; } public Button CharacterSetupButton { get; }
@@ -89,6 +99,15 @@ namespace Content.Client.UserInterface
public void UpdateUI() public void UpdateUI()
{ {
if (!_preferencesManager.ServerDataLoaded)
{
_loaded.Visible = false;
_unloaded.Visible = true;
}
else
{
_loaded.Visible = true;
_unloaded.Visible = false;
if (!(_preferencesManager.Preferences.SelectedCharacter is HumanoidCharacterProfile selectedCharacter)) if (!(_preferencesManager.Preferences.SelectedCharacter is HumanoidCharacterProfile selectedCharacter))
{ {
_summaryLabel.Text = string.Empty; _summaryLabel.Text = string.Empty;
@@ -102,6 +121,7 @@ namespace Content.Client.UserInterface
GiveDummyJobClothes(_previewDummy, selectedCharacter); GiveDummyJobClothes(_previewDummy, selectedCharacter);
} }
} }
}
public static void GiveDummyJobClothes(IEntity dummy, HumanoidCharacterProfile profile) public static void GiveDummyJobClothes(IEntity dummy, HumanoidCharacterProfile profile)
{ {

View File

@@ -0,0 +1,154 @@
// <auto-generated />
using Content.Server.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Content.Server.Database.Migrations.Postgres
{
[DbContext(typeof(PostgresPreferencesDbContext))]
[Migration("20200625230829_AddSlotPrefsIdIndex")]
partial class AddSlotPrefsIdIndex
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
.HasAnnotation("ProductVersion", "3.1.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b =>
{
b.Property<int>("HumanoidProfileId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
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>("PreferenceUnavailable")
.HasColumnType("integer");
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.HasIndex("Slot", "PrefsId")
.IsUnique();
b.ToTable("HumanoidProfile");
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.Property<int>("JobId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("JobName")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<int>("ProfileHumanoidProfileId")
.HasColumnType("integer");
b.HasKey("JobId");
b.HasIndex("ProfileHumanoidProfileId");
b.ToTable("Job");
});
modelBuilder.Entity("Content.Server.Database.Prefs", b =>
{
b.Property<int>("PrefsId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
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", "Prefs")
.WithMany("HumanoidProfiles")
.HasForeignKey("PrefsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.HasOne("Content.Server.Database.HumanoidProfile", "Profile")
.WithMany("Jobs")
.HasForeignKey("ProfileHumanoidProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Content.Server.Database.Migrations.Postgres
{
public partial class AddSlotPrefsIdIndex : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_HumanoidProfile_Slot_PrefsId",
table: "HumanoidProfile",
columns: new[] { "Slot", "PrefsId" },
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_HumanoidProfile_Slot_PrefsId",
table: "HumanoidProfile");
}
}
}

View File

@@ -1,7 +1,8 @@
// <auto-generated /> // <auto-generated />
using Content.Server.Database;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Content.Server.Database.Migrations.Postgres namespace Content.Server.Database.Migrations.Postgres
@@ -14,7 +15,7 @@ namespace Content.Server.Database.Migrations.Postgres
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
.HasAnnotation("ProductVersion", "3.1.0") .HasAnnotation("ProductVersion", "3.1.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b => modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b =>
@@ -76,6 +77,9 @@ namespace Content.Server.Database.Migrations.Postgres
b.HasIndex("PrefsId"); b.HasIndex("PrefsId");
b.HasIndex("Slot", "PrefsId")
.IsUnique();
b.ToTable("HumanoidProfile"); b.ToTable("HumanoidProfile");
}); });

View File

@@ -0,0 +1,148 @@
// <auto-generated />
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.Sqlite
{
[DbContext(typeof(SqlitePreferencesDbContext))]
[Migration("20200625230839_AddSlotPrefsIdIndex")]
partial class AddSlotPrefsIdIndex
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.4");
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>("PreferenceUnavailable")
.HasColumnType("INTEGER");
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.HasIndex("Slot", "PrefsId")
.IsUnique();
b.ToTable("HumanoidProfile");
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.Property<int>("JobId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("JobName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Priority")
.HasColumnType("INTEGER");
b.Property<int>("ProfileHumanoidProfileId")
.HasColumnType("INTEGER");
b.HasKey("JobId");
b.HasIndex("ProfileHumanoidProfileId");
b.ToTable("Job");
});
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", "Prefs")
.WithMany("HumanoidProfiles")
.HasForeignKey("PrefsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.HasOne("Content.Server.Database.HumanoidProfile", "Profile")
.WithMany("Jobs")
.HasForeignKey("ProfileHumanoidProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Content.Server.Database.Migrations.Sqlite
{
public partial class AddSlotPrefsIdIndex : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_HumanoidProfile_Slot_PrefsId",
table: "HumanoidProfile",
columns: new[] { "Slot", "PrefsId" },
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_HumanoidProfile_Slot_PrefsId",
table: "HumanoidProfile");
}
}
}

View File

@@ -1,7 +1,8 @@
// <auto-generated /> // <auto-generated />
using Content.Server.Database;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Content.Server.Database.Migrations namespace Content.Server.Database.Migrations
{ {
@@ -12,7 +13,7 @@ namespace Content.Server.Database.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "3.1.0"); .HasAnnotation("ProductVersion", "3.1.4");
modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b => modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b =>
{ {
@@ -72,6 +73,9 @@ namespace Content.Server.Database.Migrations
b.HasIndex("PrefsId"); b.HasIndex("PrefsId");
b.HasIndex("Slot", "PrefsId")
.IsUnique();
b.ToTable("HumanoidProfile"); b.ToTable("HumanoidProfile");
}); });

View File

@@ -53,12 +53,17 @@ namespace Content.Server.Database
} }
public DbSet<Prefs> Preferences { get; set; } = null!; public DbSet<Prefs> Preferences { get; set; } = null!;
public DbSet<HumanoidProfile> HumanoidProfile { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Entity<Prefs>() modelBuilder.Entity<Prefs>()
.HasIndex(p => p.Username) .HasIndex(p => p.Username)
.IsUnique(); .IsUnique();
modelBuilder.Entity<HumanoidProfile>()
.HasIndex(p => new {p.Slot, p.PrefsId})
.IsUnique();
} }
} }

View File

@@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Content.Server.Database namespace Content.Server.Database
@@ -20,16 +22,16 @@ namespace Content.Server.Database
_prefsCtx.Database.Migrate(); _prefsCtx.Database.Migrate();
} }
public Prefs GetPlayerPreferences(string username) public async Task<Prefs?> GetPlayerPreferences(string username)
{ {
return _prefsCtx return await _prefsCtx
.Preferences .Preferences
.Include(p => p.HumanoidProfiles) .Include(p => p.HumanoidProfiles)
.ThenInclude(h => h.Jobs) .ThenInclude(h => h.Jobs)
.SingleOrDefault(p => p.Username == username); .SingleOrDefaultAsync(p => p.Username == username);
} }
public void SaveSelectedCharacterIndex(string username, int slot) public async Task SaveSelectedCharacterIndex(string username, int slot)
{ {
var prefs = _prefsCtx.Preferences.SingleOrDefault(p => p.Username == username); var prefs = _prefsCtx.Preferences.SingleOrDefault(p => p.Username == username);
if (prefs is null) if (prefs is null)
@@ -40,10 +42,10 @@ namespace Content.Server.Database
}); });
else else
prefs.SelectedCharacterSlot = slot; prefs.SelectedCharacterSlot = slot;
_prefsCtx.SaveChanges(); await _prefsCtx.SaveChangesAsync();
} }
public void SaveCharacterSlot(string username, HumanoidProfile newProfile) public async Task SaveCharacterSlotAsync(string username, HumanoidProfile newProfile)
{ {
var prefs = _prefsCtx var prefs = _prefsCtx
.Preferences .Preferences
@@ -53,17 +55,29 @@ namespace Content.Server.Database
.SingleOrDefault(h => h.Slot == newProfile.Slot); .SingleOrDefault(h => h.Slot == newProfile.Slot);
if (!(oldProfile is null)) prefs.HumanoidProfiles.Remove(oldProfile); if (!(oldProfile is null)) prefs.HumanoidProfiles.Remove(oldProfile);
prefs.HumanoidProfiles.Add(newProfile); prefs.HumanoidProfiles.Add(newProfile);
_prefsCtx.SaveChanges(); await _prefsCtx.SaveChangesAsync();
} }
public void DeleteCharacterSlot(string username, int slot) public async Task DeleteCharacterSlotAsync(string username, int slot)
{ {
var profile = _prefsCtx var profile = _prefsCtx
.Preferences .Preferences
.Single(p => p.Username == username) .Single(p => p.Username == username)
.HumanoidProfiles .HumanoidProfiles
.RemoveAll(h => h.Slot == slot); .RemoveAll(h => h.Slot == slot);
_prefsCtx.SaveChanges(); await _prefsCtx.SaveChangesAsync();
}
public async Task<Dictionary<string, HumanoidProfile>> GetProfilesForPlayersAsync(List<string> usernames)
{
return await _prefsCtx.HumanoidProfile
.Include(p => p.Jobs)
.Join(_prefsCtx.Preferences,
profile => new {profile.Slot, profile.PrefsId},
prefs => new {Slot = prefs.SelectedCharacterSlot, prefs.PrefsId},
(profile, prefs) => new {prefs.Username, profile})
.Where(p => usernames.Contains(p.Username))
.ToDictionaryAsync(arg => arg.Username, arg => arg.profile);
} }
} }
} }

View File

@@ -82,9 +82,9 @@ namespace Content.Server
{ {
base.PostInit(); base.PostInit();
IoCManager.Resolve<IServerPreferencesManager>().FinishInit();
_gameTicker.Initialize(); _gameTicker.Initialize();
IoCManager.Resolve<ISandboxManager>().Initialize(); IoCManager.Resolve<ISandboxManager>().Initialize();
IoCManager.Resolve<IServerPreferencesManager>().FinishInit();
IoCManager.Resolve<RecipeManager>().Initialize(); IoCManager.Resolve<RecipeManager>().Initialize();
IoCManager.Resolve<BlackboardManager>().Initialize(); IoCManager.Resolve<BlackboardManager>().Initialize();
IoCManager.Resolve<IPDAUplinkManager>().Initialize(); IoCManager.Resolve<IPDAUplinkManager>().Initialize();

View File

@@ -19,7 +19,7 @@ namespace Content.Server.GameTicking
private readonly Dictionary<string, int> _spawnedPositions = new Dictionary<string, int>(); private readonly Dictionary<string, int> _spawnedPositions = new Dictionary<string, int>();
private Dictionary<IPlayerSession, string> AssignJobs(List<IPlayerSession> available, private Dictionary<IPlayerSession, string> AssignJobs(List<IPlayerSession> available,
Dictionary<IPlayerSession, HumanoidCharacterProfile> profiles) Dictionary<string, HumanoidCharacterProfile> profiles)
{ {
// Calculate positions available round-start for each job. // Calculate positions available round-start for each job.
var availablePositions = GetBasePositions(true); var availablePositions = GetBasePositions(true);
@@ -38,7 +38,7 @@ namespace Content.Server.GameTicking
var candidates = available var candidates = available
.Select(player => .Select(player =>
{ {
var profile = profiles[player]; var profile = profiles[player.Name];
var availableJobs = profile.JobPriorities var availableJobs = profile.JobPriorities
.Where(j => .Where(j =>

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Content.Server.GameObjects; using Content.Server.GameObjects;
using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.Components.Markers; using Content.Server.GameObjects.Components.Markers;
@@ -205,7 +206,7 @@ namespace Content.Server.GameTicking
} }
} }
public void StartRound(bool force = false) public async void StartRound(bool force = false)
{ {
DebugTools.Assert(RunLevel == GameRunLevel.PreRoundLobby); DebugTools.Assert(RunLevel == GameRunLevel.PreRoundLobby);
Logger.InfoS("ticker", "Starting round!"); Logger.InfoS("ticker", "Starting round!");
@@ -227,7 +228,18 @@ namespace Content.Server.GameTicking
RoundLengthMetric.Set(0); RoundLengthMetric.Set(0);
// Get the profiles for each player for easier lookup. // Get the profiles for each player for easier lookup.
var profiles = readyPlayers.ToDictionary(p => p, GetPlayerProfile); var profiles = (await _prefsManager.GetSelectedProfilesForPlayersAsync(
readyPlayers
.Select(p => p.Name).ToList()))
.ToDictionary(p => p.Key, p => (HumanoidCharacterProfile) p.Value);
foreach (var readyPlayer in readyPlayers)
{
if (!profiles.ContainsKey(readyPlayer.Name))
{
profiles.Add(readyPlayer.Name, HumanoidCharacterProfile.Default());
}
}
var assignedJobs = AssignJobs(readyPlayers, profiles); var assignedJobs = AssignJobs(readyPlayers, profiles);
@@ -239,7 +251,7 @@ namespace Content.Server.GameTicking
continue; continue;
} }
var profile = profiles[player]; var profile = profiles[player.Name];
if (profile.PreferenceUnavailable == PreferenceUnavailableMode.SpawnAsOverflow) if (profile.PreferenceUnavailable == PreferenceUnavailableMode.SpawnAsOverflow)
{ {
assignedJobs.Add(player, OverflowJob); assignedJobs.Add(player, OverflowJob);
@@ -259,7 +271,8 @@ namespace Content.Server.GameTicking
{ {
SetStartPreset(_configurationManager.GetCVar<string>("game.fallbackpreset")); SetStartPreset(_configurationManager.GetCVar<string>("game.fallbackpreset"));
var newPreset = MakeGamePreset(); var newPreset = MakeGamePreset();
_chatManager.DispatchServerAnnouncement($"Failed to start {preset.ModeTitle} mode! Defaulting to {newPreset.ModeTitle}..."); _chatManager.DispatchServerAnnouncement(
$"Failed to start {preset.ModeTitle} mode! Defaulting to {newPreset.ModeTitle}...");
if (!newPreset.Start(readyPlayers, force)) if (!newPreset.Start(readyPlayers, force))
{ {
throw new ApplicationException("Fallback preset failed to start!"); throw new ApplicationException("Fallback preset failed to start!");
@@ -278,8 +291,9 @@ namespace Content.Server.GameTicking
IoCManager.Resolve<IServerNetManager>().ServerSendToAll(msg); IoCManager.Resolve<IServerNetManager>().ServerSendToAll(msg);
} }
private HumanoidCharacterProfile GetPlayerProfile(IPlayerSession p) => private async Task<HumanoidCharacterProfile> GetPlayerProfileAsync(IPlayerSession p) =>
(HumanoidCharacterProfile) _prefsManager.GetPreferences(p.SessionId.Username).SelectedCharacter; (HumanoidCharacterProfile) (await _prefsManager.GetPreferencesAsync(p.SessionId.Username))
.SelectedCharacter;
public void EndRound() public void EndRound()
{ {
@@ -297,17 +311,19 @@ namespace Content.Server.GameTicking
//Generate a list of basic player info to display in the end round summary. //Generate a list of basic player info to display in the end round summary.
var listOfPlayerInfo = new List<RoundEndPlayerInfo>(); var listOfPlayerInfo = new List<RoundEndPlayerInfo>();
foreach(var ply in _playerManager.GetAllPlayers().OrderBy(p => p.Name)) foreach (var ply in _playerManager.GetAllPlayers().OrderBy(p => p.Name))
{ {
var mind = ply.ContentData().Mind; var mind = ply.ContentData().Mind;
if(mind != null) if (mind != null)
{ {
var antag = mind.AllRoles.Any(role => role.Antag); var antag = mind.AllRoles.Any(role => role.Antag);
var playerEndRoundInfo = new RoundEndPlayerInfo() var playerEndRoundInfo = new RoundEndPlayerInfo()
{ {
PlayerOOCName = ply.Name, PlayerOOCName = ply.Name,
PlayerICName = mind.CurrentEntity.Name, PlayerICName = mind.CurrentEntity.Name,
Role = antag ? mind.AllRoles.First(role => role.Antag).Name : mind.AllRoles.FirstOrDefault()?.Name ?? Loc.GetString("Unknown"), Role = antag
? mind.AllRoles.First(role => role.Antag).Name
: mind.AllRoles.FirstOrDefault()?.Name ?? Loc.GetString("Unknown"),
Antag = antag Antag = antag
}; };
listOfPlayerInfo.Add(playerEndRoundInfo); listOfPlayerInfo.Add(playerEndRoundInfo);
@@ -725,14 +741,14 @@ namespace Content.Server.GameTicking
}, _updateShutdownCts.Token); }, _updateShutdownCts.Token);
} }
private void SpawnPlayer(IPlayerSession session, string jobId = null, bool lateJoin = true) private async void SpawnPlayer(IPlayerSession session, string jobId = null, bool lateJoin = true)
{ {
var character = (HumanoidCharacterProfile) _prefsManager
.GetPreferences(session.SessionId.Username)
.SelectedCharacter;
_playerJoinGame(session); _playerJoinGame(session);
var character = (HumanoidCharacterProfile) (await _prefsManager
.GetPreferencesAsync(session.SessionId.Username))
.SelectedCharacter;
var data = session.ContentData(); var data = session.ContentData();
data.WipeMind(); data.WipeMind();
data.Mind = new Mind(session.SessionId) data.Mind = new Mind(session.SessionId)
@@ -785,15 +801,16 @@ namespace Content.Server.GameTicking
accessTags.UnionWith(jobPrototype.Access); accessTags.UnionWith(jobPrototype.Access);
pdaComponent.SetPDAOwner(mob); pdaComponent.SetPDAOwner(mob);
var mindComponent = mob.GetComponent<MindComponent>(); var mindComponent = mob.GetComponent<MindComponent>();
if (mindComponent.HasMind)//Redundancy checks. if (mindComponent.HasMind) //Redundancy checks.
{ {
if (mindComponent.Mind.AllRoles.Any(role => role.Antag)) //Give antags a new uplinkaccount. if (mindComponent.Mind.AllRoles.Any(role => role.Antag)) //Give antags a new uplinkaccount.
{ {
var uplinkAccount = new UplinkAccount(mob.Uid, 20); //TODO: make me into a variable based on server pop or something. var uplinkAccount =
new UplinkAccount(mob.Uid,
20); //TODO: make me into a variable based on server pop or something.
pdaComponent.InitUplinkAccount(uplinkAccount); pdaComponent.InitUplinkAccount(uplinkAccount);
} }
} }
} }
private void AddManifestEntry(string characterName, string jobId) private void AddManifestEntry(string characterName, string jobId)
@@ -801,13 +818,14 @@ namespace Content.Server.GameTicking
_manifest.Add(new ManifestEntry(characterName, jobId)); _manifest.Add(new ManifestEntry(characterName, jobId));
} }
private void _spawnObserver(IPlayerSession session) private async void _spawnObserver(IPlayerSession session)
{ {
var name = _prefsManager _playerJoinGame(session);
.GetPreferences(session.SessionId.Username)
var name = (await _prefsManager
.GetPreferencesAsync(session.SessionId.Username))
.SelectedCharacter.Name; .SelectedCharacter.Name;
_playerJoinGame(session);
var data = session.ContentData(); var data = session.ContentData();
data.WipeMind(); data.WipeMind();
data.Mind = new Mind(session.SessionId); data.Mind = new Mind(session.SessionId);
@@ -868,7 +886,7 @@ namespace Content.Server.GameTicking
return _localization.GetString(@"Hi and welcome to [color=white]Space Station 14![/color] return _localization.GetString(@"Hi and welcome to [color=white]Space Station 14![/color]
The current game mode is: [color=white]{0}[/color]. The current game mode is: [color=white]{0}[/color].
[color=yellow]{1}[/color]", gmTitle, desc ); [color=yellow]{1}[/color]", gmTitle, desc);
} }
private void UpdateInfoText() private void UpdateInfoText()

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
@@ -7,7 +9,8 @@ namespace Content.Server.Interfaces
{ {
void FinishInit(); void FinishInit();
void OnClientConnected(IPlayerSession session); void OnClientConnected(IPlayerSession session);
PlayerPreferences GetPreferences(string username); Task<PlayerPreferences> GetPreferencesAsync(string username);
Task<IEnumerable<KeyValuePair<string, ICharacterProfile>>> GetSelectedProfilesForPlayersAsync(List<string> usernames);
void StartInit(); void StartInit();
} }
} }

View File

@@ -1,5 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Database; using Content.Server.Database;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Robust.Shared.Maths; using Robust.Shared.Maths;
@@ -16,38 +19,28 @@ namespace Content.Server.Preferences
private readonly int _maxCharacterSlots; private readonly int _maxCharacterSlots;
private readonly PrefsDb _prefsDb; private readonly PrefsDb _prefsDb;
// We use a single DbContext for the entire DB connection, and EFCore doesn't allow concurrent access.
// So we need this semaphore to prevent bugs.
private readonly SemaphoreSlim _prefsSemaphore = new SemaphoreSlim(1, 1);
public PreferencesDatabase(IDatabaseConfiguration dbConfig, int maxCharacterSlots) public PreferencesDatabase(IDatabaseConfiguration dbConfig, int maxCharacterSlots)
{ {
_maxCharacterSlots = maxCharacterSlots; _maxCharacterSlots = maxCharacterSlots;
_prefsDb = new PrefsDb(dbConfig); _prefsDb = new PrefsDb(dbConfig);
} }
public PlayerPreferences GetPlayerPreferences(string username) public async Task<PlayerPreferences> GetPlayerPreferencesAsync(string username)
{ {
var prefs = _prefsDb.GetPlayerPreferences(username); await _prefsSemaphore.WaitAsync();
try
{
var prefs = await _prefsDb.GetPlayerPreferences(username);
if (prefs is null) return null; if (prefs is null) return null;
var profiles = new ICharacterProfile[_maxCharacterSlots]; var profiles = new ICharacterProfile[_maxCharacterSlots];
foreach (var profile in prefs.HumanoidProfiles) foreach (var profile in prefs.HumanoidProfiles)
{ {
var jobs = profile.Jobs.ToDictionary(j => j.JobName, j => (JobPriority) j.Priority); profiles[profile.Slot] = ConvertProfiles(profile);
profiles[profile.Slot] = new HumanoidCharacterProfile(
profile.CharacterName,
profile.Age,
profile.Sex == "Male" ? Male : Female,
new HumanoidCharacterAppearance
(
profile.HairName,
Color.FromHex(profile.HairColor),
profile.FacialHairName,
Color.FromHex(profile.FacialHairColor),
Color.FromHex(profile.EyeColor),
Color.FromHex(profile.SkinColor)
),
jobs,
(PreferenceUnavailableMode) profile.PreferenceUnavailable
);
} }
return new PlayerPreferences return new PlayerPreferences
@@ -56,20 +49,37 @@ namespace Content.Server.Preferences
prefs.SelectedCharacterSlot prefs.SelectedCharacterSlot
); );
} }
finally
public void SaveSelectedCharacterIndex(string username, int index)
{ {
index = index.Clamp(0, _maxCharacterSlots - 1); _prefsSemaphore.Release();
_prefsDb.SaveSelectedCharacterIndex(username, index); }
} }
public void SaveCharacterSlot(string username, ICharacterProfile profile, int slot) public async Task SaveSelectedCharacterIndexAsync(string username, int index)
{
await _prefsSemaphore.WaitAsync();
try
{
index = index.Clamp(0, _maxCharacterSlots - 1);
await _prefsDb.SaveSelectedCharacterIndex(username, index);
}
finally
{
_prefsSemaphore.Release();
}
}
public async Task SaveCharacterSlotAsync(string username, ICharacterProfile profile, int slot)
{ {
if (slot < 0 || slot >= _maxCharacterSlots) if (slot < 0 || slot >= _maxCharacterSlots)
return; return;
await _prefsSemaphore.WaitAsync();
try
{
if (profile is null) if (profile is null)
{ {
DeleteCharacterSlot(username, slot); await DeleteCharacterSlotAsync(username, slot);
return; return;
} }
@@ -97,12 +107,55 @@ namespace Content.Server.Preferences
.Where(j => j.Value != JobPriority.Never) .Where(j => j.Value != JobPriority.Never)
.Select(j => new Job {JobName = j.Key, Priority = (DbJobPriority) j.Value}) .Select(j => new Job {JobName = j.Key, Priority = (DbJobPriority) j.Value})
); );
_prefsDb.SaveCharacterSlot(username, entity); await _prefsDb.SaveCharacterSlotAsync(username, entity);
}
finally
{
_prefsSemaphore.Release();
}
} }
private void DeleteCharacterSlot(string username, int slot)
private async Task DeleteCharacterSlotAsync(string username, int slot)
{ {
_prefsDb.DeleteCharacterSlot(username, slot); await _prefsDb.DeleteCharacterSlotAsync(username, slot);
}
public async Task<IEnumerable<KeyValuePair<string, ICharacterProfile>>> GetSelectedProfilesForPlayersAsync(
List<string> usernames)
{
await _prefsSemaphore.WaitAsync();
try
{
var profiles = await _prefsDb.GetProfilesForPlayersAsync(usernames);
return profiles.Select(
p => new KeyValuePair<string, ICharacterProfile>(p.Key, ConvertProfiles(p.Value)));
}
finally
{
_prefsSemaphore.Release();
}
}
private static HumanoidCharacterProfile ConvertProfiles(HumanoidProfile profile)
{
var jobs = profile.Jobs.ToDictionary(j => j.JobName, j => (JobPriority) j.Priority);
return new HumanoidCharacterProfile(
profile.CharacterName,
profile.Age,
profile.Sex == "Male" ? Male : Female,
new HumanoidCharacterAppearance
(
profile.HairName,
Color.FromHex(profile.HairColor),
profile.FacialHairName,
Color.FromHex(profile.FacialHairColor),
Color.FromHex(profile.EyeColor),
Color.FromHex(profile.SkinColor)
),
jobs,
(PreferenceUnavailableMode) profile.PreferenceUnavailable
);
} }
} }
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Database; using Content.Server.Database;
@@ -82,20 +83,22 @@ namespace Content.Server.Preferences
_preferencesDb = _prefsDbLoadTask.Result; _preferencesDb = _prefsDbLoadTask.Result;
} }
private void HandleSelectCharacterMessage(MsgSelectCharacter message) private async void HandleSelectCharacterMessage(MsgSelectCharacter message)
{ {
_preferencesDb.SaveSelectedCharacterIndex(message.MsgChannel.SessionId.Username, message.SelectedCharacterIndex); await _preferencesDb.SaveSelectedCharacterIndexAsync(message.MsgChannel.SessionId.Username,
message.SelectedCharacterIndex);
} }
private void HandleUpdateCharacterMessage(MsgUpdateCharacter message) private async void HandleUpdateCharacterMessage(MsgUpdateCharacter message)
{ {
_preferencesDb.SaveCharacterSlot(message.MsgChannel.SessionId.Username, message.Profile, message.Slot); await _preferencesDb.SaveCharacterSlotAsync(message.MsgChannel.SessionId.Username, message.Profile,
message.Slot);
} }
public void OnClientConnected(IPlayerSession session) public async void OnClientConnected(IPlayerSession session)
{ {
var msg = _netManager.CreateNetMessage<MsgPreferencesAndSettings>(); var msg = _netManager.CreateNetMessage<MsgPreferencesAndSettings>();
msg.Preferences = GetPreferences(session.SessionId.Username); msg.Preferences = await GetPreferencesAsync(session.SessionId.Username);
msg.Settings = new GameSettings msg.Settings = new GameSettings
{ {
MaxCharacterSlots = _configuration.GetCVar<int>("game.maxcharacterslots") MaxCharacterSlots = _configuration.GetCVar<int>("game.maxcharacterslots")
@@ -106,26 +109,31 @@ namespace Content.Server.Preferences
/// <summary> /// <summary>
/// Returns the requested <see cref="PlayerPreferences"/> or null if not found. /// Returns the requested <see cref="PlayerPreferences"/> or null if not found.
/// </summary> /// </summary>
private PlayerPreferences GetFromSql(string username) private async Task<PlayerPreferences> GetFromSql(string username)
{ {
return _preferencesDb.GetPlayerPreferences(username); return await _preferencesDb.GetPlayerPreferencesAsync(username);
} }
/// <summary> /// <summary>
/// Retrieves preferences for the given username from storage. /// Retrieves preferences for the given username from storage.
/// Creates and saves default preferences if they are not found, then returns them. /// Creates and saves default preferences if they are not found, then returns them.
/// </summary> /// </summary>
public PlayerPreferences GetPreferences(string username) public async Task<PlayerPreferences> GetPreferencesAsync(string username)
{ {
var prefs = GetFromSql(username); var prefs = await GetFromSql(username);
if (prefs is null) if (prefs is null)
{ {
_preferencesDb.SaveSelectedCharacterIndex(username, 0); await _preferencesDb.SaveSelectedCharacterIndexAsync(username, 0);
_preferencesDb.SaveCharacterSlot(username, HumanoidCharacterProfile.Default(), 0); await _preferencesDb.SaveCharacterSlotAsync(username, HumanoidCharacterProfile.Default(), 0);
prefs = GetFromSql(username); prefs = await GetFromSql(username);
} }
return prefs; return prefs;
} }
public async Task<IEnumerable<KeyValuePair<string, ICharacterProfile>>> GetSelectedProfilesForPlayersAsync(List<string> usernames)
{
return await _preferencesDb.GetSelectedProfilesForPlayersAsync(usernames);
}
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Content.Server.Database; using Content.Server.Database;
using Content.Server.Preferences; using Content.Server.Preferences;
using Content.Shared; using Content.Shared;
@@ -44,64 +45,64 @@ namespace Content.Tests.Server.Preferences
} }
[Test] [Test]
public void TestUserDoesNotExist() public async Task TestUserDoesNotExist()
{ {
var db = GetDb(); var db = GetDb();
Assert.Null(db.GetPlayerPreferences("[The database should be empty so any string should do]")); Assert.Null(await db.GetPlayerPreferencesAsync("[The database should be empty so any string should do]"));
} }
[Test] [Test]
public void TestUserDoesExist() public async Task TestUserDoesExist()
{ {
var db = GetDb(); var db = GetDb();
const string username = "bobby"; const string username = "bobby";
db.SaveSelectedCharacterIndex(username, 0); await db.SaveSelectedCharacterIndexAsync(username, 0);
var prefs = db.GetPlayerPreferences(username); var prefs = await db.GetPlayerPreferencesAsync(username);
Assert.NotNull(prefs); Assert.NotNull(prefs);
Assert.Zero(prefs.SelectedCharacterIndex); Assert.Zero(prefs.SelectedCharacterIndex);
Assert.That(prefs.Characters.ToList().TrueForAll(character => character is null)); Assert.That(prefs.Characters.ToList().TrueForAll(character => character is null));
} }
[Test] [Test]
public void TestUpdateCharacter() public async Task TestUpdateCharacter()
{ {
var db = GetDb(); var db = GetDb();
const string username = "charlie"; const string username = "charlie";
const int slot = 0; const int slot = 0;
var originalProfile = CharlieCharlieson(); var originalProfile = CharlieCharlieson();
db.SaveSelectedCharacterIndex(username, slot); await db.SaveSelectedCharacterIndexAsync(username, slot);
db.SaveCharacterSlot(username, originalProfile, slot); await db.SaveCharacterSlotAsync(username, originalProfile, slot);
var prefs = db.GetPlayerPreferences(username); var prefs = await db.GetPlayerPreferencesAsync(username);
Assert.That(prefs.Characters.ElementAt(slot).MemberwiseEquals(originalProfile)); Assert.That(prefs.Characters.ElementAt(slot).MemberwiseEquals(originalProfile));
} }
[Test] [Test]
public void TestDeleteCharacter() public async Task TestDeleteCharacter()
{ {
var db = GetDb(); var db = GetDb();
const string username = "charlie"; const string username = "charlie";
const int slot = 0; const int slot = 0;
db.SaveSelectedCharacterIndex(username, slot); await db.SaveSelectedCharacterIndexAsync(username, slot);
db.SaveCharacterSlot(username, CharlieCharlieson(), slot); await db.SaveCharacterSlotAsync(username, CharlieCharlieson(), slot);
db.SaveCharacterSlot(username, null, slot); await db.SaveCharacterSlotAsync(username, null, slot);
var prefs = db.GetPlayerPreferences(username); var prefs = await db.GetPlayerPreferencesAsync(username);
Assert.That(prefs.Characters.ToList().TrueForAll(character => character is null)); Assert.That(prefs.Characters.ToList().TrueForAll(character => character is null));
} }
[Test] [Test]
public void TestInvalidSlot() public async Task TestInvalidSlot()
{ {
var db = GetDb(); var db = GetDb();
const string username = "charlie"; const string username = "charlie";
const int slot = -1; const int slot = -1;
db.SaveSelectedCharacterIndex(username, slot); await db.SaveSelectedCharacterIndexAsync(username, slot);
db.SaveCharacterSlot(username, CharlieCharlieson(), slot); await db.SaveCharacterSlotAsync(username, CharlieCharlieson(), slot);
var prefs = db.GetPlayerPreferences(username); var prefs = await db.GetPlayerPreferencesAsync(username);
Assert.AreEqual(prefs.SelectedCharacterIndex, 0); Assert.AreEqual(prefs.SelectedCharacterIndex, 0);
db.SaveSelectedCharacterIndex(username, MaxCharacterSlots); await db.SaveSelectedCharacterIndexAsync(username, MaxCharacterSlots);
prefs = db.GetPlayerPreferences(username); prefs = await db.GetPlayerPreferencesAsync(username);
Assert.AreEqual(prefs.SelectedCharacterIndex, MaxCharacterSlots - 1); Assert.AreEqual(prefs.SelectedCharacterIndex, MaxCharacterSlots - 1);
} }
} }