diff --git a/Fuyu.Launcher/Fuyu.Launcher.csproj b/Fuyu.Launcher/Fuyu.Launcher.csproj index 3ab2938..49a555b 100644 --- a/Fuyu.Launcher/Fuyu.Launcher.csproj +++ b/Fuyu.Launcher/Fuyu.Launcher.csproj @@ -9,9 +9,9 @@ true true - Resources\icon.ico true true + Resources\icon.ico @@ -25,10 +25,29 @@ + + + + Always + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/Fuyu.Launcher/MainWindow.xaml b/Fuyu.Launcher/MainWindow.xaml index 7ab5dcd..410a070 100644 --- a/Fuyu.Launcher/MainWindow.xaml +++ b/Fuyu.Launcher/MainWindow.xaml @@ -6,6 +6,7 @@ xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf" xmlns:local="clr-namespace:Fuyu.Launcher" mc:Ignorable="d" + Icon="Resources\Icon.ico" WindowStartupLocation="CenterScreen" Title="Fuyu.Launcher" Height="450" Width="800"> diff --git a/Fuyu.Launcher/Resources/Resources.Designer.cs b/Fuyu.Launcher/Resources/Resources.Designer.cs new file mode 100644 index 0000000..3272c93 --- /dev/null +++ b/Fuyu.Launcher/Resources/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Fuyu.Launcher.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Fuyu.Launcher.Resources.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/Fuyu.Launcher/Resources/Resources.resx b/Fuyu.Launcher/Resources/Resources.resx new file mode 100644 index 0000000..4fdb1b6 --- /dev/null +++ b/Fuyu.Launcher/Resources/Resources.resx @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Fuyu.Platform.Common/Collections/ThreadDictionary.cs b/Fuyu.Platform.Common/Collections/ThreadDictionary.cs new file mode 100644 index 0000000..953ed8f --- /dev/null +++ b/Fuyu.Platform.Common/Collections/ThreadDictionary.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; + +namespace Fuyu.Platform.Common.Collections +{ + // NOTE: Why not ConcurrentDictionary? Because it's horribly slow + // in .NET 8.0, let alone .NET Framework 4.7.1. Manual locking is + // about 700x faster in most use-cases. + + public class ThreadDictionary + { + private readonly Dictionary _dictionary; + private readonly object _lock; + + public ThreadDictionary() + { + _dictionary = new Dictionary(); + _lock = new object(); + } + + public Dictionary ToDictionary() + { + return _dictionary; + } + + public T2 Get(T1 key) + { + return _dictionary[key]; + } + + public void Set(T1 key, T2 value) + { + lock (_lock) + { + _dictionary[key] = value; + } + } + + public void Add(T1 key, T2 value) + { + lock (_lock) + { + _dictionary.Add(key, value); + } + } + + public void Remove(T1 key) + { + lock (_lock) + { + _dictionary.Remove(key); + } + } + } +} \ No newline at end of file diff --git a/Fuyu.Platform.Common/Collections/ThreadList.cs b/Fuyu.Platform.Common/Collections/ThreadList.cs new file mode 100644 index 0000000..1d2aa23 --- /dev/null +++ b/Fuyu.Platform.Common/Collections/ThreadList.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; + +namespace Fuyu.Platform.Common.Collections +{ + // NOTE: Why not ConcurrentBag? While it's performance is very fast in + // in intensive parallel access under .NET 8.0, it leaves a lot to be + // desired in .NET Framework 4.7.1. For random read-write, List with + // manual lock was still faster for me. + + public class ThreadList + { + private readonly List _list; + private readonly object _lock; + + public ThreadList() + { + _list = new List(); + _lock = new object(); + } + + public List ToList() + { + return _list; + } + + public T Get(int index) + { + return _list[index]; + } + + public void Set(int index, T value) + { + lock (_lock) + { + _list[index] = value; + } + } + + public void Add(T value) + { + lock (_lock) + { + _list.Add(value); + } + } + + public void Remove(T value) + { + lock (_lock) + { + _list.Remove(value); + } + } + + public void RemoveAt(int index) + { + lock (_lock) + { + _list.RemoveAt(index); + } + } + } +} \ No newline at end of file diff --git a/Fuyu.Platform.Common/Collections/ThreadObject.cs b/Fuyu.Platform.Common/Collections/ThreadObject.cs new file mode 100644 index 0000000..84cbfc6 --- /dev/null +++ b/Fuyu.Platform.Common/Collections/ThreadObject.cs @@ -0,0 +1,27 @@ +namespace Fuyu.Platform.Common.Collections +{ + public class ThreadObject + { + private T _object; + private readonly object _lock; + + public ThreadObject(T value) + { + _object = value; + _lock = new object(); + } + + public T Get() + { + return _object; + } + + public void Set(T value) + { + lock (_lock) + { + _object = value; + } + } + } +} \ No newline at end of file diff --git a/Fuyu.Platform.Server/Databases/EFT/LocaleTable.cs b/Fuyu.Platform.Server/Databases/EFT/LocaleTable.cs index 6e5779b..489af28 100644 --- a/Fuyu.Platform.Server/Databases/EFT/LocaleTable.cs +++ b/Fuyu.Platform.Server/Databases/EFT/LocaleTable.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Fuyu.Platform.Common.Collections; using Fuyu.Platform.Common.IO; using Fuyu.Platform.Common.Models.EFT.Responses; using Fuyu.Platform.Common.Serialization; @@ -7,18 +8,11 @@ namespace Fuyu.Platform.Server.Databases.EFT { public class LocaleTable { - static LocaleTable() - { - _languagesLock = new object(); - _globalLocalesLock = new object(); - _menuLocalesLock = new object(); - } - public LocaleTable() { - _languages = new Dictionary(); - _globalLocales = new Dictionary>(); - _menuLocales = new Dictionary(); + _languages = new ThreadDictionary(); + _globalLocales = new ThreadDictionary>(); + _menuLocales = new ThreadDictionary(); } public void Load() @@ -29,9 +23,8 @@ public void Load() } #region Languages -// langid name - private readonly Dictionary _languages; - private static readonly object _languagesLock; +// langid name + private readonly ThreadDictionary _languages; private void LoadLanguages() { @@ -46,43 +39,33 @@ private void LoadLanguages() public Dictionary GetLanguages() { - return _languages; + return _languages.ToDictionary(); } public string GetLanguage(string languageId) { - return _languages[languageId]; + return _languages.Get(languageId); } public void SetLanguage(string languageId, string name) { - lock (_languagesLock) - { - _languages[languageId] = name; - } + _languages.Set(languageId, name); } public void AddLanguage(string languageId, string name) { - lock (_languagesLock) - { - _languages.Add(languageId, name); - } + _languages.Add(languageId, name); } public void RemoveLanguage(string languageId) { - lock (_languagesLock) - { - _languages.Remove(languageId); - } + _languages.Remove(languageId); } #endregion #region GlobalLocales -// langid key value - private readonly Dictionary> _globalLocales; - private static readonly object _globalLocalesLock; +// langid key value + private readonly ThreadDictionary> _globalLocales; private void LoadGlobalLocales() { @@ -99,43 +82,33 @@ private void LoadGlobalLocales() public Dictionary> GetGlobalLocales() { - return _globalLocales; + return _globalLocales.ToDictionary(); } public Dictionary GetGlobalLocale(string languageId) { - return _globalLocales[languageId]; + return _globalLocales.Get(languageId); } public void SetGlobalLocale(string languageId, Dictionary globalLocale) { - lock (_globalLocalesLock) - { - _globalLocales[languageId] = globalLocale; - } + _globalLocales.Set(languageId, globalLocale); } public void AddGlobalLocale(string languageId, Dictionary globalLocale) { - lock (_globalLocalesLock) - { - _globalLocales.Add(languageId, globalLocale); - } + _globalLocales.Add(languageId, globalLocale); } public void RemoveGlobalLocale(string languageId) { - lock (_globalLocalesLock) - { - _globalLocales.Remove(languageId); - } + _globalLocales.Remove(languageId); } #endregion #region MenuLocales -// langid locale - private readonly Dictionary _menuLocales; - private static readonly object _menuLocalesLock; +// langid locale + private readonly ThreadDictionary _menuLocales; private void LoadMenuLocales() { @@ -152,36 +125,27 @@ private void LoadMenuLocales() public Dictionary GetMenuLocales() { - return _menuLocales; + return _menuLocales.ToDictionary(); } public MenuLocaleResponse GetMenuLocale(string languageId) { - return _menuLocales[languageId]; + return _menuLocales.Get(languageId); } public void SetMenuLocale(string languageId, MenuLocaleResponse menuLocale) { - lock (_menuLocalesLock) - { - _menuLocales[languageId] = menuLocale; - } + _menuLocales.Set(languageId, menuLocale); } public void AddMenuLocale(string languageId, MenuLocaleResponse menuLocale) { - lock (_menuLocalesLock) - { - _menuLocales.Add(languageId, menuLocale); - } + _menuLocales.Add(languageId, menuLocale); } public void RemoveMenuLocale(string languageId) { - lock (_menuLocalesLock) - { - _menuLocales.Remove(languageId); - } + _menuLocales.Remove(languageId); } #endregion } diff --git a/Fuyu.Platform.Server/Databases/EFT/TemplateTable.cs b/Fuyu.Platform.Server/Databases/EFT/TemplateTable.cs index 613cd90..478fc7f 100644 --- a/Fuyu.Platform.Server/Databases/EFT/TemplateTable.cs +++ b/Fuyu.Platform.Server/Databases/EFT/TemplateTable.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Fuyu.Platform.Common.Collections; using Fuyu.Platform.Common.IO; using Fuyu.Platform.Common.Models.EFT.Customization; using Fuyu.Platform.Common.Models.EFT.Responses; @@ -8,14 +9,9 @@ namespace Fuyu.Platform.Server.Databases.EFT { public class TemplateTable { - static TemplateTable() - { - _customizationsLock = new object(); - } - public TemplateTable() { - _customizations = new Dictionary(); + _customizations = new ThreadDictionary(); } public void Load() @@ -24,9 +20,8 @@ public void Load() } #region Customization -// custid template - private readonly Dictionary _customizations; - private static readonly object _customizationsLock; +// custid template + private readonly ThreadDictionary _customizations; private void LoadCustomizations() { @@ -41,36 +36,27 @@ private void LoadCustomizations() public Dictionary GetCustomizations() { - return _customizations; + return _customizations.ToDictionary(); } public CustomizationTemplate GetCustomization(string customizationId) { - return _customizations[customizationId]; + return _customizations.Get(customizationId); } public void SetCustomization(string customizationId, CustomizationTemplate template) { - lock (_customizationsLock) - { - _customizations[customizationId] = template; - } + _customizations.Set(customizationId, template); } public void AddCustomization(string customizationId, CustomizationTemplate template) { - lock (_customizationsLock) - { - _customizations.Add(customizationId, template); - } + _customizations.Add(customizationId, template); } public void RemoveCustomization(string customizationId) { - lock (_customizationsLock) - { - _customizations.Remove(customizationId); - } + _customizations.Remove(customizationId); } #endregion } diff --git a/Fuyu.Platform.Server/Databases/Fuyu/AccountTable.cs b/Fuyu.Platform.Server/Databases/Fuyu/AccountTable.cs index 4299b6d..7b851d7 100644 --- a/Fuyu.Platform.Server/Databases/Fuyu/AccountTable.cs +++ b/Fuyu.Platform.Server/Databases/Fuyu/AccountTable.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Fuyu.Platform.Common.Collections; using Fuyu.Platform.Common.IO; using Fuyu.Platform.Common.Models.Fuyu.Accounts; using Fuyu.Platform.Common.Serialization; @@ -8,22 +9,16 @@ namespace Fuyu.Platform.Server.Databases.Fuyu { public class AccountTable { - static AccountTable() - { - _accountsLock = new object(); - _sessionsLock = new object(); - _pathLock = new object(); - } - public AccountTable() { - _path = "./fuyu/accounts/"; - _accounts = []; - _sessions = []; + _path = new ThreadObject(string.Empty); + _accounts = new ThreadList(); + _sessions = new ThreadDictionary(); } public void Load() { + LoadPath(); LoadAccounts(); LoadSessions(); } @@ -32,20 +27,21 @@ public void Load() // TODO: // * move to a config system // -- seionmoya, 2024/09/02 - private string _path; - private static readonly object _pathLock; + private readonly ThreadObject _path; + + public void LoadPath() + { + SetPath("./fuyu/accounts/"); + } public string GetPath() { - return _path; + return _path.Get(); } public void SetPath(string path) { - lock (_pathLock) - { - _path = path; - } + _path.Set(path); } #endregion @@ -57,20 +53,21 @@ public void SetPath(string path) // TryGetValue() but that uses 'out' which I want to avoid). // -- seionmoya, 2024/09/02 - private readonly List _accounts; - private static readonly object _accountsLock; + private readonly ThreadList _accounts; // TODO: // * separate database from loading functionality // -- seionmoya, 2024/09/02 private void LoadAccounts() { - if (!VFS.DirectoryExists(_path)) + var path = _path.Get(); + + if (!VFS.DirectoryExists(path)) { - VFS.CreateDirectory(_path); + VFS.CreateDirectory(path); } - var files = VFS.GetFiles(_path); + var files = VFS.GetFiles(path); foreach (var filepath in files) { @@ -84,18 +81,18 @@ private void LoadAccounts() public List GetAccounts() { - return _accounts; + return _accounts.ToList(); } public Account GetAccount(string sessionId) { var accountId = GetSession(sessionId); - return _accounts[accountId]; + return _accounts.Get(accountId); } public Account GetAccount(int accountId) { - foreach (var entry in _accounts) + foreach (var entry in _accounts.ToList()) { if (entry.Id == accountId) { @@ -108,49 +105,41 @@ public Account GetAccount(int accountId) public void SetAccount(int accountId, Account account) { - for (var i = 0; i < _accounts.Count; ++i) + var accounts = _accounts.ToList(); + + for (var i = 0; i < accounts.Count; ++i) { - if (_accounts[i].Id == accountId) + if (accounts[i].Id == accountId) { - lock (_accountsLock) - { - _accounts[i] = account; - } - + _accounts.Set(i, account); return; } } - - throw new Exception($"Account with {accountId} does not exist."); } public void AddAccount(Account account) { - foreach (var entry in _accounts) + var accounts = _accounts.ToList(); + + for (var i = 0; i < accounts.Count; ++i) { - if (entry.Id == account.Id) + if (accounts[i].Id == account.Id) { - _accounts.Remove(entry); + _accounts.RemoveAt(i); + break; } } - lock (_accountsLock) - { - _accounts.Add(account); - } + _accounts.Add(account); } public void RemoveAccount(int accountId) { - foreach (var entry in _accounts) + foreach (var entry in _accounts.ToList()) { if (entry.Id == accountId) { - lock (_accountsLock) - { - _accounts.Remove(entry); - } - + _accounts.Remove(entry); return; } } @@ -160,9 +149,8 @@ public void RemoveAccount(int accountId) #endregion #region Sessions -// sessid aid - private readonly Dictionary _sessions; - private static readonly object _sessionsLock; +// sessid aid + private readonly ThreadDictionary _sessions; public void LoadSessions() { @@ -171,58 +159,29 @@ public void LoadSessions() // -- seionmoya, 2024/09/02 } - public int GetSession(string sessionId) + public Dictionary GetSessions() { - if (!_sessions.ContainsKey(sessionId)) - { - throw new Exception($"Session {sessionId} does not exist."); - } + return _sessions.ToDictionary(); + } - return _sessions[sessionId]; + public int GetSession(string sessionId) + { + return _sessions.Get(sessionId); } public void SetSession(string sessionId, int accountId) { - if (!_sessions.ContainsKey(sessionId)) - { - throw new Exception($"Session {sessionId} does not exist."); - } - - lock (_sessionsLock) - { - _sessions[sessionId] = accountId; - } + _sessions.Set(sessionId, accountId); } public void AddSession(string sessionId, int accountId) { - if (_sessions.ContainsKey(sessionId)) - { - lock (_sessionsLock) - { - _sessions[sessionId] = accountId; - } - } - else - { - lock (_sessionsLock) - { - _sessions.Add(sessionId, accountId); - } - } + _sessions.Add(sessionId, accountId); } public void RemoveSession(string sessionId) { - if (!_sessions.ContainsKey(sessionId)) - { - throw new Exception($"Session {sessionId} does not exist."); - } - - lock (_sessionsLock) - { - _sessions.Remove(sessionId); - } + _sessions.Remove(sessionId); } #endregion } diff --git a/Fuyu.Platform.Server/Services/Fuyu/AccountService.cs b/Fuyu.Platform.Server/Services/Fuyu/AccountService.cs index 4b55f8d..8a21950 100644 --- a/Fuyu.Platform.Server/Services/Fuyu/AccountService.cs +++ b/Fuyu.Platform.Server/Services/Fuyu/AccountService.cs @@ -47,20 +47,30 @@ public static string LoginAccount(string username, string password) { var accountId = AccountExists(username, password); - if (accountId != -1) + if (accountId == -1) { - // NOTE: MongoId's are used internally, but EFT's launcher uses - // a different ID system (hwid+timestamp hash). Instead of - // fully mimicking this, I decided to generate a new MongoId - // for each login. - // -- seionmoya, 2024/09/02 - var sessionId = new MongoId().ToString(); - - FuyuDatabase.Accounts.AddSession(sessionId, accountId); - return sessionId.ToString(); + return string.Empty; } - return string.Empty; + var sessions = FuyuDatabase.Accounts.GetSessions(); + + foreach (var kvp in sessions) + { + if (kvp.Value == accountId) + { + // session already exists + return kvp.Key; + } + } + + // NOTE: MongoId's are used internally, but EFT's launcher uses + // a different ID system (hwid+timestamp hash). Instead of + // fully mimicking this, I decided to generate a new MongoId + // for each login. + // -- seionmoya, 2024/09/02 + var sessionId = new MongoId().ToString(); + FuyuDatabase.Accounts.AddSession(sessionId, accountId); + return sessionId.ToString(); } private static int GetNewAccountId() diff --git a/Fuyu.Tests/EndToEnd/BackendTest.cs b/Fuyu.Tests/EndToEnd/BackendTest.cs index 476e6a4..9c69f99 100644 --- a/Fuyu.Tests/EndToEnd/BackendTest.cs +++ b/Fuyu.Tests/EndToEnd/BackendTest.cs @@ -9,15 +9,12 @@ using Fuyu.Platform.Server; using Fuyu.Platform.Server.Databases; using Fuyu.Platform.Server.Services.Fuyu; -using Fuyu.Platform.Server.Behaviours.EFT; -using Fuyu.Platform.Common.Models.Fuyu.Requests; namespace Fuyu.Tests.EndToEnd { [TestClass] public class BackendTest { - private static HttpClient _fuyuClient; private static HttpClient _eftMainClient; [AssemblyInitialize] @@ -36,53 +33,9 @@ public static void AssemblyInitialize(TestContext testContext) var sessionId = AccountService.LoginAccount("test-username", "test-password"); // create request clients - _fuyuClient = new HttpClient("http://localhost:8000"); _eftMainClient = new HttpClient("http://localhost:8001", sessionId); } - [TestMethod] - public async Task TestAccountRegister() - { - // get request data - var request = new AccountRegisterRequest() - { - Username = "senko-san", - Password = "test-password", - Edition = "unheard" - }; - - // get request body - var json = Json.Stringify(request); - var body = Encoding.UTF8.GetBytes(json); - - // get response - var data = await _fuyuClient.PostAsync("/account/register", body); - var result = Encoding.UTF8.GetString(data); - - Assert.IsFalse(string.IsNullOrEmpty(result)); - } - - [TestMethod] - public async Task TestLoginCreate() - { - // get request data - var request = new AccountLoginRequest() - { - Username = "senko-san", - Password = "test-password" - }; - - // get request body - var json = Json.Stringify(request); - var body = Encoding.UTF8.GetBytes(json); - - // get response - var data = await _fuyuClient.PostAsync("/account/login", body); - var result = Encoding.UTF8.GetString(data); - - Assert.IsFalse(string.IsNullOrEmpty(result)); - } - [TestMethod] public async Task TestClientAccountCustomization() {