Compare commits

...

14 Commits

Author SHA1 Message Date
e6df37bcca Merge pull request 'slight-tweaks' (#143) from slight-tweaks into 2.0.3
Reviewed-on: #143
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
2026-01-20 15:49:04 +00:00
60d144b881 adjust log level 2026-01-21 00:47:55 +09:00
5089dbd6c8 move texture and model processing out of download thread and fix some normalization for weights 2026-01-21 00:17:04 +09:00
cake
d8335eb04f Removal of log 2026-01-20 02:44:50 +01:00
cake
994335c6b0 Moved to trace log 2026-01-20 02:40:30 +01:00
68b4863f52 Merge pull request 'Added CLR fix for gameobject handler' (#142) from clr-fix-attempt into 2.0.3
Reviewed-on: #142
2026-01-20 00:02:58 +00:00
cake
22fe9901a4 Fixed some issues. 2026-01-20 00:25:26 +01:00
cake
cff866dcc2 Added ptrguard to be used whenever ptrs are being used. 2026-01-20 00:24:42 +01:00
06f89955d3 Merge pull request 'clr-fix-attempt' (#141) from clr-fix-attempt into 2.0.3
Reviewed-on: #141
2026-01-19 18:33:55 +00:00
cake
367af2c3d0 Remove blankspace 2026-01-19 19:26:00 +01:00
cake
19a238c808 Removal of unsafe and check if PTR is correctly aligning, checking in virtual query instead of memory 2026-01-19 19:24:44 +01:00
7d86b41cee Merge pull request 'update-decimation-filters' (#140) from update-decimation-filters into 2.0.3
Reviewed-on: #140
2026-01-19 17:22:27 +00:00
0185e6b534 Merge pull request 'Implement Lifestream With Location Sharing.' (#139) from lifestream-location-share into 2.0.3
Reviewed-on: #139
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
2026-01-19 17:02:13 +00:00
defnotken
90bf84f8eb Implement Lifestream With Location Sharing. 2026-01-19 10:58:37 -06:00
18 changed files with 692 additions and 98 deletions

View File

@@ -0,0 +1,51 @@
namespace Lifestream.Enums;
public enum TerritoryTypeIdHousing
{
None = -1,
// Mist (Limsa Lominsa)
Mist = 339,
MistSmall = 282,
MistMedium = 283,
MistLarge = 284,
MistFCRoom = 384,
MistFCWorkshop = 423,
MistApartment = 608,
// Lavender Beds (Gridania)
Lavender = 340,
LavenderSmall = 342,
LavenderMedium = 343,
LavenderLarge = 344,
LavenderFCRoom = 385,
LavenderFCWorkshop = 425,
LavenderApartment = 609,
// Goblet (Ul'dah)
Goblet = 341,
GobletSmall = 345,
GobletMedium = 346,
GobletLarge = 347,
GobletFCRoom = 386,
GobletFCWorkshop = 424,
GobletApartment = 610,
// Shirogane (Kugane)
Shirogane = 641,
ShiroganeSmall = 649,
ShiroganeMedium = 650,
ShiroganeLarge = 651,
ShiroganeFCRoom = 652,
ShiroganeFCWorkshop = 653,
ShiroganeApartment = 655,
// Empyreum (Ishgard)
Empyream = 979,
EmpyreamSmall = 980,
EmpyreamMedium = 981,
EmpyreamLarge = 982,
EmpyreamFCRoom = 983,
EmpyreamFCWorkshop = 984,
EmpyreamApartment = 999,
}

View File

@@ -92,7 +92,7 @@ public sealed class PenumbraTexture : PenumbraBase
{
token.ThrowIfCancellationRequested();
logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
logger.LogDebug("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps);
await convertTask.ConfigureAwait(false);

View File

@@ -1,6 +1,5 @@
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using LightlessSync.API.Data.Enum;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
@@ -14,6 +13,7 @@ using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.PlayerData.Factories;
@@ -119,46 +119,30 @@ public class PlayerDataFactory
return null;
}
private static readonly int _drawObjectOffset =
(int)Marshal.OffsetOf<GameObject>(nameof(GameObject.DrawObject));
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
private unsafe static bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
{
if (playerPointer == IntPtr.Zero)
return true;
if (!IsPointerValid(playerPointer))
return true;
var character = (Character*)playerPointer;
if (character == null)
return true;
var gameObject = &character->GameObject;
if (gameObject == null)
return true;
if (!IsPointerValid((IntPtr)gameObject))
return true;
return gameObject->DrawObject == null;
}
private static bool IsPointerValid(IntPtr ptr)
{
if (ptr == IntPtr.Zero)
return false;
try
=> await _dalamudUtil.RunOnFrameworkThread(() =>
{
_ = Marshal.ReadByte(ptr);
return true;
}
catch
{
return false;
}
}
nint basePtr = playerPointer;
if (!PtrGuard.LooksLikePtr(basePtr))
return true;
nint drawObjAddr = basePtr + _drawObjectOffset;
if (!PtrGuard.IsReadable(drawObjAddr, (nuint)IntPtr.Size))
return true;
if (!PtrGuard.TryReadIntPtr(drawObjAddr, out var drawObj))
return true;
if (drawObj != 0 && !PtrGuard.LooksLikePtr(drawObj))
return true;
return drawObj == 0;
}).ConfigureAwait(false);
private static bool IsCacheFresh(CacheEntry entry)
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;

View File

@@ -2,11 +2,12 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
namespace LightlessSync.PlayerData.Handlers;
@@ -177,18 +178,41 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
var prevDrawObj = DrawObjectAddress;
string? nameString = null;
Address = _getAddress();
var nextAddr = _getAddress();
if (nextAddr != IntPtr.Zero && !PtrGuard.LooksLikePtr(nextAddr))
{
nextAddr = IntPtr.Zero;
}
if (nextAddr != IntPtr.Zero &&
!PtrGuard.IsReadable(nextAddr, (nuint)sizeof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject)))
{
nextAddr = IntPtr.Zero;
}
Address = nextAddr;
if (Address != IntPtr.Zero)
{
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
DrawObjectAddress = (IntPtr)gameObject->DrawObject;
var draw = (nint)gameObject->DrawObject;
if (!PtrGuard.LooksLikePtr(draw) || !PtrGuard.IsReadable(draw, (nuint)sizeof(DrawObject)))
draw = 0;
DrawObjectAddress = draw;
EntityId = gameObject->EntityId;
var chara = (Character*)Address;
nameString = chara->GameObject.NameString;
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
Name = nameString;
if (PtrGuard.IsReadable(Address, (nuint)sizeof(Character)))
{
var chara = (Character*)Address;
nameString = chara->GameObject.NameString;
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
Name = nameString;
}
}
else
{
@@ -196,22 +220,27 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
EntityId = uint.MaxValue;
}
CurrentDrawCondition = IsBeingDrawnUnsafe();
CurrentDrawCondition = (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
? IsBeingDrawnUnsafe()
: DrawCondition.DrawObjectZero;
if (_haltProcessing || !allowPublish) return;
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
bool addrDiff = Address != prevAddr;
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero
&& PtrGuard.IsReadable(Address, (nuint)sizeof(Character))
&& PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(DrawObject)))
{
var chara = (Character*)Address;
var drawObj = (DrawObject*)DrawObjectAddress;
var objType = drawObj->Object.GetObjectType();
var isHuman = objType == ObjectType.CharacterBase
&& ((CharacterBase*)drawObj)->GetModelType() == CharacterBase.ModelType.Human;
nameString ??= ((Character*)Address)->GameObject.NameString;
nameString ??= chara->GameObject.NameString;
var nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
if (nameChange) Name = nameString;
@@ -219,32 +248,36 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
if (isHuman)
{
var classJob = chara->CharacterData.ClassJob;
if (classJob != _classJob)
if (PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
{
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
_classJob = classJob;
Mediator.Publish(new ClassJobChangedMessage(this));
var classJob = chara->CharacterData.ClassJob;
if (classJob != _classJob)
{
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
_classJob = classJob;
Mediator.Publish(new ClassJobChangedMessage(this));
}
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
}
else
{
isHuman = false;
}
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
if (equipDiff)
Logger.LogTrace("Checking [{this}] equip data as human from draw obj, result: {diff}", this, equipDiff);
}
else
if (!isHuman)
{
equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0]));
if (equipDiff)
Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff);
}
if (equipDiff && !_isOwnedObject) // send the message out immediately and cancel out, no reason to continue if not self
if (equipDiff && !_isOwnedObject)
{
Logger.LogTrace("[{this}] Changed", this);
return;
@@ -252,11 +285,13 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
bool customizeDiff = false;
if (isHuman)
if (isHuman && PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
{
var gender = ((Human*)drawObj)->Customize.Sex;
var raceId = ((Human*)drawObj)->Customize.Race;
var tribeId = ((Human*)drawObj)->Customize.Tribe;
var human = (Human*)drawObj;
var gender = human->Customize.Sex;
var raceId = human->Customize.Race;
var tribeId = human->Customize.Tribe;
if (_isOwnedObject && ObjectKind == ObjectKind.Player
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
@@ -267,15 +302,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
TribeId = tribeId;
}
customizeDiff = CompareAndUpdateCustomizeData(((Human*)drawObj)->Customize.Data);
if (customizeDiff)
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
customizeDiff = CompareAndUpdateCustomizeData(human->Customize.Data);
}
else
{
customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data);
if (customizeDiff)
Logger.LogTrace("Checking [{this}] customize data from game obj, result: {diff}", this, equipDiff);
}
if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject)
@@ -289,12 +320,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
CurrentDrawCondition = DrawCondition.DrawObjectZero;
Logger.LogTrace("[{this}] Changed", this);
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
{
Mediator.Publish(new ClearCacheForObjectMessage(this));
}
}
}
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
{
bool hasChanges = false;
@@ -330,7 +360,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
private unsafe bool CompareAndUpdateMainHand(Weapon* weapon)
{
if ((nint)weapon == nint.Zero) return false;
var p = (nint)weapon;
if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon)))
return false;
bool hasChanges = false;
hasChanges |= weapon->ModelSetId != MainHandData[0];
MainHandData[0] = weapon->ModelSetId;
@@ -343,7 +376,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
private unsafe bool CompareAndUpdateOffHand(Weapon* weapon)
{
if ((nint)weapon == nint.Zero) return false;
var p = (nint)weapon;
if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon)))
return false;
bool hasChanges = false;
hasChanges |= weapon->ModelSetId != OffHandData[0];
OffHandData[0] = weapon->ModelSetId;

