2026-08-09 21:32:55 +08:00
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
using System.Threading.Tasks;
|
2026-08-08 14:07:56 +08:00
|
|
|
using System.Windows;
|
|
|
|
|
using System.Windows.Controls;
|
2026-08-09 21:32:55 +08:00
|
|
|
using Microsoft.Web.WebView2.Core;
|
|
|
|
|
using Microsoft.Web.WebView2.Wpf;
|
2026-08-08 14:07:56 +08:00
|
|
|
|
|
|
|
|
namespace MRCC.Launcher;
|
|
|
|
|
|
|
|
|
|
public partial class MainWindow : Window
|
|
|
|
|
{
|
2026-08-09 21:32:55 +08:00
|
|
|
private WebView2? _wv;
|
|
|
|
|
|
2026-08-08 14:07:56 +08:00
|
|
|
public MainWindow()
|
|
|
|
|
{
|
|
|
|
|
InitializeComponent();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 21:32:55 +08:00
|
|
|
private async void OnWindowLoaded(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var gameDir = FindGamePath();
|
|
|
|
|
if (gameDir == null)
|
|
|
|
|
{
|
|
|
|
|
Content = new TextBlock { Text = "游戏文件未找到", Foreground = System.Windows.Media.Brushes.White, FontSize = 14, Margin = new Thickness(20) };
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var port = GameServer.Start(gameDir);
|
|
|
|
|
var url = $"http://localhost:{port}/voxel-world.html";
|
|
|
|
|
|
|
|
|
|
_wv = new WebView2();
|
|
|
|
|
Content = _wv;
|
|
|
|
|
|
|
|
|
|
await _wv.EnsureCoreWebView2Async();
|
|
|
|
|
_wv.CoreWebView2.Settings.IsWebMessageEnabled = true;
|
|
|
|
|
_wv.CoreWebView2.WebMessageReceived += OnWebMessage;
|
|
|
|
|
|
|
|
|
|
_wv.CoreWebView2.Navigate(url);
|
|
|
|
|
Program.Log($"Launcher UI loaded: {url}");
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Program.Log($"Launcher load failed: {ex}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnWebMessage(object? sender, CoreWebView2WebMessageReceivedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// WebView2 的 postMessage 会 JSON 编码,需先解一层
|
|
|
|
|
var rawJson = e.TryGetWebMessageAsString();
|
|
|
|
|
if (string.IsNullOrEmpty(rawJson)) return;
|
|
|
|
|
using var doc = JsonDocument.Parse(rawJson);
|
|
|
|
|
var root = doc.RootElement;
|
|
|
|
|
if (!root.TryGetProperty("action", out var action) || action.GetString() != "launch_game") return;
|
|
|
|
|
|
|
|
|
|
var username = "Player";
|
|
|
|
|
if (root.TryGetProperty("username", out var u) && u.ValueKind == JsonValueKind.String)
|
|
|
|
|
username = u.GetString()!;
|
|
|
|
|
// 不再启动 Luanti —— HTML5 世界直接在 WebView2 内运行
|
|
|
|
|
Program.Log($"Game launch requested by: {username}");
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex) { Program.Log($"WebMessage error: {ex}"); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string? FindGamePath()
|
|
|
|
|
{
|
|
|
|
|
var exeDir = AppContext.BaseDirectory;
|
|
|
|
|
foreach (var p in new[] { "game", "../game", "../../../prototype", "../../prototype", "../prototype" })
|
|
|
|
|
{
|
|
|
|
|
var full = Path.GetFullPath(Path.Combine(exeDir, p));
|
|
|
|
|
if (Directory.Exists(full) && File.Exists(Path.Combine(full, "voxel-world.html")))
|
|
|
|
|
return full;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HTTP server for static files
|
|
|
|
|
public static class GameServer
|
|
|
|
|
{
|
|
|
|
|
private static HttpListener? _listener;
|
|
|
|
|
private static CancellationTokenSource? _cts;
|
|
|
|
|
private static string _gameDir = "";
|
|
|
|
|
|
|
|
|
|
public static int Start(string gameDir)
|
|
|
|
|
{
|
|
|
|
|
_gameDir = gameDir;
|
|
|
|
|
var port = FindFreePort();
|
|
|
|
|
_cts = new CancellationTokenSource();
|
|
|
|
|
_listener = new HttpListener();
|
|
|
|
|
_listener.Prefixes.Add($"http://localhost:{port}/");
|
|
|
|
|
_listener.Start();
|
|
|
|
|
Task.Run(() => ListenLoop(_cts.Token));
|
|
|
|
|
return port;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void Stop()
|
|
|
|
|
{
|
|
|
|
|
_cts?.Cancel();
|
|
|
|
|
try { _listener?.Stop(); } catch { }
|
|
|
|
|
try { _listener?.Close(); } catch { }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task ListenLoop(CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
while (!ct.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
try { var ctx = await _listener!.GetContextAsync().WaitAsync(ct); _ = Task.Run(() => Handle(ctx)); }
|
|
|
|
|
catch (OperationCanceledException) { break; }
|
|
|
|
|
catch (HttpListenerException) { break; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task Handle(HttpListenerContext ctx)
|
2026-08-08 14:07:56 +08:00
|
|
|
{
|
2026-08-09 21:32:55 +08:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var path = ctx.Request.Url!.AbsolutePath.TrimStart('/');
|
|
|
|
|
if (string.IsNullOrEmpty(path)) path = "voxel-world.html";
|
|
|
|
|
var fp = Path.GetFullPath(Path.Combine(_gameDir, path));
|
|
|
|
|
if (!fp.StartsWith(_gameDir, StringComparison.OrdinalIgnoreCase) || !File.Exists(fp))
|
|
|
|
|
{ ctx.Response.StatusCode = 404; ctx.Response.Close(); return; }
|
2026-08-08 14:07:56 +08:00
|
|
|
|
2026-08-09 21:32:55 +08:00
|
|
|
var ct = Path.GetExtension(fp).ToLowerInvariant() switch
|
|
|
|
|
{
|
|
|
|
|
".html" => "text/html; charset=utf-8", ".css" => "text/css",
|
|
|
|
|
".js" => "application/javascript", ".json" => "application/json",
|
|
|
|
|
".png" => "image/png", ".jpg" or ".jpeg" => "image/jpeg",
|
|
|
|
|
".svg" => "image/svg+xml", _ => "application/octet-stream"
|
|
|
|
|
};
|
|
|
|
|
var bytes = await File.ReadAllBytesAsync(fp);
|
|
|
|
|
ctx.Response.ContentType = ct;
|
|
|
|
|
ctx.Response.ContentLength64 = bytes.Length;
|
|
|
|
|
ctx.Response.Headers.Add("Access-Control-Allow-Origin", "*");
|
|
|
|
|
ctx.Response.Headers.Add("Cache-Control", "no-cache");
|
|
|
|
|
await ctx.Response.OutputStream.WriteAsync(bytes);
|
|
|
|
|
ctx.Response.Close();
|
|
|
|
|
}
|
|
|
|
|
catch { try { ctx.Response.StatusCode = 500; ctx.Response.Close(); } catch { } }
|
2026-08-08 14:07:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-09 21:32:55 +08:00
|
|
|
private static int FindFreePort()
|
2026-08-08 14:07:56 +08:00
|
|
|
{
|
2026-08-09 21:32:55 +08:00
|
|
|
var l = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
|
|
|
|
|
l.Start(); var port = ((System.Net.IPEndPoint)l.LocalEndpoint).Port; l.Stop();
|
|
|
|
|
return port;
|
2026-08-08 14:07:56 +08:00
|
|
|
}
|
|
|
|
|
}
|