Compare commits

..

32 Commits

Author SHA1 Message Date
defnotken
59b2776943 syncing dev and stable
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m17s
2026-01-20 13:28:38 -06:00
defnotken
b3c35dbfdc Changelogs 2026-01-20 13:22:34 -06:00
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
60d144b881 adjust log level 2026-01-21 00:47:55 +09:00
cake
995e11371a updated ptrguard to use system_info for minimum calculations 2026-01-20 16:42:31 +01:00
5089dbd6c8 move texture and model processing out of download thread and fix some normalization for weights 2026-01-21 00:17:04 +09:00
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
28 changed files with 1357 additions and 205 deletions

View File

@@ -1,11 +1,86 @@
tagline: "Lightless Sync v2.0.1" tagline: "Lightless Sync v2.1.0"
subline: "LIGHTLESS IS EVOLVING!!" subline: "Location Sharing, Animation Scanning, Sync Improvements, Lifestream IPC, and More!"
changelog: 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, its included in what gets sent to other players. If it doesnt, 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" - name: "v2.0.2"
tagline: "Last update of 2025!... ... ... If Nothing breaks" tagline: "Last update of 2025!... ... ... If Nothing breaks"
date: "December 28 2025" date: "December 28 2025"
# be sure to set this every new version
isCurrent: true
versions: versions:
- number: "Chat" - number: "Chat"
icon: "" icon: ""

View File

@@ -11,6 +11,8 @@ credits:
role: "Developer" role: "Developer"
- name: "Kenny" - name: "Kenny"
role: "Developer" role: "Developer"
- name: "Tsubasa"
role: "Developer"
- name: "Zura" - name: "Zura"
role: "Developer" role: "Developer"
- name: "Additional Contributors" - name: "Additional Contributors"

View File

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

View File

@@ -92,7 +92,7 @@ public sealed class PenumbraTexture : PenumbraBase
{ {
token.ThrowIfCancellationRequested(); 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); var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps);
await convertTask.ConfigureAwait(false); await convertTask.ConfigureAwait(false);

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.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LightlessSync.LightlessConfiguration; namespace LightlessSync.LightlessConfiguration;
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, TransientConfigService transientConfigService, public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, TransientConfigService transientConfigService,
ServerConfigService serverConfigService) : IHostedService ServerConfigService serverConfigService, TempCollectionConfigService tempCollectionConfigService,
LightlessConfigService lightlessConfigService) : IHostedService
{ {
private readonly ILogger<ConfigurationMigrator> _logger = logger; private readonly ILogger<ConfigurationMigrator> _logger = logger;
@@ -51,6 +57,8 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
serverConfigService.Current.Version = 2; serverConfigService.Current.Version = 2;
serverConfigService.Save(); serverConfigService.Save();
} }
MigrateTempCollectionConfig(tempCollectionConfigService, lightlessConfigService);
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
@@ -63,4 +71,273 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
{ {
return Task.CompletedTask; 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); _logger.LogTrace("Saving {configName}", config.ConfigurationName);
var configDir = config.ConfigurationPath.Replace(config.ConfigurationName, string.Empty); 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); try
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())
{ {
foreach (var oldBak in existingConfigs.Skip(10).ToList()) var configBackupFolder = Path.Join(configDir, BackupFolder);
{ if (!Directory.Exists(configBackupFolder))
oldBak.Delete(); Directory.CreateDirectory(configBackupFolder);
}
}
string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]); var configNameSplit = config.ConfigurationName.Split(".");
_logger.LogTrace("Backing up current config to {backupPath}", backupPath); var existingConfigs = Directory.EnumerateFiles(
File.Copy(config.ConfigurationPath, backupPath, overwrite: true); configBackupFolder,
FileInfo fi = new(backupPath); configNameSplit[0] + "*")
fi.LastWriteTimeUtc = DateTime.UtcNow; .Select(c => new FileInfo(c))
} .OrderByDescending(c => c.LastWriteTime).ToList();
catch (Exception ex) if (existingConfigs.Skip(10).Any())
{ {
// ignore if file cannot be backupped foreach (var oldBak in existingConfigs.Skip(10).ToList())
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath); {
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"; 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() await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(config.Current, typeof(T), new JsonSerializerOptions()
{ {
WriteIndented = true WriteIndented = !isTempCollections
})).ConfigureAwait(false); })).ConfigureAwait(false);
File.Move(temp, config.ConfigurationPath, true); File.Move(temp, config.ConfigurationPath, true);
config.UpdateLastWriteTime(); config.UpdateLastWriteTime();

View File

