using System.Diagnostics;
using System.Text;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Native;
public sealed record NativeBridgeDescription(int ProtocolVersion, int ProcessId, string ImageSha256, IReadOnlyList<string> Capabilities, string Status);
/// <summary>Offline research only. Compiled into the test harness, never into the shipping application.</summary>
public sealed class NativeBridgeClient : IAsyncDisposable
{
private readonly SemaphoreSlim gate = new(1, 1);
private Process? process;
public NativeBridgeDescription? Description { get; private set; }
public bool Connected => process is { HasExited: false };
public event Action<string>? Diagnostic;
public async Task ConnectAsync(string python, string hostScript, GameEnvironment environment, CancellationToken token = default)
{
if (environment.ProfileMode != "isolated-offline") throw new InvalidOperationException("原生研究仅限已识别的隔离离线副本,禁止连接在线客户端。当前发布版只使用文件与公共行情读取。");
if (environment.ProcessId is null || environment.ImageSha256.Length != 64) throw new InvalidOperationException("无法唯一识别游戏进程及映像版本。");
if (Connected && Description?.ProcessId == environment.ProcessId && Description.ImageSha256 == environment.ImageSha256) return;
if (process != null) await DisposeAsync();
var start = new ProcessStartInfo(python)
{
UseShellExecute = false, CreateNoWindow = true, RedirectStandardInput = true, RedirectStandardOutput = true,
RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8
};
foreach (var arg in new[] { "-u", hostScript, "--pid", environment.ProcessId.Value.ToString(), "--image", environment.Executable, "--sha256", environment.ImageSha256 }) start.ArgumentList.Add(arg);
start.Environment["PYTHONIOENCODING"] = "utf-8";
process = Process.Start(start) ?? throw new InvalidOperationException("原生桥接进程无法启动。");
process.ErrorDataReceived += (_, e) => { if (e.Data is { Length: > 0 } text) Diagnostic?.Invoke(text[..Math.Min(300, text.Length)]); };
process.BeginErrorReadLine();
try
{
var result = await CallAsync("describe", null, token);
Description = JsonSerializer.Deserialize<NativeBridgeDescription>(result.GetRawText(), ToolboxSettings.JsonOptions) ?? throw new InvalidDataException("原生桥接未返回版本信息。");
if (Description.ProtocolVersion != 1 || Description.ProcessId != environment.ProcessId || Description.ImageSha256 != environment.ImageSha256) throw new InvalidDataException("原生桥接身份或协议版本不匹配。");
}
catch { await DisposeAsync(); throw; }
}
public async Task<JsonElement> CallAsync(string method, object? parameters = null, CancellationToken token = default)
{
await gate.WaitAsync(token);
try
{
if (!Connected) throw new InvalidOperationException("原生桥接未连接。");
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(token); timeout.CancelAfter(TimeSpan.FromSeconds(12));
var id = Guid.NewGuid().ToString("N");
var request = JsonSerializer.Serialize(new { id, method, parameters }, ToolboxSettings.JsonOptions).ReplaceLineEndings("");
await process!.StandardInput.WriteLineAsync(request.AsMemory(), timeout.Token); await process.StandardInput.FlushAsync(timeout.Token);
var line = await process.StandardOutput.ReadLineAsync(timeout.Token) ?? throw new IOException("原生桥接已断开。");
using var doc = JsonDocument.Parse(line);
var root = doc.RootElement;
if (root.Text("id") != id) throw new InvalidDataException("原生指令关联编号不匹配;不自动重试交易。");
if (!root.Flag("ok")) throw new InvalidOperationException(root.Text("error", "原生指令被拒绝。"));
return root.Get("result").Clone();
}
finally { gate.Release(); }
}
public async ValueTask DisposeAsync()
{
if (process == null) return;
try { if (!process.HasExited) { process.StandardInput.Close(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3)); await process.WaitForExitAsync(timeout.Token); } }
catch (OperationCanceledException) { if (!process.HasExited) process.Kill(true); }
finally { process.Dispose(); process = null; Description = null; }
}
}
public sealed class NativeTradeDriver(NativeBridgeClient bridge) : ITradeCommandDriver
{
public Task PreflightAsync(CancellationToken token)
{
var description = bridge.Description ?? throw new InvalidOperationException("请先连接原生桥接。");
var missing = new[] { "trade.buy", "trade.sell", "navigation.travel", "station.services" }.Except(description.Capabilities).ToArray();
if (missing.Length > 0) throw new InvalidOperationException("原生入口尚未验证:" + string.Join("、", missing) + "。观察接入不能作为自动跑商已完成的证明。");
return Task.CompletedTask;
}
public async Task ServiceDockedAsync(DockedServiceRequest request,Func<CancellationToken,Task> beforeInput,CancellationToken token)
{
await beforeInput(token);
await bridge.CallAsync("station.services",new{marketId=request.MarketId,reservedCargo=request.ReservedCargo},token);
}
public async Task ExecuteAsync(TradeAction action, Func<CancellationToken, Task> beforeInput, CancellationToken token)
{
await beforeInput(token);
var method = action.Kind switch { TradeActionKind.Buy => "trade.buy", TradeActionKind.Sell => "trade.sell", _ => "navigation.travel" };
await bridge.CallAsync(method, new { sourceMarketId = action.Leg.From.MarketId, destinationMarketId = action.Leg.To.MarketId, system = action.Leg.To.SystemName, commodity = action.Order?.Commodity, quantity = action.Order?.Quantity, expectedPrice = action.Kind == TradeActionKind.Buy ? action.Order?.BuyPrice : action.Order?.SellPrice }, token);
}
public async Task ReleaseAllAsync()
{
if (bridge.Connected) try { await bridge.CallAsync("commands.cancel"); } catch (IOException) { } catch (InvalidOperationException) { }
}
}
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Native;
public sealed record NativeBridgeDescription(int ProtocolVersion, int ProcessId, string ImageSha256, IReadOnlyList<string> Capabilities, string Status);
/// <summary>Offline research only. Compiled into the test harness, never into the shipping application.</summary>
public sealed class NativeBridgeClient : IAsyncDisposable
{
private readonly SemaphoreSlim gate = new(1, 1);
private Process? process;
public NativeBridgeDescription? Description { get; private set; }
public bool Connected => process is { HasExited: false };
public event Action<string>? Diagnostic;
public async Task ConnectAsync(string python, string hostScript, GameEnvironment environment, CancellationToken token = default)
{
if (environment.ProfileMode != "isolated-offline") throw new InvalidOperationException("原生研究仅限已识别的隔离离线副本,禁止连接在线客户端。当前发布版只使用文件与公共行情读取。");
if (environment.ProcessId is null || environment.ImageSha256.Length != 64) throw new InvalidOperationException("无法唯一识别游戏进程及映像版本。");
if (Connected && Description?.ProcessId == environment.ProcessId && Description.ImageSha256 == environment.ImageSha256) return;
if (process != null) await DisposeAsync();
var start = new ProcessStartInfo(python)
{
UseShellExecute = false, CreateNoWindow = true, RedirectStandardInput = true, RedirectStandardOutput = true,
RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8
};
foreach (var arg in new[] { "-u", hostScript, "--pid", environment.ProcessId.Value.ToString(), "--image", environment.Executable, "--sha256", environment.ImageSha256 }) start.ArgumentList.Add(arg);
start.Environment["PYTHONIOENCODING"] = "utf-8";
process = Process.Start(start) ?? throw new InvalidOperationException("原生桥接进程无法启动。");
process.ErrorDataReceived += (_, e) => { if (e.Data is { Length: > 0 } text) Diagnostic?.Invoke(text[..Math.Min(300, text.Length)]); };
process.BeginErrorReadLine();
try
{
var result = await CallAsync("describe", null, token);
Description = JsonSerializer.Deserialize<NativeBridgeDescription>(result.GetRawText(), ToolboxSettings.JsonOptions) ?? throw new InvalidDataException("原生桥接未返回版本信息。");
if (Description.ProtocolVersion != 1 || Description.ProcessId != environment.ProcessId || Description.ImageSha256 != environment.ImageSha256) throw new InvalidDataException("原生桥接身份或协议版本不匹配。");
}
catch { await DisposeAsync(); throw; }
}
public async Task<JsonElement> CallAsync(string method, object? parameters = null, CancellationToken token = default)
{
await gate.WaitAsync(token);
try
{
if (!Connected) throw new InvalidOperationException("原生桥接未连接。");
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(token); timeout.CancelAfter(TimeSpan.FromSeconds(12));
var id = Guid.NewGuid().ToString("N");
var request = JsonSerializer.Serialize(new { id, method, parameters }, ToolboxSettings.JsonOptions).ReplaceLineEndings("");
await process!.StandardInput.WriteLineAsync(request.AsMemory(), timeout.Token); await process.StandardInput.FlushAsync(timeout.Token);
var line = await process.StandardOutput.ReadLineAsync(timeout.Token) ?? throw new IOException("原生桥接已断开。");
using var doc = JsonDocument.Parse(line);
var root = doc.RootElement;
if (root.Text("id") != id) throw new InvalidDataException("原生指令关联编号不匹配;不自动重试交易。");
if (!root.Flag("ok")) throw new InvalidOperationException(root.Text("error", "原生指令被拒绝。"));
return root.Get("result").Clone();
}
finally { gate.Release(); }
}
public async ValueTask DisposeAsync()
{
if (process == null) return;
try { if (!process.HasExited) { process.StandardInput.Close(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3)); await process.WaitForExitAsync(timeout.Token); } }
catch (OperationCanceledException) { if (!process.HasExited) process.Kill(true); }
finally { process.Dispose(); process = null; Description = null; }
}
}
public sealed class NativeTradeDriver(NativeBridgeClient bridge) : ITradeCommandDriver
{
public Task PreflightAsync(CancellationToken token)
{
var description = bridge.Description ?? throw new InvalidOperationException("请先连接原生桥接。");
var missing = new[] { "trade.buy", "trade.sell", "navigation.travel", "station.services" }.Except(description.Capabilities).ToArray();
if (missing.Length > 0) throw new InvalidOperationException("原生入口尚未验证:" + string.Join("、", missing) + "。观察接入不能作为自动跑商已完成的证明。");
return Task.CompletedTask;
}
public async Task ServiceDockedAsync(DockedServiceRequest request,Func<CancellationToken,Task> beforeInput,CancellationToken token)
{
await beforeInput(token);
await bridge.CallAsync("station.services",new{marketId=request.MarketId,reservedCargo=request.ReservedCargo},token);
}
public async Task ExecuteAsync(TradeAction action, Func<CancellationToken, Task> beforeInput, CancellationToken token)
{
await beforeInput(token);
var method = action.Kind switch { TradeActionKind.Buy => "trade.buy", TradeActionKind.Sell => "trade.sell", _ => "navigation.travel" };
await bridge.CallAsync(method, new { sourceMarketId = action.Leg.From.MarketId, destinationMarketId = action.Leg.To.MarketId, system = action.Leg.To.SystemName, commodity = action.Order?.Commodity, quantity = action.Order?.Quantity, expectedPrice = action.Kind == TradeActionKind.Buy ? action.Order?.BuyPrice : action.Order?.SellPrice }, token);
}
public async Task ReleaseAllAsync()
{
if (bridge.Connected) try { await bridge.CallAsync("commands.cancel"); } catch (IOException) { } catch (InvalidOperationException) { }
}
}