Compare commits

...

44 Commits

Author SHA1 Message Date
defnotken
84a3293f6b eighty three
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m15s
2026-01-20 10:33:21 -06:00
defnotken
28db3d6fd2 Merge branch '2.0.3' into dev 2026-01-20 10:32:54 -06:00
d83ca98008 Merge pull request 'updated ptrguard to use system_info for minimum calculations' (#144) from clr-fix-attempt into 2.0.3
Reviewed-on: #144
2026-01-20 16:09:57 +00:00
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
cake
995e11371a updated ptrguard to use system_info for minimum calculations 2026-01-20 16:42:31 +01:00
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
defnotken
c7a2b679f2 bumpity bump
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m2s
2026-01-19 11:23:49 -06:00
defnotken
bec69074a5 Merge branch '2.0.3' into dev 2026-01-19 11:23:23 -06: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
f27db300ec fix temp collection config and migrate it 2026-01-19 23:04:54 +09:00
828be6eb5b fix body materials 2026-01-19 21:20:54 +09:00
d039d2fd90 add emperors oops 2026-01-19 14:19:08 +09:00
e75a371475 improve filtering 2026-01-19 14:14:14 +09:00
defnotken
ac711d9a43 Bump plugin testing
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m19s
2026-01-18 20:53:14 -06:00
defnotken
b875e0c3a1 Merge branch '2.0.3' into dev 2026-01-18 20:51:58 -06:00
defnotken
46e76bbfe6 Merge branch '2.0.3' into dev 2026-01-18 20:29:53 -06:00
cake
3654365f2a bump version
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m6s
2026-01-06 14:45:23 +01:00
cake
9b256dd185 Merge branch '2.0.3' into dev 2026-01-06 14:45:02 +01:00
defnotken
223ade39cb another push
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m13s
2026-01-05 20:48:24 -06:00
defnotken
5aca9e70b2 Merge branch '2.0.3' into dev 2026-01-05 20:47:38 -06:00
defnotken
92772cf334 dev push
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m12s
2026-01-05 20:21:26 -06:00
defnotken
0395e81a9f Merge branch '2.0.3' into dev 2026-01-05 20:17:12 -06:00
defnotken
7734a7bf7e dev build
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m8s
2026-01-05 17:42:21 -06:00
defnotken
db2d19bb1e Merge branch '2.0.3' into dev 2026-01-05 17:41:48 -06:00
defnotken
ab305a249c more checks
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m7s
2026-01-05 15:48:54 -06:00
defnotken
9d104a9dd8 Merge branch '2.0.3' into dev 2026-01-05 15:42:15 -06:00
defnotken
bcd3bd5ca2 add more checks
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m8s
2026-01-05 15:08:26 -06:00
defnotken
c1829a9837 Merge branch '2.0.3' into dev
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m14s
2026-01-05 14:48:47 -06:00
defnotken
cca23f6e05 Building Dev
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s
2026-01-05 10:50:25 -06:00
20 changed files with 1061 additions and 188 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

@@ -1,11 +1,17 @@
using LightlessSync.WebAPI;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace LightlessSync.LightlessConfiguration;
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, TransientConfigService transientConfigService,
ServerConfigService serverConfigService) : IHostedService
ServerConfigService serverConfigService, TempCollectionConfigService tempCollectionConfigService,
LightlessConfigService lightlessConfigService) : IHostedService
{
private readonly ILogger<ConfigurationMigrator> _logger = logger;
@@ -51,6 +57,8 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
serverConfigService.Current.Version = 2;
serverConfigService.Save();
}
MigrateTempCollectionConfig(tempCollectionConfigService, lightlessConfigService);
}
public Task StartAsync(CancellationToken cancellationToken)
@@ -63,4 +71,273 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
{
return Task.CompletedTask;
}
private void MigrateTempCollectionConfig(TempCollectionConfigService tempCollectionConfigService, LightlessConfigService lightlessConfigService)
{
var now = DateTime.UtcNow;
TempCollectionConfig tempConfig = tempCollectionConfigService.Current;
var tempChanged = false;
var tempNeedsSave = false;
if (TryReadTempCollectionData(lightlessConfigService.ConfigurationPath, out var root, out var ids, out var entries))
{
tempChanged |= MergeTempCollectionData(tempConfig, ids, entries, now);
var removed = root.Remove("OrphanableTempCollections");
removed |= root.Remove("OrphanableTempCollectionEntries");
if (removed)
{
try
{
string updatedJson = root.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(lightlessConfigService.ConfigurationPath, updatedJson);
lightlessConfigService.UpdateLastWriteTime();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to rewrite {config} after temp collection migration", lightlessConfigService.ConfigurationPath);
return;
}
}
if (ids.Count > 0 || entries.Count > 0)
{
_logger.LogInformation("Migrated {ids} temp collection ids and {entries} entries to {configName}",
ids.Count, entries.Count, tempCollectionConfigService.ConfigurationName);
}
}
if (TryReadTempCollectionData(tempCollectionConfigService.ConfigurationPath, out var tempRoot, out var tempIds, out var tempEntries))
{
tempChanged |= MergeTempCollectionData(tempConfig, tempIds, tempEntries, now);
if (tempRoot.Remove("OrphanableTempCollections"))
{
tempNeedsSave = true;
}
}
if (tempChanged || tempNeedsSave)
{
tempCollectionConfigService.Save();
}
}
private bool TryReadTempCollectionData(string configPath, out JsonObject root, out HashSet<Guid> ids, out List<OrphanableTempCollectionEntry> entries)
{
root = new JsonObject();
ids = [];
entries = [];
if (!File.Exists(configPath))
{
return false;
}
try
{
root = JsonNode.Parse(File.ReadAllText(configPath)) as JsonObject ?? new JsonObject();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to read temp collection data from {config}", configPath);
return false;
}
root.TryGetPropertyValue("OrphanableTempCollections", out JsonNode? idsNode);
root.TryGetPropertyValue("OrphanableTempCollectionEntries", out JsonNode? entriesNode);
if (idsNode == null && entriesNode == null)
{
return false;
}
ids = ParseGuidSet(idsNode);
entries = ParseEntries(entriesNode);
return true;
}
private static HashSet<Guid> ParseGuidSet(JsonNode? node)
{
HashSet<Guid> ids = [];
if (node is not JsonArray array)
{
return ids;
}
foreach (JsonNode? item in array)
{
Guid id = ParseGuid(item);
if (id != Guid.Empty)
{
ids.Add(id);
}
}
return ids;
}
private static List<OrphanableTempCollectionEntry> ParseEntries(JsonNode? node)
{
List<OrphanableTempCollectionEntry> entries = [];
if (node is not JsonArray array)
{
return entries;
}
foreach (JsonNode? item in array)
{
if (item is not JsonObject obj)
{
continue;
}
Guid id = ParseGuid(obj["Id"]);
if (id == Guid.Empty)
{
continue;
}
DateTime registeredAtUtc = DateTime.MinValue;
if (TryParseDateTime(obj["RegisteredAtUtc"], out DateTime parsed))
{
registeredAtUtc = parsed;
}
entries.Add(new OrphanableTempCollectionEntry
{
Id = id,
RegisteredAtUtc = registeredAtUtc
});
}
return entries;
}
private static Guid ParseGuid(JsonNode? node)
{
if (node is JsonValue value)
{
if (value.TryGetValue<string>(out string? stringValue) && Guid.TryParse(stringValue, out Guid parsed))
{
return parsed;
}
}
return Guid.Empty;
}
private static bool TryParseDateTime(JsonNode? node, out DateTime value)
{
value = DateTime.MinValue;
if (node is not JsonValue val)
{
return false;
}
if (val.TryGetValue<DateTime>(out DateTime direct))
{
value = direct;
return true;
}
if (val.TryGetValue<string>(out string? stringValue)
&& DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime parsed))
{
value = parsed;
return true;
}
return false;
}
private static bool MergeTempCollectionData(TempCollectionConfig config, HashSet<Guid> ids, List<OrphanableTempCollectionEntry> entries, DateTime now)
{
bool changed = false;
Dictionary<Guid, OrphanableTempCollectionEntry> entryLookup = new();
for (var i = config.OrphanableTempCollectionEntries.Count - 1; i >= 0; i--)
{
var entry = config.OrphanableTempCollectionEntries[i];
if (entry.Id == Guid.Empty)
{
config.OrphanableTempCollectionEntries.RemoveAt(i);
changed = true;
continue;
}
if (entryLookup.TryGetValue(entry.Id, out var existing))
{
if (entry.RegisteredAtUtc != DateTime.MinValue
&& (existing.RegisteredAtUtc == DateTime.MinValue || entry.RegisteredAtUtc < existing.RegisteredAtUtc))
{
existing.RegisteredAtUtc = entry.RegisteredAtUtc;
changed = true;
}
config.OrphanableTempCollectionEntries.RemoveAt(i);
changed = true;
continue;
}
entryLookup[entry.Id] = entry;
}
foreach (OrphanableTempCollectionEntry entry in entries)
{
if (entry.Id == Guid.Empty)
{
continue;
}
if (!entryLookup.TryGetValue(entry.Id, out OrphanableTempCollectionEntry? existing))
{
var added = new OrphanableTempCollectionEntry
{
Id = entry.Id,
RegisteredAtUtc = entry.RegisteredAtUtc
};
config.OrphanableTempCollectionEntries.Add(added);
entryLookup[entry.Id] = added;
changed = true;
continue;
}
if (entry.RegisteredAtUtc != DateTime.MinValue
&& (existing.RegisteredAtUtc == DateTime.MinValue || entry.RegisteredAtUtc < existing.RegisteredAtUtc))
{
existing.RegisteredAtUtc = entry.RegisteredAtUtc;
changed = true;
}
}
foreach (Guid id in ids)
{
if (id == Guid.Empty)
{
continue;
}
if (!entryLookup.TryGetValue(id, out OrphanableTempCollectionEntry? existing))
{
var added = new OrphanableTempCollectionEntry
{
Id = id,
RegisteredAtUtc = now
};
config.OrphanableTempCollectionEntries.Add(added);
entryLookup[id] = added;
changed = true;
continue;
}
if (existing.RegisteredAtUtc == DateTime.MinValue)
{
existing.RegisteredAtUtc = now;
changed = true;
}
}
return changed;
}
}

