Skip to content

Commit 437c0f4

Browse files
authored
Merge pull request #646 from immutable/feat/windows-game-bridge
feat: windows game bridge server
2 parents 8a164c3 + 66964e8 commit 437c0f4

5 files changed

Lines changed: 204 additions & 5 deletions

File tree

src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#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))
1+
#if UWB_WEBVIEW && !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN))
22

33
using System;
44
using System.IO;
@@ -32,6 +32,7 @@ public class UwbWebView : MonoBehaviour, IWebBrowserClient
3232
#endif
3333

3434
private WebBrowserClient? webBrowserClient;
35+
private GameBridgeServer? gameBridgeServer;
3536

3637
public async UniTask Init(int engineStartupTimeoutMs, bool redactTokensInLogs, Func<string, string> redactionHandler)
3738
{
@@ -66,8 +67,9 @@ public async UniTask Init(int engineStartupTimeoutMs, bool redactTokensInLogs, F
6667
var browserEngineMainDir = WebBrowserUtils.GetAdditionFilesDirectory();
6768
browserClient.CachePath = new FileInfo(Path.Combine(browserEngineMainDir, "ImmutableSDK/UWBCache"));
6869

69-
// Game bridge path
70-
browserClient.initialUrl = GameBridge.GetFilePath();
70+
// Start local HTTP server to serve index.html
71+
gameBridgeServer = new GameBridgeServer(GameBridge.GetFileSystemPath());
72+
browserClient.initialUrl = gameBridgeServer.Start();
7173

7274
// Set up engine from standard UWB configuration asset
7375
var engineConfigAsset = Resources.Load<EngineConfiguration>("Cef Engine Configuration");
@@ -145,6 +147,8 @@ public void Dispose()
145147
if (webBrowserClient?.HasDisposed == true) return;
146148

147149
webBrowserClient?.Dispose();
150+
gameBridgeServer?.Dispose();
151+
gameBridgeServer = null;
148152
}
149153
#endif
150154
}