View File

@@ -125,6 +125,8 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<FileTransferOrchestrator>();
services.AddSingleton<LightlessPlugin>();
services.AddSingleton<LightlessProfileManager>();
services.AddSingleton<TextureProcessingQueue>();
services.AddSingleton<ModelProcessingQueue>();
services.AddSingleton<TextureCompressionService>();
services.AddSingleton<TextureDownscaleService>();
services.AddSingleton<ModelDecimationService>();

View File

@@ -0,0 +1,93 @@
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
namespace LightlessSync.Services;
public sealed class AssetProcessingQueue : IDisposable
{
private readonly BlockingCollection<WorkItem> _queue = new();
private readonly Thread _worker;
private readonly ILogger _logger;
private bool _disposed;
public AssetProcessingQueue(ILogger logger, string name)
{
_logger = logger;
_worker = new Thread(Run)
{
IsBackground = true,
Name = string.IsNullOrWhiteSpace(name) ? "LightlessSync.AssetProcessing" : name
};
_worker.Start();
}
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
{
if (work is null)
{
throw new ArgumentNullException(nameof(work));
}
var completion = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
if (token.IsCancellationRequested)
{
completion.TrySetCanceled(token);
return completion.Task;
}
if (_queue.IsAddingCompleted || _disposed)
{
completion.TrySetException(new ObjectDisposedException(nameof(AssetProcessingQueue)));
return completion.Task;
}
_queue.Add(new WorkItem(work, token, completion));
return completion.Task;
}
private void Run()
{
foreach (var item in _queue.GetConsumingEnumerable())
{
if (item.Token.IsCancellationRequested)
{
item.Completion.TrySetCanceled(item.Token);
continue;
}
try
{
item.Work(item.Token).GetAwaiter().GetResult();
item.Completion.TrySetResult(null);
}
catch (OperationCanceledException ex)
{
var token = ex.CancellationToken.IsCancellationRequested ? ex.CancellationToken : item.Token;
item.Completion.TrySetCanceled(token);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Asset processing job failed.");
item.Completion.TrySetException(ex);
}
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_queue.CompleteAdding();
_worker.Join(TimeSpan.FromSeconds(2));
_queue.Dispose();
}
private readonly record struct WorkItem(
Func<CancellationToken, Task> Work,
CancellationToken Token,
TaskCompletionSource<object?> Completion);
}

View File

@@ -701,7 +701,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
str += $" Room #{location.RoomId}";
}
}
return str;
}
public string LocationToLifestream(LocationInfo location)
{
if (location.ServerId is 0 || location.TerritoryId is 0 || ContentFinderData.Value.ContainsKey(location.TerritoryId)) return String.Empty;
var str = WorldData.Value[(ushort)location.ServerId];
if (location.HouseId is 0 && location.MapId is not 0)
{
var mapName = MapData.Value[(ushort)location.MapId].MapName;
var parts = mapName.Split(" - ", StringSplitOptions.RemoveEmptyEntries);
var locationName = parts.Length > 0 ? parts[^1] : mapName;
str += $", tp {locationName}";
string message = $"LocationToLifestream: {str}";
_logger.LogInformation(message);
}
return str;
}