View File

@@ -72,37 +72,41 @@ public class ConfigurationSaveService : IHostedService
{
_logger.LogTrace("Saving {configName}", config.ConfigurationName);
var configDir = config.ConfigurationPath.Replace(config.ConfigurationName, string.Empty);
var isTempCollections = string.Equals(config.ConfigurationName, TempCollectionConfigService.ConfigName, StringComparison.OrdinalIgnoreCase);
try
if (!isTempCollections)
{
var configBackupFolder = Path.Join(configDir, BackupFolder);
if (!Directory.Exists(configBackupFolder))
Directory.CreateDirectory(configBackupFolder);
var configNameSplit = config.ConfigurationName.Split(".");
var existingConfigs = Directory.EnumerateFiles(
configBackupFolder,
configNameSplit[0] + "*")
.Select(c => new FileInfo(c))
.OrderByDescending(c => c.LastWriteTime).ToList();
if (existingConfigs.Skip(10).Any())
try
{
foreach (var oldBak in existingConfigs.Skip(10).ToList())
{
oldBak.Delete();
}
}
var configBackupFolder = Path.Join(configDir, BackupFolder);
if (!Directory.Exists(configBackupFolder))
Directory.CreateDirectory(configBackupFolder);
string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]);
_logger.LogTrace("Backing up current config to {backupPath}", backupPath);
File.Copy(config.ConfigurationPath, backupPath, overwrite: true);
FileInfo fi = new(backupPath);
fi.LastWriteTimeUtc = DateTime.UtcNow;
}
catch (Exception ex)
{
// ignore if file cannot be backupped
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
var configNameSplit = config.ConfigurationName.Split(".");
var existingConfigs = Directory.EnumerateFiles(
configBackupFolder,
configNameSplit[0] + "*")
.Select(c => new FileInfo(c))
.OrderByDescending(c => c.LastWriteTime).ToList();
if (existingConfigs.Skip(10).Any())
{
foreach (var oldBak in existingConfigs.Skip(10).ToList())
{
oldBak.Delete();
}
}
string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]);
_logger.LogTrace("Backing up current config to {backupPath}", backupPath);
File.Copy(config.ConfigurationPath, backupPath, overwrite: true);
FileInfo fi = new(backupPath);
fi.LastWriteTimeUtc = DateTime.UtcNow;
}
catch (Exception ex)
{
// ignore if file cannot be backupped
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
}
}
var temp = config.ConfigurationPath + ".tmp";
@@ -110,7 +114,7 @@ public class ConfigurationSaveService : IHostedService
{
await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(config.Current, typeof(T), new JsonSerializerOptions()
{
WriteIndented = true
WriteIndented = !isTempCollections
})).ConfigureAwait(false);
File.Move(temp, config.ConfigurationPath, true);
config.UpdateLastWriteTime();

