Compare commits

...

46 Commits

Author SHA1 Message Date
defnotken
59b2776943 syncing dev and stable
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m17s
2026-01-20 13:28:38 -06:00
defnotken
b3c35dbfdc Changelogs 2026-01-20 13:22:34 -06:00
defnotken
84a3293f6b eighty three
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m15s
2026-01-20 10:33:21 -06:00
defnotken
28db3d6fd2 Merge branch '2.0.3' into dev 2026-01-20 10:32:54 -06:00
d83ca98008 Merge pull request 'updated ptrguard to use system_info for minimum calculations' (#144) from clr-fix-attempt into 2.0.3
Reviewed-on: #144
2026-01-20 16:09:57 +00:00
e6df37bcca Merge pull request 'slight-tweaks' (#143) from slight-tweaks into 2.0.3
Reviewed-on: #143
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
2026-01-20 15:49:04 +00:00
cake
995e11371a updated ptrguard to use system_info for minimum calculations 2026-01-20 16:42:31 +01:00
defnotken
abc324bf4f bumpers
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m23s
2026-01-19 19:48:39 -06:00
defnotken
eee0e072bd Merge branch '2.0.3' into dev 2026-01-19 19:48:28 -06:00
cake
d8335eb04f Removal of log 2026-01-20 02:44:50 +01:00
cake
994335c6b0 Moved to trace log 2026-01-20 02:40:30 +01:00
defnotken
172288c755 bumpers
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 1m57s
2026-01-19 18:31:36 -06:00
defnotken
1c17be53d0 Merge branch '2.0.3' into dev 2026-01-19 18:30:42 -06:00
68b4863f52 Merge pull request 'Added CLR fix for gameobject handler' (#142) from clr-fix-attempt into 2.0.3
Reviewed-on: #142
2026-01-20 00:02:58 +00:00
cake
22fe9901a4 Fixed some issues. 2026-01-20 00:25:26 +01:00
cake
cff866dcc2 Added ptrguard to be used whenever ptrs are being used. 2026-01-20 00:24:42 +01:00
defnotken
e8f598e695 bumpity bump
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m7s
2026-01-19 12:36:24 -06:00
defnotken
861a337029 Merge branch '2.0.3' into dev
Some checks failed
Tag and Release Lightless / tag-and-release (push) Has been cancelled
2026-01-19 12:35:29 -06:00
06f89955d3 Merge pull request 'clr-fix-attempt' (#141) from clr-fix-attempt into 2.0.3
Reviewed-on: #141
2026-01-19 18:33:55 +00:00
cake
367af2c3d0 Remove blankspace 2026-01-19 19:26:00 +01:00
cake
19a238c808 Removal of unsafe and check if PTR is correctly aligning, checking in virtual query instead of memory 2026-01-19 19:24:44 +01:00
defnotken
c7a2b679f2 bumpity bump
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m2s
2026-01-19 11:23:49 -06:00
defnotken
bec69074a5 Merge branch '2.0.3' into dev 2026-01-19 11:23:23 -06:00
7d86b41cee Merge pull request 'update-decimation-filters' (#140) from update-decimation-filters into 2.0.3
Reviewed-on: #140
2026-01-19 17:22:27 +00:00
0185e6b534 Merge pull request 'Implement Lifestream With Location Sharing.' (#139) from lifestream-location-share into 2.0.3
Reviewed-on: #139
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
2026-01-19 17:02:13 +00:00
defnotken
90bf84f8eb Implement Lifestream With Location Sharing. 2026-01-19 10:58:37 -06:00
f27db300ec fix temp collection config and migrate it 2026-01-19 23:04:54 +09:00
828be6eb5b fix body materials 2026-01-19 21:20:54 +09:00
d039d2fd90 add emperors oops 2026-01-19 14:19:08 +09:00
e75a371475 improve filtering 2026-01-19 14:14:14 +09:00
defnotken
ac711d9a43 Bump plugin testing
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m19s
2026-01-18 20:53:14 -06:00
defnotken
b875e0c3a1 Merge branch '2.0.3' into dev 2026-01-18 20:51:58 -06:00
defnotken
46e76bbfe6 Merge branch '2.0.3' into dev 2026-01-18 20:29:53 -06:00
cake
3654365f2a bump version
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m6s
2026-01-06 14:45:23 +01:00
cake
9b256dd185 Merge branch '2.0.3' into dev 2026-01-06 14:45:02 +01:00
defnotken
223ade39cb another push
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m13s
2026-01-05 20:48:24 -06:00
defnotken
5aca9e70b2 Merge branch '2.0.3' into dev 2026-01-05 20:47:38 -06:00
defnotken
92772cf334 dev push
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m12s
2026-01-05 20:21:26 -06:00
defnotken
0395e81a9f Merge branch '2.0.3' into dev 2026-01-05 20:17:12 -06:00
defnotken
7734a7bf7e dev build
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m8s
2026-01-05 17:42:21 -06:00
defnotken
db2d19bb1e Merge branch '2.0.3' into dev 2026-01-05 17:41:48 -06:00
defnotken
ab305a249c more checks
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m7s
2026-01-05 15:48:54 -06:00
defnotken
9d104a9dd8 Merge branch '2.0.3' into dev 2026-01-05 15:42:15 -06:00
defnotken
bcd3bd5ca2 add more checks
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m8s
2026-01-05 15:08:26 -06:00
defnotken
c1829a9837 Merge branch '2.0.3' into dev
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m14s
2026-01-05 14:48:47 -06:00
defnotken
cca23f6e05 Building Dev
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s
2026-01-05 10:50:25 -06:00
22 changed files with 1142 additions and 192 deletions

View File

@@ -1,11 +1,86 @@
tagline: "Lightless Sync v2.0.1" tagline: "Lightless Sync v2.1.0"
subline: "LIGHTLESS IS EVOLVING!!" subline: "Location Sharing, Animation Scanning, Sync Improvements, Lifestream IPC, and More!"
changelog: changelog:
- name: "v2.1.0"
tagline: "Happy New Year!"
date: "January 20 2026"
# be sure to set this every new version
isCurrent: true
versions:
- number: "Location Sharing (S/O to Tsubasa from the MareCN team for this feature!)"
icon: ""
items:
- "Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] or until you turn it off for them. That's up to you!"
- "To share your location with a pair, click the three dots beside the pair and choose a duration to share with them."
- "To view the location of someone who's shared with you, simply hover over the globe icon!"
- number: "Model Optimization (Mesh Decimating)"
icon: ""
items:
- "This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). "
- "Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for."
- "Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking."
- "You can find everything under Settings → Performance → Model Optimization."
- "IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE!"
- number: "Animation (PAP) Validation (Safer animations)"
icon: ""
items:
- "Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, its included in what gets sent to other players. If it doesnt, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users."
- "Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with."
- "Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives."
- number: "UI Changes (Thanks to Choco for UI Changes)"
icon: ""
items:
- "The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless."
- "Settings have gotten some changes as well to make this change more universal, and will use the same color settings."
- "The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior"
- "Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it"
- number: "LightFinder / ShellFinder"
icon: ""
items:
- "UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does."
- "A list of players using LightFinder near you is now available."
- "Improved the look of ShellFinder; a new pop-up for the join screen is included. This will be merged into more spots later."
- number: "Syncing Changes"
icon: ""
items:
- "Fixed a bottleneck that caused hitching for some users."
- "Fixed a bug where files in the downscale directory were not being deleted correctly."
- "Improved performance tracking by tracking both original and effective triangle counts."
- "Improved owned object detection."
- "Reverted collection work to old way to mitigate the syncing and VFX issues."
- "Fixed minion/mount/pet tracking to validate against player-related."
- "Added timeout support to Wait For Fully Loaded."
- "Enhanced pair handling to support model decimation. (Effective Triangles)."
- "Improved downloads through atomization."
- "Improved data management by only pushing to online pairs."
- number: "Syncshell Admin Panel changes"
icon: ""
items:
- "Changes to banned userlist to allow for preemptive banning of users."
- "Fixed when you are the owner, the buttons aren't overflowing into the side."
- "Pruning has been fixed not to include online users. It will only focus on offline users now."
- "Fixed that moderators can finally broadcast the syncshell."
- number: "Bug Fixes"
icon: ""
items:
- "Fixed a bug in the client CI/CD workflow where the plugin was contained twice in the zip. This will cut down on overall plugin size."
- "Added new flags for certain ImGUI windows to fix focusing issues with Chat2."
- "Validating PTRs for the game objects to prevent crashing while switching characters."
- number: "Lifestream Plugin integration"
icon: ""
items:
- "We have added Lifestream as an optional plugin you can install to support multiple future implementations >:3. In the near future, you'll be able to go straight to your pair using the Lifestream plugin in as easy as one click. We are also cooking something up for now; it will simply show as an optional plugin."
- "Integrated Lifestream to Location Sharing to easily teleport to all plots/apartments, and most maps."
- number: "Q1 Roadmap"
icon: ""
items:
- "Diagnostics Window - Est. Feb"
- "Ability to set a location as a 'Home' in syncshells - Est. Mid-Late Feb"
- "Lifestream support for Syncshell Homes - Est. Mid-Late Feb"
- "Community-oriented feature - In Refinement (est. Mar/Apr)"
- name: "v2.0.2" - name: "v2.0.2"
tagline: "Last update of 2025!... ... ... If Nothing breaks" tagline: "Last update of 2025!... ... ... If Nothing breaks"
date: "December 28 2025" date: "December 28 2025"
# be sure to set this every new version
isCurrent: true
versions: versions:
- number: "Chat" - number: "Chat"
icon: "" icon: ""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -431,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));
@@ -444,6 +445,7 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>()); services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>()); services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>()); services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<TempCollectionConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ServerConfigService>()); services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ServerConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>()); services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>());
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>()); services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());