View File

@@ -1,3 +1,4 @@
using Lifestream.Enums;
using LightlessSync.API.Data;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.User;
@@ -108,6 +109,144 @@ namespace LightlessSync.Services
}
}
public LocationInfo? GetLocationForLifestreamByUid(string uid)
{
try
{
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
{
return location;
}
return null;
}
catch (Exception e)
{
Logger.LogError(e,"GetLocationInfoByUid error : ");
throw;
}
}
public AddressBookEntryTuple? GetAddressBookEntryByLocation(LocationInfo location)
{
if (location.ServerId is 0 || location.TerritoryId is 0)
{
return null;
}
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
if (territoryHousing == TerritoryTypeIdHousing.None || !Enum.IsDefined(typeof(TerritoryTypeIdHousing), territoryHousing))
{
return null;
}
var city = GetResidentialAetheryteKind(territoryHousing);
if (city == ResidentialAetheryteKind.None)
{
return null;
}
if (location.HouseId is not 0 and not 100)
{
AddressBookEntryTuple addressEntry = (
Name: "",
World: (int)location.ServerId,
City: (int)city,
Ward: (int)location.WardId,
PropertyType: 0,
Plot: (int)location.HouseId,
Apartment: 0,
ApartmentSubdivision: location.DivisionId == 2,
AliasEnabled: false,
Alias: ""
);
return addressEntry;
}
else if (location.HouseId is 100)
{
AddressBookEntryTuple addressEntry = (
Name: "",
World: (int)location.ServerId,
City: (int)city,
Ward: (int)location.WardId,
PropertyType: 1,
Plot: 0,
Apartment: (int)location.RoomId,
ApartmentSubdivision: location.DivisionId == 2,
AliasEnabled: false,
Alias: ""
);
return addressEntry;
}
return null;
}
private ResidentialAetheryteKind GetResidentialAetheryteKind(TerritoryTypeIdHousing territoryHousing)
{
return territoryHousing switch
{
TerritoryTypeIdHousing.Shirogane or
TerritoryTypeIdHousing.ShiroganeApartment or
TerritoryTypeIdHousing.ShiroganeSmall or
TerritoryTypeIdHousing.ShiroganeMedium or
TerritoryTypeIdHousing.ShiroganeLarge or
TerritoryTypeIdHousing.ShiroganeFCRoom or
TerritoryTypeIdHousing.ShiroganeFCWorkshop
=> ResidentialAetheryteKind.Kugane,
TerritoryTypeIdHousing.Lavender or
TerritoryTypeIdHousing.LavenderSmall or
TerritoryTypeIdHousing.LavenderMedium or
TerritoryTypeIdHousing.LavenderLarge or
TerritoryTypeIdHousing.LavenderApartment or
TerritoryTypeIdHousing.LavenderFCRoom or
TerritoryTypeIdHousing.LavenderFCWorkshop
=> ResidentialAetheryteKind.Gridania,
TerritoryTypeIdHousing.Mist or
TerritoryTypeIdHousing.MistSmall or
TerritoryTypeIdHousing.MistMedium or
TerritoryTypeIdHousing.MistLarge or
TerritoryTypeIdHousing.MistApartment or
TerritoryTypeIdHousing.MistFCRoom or
TerritoryTypeIdHousing.MistFCWorkshop
=> ResidentialAetheryteKind.Limsa,
TerritoryTypeIdHousing.Goblet or
TerritoryTypeIdHousing.GobletSmall or
TerritoryTypeIdHousing.GobletMedium or
TerritoryTypeIdHousing.GobletLarge or
TerritoryTypeIdHousing.GobletApartment or
TerritoryTypeIdHousing.GobletFCRoom or
TerritoryTypeIdHousing.GobletFCWorkshop
=> ResidentialAetheryteKind.Uldah,
TerritoryTypeIdHousing.Empyream or
TerritoryTypeIdHousing.EmpyreamSmall or
TerritoryTypeIdHousing.EmpyreamMedium or
TerritoryTypeIdHousing.EmpyreamLarge or
TerritoryTypeIdHousing.EmpyreamApartment or
TerritoryTypeIdHousing.EmpyreamFCRoom or
TerritoryTypeIdHousing.EmpyreamFCWorkshop
=> ResidentialAetheryteKind.Foundation,
_ => ResidentialAetheryteKind.None
};
}
public string? GetMapAddressByLocation(LocationInfo location)
{
string? liString = null;
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
if (GetResidentialAetheryteKind(territoryHousing) == ResidentialAetheryteKind.None)
{
liString = _dalamudUtilService.LocationToLifestream(location);
}
return liString;
}
public DateTimeOffset GetSharingStatus(string uid)
{
try

View File

@@ -2104,6 +2104,16 @@ internal static class MdlDecimator
}
}
if (boneWeights != null
&& blendWeightEncoding == BlendWeightEncoding.Default
&& format.BlendWeightsElement is { } blendWeightsElement
&& (MdlFile.VertexType)blendWeightsElement.Type == MdlFile.VertexType.UShort4
&& ShouldTreatWeightsAsByteNormalized(boneWeights))
{
RescaleUShortAsByteWeights(boneWeights);
blendWeightEncoding = BlendWeightEncoding.UShortAsByte;
}
decoded = new DecodedMeshData(positions, normals, tangents, tangents2, colors, boneWeights, uvChannels, positionWs, normalWs, blendWeightEncoding);
return true;
}
@@ -3413,6 +3423,44 @@ internal static class MdlDecimator
return ToUShortNormalized(normalized);
}
private static bool ShouldTreatWeightsAsByteNormalized(BoneWeight[] weights)
{
const float maxByteUnorm = byte.MaxValue / (float)ushort.MaxValue;
var maxWeight = 0f;
for (var i = 0; i < weights.Length; i++)
{
var weight = weights[i];
maxWeight = Math.Max(maxWeight, weight.weight0);
maxWeight = Math.Max(maxWeight, weight.weight1);
maxWeight = Math.Max(maxWeight, weight.weight2);
maxWeight = Math.Max(maxWeight, weight.weight3);
if (maxWeight > maxByteUnorm)
{
return false;
}
}
return maxWeight > 0f;
}
private static void RescaleUShortAsByteWeights(BoneWeight[] weights)
{
var scale = ushort.MaxValue / (float)byte.MaxValue;
for (var i = 0; i < weights.Length; i++)
{
var weight = weights[i];
weights[i] = new BoneWeight(
weight.index0,
weight.index1,
weight.index2,
weight.index3,
weight.weight0 * scale,
weight.weight1 * scale,
weight.weight2 * scale,
weight.weight3 * scale);
}
}
private static void NormalizeWeights(float[] weights)
{
var sum = weights.Sum();

View File

@@ -1,6 +1,7 @@
using LightlessSync.FileCache;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.Services;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
@@ -19,6 +20,7 @@ public sealed class ModelDecimationService
private readonly FileCacheManager _fileCacheManager;
private readonly PlayerPerformanceConfigService _performanceConfigService;
private readonly XivDataStorageService _xivDataStorageService;
private readonly ModelProcessingQueue _processingQueue;
private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs);
private readonly TaskRegistry<string> _decimationDeduplicator = new();
@@ -30,13 +32,15 @@ public sealed class ModelDecimationService
LightlessConfigService configService,
FileCacheManager fileCacheManager,
PlayerPerformanceConfigService performanceConfigService,
XivDataStorageService xivDataStorageService)
XivDataStorageService xivDataStorageService,
ModelProcessingQueue processingQueue)
{
_logger = logger;
_configService = configService;
_fileCacheManager = fileCacheManager;
_performanceConfigService = performanceConfigService;
_xivDataStorageService = xivDataStorageService;
_processingQueue = processingQueue;
}
public void ScheduleDecimation(string hash, string filePath, string? gamePath = null)
@@ -53,9 +57,9 @@ public sealed class ModelDecimationService
_logger.LogDebug("Queued model decimation for {Hash}", hash);
_decimationDeduplicator.GetOrStart(hash, async () =>
_decimationDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
{
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
await _decimationSemaphore.WaitAsync(token).ConfigureAwait(false);
try
{
await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
@@ -69,7 +73,7 @@ public sealed class ModelDecimationService
{
_decimationSemaphore.Release();
}
});
}, CancellationToken.None));
}
public void ScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings)
@@ -89,9 +93,9 @@ public sealed class ModelDecimationService
_logger.LogInformation("Queued batch model decimation for {Hash}", hash);
_decimationDeduplicator.GetOrStart(hash, async () =>
_decimationDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
{
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
await _decimationSemaphore.WaitAsync(token).ConfigureAwait(false);
try
{
await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false);
@@ -105,7 +109,7 @@ public sealed class ModelDecimationService
{
_decimationSemaphore.Release();
}
});
}, CancellationToken.None));
}
public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)