View File

@@ -160,8 +160,6 @@ public class LightlessConfig : ILightlessConfiguration
public string? SelectedFinderSyncshell { get; set; } = null;
public string LastSeenVersion { get; set; } = string.Empty;
public bool EnableParticleEffects { get; set; } = true;
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
public List<OrphanableTempCollectionEntry> OrphanableTempCollectionEntries { get; set; } = [];
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Unsafe;
public bool AnimationAllowOneBasedShift { get; set; } = false;
public bool AnimationAllowNeighborIndexTolerance { get; set; } = false;

View File

@@ -0,0 +1,10 @@
using LightlessSync.LightlessConfiguration.Models;
namespace LightlessSync.LightlessConfiguration.Configurations;
[Serializable]
public sealed class TempCollectionConfig : ILightlessConfiguration
{
public int Version { get; set; } = 1;
public List<OrphanableTempCollectionEntry> OrphanableTempCollectionEntries { get; set; } = [];
}

View File

@@ -0,0 +1,12 @@
using LightlessSync.LightlessConfiguration.Configurations;
namespace LightlessSync.LightlessConfiguration;
public sealed class TempCollectionConfigService : ConfigurationServiceBase<TempCollectionConfig>
{
public const string ConfigName = "tempcollections.json";
public TempCollectionConfigService(string configDir) : base(configDir) { }
public override string ConfigurationName => ConfigName;
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<Authors></Authors>
<Company></Company>
<Version>2.0.3</Version>
<Version>2.0.2.83</Version>
<Description></Description>
<Copyright></Copyright>
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>

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

@@ -431,6 +431,7 @@ public sealed class Plugin : IDalamudPlugin
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
return cfg;
});
services.AddSingleton(sp => new TempCollectionConfigService(configDir));
services.AddSingleton(sp => new ServerConfigService(configDir));
services.AddSingleton(sp => new NotesConfigService(configDir));
services.AddSingleton(sp => new PairTagConfigService(configDir));
@@ -444,6 +445,7 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<TempCollectionConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ServerConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());

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

