Skip to content

Commit e56b8cc

Browse files
committed
chore(release): merge dev into main for v0.4.7
2 parents cb235a1 + 2aae3c0 commit e56b8cc

38 files changed

Lines changed: 3559 additions & 2725 deletions

Combat/Ui/ExtraCornerAmountLabels/ExtraCornerAmountLabelsRuntime.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,13 @@ private static void SyncAnchoredSlotHosts(
232232
pool.Add(new());
233233

234234
var entry = pool[writeIndex];
235-
var live = GetOrCreateSlotLabel(host, entry, slotNamePrefix, writeIndex, slot.TextMode);
235+
var live = GetOrCreateSlotLabel(
236+
host,
237+
entry,
238+
slotNamePrefix,
239+
writeIndex,
240+
slot.TextMode,
241+
applyHostStyle);
236242
applyHostStyle(live);
237243
ApplySlotColorOverrides(live, in slot);
238244

@@ -253,7 +259,8 @@ private static Control GetOrCreateSlotLabel(
253259
AnchoredSlotHost entry,
254260
string slotNamePrefix,
255261
int slotIndex,
256-
ExtraIconAmountLabelTextMode textMode)
262+
ExtraIconAmountLabelTextMode textMode,
263+
Action<Control> applyHostStyle)
257264
{
258265
if (GodotObject.IsInstanceValid(entry.Label) && entry.TextMode == textMode &&
259266
LabelMatchesTextMode(entry.Label, textMode))
@@ -263,6 +270,7 @@ private static Control GetOrCreateSlotLabel(
263270
entry.Label!.QueueFree();
264271

265272
var label = CreateSlotLabel($"{slotNamePrefix}{slotIndex}", textMode);
273+
applyHostStyle(label);
266274
host.AddChild(label);
267275
host.MoveChild(label, host.GetChildCount() - 1);
268276
entry.Label = label;

Const.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public static class Const
2222
/// Assembly / manifest version string.
2323
/// 程序集/清单版本字符串。
2424
/// </summary>
25-
public const string Version = "0.4.6";
25+
public const string Version = "0.4.7";
2626

2727
/// <summary>
2828
/// Root key for RitsuLib JSON settings under the mod’s user folder.

Data/Models/RitsuLibSettings.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ public sealed class RitsuLibSettings
7070
public bool DebugCompatAncientArchitect { get; set; } = true;
7171

7272
/// <summary>
73-
/// Starts the loopback-only browser debug log viewer for this session.
74-
/// 为本会话启动仅监听 loopback 的浏览器调试日志查看器
73+
/// Starts the browser debug log viewer for this session. It listens on loopback unless LAN access is enabled.
74+
/// 为本会话启动浏览器调试日志查看器;除非启用局域网访问,否则仅监听 loopback。
7575
/// </summary>
7676
[JsonPropertyName("debug_log_viewer_enabled")]
7777
public bool DebugLogViewerEnabled { get; set; } = true;
@@ -91,8 +91,15 @@ public sealed class RitsuLibSettings
9191
public bool DebugLogViewerAutoOpen { get; set; }
9292

9393
/// <summary>
94-
/// Loopback HTTP port for the debug log viewer.
95-
/// 调试日志查看器的 loopback HTTP 端口。
94+
/// When true, binds the debug log viewer to all network interfaces so devices on the same LAN can connect.
95+
/// 为 true 时,调试日志查看器会监听所有网络接口,使同一局域网设备可以连接。
96+
/// </summary>
97+
[JsonPropertyName("debug_log_viewer_lan_access_enabled")]
98+
public bool DebugLogViewerLanAccessEnabled { get; set; }
99+
100+
/// <summary>
101+
/// HTTP port for the debug log viewer.
102+
/// 调试日志查看器的 HTTP 端口。
96103
/// </summary>
97104
[JsonPropertyName("debug_log_viewer_port")]
98105
public int DebugLogViewerPort { get; set; } = 18742;
@@ -105,8 +112,8 @@ public sealed class RitsuLibSettings
105112
public int DebugLogViewerPortFallbackCount { get; set; } = 20;
106113

107114
/// <summary>
108-
/// Stable browser access token for the loopback debug log viewer.
109-
/// 本机调试日志查看器使用的稳定浏览器访问 token。
115+
/// Stable browser access token for the debug log viewer.
116+
/// 调试日志查看器使用的稳定浏览器访问 token。
110117
/// </summary>
111118
[JsonPropertyName("debug_log_viewer_access_token")]
112119
public string DebugLogViewerAccessToken { get; set; } = "";

Data/RitsuLibSettingsStore.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ internal static RitsuDebugLogViewerOptions GetDebugLogViewerOptions()
224224
s.DebugLogViewerEnabled,
225225
s.DebugLogViewerMirrorGameLogs,
226226
s.DebugLogViewerAutoOpen,
227+
s.DebugLogViewerLanAccessEnabled,
227228
Math.Clamp(s.DebugLogViewerPort, 1, 65535),
228229
Math.Clamp(s.DebugLogViewerPortFallbackCount, 0, 100),
229230
s.DebugLogViewerAccessToken,

Diagnostics/Logging/RitsuDebugLogPipeline.cs

Lines changed: 130 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.Reflection;
33
using Godot;
44
using MegaCrit.Sts2.Core.Logging;
5+
using STS2RitsuLib.Utils;
6+
using Environment = System.Environment;
57

68
namespace STS2RitsuLib.Diagnostics.Logging
79
{
@@ -12,6 +14,8 @@ internal static class RitsuDebugLogPipeline
1214
private static readonly SemaphoreSlim QueueSignal = new(0);
1315
private static readonly TimeSpan InternalWarningInterval = TimeSpan.FromSeconds(30);
1416
private static readonly TimeSpan AutoOpenDelay = TimeSpan.FromSeconds(3);
17+
private static readonly string SessionId = Guid.NewGuid().ToString("N");
18+
private static readonly DateTimeOffset SessionStartedAtUtc = DateTimeOffset.UtcNow;
1519

1620
private static CancellationTokenSource? _cts;
1721
private static RitsuDebugLogRingBuffer? _ring;
@@ -43,7 +47,12 @@ public static void Initialize(RitsuDebugLogViewerOptions options)
4347
_ring = new(Math.Clamp(options.RingBufferCapacity, 512, 100000));
4448
_cts = new();
4549

46-
_server = new(options.AccessToken, Snapshot, BuildStatus, ResolveViewerAssetRoot());
50+
_server = new(
51+
options.AccessToken,
52+
options.LanAccessEnabled,
53+
Snapshot,
54+
BuildStatus,
55+
ResolveViewerAssetRoot());
4756
_server.Start(options.Port, options.PortFallbackCount);
4857

4958
_worker = Task.Run(WorkerLoopAsync);
@@ -59,13 +68,12 @@ public static void Initialize(RitsuDebugLogViewerOptions options)
5968
_initialized = true;
6069
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
6170

62-
RitsuLibFramework.Logger.Info(
63-
$"[DebugLogViewer] Local debug log viewer listening at {_server.Url}");
71+
RitsuLibFramework.Logger.Info(CreateViewerStartMessage(_server));
6472
}
6573
catch (Exception ex)
6674
{
6775
CleanupAfterFailedStart();
68-
RitsuLibFramework.Logger.Warn($"[DebugLogViewer] Failed to start local viewer: {ex.Message}");
76+
RitsuLibFramework.Logger.Warn($"[DebugLogViewer] Failed to start viewer: {ex.Message}");
6977
}
7078
}
7179
}
@@ -96,7 +104,13 @@ public static object BuildStatus()
96104
return new
97105
{
98106
enabled = _initialized,
107+
sessionId = SessionId,
108+
sessionStartedAtUtc = SessionStartedAtUtc,
109+
processId = Environment.ProcessId,
99110
url = ViewerUrl,
111+
accessMode = _server?.AccessMode ?? "loopback",
112+
lanAccessEnabled = _server?.LanAccessEnabled ?? false,
113+
lanUrls = _server?.LanUrls ?? [],
100114
clients = _server?.ClientCount ?? 0,
101115
bufferCount = _ring?.Count ?? 0,
102116
bufferCapacity = _ring?.Capacity ?? 0,
@@ -128,6 +142,17 @@ public static (bool Success, string Message) TryOpenViewerInBrowser()
128142
: (false, $"Error {error}: Could not open browser. URL: {url}");
129143
}
130144