View File

@@ -0,0 +1,19 @@
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public sealed class ModelProcessingQueue : IDisposable
{
private readonly AssetProcessingQueue _queue;
public ModelProcessingQueue(ILogger<ModelProcessingQueue> logger)
{
_queue = new AssetProcessingQueue(logger, "LightlessSync.ModelProcessing");
}
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
=> _queue.Enqueue(work, token);
public void Dispose()
=> _queue.Dispose();
}

View File

@@ -8,6 +8,7 @@ using System.Threading;
using OtterTex;
using OtterImage = OtterTex.Image;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services;
using LightlessSync.Utils;
using LightlessSync.FileCache;
using Microsoft.Extensions.Logging;
@@ -33,6 +34,7 @@ public sealed class TextureDownscaleService
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly FileCacheManager _fileCacheManager;
private readonly TextureCompressionService _textureCompressionService;
private readonly TextureProcessingQueue _processingQueue;
private readonly TaskRegistry<string> _downscaleDeduplicator = new();
private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
@@ -73,13 +75,15 @@ public sealed class TextureDownscaleService
LightlessConfigService configService,
PlayerPerformanceConfigService playerPerformanceConfigService,
FileCacheManager fileCacheManager,
TextureCompressionService textureCompressionService)
TextureCompressionService textureCompressionService,
TextureProcessingQueue processingQueue)
{
_logger = logger;
_configService = configService;
_playerPerformanceConfigService = playerPerformanceConfigService;
_fileCacheManager = fileCacheManager;
_textureCompressionService = textureCompressionService;
_processingQueue = processingQueue;
}
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
@@ -90,7 +94,7 @@ public sealed class TextureDownscaleService
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
if (_downscaleDeduplicator.TryGetExisting(hash, out _)) return;
_downscaleDeduplicator.GetOrStart(hash, async () =>
_downscaleDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
{
TextureMapKind mapKind;
try
@@ -104,7 +108,7 @@ public sealed class TextureDownscaleService
}
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
});
}, CancellationToken.None));
}
public bool ShouldScheduleDownscale(string filePath)
@@ -382,6 +386,12 @@ public sealed class TextureDownscaleService
{
var isCompressed = sourceFormat.IsCompressed();
var targetFormat = isCompressed ? sourceFormat : DXGIFormat.B8G8R8A8UNorm;
_logger.LogDebug(
"Downscale convert target {TargetFormat} (source {SourceFormat}, compressed {IsCompressed}, penumbraFallback {PenumbraFallback})",
targetFormat,
sourceFormat,
isCompressed,
attemptPenumbraFallback);
try
{
result = source.Convert(targetFormat);
@@ -433,6 +443,7 @@ public sealed class TextureDownscaleService
{
try
{
_logger.LogDebug("Downscale Penumbra re-encode target {Target} for {Hash}.", target, hash);
using var uncompressed = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
TexFileHelper.Save(destination, uncompressed);
}

View File

@@ -0,0 +1,19 @@
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public sealed class TextureProcessingQueue : IDisposable
{
private readonly AssetProcessingQueue _queue;
public TextureProcessingQueue(ILogger<TextureProcessingQueue> logger)
{
_queue = new AssetProcessingQueue(logger, "LightlessSync.TextureProcessing");
}
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
=> _queue.Enqueue(work, token);
public void Dispose()
=> _queue.Dispose();
}

View File

@@ -4,8 +4,10 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
@@ -40,6 +42,7 @@ public class DrawUserPair
private readonly LocationShareService _locationShareService;
private readonly CharaDataManager _charaDataManager;
private readonly PairLedger _pairLedger;
private readonly IpcCallerLifestream _lifestreamIpc;
private float _menuWidth = -1;
private bool _wasHovered = false;
private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty;
@@ -60,7 +63,8 @@ public class DrawUserPair
LightlessConfigService configService,
LocationShareService locationShareService,
CharaDataManager charaDataManager,
PairLedger pairLedger)
PairLedger pairLedger,
IpcCallerLifestream lifestreamIpc)
{
_id = id;
_uiEntry = uiEntry;
@@ -79,6 +83,7 @@ public class DrawUserPair
_locationShareService = locationShareService;
_charaDataManager = charaDataManager;
_pairLedger = pairLedger;
_lifestreamIpc = lifestreamIpc;
}
public PairDisplayEntry DisplayEntry => _displayEntry;
@@ -656,6 +661,13 @@ public class DrawUserPair
using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther))
_uiSharedService.IconText(shareLocationIcon);
var popupId = $"LocationPopup_{_pair.UserData.UID}";
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && shareLocation && !string.IsNullOrEmpty(location))
{
ImGui.OpenPopup(popupId);
}
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
@@ -669,6 +681,8 @@ public class DrawUserPair
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
ImGui.SameLine();
ImGui.TextUnformatted(location);
ImGui.Separator();
ImGui.TextUnformatted("Click to teleport to this location");
}
else
{
@@ -700,6 +714,62 @@ public class DrawUserPair
}
ImGui.EndTooltip();
}
if (ImGui.BeginPopup(popupId))
{
var locationInfo = _locationShareService.GetLocationForLifestreamByUid(_pair.UserData.UID);
if (locationInfo != null)
{
var locationLi = locationInfo.Value;
var housingAddress = _locationShareService.GetAddressBookEntryByLocation(locationLi);
var mapAddress = _locationShareService.GetMapAddressByLocation(locationLi);
ImGui.TextUnformatted("Teleport to user?");
ImGui.Separator();
if (!_lifestreamIpc.APIAvailable)
{
ImGui.TextUnformatted("Lifestream IPC is not available. Please ensure Lifestream is enabled");
}
else if (housingAddress != null || mapAddress != null)
{
ImGui.TextUnformatted($"Go to {location}?");
ImGui.TextUnformatted($"NOTE: Teleporting to maps with multiple aetherytes or instances may not be accurate currently. (ie. Thavnair, Yanxia)");
}
else
{
ImGui.TextUnformatted("Lifestream cannot teleport here. If you are in a residential area, please make sure you're inside a plot.");
}
ImGui.Separator();
if (_lifestreamIpc.APIAvailable && (housingAddress != null || mapAddress != null))
{
if (locationLi.HouseId is not 0 && housingAddress != null)
{
if (ImGui.Button("Navigate"))
{
_lifestreamIpc.GoToHousingAddress(housingAddress.Value);
ImGui.CloseCurrentPopup();
}
}
else if (mapAddress != null && locationLi.HouseId is 0)
{
if (ImGui.Button("Navigate"))
{
_lifestreamIpc.ExecuteLifestreamCommand(mapAddress);
ImGui.CloseCurrentPopup();
}
}
ImGui.SameLine();
}
if (ImGui.Button("Close"))
{
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
}
}
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)

