Compare commits
34 Commits
test-abel
...
2.0.2.76-D
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3654365f2a | ||
|
|
9b256dd185 | ||
|
|
d8b9e9cf19 | ||
|
|
ad34d88336 | ||
| 9167bb1afd | |||
|
|
5161c6bad3 | ||
|
|
223ade39cb | ||
|
|
5aca9e70b2 | ||
|
|
ce28799db3 | ||
|
|
92772cf334 | ||
|
|
0395e81a9f | ||
|
|
9b9010ab8e | ||
|
|
7734a7bf7e | ||
|
|
db2d19bb1e | ||
|
|
032201ed9e | ||
|
|
775b128cf3 | ||
|
|
4bb8db8c03 | ||
|
|
f307c65c66 | ||
|
|
ab305a249c | ||
|
|
9d104a9dd8 | ||
|
|
4eec363cd2 | ||
|
|
d00df84ed6 | ||
|
|
bcd3bd5ca2 | ||
|
|
9048b3bd87 | ||
|
|
c1829a9837 | ||
|
|
a2ed9f8d2b | ||
| 8e08da7471 | |||
|
|
cca23f6e05 | ||
|
|
3205e6e0c3 | ||
|
|
d16e46200d | ||
|
|
5fc13647ae | ||
|
|
39d5d9d7c1 | ||
|
|
c19db58ead | ||
| 30717ba200 |
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
<Company></Company>
|
<Company></Company>
|
||||||
<Version>2.0.3</Version>
|
<Version>2.0.2.76</Version>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<Copyright></Copyright>
|
<Copyright></Copyright>
|
||||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||||
@@ -24,6 +24,15 @@
|
|||||||
<Compile Remove="PlayerData\Export\**" />
|
<Compile Remove="PlayerData\Export\**" />
|
||||||
<EmbeddedResource Remove="PlayerData\Export\**" />
|
<EmbeddedResource Remove="PlayerData\Export\**" />
|
||||||
<None Remove="PlayerData\Export\**" />
|
<None Remove="PlayerData\Export\**" />
|
||||||
|
<EmbeddedResource Update="Resources\Resources.resx">
|
||||||
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<Compile Update="Resources\Resources.Designer.cs">
|
||||||
|
<DesignTime>True</DesignTime>
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -68,8 +77,6 @@
|
|||||||
</None>
|
</None>
|
||||||
<EmbeddedResource Include="Changelog\changelog.yaml" />
|
<EmbeddedResource Include="Changelog\changelog.yaml" />
|
||||||
<EmbeddedResource Include="Changelog\credits.yaml" />
|
<EmbeddedResource Include="Changelog\credits.yaml" />
|
||||||
<EmbeddedResource Include="Localization\de.json" />
|
|
||||||
<EmbeddedResource Include="Localization\fr.json" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
3
LightlessSync/LightlessSync.csproj.DotSettings
Normal file
3
LightlessSync/LightlessSync.csproj.DotSettings
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeEditing/Localization/Localizable/@EntryValue">Yes</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeEditing/Localization/LocalizableInspector/@EntryValue">Pessimistic</s:String></wpf:ResourceDictionary>
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using Dalamud.Utility;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
@@ -11,6 +12,8 @@ using LightlessSync.Services.Mediator;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Factories;
|
namespace LightlessSync.PlayerData.Factories;
|
||||||
|
|
||||||
@@ -124,6 +127,9 @@ public class PlayerDataFactory
|
|||||||
if (playerPointer == IntPtr.Zero)
|
if (playerPointer == IntPtr.Zero)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
if (!IsPointerValid(playerPointer))
|
||||||
|
return true;
|
||||||
|
|
||||||
var character = (Character*)playerPointer;
|
var character = (Character*)playerPointer;
|
||||||
if (character == null)
|
if (character == null)
|
||||||
return true;
|
return true;
|
||||||
@@ -132,9 +138,28 @@ public class PlayerDataFactory
|
|||||||
if (gameObject == null)
|
if (gameObject == null)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
if (!IsPointerValid((IntPtr)gameObject))
|
||||||
|
return true;
|
||||||
|
|
||||||
return gameObject->DrawObject == null;
|
return gameObject->DrawObject == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsPointerValid(IntPtr ptr)
|
||||||
|
{
|
||||||
|
if (ptr == IntPtr.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = Marshal.ReadByte(ptr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsCacheFresh(CacheEntry entry)
|
private static bool IsCacheFresh(CacheEntry entry)
|
||||||
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;
|
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;
|
||||||
|
|
||||||
@@ -169,11 +194,6 @@ public class PlayerDataFactory
|
|||||||
using var cts = new CancellationTokenSource(_hardBuildTimeout);
|
using var cts = new CancellationTokenSource(_hardBuildTimeout);
|
||||||
var fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false);
|
var fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
fragment.FileReplacements =
|
|
||||||
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
|
|
||||||
.Where(p => p.HasFileReplacement).ToHashSet();
|
|
||||||
var allowedExtensions = CacheMonitor.AllowedFileExtensions;
|
|
||||||
fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !allowedExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
|
|
||||||
_characterBuildCache[key] = new CacheEntry(fragment, DateTime.UtcNow);
|
_characterBuildCache[key] = new CacheEntry(fragment, DateTime.UtcNow);
|
||||||
PruneCharacterCacheIfNeeded();
|
PruneCharacterCacheIfNeeded();
|
||||||
|
|
||||||
@@ -219,10 +239,6 @@ public class PlayerDataFactory
|
|||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
// get all remaining paths and resolve them
|
// get all remaining paths and resolve them
|
||||||
var transientPaths = ManageSemiTransientData(objectKind);
|
|
||||||
var resolvedTransientPaths = transientPaths.Count == 0
|
|
||||||
? new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly()
|
|
||||||
: await GetFileReplacementsFromPaths(playerRelatedObject, transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (await CheckForNullDrawObject(playerRelatedObject.Address).ConfigureAwait(false))
|
if (await CheckForNullDrawObject(playerRelatedObject.Address).ConfigureAwait(false))
|
||||||
@@ -540,13 +556,31 @@ public class PlayerDataFactory
|
|||||||
|
|
||||||
var hash = g.Key;
|
var hash = g.Key;
|
||||||
|
|
||||||
|
var resolvedPath = g.Select(f => f.ResolvedPath).Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var papPathSummary = string.Join(", ", resolvedPath);
|
||||||
|
if (papPathSummary.IsNullOrEmpty())
|
||||||
|
papPathSummary = "<unknown pap path>";
|
||||||
|
|
||||||
Dictionary<string, List<ushort>>? papIndices = null;
|
Dictionary<string, List<ushort>>? papIndices = null;
|
||||||
|
|
||||||
await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false);
|
await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
papIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash), ct)
|
var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash);
|
||||||
.ConfigureAwait(false);
|
var papPath = cacheEntity?.ResolvedFilepath;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(papPath) && File.Exists(papPath))
|
||||||
|
{
|
||||||
|
var havokBytes = await Task.Run(() => XivDataAnalyzer.ReadHavokBytesFromPap(papPath), ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (havokBytes is { Length: > 8 })
|
||||||
|
{
|
||||||
|
papIndices = await _dalamudUtil.RunOnFrameworkThread(
|
||||||
|
() => _modelAnalyzer.ParseHavokBytesOnFrameworkThread(havokBytes, hash, persistToConfig: false))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -556,43 +590,61 @@ public class PlayerDataFactory
|
|||||||
if (papIndices == null || papIndices.Count == 0)
|
if (papIndices == null || papIndices.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
{
|
{
|
||||||
var papBuckets = papIndices
|
try
|
||||||
.Select(kvp => new
|
{
|
||||||
{
|
var papBuckets = papIndices
|
||||||
Raw = kvp.Key,
|
.Where(kvp => kvp.Value is { Count: > 0 })
|
||||||
Key = XivDataAnalyzer.CanonicalizeSkeletonKey(kvp.Key),
|
.Select(kvp => new
|
||||||
Indices = kvp.Value
|
{
|
||||||
})
|
Raw = kvp.Key,
|
||||||
.Where(x => x.Indices is { Count: > 0 })
|
Key = XivDataAnalyzer.CanonicalizeSkeletonKey(kvp.Key),
|
||||||
.GroupBy(x => string.IsNullOrEmpty(x.Key) ? x.Raw : x.Key!, StringComparer.OrdinalIgnoreCase)
|
Indices = kvp.Value
|
||||||
.Select(grp =>
|
})
|
||||||
{
|
.Where(x => x.Indices is { Count: > 0 })
|
||||||
var all = grp.SelectMany(v => v.Indices).ToList();
|
.GroupBy(x => string.IsNullOrEmpty(x.Key) ? x.Raw : x.Key!, StringComparer.OrdinalIgnoreCase)
|
||||||
var min = all.Count > 0 ? all.Min() : 0;
|
.Select(grp =>
|
||||||
var max = all.Count > 0 ? all.Max() : 0;
|
{
|
||||||
var raws = string.Join(',', grp.Select(v => v.Raw).Distinct(StringComparer.OrdinalIgnoreCase));
|
var all = grp.SelectMany(v => v.Indices).ToList();
|
||||||
return $"{grp.Key}(min={min},max={max},raw=[{raws}])";
|
var min = all.Count > 0 ? all.Min() : 0;
|
||||||
})
|
var max = all.Count > 0 ? all.Max() : 0;
|
||||||
.ToList();
|
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}",
|
_logger.LogDebug("SEND pap buckets for hash={hash}: {b}",
|
||||||
hash,
|
hash,
|
||||||
string.Join(" | ", papBuckets));
|
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;
|
continue;
|
||||||
|
|
||||||
noValidationFailed++;
|
noValidationFailed++;
|
||||||
|
|
||||||
_logger.LogWarning(
|
_logger.LogWarning(
|
||||||
"Animation PAP hash {hash} is not compatible with local skeletons; dropping all mappings for this hash. Reason: {reason}",
|
"Animation PAP is not compatible with local skeletons; dropping mappings for {papPath}. Reason: {reason}",
|
||||||
hash,
|
papPathSummary,
|
||||||
reason);
|
reason);
|
||||||
|
|
||||||
var removedGamePaths = fragment.FileReplacements
|
var removedGamePaths = fragment.FileReplacements
|
||||||
@@ -637,8 +689,8 @@ public class PlayerDataFactory
|
|||||||
return new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
return new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
var forwardPathsLower = forwardPaths.Length == 0 ? Array.Empty<string>() : forwardPaths.Select(p => p.ToLowerInvariant()).ToArray();
|
var forwardPathsLower = forwardPaths.Length == 0 ? [] : forwardPaths.Select(p => p.ToLowerInvariant()).ToArray();
|
||||||
var reversePathsLower = reversePaths.Length == 0 ? Array.Empty<string>() : reversePaths.Select(p => p.ToLowerInvariant()).ToArray();
|
var reversePathsLower = reversePaths.Length == 0 ? [] : reversePaths.Select(p => p.ToLowerInvariant()).ToArray();
|
||||||
|
|
||||||
Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal);
|
Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal);
|
||||||
if (handler.ObjectKind != ObjectKind.Player)
|
if (handler.ObjectKind != ObjectKind.Player)
|
||||||
@@ -672,7 +724,7 @@ public class PlayerDataFactory
|
|||||||
list.Add(forwardPaths[i].ToLowerInvariant());
|
list.Add(forwardPaths[i].ToLowerInvariant());
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
|
resolvedPaths[filePath] = [forwardPathsLower[i]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,16 +113,16 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<Dalamud.Game.ClientState.Objects.Types.ICharacter> act, CancellationToken token)
|
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<Dalamud.Game.ClientState.Objects.Types.ICharacter> act, CancellationToken token)
|
||||||
{
|
{
|
||||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
EnsureLatestObjectState();
|
EnsureLatestObjectState();
|
||||||
if (CurrentDrawCondition != DrawCondition.None) return true;
|
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||||
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||||
{
|
{
|
||||||
act.Invoke(chara);
|
act.Invoke(chara);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}).ConfigureAwait(false))
|
}).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await Task.Delay(250, token).ConfigureAwait(false);
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -169,37 +169,36 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
private void CheckAndUpdateObject() => CheckAndUpdateObject(allowPublish: true);
|
||||||
{
|
|
||||||
base.Dispose(disposing);
|
|
||||||
|
|
||||||
Mediator.Publish(new GameObjectHandlerDestroyedMessage(this, _isOwnedObject));
|
private unsafe void CheckAndUpdateObject(bool allowPublish)
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void CheckAndUpdateObject()
|
|
||||||
{
|
{
|
||||||
var prevAddr = Address;
|
var prevAddr = Address;
|
||||||
var prevDrawObj = DrawObjectAddress;
|
var prevDrawObj = DrawObjectAddress;
|
||||||
|
string? nameString = null;
|
||||||
|
|
||||||
Address = _getAddress();
|
Address = _getAddress();
|
||||||
|
|
||||||
if (Address != IntPtr.Zero)
|
if (Address != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
||||||
var drawObjAddr = (IntPtr)gameObject->DrawObject;
|
DrawObjectAddress = (IntPtr)gameObject->DrawObject;
|
||||||
DrawObjectAddress = drawObjAddr;
|
|
||||||
EntityId = gameObject->EntityId;
|
EntityId = gameObject->EntityId;
|
||||||
CurrentDrawCondition = DrawCondition.None;
|
|
||||||
|
var chara = (Character*)Address;
|
||||||
|
nameString = chara->GameObject.NameString;
|
||||||
|
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
||||||
|
Name = nameString;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DrawObjectAddress = IntPtr.Zero;
|
DrawObjectAddress = IntPtr.Zero;
|
||||||
EntityId = uint.MaxValue;
|
EntityId = uint.MaxValue;
|
||||||
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentDrawCondition = IsBeingDrawnUnsafe();
|
CurrentDrawCondition = IsBeingDrawnUnsafe();
|
||||||
|
|
||||||
if (_haltProcessing) return;
|
if (_haltProcessing || !allowPublish) return;
|
||||||
|
|
||||||
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
||||||
bool addrDiff = Address != prevAddr;
|
bool addrDiff = Address != prevAddr;
|
||||||
@@ -207,16 +206,18 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
var chara = (Character*)Address;
|
var chara = (Character*)Address;
|
||||||
var name = chara->GameObject.NameString;
|
var drawObj = (DrawObject*)DrawObjectAddress;
|
||||||
bool nameChange = !string.Equals(name, Name, StringComparison.Ordinal);
|
var objType = drawObj->Object.GetObjectType();
|
||||||
if (nameChange)
|
var isHuman = objType == ObjectType.CharacterBase
|
||||||
{
|
&& ((CharacterBase*)drawObj)->GetModelType() == CharacterBase.ModelType.Human;
|
||||||
Name = name;
|
|
||||||
}
|
nameString ??= ((Character*)Address)->GameObject.NameString;
|
||||||
|
var nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
||||||
|
if (nameChange) Name = nameString;
|
||||||
|
|
||||||
bool equipDiff = false;
|
bool equipDiff = false;
|
||||||
|
|
||||||
if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase
|
if (isHuman)
|
||||||
&& ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human)
|
|
||||||
{
|
{
|
||||||
var classJob = chara->CharacterData.ClassJob;
|
var classJob = chara->CharacterData.ClassJob;
|
||||||
if (classJob != _classJob)
|
if (classJob != _classJob)
|
||||||
@@ -226,7 +227,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)DrawObjectAddress)->Head);
|
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
|
||||||
|
|
||||||
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
||||||
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
||||||
@@ -251,12 +252,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
|
|
||||||
bool customizeDiff = false;
|
bool customizeDiff = false;
|
||||||
|
|
||||||
if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase
|
if (isHuman)
|
||||||
&& ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human)
|
|
||||||
{
|
{
|
||||||
var gender = ((Human*)DrawObjectAddress)->Customize.Sex;
|
var gender = ((Human*)drawObj)->Customize.Sex;
|
||||||
var raceId = ((Human*)DrawObjectAddress)->Customize.Race;
|
var raceId = ((Human*)drawObj)->Customize.Race;
|
||||||
var tribeId = ((Human*)DrawObjectAddress)->Customize.Tribe;
|
var tribeId = ((Human*)drawObj)->Customize.Tribe;
|
||||||
|
|
||||||
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
||||||
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
||||||
@@ -267,7 +267,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
TribeId = tribeId;
|
TribeId = tribeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
customizeDiff = CompareAndUpdateCustomizeData(((Human*)DrawObjectAddress)->Customize.Data);
|
customizeDiff = CompareAndUpdateCustomizeData(((Human*)drawObj)->Customize.Data);
|
||||||
if (customizeDiff)
|
if (customizeDiff)
|
||||||
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
|
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
|
||||||
}
|
}
|
||||||
@@ -356,12 +356,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
|
|
||||||
private void FrameworkUpdate()
|
private void FrameworkUpdate()
|
||||||
{
|
{
|
||||||
if (!_delayedZoningTask?.IsCompleted ?? false) return;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_performanceCollector.LogPerformance(this, $"CheckAndUpdateObject>{(_isOwnedObject ? "Self" : "Other")}+{ObjectKind}/{(string.IsNullOrEmpty(Name) ? "Unk" : Name)}"
|
var zoningDelayActive = !(_delayedZoningTask?.IsCompleted ?? true);
|
||||||
+ $"+{Address.ToString("X")}", CheckAndUpdateObject);
|
_performanceCollector.LogPerformance(this, $"CheckAndUpdateObject>{(_isOwnedObject ? "Self" : "Other")}+{ObjectKind}/{(string.IsNullOrEmpty(Name) ? "Unk" : Name)}", () => CheckAndUpdateObject(allowPublish: !zoningDelayActive));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -462,6 +460,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
Logger.LogDebug("[{this}] Delay after zoning complete", this);
|
Logger.LogDebug("[{this}] Delay after zoning complete", this);
|
||||||
_zoningCts.Dispose();
|
_zoningCts.Dispose();
|
||||||
}
|
}
|
||||||
});
|
}, _zoningCts.Token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,6 @@ using Microsoft.Extensions.Logging;
|
|||||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||||
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
|
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
|
||||||
using LightlessSync.LightlessConfiguration;
|
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Pairs;
|
namespace LightlessSync.PlayerData.Pairs;
|
||||||
|
|
||||||
@@ -96,6 +95,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private readonly Dictionary<ObjectKind, HashSet<PlayerChanges>> _pendingOwnedChanges = new();
|
private readonly Dictionary<ObjectKind, HashSet<PlayerChanges>> _pendingOwnedChanges = new();
|
||||||
private CancellationTokenSource? _ownedRetryCts;
|
private CancellationTokenSource? _ownedRetryCts;
|
||||||
private Task _ownedRetryTask = Task.CompletedTask;
|
private Task _ownedRetryTask = Task.CompletedTask;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private static readonly TimeSpan OwnedRetryInitialDelay = TimeSpan.FromSeconds(1);
|
private static readonly TimeSpan OwnedRetryInitialDelay = TimeSpan.FromSeconds(1);
|
||||||
private static readonly TimeSpan OwnedRetryMaxDelay = TimeSpan.FromSeconds(10);
|
private static readonly TimeSpan OwnedRetryMaxDelay = TimeSpan.FromSeconds(10);
|
||||||
private static readonly TimeSpan OwnedRetryStaleDataGrace = TimeSpan.FromMinutes(5);
|
private static readonly TimeSpan OwnedRetryStaleDataGrace = TimeSpan.FromMinutes(5);
|
||||||
@@ -109,6 +111,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, byte> _blockedPapHashes = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, byte> _blockedPapHashes = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private AnimationValidationMode _lastAnimMode = (AnimationValidationMode)(-1);
|
||||||
|
private bool _lastAllowOneBasedShift;
|
||||||
|
private bool _lastAllowNeighborTolerance;
|
||||||
private readonly ConcurrentDictionary<string, byte> _dumpedRemoteSkeletonForHash = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, byte> _dumpedRemoteSkeletonForHash = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private DateTime? _invisibleSinceUtc;
|
private DateTime? _invisibleSinceUtc;
|
||||||
@@ -116,6 +121,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private DateTime _nextActorLookupUtc = DateTime.MinValue;
|
private DateTime _nextActorLookupUtc = DateTime.MinValue;
|
||||||
private static readonly TimeSpan ActorLookupInterval = TimeSpan.FromSeconds(1);
|
private static readonly TimeSpan ActorLookupInterval = TimeSpan.FromSeconds(1);
|
||||||
private static readonly SemaphoreSlim ActorInitializationLimiter = new(1, 1);
|
private static readonly SemaphoreSlim ActorInitializationLimiter = new(1, 1);
|
||||||
|
private static readonly SemaphoreSlim _papParseLimiter = new(1, 1);
|
||||||
private const int FullyLoadedTimeoutMsPlayer = 30000;
|
private const int FullyLoadedTimeoutMsPlayer = 30000;
|
||||||
private const int FullyLoadedTimeoutMsOther = 5000;
|
private const int FullyLoadedTimeoutMsOther = 5000;
|
||||||
private readonly object _actorInitializationGate = new();
|
private readonly object _actorInitializationGate = new();
|
||||||
@@ -2455,6 +2461,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
RefreshPapBlockCacheIfAnimSettingsChanged();
|
||||||
|
|
||||||
var replacementList = charaData.FileReplacements.SelectMany(k => k.Value.Where(v => string.IsNullOrEmpty(v.FileSwapPath))).ToList();
|
var replacementList = charaData.FileReplacements.SelectMany(k => k.Value.Where(v => string.IsNullOrEmpty(v.FileSwapPath))).ToList();
|
||||||
Parallel.ForEach(replacementList, new ParallelOptions()
|
Parallel.ForEach(replacementList, new ParallelOptions()
|
||||||
{
|
{
|
||||||
@@ -2855,6 +2863,26 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RefreshPapBlockCacheIfAnimSettingsChanged()
|
||||||
|
{
|
||||||
|
var cfg = _configService.Current;
|
||||||
|
|
||||||
|
if (cfg.AnimationValidationMode != _lastAnimMode
|
||||||
|
|| cfg.AnimationAllowOneBasedShift != _lastAllowOneBasedShift
|
||||||
|
|| cfg.AnimationAllowNeighborIndexTolerance != _lastAllowNeighborTolerance)
|
||||||
|
{
|
||||||
|
_lastAnimMode = cfg.AnimationValidationMode;
|
||||||
|
_lastAllowOneBasedShift = cfg.AnimationAllowOneBasedShift;
|
||||||
|
_lastAllowNeighborTolerance = cfg.AnimationAllowNeighborIndexTolerance;
|
||||||
|
|
||||||
|
_blockedPapHashes.Clear();
|
||||||
|
_dumpedRemoteSkeletonForHash.Clear();
|
||||||
|
|
||||||
|
Logger.LogDebug("{handler}: Cleared blocked PAP cache due to animation setting change (mode={mode}, shift={shift}, neigh={neigh})",
|
||||||
|
GetLogIdentifier(), _lastAnimMode, _lastAllowOneBasedShift, _lastAllowNeighborTolerance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void SplitPapMappings(
|
private static void SplitPapMappings(
|
||||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths,
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths,
|
||||||
out Dictionary<(string GamePath, string? Hash), string> withoutPap,
|
out Dictionary<(string GamePath, string? Hash), string> withoutPap,
|
||||||
@@ -2879,15 +2907,17 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
Dictionary<(string GamePath, string? Hash), string> papOnly,
|
Dictionary<(string GamePath, string? Hash), string> papOnly,
|
||||||
CancellationToken token)
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
|
RefreshPapBlockCacheIfAnimSettingsChanged();
|
||||||
|
|
||||||
var mode = _configService.Current.AnimationValidationMode;
|
var mode = _configService.Current.AnimationValidationMode;
|
||||||
var allowBasedShift = _configService.Current.AnimationAllowOneBasedShift;
|
var allowBasedShift = _configService.Current.AnimationAllowOneBasedShift;
|
||||||
var allownNightIndex = _configService.Current.AnimationAllowNeighborIndexTolerance;
|
var allowNeighborIndex = _configService.Current.AnimationAllowNeighborIndexTolerance;
|
||||||
|
|
||||||
if (mode == AnimationValidationMode.Unsafe || papOnly.Count == 0)
|
if (mode == AnimationValidationMode.Unsafe || papOnly.Count == 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
var boneIndices = await _dalamudUtil.RunOnFrameworkThread(
|
var boneIndices = await _dalamudUtil.RunOnFrameworkThread(
|
||||||
() => _modelAnalyzer.GetSkeletonBoneIndices(handlerForApply))
|
() => _modelAnalyzer.GetSkeletonBoneIndices(handlerForApply))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (boneIndices == null || boneIndices.Count == 0)
|
if (boneIndices == null || boneIndices.Count == 0)
|
||||||
@@ -2901,47 +2931,86 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
foreach (var (rawKey, list) in boneIndices)
|
foreach (var (rawKey, list) in boneIndices)
|
||||||
{
|
{
|
||||||
var key = XivDataAnalyzer.CanonicalizeSkeletonKey(rawKey);
|
var key = XivDataAnalyzer.CanonicalizeSkeletonKey(rawKey);
|
||||||
if (string.IsNullOrEmpty(key)) continue;
|
if (string.IsNullOrEmpty(key) || list == null || list.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (!localBoneSets.TryGetValue(key, out var set))
|
if (!localBoneSets.TryGetValue(key, out var set))
|
||||||
localBoneSets[key] = set = [];
|
localBoneSets[key] = set = new HashSet<ushort>();
|
||||||
|
|
||||||
foreach (var v in list)
|
foreach (var v in list)
|
||||||
set.Add(v);
|
set.Add(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (localBoneSets.Count == 0)
|
||||||
|
{
|
||||||
|
var removedCount = papOnly.Count;
|
||||||
|
papOnly.Clear();
|
||||||
|
return removedCount;
|
||||||
|
}
|
||||||
|
|
||||||
int removed = 0;
|
int removed = 0;
|
||||||
|
|
||||||
foreach (var hash in papOnly.Keys.Select(k => k.Hash).Where(h => !string.IsNullOrEmpty(h)).Distinct(StringComparer.OrdinalIgnoreCase).ToList())
|
var groups = papOnly
|
||||||
|
.Where(kvp => !string.IsNullOrEmpty(kvp.Key.Hash))
|
||||||
|
.GroupBy(kvp => kvp.Key.Hash!, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var grp in groups)
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var papIndices = await _dalamudUtil.RunOnFrameworkThread(
|
var hash = grp.Key;
|
||||||
() => _modelAnalyzer.GetBoneIndicesFromPap(hash!))
|
|
||||||
.ConfigureAwait(false);
|
var papPath = grp.Select(x => x.Value)
|
||||||
|
.FirstOrDefault(p => !string.IsNullOrEmpty(p) && File.Exists(p));
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(papPath))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var havokBytes = await Task.Run(() => XivDataAnalyzer.ReadHavokBytesFromPap(papPath), token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (havokBytes is not { Length: > 8 })
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Dictionary<string, List<ushort>>? papIndices;
|
||||||
|
|
||||||
|
await _papParseLimiter.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
papIndices = await _dalamudUtil.RunOnFrameworkThread(
|
||||||
|
() => _modelAnalyzer.ParseHavokBytesOnFrameworkThread(havokBytes, hash, persistToConfig: false))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_papParseLimiter.Release();
|
||||||
|
}
|
||||||
|
|
||||||
if (papIndices == null || papIndices.Count == 0)
|
if (papIndices == null || papIndices.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105))
|
if (papIndices.All(k => k.Value == null || k.Value.Count == 0 || k.Value.Max() <= 105))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out var reason))
|
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allowNeighborIndex, out var reason))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var keysToRemove = papOnly.Keys.Where(k => string.Equals(k.Hash, hash, StringComparison.OrdinalIgnoreCase)).ToList();
|
var keysToRemove = grp.Select(x => x.Key).ToList();
|
||||||
foreach (var k in keysToRemove)
|
foreach (var k in keysToRemove)
|
||||||
papOnly.Remove(k);
|
papOnly.Remove(k);
|
||||||
|
|
||||||
removed += keysToRemove.Count;
|
removed += keysToRemove.Count;
|
||||||
|
|
||||||
if (_blockedPapHashes.TryAdd(hash!, 0))
|
if (_blockedPapHashes.TryAdd(hash, 0))
|
||||||
Logger.LogWarning("Blocked remote object PAP (hash {hash}) for {handler}: {reason}", hash, GetLogIdentifier(), reason);
|
Logger.LogWarning("Blocked remote object PAP {papPath} (hash {hash}) for {handler}: {reason}",
|
||||||
|
papPath, hash, GetLogIdentifier(), reason);
|
||||||
|
|
||||||
if (charaData.FileReplacements.TryGetValue(ObjectKind.Player, out var list))
|
if (charaData.FileReplacements.TryGetValue(ObjectKind.Player, out var list))
|
||||||
{
|
{
|
||||||
list.RemoveAll(r => string.Equals(r.Hash, hash, StringComparison.OrdinalIgnoreCase)
|
list.RemoveAll(r =>
|
||||||
&& r.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
|
string.Equals(r.Hash, hash, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
r.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2955,6 +3024,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind)
|
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind)
|
||||||
{
|
{
|
||||||
_customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
_customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -123,7 +123,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton<HubFactory>();
|
services.AddSingleton<HubFactory>();
|
||||||
services.AddSingleton<FileUploadManager>();
|
services.AddSingleton<FileUploadManager>();
|
||||||
services.AddSingleton<FileTransferOrchestrator>();
|
services.AddSingleton<FileTransferOrchestrator>();
|
||||||
services.AddSingleton<FileDownloadDeduplicator>();
|
|
||||||
services.AddSingleton<LightlessPlugin>();
|
services.AddSingleton<LightlessPlugin>();
|
||||||
services.AddSingleton<LightlessProfileManager>();
|
services.AddSingleton<LightlessProfileManager>();
|
||||||
services.AddSingleton<TextureCompressionService>();
|
services.AddSingleton<TextureCompressionService>();
|
||||||
|
|||||||
9
LightlessSync/Resources/LocalizationExtention.cs
Normal file
9
LightlessSync/Resources/LocalizationExtention.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
171
LightlessSync/Resources/Resources.Designer.cs
generated
Normal file
171
LightlessSync/Resources/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// 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.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace LightlessSync.Resources {
|
||||||
|
using System;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
/// </summary>
|
||||||
|
// 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", "18.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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the cached ResourceManager instance used by this class.
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides the current thread's CurrentUICulture property for all
|
||||||
|
/// resource lookups using this strongly typed resource class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
public static global::System.Globalization.CultureInfo Culture {
|
||||||
|
get {
|
||||||
|
return resourceCulture;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
resourceCulture = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to I agree.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSStrings_AgreeLabel {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ToSStrings_AgreeLabel", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Agreement of Usage of Service.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSStrings_AgreementLabel {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ToSStrings_AgreementLabel", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to 'I agree' button will be available in.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSStrings_ButtonWillBeAvailableIn {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ToSStrings_ButtonWillBeAvailableIn", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Language.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSStrings_LanguageLabel {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ToSStrings_LanguageLabel", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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..
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSStrings_Paragraph1 {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ToSStrings_Paragraph1", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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..
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSStrings_Paragraph2 {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ToSStrings_Paragraph2", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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..
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSStrings_Paragraph3 {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ToSStrings_Paragraph3", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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..
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSStrings_Paragraph4 {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ToSStrings_Paragraph4", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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..
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSStrings_Paragraph5 {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ToSStrings_Paragraph5", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to This service is provided as-is. In case of abuse join the Lightless Sync Discord..
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSStrings_Paragraph6 {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ToSStrings_Paragraph6", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to READ THIS CAREFULLY.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToSStrings_ReadLabel {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ToSStrings_ReadLabel", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Users Online.
|
||||||
|
/// </summary>
|
||||||
|
public static string Users_Online {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Users_Online", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
LightlessSync/Resources/Resources.de.resx
Normal file
47
LightlessSync/Resources/Resources.de.resx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<root>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>1.3</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="ToSStrings_LanguageLabel" xml:space="preserve">
|
||||||
|
<value>Language</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_AgreementLabel" xml:space="preserve">
|
||||||
|
<value>Nutzungsbedingungen</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_ReadLabel" xml:space="preserve">
|
||||||
|
<value>BITTE LIES DIES SORGFÄLTIG</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph1" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph2" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph3" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph4" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph5" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph6" xml:space="preserve">
|
||||||
|
<value>Dieser Dienst wird ohne Gewähr angeboten. Im Falle eines Missbrauchs tretet dem Lightless Sync Discord bei.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_AgreeLabel" xml:space="preserve">
|
||||||
|
<value>Ich Stimme zu</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_ButtonWillBeAvailableIn" xml:space="preserve">
|
||||||
|
<value>"Ich stimme zu" Knopf verfügbar in</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
47
LightlessSync/Resources/Resources.fr.resx
Normal file
47
LightlessSync/Resources/Resources.fr.resx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<root>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>1.3</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="ToSStrings_LanguageLabel" xml:space="preserve">
|
||||||
|
<value>Language</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_AgreementLabel" xml:space="preserve">
|
||||||
|
<value>Conditions d'Utilisation</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_ReadLabel" xml:space="preserve">
|
||||||
|
<value>LISEZ CES INFORMATIONS ATTENTIVEMENT</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph1" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph2" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph3" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph4" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph5" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph6" xml:space="preserve">
|
||||||
|
<value>Ce service et ses composants vous sont fournis en l'état. En cas d'abus rejoindre le serveur Discord Lightless Sync.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_AgreeLabel" xml:space="preserve">
|
||||||
|
<value>J'accept</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_ButtonWillBeAvailableIn" xml:space="preserve">
|
||||||
|
<value>Bouton "J'accept" disposible dans</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
57
LightlessSync/Resources/Resources.resx
Normal file
57
LightlessSync/Resources/Resources.resx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>1.3</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="ToSStrings_AgreeLabel" xml:space="preserve">
|
||||||
|
<value>I agree</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_AgreementLabel" xml:space="preserve">
|
||||||
|
<value>Agreement of Usage of Service</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_ButtonWillBeAvailableIn" xml:space="preserve">
|
||||||
|
<value>'I agree' button will be available in</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph1" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph2" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph3" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph4" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph5" xml:space="preserve">
|
||||||
|
<value>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.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_Paragraph6" xml:space="preserve">
|
||||||
|
<value>This service is provided as-is. In case of abuse join the Lightless Sync Discord.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_ReadLabel" xml:space="preserve">
|
||||||
|
<value>READ THIS CAREFULLY</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToSStrings_LanguageLabel" xml:space="preserve">
|
||||||
|
<value>Language</value>
|
||||||
|
</data>
|
||||||
|
<data name="Users_Online" xml:space="preserve">
|
||||||
|
<value>Users Online</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
20
LightlessSync/Resources/Resources.zh.resx
Normal file
20
LightlessSync/Resources/Resources.zh.resx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<root>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>1.3</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="ToSStrings_LanguageLabel" xml:space="preserve">
|
||||||
|
<value>语言</value>
|
||||||
|
</data>
|
||||||
|
<data name="Users_Online" xml:space="preserve">
|
||||||
|
<value>用户在线</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -705,7 +705,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
if (location.ServerId is 0 || location.TerritoryId is 0) return String.Empty;
|
if (location.ServerId is 0 || location.TerritoryId is 0) return String.Empty;
|
||||||
var str = WorldData.Value[(ushort)location.ServerId];
|
var str = WorldData.Value[(ushort)location.ServerId];
|
||||||
|
|
||||||
if (ContentFinderData.Value.TryGetValue(location.TerritoryId , out var dutyName))
|
if (ContentFinderData.Value.TryGetValue(location.TerritoryId, out var dutyName))
|
||||||
{
|
{
|
||||||
str += $" - [In Duty]{dutyName}";
|
str += $" - [In Duty]{dutyName}";
|
||||||
}
|
}
|
||||||
@@ -881,7 +881,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
catch (AccessViolationException ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
|
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
|
||||||
}
|
}
|
||||||
@@ -953,37 +953,87 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern bool IsBadReadPtr(IntPtr ptr, UIntPtr size);
|
||||||
|
|
||||||
|
private static bool IsValidPointer(nint ptr, int size = 8)
|
||||||
|
{
|
||||||
|
if (ptr == nint.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Util.IsWine())
|
||||||
|
{
|
||||||
|
return !IsBadReadPtr(ptr, (UIntPtr)size);
|
||||||
|
}
|
||||||
|
return ptr != nint.Zero && (ptr % IntPtr.Size) == 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
|
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
|
||||||
{
|
{
|
||||||
|
if (address == nint.Zero)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!IsValidPointer(address))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Invalid pointer for character {name} at {addr}", characterName, address.ToString("X"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var gameObj = (GameObject*)address;
|
var gameObj = (GameObject*)address;
|
||||||
|
|
||||||
|
if (gameObj == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_objectTable.Any(o => o?.Address == address))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Character {name} at {addr} no longer in object table", characterName, address.ToString("X"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameObj->ObjectKind == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
var drawObj = gameObj->DrawObject;
|
var drawObj = gameObj->DrawObject;
|
||||||
bool isDrawing = false;
|
bool isDrawing = false;
|
||||||
bool isDrawingChanged = false;
|
bool isDrawingChanged = false;
|
||||||
if ((nint)drawObj != IntPtr.Zero)
|
|
||||||
|
if ((nint)drawObj != IntPtr.Zero && IsValidPointer((nint)drawObj))
|
||||||
{
|
{
|
||||||
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
||||||
|
|
||||||
if (!isDrawing)
|
if (!isDrawing)
|
||||||
{
|
{
|
||||||
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
var charBase = (CharacterBase*)drawObj;
|
||||||
if (!isDrawing)
|
if (charBase != null && IsValidPointer((nint)charBase))
|
||||||
{
|
{
|
||||||
isDrawing = ((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0;
|
isDrawing = charBase->HasModelInSlotLoaded != 0;
|
||||||
if (isDrawing && !string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
if (!isDrawing)
|
||||||
&& !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
_lastGlobalBlockPlayer = characterName;
|
isDrawing = charBase->HasModelFilesInSlotLoaded != 0;
|
||||||
_lastGlobalBlockReason = "HasModelFilesInSlotLoaded";
|
if (isDrawing && !string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
||||||
isDrawingChanged = true;
|
&& !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_lastGlobalBlockPlayer = characterName;
|
||||||
|
_lastGlobalBlockReason = "HasModelFilesInSlotLoaded";
|
||||||
|
isDrawingChanged = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
|
||||||
&& !string.Equals(_lastGlobalBlockReason, "HasModelInSlotLoaded", StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
_lastGlobalBlockPlayer = characterName;
|
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
||||||
_lastGlobalBlockReason = "HasModelInSlotLoaded";
|
&& !string.Equals(_lastGlobalBlockReason, "HasModelInSlotLoaded", StringComparison.Ordinal))
|
||||||
isDrawingChanged = true;
|
{
|
||||||
|
_lastGlobalBlockPlayer = characterName;
|
||||||
|
_lastGlobalBlockReason = "HasModelInSlotLoaded";
|
||||||
|
isDrawingChanged = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1014,6 +1064,11 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
private unsafe void FrameworkOnUpdateInternal()
|
private unsafe void FrameworkOnUpdateInternal()
|
||||||
{
|
{
|
||||||
|
if (!_clientState.IsLoggedIn || _objectTable.LocalPlayer == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ((_objectTable.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
|
if ((_objectTable.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -1033,12 +1088,17 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
||||||
for (var i = 0; i < playerDescriptors.Count; i++)
|
var descriptorCount = playerDescriptors.Count;
|
||||||
|
|
||||||
|
for (var i = 0; i < descriptorCount; i++)
|
||||||
{
|
{
|
||||||
|
if (i >= playerDescriptors.Count)
|
||||||
|
break;
|
||||||
|
|
||||||
var actor = playerDescriptors[i];
|
var actor = playerDescriptors[i];
|
||||||
|
|
||||||
var playerAddress = actor.Address;
|
var playerAddress = actor.Address;
|
||||||
if (playerAddress == nint.Zero)
|
if (playerAddress == nint.Zero || !IsValidPointer(playerAddress))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (actor.ObjectIndex >= 200)
|
if (actor.ObjectIndex >= 200)
|
||||||
@@ -1052,17 +1112,16 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
if (!IsAnythingDrawing)
|
if (!IsAnythingDrawing)
|
||||||
{
|
{
|
||||||
var gameObj = (GameObject*)playerAddress;
|
if (!_objectTable.Any(o => o?.Address == playerAddress))
|
||||||
var currentName = gameObj != null ? gameObj->NameString ?? string.Empty : string.Empty;
|
{
|
||||||
var charaName = string.IsNullOrEmpty(currentName) ? actor.Name : currentName;
|
continue;
|
||||||
CheckCharacterForDrawing(playerAddress, charaName);
|
}
|
||||||
|
|
||||||
|
CheckCharacterForDrawing(playerAddress, actor.Name);
|
||||||
|
|
||||||
if (IsAnythingDrawing)
|
if (IsAnythingDrawing)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1131,7 +1190,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Cutscene
|
// Cutscene
|
||||||
HandleStateTransition(() => IsInCutscene,v => IsInCutscene = v, shouldBeInCutscene, "Cutscene",
|
HandleStateTransition(() => IsInCutscene, v => IsInCutscene = v, shouldBeInCutscene, "Cutscene",
|
||||||
onEnter: () =>
|
onEnter: () =>
|
||||||
{
|
{
|
||||||
Mediator.Publish(new CutsceneStartMessage());
|
Mediator.Publish(new CutsceneStartMessage());
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using FFXIVClientStructs.Havok.Common.Serialize.Resource;
|
||||||
using FFXIVClientStructs.Havok.Animation;
|
using FFXIVClientStructs.Havok.Animation;
|
||||||
using FFXIVClientStructs.Havok.Common.Base.Types;
|
using FFXIVClientStructs.Havok.Common.Base.Types;
|
||||||
|
using FFXIVClientStructs.Havok.Common.Serialize.Resource;
|
||||||
using FFXIVClientStructs.Havok.Common.Serialize.Util;
|
using FFXIVClientStructs.Havok.Common.Serialize.Util;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.GameModel;
|
using LightlessSync.Interop.GameModel;
|
||||||
@@ -9,6 +11,7 @@ using LightlessSync.LightlessConfiguration;
|
|||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using OtterGui.Text.EndObjects;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -129,118 +132,97 @@ public sealed partial class XivDataAnalyzer
|
|||||||
return (output.Count != 0 && output.Values.All(v => v.Count > 0)) ? output : null;
|
return (output.Count != 0 && output.Values.All(v => v.Count > 0)) ? output : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe Dictionary<string, List<ushort>>? GetBoneIndicesFromPap(string hash, bool persistToConfig = true)
|
public static byte[]? ReadHavokBytesFromPap(string papPath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(hash))
|
using var fs = File.Open(papPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
return null;
|
|
||||||
|
|
||||||
if (_configService.Current.BonesDictionary.TryGetValue(hash, out var cached) && cached is not null)
|
|
||||||
return cached;
|
|
||||||
|
|
||||||
var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash);
|
|
||||||
if (cacheEntity == null || string.IsNullOrEmpty(cacheEntity.ResolvedFilepath) || !File.Exists(cacheEntity.ResolvedFilepath))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
using var fs = File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
||||||
using var reader = new BinaryReader(fs);
|
using var reader = new BinaryReader(fs);
|
||||||
|
|
||||||
// PAP header (mostly from vfxeditor)
|
_ = reader.ReadInt32();
|
||||||
_ = reader.ReadInt32(); // ignore
|
_ = reader.ReadInt32();
|
||||||
_ = reader.ReadInt32(); // ignore
|
_ = reader.ReadInt16();
|
||||||
_ = reader.ReadInt16(); // num animations
|
_ = reader.ReadInt16();
|
||||||
_ = reader.ReadInt16(); // modelid
|
|
||||||
|
|
||||||
var type = reader.ReadByte(); // type
|
var type = reader.ReadByte();
|
||||||
if (type != 0)
|
if (type != 0) return null;
|
||||||
return null; // not human
|
|
||||||
|
|
||||||
_ = reader.ReadByte(); // variant
|
_ = reader.ReadByte();
|
||||||
_ = reader.ReadInt32(); // ignore
|
_ = reader.ReadInt32();
|
||||||
|
|
||||||
var havokPosition = reader.ReadInt32();
|
var havokPosition = reader.ReadInt32();
|
||||||
var footerPosition = reader.ReadInt32();
|
var footerPosition = reader.ReadInt32();
|
||||||
|
|
||||||
// sanity checks
|
|
||||||
if (havokPosition <= 0 || footerPosition <= havokPosition || footerPosition > fs.Length)
|
if (havokPosition <= 0 || footerPosition <= havokPosition || footerPosition > fs.Length)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var havokDataSizeLong = (long)footerPosition - havokPosition;
|
var sizeLong = (long)footerPosition - havokPosition;
|
||||||
if (havokDataSizeLong <= 8 || havokDataSizeLong > int.MaxValue)
|
if (sizeLong <= 8 || sizeLong > int.MaxValue)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var havokDataSize = (int)havokDataSizeLong;
|
var size = (int)sizeLong;
|
||||||
|
|
||||||
reader.BaseStream.Position = havokPosition;
|
fs.Position = havokPosition;
|
||||||
var havokData = reader.ReadBytes(havokDataSize);
|
var bytes = reader.ReadBytes(size);
|
||||||
if (havokData.Length <= 8)
|
return bytes.Length > 8 ? bytes : null;
|
||||||
return null;
|
}
|
||||||
|
|
||||||
|
public unsafe Dictionary<string, List<ushort>>? ParseHavokBytesOnFrameworkThread(
|
||||||
|
byte[] havokData,
|
||||||
|
string hash,
|
||||||
|
bool persistToConfig)
|
||||||
|
{
|
||||||
var tempSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
|
var tempSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var tempHavokDataPath = Path.Combine(Path.GetTempPath(), $"lightless_{Guid.NewGuid():N}.hkx");
|
var tempHkxPath = Path.Combine(Path.GetTempPath(), $"lightless_{Guid.NewGuid():N}.hkx");
|
||||||
IntPtr tempHavokDataPathAnsi = IntPtr.Zero;
|
IntPtr pathAnsi = IntPtr.Zero;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllBytes(tempHavokDataPath, havokData);
|
File.WriteAllBytes(tempHkxPath, havokData);
|
||||||
|
|
||||||
if (!File.Exists(tempHavokDataPath))
|
pathAnsi = Marshal.StringToHGlobalAnsi(tempHkxPath);
|
||||||
{
|
|
||||||
_logger.LogTrace("Temporary havok file did not exist when attempting to load: {path}", tempHavokDataPath);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath);
|
hkSerializeUtil.LoadOptions loadOptions = default;
|
||||||
|
loadOptions.TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry();
|
||||||
var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1];
|
loadOptions.ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry();
|
||||||
loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry();
|
loadOptions.Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int>
|
||||||
loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry();
|
|
||||||
loadoptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int>
|
|
||||||
{
|
{
|
||||||
Storage = (int)hkSerializeUtil.LoadOptionBits.Default
|
Storage = (int)hkSerializeUtil.LoadOptionBits.Default
|
||||||
};
|
};
|
||||||
|
|
||||||
var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions);
|
hkSerializeUtil.LoadOptions* pOpts = &loadOptions;
|
||||||
|
|
||||||
|
var resource = hkSerializeUtil.LoadFromFile((byte*)pathAnsi, errorResult: null, pOpts);
|
||||||
if (resource == null)
|
if (resource == null)
|
||||||
{
|
|
||||||
_logger.LogWarning("Havok resource was null after loading from {path}", tempHavokDataPath);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
var rootLevelName = @"hkRootLevelContainer"u8;
|
var rootLevelName = @"hkRootLevelContainer"u8;
|
||||||
fixed (byte* n1 = rootLevelName)
|
fixed (byte* n1 = rootLevelName)
|
||||||
{
|
{
|
||||||
var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry());
|
var container = (hkRootLevelContainer*)resource->GetContentsPointer(
|
||||||
if (container == null)
|
n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry());
|
||||||
return null;
|
|
||||||
|
if (container == null) return null;
|
||||||
|
|
||||||
var animationName = @"hkaAnimationContainer"u8;
|
var animationName = @"hkaAnimationContainer"u8;
|
||||||
fixed (byte* n2 = animationName)
|
fixed (byte* n2 = animationName)
|
||||||
{
|
{
|
||||||
var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null);
|
var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null);
|
||||||
if (animContainer == null)
|
if (animContainer == null) return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
for (int i = 0; i < animContainer->Bindings.Length; i++)
|
for (int i = 0; i < animContainer->Bindings.Length; i++)
|
||||||
{
|
{
|
||||||
var binding = animContainer->Bindings[i].ptr;
|
var binding = animContainer->Bindings[i].ptr;
|
||||||
if (binding == null)
|
if (binding == null) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
var rawSkel = binding->OriginalSkeletonName.String;
|
var rawSkel = binding->OriginalSkeletonName.String;
|
||||||
var skeletonKey = CanonicalizeSkeletonKey(rawSkel);
|
var skeletonKey = CanonicalizeSkeletonKey(rawSkel);
|
||||||
if (string.IsNullOrEmpty(skeletonKey))
|
if (string.IsNullOrEmpty(skeletonKey)) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
var boneTransform = binding->TransformTrackToBoneIndices;
|
var boneTransform = binding->TransformTrackToBoneIndices;
|
||||||
if (boneTransform.Length <= 0)
|
if (boneTransform.Length <= 0) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!tempSets.TryGetValue(skeletonKey, out var set))
|
if (!tempSets.TryGetValue(skeletonKey, out var set))
|
||||||
{
|
tempSets[skeletonKey] = set = [];
|
||||||
set = [];
|
|
||||||
tempSets[skeletonKey] = set;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++)
|
for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++)
|
||||||
{
|
{
|
||||||
@@ -252,52 +234,34 @@ public sealed partial class XivDataAnalyzer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (tempHavokDataPathAnsi != IntPtr.Zero)
|
if (pathAnsi != IntPtr.Zero)
|
||||||
Marshal.FreeHGlobal(tempHavokDataPathAnsi);
|
Marshal.FreeHGlobal(pathAnsi);
|
||||||
|
|
||||||
try
|
try { if (File.Exists(tempHkxPath)) File.Delete(tempHkxPath); }
|
||||||
{
|
catch { /* ignore */ }
|
||||||
if (File.Exists(tempHavokDataPath))
|
|
||||||
File.Delete(tempHavokDataPath);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogTrace(ex, "Could not delete temporary havok file: {path}", tempHavokDataPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tempSets.Count == 0)
|
if (tempSets.Count == 0) return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
var output = new Dictionary<string, List<ushort>>(tempSets.Count, StringComparer.OrdinalIgnoreCase);
|
var output = new Dictionary<string, List<ushort>>(tempSets.Count, StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var (key, set) in tempSets)
|
foreach (var (key, set) in tempSets)
|
||||||
{
|
{
|
||||||
if (set.Count == 0) continue;
|
if (set.Count == 0) continue;
|
||||||
|
|
||||||
var list = set.ToList();
|
var list = set.ToList();
|
||||||
list.Sort();
|
list.Sort();
|
||||||
output[key] = list;
|
output[key] = list;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (output.Count == 0)
|
if (output.Count == 0) return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
_configService.Current.BonesDictionary[hash] = output;
|
_configService.Current.BonesDictionary[hash] = output;
|
||||||
|
if (persistToConfig) _configService.Save();
|
||||||
if (persistToConfig)
|
|
||||||
_configService.Save();
|
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static string CanonicalizeSkeletonKey(string? raw)
|
public static string CanonicalizeSkeletonKey(string? raw)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(raw))
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
@@ -375,41 +339,56 @@ public sealed partial class XivDataAnalyzer
|
|||||||
if (mode == AnimationValidationMode.Unsafe)
|
if (mode == AnimationValidationMode.Unsafe)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
var papBuckets = papBoneIndices.Keys
|
var papByBucket = new Dictionary<string, List<ushort>>(StringComparer.OrdinalIgnoreCase);
|
||||||
.Select(CanonicalizeSkeletonKey)
|
|
||||||
.Where(k => !string.IsNullOrEmpty(k))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (papBuckets.Count == 0)
|
foreach (var (rawKey, list) in papBoneIndices)
|
||||||
|
{
|
||||||
|
var key = CanonicalizeSkeletonKey(rawKey);
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (string.Equals(key, "skeleton", StringComparison.OrdinalIgnoreCase))
|
||||||
|
key = "__any__";
|
||||||
|
|
||||||
|
if (!papByBucket.TryGetValue(key, out var acc))
|
||||||
|
papByBucket[key] = acc = [];
|
||||||
|
|
||||||
|
if (list is { Count: > 0 })
|
||||||
|
acc.AddRange(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var k in papByBucket.Keys.ToList())
|
||||||
|
papByBucket[k] = papByBucket[k].Distinct().ToList();
|
||||||
|
|
||||||
|
if (papByBucket.Count == 0)
|
||||||
{
|
{
|
||||||
reason = "No skeleton bucket bindings found in the PAP";
|
reason = "No skeleton bucket bindings found in the PAP";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode == AnimationValidationMode.Safe)
|
static bool AllIndicesOk(
|
||||||
|
HashSet<ushort> available,
|
||||||
|
List<ushort> indices,
|
||||||
|
bool papLikelyOneBased,
|
||||||
|
bool allowOneBasedShift,
|
||||||
|
bool allowNeighborTolerance,
|
||||||
|
out ushort missing)
|
||||||
{
|
{
|
||||||
if (papBuckets.Any(b => localBoneSets.ContainsKey(b)))
|
foreach (var idx in indices)
|
||||||
return true;
|
|
||||||
|
|
||||||
reason = $"No matching skeleton bucket between PAP [{string.Join(", ", papBuckets)}] and local [{string.Join(", ", localBoneSets.Keys.Order())}].";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var bucket in papBuckets)
|
|
||||||
{
|
|
||||||
if (!localBoneSets.TryGetValue(bucket, out var available))
|
|
||||||
{
|
{
|
||||||
reason = $"Missing skeleton bucket '{bucket}' on local actor.";
|
if (!ContainsIndexCompat(available, idx, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance))
|
||||||
return false;
|
{
|
||||||
|
missing = idx;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var indices = papBoneIndices
|
missing = 0;
|
||||||
.Where(kvp => string.Equals(CanonicalizeSkeletonKey(kvp.Key), bucket, StringComparison.OrdinalIgnoreCase))
|
return true;
|
||||||
.SelectMany(kvp => kvp.Value ?? Enumerable.Empty<ushort>())
|
}
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
|
foreach (var (bucket, indices) in papByBucket)
|
||||||
|
{
|
||||||
if (indices.Count == 0)
|
if (indices.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -423,14 +402,32 @@ public sealed partial class XivDataAnalyzer
|
|||||||
}
|
}
|
||||||
bool papLikelyOneBased = allowOneBasedShift && (min == 1) && has1 && !has0;
|
bool papLikelyOneBased = allowOneBasedShift && (min == 1) && has1 && !has0;
|
||||||
|
|
||||||
foreach (var idx in indices)
|
if (string.Equals(bucket, "__any__", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (!ContainsIndexCompat(available, idx, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance))
|
foreach (var (lk, ls) in localBoneSets)
|
||||||
{
|
{
|
||||||
reason = $"No compatible local skeleton for PAP '{bucket}': missing bone index {idx}.";
|
if (AllIndicesOk(ls, indices, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance, out _))
|
||||||
return false;
|
goto nextBucket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reason = $"No compatible local skeleton bucket for generic PAP skeleton '{bucket}'. Local buckets: {string.Join(", ", localBoneSets.Keys)}";
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!localBoneSets.TryGetValue(bucket, out var available))
|
||||||
|
{
|
||||||
|
reason = $"Missing skeleton bucket '{bucket}' on local actor.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AllIndicesOk(available, indices, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance, out var missing))
|
||||||
|
{
|
||||||
|
reason = $"No compatible local skeleton for PAP '{bucket}': missing bone index {missing}.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextBucket:
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -52,12 +52,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly PairLedger _pairLedger;
|
private readonly PairLedger _pairLedger;
|
||||||
private readonly ConcurrentDictionary<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
|
||||||
private readonly DrawEntityFactory _drawEntityFactory;
|
|
||||||
private readonly FileUploadManager _fileTransferManager;
|
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly TagHandler _tagHandler;
|
private readonly TagHandler _tagHandler;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
@@ -171,9 +167,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
|
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
|
||||||
Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiSharedService_GposeStart());
|
Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiSharedService_GposeStart());
|
||||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
||||||
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
|
Mediator.Subscribe<DownloadStartedMessage>(this, msg =>
|
||||||
|
{
|
||||||
|
_currentDownloads[msg.DownloadId] = new Dictionary<string, FileDownloadStatus>(msg.DownloadStatus, StringComparer.Ordinal);
|
||||||
|
});
|
||||||
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
||||||
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = DrawFolders.ToList());
|
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = [.. DrawFolders]);
|
||||||
|
|
||||||
_characterAnalyzer = characterAnalyzer;
|
_characterAnalyzer = characterAnalyzer;
|
||||||
_playerPerformanceConfig = playerPerformanceConfig;
|
_playerPerformanceConfig = playerPerformanceConfig;
|
||||||
|
|||||||
@@ -65,12 +65,14 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
|
|
||||||
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) =>
|
Mediator.Subscribe<DownloadStartedMessage>(this, msg =>
|
||||||
{
|
{
|
||||||
_currentDownloads[msg.DownloadId] = msg.DownloadStatus;
|
_currentDownloads[msg.DownloadId] = msg.DownloadStatus;
|
||||||
// Capture initial totals when download starts
|
|
||||||
var totalFiles = msg.DownloadStatus.Values.Sum(s => s.TotalFiles);
|
var snap = msg.DownloadStatus.ToArray();
|
||||||
var totalBytes = msg.DownloadStatus.Values.Sum(s => s.TotalBytes);
|
var totalFiles = snap.Sum(kv => kv.Value?.TotalFiles ?? 0);
|
||||||
|
var totalBytes = snap.Sum(kv => kv.Value?.TotalBytes ?? 0);
|
||||||
|
|
||||||
_downloadInitialTotals[msg.DownloadId] = (totalFiles, totalBytes);
|
_downloadInitialTotals[msg.DownloadId] = (totalFiles, totalBytes);
|
||||||
_notificationDismissed = false;
|
_notificationDismissed = false;
|
||||||
});
|
});
|
||||||
@@ -79,7 +81,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
_currentDownloads.TryRemove(msg.DownloadId, out _);
|
_currentDownloads.TryRemove(msg.DownloadId, out _);
|
||||||
|
|
||||||
// Dismiss notification if all downloads are complete
|
// Dismiss notification if all downloads are complete
|
||||||
if (!_currentDownloads.Any() && !_notificationDismissed)
|
if (_currentDownloads.IsEmpty && !_notificationDismissed)
|
||||||
{
|
{
|
||||||
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
||||||
_notificationDismissed = true;
|
_notificationDismissed = true;
|
||||||
@@ -474,7 +476,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
totalBytes += playerTotalBytes;
|
totalBytes += playerTotalBytes;
|
||||||
transferredBytes += playerTransferredBytes;
|
transferredBytes += playerTransferredBytes;
|
||||||
|
|
||||||
// per-player W/Q/P/D
|
// per-player W/Q/P/D/C
|
||||||
var playerDlSlot = 0;
|
var playerDlSlot = 0;
|
||||||
var playerDlQueue = 0;
|
var playerDlQueue = 0;
|
||||||
var playerDlProg = 0;
|
var playerDlProg = 0;
|
||||||
@@ -487,6 +489,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
switch (fileStatus.DownloadStatus)
|
switch (fileStatus.DownloadStatus)
|
||||||
{
|
{
|
||||||
case DownloadStatus.Initializing:
|
case DownloadStatus.Initializing:
|
||||||
|
case DownloadStatus.WaitingForQueue:
|
||||||
playerDlQueue++;
|
playerDlQueue++;
|
||||||
totalDlQueue++;
|
totalDlQueue++;
|
||||||
break;
|
break;
|
||||||
@@ -494,10 +497,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
playerDlSlot++;
|
playerDlSlot++;
|
||||||
totalDlSlot++;
|
totalDlSlot++;
|
||||||
break;
|
break;
|
||||||
case DownloadStatus.WaitingForQueue:
|
|
||||||
playerDlQueue++;
|
|
||||||
totalDlQueue++;
|
|
||||||
break;
|
|
||||||
case DownloadStatus.Downloading:
|
case DownloadStatus.Downloading:
|
||||||
playerDlProg++;
|
playerDlProg++;
|
||||||
totalDlProg++;
|
totalDlProg++;
|
||||||
@@ -550,11 +549,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
if (totalFiles == 0 || totalBytes == 0)
|
if (totalFiles == 0 || totalBytes == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// max speed for per-player bar scale (clamped)
|
|
||||||
double maxSpeed = perPlayer.Count > 0 ? perPlayer.Max(p => p.SpeedBytesPerSecond) : 0;
|
|
||||||
if (maxSpeed <= 0)
|
|
||||||
maxSpeed = 1;
|
|
||||||
|
|
||||||
var drawList = ImGui.GetBackgroundDrawList();
|
var drawList = ImGui.GetBackgroundDrawList();
|
||||||
var windowPos = ImGui.GetWindowPos();
|
var windowPos = ImGui.GetWindowPos();
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ using Dalamud.Utility;
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.Localization;
|
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly CacheMonitor _cacheMonitor;
|
private readonly CacheMonitor _cacheMonitor;
|
||||||
private readonly Dictionary<string, string> _languages = new(StringComparer.Ordinal) { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } };
|
private readonly Dictionary<string, string> _languages = new(StringComparer.Ordinal) { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" }, { "中文", "zh"} };
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly UiSharedService _uiShared;
|
private readonly UiSharedService _uiShared;
|
||||||
@@ -31,7 +31,6 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
private string _secretKey = string.Empty;
|
private string _secretKey = string.Empty;
|
||||||
private string _timeoutLabel = string.Empty;
|
private string _timeoutLabel = string.Empty;
|
||||||
private Task? _timeoutTask;
|
private Task? _timeoutTask;
|
||||||
private string[]? _tosParagraphs;
|
|
||||||
private bool _useLegacyLogin = false;
|
private bool _useLegacyLogin = false;
|
||||||
|
|
||||||
public IntroUi(ILogger<IntroUi> logger, UiSharedService uiShared, LightlessConfigService configService,
|
public IntroUi(ILogger<IntroUi> logger, UiSharedService uiShared, LightlessConfigService configService,
|
||||||
@@ -51,7 +50,6 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
.SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 2000))
|
.SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 2000))
|
||||||
.Apply();
|
.Apply();
|
||||||
|
|
||||||
GetToSLocalization();
|
|
||||||
|
|
||||||
Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => IsOpen = false);
|
Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => IsOpen = false);
|
||||||
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) =>
|
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) =>
|
||||||
@@ -88,7 +86,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
for (int i = 60; i > 0; i--)
|
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);
|
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -102,44 +100,46 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
Vector2 textSize;
|
Vector2 textSize;
|
||||||
using (_uiShared.UidFont.Push())
|
using (_uiShared.UidFont.Push())
|
||||||
{
|
{
|
||||||
textSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel);
|
textSize = ImGui.CalcTextSize(Resources.Resources.ToSStrings_LanguageLabel);
|
||||||
ImGui.TextUnformatted(Strings.ToS.AgreementLabel);
|
ImGui.TextUnformatted(Resources.Resources.ToSStrings_AgreementLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
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.SetCursorPosX(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - languageSize.X - 80);
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - languageSize.Y / 2);
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - languageSize.Y / 2);
|
||||||
|
|
||||||
ImGui.TextUnformatted(Strings.ToS.LanguageLabel);
|
ImGui.TextUnformatted(Resources.Resources.ToSStrings_LanguageLabel);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - (languageSize.Y + ImGui.GetStyle().FramePadding.Y) / 2);
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - (languageSize.Y + ImGui.GetStyle().FramePadding.Y) / 2);
|
||||||
ImGui.SetNextItemWidth(80);
|
ImGui.SetNextItemWidth(80);
|
||||||
if (ImGui.Combo("", ref _currentLanguage, _languages.Keys.ToArray(), _languages.Count))
|
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.Separator();
|
||||||
ImGui.SetWindowFontScale(1.5f);
|
ImGui.SetWindowFontScale(1.5f);
|
||||||
string readThis = Strings.ToS.ReadLabel;
|
string readThis = Resources.Resources.ToSStrings_ReadLabel;
|
||||||
textSize = ImGui.CalcTextSize(readThis);
|
textSize = ImGui.CalcTextSize(readThis);
|
||||||
ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2);
|
ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2);
|
||||||
UiSharedService.ColorText(readThis, ImGuiColors.DalamudRed);
|
UiSharedService.ColorText(readThis, ImGuiColors.DalamudRed);
|
||||||
ImGui.SetWindowFontScale(1.0f);
|
ImGui.SetWindowFontScale(1.0f);
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
UiSharedService.TextWrapped(_tosParagraphs![0]);
|
UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph1);
|
||||||
UiSharedService.TextWrapped(_tosParagraphs![1]);
|
UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph2);
|
||||||
UiSharedService.TextWrapped(_tosParagraphs![2]);
|
UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph3);
|
||||||
UiSharedService.TextWrapped(_tosParagraphs![3]);
|
UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph4);
|
||||||
UiSharedService.TextWrapped(_tosParagraphs![4]);
|
UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph5);
|
||||||
UiSharedService.TextWrapped(_tosParagraphs![5]);
|
UiSharedService.TextWrapped(Resources.Resources.ToSStrings_Paragraph6);
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
if (_timeoutTask?.IsCompleted ?? true)
|
if (_timeoutTask?.IsCompleted ?? true)
|
||||||
{
|
{
|
||||||
if (ImGui.Button(Strings.ToS.AgreeLabel + "##toSetup"))
|
if (ImGui.Button(Resources.Resources.ToSStrings_AgreeLabel + "##toSetup"))
|
||||||
{
|
{
|
||||||
_configService.Current.AcceptedAgreement = true;
|
_configService.Current.AcceptedAgreement = true;
|
||||||
_configService.Save();
|
_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)]
|
[GeneratedRegex("^[A-F0-9]{64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant)]
|
||||||
private static partial Regex SecretRegex();
|
private static partial Regex SecretRegex();
|
||||||
}
|
}
|
||||||
@@ -4812,7 +4812,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.TextColored(UIColors.Get("LightlessBlue"),
|
ImGui.TextColored(UIColors.Get("LightlessBlue"),
|
||||||
_apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
|
_apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.TextUnformatted("Users Online");
|
ImGui.TextUnformatted(Resources.Resources.Users_Online);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.TextUnformatted(")");
|
ImGui.TextUnformatted(")");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ using LightlessSync.Interop.Ipc;
|
|||||||
using LightlessSync.Interop.Ipc.Framework;
|
using LightlessSync.Interop.Ipc.Framework;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.Localization;
|
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
@@ -1468,12 +1467,6 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadLocalization(string languageCode)
|
|
||||||
{
|
|
||||||
_localization.SetupWithLangCode(languageCode);
|
|
||||||
Strings.ToS = new Strings.ToSStrings();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void DistanceSeparator()
|
internal static void DistanceSeparator()
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|||||||
@@ -504,7 +504,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
&& decompressed.LongLength != expectedRawSize)
|
&& decompressed.LongLength != expectedRawSize)
|
||||||
{
|
{
|
||||||
await _fileCompactor.WriteAllBytesAsync(filePath, Array.Empty<byte>(), ct).ConfigureAwait(false);
|
await _fileCompactor.WriteAllBytesAsync(filePath, Array.Empty<byte>(), ct).ConfigureAwait(false);
|
||||||
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
|
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale, skipDecimation);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,7 +526,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
// write to file without compacting during download
|
// write to file without compacting during download
|
||||||
await File.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false);
|
await File.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false);
|
||||||
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
|
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale, skipDecimation);
|
||||||
}, ct).ConfigureAwait(false);
|
}, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
Reference in New Issue
Block a user