@@ -1343,22 +1343,11 @@ internal static class MdlDecimator
return false;
}
return IsBodyMaterial(mdl.Materials[mesh.MaterialIndex]);
return ModelDecimationFilters.IsBodyMaterial(mdl.Materials[mesh.MaterialIndex]);
}
private static bool IsBodyMaterial(string materialPath)
{
if (string.IsNullOrWhiteSpace(materialPath))
{
return false;
}
var normalized = materialPath.Replace('\\', '/').ToLowerInvariant();
var nameStart = normalized.LastIndexOf('/');
var fileName = nameStart >= 0 ? normalized[(nameStart + 1)..] : normalized;
return fileName.Contains("_bibo", StringComparison.Ordinal)
|| fileName.EndsWith("_a.mtrl", StringComparison.Ordinal);
}
=> ModelDecimationFilters.IsBodyMaterial(materialPath);
private sealed class BodyCollisionData
{

View File

@@ -0,0 +1,132 @@
namespace LightlessSync.Services.ModelDecimation;
internal static class ModelDecimationFilters
{
// MODELS ONLY HERE, NOT MATERIALS
internal static readonly string[] HairPaths =
[
"/hair/",
"hir.mdl",
];
internal static readonly string[] ClothingPaths =
[
"chara/equipment/",
"/equipment/",
"met.mdl",
"top.mdl",
"glv.mdl",
"dwn.mdl",
"sho.mdl",
];
internal static readonly string[] AccessoryPaths =
[
"/accessory/",
"chara/accessory/",
"ear.mdl",
"nek.mdl",
"wrs.mdl",
"ril.mdl",
"rir.mdl",
];
internal static readonly string[] BodyPaths =
[
"/body/",
"chara/equipment/e0000/model/",
"chara/equipment/e9903/model/",
"chara/equipment/e9903/model/",
"chara/equipment/e0279/model/",
];
internal static readonly string[] FaceHeadPaths =
[
"/face/",
"/obj/face/",
"/head/",
"fac.mdl",
];
internal static readonly string[] TailOrEarPaths =
[
"/tail/",
"/obj/tail/",
"/zear/",
"/obj/zear/",
"til.mdl",
"zer.mdl",
];
// BODY MATERIALS ONLY, NOT MESHES
internal static readonly string[] BodyMaterials =
[
"b0001_bibo.mtrl",
"b0101_bibo.mtrl",
"b0001_a.mtrl",
"b0001_b.mtrl",
"b0101_a.mtrl",
"b0101_b.mtrl",
];
internal static string NormalizePath(string path)
=> path.Replace('\\', '/').ToLowerInvariant();
internal static bool IsHairPath(string normalizedPath)
=> ContainsAny(normalizedPath, HairPaths);
internal static bool IsClothingPath(string normalizedPath)
=> ContainsAny(normalizedPath, ClothingPaths);
internal static bool IsAccessoryPath(string normalizedPath)
=> ContainsAny(normalizedPath, AccessoryPaths);
internal static bool IsBodyPath(string normalizedPath)
=> ContainsAny(normalizedPath, BodyPaths);
internal static bool IsFaceHeadPath(string normalizedPath)
=> ContainsAny(normalizedPath, FaceHeadPaths);
internal static bool IsTailOrEarPath(string normalizedPath)
=> ContainsAny(normalizedPath, TailOrEarPaths);
internal static bool ContainsAny(string normalizedPath, IReadOnlyList<string> markers)
{
for (var i = 0; i < markers.Count; i++)
{
if (normalizedPath.Contains(markers[i], StringComparison.Ordinal))
{
return true;
}
}
return false;
}
internal static bool IsBodyMaterial(string materialPath)
{
if (string.IsNullOrWhiteSpace(materialPath))
{
return false;
}
var normalized = NormalizePath(materialPath);
var nameStart = normalized.LastIndexOf('/');
var fileName = nameStart >= 0 ? normalized[(nameStart + 1)..] : normalized;
foreach (var marker in BodyMaterials)
{
if (fileName.Contains(marker, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
}

View File

@@ -352,46 +352,40 @@ public sealed class ModelDecimationService
return true;
}
var normalized = NormalizeGamePath(gamePath);
if (normalized.Contains("/hair/", StringComparison.Ordinal))
var normalized = ModelDecimationFilters.NormalizePath(gamePath);
if (ModelDecimationFilters.IsHairPath(normalized))
{
return false;
}
if (normalized.Contains("/chara/equipment/", StringComparison.Ordinal))
if (ModelDecimationFilters.IsClothingPath(normalized))
{
return _performanceConfigService.Current.ModelDecimationAllowClothing;
}
if (normalized.Contains("/chara/accessory/", StringComparison.Ordinal))
if (ModelDecimationFilters.IsAccessoryPath(normalized))
{
return _performanceConfigService.Current.ModelDecimationAllowAccessories;
}
if (normalized.Contains("/chara/human/", StringComparison.Ordinal))
if (ModelDecimationFilters.IsBodyPath(normalized))
{
if (normalized.Contains("/body/", StringComparison.Ordinal))
{
return _performanceConfigService.Current.ModelDecimationAllowBody;
}
return _performanceConfigService.Current.ModelDecimationAllowBody;
}
if (normalized.Contains("/face/", StringComparison.Ordinal) || normalized.Contains("/head/", StringComparison.Ordinal))
{
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
}
if (ModelDecimationFilters.IsFaceHeadPath(normalized))
{
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
}
if (normalized.Contains("/tail/", StringComparison.Ordinal))
{
return _performanceConfigService.Current.ModelDecimationAllowTail;
}
if (ModelDecimationFilters.IsTailOrEarPath(normalized))
{
return _performanceConfigService.Current.ModelDecimationAllowTail;
}
return true;
}
private static string NormalizeGamePath(string path)
=> path.Replace('\\', '/').ToLowerInvariant();
private bool TryGetDecimationSettings(out ModelDecimationSettings settings)
{
settings = new ModelDecimationSettings(

View File

@@ -10,15 +10,18 @@ namespace LightlessSync.Services;
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
{
private readonly IpcManager _ipc;
private readonly LightlessConfigService _config;
private readonly TempCollectionConfigService _config;
private readonly CancellationTokenSource _cleanupCts = new();
private int _ran;
private const int CleanupBatchSize = 50;
private static readonly TimeSpan CleanupBatchDelay = TimeSpan.FromMilliseconds(50);
private static readonly TimeSpan OrphanCleanupDelay = TimeSpan.FromDays(1);
public PenumbraTempCollectionJanitor(
ILogger<PenumbraTempCollectionJanitor> logger,
LightlessMediator mediator,
IpcManager ipc,
LightlessConfigService config) : base(logger, mediator)
TempCollectionConfigService config) : base(logger, mediator)
{
_ipc = ipc;
_config = config;
@@ -31,10 +34,6 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
if (id == Guid.Empty) return;
var changed = false;
var config = _config.Current;
if (config.OrphanableTempCollections.Add(id))
{
changed = true;
}
var now = DateTime.UtcNow;
var existing = config.OrphanableTempCollectionEntries.FirstOrDefault(entry => entry.Id == id);
@@ -63,8 +62,7 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
{
if (id == Guid.Empty) return;
var config = _config.Current;
var changed = config.OrphanableTempCollections.Remove(id);
changed |= RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
var changed = RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
if (changed)
{
_config.Save();
@@ -79,14 +77,31 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
if (!_ipc.Penumbra.APIAvailable)
return;
_ = Task.Run(async () =>
{
try
{
await CleanupOrphansOnBootAsync(_cleanupCts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
Logger.LogError(ex, "Error cleaning orphaned temp collections");
}
});
}
private async Task CleanupOrphansOnBootAsync(CancellationToken token)
{
var config = _config.Current;
var ids = config.OrphanableTempCollections;
var entries = config.OrphanableTempCollectionEntries;
if (ids.Count == 0 && entries.Count == 0)
if (entries.Count == 0)
return;
var now = DateTime.UtcNow;
var changed = EnsureEntries(ids, entries, now);
var changed = EnsureEntryTimes(entries, now);
var cutoff = now - OrphanCleanupDelay;
var expired = entries
.Where(entry => entry.Id != Guid.Empty && entry.RegisteredAtUtc != DateTime.MinValue && entry.RegisteredAtUtc <= cutoff)
@@ -105,25 +120,47 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
var appId = Guid.NewGuid();
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections older than {delay}", expired.Count, OrphanCleanupDelay);
List<Guid> removedIds = [];
foreach (var id in expired)
{
if (token.IsCancellationRequested)
{
break;
}
try
{
_ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id)
.GetAwaiter().GetResult();
await _ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed removing orphaned temp collection {id}", id);
}
removedIds.Add(id);
if (removedIds.Count % CleanupBatchSize == 0)
{
try
{
await Task.Delay(CleanupBatchDelay, token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
break;
}
}
}
foreach (var id in expired)
if (removedIds.Count == 0)
{
ids.Remove(id);
if (changed)
{
_config.Save();
}
return;
}
foreach (var id in expired)
foreach (var id in removedIds)
{
RemoveEntry(entries, id);
}
@@ -131,6 +168,17 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
_config.Save();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_cleanupCts.Cancel();
_cleanupCts.Dispose();
}
base.Dispose(disposing);
}
private static int RemoveEntry(List<OrphanableTempCollectionEntry> entries, Guid id)
{
var removed = 0;
@@ -148,29 +196,9 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
return removed;
}
private static bool EnsureEntries(HashSet<Guid> ids, List<OrphanableTempCollectionEntry> entries, DateTime now)
private static bool EnsureEntryTimes(List<OrphanableTempCollectionEntry> entries, DateTime now)
{
var changed = false;
foreach (var id in ids)
{
if (id == Guid.Empty)
{
continue;
}
if (entries.Any(entry => entry.Id == id))
{
continue;
}
entries.Add(new OrphanableTempCollectionEntry
{
Id = id,
RegisteredAtUtc = now
});
changed = true;
}
foreach (var entry in entries)
{
if (entry.Id == Guid.Empty || entry.RegisteredAtUtc != DateTime.MinValue)

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

@@ -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,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;
}
}
}

View 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;
}
}
}