View File

@@ -56,6 +56,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private readonly ModelDecimationService _modelDecimationService;
private readonly TextureCompressionService _textureCompressionService;
private readonly TextureMetadataHelper _textureMetadataHelper;
private readonly TextureProcessingQueue _processingQueue;
private readonly List<TextureRow> _textureRows = new();
private readonly Dictionary<string, TextureCompressionTarget> _textureSelections = new(StringComparer.OrdinalIgnoreCase);
@@ -137,7 +138,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
LightlessConfigService configService,
PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager,
TransientConfigService transientConfigService, ModelDecimationService modelDecimationService,
TextureCompressionService textureCompressionService, TextureMetadataHelper textureMetadataHelper)
TextureCompressionService textureCompressionService, TextureMetadataHelper textureMetadataHelper,
TextureProcessingQueue processingQueue)
: base(logger, mediator, "Lightless Character Data Analysis", performanceCollectorService)
{
_characterAnalyzer = characterAnalyzer;
@@ -150,6 +152,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_modelDecimationService = modelDecimationService;
_textureCompressionService = textureCompressionService;
_textureMetadataHelper = textureMetadataHelper;
_processingQueue = processingQueue;
Mediator.Subscribe<CharacterDataAnalyzedMessage>(this, (_) =>
{
_hasUpdate = true;
@@ -3716,7 +3719,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_conversionCurrentFileProgress = 0;
_conversionFailed = false;
_conversionTask = RunTextureConversionAsync(requests, _conversionCancellationTokenSource.Token);
var conversionToken = _conversionCancellationTokenSource.Token;
_conversionTask = _processingQueue.Enqueue(
queueToken => RunTextureConversionAsync(requests, queueToken),
conversionToken);
_showModal = true;
}

View File

@@ -16,6 +16,7 @@ using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using LightlessSync.Interop.Ipc;
namespace LightlessSync.UI;
@@ -40,6 +41,7 @@ public class DrawEntityFactory
private readonly IdDisplayHandler _uidDisplayHandler;
private readonly PairLedger _pairLedger;
private readonly PairFactory _pairFactory;
private readonly IpcCallerLifestream _lifestreamIpc;
public DrawEntityFactory(
ILogger<DrawEntityFactory> logger,
@@ -60,7 +62,8 @@ public class DrawEntityFactory
RenameSyncshellTagUi renameSyncshellTagUi,
SelectSyncshellForTagUi selectSyncshellForTagUi,
PairLedger pairLedger,
PairFactory pairFactory)
PairFactory pairFactory,
IpcCallerLifestream lifestreamIpc)
{
_logger = logger;
_apiController = apiController;
@@ -81,6 +84,7 @@ public class DrawEntityFactory
_selectSyncshellForTagUi = selectSyncshellForTagUi;
_pairLedger = pairLedger;
_pairFactory = pairFactory;
_lifestreamIpc = lifestreamIpc;
}
public DrawFolderGroup CreateGroupFolder(
@@ -167,7 +171,8 @@ public class DrawEntityFactory
_configService,
_locationShareService,
_charaDataManager,
_pairLedger);
_pairLedger,
_lifestreamIpc);
}
public IReadOnlyList<PairUiEntry> GetAllEntries()