src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridge.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,15 @@ public static string GetFilePath()
4141
filePath = filePath.Replace(" ", "%20");
4242
return filePath;
4343
}
44+
45+
/// <summary>
46+
/// Gets the file system path to index.html (without file:// scheme or URL encoding).
47+
/// </summary>
48+
public static string GetFileSystemPath()
49+
{
50+
return GetFilePath()
51+
.Replace(SCHEME_FILE, "")
52+
.Replace("%20", " ");
53+
}
4454
}
4555
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)
2+
3+
using System;
4+
using System.IO;
5+
using System.Net;
6+
using System.Net.Sockets;
7+
using System.Threading;
8+
using UnityEngine;
9+
10+
namespace Immutable.Browser.Core
11+
{
12+
/// <summary>
13+
/// Local HTTP server for index.html to provide a proper origin instead of null from file:// URLs.
14+
/// </summary>
15+
public class GameBridgeServer : IDisposable
16+
{
17+
private const string TAG = "[Game Bridge Server]";
18+
19+
// Fixed port to maintain consistent origin for localStorage/IndexedDB persistence
20+
private const int PORT = 51990;
21+
private static readonly string URL = "http://localhost:" + PORT + "/";
22+
23+
private HttpListener? _listener;
24+
private Thread? _listenerThread;
25+
private byte[]? _indexHtmlContent;
26+
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
27+
private bool _disposed;
28+
29+
/// <summary>
30+
/// Creates a new GameBridgeServer instance.
31+
/// </summary>
32+
/// <param name="indexHtmlPath">The file system path to the index.html file.</param>
33+
public GameBridgeServer(string indexHtmlPath)
34+
{
35+
if (string.IsNullOrEmpty(indexHtmlPath))
36+
throw new ArgumentNullException(nameof(indexHtmlPath));
37+
38+
if (!File.Exists(indexHtmlPath))
39+
throw new FileNotFoundException($"{TAG} index.html not found: {indexHtmlPath}");
40+
41+
_indexHtmlContent = File.ReadAllBytes(indexHtmlPath);
42+
Debug.Log($"{TAG} Loaded index.html ({_indexHtmlContent.Length} bytes)");
43+
}
44+
45+
/// <summary>
46+
/// Starts the game bridge server.
47+
/// </summary>
48+
/// <returns>The URL to the index.html file.</returns>
49+
public string Start()
50+
{
51+
if (_disposed)
52+
throw new ObjectDisposedException(nameof(GameBridgeServer));
53+
54+
if (_listener?.IsListening == true)
55+
return URL;
56+
57+
EnsurePortAvailable();
58+
59+
_listener = new HttpListener();
60+
_listener.Prefixes.Add(URL);
61+
_listener.Start();
62+
63+
Debug.Log($"{TAG} Started on {URL}");
64+
65+
_listenerThread = new Thread(ListenerLoop)
66+
{
67+
Name = "GameBridgeServer",
68+
IsBackground = true
69+
};
70+
_listenerThread.Start();
71+
72+
return URL;
73+
}
74+
75+
private void ListenerLoop()
76+
{
77+
while (!_cts.Token.IsCancellationRequested && _listener?.IsListening == true)
78+
{
79+
try
80+
{
81+
var context = _listener.GetContext();
82+
HandleRequest(context);
83+
}
84+
catch (HttpListenerException) when (_cts.Token.IsCancellationRequested)
85+
{
86+
break;
87+
}
88+
catch (ObjectDisposedException)
89+
{
90+
break;
91+
}
92+
catch (Exception ex)
93+
{
94+
if (!_cts.Token.IsCancellationRequested)
95+
Debug.LogError($"{TAG} Error: {ex.Message}");
96+
}
97+
}
98+
}
99+
100+
private void HandleRequest(HttpListenerContext context)
101+
{
102+
var response = context.Response;
103+
try
104+
{
105+
response.StatusCode = 200;
106+
response.ContentType = "text/html; charset=utf-8";
107+
response.ContentLength64 = _indexHtmlContent!.Length;
108+
response.OutputStream.Write(_indexHtmlContent, 0, _indexHtmlContent.Length);
109+
}
110+
catch (Exception ex)
111+
{
112+
Debug.LogError($"{TAG} Error handling request: {ex.Message}");
113+
}
114+
finally
115+
{
116+
try { response.Close(); } catch { }
117+
}
118+
}
119+
120+
private void EnsurePortAvailable()
121+
{
122+
if (!IsPortAvailable(PORT))
123+
{
124+
throw new InvalidOperationException(
125+
$"{TAG} Port {PORT} is already in use. " +
126+
"Please close any application using this port to ensure localStorage/IndexedDB data persists correctly.");
127+
}
128+
}
129+
130+
private bool IsPortAvailable(int port)
131+
{
132+
try
133+
{
134+
var listener = new TcpListener(IPAddress.Loopback, port);
135+
listener.Start();
136+
listener.Stop();
137+
return true;
138+
}
139+
catch
140+
{
141+
return false;
142+
}
143+
}
144+
145+
public void Dispose()
146+
{
147+
if (_disposed) return;
148+
_disposed = true;
149+
150+
_cts.Cancel();
151+
try
152+
{
153+
_listener?.Stop();
154+
_listener?.Close();
155+
}
156+
catch { }
157+
158+
_listenerThread?.Join(TimeSpan.FromSeconds(1));
159+
_cts.Dispose();
160+
_indexHtmlContent = null;
161+
162+
Debug.Log($"{TAG} Stopped");
163+
}
164+
}
165+
}
166+
167+
#endif

src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/WindowsWebBrowserClientAdapter.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)
22

3-
using System.IO;
3+
using System.Net.Sockets;
44
using UnityEngine;
55
using Immutable.Browser.Core;
6+
using Immutable.Passport;
67
using Immutable.Passport.Core.Logging;
78
using Cysharp.Threading.Tasks;
89

@@ -13,6 +14,7 @@ public class WindowsWebBrowserClientAdapter : IWebBrowserClient
1314
public event OnUnityPostMessageDelegate OnUnityPostMessage;
1415

1516
private readonly IWindowsWebBrowserClient webBrowserClient;
17+
private GameBridgeServer? gameBridgeServer;
1618

1719
public WindowsWebBrowserClientAdapter(IWindowsWebBrowserClient windowsWebBrowserClient)
1820
{
@@ -33,8 +35,11 @@ public async UniTask Init()
3335
// Initialise the web browser client asynchronously
3436
await webBrowserClient.Init();
3537

38+
// Start local HTTP server to serve index.html
39+
gameBridgeServer = new GameBridgeServer(GameBridge.GetFileSystemPath());
40+
3641
// Load the game bridge file into the web browser client
37-
webBrowserClient.LoadUrl(GameBridge.GetFilePath());
42+
webBrowserClient.LoadUrl(gameBridgeServer.Start());
3843

3944
// Get the JavaScript API call for posting messages from the web page to the Unity application
4045
string postMessageApiCall = webBrowserClient.GetPostMessageApiCall();
@@ -59,6 +64,8 @@ public void LaunchAuthURL(string url, string? redirectUri)
5964
public void Dispose()
6065
{
6166
webBrowserClient.Dispose();
67+
gameBridgeServer?.Dispose();
68+
gameBridgeServer = null;
6269
}
6370
}
6471
}

0 commit comments

Comments
 (0)