View File

@@ -701,7 +701,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
str += $" Room #{location.RoomId}"; str += $" Room #{location.RoomId}";
} }
} }
return str;
}
public string LocationToLifestream(LocationInfo location)
{
if (location.ServerId is 0 || location.TerritoryId is 0 || ContentFinderData.Value.ContainsKey(location.TerritoryId)) return String.Empty;
var str = WorldData.Value[(ushort)location.ServerId];
if (location.HouseId is 0 && location.MapId is not 0)
{
var mapName = MapData.Value[(ushort)location.MapId].MapName;
var parts = mapName.Split(" - ", StringSplitOptions.RemoveEmptyEntries);
var locationName = parts.Length > 0 ? parts[^1] : mapName;
str += $", tp {locationName}";
string message = $"LocationToLifestream: {str}";
_logger.LogInformation(message);
}
return str; return str;
} }

View File

@@ -1,3 +1,4 @@
using Lifestream.Enums;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Dto.CharaData; using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
@@ -108,6 +109,144 @@ namespace LightlessSync.Services
} }
} }
public LocationInfo? GetLocationForLifestreamByUid(string uid)
{
try
{
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
{
return location;
}
return null;
}
catch (Exception e)
{
Logger.LogError(e,"GetLocationInfoByUid error : ");
throw;
}
}
public AddressBookEntryTuple? GetAddressBookEntryByLocation(LocationInfo location)
{
if (location.ServerId is 0 || location.TerritoryId is 0)
{
return null;
}
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
if (territoryHousing == TerritoryTypeIdHousing.None || !Enum.IsDefined(typeof(TerritoryTypeIdHousing), territoryHousing))
{
return null;
}
var city = GetResidentialAetheryteKind(territoryHousing);
if (city == ResidentialAetheryteKind.None)
{
return null;
}
if (location.HouseId is not 0 and not 100)
{
AddressBookEntryTuple addressEntry = (
Name: "",
World: (int)location.ServerId,
City: (int)city,
Ward: (int)location.WardId,
PropertyType: 0,
Plot: (int)location.HouseId,
Apartment: 0,
ApartmentSubdivision: location.DivisionId == 2,
AliasEnabled: false,
Alias: ""
);
return addressEntry;
}
else if (location.HouseId is 100)
{
AddressBookEntryTuple addressEntry = (
Name: "",
World: (int)location.ServerId,
City: (int)city,
Ward: (int)location.WardId,
PropertyType: 1,
Plot: 0,
Apartment: (int)location.RoomId,
ApartmentSubdivision: location.DivisionId == 2,
AliasEnabled: false,
Alias: ""
);
return addressEntry;
}
return null;
}
private ResidentialAetheryteKind GetResidentialAetheryteKind(TerritoryTypeIdHousing territoryHousing)
{
return territoryHousing switch
{
TerritoryTypeIdHousing.Shirogane or
TerritoryTypeIdHousing.ShiroganeApartment or
TerritoryTypeIdHousing.ShiroganeSmall or
TerritoryTypeIdHousing.ShiroganeMedium or
TerritoryTypeIdHousing.ShiroganeLarge or
TerritoryTypeIdHousing.ShiroganeFCRoom or
TerritoryTypeIdHousing.ShiroganeFCWorkshop
=> ResidentialAetheryteKind.Kugane,
TerritoryTypeIdHousing.Lavender or
TerritoryTypeIdHousing.LavenderSmall or
TerritoryTypeIdHousing.LavenderMedium or
TerritoryTypeIdHousing.LavenderLarge or
TerritoryTypeIdHousing.LavenderApartment or
TerritoryTypeIdHousing.LavenderFCRoom or
TerritoryTypeIdHousing.LavenderFCWorkshop
=> ResidentialAetheryteKind.Gridania,
TerritoryTypeIdHousing.Mist or
TerritoryTypeIdHousing.MistSmall or
TerritoryTypeIdHousing.MistMedium or
TerritoryTypeIdHousing.MistLarge or
TerritoryTypeIdHousing.MistApartment or
TerritoryTypeIdHousing.MistFCRoom or
TerritoryTypeIdHousing.MistFCWorkshop
=> ResidentialAetheryteKind.Limsa,
TerritoryTypeIdHousing.Goblet or
TerritoryTypeIdHousing.GobletSmall or
TerritoryTypeIdHousing.GobletMedium or
TerritoryTypeIdHousing.GobletLarge or
TerritoryTypeIdHousing.GobletApartment or
TerritoryTypeIdHousing.GobletFCRoom or
TerritoryTypeIdHousing.GobletFCWorkshop
=> ResidentialAetheryteKind.Uldah,
TerritoryTypeIdHousing.Empyream or
TerritoryTypeIdHousing.EmpyreamSmall or
TerritoryTypeIdHousing.EmpyreamMedium or
TerritoryTypeIdHousing.EmpyreamLarge or
TerritoryTypeIdHousing.EmpyreamApartment or
TerritoryTypeIdHousing.EmpyreamFCRoom or
TerritoryTypeIdHousing.EmpyreamFCWorkshop
=> ResidentialAetheryteKind.Foundation,
_ => ResidentialAetheryteKind.None
};
}
public string? GetMapAddressByLocation(LocationInfo location)
{
string? liString = null;
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
if (GetResidentialAetheryteKind(territoryHousing) == ResidentialAetheryteKind.None)
{
liString = _dalamudUtilService.LocationToLifestream(location);
}
return liString;
}
public DateTimeOffset GetSharingStatus(string uid) public DateTimeOffset GetSharingStatus(string uid)
{ {
try try

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,8 +4,10 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs; using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services; using LightlessSync.Services;
@@ -40,6 +42,7 @@ public class DrawUserPair
private readonly LocationShareService _locationShareService; private readonly LocationShareService _locationShareService;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly PairLedger _pairLedger; private readonly PairLedger _pairLedger;
private readonly IpcCallerLifestream _lifestreamIpc;
private float _menuWidth = -1; private float _menuWidth = -1;
private bool _wasHovered = false; private bool _wasHovered = false;
private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty; private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty;
@@ -60,7 +63,8 @@ public class DrawUserPair
LightlessConfigService configService, LightlessConfigService configService,
LocationShareService locationShareService, LocationShareService locationShareService,
CharaDataManager charaDataManager, CharaDataManager charaDataManager,
PairLedger pairLedger) PairLedger pairLedger,
IpcCallerLifestream lifestreamIpc)
{ {
_id = id; _id = id;
_uiEntry = uiEntry; _uiEntry = uiEntry;
@@ -79,6 +83,7 @@ public class DrawUserPair
_locationShareService = locationShareService; _locationShareService = locationShareService;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_pairLedger = pairLedger; _pairLedger = pairLedger;
_lifestreamIpc = lifestreamIpc;
} }
public PairDisplayEntry DisplayEntry => _displayEntry; public PairDisplayEntry DisplayEntry => _displayEntry;
@@ -656,6 +661,13 @@ public class DrawUserPair
using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther)) using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther))
_uiSharedService.IconText(shareLocationIcon); _uiSharedService.IconText(shareLocationIcon);
var popupId = $"LocationPopup_{_pair.UserData.UID}";
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && shareLocation && !string.IsNullOrEmpty(location))
{
ImGui.OpenPopup(popupId);
}
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
{ {
ImGui.BeginTooltip(); ImGui.BeginTooltip();
@@ -669,6 +681,8 @@ public class DrawUserPair
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow); _uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted(location); ImGui.TextUnformatted(location);
ImGui.Separator();
ImGui.TextUnformatted("Click to teleport to this location");
} }
else else
{ {
@@ -700,6 +714,62 @@ public class DrawUserPair
} }
ImGui.EndTooltip(); ImGui.EndTooltip();
} }
if (ImGui.BeginPopup(popupId))
{
var locationInfo = _locationShareService.GetLocationForLifestreamByUid(_pair.UserData.UID);
if (locationInfo != null)
{
var locationLi = locationInfo.Value;
var housingAddress = _locationShareService.GetAddressBookEntryByLocation(locationLi);
var mapAddress = _locationShareService.GetMapAddressByLocation(locationLi);
ImGui.TextUnformatted("Teleport to user?");
ImGui.Separator();
if (!_lifestreamIpc.APIAvailable)
{
ImGui.TextUnformatted("Lifestream IPC is not available. Please ensure Lifestream is enabled");
}
else if (housingAddress != null || mapAddress != null)
{
ImGui.TextUnformatted($"Go to {location}?");
ImGui.TextUnformatted($"NOTE: Teleporting to maps with multiple aetherytes or instances may not be accurate currently. (ie. Thavnair, Yanxia)");
}
else
{
ImGui.TextUnformatted("Lifestream cannot teleport here. If you are in a residential area, please make sure you're inside a plot.");
}
ImGui.Separator();
if (_lifestreamIpc.APIAvailable && (housingAddress != null || mapAddress != null))
{
if (locationLi.HouseId is not 0 && housingAddress != null)
{
if (ImGui.Button("Navigate"))
{
_lifestreamIpc.GoToHousingAddress(housingAddress.Value);
ImGui.CloseCurrentPopup();
}
}
else if (mapAddress != null && locationLi.HouseId is 0)
{
if (ImGui.Button("Navigate"))
{
_lifestreamIpc.ExecuteLifestreamCommand(mapAddress);
ImGui.CloseCurrentPopup();
}
}
ImGui.SameLine();
}
if (ImGui.Button("Close"))
{
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
}
} }
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky) if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)