View File

@@ -0,0 +1,55 @@
using System.Runtime.InteropServices;
using static LightlessSync.Utils.PtrGuardMemory;
namespace LightlessSync.Utils
{
public static partial class PtrGuard
{
private const ulong _minLikelyPtr = 0x0000_0001_0000_0000UL;
private const ulong _maxUserPtr = 0x0000_7FFF_FFFF_FFFFUL;
private const ulong _aligmentPtr = 0x7UL;
public static bool LooksLikePtr(nint p)
{
if (p == 0) return false;
var u = (ulong)p;
if (u < _minLikelyPtr) return false;
if (u > _maxUserPtr) return false;
if ((u & _aligmentPtr) != 0) return false;
return true;
}
public static bool TryReadIntPtr(nint addr, out nint value)
{
value = 0;
if (!LooksLikePtr(addr))
return false;
return ReadProcessMemory(GetCurrentProcess(), addr, out value, (nuint)IntPtr.Size, out nuint bytesRead)
&& bytesRead == (nuint)IntPtr.Size;
}
public static bool IsReadable(nint addr, nuint size)
{
if (addr == 0 || size == 0) return false;
if (VirtualQuery(addr, out var mbi, (nuint)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == 0)
return false;
const uint Commit = 0x1000;
const uint NoAccess = 0x01;
const uint PageGuard = 0x100;
if (mbi.State != Commit) return false;
if ((mbi.Protect & PageGuard) != 0) return false;
if (mbi.Protect == NoAccess) return false;
ulong start = (ulong)addr;
ulong end = start + size - 1;
ulong r0 = (ulong)mbi.BaseAddress;
ulong r1 = r0 + mbi.RegionSize - 1;
return start >= r0 && end <= r1;
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Runtime.InteropServices;
namespace LightlessSync.Utils
{
internal static class PtrGuardMemory
{
[StructLayout(LayoutKind.Sequential)]
internal struct MEMORY_BASIC_INFORMATION
{
public nint BaseAddress;
public nint AllocationBase;
public uint AllocationProtect;
public nuint RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern nuint VirtualQuery(
nint lpAddress,
out MEMORY_BASIC_INFORMATION lpBuffer,
nuint dwLength);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool ReadProcessMemory(
nint hProcess,
nint lpBaseAddress,
out nint lpBuffer,
nuint nSize,
out nuint lpNumberOfBytesRead);
[DllImport("kernel32.dll")]
internal static extern nint GetCurrentProcess();
}
}