From 9b9010ab8e4c857be574b76a91ee7dcef9e27dc8 Mon Sep 17 00:00:00 2001 From: defnotken Date: Mon, 5 Jan 2026 18:57:18 -0600 Subject: [PATCH 1/3] Defenses? --- .../PlayerData/Factories/PlayerDataFactory.cs | 95 +++-- LightlessSync/Services/XivDataAnalyzer.cs | 366 ++++++++++++------ 2 files changed, 324 insertions(+), 137 deletions(-) diff --git a/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs b/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs index 5e1d99e..9141a9b 100644 --- a/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs +++ b/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs @@ -566,8 +566,21 @@ public class PlayerDataFactory await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false); try { - papIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash), ct) - .ConfigureAwait(false); + try + { + papIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash, persistToConfig: false), ct) + .ConfigureAwait(false); + } + catch (SEHException ex) + { + _logger.LogError(ex, "SEH exception while parsing PAP file (hash={hash}, path={path}). Error code: 0x{code:X}. Skipping this animation.", hash, papPathSummary, ex.ErrorCode); + continue; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error parsing PAP file (hash={hash}, path={path}). Skipping this animation.", hash, papPathSummary); + continue; + } } finally { @@ -577,36 +590,68 @@ public class PlayerDataFactory if (papIndices == null || papIndices.Count == 0) continue; - if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105)) + bool hasValidIndices = false; + try + { + hasValidIndices = papIndices.All(k => k.Value != null && k.Value.DefaultIfEmpty().Max() <= 105); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error validating bone indices for PAP (hash={hash}, path={path}). Skipping.", hash, papPathSummary); + continue; + } + + if (hasValidIndices) continue; if (_logger.IsEnabled(LogLevel.Debug)) { - var papBuckets = papIndices - .Select(kvp => new - { - Raw = kvp.Key, - Key = XivDataAnalyzer.CanonicalizeSkeletonKey(kvp.Key), - Indices = kvp.Value - }) - .Where(x => x.Indices is { Count: > 0 }) - .GroupBy(x => string.IsNullOrEmpty(x.Key) ? x.Raw : x.Key!, StringComparer.OrdinalIgnoreCase) - .Select(grp => - { - var all = grp.SelectMany(v => v.Indices).ToList(); - var min = all.Count > 0 ? all.Min() : 0; - var max = all.Count > 0 ? all.Max() : 0; - var raws = string.Join(',', grp.Select(v => v.Raw).Distinct(StringComparer.OrdinalIgnoreCase)); - return $"{grp.Key}(min={min},max={max},raw=[{raws}])"; - }) - .ToList(); + try + { + var papBuckets = papIndices + .Where(kvp => kvp.Value is { Count: > 0 }) + .Select(kvp => new + { + Raw = kvp.Key, + Key = XivDataAnalyzer.CanonicalizeSkeletonKey(kvp.Key), + Indices = kvp.Value + }) + .Where(x => x.Indices is { Count: > 0 }) + .GroupBy(x => string.IsNullOrEmpty(x.Key) ? x.Raw : x.Key!, StringComparer.OrdinalIgnoreCase) + .Select(grp => + { + var all = grp.SelectMany(v => v.Indices).ToList(); + var min = all.Count > 0 ? all.Min() : 0; + var max = all.Count > 0 ? all.Max() : 0; + var raws = string.Join(',', grp.Select(v => v.Raw).Distinct(StringComparer.OrdinalIgnoreCase)); + return $"{grp.Key}(min={min},max={max},raw=[{raws}])"; + }) + .ToList(); - _logger.LogDebug("SEND pap buckets for hash={hash}: {b}", - hash, - string.Join(" | ", papBuckets)); + _logger.LogDebug("SEND pap buckets for hash={hash}: {b}", + hash, + string.Join(" | ", papBuckets)); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Error logging PAP bucket details for hash={hash}", hash); + } } - if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out var reason)) + bool isCompatible = false; + string reason = string.Empty; + try + { + isCompatible = XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out reason); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error checking PAP compatibility for hash={hash}, path={path}. Treating as incompatible.", hash, papPathSummary); + reason = $"Exception during compatibility check: {ex.Message}"; + isCompatible = false; + } + + if (isCompatible) continue; noValidationFailed++; diff --git a/LightlessSync/Services/XivDataAnalyzer.cs b/LightlessSync/Services/XivDataAnalyzer.cs index 997df16..cd3b20c 100644 --- a/LightlessSync/Services/XivDataAnalyzer.cs +++ b/LightlessSync/Services/XivDataAnalyzer.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.Havok.Common.Serialize.Resource; using FFXIVClientStructs.Havok.Animation; using FFXIVClientStructs.Havok.Common.Base.Types; using FFXIVClientStructs.Havok.Common.Serialize.Util; @@ -145,156 +146,297 @@ public sealed partial class XivDataAnalyzer using var reader = new BinaryReader(fs); // PAP header (mostly from vfxeditor) - _ = reader.ReadInt32(); // ignore - _ = reader.ReadInt32(); // ignore - _ = reader.ReadInt16(); // num animations - _ = reader.ReadInt16(); // modelid - - var type = reader.ReadByte(); // type - if (type != 0) - return null; // not human - - _ = reader.ReadByte(); // variant - _ = reader.ReadInt32(); // ignore - - var havokPosition = reader.ReadInt32(); - var footerPosition = reader.ReadInt32(); - - // sanity checks - if (havokPosition <= 0 || footerPosition <= havokPosition || footerPosition > fs.Length) - return null; - - var havokDataSizeLong = (long)footerPosition - havokPosition; - if (havokDataSizeLong <= 8 || havokDataSizeLong > int.MaxValue) - return null; - - var havokDataSize = (int)havokDataSizeLong; - - reader.BaseStream.Position = havokPosition; - var havokData = reader.ReadBytes(havokDataSize); - if (havokData.Length <= 8) - return null; - - var tempSets = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - var tempHavokDataPath = Path.Combine(Path.GetTempPath(), $"lightless_{Guid.NewGuid():N}.hkx"); - IntPtr tempHavokDataPathAnsi = IntPtr.Zero; - try { - File.WriteAllBytes(tempHavokDataPath, havokData); + _ = reader.ReadInt32(); // ignore + _ = reader.ReadInt32(); // ignore + var numAnimations = reader.ReadInt16(); // num animations + var modelId = reader.ReadInt16(); // modelid - if (!File.Exists(tempHavokDataPath)) + if (numAnimations < 0 || numAnimations > 1000) { - _logger.LogTrace("Temporary havok file did not exist when attempting to load: {path}", tempHavokDataPath); + _logger.LogWarning("PAP file {hash} has invalid animation count {count}, skipping", hash, numAnimations); return null; } - tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath); + var type = reader.ReadByte(); // type + if (type != 0) + return null; // not human - var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1]; - loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry(); - loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry(); - loadoptions->Flags = new hkFlags - { - Storage = (int)hkSerializeUtil.LoadOptionBits.Default - }; + _ = reader.ReadByte(); // variant + _ = reader.ReadInt32(); // ignore - var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions); - if (resource == null) + var havokPosition = reader.ReadInt32(); + var footerPosition = reader.ReadInt32(); + + if (havokPosition <= 0 || footerPosition <= havokPosition || + footerPosition > fs.Length || havokPosition >= fs.Length) { - _logger.LogWarning("Havok resource was null after loading from {path}", tempHavokDataPath); + _logger.LogWarning("PAP file {hash} has invalid offsets (havok={havok}, footer={footer}, length={length})", + hash, havokPosition, footerPosition, fs.Length); return null; } - var rootLevelName = @"hkRootLevelContainer"u8; - fixed (byte* n1 = rootLevelName) + var havokDataSizeLong = (long)footerPosition - havokPosition; + if (havokDataSizeLong <= 8 || havokDataSizeLong > int.MaxValue) { - var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry()); - if (container == null) - return null; + _logger.LogWarning("PAP file {hash} has invalid Havok data size {size}", hash, havokDataSizeLong); + return null; + } - var animationName = @"hkaAnimationContainer"u8; - fixed (byte* n2 = animationName) + var havokDataSize = (int)havokDataSizeLong; + + reader.BaseStream.Position = havokPosition; + + var havokData = new byte[havokDataSize]; + var bytesRead = reader.Read(havokData, 0, havokDataSize); + if (bytesRead != havokDataSize) + { + _logger.LogWarning("PAP file {hash}: Expected to read {expected} bytes but got {actual}", + hash, havokDataSize, bytesRead); + return null; + } + + if (havokData.Length < 8) + return null; + + var tempSets = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + var tempFileName = $"lightless_pap_{Guid.NewGuid():N}_{hash.Substring(0, Math.Min(8, hash.Length))}.hkx"; + var tempHavokDataPath = Path.Combine(Path.GetTempPath(), tempFileName); + IntPtr tempHavokDataPathAnsi = IntPtr.Zero; + + try + { + var tempDir = Path.GetDirectoryName(tempHavokDataPath); + if (!Directory.Exists(tempDir)) { - var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null); - if (animContainer == null) - return null; + _logger.LogWarning("Temp directory {dir} doesn't exist", tempDir); + return null; + } - for (int i = 0; i < animContainer->Bindings.Length; i++) + File.WriteAllBytes(tempHavokDataPath, havokData); + + if (!File.Exists(tempHavokDataPath)) + { + _logger.LogWarning("Temporary havok file was not created at {path}", tempHavokDataPath); + return null; + } + + var writtenFileInfo = new FileInfo(tempHavokDataPath); + if (writtenFileInfo.Length != havokData.Length) + { + _logger.LogWarning("Written temp file size mismatch: expected {expected}, got {actual}", + havokData.Length, writtenFileInfo.Length); + File.Delete(tempHavokDataPath); + return null; + } + + tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath); + + var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1]; + loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry(); + loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry(); + loadoptions->Flags = new hkFlags + { + Storage = (int)hkSerializeUtil.LoadOptionBits.Default + }; + + hkResource* resource = null; + try + { + resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions); + } + catch (SEHException ex) + { + _logger.LogError(ex, "SEH exception loading Havok file from {path} (hash={hash}). Native error code: 0x{code:X}", + tempHavokDataPath, hash, ex.ErrorCode); + return null; + } + + if (resource == null) + { + _logger.LogDebug("Havok resource was null after loading from {path} (hash={hash})", tempHavokDataPath, hash); + return null; + } + + if ((nint)resource == nint.Zero || !IsValidPointer((IntPtr)resource)) + { + _logger.LogDebug("Havok resource pointer is invalid (hash={hash})", hash); + return null; + } + + var rootLevelName = @"hkRootLevelContainer"u8; + fixed (byte* n1 = rootLevelName) + { + var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry()); + if (container == null) { - var binding = animContainer->Bindings[i].ptr; - if (binding == null) - continue; + _logger.LogDebug("hkRootLevelContainer is null (hash={hash})", hash); + return null; + } - var rawSkel = binding->OriginalSkeletonName.String; - var skeletonKey = CanonicalizeSkeletonKey(rawSkel); - if (string.IsNullOrEmpty(skeletonKey)) - continue; + if ((nint)container == nint.Zero || !IsValidPointer((IntPtr)container)) + { + _logger.LogDebug("hkRootLevelContainer pointer is invalid (hash={hash})", hash); + return null; + } - var boneTransform = binding->TransformTrackToBoneIndices; - if (boneTransform.Length <= 0) - continue; - - if (!tempSets.TryGetValue(skeletonKey, out var set)) + var animationName = @"hkaAnimationContainer"u8; + fixed (byte* n2 = animationName) + { + var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null); + if (animContainer == null) { - set = []; - tempSets[skeletonKey] = set; + _logger.LogDebug("hkaAnimationContainer is null (hash={hash})", hash); + return null; } - for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++) + if ((nint)animContainer == nint.Zero || !IsValidPointer((IntPtr)animContainer)) { - var v = boneTransform[boneIdx]; - if (v < 0) continue; - set.Add((ushort)v); + _logger.LogDebug("hkaAnimationContainer pointer is invalid (hash={hash})", hash); + return null; + } + + if (animContainer->Bindings.Length < 0 || animContainer->Bindings.Length > 10000) + { + _logger.LogDebug("Invalid bindings count {count} (hash={hash})", animContainer->Bindings.Length, hash); + return null; + } + + for (int i = 0; i < animContainer->Bindings.Length; i++) + { + var binding = animContainer->Bindings[i].ptr; + if (binding == null) + continue; + + if ((nint)binding == nint.Zero || !IsValidPointer((IntPtr)binding)) + { + _logger.LogDebug("Skipping invalid binding at index {index} (hash={hash})", i, hash); + continue; + } + + var rawSkel = binding->OriginalSkeletonName.String; + var skeletonKey = CanonicalizeSkeletonKey(rawSkel); + if (string.IsNullOrEmpty(skeletonKey)) + continue; + + var boneTransform = binding->TransformTrackToBoneIndices; + if (boneTransform.Length <= 0 || boneTransform.Length > 10000) + { + _logger.LogDebug("Invalid bone transform length {length} for skeleton {skel} (hash={hash})", + boneTransform.Length, skeletonKey, hash); + continue; + } + + if (!tempSets.TryGetValue(skeletonKey, out var set)) + { + set = []; + tempSets[skeletonKey] = set; + } + + for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++) + { + var v = boneTransform[boneIdx]; + if (v < 0 || v > ushort.MaxValue) + continue; + set.Add((ushort)v); + } } } } } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath); - return null; - } - finally - { - if (tempHavokDataPathAnsi != IntPtr.Zero) - Marshal.FreeHGlobal(tempHavokDataPathAnsi); - - try + catch (SEHException ex) { - if (File.Exists(tempHavokDataPath)) - File.Delete(tempHavokDataPath); + _logger.LogError(ex, "SEH exception processing PAP file {hash} from {path}. Error code: 0x{code:X}", + hash, tempHavokDataPath, ex.ErrorCode); + return null; } catch (Exception ex) { - _logger.LogTrace(ex, "Could not delete temporary havok file: {path}", tempHavokDataPath); + _logger.LogError(ex, "Managed exception loading havok file {hash} from {path}", hash, tempHavokDataPath); + return null; } + finally + { + if (tempHavokDataPathAnsi != IntPtr.Zero) + Marshal.FreeHGlobal(tempHavokDataPathAnsi); + + int retryCount = 3; + while (retryCount > 0 && File.Exists(tempHavokDataPath)) + { + try + { + File.Delete(tempHavokDataPath); + break; + } + catch (IOException ex) + { + retryCount--; + if (retryCount == 0) + { + _logger.LogDebug(ex, "Failed to delete temporary havok file after retries: {path}", tempHavokDataPath); + } + else + { + Thread.Sleep(50); + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Unexpected error deleting temporary havok file: {path}", tempHavokDataPath); + break; + } + } + } + + if (tempSets.Count == 0) + { + _logger.LogDebug("No bone sets found in PAP file (hash={hash})", hash); + return null; + } + + var output = new Dictionary>(tempSets.Count, StringComparer.OrdinalIgnoreCase); + foreach (var (key, set) in tempSets) + { + if (set.Count == 0) continue; + + var list = set.ToList(); + list.Sort(); + output[key] = list; + } + + if (output.Count == 0) + return null; + + _configService.Current.BonesDictionary[hash] = output; + + if (persistToConfig) + _configService.Save(); + + return output; } - - if (tempSets.Count == 0) - return null; - - var output = new Dictionary>(tempSets.Count, StringComparer.OrdinalIgnoreCase); - foreach (var (key, set) in tempSets) + catch (Exception ex) { - if (set.Count == 0) continue; - - var list = set.ToList(); - list.Sort(); - output[key] = list; - } - - if (output.Count == 0) + _logger.LogError(ex, "Outer exception reading PAP file (hash={hash})", hash); return null; + } + } - _configService.Current.BonesDictionary[hash] = output; + private static bool IsValidPointer(IntPtr ptr) + { + if (ptr == IntPtr.Zero) + return false; - if (persistToConfig) - _configService.Save(); - - return output; + try + { + _ = Marshal.ReadByte(ptr); + return true; + } + catch + { + return false; + } } From ce28799db388796ff50a0b55da26eaafab5fd260 Mon Sep 17 00:00:00 2001 From: defnotken Date: Mon, 5 Jan 2026 20:46:14 -0600 Subject: [PATCH 2/3] More checks for animations and bones. --- LightlessSync/Services/XivDataAnalyzer.cs | 51 ++++++++++++++++++++--- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/LightlessSync/Services/XivDataAnalyzer.cs b/LightlessSync/Services/XivDataAnalyzer.cs index cd3b20c..65d9346 100644 --- a/LightlessSync/Services/XivDataAnalyzer.cs +++ b/LightlessSync/Services/XivDataAnalyzer.cs @@ -202,7 +202,7 @@ public sealed partial class XivDataAnalyzer var tempSets = new Dictionary>(StringComparer.OrdinalIgnoreCase); - var tempFileName = $"lightless_pap_{Guid.NewGuid():N}_{hash.Substring(0, Math.Min(8, hash.Length))}.hkx"; + var tempFileName = $"lightless_pap_{Guid.NewGuid():N}_{hash[..Math.Min(8, hash.Length)]}.hkx"; var tempHavokDataPath = Path.Combine(Path.GetTempPath(), tempFileName); IntPtr tempHavokDataPathAnsi = IntPtr.Zero; @@ -215,7 +215,16 @@ public sealed partial class XivDataAnalyzer return null; } - File.WriteAllBytes(tempHavokDataPath, havokData); + // Write the file with explicit error handling + try + { + File.WriteAllBytes(tempHavokDataPath, havokData); + } + catch (Exception writeEx) + { + _logger.LogError(writeEx, "Failed to write temporary Havok file to {path}", tempHavokDataPath); + return null; + } if (!File.Exists(tempHavokDataPath)) { @@ -228,7 +237,26 @@ public sealed partial class XivDataAnalyzer { _logger.LogWarning("Written temp file size mismatch: expected {expected}, got {actual}", havokData.Length, writtenFileInfo.Length); - File.Delete(tempHavokDataPath); + try { File.Delete(tempHavokDataPath); } catch { } + return null; + } + + Thread.Sleep(10); // stabilize file system + + try + { + using var testStream = File.OpenRead(tempHavokDataPath); + if (testStream.Length != havokData.Length) + { + _logger.LogWarning("File verification failed: length mismatch after write"); + try { File.Delete(tempHavokDataPath); } catch { } + return null; + } + } + catch (Exception readEx) + { + _logger.LogError(readEx, "Cannot read back temporary file at {path}", tempHavokDataPath); + try { File.Delete(tempHavokDataPath); } catch { } return null; } @@ -245,18 +273,31 @@ public sealed partial class XivDataAnalyzer hkResource* resource = null; try { + if (tempHavokDataPathAnsi == IntPtr.Zero) + { + _logger.LogError("Failed to allocate ANSI string for path"); + return null; + } + resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions); } catch (SEHException ex) { - _logger.LogError(ex, "SEH exception loading Havok file from {path} (hash={hash}). Native error code: 0x{code:X}", + _logger.LogError(ex, "SEH exception loading Havok file from {path} (hash={hash}). Native error code: 0x{code:X}. This may indicate a corrupted PAP file or incompatible Havok format.", tempHavokDataPath, hash, ex.ErrorCode); return null; } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected exception loading Havok file from {path} (hash={hash})", + tempHavokDataPath, hash); + return null; + } if (resource == null) { - _logger.LogDebug("Havok resource was null after loading from {path} (hash={hash})", tempHavokDataPath, hash); + _logger.LogDebug("Havok resource was null after loading from {path} (hash={hash}). File may be corrupted or in an unsupported format.", + tempHavokDataPath, hash); return null; } From 9167bb1afd30fa68fca335ff9ea9fd82261e537d Mon Sep 17 00:00:00 2001 From: Tsubasa Date: Tue, 6 Jan 2026 12:51:29 +0000 Subject: [PATCH 3/3] i18n init (#135) shouldnt break anything? Co-authored-by: Tsubasahane Reviewed-on: https://git.lightless-sync.org/Lightless-Sync/LightlessClient/pulls/135 Co-authored-by: Tsubasa Co-committed-by: Tsubasa --- LightlessAPI | 2 +- LightlessSync/LightlessSync.csproj | 11 +- .../LightlessSync.csproj.DotSettings | 3 + LightlessSync/Localization/Strings.cs | 44 ----- LightlessSync/Localization/de.json | 46 ----- LightlessSync/Localization/fr.json | 46 ----- .../Resources/LocalizationExtention.cs | 9 + LightlessSync/Resources/Resources.Designer.cs | 170 ++++++++++++++++++ LightlessSync/Resources/Resources.de.resx | 47 +++++ LightlessSync/Resources/Resources.fr.resx | 47 +++++ LightlessSync/Resources/Resources.resx | 57 ++++++ LightlessSync/Resources/Resources.zh.resx | 20 +++ LightlessSync/UI/IntroUI.cs | 48 ++--- LightlessSync/UI/SettingsUi.cs | 2 +- LightlessSync/UI/UISharedService.cs | 7 - 15 files changed, 383 insertions(+), 176 deletions(-) create mode 100644 LightlessSync/LightlessSync.csproj.DotSettings delete mode 100644 LightlessSync/Localization/Strings.cs delete mode 100644 LightlessSync/Localization/de.json delete mode 100644 LightlessSync/Localization/fr.json create mode 100644 LightlessSync/Resources/LocalizationExtention.cs create mode 100644 LightlessSync/Resources/Resources.Designer.cs create mode 100644 LightlessSync/Resources/Resources.de.resx create mode 100644 LightlessSync/Resources/Resources.fr.resx create mode 100644 LightlessSync/Resources/Resources.resx create mode 100644 LightlessSync/Resources/Resources.zh.resx diff --git a/LightlessAPI b/LightlessAPI index 4ecd537..c3caa7e 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 4ecd5375e63082f44b841bcba38d5dd3f4a2a79b +Subproject commit c3caa7e25cf17fd52c4765bf051ec37c8fd92082 diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 938d413..b0b7b8e 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -24,6 +24,15 @@ + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + True + True + Resources.resx + @@ -68,8 +77,6 @@ - - diff --git a/LightlessSync/LightlessSync.csproj.DotSettings b/LightlessSync/LightlessSync.csproj.DotSettings new file mode 100644 index 0000000..71a1e0f --- /dev/null +++ b/LightlessSync/LightlessSync.csproj.DotSettings @@ -0,0 +1,3 @@ + + Yes + Pessimistic \ No newline at end of file diff --git a/LightlessSync/Localization/Strings.cs b/LightlessSync/Localization/Strings.cs deleted file mode 100644 index 56f938b..0000000 --- a/LightlessSync/Localization/Strings.cs +++ /dev/null @@ -1,44 +0,0 @@ -using CheapLoc; - -namespace LightlessSync.Localization; - -public static class Strings -{ - public static ToSStrings ToS { get; set; } = new(); - - public class ToSStrings - { - public readonly string AgreeLabel = Loc.Localize("AgreeLabel", "I agree"); - public readonly string AgreementLabel = Loc.Localize("AgreementLabel", "Agreement of Usage of Service"); - public readonly string ButtonWillBeAvailableIn = Loc.Localize("ButtonWillBeAvailableIn", "'I agree' button will be available in"); - public readonly string LanguageLabel = Loc.Localize("LanguageLabel", "Language"); - - public readonly string Paragraph1 = Loc.Localize("Paragraph1", - "All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. " + - "The plugin will exclusively upload the necessary mod files and not the whole mod."); - - public readonly string Paragraph2 = Loc.Localize("Paragraph2", - "If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. " + - "Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. " + - "Files present on the service that already represent your active mod files will not be uploaded again."); - - public readonly string Paragraph3 = Loc.Localize("Paragraph3", - "The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. " + - "Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. " + - "Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod."); - - public readonly string Paragraph4 = Loc.Localize("Paragraph4", - "The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone."); - - public readonly string Paragraph5 = Loc.Localize("Paragraph5", - "Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. " + - "After a period of not being used, the mod files will be automatically deleted. " + - "You will also be able to wipe all the files you have personally uploaded on request. " + - "The service holds no information about which mod files belong to which mod."); - - public readonly string Paragraph6 = Loc.Localize("Paragraph6", - "This service is provided as-is. In case of abuse join the Lightless Sync Discord."); - - public readonly string ReadLabel = Loc.Localize("ReadLabel", "READ THIS CAREFULLY"); - } -} \ No newline at end of file diff --git a/LightlessSync/Localization/de.json b/LightlessSync/Localization/de.json deleted file mode 100644 index b2552cb..0000000 --- a/LightlessSync/Localization/de.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "LanguageLabel": { - "message": "Language", - "description": "ToSStrings..ctor" - }, - "AgreementLabel": { - "message": "Nutzungsbedingungen", - "description": "ToSStrings..ctor" - }, - "ReadLabel": { - "message": "BITTE LIES DIES SORGFÄLTIG", - "description": "ToSStrings..ctor" - }, - "Paragraph1": { - "message": "Alle Moddateien, die aktuell auf deinem Charakter aktiv sind und dein Charakterzustand werden automatisch zu dem Service, an dem du dich registriert hast, hochgeladen. Das Plugin wird ausschließlich die nötigen Moddateien hochladen und nicht die gesamte Modifikation.", - "description": "ToSStrings..ctor" - }, - "Paragraph2": { - "message": "Falls du mit einer getakteten Internetverbindung verbunden bist, können durch den Datentransfer von Hoch- und Runtergeladenen Moddateien höhere Kosten entstehen. Moddateien werden beim Hoch- und Runterladen komprimiert um Bandbreite zu sparen. Durch unterschiedliche Hoch- und Runterladgeschwindigkeiten ist es möglich, dass Änderungen an Charakteren nicht sofort sichtbar sind. Dateien die bereits auf dem Service existieren, werden nicht nochmals hochgeladen.", - "description": "ToSStrings..ctor" - }, - "Paragraph3": { - "message": "Die Moddateien die du hochlädst sind vertraulich und werden nicht mit anderen Nutzern geteilt, die nicht die exakt selben Dateien anfordern. Bitte überlege dir sorgfältig mit wem du deinen Identifikationscode teilst, da es unvermeidlich ist, dass die andere Person deine Moddateien erhält und lokal zwischenspeichert. Lokal zwischengespeicherte Dateien haben willkürrliche Namen um vor Versuchen abzuschrecken die originalen Moddateien aus diesen wiederherzustellen.", - "description": "ToSStrings..ctor" - }, - "Paragraph4": { - "message": "Der Ersteller des Plugins hat sein Bestes getan, um deine Sicherheit zu gewährleisten. Es gibt jedoch keine Garantie für 100%ige Sicherheit. Teile deinen Identifikationscode nicht blind mit jedem.", - "description": "ToSStrings..ctor" - }, - "Paragraph5": { - "message": "Moddateien, die auf dem Service gespeichert sind, verbleiben auf dem Service, solange es Anforderungen für diese Dateien gibt. Nach einer Zeitspanne in der die Dateien nicht verwendet wurden, werden diese automatisch gelöscht. Du hast auch die Möglichkeit manuell alle Dateien auf dem Service zu löschen. Der Service hat keine Informationen welche Moddateien zu welcher Modifikation gehören.", - "description": "ToSStrings..ctor" - }, - "Paragraph6": { - "message": "Dieser Dienst wird ohne Gewähr angeboten. Im Falle eines Missbrauchs tretet dem Lightless Sync Discord bei.", - "description": "ToSStrings..ctor" - }, - "AgreeLabel": { - "message": "Ich Stimme zu", - "description": "ToSStrings..ctor" - }, - "ButtonWillBeAvailableIn": { - "message": "\"Ich stimme zu\" Knopf verfügbar in", - "description": "ToSStrings..ctor" - } -} \ No newline at end of file diff --git a/LightlessSync/Localization/fr.json b/LightlessSync/Localization/fr.json deleted file mode 100644 index 7a2f328..0000000 --- a/LightlessSync/Localization/fr.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "LanguageLabel": { - "message": "Language", - "description": "ToSStrings..ctor" - }, - "AgreementLabel": { - "message": "Conditions d'Utilisation", - "description": "ToSStrings..ctor" - }, - "ReadLabel": { - "message": "LISEZ CES INFORMATIONS ATTENTIVEMENT", - "description": "ToSStrings..ctor" - }, - "Paragraph1": { - "message": "Tous les fichiers moddés actuellement en cours d'utilisation ainsi que le statut actuel de votre personnage vont être mix en ligne via le service sur lequel vous vous êtes automatiquement enregistré. Seuls les fichiers nécessaires seront téléversés par le plugin et non pas le mod en entier.", - "description": "ToSStrings..ctor" - }, - "Paragraph2": { - "message": "Si le débit de votre connexion internet est limité, le téléchargement et téléversement d'un grand nombre de fichiers peut entraîner des coûts supplémentaires. Les fichiers seront compressés au chargement et versement pour réduire l'impact sur votre bande passants. Selon la rapidité de vos téléchargements et téléversements, les changements ne seront peut-être pas visibles instantanément sur les personnages. Les fichiers déja présents sur le service qui correspondent à ceux de vos mods en cours d'utilisation ne seront pas remis en ligne.", - "description": "ToSStrings..ctor" - }, - "Paragraph3": { - "message": "Les fichiers que vous allez partager sont confidentiels et ne seront envoyés qu'aux utilisateurs qui feront une requête exacte de ceux-çi. Nous vous demandons de (re)considérer qui sera synchronisé avec vous, puisqu'ils recevront et stockeront inévitablement en local les fichiers nécéssaires utilisés à cet instant. Les noms des fichiers stockés localement sont changés de manière arbitraire afin de décourager toute tentative de réplication des originaux.", - "description": "ToSStrings..ctor" - }, - "Paragraph4": { - "message": "Le créateur de ce plugin a tenté de sécuriser l'application du mieux possible. Cependant, il ne peut pas garantir une protection 100% infaillible. Pour votre sécurité, ne vous synchronisez pas aveuglément et avec n'importe qui.", - "description": "ToSStrings..ctor" - }, - "Paragraph5": { - "message": "Les fichiers sauvegardés sur le service resteront en ligne tant que des utilisateurs en feront usage. Ils seront effacés automatiquement après une certaine période d'inactivité. Vous pouvez également demander l'effacement de tous les fichiers que vous avez mis en ligne vous-même. Le service en soi ne contient aucune information pouvant identifier quel fichier appartient à quel mod.", - "description": "ToSStrings..ctor" - }, - "Paragraph6": { - "message": "Ce service et ses composants vous sont fournis en l'état. En cas d'abus rejoindre le serveur Discord Lightless Sync.", - "description": "ToSStrings..ctor" - }, - "AgreeLabel": { - "message": "J'accept", - "description": "ToSStrings..ctor" - }, - "ButtonWillBeAvailableIn": { - "message": "Bouton \"J'accept\" disposible dans", - "description": "ToSStrings..ctor" - } -} \ No newline at end of file diff --git a/LightlessSync/Resources/LocalizationExtention.cs b/LightlessSync/Resources/LocalizationExtention.cs new file mode 100644 index 0000000..58fd6df --- /dev/null +++ b/LightlessSync/Resources/LocalizationExtention.cs @@ -0,0 +1,9 @@ +namespace LightlessSync.Resources; + +public static class LocalizationExtensions +{ + public static string F(this string mask, params object[] args) + { + return string.Format(mask, args); + } +} \ No newline at end of file diff --git a/LightlessSync/Resources/Resources.Designer.cs b/LightlessSync/Resources/Resources.Designer.cs new file mode 100644 index 0000000..8a909b1 --- /dev/null +++ b/LightlessSync/Resources/Resources.Designer.cs @@ -0,0 +1,170 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace LightlessSync.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", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public 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)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("LightlessSync.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)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to I agree. + /// + public static string ToSStrings_AgreeLabel { + get { + return ResourceManager.GetString("ToSStrings_AgreeLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Agreement of Usage of Service. + /// + public static string ToSStrings_AgreementLabel { + get { + return ResourceManager.GetString("ToSStrings_AgreementLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 'I agree' button will be available in. + /// + public static string ToSStrings_ButtonWillBeAvailableIn { + get { + return ResourceManager.GetString("ToSStrings_ButtonWillBeAvailableIn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Language. + /// + public static string ToSStrings_LanguageLabel { + get { + return ResourceManager.GetString("ToSStrings_LanguageLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. The plugin will exclusively upload the necessary mod files and not the whole mod.. + /// + public static string ToSStrings_Paragraph1 { + get { + return ResourceManager.GetString("ToSStrings_Paragraph1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. Files present on the service that already represent your active mod files will not be uploaded again.. + /// + public static string ToSStrings_Paragraph2 { + get { + return ResourceManager.GetString("ToSStrings_Paragraph2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod.. + /// + public static string ToSStrings_Paragraph3 { + get { + return ResourceManager.GetString("ToSStrings_Paragraph3", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone.. + /// + public static string ToSStrings_Paragraph4 { + get { + return ResourceManager.GetString("ToSStrings_Paragraph4", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted. You will also be able to wipe all the files you have personally uploaded on request. The service holds no information about which mod files belong to which mod.. + /// + public static string ToSStrings_Paragraph5 { + get { + return ResourceManager.GetString("ToSStrings_Paragraph5", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This service is provided as-is. In case of abuse join the Lightless Sync Discord.. + /// + public static string ToSStrings_Paragraph6 { + get { + return ResourceManager.GetString("ToSStrings_Paragraph6", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to READ THIS CAREFULLY. + /// + public static string ToSStrings_ReadLabel { + get { + return ResourceManager.GetString("ToSStrings_ReadLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Users Online. + /// + public static string Users_Online { + get { + return ResourceManager.GetString("Users_Online", resourceCulture); + } + } + } +} diff --git a/LightlessSync/Resources/Resources.de.resx b/LightlessSync/Resources/Resources.de.resx new file mode 100644 index 0000000..ac3a1f5 --- /dev/null +++ b/LightlessSync/Resources/Resources.de.resx @@ -0,0 +1,47 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Language + + + Nutzungsbedingungen + + + BITTE LIES DIES SORGFÄLTIG + + + Alle Moddateien, die aktuell auf deinem Charakter aktiv sind und dein Charakterzustand werden automatisch zu dem Service, an dem du dich registriert hast, hochgeladen. Das Plugin wird ausschließlich die nötigen Moddateien hochladen und nicht die gesamte Modifikation. + + + Falls du mit einer getakteten Internetverbindung verbunden bist, können durch den Datentransfer von Hoch- und Runtergeladenen Moddateien höhere Kosten entstehen. Moddateien werden beim Hoch- und Runterladen komprimiert um Bandbreite zu sparen. Durch unterschiedliche Hoch- und Runterladgeschwindigkeiten ist es möglich, dass Änderungen an Charakteren nicht sofort sichtbar sind. Dateien die bereits auf dem Service existieren, werden nicht nochmals hochgeladen. + + + Die Moddateien die du hochlädst sind vertraulich und werden nicht mit anderen Nutzern geteilt, die nicht die exakt selben Dateien anfordern. Bitte überlege dir sorgfältig mit wem du deinen Identifikationscode teilst, da es unvermeidlich ist, dass die andere Person deine Moddateien erhält und lokal zwischenspeichert. Lokal zwischengespeicherte Dateien haben willkürrliche Namen um vor Versuchen abzuschrecken die originalen Moddateien aus diesen wiederherzustellen. + + + Der Ersteller des Plugins hat sein Bestes getan, um deine Sicherheit zu gewährleisten. Es gibt jedoch keine Garantie für 100%ige Sicherheit. Teile deinen Identifikationscode nicht blind mit jedem. + + + Moddateien, die auf dem Service gespeichert sind, verbleiben auf dem Service, solange es Anforderungen für diese Dateien gibt. Nach einer Zeitspanne in der die Dateien nicht verwendet wurden, werden diese automatisch gelöscht. Du hast auch die Möglichkeit manuell alle Dateien auf dem Service zu löschen. Der Service hat keine Informationen welche Moddateien zu welcher Modifikation gehören. + + + Dieser Dienst wird ohne Gewähr angeboten. Im Falle eines Missbrauchs tretet dem Lightless Sync Discord bei. + + + Ich Stimme zu + + + "Ich stimme zu" Knopf verfügbar in + + \ No newline at end of file diff --git a/LightlessSync/Resources/Resources.fr.resx b/LightlessSync/Resources/Resources.fr.resx new file mode 100644 index 0000000..ab3e580 --- /dev/null +++ b/LightlessSync/Resources/Resources.fr.resx @@ -0,0 +1,47 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Language + + + Conditions d'Utilisation + + + LISEZ CES INFORMATIONS ATTENTIVEMENT + + + Tous les fichiers moddés actuellement en cours d'utilisation ainsi que le statut actuel de votre personnage vont être mix en ligne via le service sur lequel vous vous êtes automatiquement enregistré. Seuls les fichiers nécessaires seront téléversés par le plugin et non pas le mod en entier. + + + Si le débit de votre connexion internet est limité, le téléchargement et téléversement d'un grand nombre de fichiers peut entraîner des coûts supplémentaires. Les fichiers seront compressés au chargement et versement pour réduire l'impact sur votre bande passants. Selon la rapidité de vos téléchargements et téléversements, les changements ne seront peut-être pas visibles instantanément sur les personnages. Les fichiers déja présents sur le service qui correspondent à ceux de vos mods en cours d'utilisation ne seront pas remis en ligne. + + + Les fichiers que vous allez partager sont confidentiels et ne seront envoyés qu'aux utilisateurs qui feront une requête exacte de ceux-çi. Nous vous demandons de (re)considérer qui sera synchronisé avec vous, puisqu'ils recevront et stockeront inévitablement en local les fichiers nécéssaires utilisés à cet instant. Les noms des fichiers stockés localement sont changés de manière arbitraire afin de décourager toute tentative de réplication des originaux. + + + Le créateur de ce plugin a tenté de sécuriser l'application du mieux possible. Cependant, il ne peut pas garantir une protection 100% infaillible. Pour votre sécurité, ne vous synchronisez pas aveuglément et avec n'importe qui. + + + Les fichiers sauvegardés sur le service resteront en ligne tant que des utilisateurs en feront usage. Ils seront effacés automatiquement après une certaine période d'inactivité. Vous pouvez également demander l'effacement de tous les fichiers que vous avez mis en ligne vous-même. Le service en soi ne contient aucune information pouvant identifier quel fichier appartient à quel mod. + + + Ce service et ses composants vous sont fournis en l'état. En cas d'abus rejoindre le serveur Discord Lightless Sync. + + + J'accept + + + Bouton "J'accept" disposible dans + + \ No newline at end of file diff --git a/LightlessSync/Resources/Resources.resx b/LightlessSync/Resources/Resources.resx new file mode 100644 index 0000000..3576563 --- /dev/null +++ b/LightlessSync/Resources/Resources.resx @@ -0,0 +1,57 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + I agree + + + Agreement of Usage of Service + + + 'I agree' button will be available in + + + All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. The plugin will exclusively upload the necessary mod files and not the whole mod. + + + If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. Files present on the service that already represent your active mod files will not be uploaded again. + + + The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod. + + + The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone. + + + Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted. You will also be able to wipe all the files you have personally uploaded on request. The service holds no information about which mod files belong to which mod. + + + This service is provided as-is. In case of abuse join the Lightless Sync Discord. + + + READ THIS CAREFULLY + + + Language + + + Users Online + + \ No newline at end of file diff --git a/LightlessSync/Resources/Resources.zh.resx b/LightlessSync/Resources/Resources.zh.resx new file mode 100644 index 0000000..c40c057 --- /dev/null +++ b/LightlessSync/Resources/Resources.zh.resx @@ -0,0 +1,20 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 语言 + + + 用户在线 + + \ No newline at end of file diff --git a/LightlessSync/UI/IntroUI.cs b/LightlessSync/UI/IntroUI.cs index 4fab7ef..53cf350 100644 --- a/LightlessSync/UI/IntroUI.cs +++ b/LightlessSync/UI/IntroUI.cs @@ -6,12 +6,12 @@ using Dalamud.Utility; using LightlessSync.FileCache; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; -using LightlessSync.Localization; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.Utils; using Microsoft.Extensions.Logging; +using System.Globalization; using System.Numerics; using System.Text.RegularExpressions; @@ -21,7 +21,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase { private readonly LightlessConfigService _configService; private readonly CacheMonitor _cacheMonitor; - private readonly Dictionary _languages = new(StringComparer.Ordinal) { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } }; + private readonly Dictionary _languages = new(StringComparer.Ordinal) { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" }, { "中文", "zh"} }; private readonly ServerConfigurationManager _serverConfigurationManager; private readonly DalamudUtilService _dalamudUtilService; private readonly UiSharedService _uiShared; @@ -31,7 +31,6 @@ public partial class IntroUi : WindowMediatorSubscriberBase private string _secretKey = string.Empty; private string _timeoutLabel = string.Empty; private Task? _timeoutTask; - private string[]? _tosParagraphs; private bool _useLegacyLogin = false; public IntroUi(ILogger logger, UiSharedService uiShared, LightlessConfigService configService, @@ -50,8 +49,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase WindowBuilder.For(this) .SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 2000)) .Apply(); - - GetToSLocalization(); + Mediator.Subscribe(this, (_) => IsOpen = false); Mediator.Subscribe(this, (_) => @@ -88,7 +86,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase { for (int i = 60; i > 0; i--) { - _timeoutLabel = $"{Strings.ToS.ButtonWillBeAvailableIn} {i}s"; + _timeoutLabel = $"{Resources.Resources.ToSStrings_ButtonWillBeAvailableIn} {i}s"; await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); } }); @@ -102,44 +100,46 @@ public partial class IntroUi : WindowMediatorSubscriberBase Vector2 textSize; using (_uiShared.UidFont.Push()) { - textSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel); - ImGui.TextUnformatted(Strings.ToS.AgreementLabel); + textSize = ImGui.CalcTextSize(Resources.Resources.ToSStrings_LanguageLabel); + ImGui.TextUnformatted(Resources.Resources.ToSStrings_AgreementLabel); } ImGui.SameLine(); - var languageSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel); + var languageSize = ImGui.CalcTextSize(Resources.Resources.ToSStrings_LanguageLabel); ImGui.SetCursorPosX(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - languageSize.X - 80); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - languageSize.Y / 2); - ImGui.TextUnformatted(Strings.ToS.LanguageLabel); + ImGui.TextUnformatted(Resources.Resources.ToSStrings_LanguageLabel); ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - (languageSize.Y + ImGui.GetStyle().FramePadding.Y) / 2); ImGui.SetNextItemWidth(80); if (ImGui.Combo("", ref _currentLanguage, _languages.Keys.ToArray(), _languages.Count)) { - GetToSLocalization(_currentLanguage); + var culture = new CultureInfo(_languages.Values.ToArray()[_currentLanguage]); + CultureInfo.DefaultThreadCurrentCulture = culture; + CultureInfo.DefaultThreadCurrentUICulture = culture; } ImGui.Separator(); ImGui.SetWindowFontScale(1.5f); - string readThis = Strings.ToS.ReadLabel; + string readThis = Resources.Resources.ToSStrings_ReadLabel; textSize = ImGui.CalcTextSize(readThis); ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2); UiSharedService.ColorText(readThis, ImGuiColors.DalamudRed); ImGui.SetWindowFontScale(1.0f); ImGui.Separator(); - UiSharedService.TextWrapped(_tosParagraphs![0]); - UiSharedService.TextWrapped(_tosParagraphs![1]); - UiSharedService.TextWrapped(_tosParagraphs![2]); - UiSharedService.TextWrapped(_tosParagraphs![3]); - UiSharedService.TextWrapped(_tosParagraphs![4]); - UiSharedService.TextWrapped(_tosParagraphs![5]); + UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph1); + UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph2); + UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph3); + UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph4); + UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph5); + UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph6); ImGui.Separator(); if (_timeoutTask?.IsCompleted ?? true) { - if (ImGui.Button(Strings.ToS.AgreeLabel + "##toSetup")) + if (ImGui.Button(Resources.Resources.ToSStrings_AgreeLabel + "##toSetup")) { _configService.Current.AcceptedAgreement = true; _configService.Save(); @@ -349,16 +349,6 @@ public partial class IntroUi : WindowMediatorSubscriberBase } } - private void GetToSLocalization(int changeLanguageTo = -1) - { - if (changeLanguageTo != -1) - { - _uiShared.LoadLocalization(_languages.ElementAt(changeLanguageTo).Value); - } - - _tosParagraphs = [Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6]; - } - [GeneratedRegex("^[A-F0-9]{64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] private static partial Regex SecretRegex(); } \ No newline at end of file diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 9c2f1ef..96a300b 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -4812,7 +4812,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.TextColored(UIColors.Get("LightlessBlue"), _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture)); ImGui.SameLine(); - ImGui.TextUnformatted("Users Online"); + ImGui.TextUnformatted(Resources.Resources.Users_Online); ImGui.SameLine(); ImGui.TextUnformatted(")"); } diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index 514f31e..bbcaa44 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -18,7 +18,6 @@ using LightlessSync.Interop.Ipc; using LightlessSync.Interop.Ipc.Framework; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; -using LightlessSync.Localization; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; @@ -1468,12 +1467,6 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase return false; } - public void LoadLocalization(string languageCode) - { - _localization.SetupWithLangCode(languageCode); - Strings.ToS = new Strings.ToSStrings(); - } - internal static void DistanceSeparator() { ImGuiHelpers.ScaledDummy(5);