Compare commits
44 Commits
update-dec
...
2.0.2.88-D
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59b2776943 | ||
|
|
b3c35dbfdc | ||
|
|
84a3293f6b | ||
|
|
28db3d6fd2 | ||
| d83ca98008 | |||
| e6df37bcca | |||
| 60d144b881 | |||
|
|
995e11371a | ||
| 5089dbd6c8 | |||
|
|
abc324bf4f | ||
|
|
eee0e072bd | ||
|
|
d8335eb04f | ||
|
|
994335c6b0 | ||
|
|
172288c755 | ||
|
|
1c17be53d0 | ||
| 68b4863f52 | |||
|
|
22fe9901a4 | ||
|
|
cff866dcc2 | ||
|
|
e8f598e695 | ||
|
|
861a337029 | ||
| 06f89955d3 | |||
|
|
367af2c3d0 | ||
|
|
19a238c808 | ||
|
|
c7a2b679f2 | ||
|
|
bec69074a5 | ||
| 7d86b41cee | |||
| 0185e6b534 | |||
|
|
90bf84f8eb | ||
|
|
ac711d9a43 | ||
|
|
b875e0c3a1 | ||
|
|
46e76bbfe6 | ||
|
|
3654365f2a | ||
|
|
9b256dd185 | ||
|
|
223ade39cb | ||
|
|
5aca9e70b2 | ||
|
|
92772cf334 | ||
|
|
0395e81a9f | ||
|
|
7734a7bf7e | ||
|
|
db2d19bb1e | ||
|
|
ab305a249c | ||
|
|
9d104a9dd8 | ||
|
|
bcd3bd5ca2 | ||
|
|
c1829a9837 | ||
|
|
cca23f6e05 |
@@ -1,11 +1,86 @@
|
||||
tagline: "Lightless Sync v2.0.1"
|
||||
subline: "LIGHTLESS IS EVOLVING!!"
|
||||
tagline: "Lightless Sync v2.1.0"
|
||||
subline: "Location Sharing, Animation Scanning, Sync Improvements, Lifestream IPC, and More!"
|
||||
changelog:
|
||||
- name: "v2.1.0"
|
||||
tagline: "Happy New Year!"
|
||||
date: "January 20 2026"
|
||||
# be sure to set this every new version
|
||||
isCurrent: true
|
||||
versions:
|
||||
- number: "Location Sharing (S/O to Tsubasa from the MareCN team for this feature!)"
|
||||
icon: ""
|
||||
items:
|
||||
- "Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] or until you turn it off for them. That's up to you!"
|
||||
- "To share your location with a pair, click the three dots beside the pair and choose a duration to share with them."
|
||||
- "To view the location of someone who's shared with you, simply hover over the globe icon!"
|
||||
- number: "Model Optimization (Mesh Decimating)"
|
||||
icon: ""
|
||||
items:
|
||||
- "This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). "
|
||||
- "Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for."
|
||||
- "Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking."
|
||||
- "You can find everything under Settings → Performance → Model Optimization."
|
||||
- "IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE!"
|
||||
- number: "Animation (PAP) Validation (Safer animations)"
|
||||
icon: ""
|
||||
items:
|
||||
- "Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users."
|
||||
- "Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with."
|
||||
- "Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives."
|
||||
- number: "UI Changes (Thanks to Choco for UI Changes)"
|
||||
icon: ""
|
||||
items:
|
||||
- "The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless."
|
||||
- "Settings have gotten some changes as well to make this change more universal, and will use the same color settings."
|
||||
- "The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior"
|
||||
- "Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it"
|
||||
- number: "LightFinder / ShellFinder"
|
||||
icon: ""
|
||||
items:
|
||||
- "UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does."
|
||||
- "A list of players using LightFinder near you is now available."
|
||||
- "Improved the look of ShellFinder; a new pop-up for the join screen is included. This will be merged into more spots later."
|
||||
- number: "Syncing Changes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed a bottleneck that caused hitching for some users."
|
||||
- "Fixed a bug where files in the downscale directory were not being deleted correctly."
|
||||
- "Improved performance tracking by tracking both original and effective triangle counts."
|
||||
- "Improved owned object detection."
|
||||
- "Reverted collection work to old way to mitigate the syncing and VFX issues."
|
||||
- "Fixed minion/mount/pet tracking to validate against player-related."
|
||||
- "Added timeout support to Wait For Fully Loaded."
|
||||
- "Enhanced pair handling to support model decimation. (Effective Triangles)."
|
||||
- "Improved downloads through atomization."
|
||||
- "Improved data management by only pushing to online pairs."
|
||||
- number: "Syncshell Admin Panel changes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Changes to banned userlist to allow for preemptive banning of users."
|
||||
- "Fixed when you are the owner, the buttons aren't overflowing into the side."
|
||||
- "Pruning has been fixed not to include online users. It will only focus on offline users now."
|
||||
- "Fixed that moderators can finally broadcast the syncshell."
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed a bug in the client CI/CD workflow where the plugin was contained twice in the zip. This will cut down on overall plugin size."
|
||||
- "Added new flags for certain ImGUI windows to fix focusing issues with Chat2."
|
||||
- "Validating PTRs for the game objects to prevent crashing while switching characters."
|
||||
- number: "Lifestream Plugin integration"
|
||||
icon: ""
|
||||
items:
|
||||
- "We have added Lifestream as an optional plugin you can install to support multiple future implementations >:3. In the near future, you'll be able to go straight to your pair using the Lifestream plugin in as easy as one click. We are also cooking something up for now; it will simply show as an optional plugin."
|
||||
- "Integrated Lifestream to Location Sharing to easily teleport to all plots/apartments, and most maps."
|
||||
- number: "Q1 Roadmap"
|
||||
icon: ""
|
||||
items:
|
||||
- "Diagnostics Window - Est. Feb"
|
||||
- "Ability to set a location as a 'Home' in syncshells - Est. Mid-Late Feb"
|
||||
- "Lifestream support for Syncshell Homes - Est. Mid-Late Feb"
|
||||
- "Community-oriented feature - In Refinement (est. Mar/Apr)"
|
||||
- name: "v2.0.2"
|
||||
tagline: "Last update of 2025!... ... ... If Nothing breaks"
|
||||
date: "December 28 2025"
|
||||
# be sure to set this every new version
|
||||
isCurrent: true
|
||||
versions:
|
||||
- number: "Chat"
|
||||
icon: ""
|
||||
|
||||
@@ -11,6 +11,8 @@ credits:
|
||||
role: "Developer"
|
||||
- name: "Kenny"
|
||||
role: "Developer"
|
||||
- name: "Tsubasa"
|
||||
role: "Developer"
|
||||
- name: "Zura"
|
||||
role: "Developer"
|
||||
- name: "Additional Contributors"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors></Authors>
|
||||
<Company></Company>
|
||||
<Version>2.0.3</Version>
|
||||
<Version>2.0.2.88</Version>
|
||||
<Description></Description>
|
||||
<Copyright></Copyright>
|
||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>();
|
||||
|
||||
93
LightlessSync/Services/AssetProcessingQueue.cs
Normal file
93
LightlessSync/Services/AssetProcessingQueue.cs
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
19
LightlessSync/Services/ModelProcessingQueue.cs
Normal file
19
LightlessSync/Services/ModelProcessingQueue.cs
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
19
LightlessSync/Services/TextureProcessingQueue.cs
Normal file
19
LightlessSync/Services/TextureProcessingQueue.cs
Normal 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();
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
71
LightlessSync/Utils/PtrGuard.cs
Normal file
71
LightlessSync/Utils/PtrGuard.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using static LightlessSync.Utils.PtrGuardMemory;
|
||||
|
||||
namespace LightlessSync.Utils
|
||||
{
|
||||
public static partial class PtrGuard
|
||||
{
|
||||
private const ulong _aligmentPtr = 0x7UL;
|
||||
private static readonly nuint _minAppAddr = (nuint)GetMinAppAddr();
|
||||
private static readonly nuint _maxAppAddr = (nuint)GetMaxAppAddr();
|
||||
|
||||
private static nint GetMinAppAddr()
|
||||
{
|
||||
GetSystemInfo(out var si);
|
||||
return si.lpMinimumApplicationAddress;
|
||||
}
|
||||
|
||||
private static nint GetMaxAppAddr()
|
||||
{
|
||||
GetSystemInfo(out var si);
|
||||
return si.lpMaximumApplicationAddress;
|
||||
}
|
||||
|
||||
public static bool LooksLikePtr(nint p)
|
||||
{
|
||||
if (p == 0) return false;
|
||||
nuint u = (nuint)p;
|
||||
|
||||
if (u < _minAppAddr) return false;
|
||||
if (u > _maxAppAddr) return false;
|
||||
if ((u & _aligmentPtr) != 0) return false;
|
||||
if ((uint)u == 0x12345679u) 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
LightlessSync/Utils/PtrGuardMemory.cs
Normal file
55
LightlessSync/Utils/PtrGuardMemory.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
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();
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
internal static extern void GetSystemInfo(out SYSTEM_INFO lpSystemInfo);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct SYSTEM_INFO
|
||||
{
|
||||
public ushort wProcessorArchitecture;
|
||||
public ushort wReserved;
|
||||
public uint dwPageSize;
|
||||
public nint lpMinimumApplicationAddress;
|
||||
public nint lpMaximumApplicationAddress;
|
||||
public nint dwActiveProcessorMask;
|
||||
public uint dwNumberOfProcessors;
|
||||
public uint dwProcessorType;
|
||||
public uint dwAllocationGranularity;
|
||||
public ushort wProcessorLevel;
|
||||
public ushort wProcessorRevision;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user