145+
private static string CreateViewerStartMessage(RitsuDebugLogViewerServer server)
146+
{
147+
if (!server.LanAccessEnabled)
148+
return $"[DebugLogViewer] Local debug log viewer listening at {server.Url}";
149+
150+
var lanUrls = server.LanUrls;
151+
return lanUrls.Count == 0
152+
? $"[DebugLogViewer] LAN debug log viewer listening at {server.Url}; no LAN IPv4 address was found."
153+
: $"[DebugLogViewer] LAN debug log viewer listening at {server.Url}; LAN URLs: {string.Join(", ", lanUrls)}";
154+
}
155+
131156
private static async Task WorkerLoopAsync()
132157
{
133158
var token = _cts!.Token;
@@ -212,15 +237,22 @@ internal static void EmitGodotLogError(
212237

213238
private static RitsuDebugLogRecord CreateFromGodotLog(string text, bool error)
214239
{
215-
var (logLevel, unwrappedText) = ParseLevelPrefix(text, error);
216-
var (source, category, body) = ParseFormattedLogText(unwrappedText);
240+
var plainText = RitsuAnsiText.StripControlSequences(text);
241+
var (logLevel, unwrappedText, unwrappedTextStart) = ParseLevelPrefix(plainText, error);
242+
var (source, category, body, bodyStartInUnwrappedText) = ParseFormattedLogText(unwrappedText);
217243
var severityText = logLevel.ToString().ToUpperInvariant();
218244
var severityNumber = MapSeverityNumber(logLevel);
245+
var bodyStart = unwrappedTextStart + bodyStartInUnwrappedText;
219246
var attributes = new Dictionary<string, object?>
220247
{
221248
["log.record.original"] = text,
222249
["ritsulib.log.mirrored_from_godot"] = true,
223250
};
251+
if (!string.Equals(plainText, text, StringComparison.Ordinal))
252+
{
253+
attributes["ritsulib.log.ansi_stripped"] = true;
254+
attributes["ritsulib.log.plain_text"] = plainText;
255+
}
224256

225257
if (!string.IsNullOrWhiteSpace(source))
226258
attributes["ritsulib.log.source"] = source;
@@ -234,6 +266,7 @@ private static RitsuDebugLogRecord CreateFromGodotLog(string text, bool error)
234266
SeverityText = severityText,
235267
SeverityNumber = severityNumber,
236268
Body = body,
269+
BodySegments = BuildBodySegments(text, body, bodyStart),
237270
Source = source,
238271
Category = category,
239272
LoggerName = source,
@@ -258,6 +291,9 @@ private static RitsuDebugLogRecord Normalize(RitsuDebugLogRecord record)
258291
Id = id,
259292
Timestamp = timestamp,
260293
TimeUnixNano = ToUnixNanoString(timestamp),
294+
BodySegments = record.BodySegments is { Count: > 0 }
295+
? record.BodySegments
296+
: BuildPlainBodySegments(record.Body),
261297
Resource = new Dictionary<string, object?>
262298
{
263299
["service.name"] = Const.ModId,
@@ -272,49 +308,118 @@ private static RitsuDebugLogRecord Normalize(RitsuDebugLogRecord record)
272308
};
273309
}
274310

275-
private static (string? Source, string? Category, string Body) ParseFormattedLogText(string text)
311+
private static (string? Source, string? Category, string Body, int BodyStart) ParseFormattedLogText(string text)
276312
{
277313
var remaining = text.TrimStart();
314+
var remainingStart = text.Length - remaining.Length;
278315
if (!TryReadBracketPrefix(remaining, out var first, out remaining))
279-
return (null, null, text);
316+
return (null, null, text, 0);
280317

281318
var source = first;
319+
remainingStart += text[remainingStart..].Length - remaining.Length;
282320
remaining = remaining.TrimStart();
321+
remainingStart += text[remainingStart..].Length - remaining.Length;
283322
string? category = null;
284323
if (!TryReadBracketPrefix(remaining, out var second, out var afterSecond))
285-
return (source, category, remaining.Length == 0 ? text : remaining);
324+
return remaining.Length == 0
325+
? (source, category, text, 0)
326+
: (source, category, remaining, remainingStart);
327+
286328
category = second;
329+
remainingStart += remaining.Length - afterSecond.Length;
287330
remaining = afterSecond.TrimStart();
331+
remainingStart += afterSecond.Length - remaining.Length;
288332

289-
return (source, category, remaining.Length == 0 ? text : remaining);
333+
return remaining.Length == 0
334+
? (source, category, text, 0)
335+
: (source, category, remaining, remainingStart);
290336
}
291337

292-
private static (LogLevel Level, string Text) ParseLevelPrefix(string text, bool error)
338+
private static IReadOnlyList<RitsuTextSegment> BuildBodySegments(string rawText, string body, int bodyStart)
339+
{
340+
if (string.IsNullOrEmpty(body))
341+
return [];
342+
343+
var segments = SliceVisibleSegments(RitsuAnsiText.ParseSegments(rawText), bodyStart, body.Length);
344+
return segments.Count == 0 ? BuildPlainBodySegments(body) : segments;
345+
}
346+
347+
private static IReadOnlyList<RitsuTextSegment> BuildPlainBodySegments(string body)
348+
{
349+
return string.IsNullOrEmpty(body) ? [] : [new() { Text = body }];
350+
}
351+
352+
private static IReadOnlyList<RitsuTextSegment> SliceVisibleSegments(
353+
IReadOnlyList<RitsuTextSegment> segments,
354+
int start,
355+
int length)
356+
{
357+
if (segments.Count == 0 || start < 0 || length <= 0)
358+
return [];
359+
360+
var result = new List<RitsuTextSegment>();
361+
var end = start + length;
362+
var position = 0;
363+
foreach (var segment in segments)
364+
{
365+
var nextPosition = position + segment.Text.Length;
366+
if (nextPosition <= start)
367+
{
368+
position = nextPosition;
369+
continue;
370+
}
371+
372+
if (position >= end)
373+
break;
374+
375+
var segmentStart = Math.Max(0, start - position);
376+
var segmentEnd = Math.Min(segment.Text.Length, end - position);
377+
if (segmentEnd > segmentStart)
378+
result.Add(segment with { Text = segment.Text[segmentStart..segmentEnd] });
379+
380+
position = nextPosition;
381+
}
382+
383+
return result;
384+
}
385+
386+
private static (LogLevel Level, string Text, int TextStart) ParseLevelPrefix(string text, bool error)
293387
{
294388
if (error)
295-
return (LogLevel.Error, StripKnownLevelPrefix(text));
389+
{
390+
var (strippedText, strippedTextStart) = StripKnownLevelPrefix(text);
391+
return (LogLevel.Error, strippedText, strippedTextStart);
392+
}
296393

297394
var trimmed = text.TrimStart();
395+
var trimmedStart = text.Length - trimmed.Length;
298396
if (TryReadBracketPrefix(trimmed, out var bracketLevel, out var afterBracket) &&
299397
TryParseLogLevel(bracketLevel, out var level))
300-
return (level, afterBracket.TrimStart());
398+
{
399+
var afterBracketStart = trimmedStart + trimmed.Length - afterBracket.Length;
400+
var afterBracketTrimmed = afterBracket.TrimStart();
401+
var afterBracketTrimmedStart = afterBracketStart + afterBracket.Length - afterBracketTrimmed.Length;
402+
return (level, afterBracketTrimmed, afterBracketTrimmedStart);
403+
}
301404

302405
var colon = trimmed.IndexOf(':');
303-
if (colon is > 0 and <= 10 &&
304-
TryParseLogLevel(trimmed[..colon], out level))
305-
return (level, trimmed[(colon + 1)..].TrimStart());
306-
307-
return (LogLevel.Info, text);
406+
if (colon is <= 0 or > 10 ||
407+
!TryParseLogLevel(trimmed[..colon], out level)) return (LogLevel.Info, text, 0);
408+
var afterColon = trimmed[(colon + 1)..];
409+
var afterColonTrimmed = afterColon.TrimStart();
410+
var afterColonTrimmedStart = trimmedStart + colon + 1 + afterColon.Length - afterColonTrimmed.Length;
411+
return (level, afterColonTrimmed, afterColonTrimmedStart);
308412
}
309413

310-
private static string StripKnownLevelPrefix(string text)
414+
private static (string Text, int TextStart) StripKnownLevelPrefix(string text)
311415
{
312416
var trimmed = text.TrimStart();
313-
if (TryReadBracketPrefix(trimmed, out var bracketLevel, out var afterBracket) &&
314-
TryParseLogLevel(bracketLevel, out _))
315-
return afterBracket.TrimStart();
316-
317-
return text;
417+
var trimmedStart = text.Length - trimmed.Length;
418+
if (!TryReadBracketPrefix(trimmed, out var bracketLevel, out var afterBracket) ||
419+
!TryParseLogLevel(bracketLevel, out _)) return (text, 0);
420+
var afterBracketStart = trimmedStart + trimmed.Length - afterBracket.Length;
421+
var afterBracketTrimmed = afterBracket.TrimStart();
422+
return (afterBracketTrimmed, afterBracketStart + afterBracket.Length - afterBracketTrimmed.Length);
318423
}
319424

320425
private static bool TryParseLogLevel(string value, out LogLevel level)
@@ -339,7 +444,7 @@ private static bool TryReadBracketPrefix(string text, out string value, out stri
339444
value = "";
340445
remaining = text;
341446

342-
if (!text.StartsWith("[", StringComparison.Ordinal))
447+
if (!text.StartsWith('['))
343448
return false;
344449

345450
var end = text.IndexOf(']', 1);

Diagnostics/Logging/RitsuDebugLogRecord.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Text.Json.Serialization;
2+
using STS2RitsuLib.Utils;
23

34
namespace STS2RitsuLib.Diagnostics.Logging
45
{
@@ -20,6 +21,8 @@ internal sealed record RitsuDebugLogRecord
2021

2122
[JsonPropertyName("body")] public string Body { get; init; } = "";
2223

24+
[JsonPropertyName("bodySegments")] public IReadOnlyList<RitsuTextSegment>? BodySegments { get; init; }
25+
2326
[JsonPropertyName("source")] public string? Source { get; init; }
2427

2528
[JsonPropertyName("category")] public string? Category { get; init; }

Diagnostics/Logging/RitsuDebugLogViewerOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ internal sealed record RitsuDebugLogViewerOptions(
44
bool Enabled,
55
bool MirrorGameLogs,
66
bool AutoOpen,
7+
bool LanAccessEnabled,
78
int Port,
89
int PortFallbackCount,
910
string AccessToken,

0 commit comments

Comments
 (0)