View File

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

View File

@@ -0,0 +1,71 @@
using System.Runtime.InteropServices;
using static LightlessSync.Utils.PtrGuardMemory;
namespace LightlessSync.Utils
{
public static partial class PtrGuard
{
private const ulong _aligmentPtr = 0x7UL;
private static readonly nuint _minAppAddr = (nuint)GetMinAppAddr();
private static readonly nuint _maxAppAddr = (nuint)GetMaxAppAddr();
private static nint GetMinAppAddr()
{
GetSystemInfo(out var si);
return si.lpMinimumApplicationAddress;
}
private static nint GetMaxAppAddr()
{
GetSystemInfo(out var si);
return si.lpMaximumApplicationAddress;
}
public static bool LooksLikePtr(nint p)
{
if (p == 0) return false;
nuint u = (nuint)p;
if (u < _minAppAddr) return false;
if (u > _maxAppAddr) return false;
if ((u & _aligmentPtr) != 0) return false;
if ((uint)u == 0x12345679u) return false;
return true;
}
public static bool TryReadIntPtr(nint addr, out nint value)
{
value = 0;
if (!LooksLikePtr(addr))
return false;
return ReadProcessMemory(GetCurrentProcess(), addr, out value, (nuint)IntPtr.Size, out nuint bytesRead)
&& bytesRead == (nuint)IntPtr.Size;
}
public static bool IsReadable(nint addr, nuint size)
{
if (addr == 0 || size == 0) return false;
if (VirtualQuery(addr, out var mbi, (nuint)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == 0)
return false;
const uint Commit = 0x1000;
const uint NoAccess = 0x01;
const uint PageGuard = 0x100;
if (mbi.State != Commit) return false;
if ((mbi.Protect & PageGuard) != 0) return false;
if (mbi.Protect == NoAccess) return false;
ulong start = (ulong)addr;
ulong end = start + size - 1;
ulong r0 = (ulong)mbi.BaseAddress;
ulong r1 = r0 + mbi.RegionSize - 1;
return start >= r0 && end <= r1;
}
}
}

View File

@@ -0,0 +1,55 @@
using System.Runtime.InteropServices;
namespace LightlessSync.Utils
{
internal static class PtrGuardMemory
{
[StructLayout(LayoutKind.Sequential)]
internal struct MEMORY_BASIC_INFORMATION
{
public nint BaseAddress;
public nint AllocationBase;
public uint AllocationProtect;
public nuint RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern nuint VirtualQuery(
nint lpAddress,
out MEMORY_BASIC_INFORMATION lpBuffer,
nuint dwLength);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool ReadProcessMemory(
nint hProcess,
nint lpBaseAddress,
out nint lpBuffer,
nuint nSize,
out nuint lpNumberOfBytesRead);
[DllImport("kernel32.dll")]
internal static extern nint GetCurrentProcess();
[DllImport("kernel32.dll")]
internal static extern void GetSystemInfo(out SYSTEM_INFO lpSystemInfo);
[StructLayout(LayoutKind.Sequential)]
internal struct SYSTEM_INFO
{
public ushort wProcessorArchitecture;
public ushort wReserved;
public uint dwPageSize;
public nint lpMinimumApplicationAddress;
public nint lpMaximumApplicationAddress;
public nint dwActiveProcessorMask;
public uint dwNumberOfProcessors;
public uint dwProcessorType;
public uint dwAllocationGranularity;
public ushort wProcessorLevel;
public ushort wProcessorRevision;
}
}
}