Compare commits
2 Commits
slight-twe
...
2.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e00f80d7bd | ||
| 72a62b7449 |
@@ -1,11 +1,86 @@
|
||||
tagline: "Lightless Sync v2.0.1"
|
||||
subline: "LIGHTLESS IS EVOLVING!!"
|
||||
tagline: "Lightless Sync v2.1.0"
|
||||
subline: "Location Sharing, Animation Scanning, Sync Improvements, Lifestream IPC, and More!"
|
||||
changelog:
|
||||
- name: "v2.1.0"
|
||||
tagline: "Happy New Year!"
|
||||
date: "January 20 2026"
|
||||
# be sure to set this every new version
|
||||
isCurrent: true
|
||||
versions:
|
||||
- number: "Location Sharing (S/O to Tsubasa from the MareCN team for this feature!)"
|
||||
icon: ""
|
||||
items:
|
||||
- "Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] or until you turn it off for them. That's up to you!"
|
||||
- "To share your location with a pair, click the three dots beside the pair and choose a duration to share with them."
|
||||
- "To view the location of someone who's shared with you, simply hover over the globe icon!"
|
||||
- number: "Model Optimization (Mesh Decimating)"
|
||||
icon: ""
|
||||
items:
|
||||
- "This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). "
|
||||
- "Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for."
|
||||
- "Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking."
|
||||
- "You can find everything under Settings → Performance → Model Optimization."
|
||||
- "IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE!"
|
||||
- number: "Animation (PAP) Validation (Safer animations)"
|
||||
icon: ""
|
||||
items:
|
||||
- "Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users."
|
||||
- "Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with."
|
||||
- "Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives."
|
||||
- number: "UI Changes (Thanks to Choco for UI Changes)"
|
||||
icon: ""
|
||||
items:
|
||||
- "The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless."
|
||||
- "Settings have gotten some changes as well to make this change more universal, and will use the same color settings."
|
||||
- "The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior"
|
||||
- "Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it"
|
||||
- number: "LightFinder / ShellFinder"
|
||||
icon: ""
|
||||
items:
|
||||
- "UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does."
|
||||
- "A list of players using LightFinder near you is now available."
|
||||
- "Improved the look of ShellFinder; a new pop-up for the join screen is included. This will be merged into more spots later."
|
||||
- number: "Syncing Changes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed a bottleneck that caused hitching for some users."
|
||||
- "Fixed a bug where files in the downscale directory were not being deleted correctly."
|
||||
- "Improved performance tracking by tracking both original and effective triangle counts."
|
||||
- "Improved owned object detection."
|
||||
- "Reverted collection work to old way to mitigate the syncing and VFX issues."
|
||||
- "Fixed minion/mount/pet tracking to validate against player-related."
|
||||
- "Added timeout support to Wait For Fully Loaded."
|
||||
- "Enhanced pair handling to support model decimation. (Effective Triangles)."
|
||||
- "Improved downloads through atomization."
|
||||
- "Improved data management by only pushing to online pairs."
|
||||
- number: "Syncshell Admin Panel changes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Changes to banned userlist to allow for preemptive banning of users."
|
||||
- "Fixed when you are the owner, the buttons aren't overflowing into the side."
|
||||
- "Pruning has been fixed not to include online users. It will only focus on offline users now."
|
||||
- "Fixed that moderators can finally broadcast the syncshell."
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed a bug in the client CI/CD workflow where the plugin was contained twice in the zip. This will cut down on overall plugin size."
|
||||
- "Added new flags for certain ImGUI windows to fix focusing issues with Chat2."
|
||||
- "Validating PTRs for the game objects to prevent crashing while switching characters."
|
||||
- number: "Lifestream Plugin integration"
|
||||
icon: ""
|
||||
items:
|
||||
- "We have added Lifestream as an optional plugin you can install to support multiple future implementations >:3. In the near future, you'll be able to go straight to your pair using the Lifestream plugin in as easy as one click. We are also cooking something up for now; it will simply show as an optional plugin."
|
||||
- "Integrated Lifestream to Location Sharing to easily teleport to all plots/apartments, and most maps."
|
||||
- number: "Q1 Roadmap"
|
||||
icon: ""
|
||||
items:
|
||||
- "Diagnostics Window - Est. Feb"
|
||||
- "Ability to set a location as a 'Home' in syncshells - Est. Mid-Late Feb"
|
||||
- "Lifestream support for Syncshell Homes - Est. Mid-Late Feb"
|
||||
- "Community-oriented feature - In Refinement (est. Mar/Apr)"
|
||||
- name: "v2.0.2"
|
||||
tagline: "Last update of 2025!... ... ... If Nothing breaks"
|
||||
date: "December 28 2025"
|
||||
# be sure to set this every new version
|
||||
isCurrent: true
|
||||
versions:
|
||||
- number: "Chat"
|
||||
icon: ""
|
||||
|
||||
@@ -11,6 +11,8 @@ credits:
|
||||
role: "Developer"
|
||||
- name: "Kenny"
|
||||
role: "Developer"
|
||||
- name: "Tsubasa"
|
||||
role: "Developer"
|
||||
- name: "Zura"
|
||||
role: "Developer"
|
||||
- name: "Additional Contributors"
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace Lifestream.Enums;
|
||||
|
||||
public enum TerritoryTypeIdHousing
|
||||
{
|
||||
None = -1,
|
||||
|
||||
// Mist (Limsa Lominsa)
|
||||
Mist = 339,
|
||||
MistSmall = 282,
|
||||
MistMedium = 283,
|
||||
MistLarge = 284,
|
||||
MistFCRoom = 384,
|
||||
MistFCWorkshop = 423,
|
||||
MistApartment = 608,
|
||||
|
||||
// Lavender Beds (Gridania)
|
||||
Lavender = 340,
|
||||
LavenderSmall = 342,
|
||||
LavenderMedium = 343,
|
||||
LavenderLarge = 344,
|
||||
LavenderFCRoom = 385,
|
||||
LavenderFCWorkshop = 425,
|
||||
LavenderApartment = 609,
|
||||
|
||||
// Goblet (Ul'dah)
|
||||
Goblet = 341,
|
||||
GobletSmall = 345,
|
||||
GobletMedium = 346,
|
||||
GobletLarge = 347,
|
||||
GobletFCRoom = 386,
|
||||
GobletFCWorkshop = 424,
|
||||
GobletApartment = 610,
|
||||
|
||||
// Shirogane (Kugane)
|
||||
Shirogane = 641,
|
||||
ShiroganeSmall = 649,
|
||||
ShiroganeMedium = 650,
|
||||
ShiroganeLarge = 651,
|
||||
ShiroganeFCRoom = 652,
|
||||
ShiroganeFCWorkshop = 653,
|
||||
ShiroganeApartment = 655,
|
||||
|
||||
// Empyreum (Ishgard)
|
||||
Empyream = 979,
|
||||
EmpyreamSmall = 980,
|
||||
EmpyreamMedium = 981,
|
||||
EmpyreamLarge = 982,
|
||||
EmpyreamFCRoom = 983,
|
||||
EmpyreamFCWorkshop = 984,
|
||||
EmpyreamApartment = 999,
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
using LightlessSync.WebAPI;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration;
|
||||
|
||||
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, TransientConfigService transientConfigService,
|
||||
ServerConfigService serverConfigService) : IHostedService
|
||||
ServerConfigService serverConfigService, TempCollectionConfigService tempCollectionConfigService,
|
||||
LightlessConfigService lightlessConfigService) : IHostedService
|
||||
{
|
||||
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
||||
|
||||
@@ -51,6 +57,8 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
|
||||
serverConfigService.Current.Version = 2;
|
||||
serverConfigService.Save();
|
||||
}
|
||||
|
||||
MigrateTempCollectionConfig(tempCollectionConfigService, lightlessConfigService);
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
@@ -63,4 +71,273 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void MigrateTempCollectionConfig(TempCollectionConfigService tempCollectionConfigService, LightlessConfigService lightlessConfigService)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
TempCollectionConfig tempConfig = tempCollectionConfigService.Current;
|
||||
var tempChanged = false;
|
||||
var tempNeedsSave = false;
|
||||
|
||||
if (TryReadTempCollectionData(lightlessConfigService.ConfigurationPath, out var root, out var ids, out var entries))
|
||||
{
|
||||
tempChanged |= MergeTempCollectionData(tempConfig, ids, entries, now);
|
||||
var removed = root.Remove("OrphanableTempCollections");
|
||||
removed |= root.Remove("OrphanableTempCollectionEntries");
|
||||
if (removed)
|
||||
{
|
||||
try
|
||||
{
|
||||
string updatedJson = root.ToJsonString(new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
File.WriteAllText(lightlessConfigService.ConfigurationPath, updatedJson);
|
||||
lightlessConfigService.UpdateLastWriteTime();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to rewrite {config} after temp collection migration", lightlessConfigService.ConfigurationPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.Count > 0 || entries.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Migrated {ids} temp collection ids and {entries} entries to {configName}",
|
||||
ids.Count, entries.Count, tempCollectionConfigService.ConfigurationName);
|
||||
}
|
||||
}
|
||||
|
||||
if (TryReadTempCollectionData(tempCollectionConfigService.ConfigurationPath, out var tempRoot, out var tempIds, out var tempEntries))
|
||||
{
|
||||
tempChanged |= MergeTempCollectionData(tempConfig, tempIds, tempEntries, now);
|
||||
if (tempRoot.Remove("OrphanableTempCollections"))
|
||||
{
|
||||
tempNeedsSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (tempChanged || tempNeedsSave)
|
||||
{
|
||||
tempCollectionConfigService.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryReadTempCollectionData(string configPath, out JsonObject root, out HashSet<Guid> ids, out List<OrphanableTempCollectionEntry> entries)
|
||||
{
|
||||
root = new JsonObject();
|
||||
ids = [];
|
||||
entries = [];
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
root = JsonNode.Parse(File.ReadAllText(configPath)) as JsonObject ?? new JsonObject();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to read temp collection data from {config}", configPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
root.TryGetPropertyValue("OrphanableTempCollections", out JsonNode? idsNode);
|
||||
root.TryGetPropertyValue("OrphanableTempCollectionEntries", out JsonNode? entriesNode);
|
||||
|
||||
if (idsNode == null && entriesNode == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ids = ParseGuidSet(idsNode);
|
||||
entries = ParseEntries(entriesNode);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static HashSet<Guid> ParseGuidSet(JsonNode? node)
|
||||
{
|
||||
HashSet<Guid> ids = [];
|
||||
if (node is not JsonArray array)
|
||||
{
|
||||
return ids;
|
||||
}
|
||||
|
||||
foreach (JsonNode? item in array)
|
||||
{
|
||||
Guid id = ParseGuid(item);
|
||||
if (id != Guid.Empty)
|
||||
{
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static List<OrphanableTempCollectionEntry> ParseEntries(JsonNode? node)
|
||||
{
|
||||
List<OrphanableTempCollectionEntry> entries = [];
|
||||
if (node is not JsonArray array)
|
||||
{
|
||||
return entries;
|
||||
}
|
||||
|
||||
foreach (JsonNode? item in array)
|
||||
{
|
||||
if (item is not JsonObject obj)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Guid id = ParseGuid(obj["Id"]);
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DateTime registeredAtUtc = DateTime.MinValue;
|
||||
if (TryParseDateTime(obj["RegisteredAtUtc"], out DateTime parsed))
|
||||
{
|
||||
registeredAtUtc = parsed;
|
||||
}
|
||||
|
||||
entries.Add(new OrphanableTempCollectionEntry
|
||||
{
|
||||
Id = id,
|
||||
RegisteredAtUtc = registeredAtUtc
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static Guid ParseGuid(JsonNode? node)
|
||||
{
|
||||
if (node is JsonValue value)
|
||||
{
|
||||
if (value.TryGetValue<string>(out string? stringValue) && Guid.TryParse(stringValue, out Guid parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
private static bool TryParseDateTime(JsonNode? node, out DateTime value)
|
||||
{
|
||||
value = DateTime.MinValue;
|
||||
if (node is not JsonValue val)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (val.TryGetValue<DateTime>(out DateTime direct))
|
||||
{
|
||||
value = direct;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (val.TryGetValue<string>(out string? stringValue)
|
||||
&& DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime parsed))
|
||||
{
|
||||
value = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool MergeTempCollectionData(TempCollectionConfig config, HashSet<Guid> ids, List<OrphanableTempCollectionEntry> entries, DateTime now)
|
||||
{
|
||||
bool changed = false;
|
||||
Dictionary<Guid, OrphanableTempCollectionEntry> entryLookup = new();
|
||||
for (var i = config.OrphanableTempCollectionEntries.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var entry = config.OrphanableTempCollectionEntries[i];
|
||||
if (entry.Id == Guid.Empty)
|
||||
{
|
||||
config.OrphanableTempCollectionEntries.RemoveAt(i);
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entryLookup.TryGetValue(entry.Id, out var existing))
|
||||
{
|
||||
if (entry.RegisteredAtUtc != DateTime.MinValue
|
||||
&& (existing.RegisteredAtUtc == DateTime.MinValue || entry.RegisteredAtUtc < existing.RegisteredAtUtc))
|
||||
{
|
||||
existing.RegisteredAtUtc = entry.RegisteredAtUtc;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
config.OrphanableTempCollectionEntries.RemoveAt(i);
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
entryLookup[entry.Id] = entry;
|
||||
}
|
||||
|
||||
foreach (OrphanableTempCollectionEntry entry in entries)
|
||||
{
|
||||
if (entry.Id == Guid.Empty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entryLookup.TryGetValue(entry.Id, out OrphanableTempCollectionEntry? existing))
|
||||
{
|
||||
var added = new OrphanableTempCollectionEntry
|
||||
{
|
||||
Id = entry.Id,
|
||||
RegisteredAtUtc = entry.RegisteredAtUtc
|
||||
};
|
||||
config.OrphanableTempCollectionEntries.Add(added);
|
||||
entryLookup[entry.Id] = added;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.RegisteredAtUtc != DateTime.MinValue
|
||||
&& (existing.RegisteredAtUtc == DateTime.MinValue || entry.RegisteredAtUtc < existing.RegisteredAtUtc))
|
||||
{
|
||||
existing.RegisteredAtUtc = entry.RegisteredAtUtc;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Guid id in ids)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entryLookup.TryGetValue(id, out OrphanableTempCollectionEntry? existing))
|
||||
{
|
||||
var added = new OrphanableTempCollectionEntry
|
||||
{
|
||||
Id = id,
|
||||
RegisteredAtUtc = now
|
||||
};
|
||||
config.OrphanableTempCollectionEntries.Add(added);
|
||||
entryLookup[id] = added;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existing.RegisteredAtUtc == DateTime.MinValue)
|
||||
{
|
||||
existing.RegisteredAtUtc = now;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,37 +72,41 @@ public class ConfigurationSaveService : IHostedService
|
||||
{
|
||||
_logger.LogTrace("Saving {configName}", config.ConfigurationName);
|
||||
var configDir = config.ConfigurationPath.Replace(config.ConfigurationName, string.Empty);
|
||||
var isTempCollections = string.Equals(config.ConfigurationName, TempCollectionConfigService.ConfigName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
try
|
||||
if (!isTempCollections)
|
||||
{
|
||||
var configBackupFolder = Path.Join(configDir, BackupFolder);
|
||||
if (!Directory.Exists(configBackupFolder))
|
||||
Directory.CreateDirectory(configBackupFolder);
|
||||
|
||||
var configNameSplit = config.ConfigurationName.Split(".");
|
||||
var existingConfigs = Directory.EnumerateFiles(
|
||||
configBackupFolder,
|
||||
configNameSplit[0] + "*")
|
||||
.Select(c => new FileInfo(c))
|
||||
.OrderByDescending(c => c.LastWriteTime).ToList();
|
||||
if (existingConfigs.Skip(10).Any())
|
||||
try
|
||||
{
|
||||
foreach (var oldBak in existingConfigs.Skip(10).ToList())
|
||||
{
|
||||
oldBak.Delete();
|
||||
}
|
||||
}
|
||||
var configBackupFolder = Path.Join(configDir, BackupFolder);
|
||||
if (!Directory.Exists(configBackupFolder))
|
||||
Directory.CreateDirectory(configBackupFolder);
|
||||
|
||||
string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]);
|
||||
_logger.LogTrace("Backing up current config to {backupPath}", backupPath);
|
||||
File.Copy(config.ConfigurationPath, backupPath, overwrite: true);
|
||||
FileInfo fi = new(backupPath);
|
||||
fi.LastWriteTimeUtc = DateTime.UtcNow;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// ignore if file cannot be backupped
|
||||
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
|
||||
var configNameSplit = config.ConfigurationName.Split(".");
|
||||
var existingConfigs = Directory.EnumerateFiles(
|
||||
configBackupFolder,
|
||||
configNameSplit[0] + "*")
|
||||
.Select(c => new FileInfo(c))
|
||||
.OrderByDescending(c => c.LastWriteTime).ToList();
|
||||
if (existingConfigs.Skip(10).Any())
|
||||
{
|
||||
foreach (var oldBak in existingConfigs.Skip(10).ToList())
|
||||
{
|
||||
oldBak.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]);
|
||||
_logger.LogTrace("Backing up current config to {backupPath}", backupPath);
|
||||
File.Copy(config.ConfigurationPath, backupPath, overwrite: true);
|
||||
FileInfo fi = new(backupPath);
|
||||
fi.LastWriteTimeUtc = DateTime.UtcNow;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// ignore if file cannot be backupped
|
||||
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
|
||||
}
|
||||
}
|
||||
|
||||
var temp = config.ConfigurationPath + ".tmp";
|
||||
@@ -110,7 +114,7 @@ public class ConfigurationSaveService : IHostedService
|
||||
{
|
||||
await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(config.Current, typeof(T), new JsonSerializerOptions()
|
||||
{
|
||||
WriteIndented = true
|
||||
WriteIndented = !isTempCollections
|
||||
})).ConfigureAwait(false);
|
||||
File.Move(temp, config.ConfigurationPath, true);
|
||||
config.UpdateLastWriteTime();
|
||||
|
||||
@@ -160,8 +160,6 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||
public string LastSeenVersion { get; set; } = string.Empty;
|
||||
public bool EnableParticleEffects { get; set; } = true;
|
||||
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
||||
public List<OrphanableTempCollectionEntry> OrphanableTempCollectionEntries { get; set; } = [];
|
||||
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Unsafe;
|
||||
public bool AnimationAllowOneBasedShift { get; set; } = false;
|
||||
public bool AnimationAllowNeighborIndexTolerance { get; set; } = false;
|
||||
|
||||
@@ -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>
|
||||
<Authors></Authors>
|
||||
<Company></Company>
|
||||
<Version>2.0.3</Version>
|
||||
<Version>2.1.1</Version>
|
||||
<Description></Description>
|
||||
<Copyright></Copyright>
|
||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
@@ -14,6 +13,7 @@ using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
|
||||
namespace LightlessSync.PlayerData.Factories;
|
||||
|
||||
@@ -119,46 +119,30 @@ public class PlayerDataFactory
|
||||
return null;
|
||||
}
|
||||
|
||||
private static readonly int _drawObjectOffset =
|
||||
(int)Marshal.OffsetOf<GameObject>(nameof(GameObject.DrawObject));
|
||||
|
||||
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
||||
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
|
||||
|
||||
private unsafe static bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
||||
{
|
||||
if (playerPointer == IntPtr.Zero)
|
||||
return true;
|
||||
|
||||
if (!IsPointerValid(playerPointer))
|
||||
return true;
|
||||
|
||||
var character = (Character*)playerPointer;
|
||||
if (character == null)
|
||||
return true;
|
||||
|
||||
var gameObject = &character->GameObject;
|
||||
if (gameObject == null)
|
||||
return true;
|
||||
|
||||
if (!IsPointerValid((IntPtr)gameObject))
|
||||
return true;
|
||||
|
||||
return gameObject->DrawObject == null;
|
||||
}
|
||||
|
||||
private static bool IsPointerValid(IntPtr ptr)
|
||||
{
|
||||
if (ptr == IntPtr.Zero)
|
||||
return false;
|
||||
|
||||
try
|
||||
=> await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
_ = Marshal.ReadByte(ptr);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
nint basePtr = playerPointer;
|
||||
|
||||
if (!PtrGuard.LooksLikePtr(basePtr))
|
||||
return true;
|
||||
|
||||
nint drawObjAddr = basePtr + _drawObjectOffset;
|
||||
|
||||
if (!PtrGuard.IsReadable(drawObjAddr, (nuint)IntPtr.Size))
|
||||
return true;
|
||||
|
||||
if (!PtrGuard.TryReadIntPtr(drawObjAddr, out var drawObj))
|
||||
return true;
|
||||
|
||||
if (drawObj != 0 && !PtrGuard.LooksLikePtr(drawObj))
|
||||
return true;
|
||||
|
||||
return drawObj == 0;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
private static bool IsCacheFresh(CacheEntry entry)
|
||||
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
|
||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||
|
||||
namespace LightlessSync.PlayerData.Handlers;
|
||||
|
||||
@@ -177,18 +178,41 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
var prevDrawObj = DrawObjectAddress;
|
||||
string? nameString = null;
|
||||
|
||||
Address = _getAddress();
|
||||
var nextAddr = _getAddress();
|
||||
|
||||
if (nextAddr != IntPtr.Zero && !PtrGuard.LooksLikePtr(nextAddr))
|
||||
{
|
||||
nextAddr = IntPtr.Zero;
|
||||
}
|
||||
|
||||
if (nextAddr != IntPtr.Zero &&
|
||||
!PtrGuard.IsReadable(nextAddr, (nuint)sizeof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject)))
|
||||
{
|
||||
nextAddr = IntPtr.Zero;
|
||||
}
|
||||
|
||||
Address = nextAddr;
|
||||
|
||||
if (Address != IntPtr.Zero)
|
||||
{
|
||||
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
||||
DrawObjectAddress = (IntPtr)gameObject->DrawObject;
|
||||
|
||||
var draw = (nint)gameObject->DrawObject;
|
||||
|
||||
if (!PtrGuard.LooksLikePtr(draw) || !PtrGuard.IsReadable(draw, (nuint)sizeof(DrawObject)))
|
||||
draw = 0;
|
||||
|
||||
DrawObjectAddress = draw;
|
||||
EntityId = gameObject->EntityId;
|
||||
|
||||
var chara = (Character*)Address;
|
||||
nameString = chara->GameObject.NameString;
|
||||
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
||||
Name = nameString;
|
||||
if (PtrGuard.IsReadable(Address, (nuint)sizeof(Character)))
|
||||
{
|
||||
var chara = (Character*)Address;
|
||||
nameString = chara->GameObject.NameString;
|
||||
|
||||
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
||||
Name = nameString;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -196,22 +220,27 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
EntityId = uint.MaxValue;
|
||||
}
|
||||
|
||||
CurrentDrawCondition = IsBeingDrawnUnsafe();
|
||||
CurrentDrawCondition = (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
||||
? IsBeingDrawnUnsafe()
|
||||
: DrawCondition.DrawObjectZero;
|
||||
|
||||
if (_haltProcessing || !allowPublish) return;
|
||||
|
||||
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
||||
bool addrDiff = Address != prevAddr;
|
||||
|
||||
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
||||
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero
|
||||
&& PtrGuard.IsReadable(Address, (nuint)sizeof(Character))
|
||||
&& PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(DrawObject)))
|
||||
{
|
||||
var chara = (Character*)Address;
|
||||
var drawObj = (DrawObject*)DrawObjectAddress;
|
||||
|
||||
var objType = drawObj->Object.GetObjectType();
|
||||
var isHuman = objType == ObjectType.CharacterBase
|
||||
&& ((CharacterBase*)drawObj)->GetModelType() == CharacterBase.ModelType.Human;
|
||||
|
||||
nameString ??= ((Character*)Address)->GameObject.NameString;
|
||||
nameString ??= chara->GameObject.NameString;
|
||||
var nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
||||
if (nameChange) Name = nameString;
|
||||
|
||||
@@ -219,32 +248,36 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
if (isHuman)
|
||||
{
|
||||
var classJob = chara->CharacterData.ClassJob;
|
||||
if (classJob != _classJob)
|
||||
if (PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
|
||||
{
|
||||
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
||||
_classJob = classJob;
|
||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||
var classJob = chara->CharacterData.ClassJob;
|
||||
if (classJob != _classJob)
|
||||
{
|
||||
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
||||
_classJob = classJob;
|
||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||
}
|
||||
|
||||
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
|
||||
|
||||
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
||||
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
||||
|
||||
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
|
||||
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
isHuman = false;
|
||||
}
|
||||
|
||||
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
|
||||
|
||||
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
||||
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
||||
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
|
||||
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
|
||||
|
||||
if (equipDiff)
|
||||
Logger.LogTrace("Checking [{this}] equip data as human from draw obj, result: {diff}", this, equipDiff);
|
||||
}
|
||||
else
|
||||
|
||||
if (!isHuman)
|
||||
{
|
||||
equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0]));
|
||||
if (equipDiff)
|
||||
Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff);
|
||||
}
|
||||
|
||||
if (equipDiff && !_isOwnedObject) // send the message out immediately and cancel out, no reason to continue if not self
|
||||
if (equipDiff && !_isOwnedObject)
|
||||
{
|
||||
Logger.LogTrace("[{this}] Changed", this);
|
||||
return;
|
||||
@@ -252,11 +285,13 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
bool customizeDiff = false;
|
||||
|
||||
if (isHuman)
|
||||
if (isHuman && PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
|
||||
{
|
||||
var gender = ((Human*)drawObj)->Customize.Sex;
|
||||
var raceId = ((Human*)drawObj)->Customize.Race;
|
||||
var tribeId = ((Human*)drawObj)->Customize.Tribe;
|
||||
var human = (Human*)drawObj;
|
||||
|
||||
var gender = human->Customize.Sex;
|
||||
var raceId = human->Customize.Race;
|
||||
var tribeId = human->Customize.Tribe;
|
||||
|
||||
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
||||
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
||||
@@ -267,15 +302,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
TribeId = tribeId;
|
||||
}
|
||||
|
||||
customizeDiff = CompareAndUpdateCustomizeData(((Human*)drawObj)->Customize.Data);
|
||||
if (customizeDiff)
|
||||
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
|
||||
customizeDiff = CompareAndUpdateCustomizeData(human->Customize.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data);
|
||||
if (customizeDiff)
|
||||
Logger.LogTrace("Checking [{this}] customize data from game obj, result: {diff}", this, equipDiff);
|
||||
}
|
||||
|
||||
if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject)
|
||||
@@ -289,12 +320,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
||||
Logger.LogTrace("[{this}] Changed", this);
|
||||
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
|
||||
{
|
||||
Mediator.Publish(new ClearCacheForObjectMessage(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
|
||||
{
|
||||
bool hasChanges = false;
|
||||
@@ -330,7 +360,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
private unsafe bool CompareAndUpdateMainHand(Weapon* weapon)
|
||||
{
|
||||
if ((nint)weapon == nint.Zero) return false;
|
||||
var p = (nint)weapon;
|
||||
if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon)))
|
||||
return false;
|
||||
|
||||
bool hasChanges = false;
|
||||
hasChanges |= weapon->ModelSetId != MainHandData[0];
|
||||
MainHandData[0] = weapon->ModelSetId;
|
||||
@@ -343,7 +376,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
private unsafe bool CompareAndUpdateOffHand(Weapon* weapon)
|
||||
{
|
||||
if ((nint)weapon == nint.Zero) return false;
|
||||
var p = (nint)weapon;
|
||||
if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon)))
|
||||
return false;
|
||||
|
||||
bool hasChanges = false;
|
||||
hasChanges |= weapon->ModelSetId != OffHandData[0];
|
||||
OffHandData[0] = weapon->ModelSetId;
|
||||
|
||||
@@ -431,6 +431,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
|
||||
return cfg;
|
||||
});
|
||||
services.AddSingleton(sp => new TempCollectionConfigService(configDir));
|
||||
services.AddSingleton(sp => new ServerConfigService(configDir));
|
||||
services.AddSingleton(sp => new NotesConfigService(configDir));
|
||||
services.AddSingleton(sp => new PairTagConfigService(configDir));
|
||||
@@ -444,6 +445,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<TempCollectionConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ServerConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());
|
||||
|
||||
@@ -701,7 +701,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
str += $" Room #{location.RoomId}";
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
public string LocationToLifestream(LocationInfo location)
|
||||
{
|
||||
if (location.ServerId is 0 || location.TerritoryId is 0 || ContentFinderData.Value.ContainsKey(location.TerritoryId)) return String.Empty;
|
||||
var str = WorldData.Value[(ushort)location.ServerId];
|
||||
if (location.HouseId is 0 && location.MapId is not 0)
|
||||
{
|
||||
var mapName = MapData.Value[(ushort)location.MapId].MapName;
|
||||
var parts = mapName.Split(" - ", StringSplitOptions.RemoveEmptyEntries);
|
||||
var locationName = parts.Length > 0 ? parts[^1] : mapName;
|
||||
str += $", tp {locationName}";
|
||||
string message = $"LocationToLifestream: {str}";
|
||||
_logger.LogInformation(message);
|
||||
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Lifestream.Enums;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.API.Dto.User;
|
||||
@@ -108,6 +109,144 @@ namespace LightlessSync.Services
|
||||
}
|
||||
}
|
||||
|
||||
public LocationInfo? GetLocationForLifestreamByUid(string uid)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
|
||||
{
|
||||
return location;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e,"GetLocationInfoByUid error : ");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public AddressBookEntryTuple? GetAddressBookEntryByLocation(LocationInfo location)
|
||||
{
|
||||
if (location.ServerId is 0 || location.TerritoryId is 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
|
||||
|
||||
if (territoryHousing == TerritoryTypeIdHousing.None || !Enum.IsDefined(typeof(TerritoryTypeIdHousing), territoryHousing))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var city = GetResidentialAetheryteKind(territoryHousing);
|
||||
|
||||
if (city == ResidentialAetheryteKind.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (location.HouseId is not 0 and not 100)
|
||||
{
|
||||
AddressBookEntryTuple addressEntry = (
|
||||
Name: "",
|
||||
World: (int)location.ServerId,
|
||||
City: (int)city,
|
||||
Ward: (int)location.WardId,
|
||||
PropertyType: 0,
|
||||
Plot: (int)location.HouseId,
|
||||
Apartment: 0,
|
||||
ApartmentSubdivision: location.DivisionId == 2,
|
||||
AliasEnabled: false,
|
||||
Alias: ""
|
||||
);
|
||||
return addressEntry;
|
||||
}
|
||||
else if (location.HouseId is 100)
|
||||
{
|
||||
AddressBookEntryTuple addressEntry = (
|
||||
Name: "",
|
||||
World: (int)location.ServerId,
|
||||
City: (int)city,
|
||||
Ward: (int)location.WardId,
|
||||
PropertyType: 1,
|
||||
Plot: 0,
|
||||
Apartment: (int)location.RoomId,
|
||||
ApartmentSubdivision: location.DivisionId == 2,
|
||||
AliasEnabled: false,
|
||||
Alias: ""
|
||||
);
|
||||
return addressEntry;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ResidentialAetheryteKind GetResidentialAetheryteKind(TerritoryTypeIdHousing territoryHousing)
|
||||
{
|
||||
return territoryHousing switch
|
||||
{
|
||||
TerritoryTypeIdHousing.Shirogane or
|
||||
TerritoryTypeIdHousing.ShiroganeApartment or
|
||||
TerritoryTypeIdHousing.ShiroganeSmall or
|
||||
TerritoryTypeIdHousing.ShiroganeMedium or
|
||||
TerritoryTypeIdHousing.ShiroganeLarge or
|
||||
TerritoryTypeIdHousing.ShiroganeFCRoom or
|
||||
TerritoryTypeIdHousing.ShiroganeFCWorkshop
|
||||
=> ResidentialAetheryteKind.Kugane,
|
||||
|
||||
TerritoryTypeIdHousing.Lavender or
|
||||
TerritoryTypeIdHousing.LavenderSmall or
|
||||
TerritoryTypeIdHousing.LavenderMedium or
|
||||
TerritoryTypeIdHousing.LavenderLarge or
|
||||
TerritoryTypeIdHousing.LavenderApartment or
|
||||
TerritoryTypeIdHousing.LavenderFCRoom or
|
||||
TerritoryTypeIdHousing.LavenderFCWorkshop
|
||||
=> ResidentialAetheryteKind.Gridania,
|
||||
|
||||
TerritoryTypeIdHousing.Mist or
|
||||
TerritoryTypeIdHousing.MistSmall or
|
||||
TerritoryTypeIdHousing.MistMedium or
|
||||
TerritoryTypeIdHousing.MistLarge or
|
||||
TerritoryTypeIdHousing.MistApartment or
|
||||
TerritoryTypeIdHousing.MistFCRoom or
|
||||
TerritoryTypeIdHousing.MistFCWorkshop
|
||||
=> ResidentialAetheryteKind.Limsa,
|
||||
|
||||
TerritoryTypeIdHousing.Goblet or
|
||||
TerritoryTypeIdHousing.GobletSmall or
|
||||
TerritoryTypeIdHousing.GobletMedium or
|
||||
TerritoryTypeIdHousing.GobletLarge or
|
||||
TerritoryTypeIdHousing.GobletApartment or
|
||||
TerritoryTypeIdHousing.GobletFCRoom or
|
||||
TerritoryTypeIdHousing.GobletFCWorkshop
|
||||
=> ResidentialAetheryteKind.Uldah,
|
||||
|
||||
TerritoryTypeIdHousing.Empyream or
|
||||
TerritoryTypeIdHousing.EmpyreamSmall or
|
||||
TerritoryTypeIdHousing.EmpyreamMedium or
|
||||
TerritoryTypeIdHousing.EmpyreamLarge or
|
||||
TerritoryTypeIdHousing.EmpyreamApartment or
|
||||
TerritoryTypeIdHousing.EmpyreamFCRoom or
|
||||
TerritoryTypeIdHousing.EmpyreamFCWorkshop
|
||||
=> ResidentialAetheryteKind.Foundation,
|
||||
|
||||
_ => ResidentialAetheryteKind.None
|
||||
};
|
||||
}
|
||||
|
||||
public string? GetMapAddressByLocation(LocationInfo location)
|
||||
{
|
||||
string? liString = null;
|
||||
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
|
||||
if (GetResidentialAetheryteKind(territoryHousing) == ResidentialAetheryteKind.None)
|
||||
{
|
||||
liString = _dalamudUtilService.LocationToLifestream(location);
|
||||
}
|
||||
return liString;
|
||||
}
|
||||
|
||||
public DateTimeOffset GetSharingStatus(string uid)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1343,22 +1343,11 @@ internal static class MdlDecimator
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsBodyMaterial(mdl.Materials[mesh.MaterialIndex]);
|
||||
return ModelDecimationFilters.IsBodyMaterial(mdl.Materials[mesh.MaterialIndex]);
|
||||
}
|
||||
|
||||
private static bool IsBodyMaterial(string materialPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(materialPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = materialPath.Replace('\\', '/').ToLowerInvariant();
|
||||
var nameStart = normalized.LastIndexOf('/');
|
||||
var fileName = nameStart >= 0 ? normalized[(nameStart + 1)..] : normalized;
|
||||
return fileName.Contains("_bibo", StringComparison.Ordinal)
|
||||
|| fileName.EndsWith("_a.mtrl", StringComparison.Ordinal);
|
||||
}
|
||||
=> ModelDecimationFilters.IsBodyMaterial(materialPath);
|
||||
|
||||
private sealed class BodyCollisionData
|
||||
{
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -352,46 +352,40 @@ public sealed class ModelDecimationService
|
||||
return true;
|
||||
}
|
||||
|
||||
var normalized = NormalizeGamePath(gamePath);
|
||||
if (normalized.Contains("/hair/", StringComparison.Ordinal))
|
||||
var normalized = ModelDecimationFilters.NormalizePath(gamePath);
|
||||
if (ModelDecimationFilters.IsHairPath(normalized))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/chara/equipment/", StringComparison.Ordinal))
|
||||
if (ModelDecimationFilters.IsClothingPath(normalized))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowClothing;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/chara/accessory/", StringComparison.Ordinal))
|
||||
if (ModelDecimationFilters.IsAccessoryPath(normalized))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowAccessories;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/chara/human/", StringComparison.Ordinal))
|
||||
if (ModelDecimationFilters.IsBodyPath(normalized))
|
||||
{
|
||||
if (normalized.Contains("/body/", StringComparison.Ordinal))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowBody;
|
||||
}
|
||||
return _performanceConfigService.Current.ModelDecimationAllowBody;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/face/", StringComparison.Ordinal) || normalized.Contains("/head/", StringComparison.Ordinal))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
|
||||
}
|
||||
if (ModelDecimationFilters.IsFaceHeadPath(normalized))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/tail/", StringComparison.Ordinal))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowTail;
|
||||
}
|
||||
if (ModelDecimationFilters.IsTailOrEarPath(normalized))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowTail;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string NormalizeGamePath(string path)
|
||||
=> path.Replace('\\', '/').ToLowerInvariant();
|
||||
|
||||
private bool TryGetDecimationSettings(out ModelDecimationSettings settings)
|
||||
{
|
||||
settings = new ModelDecimationSettings(
|
||||
|
||||
@@ -10,15 +10,18 @@ namespace LightlessSync.Services;
|
||||
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly IpcManager _ipc;
|
||||
private readonly LightlessConfigService _config;
|
||||
private readonly TempCollectionConfigService _config;
|
||||
private readonly CancellationTokenSource _cleanupCts = new();
|
||||
private int _ran;
|
||||
private const int CleanupBatchSize = 50;
|
||||
private static readonly TimeSpan CleanupBatchDelay = TimeSpan.FromMilliseconds(50);
|
||||
private static readonly TimeSpan OrphanCleanupDelay = TimeSpan.FromDays(1);
|
||||
|
||||
public PenumbraTempCollectionJanitor(
|
||||
ILogger<PenumbraTempCollectionJanitor> logger,
|
||||
LightlessMediator mediator,
|
||||
IpcManager ipc,
|
||||
LightlessConfigService config) : base(logger, mediator)
|
||||
TempCollectionConfigService config) : base(logger, mediator)
|
||||
{
|
||||
_ipc = ipc;
|
||||
_config = config;
|
||||
@@ -31,10 +34,6 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
if (id == Guid.Empty) return;
|
||||
var changed = false;
|
||||
var config = _config.Current;
|
||||
if (config.OrphanableTempCollections.Add(id))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var existing = config.OrphanableTempCollectionEntries.FirstOrDefault(entry => entry.Id == id);
|
||||
@@ -63,8 +62,7 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
{
|
||||
if (id == Guid.Empty) return;
|
||||
var config = _config.Current;
|
||||
var changed = config.OrphanableTempCollections.Remove(id);
|
||||
changed |= RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
|
||||
var changed = RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
|
||||
if (changed)
|
||||
{
|
||||
_config.Save();
|
||||
@@ -79,14 +77,31 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
if (!_ipc.Penumbra.APIAvailable)
|
||||
return;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await CleanupOrphansOnBootAsync(_cleanupCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error cleaning orphaned temp collections");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task CleanupOrphansOnBootAsync(CancellationToken token)
|
||||
{
|
||||
var config = _config.Current;
|
||||
var ids = config.OrphanableTempCollections;
|
||||
var entries = config.OrphanableTempCollectionEntries;
|
||||
if (ids.Count == 0 && entries.Count == 0)
|
||||
if (entries.Count == 0)
|
||||
return;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var changed = EnsureEntries(ids, entries, now);
|
||||
var changed = EnsureEntryTimes(entries, now);
|
||||
var cutoff = now - OrphanCleanupDelay;
|
||||
var expired = entries
|
||||
.Where(entry => entry.Id != Guid.Empty && entry.RegisteredAtUtc != DateTime.MinValue && entry.RegisteredAtUtc <= cutoff)
|
||||
@@ -105,25 +120,47 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
var appId = Guid.NewGuid();
|
||||
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections older than {delay}", expired.Count, OrphanCleanupDelay);
|
||||
|
||||
List<Guid> removedIds = [];
|
||||
foreach (var id in expired)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id)
|
||||
.GetAwaiter().GetResult();
|
||||
await _ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed removing orphaned temp collection {id}", id);
|
||||
}
|
||||
|
||||
removedIds.Add(id);
|
||||
if (removedIds.Count % CleanupBatchSize == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(CleanupBatchDelay, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var id in expired)
|
||||
if (removedIds.Count == 0)
|
||||
{
|
||||
ids.Remove(id);
|
||||
if (changed)
|
||||
{
|
||||
_config.Save();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var id in expired)
|
||||
foreach (var id in removedIds)
|
||||
{
|
||||
RemoveEntry(entries, id);
|
||||
}
|
||||
@@ -131,6 +168,17 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_cleanupCts.Cancel();
|
||||
_cleanupCts.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private static int RemoveEntry(List<OrphanableTempCollectionEntry> entries, Guid id)
|
||||
{
|
||||
var removed = 0;
|
||||
@@ -148,29 +196,9 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
return removed;
|
||||
}
|
||||
|
||||
private static bool EnsureEntries(HashSet<Guid> ids, List<OrphanableTempCollectionEntry> entries, DateTime now)
|
||||
private static bool EnsureEntryTimes(List<OrphanableTempCollectionEntry> entries, DateTime now)
|
||||
{
|
||||
var changed = false;
|
||||
foreach (var id in ids)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entries.Any(entry => entry.Id == id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.Add(new OrphanableTempCollectionEntry
|
||||
{
|
||||
Id = id,
|
||||
RegisteredAtUtc = now
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry.Id == Guid.Empty || entry.RegisteredAtUtc != DateTime.MinValue)
|
||||
|
||||
@@ -4,8 +4,10 @@ using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
@@ -40,6 +42,7 @@ public class DrawUserPair
|
||||
private readonly LocationShareService _locationShareService;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly IpcCallerLifestream _lifestreamIpc;
|
||||
private float _menuWidth = -1;
|
||||
private bool _wasHovered = false;
|
||||
private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty;
|
||||
@@ -60,7 +63,8 @@ public class DrawUserPair
|
||||
LightlessConfigService configService,
|
||||
LocationShareService locationShareService,
|
||||
CharaDataManager charaDataManager,
|
||||
PairLedger pairLedger)
|
||||
PairLedger pairLedger,
|
||||
IpcCallerLifestream lifestreamIpc)
|
||||
{
|
||||
_id = id;
|
||||
_uiEntry = uiEntry;
|
||||
@@ -79,6 +83,7 @@ public class DrawUserPair
|
||||
_locationShareService = locationShareService;
|
||||
_charaDataManager = charaDataManager;
|
||||
_pairLedger = pairLedger;
|
||||
_lifestreamIpc = lifestreamIpc;
|
||||
}
|
||||
|
||||
public PairDisplayEntry DisplayEntry => _displayEntry;
|
||||
@@ -656,6 +661,13 @@ public class DrawUserPair
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther))
|
||||
_uiSharedService.IconText(shareLocationIcon);
|
||||
|
||||
var popupId = $"LocationPopup_{_pair.UserData.UID}";
|
||||
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && shareLocation && !string.IsNullOrEmpty(location))
|
||||
{
|
||||
ImGui.OpenPopup(popupId);
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
@@ -669,6 +681,8 @@ public class DrawUserPair
|
||||
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(location);
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted("Click to teleport to this location");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -700,6 +714,62 @@ public class DrawUserPair
|
||||
}
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
if (ImGui.BeginPopup(popupId))
|
||||
{
|
||||
|
||||
var locationInfo = _locationShareService.GetLocationForLifestreamByUid(_pair.UserData.UID);
|
||||
if (locationInfo != null)
|
||||
{
|
||||
var locationLi = locationInfo.Value;
|
||||
var housingAddress = _locationShareService.GetAddressBookEntryByLocation(locationLi);
|
||||
var mapAddress = _locationShareService.GetMapAddressByLocation(locationLi);
|
||||
ImGui.TextUnformatted("Teleport to user?");
|
||||
ImGui.Separator();
|
||||
if (!_lifestreamIpc.APIAvailable)
|
||||
{
|
||||
ImGui.TextUnformatted("Lifestream IPC is not available. Please ensure Lifestream is enabled");
|
||||
}
|
||||
else if (housingAddress != null || mapAddress != null)
|
||||
{
|
||||
ImGui.TextUnformatted($"Go to {location}?");
|
||||
ImGui.TextUnformatted($"NOTE: Teleporting to maps with multiple aetherytes or instances may not be accurate currently. (ie. Thavnair, Yanxia)");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted("Lifestream cannot teleport here. If you are in a residential area, please make sure you're inside a plot.");
|
||||
}
|
||||
ImGui.Separator();
|
||||
if (_lifestreamIpc.APIAvailable && (housingAddress != null || mapAddress != null))
|
||||
{
|
||||
if (locationLi.HouseId is not 0 && housingAddress != null)
|
||||
{
|
||||
if (ImGui.Button("Navigate"))
|
||||
{
|
||||
_lifestreamIpc.GoToHousingAddress(housingAddress.Value);
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
else if (mapAddress != null && locationLi.HouseId is 0)
|
||||
{
|
||||
if (ImGui.Button("Navigate"))
|
||||
{
|
||||
_lifestreamIpc.ExecuteLifestreamCommand(mapAddress);
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
}
|
||||
if (ImGui.Button("Close"))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)
|
||||
|
||||
@@ -16,6 +16,7 @@ using LightlessSync.UI.Handlers;
|
||||
using LightlessSync.UI.Models;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
@@ -40,6 +41,7 @@ public class DrawEntityFactory
|
||||
private readonly IdDisplayHandler _uidDisplayHandler;
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly PairFactory _pairFactory;
|
||||
private readonly IpcCallerLifestream _lifestreamIpc;
|
||||
|
||||
public DrawEntityFactory(
|
||||
ILogger<DrawEntityFactory> logger,
|
||||
@@ -60,7 +62,8 @@ public class DrawEntityFactory
|
||||
RenameSyncshellTagUi renameSyncshellTagUi,
|
||||
SelectSyncshellForTagUi selectSyncshellForTagUi,
|
||||
PairLedger pairLedger,
|
||||
PairFactory pairFactory)
|
||||
PairFactory pairFactory,
|
||||
IpcCallerLifestream lifestreamIpc)
|
||||
{
|
||||
_logger = logger;
|
||||
_apiController = apiController;
|
||||
@@ -81,6 +84,7 @@ public class DrawEntityFactory
|
||||
_selectSyncshellForTagUi = selectSyncshellForTagUi;
|
||||
_pairLedger = pairLedger;
|
||||
_pairFactory = pairFactory;
|
||||
_lifestreamIpc = lifestreamIpc;
|
||||
}
|
||||
|
||||
public DrawFolderGroup CreateGroupFolder(
|
||||
@@ -167,7 +171,8 @@ public class DrawEntityFactory
|
||||
_configService,
|
||||
_locationShareService,
|
||||
_charaDataManager,
|
||||
_pairLedger);
|
||||
_pairLedger,
|
||||
_lifestreamIpc);
|
||||
}
|
||||
|
||||
public IReadOnlyList<PairUiEntry> GetAllEntries()
|
||||
|
||||
71
LightlessSync/Utils/PtrGuard.cs
Normal file
71
LightlessSync/Utils/PtrGuard.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using static LightlessSync.Utils.PtrGuardMemory;
|
||||
|
||||
namespace LightlessSync.Utils
|
||||
{
|
||||
public static partial class PtrGuard
|
||||
{
|
||||
private const ulong _aligmentPtr = 0x7UL;
|
||||
private static readonly nuint _minAppAddr = (nuint)GetMinAppAddr();
|
||||
private static readonly nuint _maxAppAddr = (nuint)GetMaxAppAddr();
|
||||
|
||||
private static nint GetMinAppAddr()
|
||||
{
|
||||
GetSystemInfo(out var si);
|
||||
return si.lpMinimumApplicationAddress;
|
||||
}
|
||||
|
||||
private static nint GetMaxAppAddr()
|
||||
{
|
||||
GetSystemInfo(out var si);
|
||||
return si.lpMaximumApplicationAddress;
|
||||
}
|
||||
|
||||
public static bool LooksLikePtr(nint p)
|
||||
{
|
||||
if (p == 0) return false;
|
||||
nuint u = (nuint)p;
|
||||
|
||||
if (u < _minAppAddr) return false;
|
||||
if (u > _maxAppAddr) return false;
|
||||
if ((u & _aligmentPtr) != 0) return false;
|
||||
if ((uint)u == 0x12345679u) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryReadIntPtr(nint addr, out nint value)
|
||||
{
|
||||
value = 0;
|
||||
|
||||
if (!LooksLikePtr(addr))
|
||||
return false;
|
||||
|
||||
return ReadProcessMemory(GetCurrentProcess(), addr, out value, (nuint)IntPtr.Size, out nuint bytesRead)
|
||||
&& bytesRead == (nuint)IntPtr.Size;
|
||||
}
|
||||
|
||||
public static bool IsReadable(nint addr, nuint size)
|
||||
{
|
||||
if (addr == 0 || size == 0) return false;
|
||||
|
||||
if (VirtualQuery(addr, out var mbi, (nuint)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == 0)
|
||||
return false;
|
||||
|
||||
const uint Commit = 0x1000;
|
||||
const uint NoAccess = 0x01;
|
||||
const uint PageGuard = 0x100;
|
||||
|
||||
if (mbi.State != Commit) return false;
|
||||
if ((mbi.Protect & PageGuard) != 0) return false;
|
||||
if (mbi.Protect == NoAccess) return false;
|
||||
|
||||
ulong start = (ulong)addr;
|
||||
ulong end = start + size - 1;
|
||||
ulong r0 = (ulong)mbi.BaseAddress;
|
||||
ulong r1 = r0 + mbi.RegionSize - 1;
|
||||
|
||||
return start >= r0 && end <= r1;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
LightlessSync/Utils/PtrGuardMemory.cs
Normal file
55
LightlessSync/Utils/PtrGuardMemory.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LightlessSync.Utils
|
||||
{
|
||||
internal static class PtrGuardMemory
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct MEMORY_BASIC_INFORMATION
|
||||
{
|
||||
public nint BaseAddress;
|
||||
public nint AllocationBase;
|
||||
public uint AllocationProtect;
|
||||
public nuint RegionSize;
|
||||
public uint State;
|
||||
public uint Protect;
|
||||
public uint Type;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
internal static extern nuint VirtualQuery(
|
||||
nint lpAddress,
|
||||
out MEMORY_BASIC_INFORMATION lpBuffer,
|
||||
nuint dwLength);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
internal static extern bool ReadProcessMemory(
|
||||
nint hProcess,
|
||||
nint lpBaseAddress,
|
||||
out nint lpBuffer,
|
||||
nuint nSize,
|
||||
out nuint lpNumberOfBytesRead);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
internal static extern nint GetCurrentProcess();
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
internal static extern void GetSystemInfo(out SYSTEM_INFO lpSystemInfo);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct SYSTEM_INFO
|
||||
{
|
||||
public ushort wProcessorArchitecture;
|
||||
public ushort wReserved;
|
||||
public uint dwPageSize;
|
||||
public nint lpMinimumApplicationAddress;
|
||||
public nint lpMaximumApplicationAddress;
|
||||
public nint dwActiveProcessorMask;
|
||||
public uint dwNumberOfProcessors;
|
||||
public uint dwProcessorType;
|
||||
public uint dwAllocationGranularity;
|
||||
public ushort wProcessorLevel;
|
||||
public ushort wProcessorRevision;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user