2.1.0 (#123)
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s

# Patchnotes 2.1.0
The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update.

We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which:

# Location Sharing (Big shout out to @tsubasahane for bringing this feature)

- 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] minutes or until you turn it off for them. That's up to you! [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)
- To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)
- To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)

[1]

# Model Optimization (Mesh Decimating)
 - 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%). [#131](<#131>)
 - 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. [#131](<#131>)
 - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking.
 - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>)
+ ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE  **

[2]

# Animation (PAP) Validation (Safer animations)
 - 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. [#131](<#131>)
 - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>)
 - 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. [#131](<#131>)

# UI Changes (Thanks to @kyuwu for UI Changes)
- 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. [#127](<#127>)

[3]

- Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>)
- The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>)
- Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>)

# LightFinder / ShellFinder
- UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does.  [#127](<#127>)

[4]

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
Co-authored-by: choco <choco@patat.nl>
Co-authored-by: celine <aaa@aaa.aaa>
Co-authored-by: celine <celine@noreply.git.lightless-sync.org>
Co-authored-by: Tsubasahane <wozaiha@gmail.com>
Co-authored-by: cake <cake@noreply.git.lightless-sync.org>
Reviewed-on: #123
This commit was merged in pull request #123.
This commit is contained in:
2026-01-20 19:43:00 +00:00
parent ed7932ab83
commit 72a62b7449
178 changed files with 33356 additions and 4568 deletions

View File

@@ -4,8 +4,10 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
@@ -37,8 +39,10 @@ public class DrawUserPair
private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _performanceConfigService;
private readonly LightlessConfigService _configService;
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;
@@ -57,8 +61,10 @@ public class DrawUserPair
UiSharedService uiSharedService,
PlayerPerformanceConfigService performanceConfigService,
LightlessConfigService configService,
LocationShareService locationShareService,
CharaDataManager charaDataManager,
PairLedger pairLedger)
PairLedger pairLedger,
IpcCallerLifestream lifestreamIpc)
{
_id = id;
_uiEntry = uiEntry;
@@ -74,8 +80,10 @@ public class DrawUserPair
_uiSharedService = uiSharedService;
_performanceConfigService = performanceConfigService;
_configService = configService;
_locationShareService = locationShareService;
_charaDataManager = charaDataManager;
_pairLedger = pairLedger;
_lifestreamIpc = lifestreamIpc;
}
public PairDisplayEntry DisplayEntry => _displayEntry;
@@ -216,6 +224,48 @@ public class DrawUserPair
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
}
UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty));
ImGui.SetCursorPosX(10f);
_uiSharedService.IconText(FontAwesomeIcon.Globe);
ImGui.SameLine();
if (ImGui.BeginMenu("Toggle Location sharing"))
{
if (ImGui.MenuItem("Share for 30 Mins"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddMinutes(30));
}
if (ImGui.MenuItem("Share for 1 Hour"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(1));
}
if (ImGui.MenuItem("Share for 3 Hours"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(3));
}
if (ImGui.MenuItem("Share until manually stop"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MaxValue);
}
ImGui.Separator();
if (ImGui.MenuItem("Stop Sharing"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MinValue);
}
ImGui.EndMenu();
}
}
private async Task ToggleLocationSharing(List<string> users, DateTimeOffset expireAt)
{
var updated = await _apiController.ToggleLocationSharing(new LocationSharingToggleDto(users, expireAt)).ConfigureAwait(false);
if (updated)
{
_locationShareService.UpdateSharingStatus(users, expireAt);
}
}
private void DrawIndividualMenu()
@@ -295,7 +345,10 @@ public class DrawUserPair
? FontAwesomeIcon.User : FontAwesomeIcon.Users);
}
UiSharedService.AttachToolTip(GetUserTooltip());
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
UiSharedService.AttachToolTip(GetUserTooltip());
}
if (_performanceConfigService.Current.ShowPerformanceIndicator
&& !_performanceConfigService.Current.UIDsToIgnore
@@ -309,22 +362,25 @@ public class DrawUserPair
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
string userWarningText = "WARNING: This user exceeds one or more of your defined thresholds:" + UiSharedService.TooltipSeparator;
bool shownVram = false;
if (_performanceConfigService.Current.VRAMSizeWarningThresholdMiB > 0
&& _performanceConfigService.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < _pair.LastAppliedApproximateVRAMBytes)
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
shownVram = true;
userWarningText += $"Approx. VRAM Usage: Used: {UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes)}, Threshold: {_performanceConfigService.Current.VRAMSizeWarningThresholdMiB} MiB";
}
if (_performanceConfigService.Current.TrisWarningThresholdThousands > 0
&& _performanceConfigService.Current.TrisWarningThresholdThousands * 1024 < _pair.LastAppliedDataTris)
{
if (shownVram) userWarningText += Environment.NewLine;
userWarningText += $"Approx. Triangle count: Used: {_pair.LastAppliedDataTris}, Threshold: {_performanceConfigService.Current.TrisWarningThresholdThousands * 1000}";
}
string userWarningText = "WARNING: This user exceeds one or more of your defined thresholds:" + UiSharedService.TooltipSeparator;
bool shownVram = false;
if (_performanceConfigService.Current.VRAMSizeWarningThresholdMiB > 0
&& _performanceConfigService.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < _pair.LastAppliedApproximateVRAMBytes)
{
shownVram = true;
userWarningText += $"Approx. VRAM Usage: Used: {UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes)}, Threshold: {_performanceConfigService.Current.VRAMSizeWarningThresholdMiB} MiB";
}
if (_performanceConfigService.Current.TrisWarningThresholdThousands > 0
&& _performanceConfigService.Current.TrisWarningThresholdThousands * 1024 < _pair.LastAppliedDataTris)
{
if (shownVram) userWarningText += Environment.NewLine;
userWarningText += $"Approx. Triangle count: Used: {_pair.LastAppliedDataTris}, Threshold: {_performanceConfigService.Current.TrisWarningThresholdThousands * 1000}";
}
UiSharedService.AttachToolTip(userWarningText);
UiSharedService.AttachToolTip(userWarningText);
}
}
ImGui.SameLine();
@@ -384,6 +440,7 @@ public class DrawUserPair
_pair.LastAppliedApproximateVRAMBytes,
_pair.LastAppliedApproximateEffectiveVRAMBytes,
_pair.LastAppliedDataTris,
_pair.LastAppliedApproximateEffectiveTris,
_pair.IsPaired,
groupDisplays is null ? ImmutableArray<string>.Empty : ImmutableArray.CreateRange(groupDisplays));
@@ -399,6 +456,8 @@ public class DrawUserPair
private static string BuildTooltip(in TooltipSnapshot snapshot)
{
var builder = new StringBuilder(256);
static string FormatTriangles(long count) =>
count > 1000 ? (count / 1000d).ToString("0.0'k'") : count.ToString();
if (snapshot.IsPaused)
{
@@ -465,9 +524,13 @@ public class DrawUserPair
{
builder.Append(Environment.NewLine);
builder.Append("Approx. Triangle Count (excl. Vanilla): ");
builder.Append(snapshot.LastAppliedDataTris > 1000
? (snapshot.LastAppliedDataTris / 1000d).ToString("0.0'k'")
: snapshot.LastAppliedDataTris);
builder.Append(FormatTriangles(snapshot.LastAppliedDataTris));
if (snapshot.LastAppliedApproximateEffectiveTris >= 0)
{
builder.Append(" (Effective: ");
builder.Append(FormatTriangles(snapshot.LastAppliedApproximateEffectiveTris));
builder.Append(')');
}
}
}
@@ -499,11 +562,12 @@ public class DrawUserPair
long LastAppliedApproximateVRAMBytes,
long LastAppliedApproximateEffectiveVRAMBytes,
long LastAppliedDataTris,
long LastAppliedApproximateEffectiveTris,
bool IsPaired,
ImmutableArray<string> GroupDisplays)
{
public static TooltipSnapshot Empty { get; } =
new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, -1, false, ImmutableArray<string>.Empty);
new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, -1, -1, false, ImmutableArray<string>.Empty);
}
private void DrawPairedClientMenu()
@@ -560,12 +624,15 @@ public class DrawUserPair
perm.SetPaused(!perm.IsPaused());
_ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm));
}
UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused()
? ("Pause pairing with " + _pair.UserData.AliasOrUID
+ (_pair.UserPair!.OwnPermissions.IsSticky()
? string.Empty
: UiSharedService.TooltipSeparator + "Hold CTRL to enable preferred permissions while pausing." + Environment.NewLine + "This will leave this pair paused even if unpausing syncshells including this pair."))
: "Resume pairing with " + _pair.UserData.AliasOrUID);
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused()
? ("Pause pairing with " + _pair.UserData.AliasOrUID
+ (_pair.UserPair!.OwnPermissions.IsSticky()
? string.Empty
: UiSharedService.TooltipSeparator + "Hold CTRL to enable preferred permissions while pausing." + Environment.NewLine + "This will leave this pair paused even if unpausing syncshells including this pair."))
: "Resume pairing with " + _pair.UserData.AliasOrUID);
}
if (_pair.IsPaired)
{
@@ -574,6 +641,136 @@ public class DrawUserPair
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky();
var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle;
var shareLocationIcon = FontAwesomeIcon.Globe;
var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID);
var shareLocation = !string.IsNullOrEmpty(location);
var expireAt = _locationShareService.GetSharingStatus(_pair.UserPair!.User.UID);
var shareLocationToOther = expireAt > DateTimeOffset.UtcNow;
var shareColor = shareLocation switch
{
true when shareLocationToOther => UIColors.Get("LightlessGreen"),
true when !shareLocationToOther => UIColors.Get("LightlessBlue"),
_ => UIColors.Get("LightlessYellow"),
};
if (shareLocation || shareLocationToOther)
{
currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX);
ImGui.SameLine(currentRightSide);
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();
if (_pair.IsOnline)
{
if (shareLocation)
{
if (!string.IsNullOrEmpty(location))
{
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
ImGui.SameLine();
ImGui.TextUnformatted(location);
ImGui.Separator();
ImGui.TextUnformatted("Click to teleport to this location");
}
else
{
ImGui.TextUnformatted("Location info not updated, reconnect or wait for update.");
}
}
else
{
ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)");
}
}
else
{
ImGui.TextUnformatted("User not online. (´・ω・`)?");
}
ImGui.Separator();
if (shareLocationToOther)
{
ImGui.TextUnformatted("Sharing your location. ヾ(•ω•`)o");
if (expireAt != DateTimeOffset.MaxValue)
{
ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g"));
}
}
else
{
ImGui.TextUnformatted("NOT sharing your location.  ̄へ ̄");
}
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)
{
@@ -663,8 +860,11 @@ public class DrawUserPair
currentRightSide -= (_uiSharedService.GetIconSize(FontAwesomeIcon.Running).X + (spacingX / 2f));
ImGui.SameLine(currentRightSide);
_uiSharedService.IconText(FontAwesomeIcon.Running);
UiSharedService.AttachToolTip($"This user has shared {sharedData.Count} Character Data Sets with you." + UiSharedService.TooltipSeparator
+ "Click to open the Character Data Hub and show the entries.");
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
UiSharedService.AttachToolTip($"This user has shared {sharedData.Count} Character Data Sets with you." + UiSharedService.TooltipSeparator
+ "Click to open the Character Data Hub and show the entries.");
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
_mediator.Publish(new OpenCharaDataHubWithFilterMessage(_pair.UserData));