@@ -160,8 +160,6 @@ public class LightlessConfig : ILightlessConfiguration
public string? SelectedFinderSyncshell { get; set; } = null; public string? SelectedFinderSyncshell { get; set; } = null;
public string LastSeenVersion { get; set; } = string.Empty; public string LastSeenVersion { get; set; } = string.Empty;
public bool EnableParticleEffects { get; set; } = true; 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 AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Unsafe;
public bool AnimationAllowOneBasedShift { get; set; } = false; public bool AnimationAllowOneBasedShift { get; set; } = false;
public bool AnimationAllowNeighborIndexTolerance { 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> <PropertyGroup>
<Authors></Authors> <Authors></Authors>
<Company></Company> <Company></Company>
<Version>2.0.2.78</Version> <Version>2.0.2.88</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

@@ -125,6 +125,8 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<FileTransferOrchestrator>(); services.AddSingleton<FileTransferOrchestrator>();
services.AddSingleton<LightlessPlugin>(); services.AddSingleton<LightlessPlugin>();
services.AddSingleton<LightlessProfileManager>(); services.AddSingleton<LightlessProfileManager>();
services.AddSingleton<TextureProcessingQueue>();
services.AddSingleton<ModelProcessingQueue>();
services.AddSingleton<TextureCompressionService>(); services.AddSingleton<TextureCompressionService>();
services.AddSingleton<TextureDownscaleService>(); services.AddSingleton<TextureDownscaleService>();
services.AddSingleton<ModelDecimationService>(); services.AddSingleton<ModelDecimationService>();
@@ -429,6 +431,7 @@ public sealed class Plugin : IDalamudPlugin
LightlessSync.UI.Style.MainStyle.Init(cfg, theme); LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
return cfg; return cfg;
}); });
services.AddSingleton(sp => new TempCollectionConfigService(configDir));
services.AddSingleton(sp => new ServerConfigService(configDir)); services.AddSingleton(sp => new ServerConfigService(configDir));
services.AddSingleton(sp => new NotesConfigService(configDir)); services.AddSingleton(sp => new NotesConfigService(configDir));
services.AddSingleton(sp => new PairTagConfigService(configDir)); services.AddSingleton(sp => new PairTagConfigService(configDir));
@@ -442,6 +445,7 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>()); services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>()); services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>()); 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<ServerConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>()); services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>()); services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());

View File

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

View File

@@ -701,7 +701,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
str += $" Room #{location.RoomId}"; 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; return str;
} }

View File

