22using System . Reflection ;
33using Godot ;
44using MegaCrit . Sts2 . Core . Logging ;
5+ using STS2RitsuLib . Utils ;
6+ using Environment = System . Environment ;
57
68namespace 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 ) ;
0 commit comments