Compare commits
32 Commits
2.0.2.78-D
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59b2776943 | ||
|
|
b3c35dbfdc | ||
|
|
84a3293f6b | ||
|
|
28db3d6fd2 | ||
| d83ca98008 | |||
| e6df37bcca | |||
| 60d144b881 | |||
|
|
995e11371a | ||
| 5089dbd6c8 | |||
|
|
abc324bf4f | ||
|
|
eee0e072bd | ||
|
|
d8335eb04f | ||
|
|
994335c6b0 | ||
|
|
172288c755 | ||
|
|
1c17be53d0 | ||
| 68b4863f52 | |||
|
|
22fe9901a4 | ||
|
|
cff866dcc2 | ||
|
|
e8f598e695 | ||
|
|
861a337029 | ||
| 06f89955d3 | |||
|
|
367af2c3d0 | ||
|
|
19a238c808 | ||
|
|
c7a2b679f2 | ||
|
|
bec69074a5 | ||
| 7d86b41cee | |||
| 0185e6b534 | |||
|
|
90bf84f8eb | ||
| f27db300ec | |||
| 828be6eb5b | |||
| d039d2fd90 | |||
| e75a371475 |
@@ -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, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users."
|
||||||
|
- "Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with."
|
||||||
|
- "Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives."
|
||||||
|
- number: "UI Changes (Thanks to Choco for UI Changes)"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless."
|
||||||
|
- "Settings have gotten some changes as well to make this change more universal, and will use the same color settings."
|
||||||
|
- "The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior"
|
||||||
|
- "Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it"
|
||||||
|
- number: "LightFinder / ShellFinder"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does."
|
||||||
|
- "A list of players using LightFinder near you is now available."
|
||||||
|
- "Improved the look of ShellFinder; a new pop-up for the join screen is included. This will be merged into more spots later."
|
||||||
|
- number: "Syncing Changes"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Fixed a bottleneck that caused hitching for some users."
|
||||||
|
- "Fixed a bug where files in the downscale directory were not being deleted correctly."
|
||||||
|
- "Improved performance tracking by tracking both original and effective triangle counts."
|
||||||
|
- "Improved owned object detection."
|
||||||
|
- "Reverted collection work to old way to mitigate the syncing and VFX issues."
|
||||||
|
- "Fixed minion/mount/pet tracking to validate against player-related."
|
||||||
|
- "Added timeout support to Wait For Fully Loaded."
|
||||||
|
- "Enhanced pair handling to support model decimation. (Effective Triangles)."
|
||||||
|
- "Improved downloads through atomization."
|
||||||
|
- "Improved data management by only pushing to online pairs."
|
||||||
|
- number: "Syncshell Admin Panel changes"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Changes to banned userlist to allow for preemptive banning of users."
|
||||||
|
- "Fixed when you are the owner, the buttons aren't overflowing into the side."
|
||||||
|
- "Pruning has been fixed not to include online users. It will only focus on offline users now."
|
||||||
|
- "Fixed that moderators can finally broadcast the syncshell."
|
||||||
|
- number: "Bug Fixes"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Fixed a bug in the client CI/CD workflow where the plugin was contained twice in the zip. This will cut down on overall plugin size."
|
||||||
|
- "Added new flags for certain ImGUI windows to fix focusing issues with Chat2."
|
||||||
|
- "Validating PTRs for the game objects to prevent crashing while switching characters."
|
||||||
|
- number: "Lifestream Plugin integration"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "We have added Lifestream as an optional plugin you can install to support multiple future implementations >:3. In the near future, you'll be able to go straight to your pair using the Lifestream plugin in as easy as one click. We are also cooking something up for now; it will simply show as an optional plugin."
|
||||||
|
- "Integrated Lifestream to Location Sharing to easily teleport to all plots/apartments, and most maps."
|
||||||
|
- number: "Q1 Roadmap"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Diagnostics Window - Est. Feb"
|
||||||
|
- "Ability to set a location as a 'Home' in syncshells - Est. Mid-Late Feb"
|
||||||
|
- "Lifestream support for Syncshell Homes - Est. Mid-Late Feb"
|
||||||
|
- "Community-oriented feature - In Refinement (est. Mar/Apr)"
|
||||||
- name: "v2.0.2"
|
- 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: ""
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
namespace Lifestream.Enums;
|
||||||
|
|
||||||
|
public enum TerritoryTypeIdHousing
|
||||||
|
{
|
||||||
|
None = -1,
|
||||||
|
|
||||||
|
// Mist (Limsa Lominsa)
|
||||||
|
Mist = 339,
|
||||||
|
MistSmall = 282,
|
||||||
|
MistMedium = 283,
|
||||||
|
MistLarge = 284,
|
||||||
|
MistFCRoom = 384,
|
||||||
|
MistFCWorkshop = 423,
|
||||||
|
MistApartment = 608,
|
||||||
|
|
||||||
|
// Lavender Beds (Gridania)
|
||||||
|
Lavender = 340,
|
||||||
|
LavenderSmall = 342,
|
||||||
|
LavenderMedium = 343,
|
||||||
|
LavenderLarge = 344,
|
||||||
|
LavenderFCRoom = 385,
|
||||||
|
LavenderFCWorkshop = 425,
|
||||||
|
LavenderApartment = 609,
|
||||||
|
|
||||||
|
// Goblet (Ul'dah)
|
||||||
|
Goblet = 341,
|
||||||
|
GobletSmall = 345,
|
||||||
|
GobletMedium = 346,
|
||||||
|
GobletLarge = 347,
|
||||||
|
GobletFCRoom = 386,
|
||||||
|
GobletFCWorkshop = 424,
|
||||||
|
GobletApartment = 610,
|
||||||
|
|
||||||
|
// Shirogane (Kugane)
|
||||||
|
Shirogane = 641,
|
||||||
|
ShiroganeSmall = 649,
|
||||||
|
ShiroganeMedium = 650,
|
||||||
|
ShiroganeLarge = 651,
|
||||||
|
ShiroganeFCRoom = 652,
|
||||||
|
ShiroganeFCWorkshop = 653,
|
||||||
|
ShiroganeApartment = 655,
|
||||||
|
|
||||||
|
// Empyreum (Ishgard)
|
||||||
|
Empyream = 979,
|
||||||
|
EmpyreamSmall = 980,
|
||||||
|
EmpyreamMedium = 981,
|
||||||
|
EmpyreamLarge = 982,
|
||||||
|
EmpyreamFCRoom = 983,
|
||||||
|
EmpyreamFCWorkshop = 984,
|
||||||
|
EmpyreamApartment = 999,
|
||||||
|
}
|
||||||
@@ -92,7 +92,7 @@ public sealed class PenumbraTexture : PenumbraBase
|
|||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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; } = [];
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>());
|
||||||
|
|||||||
93
LightlessSync/Services/AssetProcessingQueue.cs
Normal file
93
LightlessSync/Services/AssetProcessingQueue.cs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
|
public sealed class AssetProcessingQueue : IDisposable
|
||||||
|
{
|
||||||
|
private readonly BlockingCollection<WorkItem> _queue = new();
|
||||||
|
private readonly Thread _worker;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public AssetProcessingQueue(ILogger logger, string name)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_worker = new Thread(Run)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = string.IsNullOrWhiteSpace(name) ? "LightlessSync.AssetProcessing" : name
|
||||||
|
};
|
||||||
|
_worker.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (work is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(work));
|
||||||
|
}
|
||||||
|
|
||||||
|
var completion = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
completion.TrySetCanceled(token);
|
||||||
|
return completion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_queue.IsAddingCompleted || _disposed)
|
||||||
|
{
|
||||||
|
completion.TrySetException(new ObjectDisposedException(nameof(AssetProcessingQueue)));
|
||||||
|
return completion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
_queue.Add(new WorkItem(work, token, completion));
|
||||||
|
return completion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Run()
|
||||||
|
{
|
||||||
|
foreach (var item in _queue.GetConsumingEnumerable())
|
||||||
|
{
|
||||||
|
if (item.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
item.Completion.TrySetCanceled(item.Token);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
item.Work(item.Token).GetAwaiter().GetResult();
|
||||||
|
item.Completion.TrySetResult(null);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex)
|
||||||
|
{
|
||||||
|
var token = ex.CancellationToken.IsCancellationRequested ? ex.CancellationToken : item.Token;
|
||||||
|
item.Completion.TrySetCanceled(token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Asset processing job failed.");
|
||||||
|
item.Completion.TrySetException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
_queue.CompleteAdding();
|
||||||
|
_worker.Join(TimeSpan.FromSeconds(2));
|
||||||
|
_queue.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct WorkItem(
|
||||||
|
Func<CancellationToken, Task> Work,
|
||||||
|
CancellationToken Token,
|
||||||
|
TaskCompletionSource<object?> Completion);
|
||||||
|
}
|
||||||
@@ -701,7 +701,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
str += $" Room #{location.RoomId}";
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
132
LightlessSync/Services/ModelDecimation/ModelDecimationFilters.cs
Normal file
132
LightlessSync/Services/ModelDecimation/ModelDecimationFilters.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
19
LightlessSync/Services/ModelProcessingQueue.cs
Normal file
19
LightlessSync/Services/ModelProcessingQueue.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
|
public sealed class ModelProcessingQueue : IDisposable
|
||||||
|
{
|
||||||
|
private readonly AssetProcessingQueue _queue;
|
||||||
|
|
||||||
|
public ModelProcessingQueue(ILogger<ModelProcessingQueue> logger)
|
||||||
|
{
|
||||||
|
_queue = new AssetProcessingQueue(logger, "LightlessSync.ModelProcessing");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
|
||||||
|
=> _queue.Enqueue(work, token);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
=> _queue.Dispose();
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
19
LightlessSync/Services/TextureProcessingQueue.cs
Normal file
19
LightlessSync/Services/TextureProcessingQueue.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
|
public sealed class TextureProcessingQueue : IDisposable
|
||||||
|
{
|
||||||
|
private readonly AssetProcessingQueue _queue;
|
||||||
|
|
||||||
|
public TextureProcessingQueue(ILogger<TextureProcessingQueue> logger)
|
||||||
|
{
|
||||||
|
_queue = new AssetProcessingQueue(logger, "LightlessSync.TextureProcessing");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
|
||||||
|
=> _queue.Enqueue(work, token);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
=> _queue.Dispose();
|
||||||
|
}
|
||||||
@@ -4,8 +4,10 @@ using Dalamud.Interface.Utility;
|
|||||||
using Dalamud.Interface.Utility.Raii;
|
using 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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
71
LightlessSync/Utils/PtrGuard.cs
Normal file
71
LightlessSync/Utils/PtrGuard.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using static LightlessSync.Utils.PtrGuardMemory;
|
||||||
|
|
||||||
|
namespace LightlessSync.Utils
|
||||||
|
{
|
||||||
|
public static partial class PtrGuard
|
||||||
|
{
|
||||||
|
private const ulong _aligmentPtr = 0x7UL;
|
||||||
|
private static readonly nuint _minAppAddr = (nuint)GetMinAppAddr();
|
||||||
|
private static readonly nuint _maxAppAddr = (nuint)GetMaxAppAddr();
|
||||||
|
|
||||||
|
private static nint GetMinAppAddr()
|
||||||
|
{
|
||||||
|
GetSystemInfo(out var si);
|
||||||
|
return si.lpMinimumApplicationAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint GetMaxAppAddr()
|
||||||
|
{
|
||||||
|
GetSystemInfo(out var si);
|
||||||
|
return si.lpMaximumApplicationAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool LooksLikePtr(nint p)
|
||||||
|
{
|
||||||
|
if (p == 0) return false;
|
||||||
|
nuint u = (nuint)p;
|
||||||
|
|
||||||
|
if (u < _minAppAddr) return false;
|
||||||
|
if (u > _maxAppAddr) return false;
|
||||||
|
if ((u & _aligmentPtr) != 0) return false;
|
||||||
|
if ((uint)u == 0x12345679u) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryReadIntPtr(nint addr, out nint value)
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
|
||||||
|
if (!LooksLikePtr(addr))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return ReadProcessMemory(GetCurrentProcess(), addr, out value, (nuint)IntPtr.Size, out nuint bytesRead)
|
||||||
|
&& bytesRead == (nuint)IntPtr.Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsReadable(nint addr, nuint size)
|
||||||
|
{
|
||||||
|
if (addr == 0 || size == 0) return false;
|
||||||
|
|
||||||
|
if (VirtualQuery(addr, out var mbi, (nuint)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const uint Commit = 0x1000;
|
||||||
|
const uint NoAccess = 0x01;
|
||||||
|
const uint PageGuard = 0x100;
|
||||||
|
|
||||||
|
if (mbi.State != Commit) return false;
|
||||||
|
if ((mbi.Protect & PageGuard) != 0) return false;
|
||||||
|
if (mbi.Protect == NoAccess) return false;
|
||||||
|
|
||||||
|
ulong start = (ulong)addr;
|
||||||
|
ulong end = start + size - 1;
|
||||||
|
ulong r0 = (ulong)mbi.BaseAddress;
|
||||||
|
ulong r1 = r0 + mbi.RegionSize - 1;
|
||||||
|
|
||||||
|
return start >= r0 && end <= r1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
LightlessSync/Utils/PtrGuardMemory.cs
Normal file
55
LightlessSync/Utils/PtrGuardMemory.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace LightlessSync.Utils
|
||||||
|
{
|
||||||
|
internal static class PtrGuardMemory
|
||||||
|
{
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct MEMORY_BASIC_INFORMATION
|
||||||
|
{
|
||||||
|
public nint BaseAddress;
|
||||||
|
public nint AllocationBase;
|
||||||
|
public uint AllocationProtect;
|
||||||
|
public nuint RegionSize;
|
||||||
|
public uint State;
|
||||||
|
public uint Protect;
|
||||||
|
public uint Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
internal static extern nuint VirtualQuery(
|
||||||
|
nint lpAddress,
|
||||||
|
out MEMORY_BASIC_INFORMATION lpBuffer,
|
||||||
|
nuint dwLength);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
internal static extern bool ReadProcessMemory(
|
||||||
|
nint hProcess,
|
||||||
|
nint lpBaseAddress,
|
||||||
|
out nint lpBuffer,
|
||||||
|
nuint nSize,
|
||||||
|
out nuint lpNumberOfBytesRead);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
internal static extern nint GetCurrentProcess();
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
internal static extern void GetSystemInfo(out SYSTEM_INFO lpSystemInfo);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct SYSTEM_INFO
|
||||||
|
{
|
||||||
|
public ushort wProcessorArchitecture;
|
||||||
|
public ushort wReserved;
|
||||||
|
public uint dwPageSize;
|
||||||
|
public nint lpMinimumApplicationAddress;
|
||||||
|
public nint lpMaximumApplicationAddress;
|
||||||
|
public nint dwActiveProcessorMask;
|
||||||
|
public uint dwNumberOfProcessors;
|
||||||
|
public uint dwProcessorType;
|
||||||
|
public uint dwAllocationGranularity;
|
||||||
|
public ushort wProcessorLevel;
|
||||||
|
public ushort wProcessorRevision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user