Compare commits

...

14 Commits

Author SHA1 Message Date
defnotken
abc324bf4f bumpers
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m23s
2026-01-19 19:48:39 -06:00
defnotken
eee0e072bd Merge branch '2.0.3' into dev 2026-01-19 19:48:28 -06: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
defnotken
172288c755 bumpers
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 1m57s
2026-01-19 18:31:36 -06:00
defnotken
1c17be53d0 Merge branch '2.0.3' into dev 2026-01-19 18:30:42 -06: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
defnotken
e8f598e695 bumpity bump
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m7s
2026-01-19 12:36:24 -06:00
defnotken
861a337029 Merge branch '2.0.3' into dev
Some checks failed
Tag and Release Lightless / tag-and-release (push) Has been cancelled
2026-01-19 12:35:29 -06: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
5 changed files with 194 additions and 83 deletions

View File

@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<Authors></Authors> <Authors></Authors>
<Company></Company> <Company></Company>
<Version>2.0.2.79</Version> <Version>2.0.2.82</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>

View File

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

View File

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

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();
}
}