From 561c5cc7358dfc57c33dd23e67309de2dfbd1665 Mon Sep 17 00:00:00 2001 From: Natalie Bunduwongse Date: Thu, 8 Jan 2026 08:58:43 +1300 Subject: [PATCH 1/3] feat: windows game bridge server --- .../Runtime/Scripts/Private/Uwb/UwbWebView.cs | 8 +- .../ImmutableBrowserCore/GameBridge.cs | 10 + .../ImmutableBrowserCore/GameBridgeServer.cs | 172 ++++++++++++++++++ .../GameBridgeServer.cs.meta | 11 ++ .../WindowsWebBrowserClientAdapter.cs | 11 +- 5 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs create mode 100644 src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs.meta diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs b/src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs index efd09a06f..9213efe1e 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs @@ -32,6 +32,7 @@ public class UwbWebView : MonoBehaviour, IWebBrowserClient #endif private WebBrowserClient? webBrowserClient; + private GameBridgeServer? gameBridgeServer; public async UniTask Init(int engineStartupTimeoutMs, bool redactTokensInLogs, Func redactionHandler) { @@ -63,8 +64,9 @@ public async UniTask Init(int engineStartupTimeoutMs, bool redactTokensInLogs, F var browserEngineMainDir = WebBrowserUtils.GetAdditionFilesDirectory(); browserClient.CachePath = new FileInfo(Path.Combine(browserEngineMainDir, "ImmutableSDK/UWBCache")); - // Game bridge path - browserClient.initialUrl = GameBridge.GetFilePath(); + // Start local HTTP server to serve index.html + gameBridgeServer = new GameBridgeServer(GameBridge.GetFileSystemPath()); + browserClient.initialUrl = gameBridgeServer.Start(); // Set up engine from standard UWB configuration asset var engineConfigAsset = Resources.Load("Cef Engine Configuration"); @@ -142,6 +144,8 @@ public void Dispose() if (webBrowserClient?.HasDisposed == true) return; webBrowserClient?.Dispose(); + gameBridgeServer?.Dispose(); + gameBridgeServer = null; } #endif } diff --git a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridge.cs b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridge.cs index 3e948812c..3fd11fab6 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridge.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridge.cs @@ -41,5 +41,15 @@ public static string GetFilePath() filePath = filePath.Replace(" ", "%20"); return filePath; } + + /// + /// Gets the file system path to index.html (without file:// scheme or URL encoding). + /// + public static string GetFileSystemPath() + { + return GetFilePath() + .Replace(SCHEME_FILE, "") + .Replace("%20", " "); + } } } \ No newline at end of file diff --git a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs new file mode 100644 index 000000000..32f69cf60 --- /dev/null +++ b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs @@ -0,0 +1,172 @@ +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using UnityEngine; + +namespace Immutable.Browser.Core +{ + /// + /// A lightweight HTTP server to serve the GameBridge index.html locally. + /// This provides a proper origin (http://localhost:PORT) instead of null origin from file:// URLs. + /// + public class GameBridgeServer : IDisposable + { + private const string TAG = "[Game Bridge Server]"; + private const int MIN_PORT = 49152; + private const int MAX_PORT = 65535; + private const int MAX_PORT_ATTEMPTS = 100; + + private HttpListener? _listener; + private Thread? _listenerThread; + private byte[]? _indexHtmlContent; + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + private bool _disposed; + + public int Port { get; private set; } + + /// + /// Creates a new GameBridgeServer instance. + /// + /// The file system path to the index.html file. + public GameBridgeServer(string indexHtmlPath) + { + if (string.IsNullOrEmpty(indexHtmlPath)) + throw new ArgumentNullException(nameof(indexHtmlPath)); + + if (!File.Exists(indexHtmlPath)) + throw new FileNotFoundException($"{TAG} index.html not found: {indexHtmlPath}"); + + // Cache the file content at startup + _indexHtmlContent = File.ReadAllBytes(indexHtmlPath); + Debug.Log($"{TAG} Loaded index.html ({_indexHtmlContent.Length} bytes)"); + } + + /// + /// Starts the HTTP server on an available port. + /// + /// The URL to the index.html file. + public string Start() + { + if (_disposed) + throw new ObjectDisposedException(nameof(GameBridgeServer)); + + if (_listener?.IsListening == true) + return $"http://localhost:{Port}/"; + + Port = FindAvailablePort(); + + _listener = new HttpListener(); + _listener.Prefixes.Add($"http://localhost:{Port}/"); + _listener.Start(); + + Debug.Log($"{TAG} Started on http://localhost:{Port}/"); + + _listenerThread = new Thread(ListenerLoop) + { + Name = "GameBridgeServer", + IsBackground = true + }; + _listenerThread.Start(); + + return $"http://localhost:{Port}/"; + } + + private void ListenerLoop() + { + while (!_cts.Token.IsCancellationRequested && _listener?.IsListening == true) + { + try + { + var context = _listener.GetContext(); + HandleRequest(context); + } + catch (HttpListenerException) when (_cts.Token.IsCancellationRequested) + { + break; + } + catch (ObjectDisposedException) + { + break; + } + catch (Exception ex) + { + if (!_cts.Token.IsCancellationRequested) + Debug.LogError($"{TAG} Error: {ex.Message}"); + } + } + } + + private void HandleRequest(HttpListenerContext context) + { + var response = context.Response; + try + { + response.StatusCode = 200; + response.ContentType = "text/html; charset=utf-8"; + response.ContentLength64 = _indexHtmlContent!.Length; + response.OutputStream.Write(_indexHtmlContent, 0, _indexHtmlContent.Length); + } + catch (Exception ex) + { + Debug.LogError($"{TAG} Error handling request: {ex.Message}"); + } + finally + { + try { response.Close(); } catch { } + } + } + + private int FindAvailablePort() + { + var random = new System.Random(); + for (int attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) + { + int port = random.Next(MIN_PORT, MAX_PORT); + if (IsPortAvailable(port)) + return port; + } + throw new InvalidOperationException($"{TAG} Could not find an available port"); + } + + private bool IsPortAvailable(int port) + { + try + { + var listener = new TcpListener(IPAddress.Loopback, port); + listener.Start(); + listener.Stop(); + return true; + } + catch + { + return false; + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + _cts.Cancel(); + try + { + _listener?.Stop(); + _listener?.Close(); + } + catch { } + + _listenerThread?.Join(TimeSpan.FromSeconds(1)); + _cts.Dispose(); + _indexHtmlContent = null; + + Debug.Log($"{TAG} Stopped"); + } + } +} + +#endif diff --git a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs.meta b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs.meta new file mode 100644 index 000000000..eaf5251c1 --- /dev/null +++ b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ab2317c6b7a6cea4391c77dae3a5deb7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/WindowsWebBrowserClientAdapter.cs b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/WindowsWebBrowserClientAdapter.cs index efa949343..5501b0572 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/WindowsWebBrowserClientAdapter.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/WindowsWebBrowserClientAdapter.cs @@ -1,8 +1,9 @@ #if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) -using System.IO; +using System.Net.Sockets; using UnityEngine; using Immutable.Browser.Core; +using Immutable.Passport; using Immutable.Passport.Core.Logging; using Cysharp.Threading.Tasks; @@ -13,6 +14,7 @@ public class WindowsWebBrowserClientAdapter : IWebBrowserClient public event OnUnityPostMessageDelegate OnUnityPostMessage; private readonly IWindowsWebBrowserClient webBrowserClient; + private GameBridgeServer? gameBridgeServer; public WindowsWebBrowserClientAdapter(IWindowsWebBrowserClient windowsWebBrowserClient) { @@ -33,8 +35,11 @@ public async UniTask Init() // Initialise the web browser client asynchronously await webBrowserClient.Init(); + // Start local HTTP server to serve index.html + gameBridgeServer = new GameBridgeServer(GameBridge.GetFileSystemPath()); + // Load the game bridge file into the web browser client - webBrowserClient.LoadUrl(GameBridge.GetFilePath()); + webBrowserClient.LoadUrl(gameBridgeServer.Start()); // Get the JavaScript API call for posting messages from the web page to the Unity application string postMessageApiCall = webBrowserClient.GetPostMessageApiCall(); @@ -59,6 +64,8 @@ public void LaunchAuthURL(string url, string? redirectUri) public void Dispose() { webBrowserClient.Dispose(); + gameBridgeServer?.Dispose(); + gameBridgeServer = null; } } } From 16fdd89dc8c90aedcff75c2d489e3710224375b4 Mon Sep 17 00:00:00 2001 From: Natalie Bunduwongse Date: Thu, 8 Jan 2026 09:56:30 +1300 Subject: [PATCH 2/3] fix: uwb webview compilation flags --- src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs b/src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs index 6f143e3d7..d1f8d77d5 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs @@ -1,4 +1,4 @@ -#if UWB_WEBVIEW && !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)) +#if UWB_WEBVIEW && !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)) using System; using System.IO; From 66964e88206a3d724c45603afaaf03a512e1f2a8 Mon Sep 17 00:00:00 2001 From: Natalie Bunduwongse Date: Thu, 8 Jan 2026 15:56:32 +1300 Subject: [PATCH 3/3] feat: use fixed port for game bridge server --- .../ImmutableBrowserCore/GameBridgeServer.cs | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs index 32f69cf60..e0bb53c6b 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs @@ -10,15 +10,15 @@ namespace Immutable.Browser.Core { /// - /// A lightweight HTTP server to serve the GameBridge index.html locally. - /// This provides a proper origin (http://localhost:PORT) instead of null origin from file:// URLs. + /// Local HTTP server for index.html to provide a proper origin instead of null from file:// URLs. /// public class GameBridgeServer : IDisposable { private const string TAG = "[Game Bridge Server]"; - private const int MIN_PORT = 49152; - private const int MAX_PORT = 65535; - private const int MAX_PORT_ATTEMPTS = 100; + + // Fixed port to maintain consistent origin for localStorage/IndexedDB persistence + private const int PORT = 51990; + private static readonly string URL = "http://localhost:" + PORT + "/"; private HttpListener? _listener; private Thread? _listenerThread; @@ -26,8 +26,6 @@ public class GameBridgeServer : IDisposable private readonly CancellationTokenSource _cts = new CancellationTokenSource(); private bool _disposed; - public int Port { get; private set; } - /// /// Creates a new GameBridgeServer instance. /// @@ -40,13 +38,12 @@ public GameBridgeServer(string indexHtmlPath) if (!File.Exists(indexHtmlPath)) throw new FileNotFoundException($"{TAG} index.html not found: {indexHtmlPath}"); - // Cache the file content at startup _indexHtmlContent = File.ReadAllBytes(indexHtmlPath); Debug.Log($"{TAG} Loaded index.html ({_indexHtmlContent.Length} bytes)"); } /// - /// Starts the HTTP server on an available port. + /// Starts the game bridge server. /// /// The URL to the index.html file. public string Start() @@ -55,15 +52,15 @@ public string Start() throw new ObjectDisposedException(nameof(GameBridgeServer)); if (_listener?.IsListening == true) - return $"http://localhost:{Port}/"; + return URL; - Port = FindAvailablePort(); + EnsurePortAvailable(); _listener = new HttpListener(); - _listener.Prefixes.Add($"http://localhost:{Port}/"); + _listener.Prefixes.Add(URL); _listener.Start(); - Debug.Log($"{TAG} Started on http://localhost:{Port}/"); + Debug.Log($"{TAG} Started on {URL}"); _listenerThread = new Thread(ListenerLoop) { @@ -72,7 +69,7 @@ public string Start() }; _listenerThread.Start(); - return $"http://localhost:{Port}/"; + return URL; } private void ListenerLoop() @@ -120,16 +117,14 @@ private void HandleRequest(HttpListenerContext context) } } - private int FindAvailablePort() + private void EnsurePortAvailable() { - var random = new System.Random(); - for (int attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) + if (!IsPortAvailable(PORT)) { - int port = random.Next(MIN_PORT, MAX_PORT); - if (IsPortAvailable(port)) - return port; + throw new InvalidOperationException( + $"{TAG} Port {PORT} is already in use. " + + "Please close any application using this port to ensure localStorage/IndexedDB data persists correctly."); } - throw new InvalidOperationException($"{TAG} Could not find an available port"); } private bool IsPortAvailable(int port)