Files
LightlessClient/LightlessSync/PlayerData/Pairs/PairManager.cs
defnotken 72a62b7449
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
2.1.0 (#123)
# 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
2026-01-20 19:43:00 +00:00

580 lines
19 KiB
C#

using System.Diagnostics.CodeAnalysis;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User;
namespace LightlessSync.PlayerData.Pairs;
/// <summary>
/// in memory state for pairs, groups, and syncshells
/// </summary>
public sealed class PairManager
{
private readonly object _gate = new();
private readonly Dictionary<string, PairConnection> _pairs = new(StringComparer.Ordinal);
private readonly Dictionary<string, Syncshell> _groups = new(StringComparer.Ordinal);
public PairConnection? LastAddedUser { get; private set; }
public IReadOnlyDictionary<string, PairConnection> GetAllPairs()
{
lock (_gate)
{
return new Dictionary<string, PairConnection>(_pairs);
}
}
public IReadOnlyDictionary<string, Syncshell> GetAllGroups()
{
lock (_gate)
{
return new Dictionary<string, Syncshell>(_groups);
}
}
public PairConnection? GetLastAddedUser()
{
lock (_gate)
{
return LastAddedUser;
}
}
public void ClearLastAddedUser()
{
lock (_gate)
{
LastAddedUser = null;
}
}
public void ClearAll()
{
lock (_gate)
{
_pairs.Clear();
_groups.Clear();
LastAddedUser = null;
}
}
public PairOperationResult<PairConnection> GetPair(string userId)
{
lock (_gate)
{
if (_pairs.TryGetValue(userId, out var connection))
{
return PairOperationResult<PairConnection>.Ok(connection);
}
return PairOperationResult<PairConnection>.Fail($"Pair {userId} not found.");
}
}
public bool TryGetPair(string userId, [NotNullWhen(true)] out PairConnection? connection)
{
lock (_gate)
{
return _pairs.TryGetValue(userId, out connection);
}
}
public PairOperationResult<Syncshell> GetGroup(string groupId)
{
lock (_gate)
{
if (_groups.TryGetValue(groupId, out var shell))
{
return PairOperationResult<Syncshell>.Ok(shell);
}
return PairOperationResult<Syncshell>.Fail($"Group {groupId} not found.");
}
}
public IReadOnlyList<PairConnection> GetDirectPairs()
{
lock (_gate)
{
return _pairs.Values.Where(p => p.IsDirectlyPaired).ToList();
}
}
public IReadOnlyList<PairConnection> GetPairsByIdent(string ident)
{
lock (_gate)
{
return _pairs.Values
.Where(p => p.Ident is not null && string.Equals(p.Ident, ident, StringComparison.Ordinal))
.ToList();
}
}
public IReadOnlyList<Syncshell> GetOwnedOrModeratedShells(string currentUserUid)
{
lock (_gate)
{
return _groups.Values
.Where(s =>
string.Equals(s.GroupFullInfo.Owner.UID, currentUserUid, StringComparison.OrdinalIgnoreCase)
|| s.GroupFullInfo.GroupUserInfo.HasFlag(GroupPairUserInfo.IsModerator))
.ToList();
}
}
public PairOperationResult<UserPermissions> GetPairCombinedPermissions(string userId)
{
lock (_gate)
{
if (!_pairs.TryGetValue(userId, out var connection))
{
return PairOperationResult<UserPermissions>.Fail($"Pair {userId} not found.");
}
var combined = connection.SelfToOtherPermissions | connection.OtherToSelfPermissions;
return PairOperationResult<UserPermissions>.Ok(combined);
}
}
public PairOperationResult<PairRegistration> MarkOnline(OnlineUserIdentDto dto)
{
lock (_gate)
{
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
{
connection = GetOrCreatePair(dto.User);
}
connection.SetOnline(dto.Ident);
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), dto.Ident));
}
}
public PairOperationResult<PairRegistration> MarkOffline(UserData user)
{
lock (_gate)
{
if (!_pairs.TryGetValue(user.UID, out var connection))
{
return PairOperationResult<PairRegistration>.Fail($"Pair {user.UID} not found.");
}
var ident = connection.Ident;
connection.SetOffline();
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(user.UID), ident));
}
}
public PairOperationResult<PairRegistration> AddOrUpdateIndividual(UserPairDto dto, bool markAsLastAddedUser = true)
{
lock (_gate)
{
var connection = GetOrCreatePair(dto.User, out var created);
connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions);
connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);
if (connection.Ident is null)
{
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), null));
}
if (created && markAsLastAddedUser)
{
LastAddedUser = connection;
}
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident));
}
}
public PairOperationResult<PairRegistration> AddOrUpdateIndividual(UserFullPairDto dto)
{
lock (_gate)
{
var connection = GetOrCreatePair(dto.User, out _);
connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions);
connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);
var removedGroups = connection.Groups.Keys.Where(k => !dto.Groups.Contains(k, StringComparer.Ordinal)).ToList();
foreach (var groupId in removedGroups)
{
connection.RemoveGroupRelationship(groupId);
if (_groups.TryGetValue(groupId, out var shell))
{
shell.Users.Remove(dto.User.UID);
}
}
foreach (var groupId in dto.Groups)
{
connection.EnsureGroupRelationship(groupId, null);
if (_groups.TryGetValue(groupId, out var shell))
{
shell.Users[dto.User.UID] = connection;
}
}
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident));
}
}
public PairOperationResult<PairRegistration?> RemoveIndividual(UserDto dto)
{
lock (_gate)
{
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
{
return PairOperationResult<PairRegistration?>.Fail($"Pair {dto.User.UID} not found.");
}
connection.UpdateStatus(null);
var registration = TryRemovePairIfNoConnection(connection);
return PairOperationResult<PairRegistration?>.Ok(registration);
}
}
public PairOperationResult<PairRegistration> SetPairOtherToSelfPermissions(UserPermissionsDto dto)
{
lock (_gate)
{
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
{
return PairOperationResult<PairRegistration>.Fail($"Pair {dto.User.UID} not found.");
}
connection.UpdatePermissions(connection.SelfToOtherPermissions, dto.Permissions);
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident));
}
}
public PairOperationResult<PairRegistration> SetPairSelfToOtherPermissions(UserPermissionsDto dto)
{
lock (_gate)
{
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
{
return PairOperationResult<PairRegistration>.Fail($"Pair {dto.User.UID} not found.");
}
connection.UpdatePermissions(dto.Permissions, connection.OtherToSelfPermissions);
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident));
}
}
public PairOperationResult SetIndividualStatus(UserIndividualPairStatusDto dto)
{
lock (_gate)
{
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
{
return PairOperationResult.Fail($"Pair {dto.User.UID} not found.");
}
connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);
_ = TryRemovePairIfNoConnection(connection);
return PairOperationResult.Ok();
}
}
public PairOperationResult AddOrUpdateGroupPair(GroupPairFullInfoDto dto)
{
lock (_gate)
{
var shell = GetOrCreateShell(dto.Group);
var connection = GetOrCreatePair(dto.User);
var groupInfo = shell.GroupFullInfo.GroupPairUserInfos.GetValueOrDefault(dto.User.UID, GroupPairUserInfo.None);
connection.EnsureGroupRelationship(dto.Group.GID, groupInfo == GroupPairUserInfo.None ? null : groupInfo);
connection.UpdatePermissions(dto.SelfToOtherPermissions, dto.OtherToSelfPermissions);
shell.Users[dto.User.UID] = connection;
return PairOperationResult.Ok();
}
}
public PairOperationResult<PairRegistration?> RemoveGroupPair(GroupPairDto dto)
{
lock (_gate)
{
if (_groups.TryGetValue(dto.GID, out var shell))
{
shell.Users.Remove(dto.User.UID);
}
PairRegistration? registration = null;
if (_pairs.TryGetValue(dto.User.UID, out var connection))
{
connection.RemoveGroupRelationship(dto.GID);
registration = TryRemovePairIfNoConnection(connection);
}
return PairOperationResult<PairRegistration?>.Ok(registration);
}
}
public PairOperationResult<IReadOnlyList<PairRegistration>> RemoveGroup(string groupId)
{
lock (_gate)
{
if (!_groups.Remove(groupId, out var shell))
{
return PairOperationResult<IReadOnlyList<PairRegistration>>.Fail($"Group {groupId} not found.");
}
var removed = new List<PairRegistration>();
foreach (var connection in shell.Users.Values.ToList())
{
connection.RemoveGroupRelationship(groupId);
var registration = TryRemovePairIfNoConnection(connection);
if (registration is not null)
{
removed.Add(registration);
}
}
return PairOperationResult<IReadOnlyList<PairRegistration>>.Ok(removed);
}
}
public PairOperationResult AddGroup(GroupFullInfoDto dto)
{
lock (_gate)
{
if (!_groups.TryGetValue(dto.Group.GID, out var shell))
{
shell = new Syncshell(dto);
_groups[dto.Group.GID] = shell;
}
else
{
shell.Update(dto);
shell.Users.Clear();
}
foreach (var (userId, info) in dto.GroupPairUserInfos)
{
if (_pairs.TryGetValue(userId, out var connection))
{
connection.EnsureGroupRelationship(dto.Group.GID, info == GroupPairUserInfo.None ? null : info);
shell.Users[userId] = connection;
}
}
return PairOperationResult.Ok();
}
}
public PairOperationResult UpdateGroupInfo(GroupInfoDto dto)
{
lock (_gate)
{
if (!_groups.TryGetValue(dto.Group.GID, out var shell))
{
return PairOperationResult.Fail($"Group {dto.Group.GID} not found.");
}
var updated = new GroupFullInfoDto(
dto.Group,
dto.Owner,
dto.GroupPermissions,
shell.GroupFullInfo.GroupUserPermissions,
shell.GroupFullInfo.GroupUserInfo,
new Dictionary<string, GroupPairUserInfo>(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal),
0);
shell.Update(updated);
return PairOperationResult.Ok();
}
}
public PairOperationResult UpdateGroupPairPermissions(GroupPairUserPermissionDto dto)
{
lock (_gate)
{
if (!_groups.TryGetValue(dto.Group.GID, out var shell))
{
return PairOperationResult.Fail($"Group {dto.Group.GID} not found.");
}
var updated = shell.GroupFullInfo with { GroupUserPermissions = dto.GroupPairPermissions };
shell.Update(updated);
return PairOperationResult.Ok();
}
}
public PairOperationResult UpdateGroupPermissions(GroupPermissionDto dto)
{
lock (_gate)
{
if (!_groups.TryGetValue(dto.Group.GID, out var shell))
{
return PairOperationResult.Fail($"Group {dto.Group.GID} not found.");
}
var updated = shell.GroupFullInfo with { GroupPermissions = dto.Permissions };
shell.Update(updated);
return PairOperationResult.Ok();
}
}
public PairOperationResult UpdateGroupPairStatus(GroupPairUserInfoDto dto)
{
lock (_gate)
{
if (_pairs.TryGetValue(dto.UID, out var connection))
{
connection.EnsureGroupRelationship(dto.GID, dto.GroupUserInfo == GroupPairUserInfo.None ? null : dto.GroupUserInfo);
}
if (_groups.TryGetValue(dto.GID, out var shell))
{
var infos = new Dictionary<string, GroupPairUserInfo>(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal)
{
[dto.UID] = dto.GroupUserInfo
};
var updated = shell.GroupFullInfo with { GroupPairUserInfos = infos };
shell.Update(updated);
}
return PairOperationResult.Ok();
}
}
public PairOperationResult UpdateGroupStatus(GroupPairUserInfoDto dto)
{
lock (_gate)
{
if (!_groups.TryGetValue(dto.GID, out var shell))
{
return PairOperationResult.Fail($"Group {dto.GID} not found.");
}
var updated = shell.GroupFullInfo with { GroupUserInfo = dto.GroupUserInfo };
shell.Update(updated);
return PairOperationResult.Ok();
}
}
public PairOperationResult UpdateOtherPermissions(UserPermissionsDto dto)
{
lock (_gate)
{
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
{
return PairOperationResult.Fail($"Pair {dto.User.UID} not found.");
}
connection.UpdatePermissions(connection.SelfToOtherPermissions, dto.Permissions);
return PairOperationResult.Ok();
}
}
public PairOperationResult UpdateSelfPermissions(UserPermissionsDto dto)
{
lock (_gate)
{
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
{
return PairOperationResult.Fail($"Pair {dto.User.UID} not found.");
}
connection.UpdatePermissions(dto.Permissions, connection.OtherToSelfPermissions);
return PairOperationResult.Ok();
}
}
private PairConnection GetOrCreatePair(UserData user)
{
return GetOrCreatePair(user, out _);
}
private PairConnection GetOrCreatePair(UserData user, out bool created)
{
if (_pairs.TryGetValue(user.UID, out var connection))
{
created = false;
return connection;
}
connection = new PairConnection(user);
_pairs[user.UID] = connection;
created = true;
return connection;
}
private Syncshell GetOrCreateShell(GroupData group)
{
if (_groups.TryGetValue(group.GID, out var shell))
{
return shell;
}
var placeholder = new GroupFullInfoDto(
group,
new UserData(string.Empty),
GroupPermissions.NoneSet,
GroupUserPreferredPermissions.NoneSet,
GroupPairUserInfo.None,
new Dictionary<string, GroupPairUserInfo>(StringComparer.Ordinal),
0);
shell = new Syncshell(placeholder);
_groups[group.GID] = shell;
return shell;
}
private PairRegistration? TryRemovePairIfNoConnection(PairConnection connection)
{
if (connection.HasAnyConnection)
{
return null;
}
var ident = connection.Ident;
if (connection.IsOnline)
{
connection.SetOffline();
}
var userId = connection.User.UID;
_pairs.Remove(userId);
foreach (var shell in _groups.Values)
{
shell.Users.Remove(userId);
}
return new PairRegistration(new PairUniqueIdentifier(userId), ident);
}
public static PairConnection CreateFromFullData(UserFullPairDto dto)
{
var connection = new PairConnection(dto.User);
connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions);
connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);
foreach (var groupId in dto.Groups)
{
connection.EnsureGroupRelationship(groupId, null);
}
return connection;
}
public static PairConnection CreateFromPartialData(UserPairDto dto)
{
var connection = new PairConnection(dto.User);
connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions);
connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);
return connection;
}
public static GroupPairRelationship CreateGroupPairRelationshipFromFullInfo(string userUid, GroupFullInfoDto fullInfo)
{
return new GroupPairRelationship(fullInfo.Group.GID,
fullInfo.GroupPairUserInfos.TryGetValue(userUid, out var info) && info != GroupPairUserInfo.None
? info
: null);
}
}