@@ -5,7 +5,6 @@ namespace Bloxstrap.Integrations
55 public class ActivityWatcher : IDisposable
66 {
77 private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]" ;
8- private const string StudioMessageEntry = "[FLog::Output] [FroststrapStudioRPC]" ;
98 private const string GameJoiningEntry = "[FLog::Output] ! Joining game" ;
109
1110 // these entries are technically volatile!
@@ -32,7 +31,6 @@ public class ActivityWatcher : IDisposable
3231 private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+" ;
3332 private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+" ;
3433 private const string GameMessageEntryPattern = @"\[BloxstrapRPC\] (.*)" ;
35- private const string StudioMessagePattern = @"\[FroststrapStudioRPC\] (.*)" ;
3634
3735 private int _logEntriesRead = 0 ;
3836 private bool _teleportMarker = false ;
@@ -64,6 +62,12 @@ public class ActivityWatcher : IDisposable
6462 public bool InStudioPlace = false ;
6563 public bool InRobloxStudio = false ;
6664
65+ private const int HttpPort = 4875 ;
66+ private HttpListener ? _httpListener ;
67+ private Thread ? _httpListenerThread ;
68+ private bool _isHttpListenerRunning = false ;
69+ private readonly CancellationTokenSource _httpCancellationTokenSource = new ( ) ;
70+
6771 public ActivityData Data { get ; private set ; } = new ( ) ;
6872
6973 /// <summary>
@@ -78,7 +82,7 @@ public ActivityWatcher(string? logFile = null, LaunchMode launchMode = LaunchMod
7882 if ( ! String . IsNullOrEmpty ( logFile ) )
7983 LogLocation = logFile ;
8084
81- _launchMode = launchMode ;
85+ _launchMode = launchMode ;
8286
8387 if ( _launchMode == LaunchMode . Studio || _launchMode == LaunchMode . StudioAuth )
8488 InRobloxStudio = true ;
@@ -208,6 +212,7 @@ private void ProcessStudioLogEntry(string logMessage)
208212 InStudioPlace = true ;
209213 App . Logger . WriteLine ( LOG_IDENT , "Studio place opened" ) ;
210214
215+ StartHTTPServer ( ) ;
211216 OnStudioPlaceOpened ? . Invoke ( this , EventArgs . Empty ) ;
212217 }
213218 }
@@ -218,74 +223,9 @@ private void ProcessStudioLogEntry(string logMessage)
218223 App . Logger . WriteLine ( LOG_IDENT , "Studio place closed" ) ;
219224 InStudioPlace = false ;
220225
226+ StopHTTPServer ( ) ;
221227 OnStudioPlaceClosed ? . Invoke ( this , EventArgs . Empty ) ;
222228 }
223- else if ( logMessage . StartsWith ( StudioMessageEntry ) )
224- {
225- var match = Regex . Match ( logMessage , StudioMessagePattern ) ;
226-
227- if ( match . Groups . Count != 2 )
228- {
229- App . Logger . WriteLine ( LOG_IDENT , "Failed to parse Studio RPC message" ) ;
230- return ;
231- }
232-
233- string studioMessage = match . Groups [ 1 ] . Value ;
234- App . Logger . WriteLine ( LOG_IDENT , $ "Studio RPC: { studioMessage } ") ;
235-
236- if ( studioMessage . Contains ( "| Workspace: Game" ) )
237- {
238- App . Logger . WriteLine ( LOG_IDENT , "Ignoring message because workspace is 'Game'" ) ;
239- return ;
240- }
241-
242- string workspace = "" ;
243- string activityState = studioMessage ;
244- bool testing = false ;
245- string scriptType = "developing" ;
246-
247- string [ ] parts = studioMessage . Split ( new [ ] { " | " } , StringSplitOptions . None ) ;
248-
249- foreach ( string part in parts )
250- {
251- if ( part . StartsWith ( "Workspace:" ) )
252- {
253- workspace = part . Substring ( 10 ) . Trim ( ) ;
254- }
255- else if ( part . StartsWith ( "Testing:" ) )
256- {
257- string testingStr = part . Substring ( 8 ) . Trim ( ) ;
258- testing = testingStr . Equals ( "True" , StringComparison . OrdinalIgnoreCase ) ;
259- }
260- else if ( part . StartsWith ( "Type:" ) )
261- {
262- scriptType = part . Substring ( 5 ) . Trim ( ) ;
263- }
264- else if ( ! part . Contains ( "Workspace:" ) && ! part . Contains ( "Testing:" ) && ! part . Contains ( "Type:" ) )
265- {
266- activityState = part . Trim ( ) ;
267- }
268- }
269-
270- var studioRpc = new StudioMessage
271- {
272- Data = new StudioRichPresence
273- {
274- Details = activityState ,
275- State = ! string . IsNullOrEmpty ( workspace ) ? $ "Workspace: { workspace } " : null ! ,
276- Testing = testing ,
277- ScriptType = scriptType
278- }
279- } ;
280-
281- string json = JsonSerializer . Serialize ( studioRpc ) ;
282- var rpcMessage = JsonSerializer . Deserialize < StudioMessage > ( json ) ;
283-
284- if ( rpcMessage != null )
285- {
286- OnStudioRPCMessage ? . Invoke ( this , rpcMessage ) ;
287- }
288- }
289229 }
290230 }
291231
@@ -595,6 +535,135 @@ private void ProcessPlayerLogEntry(string logMessage)
595535 }
596536 }
597537
538+ private void StartHTTPServer ( )
539+ {
540+ try
541+ {
542+ _httpListener = new HttpListener ( ) ;
543+
544+ _httpListener . Prefixes . Add ( $ "http://localhost:{ HttpPort } /") ;
545+
546+ _httpListener . Start ( ) ;
547+
548+ _isHttpListenerRunning = true ;
549+ _httpListenerThread = new Thread ( ( ) => ListenForHTTPRequests ( _httpCancellationTokenSource . Token ) ) ;
550+ _httpListenerThread . IsBackground = true ;
551+ _httpListenerThread . Name = "StudioRPC-HTTP-Listener" ;
552+ _httpListenerThread . Start ( ) ;
553+
554+ App . Logger . WriteLine ( "ActivityWatcher::StartHTTPServer" , $ "HTTP server started on port { HttpPort } ") ;
555+ }
556+ catch ( Exception ex )
557+ {
558+ App . Logger . WriteException ( "ActivityWatcher::StartHTTPServer" , ex ) ;
559+ }
560+ }
561+
562+ private async void ListenForHTTPRequests ( CancellationToken cancellationToken )
563+ {
564+ while ( _isHttpListenerRunning && _httpListener != null && _httpListener . IsListening && ! cancellationToken . IsCancellationRequested )
565+ {
566+ try
567+ {
568+ var context = await _httpListener . GetContextAsync ( ) . WaitAsync ( cancellationToken ) ;
569+ _ = Task . Run ( ( ) => ProcessHTTPRequest ( context ) , cancellationToken ) ;
570+ }
571+ catch ( HttpListenerException )
572+ {
573+ break ;
574+ }
575+ catch ( OperationCanceledException )
576+ {
577+ break ;
578+ }
579+ catch ( Exception ex )
580+ {
581+ App . Logger . WriteException ( "ActivityWatcher::ListenForHTTPRequests" , ex ) ;
582+ await Task . Delay ( 1000 , cancellationToken ) ;
583+ }
584+ }
585+ }
586+
587+ private void ProcessHTTPRequest ( HttpListenerContext context )
588+ {
589+ const string LOG_IDENT = "ActivityWatcher::ProcessHTTPRequest" ;
590+
591+ try
592+ {
593+ if ( context . Request . HttpMethod == "POST" && context . Request . Url ? . AbsolutePath == "/rpc" )
594+ {
595+ using ( var reader = new StreamReader ( context . Request . InputStream , Encoding . UTF8 ) )
596+ {
597+ string json = reader . ReadToEnd ( ) ;
598+ App . Logger . WriteLine ( LOG_IDENT , $ "Received HTTP RPC: { json } ") ;
599+
600+ var studioMessage = JsonSerializer . Deserialize < StudioMessage > ( json ) ;
601+
602+ if ( studioMessage != null )
603+ {
604+ if ( studioMessage . Data == null )
605+ {
606+ studioMessage . Data = new StudioRichPresence ( ) ;
607+ }
608+
609+ OnStudioRPCMessage ? . Invoke ( this , studioMessage ) ;
610+ }
611+ else
612+ {
613+ App . Logger . WriteLine ( LOG_IDENT , "Failed to parse JSON message" ) ;
614+ }
615+ }
616+
617+ context . Response . StatusCode = 200 ;
618+ context . Response . Close ( ) ;
619+ }
620+ else
621+ {
622+ context . Response . StatusCode = 404 ;
623+ context . Response . Close ( ) ;
624+ }
625+ }
626+ catch ( JsonException jsonEx )
627+ {
628+ App . Logger . WriteLine ( LOG_IDENT , $ "JSON parsing error: { jsonEx . Message } ") ;
629+ context . Response . StatusCode = 400 ;
630+ context . Response . Close ( ) ;
631+ }
632+ catch ( Exception ex )
633+ {
634+ App . Logger . WriteException ( LOG_IDENT , ex ) ;
635+ context . Response . StatusCode = 500 ;
636+ context . Response . Close ( ) ;
637+ }
638+ }
639+
640+ private void StopHTTPServer ( )
641+ {
642+ _isHttpListenerRunning = false ;
643+ _httpCancellationTokenSource . Cancel ( ) ;
644+
645+ if ( _httpListener != null )
646+ {
647+ try
648+ {
649+ _httpListener . Stop ( ) ;
650+ _httpListener . Close ( ) ;
651+ }
652+ catch ( Exception ex )
653+ {
654+ App . Logger . WriteException ( "ActivityWatcher::StopHTTPServer" , ex ) ;
655+ }
656+ _httpListener = null ;
657+ }
658+
659+ if ( _httpListenerThread != null && _httpListenerThread . IsAlive )
660+ {
661+ _httpListenerThread . Join ( 2000 ) ;
662+ }
663+
664+ _httpCancellationTokenSource . Dispose ( ) ;
665+ }
666+
598667 public void LoadGameHistory ( )
599668 {
600669 try
@@ -710,6 +779,7 @@ private void AddToHistory(ActivityData activity)
710779 public void Dispose ( )
711780 {
712781 IsDisposed = true ;
782+ StopHTTPServer ( ) ;
713783 GC . SuppressFinalize ( this ) ;
714784 }
715785 }
0 commit comments