@@ -1,3 +1,4 @@
using Lifestream.Enums;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Dto.CharaData; using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.User; 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) public DateTimeOffset GetSharingStatus(string uid)
{ {
try try

View File

@@ -1343,22 +1343,11 @@ internal static class MdlDecimator
return false; return false;
} }
return IsBodyMaterial(mdl.Materials[mesh.MaterialIndex]); return ModelDecimationFilters.IsBodyMaterial(mdl.Materials[mesh.MaterialIndex]);
} }
private static bool IsBodyMaterial(string materialPath) private static bool IsBodyMaterial(string materialPath)
{ => ModelDecimationFilters.IsBodyMaterial(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);
}
private sealed class BodyCollisionData private sealed class BodyCollisionData
{ {
@@ -2115,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); decoded = new DecodedMeshData(positions, normals, tangents, tangents2, colors, boneWeights, uvChannels, positionWs, normalWs, blendWeightEncoding);
return true; return true;
} }
@@ -3424,6 +3423,44 @@ internal static class MdlDecimator
return ToUShortNormalized(normalized); 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) private static void NormalizeWeights(float[] weights)
{ {
var sum = weights.Sum(); var sum = weights.Sum();

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

@@ -1,6 +1,7 @@
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.Services;
using LightlessSync.Utils; using LightlessSync.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
@@ -19,6 +20,7 @@ public sealed class ModelDecimationService
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly PlayerPerformanceConfigService _performanceConfigService; private readonly PlayerPerformanceConfigService _performanceConfigService;
private readonly XivDataStorageService _xivDataStorageService; private readonly XivDataStorageService _xivDataStorageService;
private readonly ModelProcessingQueue _processingQueue;
private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs); private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs);
private readonly TaskRegistry<string> _decimationDeduplicator = new(); private readonly TaskRegistry<string> _decimationDeduplicator = new();
@@ -30,13 +32,15 @@ public sealed class ModelDecimationService
LightlessConfigService configService, LightlessConfigService configService,
FileCacheManager fileCacheManager, FileCacheManager fileCacheManager,
PlayerPerformanceConfigService performanceConfigService, PlayerPerformanceConfigService performanceConfigService,
XivDataStorageService xivDataStorageService) XivDataStorageService xivDataStorageService,
ModelProcessingQueue processingQueue)
{ {
_logger = logger; _logger = logger;
_configService = configService; _configService = configService;
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_performanceConfigService = performanceConfigService; _performanceConfigService = performanceConfigService;
_xivDataStorageService = xivDataStorageService; _xivDataStorageService = xivDataStorageService;
_processingQueue = processingQueue;
} }
public void ScheduleDecimation(string hash, string filePath, string? gamePath = null) 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); _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 try
{ {
await DecimateInternalAsync(hash, filePath).ConfigureAwait(false); await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
@@ -69,7 +73,7 @@ public sealed class ModelDecimationService
{ {
_decimationSemaphore.Release(); _decimationSemaphore.Release();
} }
}); }, CancellationToken.None));
} }
public void ScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings) 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); _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 try
{ {
await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false); await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false);
@@ -105,7 +109,7 @@ public sealed class ModelDecimationService
{ {
_decimationSemaphore.Release(); _decimationSemaphore.Release();
} }
}); }, CancellationToken.None));
} }
public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null) public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)
@@ -348,46 +352,40 @@ public sealed class ModelDecimationService
return true; return true;
} }
var normalized = NormalizeGamePath(gamePath); var normalized = ModelDecimationFilters.NormalizePath(gamePath);
if (normalized.Contains("/hair/", StringComparison.Ordinal)) if (ModelDecimationFilters.IsHairPath(normalized))
{ {
return false; return false;
} }
if (normalized.Contains("/chara/equipment/", StringComparison.Ordinal)) if (ModelDecimationFilters.IsClothingPath(normalized))
{ {
return _performanceConfigService.Current.ModelDecimationAllowClothing; return _performanceConfigService.Current.ModelDecimationAllowClothing;
} }
if (normalized.Contains("/chara/accessory/", StringComparison.Ordinal)) if (ModelDecimationFilters.IsAccessoryPath(normalized))
{ {
return _performanceConfigService.Current.ModelDecimationAllowAccessories; 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)) if (ModelDecimationFilters.IsFaceHeadPath(normalized))
{ {
return _performanceConfigService.Current.ModelDecimationAllowFaceHead; return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
} }
if (normalized.Contains("/tail/", StringComparison.Ordinal)) if (ModelDecimationFilters.IsTailOrEarPath(normalized))
{ {
return _performanceConfigService.Current.ModelDecimationAllowTail; return _performanceConfigService.Current.ModelDecimationAllowTail;
}
} }
return true; return true;
} }
private static string NormalizeGamePath(string path)
=> path.Replace('\\', '/').ToLowerInvariant();
private bool TryGetDecimationSettings(out ModelDecimationSettings settings) private bool TryGetDecimationSettings(out ModelDecimationSettings settings)
{ {
settings = new ModelDecimationSettings( settings = new ModelDecimationSettings(

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,8 +4,10 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs; using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services; using LightlessSync.Services;
@@ -40,6 +42,7 @@ public class DrawUserPair
private readonly LocationShareService _locationShareService; private readonly LocationShareService _locationShareService;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly PairLedger _pairLedger; private readonly PairLedger _pairLedger;
private readonly IpcCallerLifestream _lifestreamIpc;
private float _menuWidth = -1; private float _menuWidth = -1;
private bool _wasHovered = false; private bool _wasHovered = false;
private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty; private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty;
@@ -60,7 +63,8 @@ public class DrawUserPair
LightlessConfigService configService, LightlessConfigService configService,
LocationShareService locationShareService, LocationShareService locationShareService,
CharaDataManager charaDataManager, CharaDataManager charaDataManager,
PairLedger pairLedger) PairLedger pairLedger,
IpcCallerLifestream lifestreamIpc)
{ {
_id = id; _id = id;
_uiEntry = uiEntry; _uiEntry = uiEntry;
@@ -79,6 +83,7 @@ public class DrawUserPair
_locationShareService = locationShareService; _locationShareService = locationShareService;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_pairLedger = pairLedger; _pairLedger = pairLedger;
_lifestreamIpc = lifestreamIpc;
} }
public PairDisplayEntry DisplayEntry => _displayEntry; public PairDisplayEntry DisplayEntry => _displayEntry;
@@ -656,6 +661,13 @@ public class DrawUserPair
using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther)) using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther))
_uiSharedService.IconText(shareLocationIcon); _uiSharedService.IconText(shareLocationIcon);
var popupId = $"LocationPopup_{_pair.UserData.UID}";
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && shareLocation && !string.IsNullOrEmpty(location))
{
ImGui.OpenPopup(popupId);
}
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
{ {
ImGui.BeginTooltip(); ImGui.BeginTooltip();
@@ -669,6 +681,8 @@ public class DrawUserPair
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow); _uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted(location); ImGui.TextUnformatted(location);
ImGui.Separator();
ImGui.TextUnformatted("Click to teleport to this location");
} }
else else
{ {
@@ -700,6 +714,62 @@ public class DrawUserPair
} }
ImGui.EndTooltip(); 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) if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)

View File

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

View File

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