XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFE.SpaceEngineers.AgentBridge

【SpaceEngineer】AI调试插件

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.Globalization;

namespace XFE.SeAgent.Cli;

public sealed record GameHostStatus(int Pid, string? ExecutablePath, DateTimeOffset? StartTimeUtc,
    bool AgentAvailable, string State, string? WorldPath, string? EvidencePath, string Detail);

public interface IWorldUseGuard
{
    IReadOnlyList<GameHostStatus> Inspect();
    void AssertUnloaded(string worldPath);
}

public sealed class HostStatus(EndpointDiscovery? discovery = null) : IWorldUseGuard
{
    private readonly EndpointDiscovery discovery = discovery ?? new();
    public static string GameDataDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SpaceEngineers");

    public IReadOnlyList<GameHostStatus> Inspect()
    {
        var endpoints = discovery.Discover();
        var result = new List<GameHostStatus>();
        foreach (var process in Process.GetProcesses())
        {
            using (process)
            {
                string name;
                try { name = process.ProcessName; }
                catch (InvalidOperationException) { continue; }
                if (!name.Contains("SpaceEngineers", StringComparison.OrdinalIgnoreCase)) continue;
                var identity = new LocalProcessInspector().Inspect(process.Id);
                bool agent = endpoints.Any(item => item.Pid == process.Id);
                var state = new GameHostStatus(process.Id, identity?.ExecutablePath, identity?.StartTimeUtc,
                    agent, "unknown", null, null, "无法确定游戏正在使用的存档;拒绝离线写入。请使用实时接口,或保存退出游戏。");
                if (identity != null && Directory.Exists(GameDataDirectory))
                {
                    // Correlate by process start AND executable in the log header, never just the newest log.
                    var logs = Directory.EnumerateFiles(GameDataDirectory, "SpaceEngineers_*.log")
                        .Where(path => LogStartMatches(path, identity.StartTimeUtc)).ToArray();
                    if (logs.Length == 1)
                    {
                        try
                        {
                            using var input = new FileStream(logs[0], FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
                            if (input.Length <= 256L * 1024 * 1024)
                            {
                                using var reader = new StreamReader(input);
                                state = ParseLog(reader, state, logs[0]);
                            }
                        }
                        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { }
                    }
                }
                result.Add(state);
            }
        }
        // Two instances cannot both be identified by the same log.
        foreach (var group in result.Where(x => x.EvidencePath != null).GroupBy(x => x.EvidencePath, StringComparer.OrdinalIgnoreCase).Where(g => g.Count() > 1))
            foreach (var host in group.ToArray()) result[result.IndexOf(host)] = host with { State = "unknown", WorldPath = null, Detail = "多个进程匹配同一日志,无法确认存档使用状态。" };
        return result.OrderBy(x => x.Pid).ToArray();
    }

    public void AssertUnloaded(string worldPath) => CheckUnloaded(worldPath, Inspect());

    public static void CheckUnloaded(string worldPath, IReadOnlyList<GameHostStatus> hosts)
    {
        foreach (var host in hosts)
        {
            if (host.State == "unknown") throw new OfflineWorldException("host_unknown", $"PID {host.Pid}: {host.Detail}");
            if (host.WorldPath != null && OfflineWorldStore.SamePath(host.WorldPath, worldPath))
                throw new OfflineWorldException("world_in_use", $"目标存档正由 PID {host.Pid} 使用;请使用实时 pb.deploy,或保存卸载后重试。");
        }
    }

    private static bool LogStartMatches(string path, DateTimeOffset processStart)
    {
        string stamp = Path.GetFileNameWithoutExtension(path)["SpaceEngineers_".Length..];
        if (!DateTime.TryParseExact(stamp, "yyyyMMdd_HHmmssfff", CultureInfo.InvariantCulture, DateTimeStyles.None, out var local)) return false;
        double delta = (DateTime.SpecifyKind(local, DateTimeKind.Local).ToUniversalTime() - processStart.UtcDateTime).TotalSeconds;
        return delta >= -2 && delta <= 30;
    }

    public static GameHostStatus ParseLog(TextReader reader, GameHostStatus host, string logPath)
    {
        bool executableMatches = false;
        string state = "unknown";
        string? world = null;
        string? line;
        while ((line = reader.ReadLine()) != null)
        {
            const string executableMarker = "Environment.CommandLine: ";
            int command = line.IndexOf(executableMarker, StringComparison.Ordinal);
            if (command >= 0 && host.ExecutablePath != null)
            {
                string value = line[(command + executableMarker.Length)..].Trim();
                executableMatches = value.StartsWith('"')
                    ? value.StartsWith('"' + host.ExecutablePath + '"', StringComparison.OrdinalIgnoreCase)
                    : value.Equals(host.ExecutablePath, StringComparison.OrdinalIgnoreCase) || value.StartsWith(host.ExecutablePath + " ", StringComparison.OrdinalIgnoreCase);
            }
            const string loading = "Loading session: ";
            int load = line.IndexOf(loading, StringComparison.Ordinal);
            if (load >= 0)
            {
                string path = line[(load + loading.Length)..].Trim();
                state = Path.IsPathFullyQualified(path) ? "loaded_or_loading" : "unknown";
                world = state == "unknown" ? null : Path.GetFullPath(path);
            }
            // Keep the last path protected until unloading is complete.
            if (line.Contains("MySession::Unload END", StringComparison.Ordinal)) { state = "unloaded"; world = null; }
        }
        if (!executableMatches) return host;
        return host with { State = state, WorldPath = world, EvidencePath = logPath,
            Detail = state == "unknown" ? host.Detail : host.AgentAvailable ? "Agent 已连接;存档状态依据该进程日志。" : "游戏未加载 AgentBridge;可管理未加载的本地存档,实时操作需通过 XFE 加载器启动。" };
    }
}