2.0.0 (#92)
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s

2.0.0 Changes:

- Reworked shell finder UI with compact or list view with profile tags showing with the listing, allowing moderators to broadcast the syncshell as well to have it be used more.
- Reworked user list in syncshell admin screen to have filter visible and moved away from table to its own thing, allowing to copy uid/note/alias when clicking on the name.
- Reworked download bars and download box to make it look more modern, removed the jitter around, so it shouldn't vibrate around much.
- Chat has been added to the top menu, working in Zone or in Syncshells to be used there.
- Paired system has been revamped to make pausing and unpausing faster, and loading people should be faster as well.
- Moved to the internal object table to have faster load times for users; people should load in faster
- Compactor is running on a multi-threaded level instead of single-threaded; this should increase the speed of compacting files
- Nameplate Service has been reworked so it wouldn't use the nameplate handler anymore.
- Files can be resized when downloading to reduce load on users if they aren't compressed. (can be toggled to resize all).
- Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many syncshells in your list.
- Lightfinder plates have been moved away from using Nameplates, but will use an overlay.
- Main UI has been changed a bit with a gradient, and on hover will glow up now.
- Reworked Profile UI for Syncshell and Users to be more user-facing with more customizable items.
- Reworked Settings UI to look more modern.
- Performance should be better due to new systems that would dispose of the collections and better caching of items.

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: choco <choco@patat.nl>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: Minmoose <KennethBohr@outlook.com>
Reviewed-on: #92
This commit was merged in pull request #92.
This commit is contained in:
2025-12-21 17:19:34 +00:00
parent 906f401940
commit 835a0a637d
191 changed files with 32636 additions and 8841 deletions

View File

@@ -1,13 +1,15 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Lumina.Text;
using System;
using Lumina.Text.Parse;
using Lumina.Text.ReadOnly;
using System.Globalization;
using System.Numerics;
using System.Threading;
using System.Reflection;
using System.Text;
using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString;
using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder;
using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder;
@@ -19,6 +21,438 @@ public static class SeStringUtils
private static int _seStringHitboxCounter;
private static int _iconHitboxCounter;
public static bool TryRenderSeStringMarkupAtCursor(string payload)
{
if (string.IsNullOrWhiteSpace(payload))
return false;
var wrapWidth = ImGui.GetContentRegionAvail().X;
if (wrapWidth <= 0f || float.IsNaN(wrapWidth) || float.IsInfinity(wrapWidth))
wrapWidth = float.MaxValue;
var normalizedPayload = payload.ReplaceLineEndings("<br>");
try
{
_ = ReadOnlySeString.FromMacroString(normalizedPayload, new MacroStringParseOptions
{
ExceptionMode = MacroStringParseExceptionMode.Throw
});
}
catch (Exception)
{
return false;
}
try
{
var drawParams = new SeStringDrawParams
{
WrapWidth = wrapWidth,
Font = ImGui.GetFont(),
Color = ImGui.GetColorU32(ImGuiCol.Text),
};
var renderId = ImGui.GetID($"SeStringMarkup##{normalizedPayload.GetHashCode(StringComparison.Ordinal)}");
var drawResult = ImGuiHelpers.CompileSeStringWrapped(normalizedPayload, drawParams, renderId);
var height = drawResult.Size.Y;
if (height <= 0f)
height = ImGui.GetTextLineHeight();
ImGui.Dummy(new Vector2(0f, height));
if (drawResult.InteractedPayloadEnvelope.Length > 0 &&
TryExtractLink(drawResult.InteractedPayloadEnvelope, drawResult.InteractedPayload, out var linkUrl, out var tooltipText))
{
var hoverText = !string.IsNullOrEmpty(linkUrl) ? linkUrl : tooltipText;
if (!string.IsNullOrEmpty(hoverText))
{
ImGui.BeginTooltip();
ImGui.TextUnformatted(hoverText);
ImGui.EndTooltip();
}
if (!string.IsNullOrEmpty(linkUrl))
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
if (drawResult.Clicked && !string.IsNullOrEmpty(linkUrl))
Dalamud.Utility.Util.OpenLink(linkUrl);
}
return true;
}
catch (Exception ex)
{
ImGui.TextDisabled($"[SeString error] {ex.Message}");
return false;
}
}
public enum SeStringSegmentType
{
Text,
Icon
}
public readonly record struct SeStringSegment(
SeStringSegmentType Type,
string? Text,
Vector4? Color,
uint IconId,
IDalamudTextureWrap? Texture,
Vector2 Size);
public static bool TryResolveSegments(
string payload,
float scale,
Func<uint, IDalamudTextureWrap?> iconResolver,
List<SeStringSegment> resolvedSegments,
out Vector2 totalSize)
{
totalSize = Vector2.Zero;
if (string.IsNullOrWhiteSpace(payload))
return false;
var parsedSegments = new List<ParsedSegment>(Math.Max(1, payload.Length / 4));
if (!ParseSegments(payload, parsedSegments))
return false;
float width = 0f;
float height = 0f;
foreach (var segment in parsedSegments)
{
switch (segment.Type)
{
case ParsedSegmentType.Text:
{
var text = segment.Text ?? string.Empty;
if (text.Length == 0)
break;
var textSize = ImGui.CalcTextSize(text);
resolvedSegments.Add(new SeStringSegment(SeStringSegmentType.Text, text, segment.Color, 0, null, textSize));
width += textSize.X;
height = MathF.Max(height, textSize.Y);
break;
}
case ParsedSegmentType.Icon:
{
var wrap = iconResolver(segment.IconId);
Vector2 iconSize;
string? fallback = null;
if (wrap != null)
{
iconSize = CalculateIconSize(wrap, scale);
}
else
{
fallback = $"[{segment.IconId}]";
iconSize = ImGui.CalcTextSize(fallback);
}
resolvedSegments.Add(new SeStringSegment(SeStringSegmentType.Icon, fallback, segment.Color, segment.IconId, wrap, iconSize));
width += iconSize.X;
height = MathF.Max(height, iconSize.Y);
break;
}
}
}
totalSize = new Vector2(width, height);
parsedSegments.Clear();
return resolvedSegments.Count > 0;
}
private enum ParsedSegmentType
{
Text,
Icon
}
private readonly record struct ParsedSegment(
ParsedSegmentType Type,
string? Text,
uint IconId,
Vector4? Color);
private static bool ParseSegments(string payload, List<ParsedSegment> segments)
{
var builder = new StringBuilder(payload.Length);
Vector4? activeColor = null;
var index = 0;
while (index < payload.Length)
{
if (payload[index] == '<')
{
var end = payload.IndexOf('>', index);
if (end == -1)
break;
var tagContent = payload.Substring(index + 1, end - index - 1);
if (TryHandleIconTag(tagContent, segments, builder, activeColor))
{
index = end + 1;
continue;
}
if (TryHandleColorTag(tagContent, segments, builder, ref activeColor))
{
index = end + 1;
continue;
}
builder.Append('<');
builder.Append(tagContent);
builder.Append('>');
index = end + 1;
}
else
{
builder.Append(payload[index]);
index++;
}
}
if (index < payload.Length)
builder.Append(payload, index, payload.Length - index);
FlushTextBuilder(builder, activeColor, segments);
return segments.Count > 0;
}
private static bool TryHandleIconTag(string tagContent, List<ParsedSegment> segments, StringBuilder textBuilder, Vector4? activeColor)
{
if (!tagContent.StartsWith("icon(", StringComparison.OrdinalIgnoreCase) || !tagContent.EndsWith(')'))
return false;
var inner = tagContent.AsSpan(5, tagContent.Length - 6).Trim();
if (!uint.TryParse(inner, NumberStyles.Integer, CultureInfo.InvariantCulture, out var iconId))
return false;
FlushTextBuilder(textBuilder, activeColor, segments);
segments.Add(new ParsedSegment(ParsedSegmentType.Icon, null, iconId, null));
return true;
}
private static bool TryHandleColorTag(string tagContent, List<ParsedSegment> segments, StringBuilder textBuilder, ref Vector4? activeColor)
{
if (tagContent.StartsWith("color", StringComparison.OrdinalIgnoreCase))
{
var equalsIndex = tagContent.IndexOf('=');
if (equalsIndex == -1)
return false;
var value = tagContent.Substring(equalsIndex + 1).Trim().Trim('"');
if (!TryParseColor(value, out var color))
return false;
FlushTextBuilder(textBuilder, activeColor, segments);
activeColor = color;
return true;
}
if (tagContent.Equals("/color", StringComparison.OrdinalIgnoreCase))
{
FlushTextBuilder(textBuilder, activeColor, segments);
activeColor = null;
return true;
}
return false;
}
private static void FlushTextBuilder(StringBuilder builder, Vector4? color, List<ParsedSegment> segments)
{
if (builder.Length == 0)
return;
segments.Add(new ParsedSegment(ParsedSegmentType.Text, builder.ToString(), 0, color));
builder.Clear();
}
private static bool TryExtractLink(ReadOnlySpan<byte> envelope, Payload? payload, out string url, out string? tooltipText)
{
url = string.Empty;
tooltipText = null;
if (envelope.Length == 0 && payload is null)
return false;
tooltipText = envelope.Length > 0 ? DalamudSeString.Parse(envelope.ToArray()).TextValue : null;
if (payload is not null && TryReadUrlFromPayload(payload, out url))
return true;
if (!string.IsNullOrWhiteSpace(tooltipText))
{
var candidate = FindFirstUrl(tooltipText);
if (!string.IsNullOrEmpty(candidate))
{
url = candidate;
return true;
}
}
url = string.Empty;
return false;
}
private static bool TryReadUrlFromPayload(object payload, out string url)
{
url = string.Empty;
var type = payload.GetType();
static string? ReadStringProperty(Type type, object instance, string propertyName)
=> type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
?.GetValue(instance) as string;
string? candidate = ReadStringProperty(type, payload, "Uri")
?? ReadStringProperty(type, payload, "Url")
?? ReadStringProperty(type, payload, "Target")
?? ReadStringProperty(type, payload, "Destination");
if (IsHttpUrl(candidate))
{
url = candidate!;
return true;
}
var dataProperty = type.GetProperty("Data", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (dataProperty?.GetValue(payload) is IEnumerable<string> sequence)
{
foreach (var entry in sequence)
{
if (IsHttpUrl(entry))
{
url = entry;
return true;
}
}
}
var extraStringProp = type.GetProperty("ExtraString", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (IsHttpUrl(extraStringProp?.GetValue(payload) as string))
{
url = (extraStringProp!.GetValue(payload) as string)!;
return true;
}
var textProp = type.GetProperty("Text", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (IsHttpUrl(textProp?.GetValue(payload) as string))
{
url = (textProp!.GetValue(payload) as string)!;
return true;
}
return false;
}
private static string? FindFirstUrl(string? text)
{
if (string.IsNullOrWhiteSpace(text))
return null;
var index = text.IndexOf("http", StringComparison.OrdinalIgnoreCase);
while (index >= 0)
{
var end = index;
while (end < text.Length && !char.IsWhiteSpace(text[end]) && !"\"')]>".Contains(text[end]))
end++;
var candidate = text.Substring(index, end - index).TrimEnd('.', ',', ';');
if (IsHttpUrl(candidate))
return candidate;
index = text.IndexOf("http", end, StringComparison.OrdinalIgnoreCase);
}
return null;
}
private static bool IsHttpUrl(string? value)
{
if (string.IsNullOrWhiteSpace(value))
return false;
return Uri.TryCreate(value, UriKind.Absolute, out var uri)
&& (string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.Ordinal) || string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.Ordinal));
}
public static string StripMarkup(string value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
var builder = new StringBuilder(value.Length);
int depth = 0;
foreach (var c in value)
{
if (c == '<')
{
depth++;
continue;
}
if (c == '>' && depth > 0)
{
depth--;
continue;
}
if (depth == 0)
builder.Append(c);
}
return builder.ToString().Trim();
}
private static bool TryParseColor(string value, out Vector4 color)
{
color = default;
if (string.IsNullOrEmpty(value) || value[0] != '#')
return false;
var hex = value.AsSpan(1);
if (!uint.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
return false;
byte a, r, g, b;
if (hex.Length == 6)
{
a = 0xFF;
r = (byte)(parsed >> 16);
g = (byte)(parsed >> 8);
b = (byte)parsed;
}
else if (hex.Length == 8)
{
a = (byte)(parsed >> 24);
r = (byte)(parsed >> 16);
g = (byte)(parsed >> 8);
b = (byte)parsed;
}
else
{
return false;
}
const float inv = 1.0f / 255f;
color = new Vector4(r * inv, g * inv, b * inv, a * inv);
return true;
}
private static Vector2 CalculateIconSize(IDalamudTextureWrap wrap, float scale)
{
const float IconHeightScale = 1.25f;
var textHeight = ImGui.GetTextLineHeight();
var baselineHeight = MathF.Max(1f, textHeight - 2f * scale);
var targetHeight = MathF.Max(baselineHeight, baselineHeight * IconHeightScale);
var aspect = wrap.Width > 0 ? wrap.Width / (float)wrap.Height : 1f;
return new Vector2(targetHeight * aspect, targetHeight);
}
public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor)
{
var b = new DalamudSeStringBuilder();
@@ -57,10 +491,9 @@ public static class SeStringUtils
continue;
var hasColor = fragment.Color.HasValue;
Vector4 color = default;
if (hasColor)
{
color = fragment.Color!.Value;
Vector4 color = fragment.Color!.Value;
builder.PushColorRgba(color);
}
@@ -106,12 +539,15 @@ public static class SeStringUtils
{
drawList ??= ImGui.GetWindowDrawList();
var usedFont = font ?? ImGui.GetFont();
var drawParams = new SeStringDrawParams
{
Font = font ?? ImGui.GetFont(),
Font = usedFont,
FontSize = usedFont.FontSize,
Color = ImGui.GetColorU32(ImGuiCol.Text),
WrapWidth = wrapWidth,
TargetDrawList = drawList
TargetDrawList = drawList,
ScreenOffset = ImGui.GetCursorScreenPos()
};
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
@@ -123,22 +559,38 @@ public static class SeStringUtils
ImGui.Dummy(new Vector2(0f, textSize.Y));
}
public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, string? id = null)
{
var drawList = ImGui.GetWindowDrawList();
var usedFont = font ?? UiBuilder.MonoFont;
var textSize = ImGui.CalcTextSize(seString.TextValue);
if (textSize.Y <= 0f)
{
textSize.Y = usedFont.FontSize;
}
var style = ImGui.GetStyle();
var fontHeight = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize();
var frameHeight = fontHeight + style.FramePadding.Y * 2f;
var hitboxHeight = MathF.Max(frameHeight, textSize.Y);
var verticalOffset = MathF.Max((hitboxHeight - textSize.Y) * 0.5f, 0f);
var drawPos = new Vector2(position.X, position.Y + verticalOffset);
var drawParams = new SeStringDrawParams
{
Font = font ?? UiBuilder.MonoFont,
FontSize = usedFont.FontSize,
ScreenOffset = drawPos,
Font = usedFont,
Color = 0xFFFFFFFF,
WrapWidth = float.MaxValue,
TargetDrawList = drawList
};
ImGui.SetCursorScreenPos(position);
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
ImGui.SetCursorScreenPos(drawPos);
var textSize = ImGui.CalcTextSize(seString.TextValue);
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
ImGui.SetCursorScreenPos(position);
if (id is not null)
@@ -152,30 +604,112 @@ public static class SeStringUtils
try
{
ImGui.InvisibleButton("##hitbox", textSize);
ImGui.InvisibleButton("##hitbox", new Vector2(textSize.X, hitboxHeight));
}
finally
{
ImGui.PopID();
}
return textSize;
return new Vector2(textSize.X, hitboxHeight);
}
public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, float? targetFontSize, ImFontPtr? font = null, string? id = null)
{
var drawList = ImGui.GetWindowDrawList();
var usedFont = font ?? ImGui.GetFont();
ImGui.PushFont(usedFont);
Vector2 rawSize;
float usedEffectiveSize;
try
{
usedEffectiveSize = ImGui.GetFontSize();
rawSize = ImGui.CalcTextSize(seString.TextValue);
}
finally
{
ImGui.PopFont();
}
var desiredSize = targetFontSize ?? usedEffectiveSize;
var scale = usedEffectiveSize > 0 ? (desiredSize / usedEffectiveSize) : 1f;
var textSize = rawSize * scale;
var style = ImGui.GetStyle();
var frameHeight = desiredSize + style.FramePadding.Y * 2f;
var hitboxHeight = MathF.Max(frameHeight, textSize.Y);
var verticalOffset = MathF.Max((hitboxHeight - textSize.Y) * 0.5f, 0f);
var drawPos = new Vector2(position.X, position.Y + verticalOffset);
var drawParams = new SeStringDrawParams
{
TargetDrawList = drawList,
ScreenOffset = drawPos,
Font = usedFont,
FontSize = desiredSize,
Color = 0xFFFFFFFF,
WrapWidth = float.MaxValue,
};
ImGui.SetCursorScreenPos(drawPos);
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
ImGui.SetCursorScreenPos(position);
ImGui.PushID(id ?? Interlocked.Increment(ref _seStringHitboxCounter).ToString());
try
{
ImGui.InvisibleButton("##hitbox", new Vector2(textSize.X, hitboxHeight));
}
finally
{
ImGui.PopID();
}
return new Vector2(textSize.X, hitboxHeight);
}
public static Vector2 RenderIconWithHitbox(int iconId, Vector2 position, ImFontPtr? font = null, string? id = null)
{
var drawList = ImGui.GetWindowDrawList();
var usedFont = font ?? UiBuilder.MonoFont;
var iconMacro = $"<icon({iconId})>";
var drawParams = new SeStringDrawParams
var measureParams = new SeStringDrawParams
{
Font = font ?? UiBuilder.MonoFont,
Font = usedFont,
FontSize = usedFont.FontSize,
Color = 0xFFFFFFFF,
WrapWidth = float.MaxValue,
TargetDrawList = drawList
WrapWidth = float.MaxValue
};
var iconMacro = $"<icon({iconId})>";
var drawResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams);
var measureResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, measureParams);
var iconSize = measureResult.Size;
if (iconSize.Y <= 0f)
{
iconSize.Y = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize();
}
var style = ImGui.GetStyle();
var fontHeight = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize();
var frameHeight = fontHeight + style.FramePadding.Y * 2f;
var hitboxHeight = MathF.Max(frameHeight, iconSize.Y);
var verticalOffset = MathF.Max((hitboxHeight - iconSize.Y) * 0.5f, 0f);
var drawPos = new Vector2(position.X, position.Y + verticalOffset);
var drawParams = new SeStringDrawParams
{
Font = usedFont,
FontSize = usedFont.FontSize,
Color = 0xFFFFFFFF,
WrapWidth = float.MaxValue,
TargetDrawList = drawList,
ScreenOffset = drawPos
};
ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams);
ImGui.SetCursorScreenPos(position);
if (id is not null)
@@ -189,14 +723,14 @@ public static class SeStringUtils
try
{
ImGui.InvisibleButton("##iconHitbox", drawResult.Size);
ImGui.InvisibleButton("##iconHitbox", new Vector2(iconSize.X, hitboxHeight));
}
finally
{
ImGui.PopID();
}
return drawResult.Size;
return new Vector2(iconSize.X, hitboxHeight);
}
#region Internal Payloads
@@ -233,7 +767,7 @@ public static class SeStringUtils
protected abstract byte ChunkType { get; }
}
private class ColorPayload : AbstractColorPayload
private sealed class ColorPayload : AbstractColorPayload
{
protected override byte ChunkType => 0x13;
@@ -247,12 +781,12 @@ public static class SeStringUtils
public ColorPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { }
}
private class ColorEndPayload : AbstractColorEndPayload
private sealed class ColorEndPayload : AbstractColorEndPayload
{
protected override byte ChunkType => 0x13;
}
private class GlowPayload : AbstractColorPayload
private sealed class GlowPayload : AbstractColorPayload
{
protected override byte ChunkType => 0x14;
@@ -266,7 +800,7 @@ public static class SeStringUtils
public GlowPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { }
}
private class GlowEndPayload : AbstractColorEndPayload
private sealed class GlowEndPayload : AbstractColorEndPayload
{
protected override byte ChunkType => 0x14;
}