using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using TMPro;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("TFWROracleRunner")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.2.0.0")]
[assembly: AssemblyInformationalVersion("0.2.0+55514a27d14a3819e28577bb4e1b027beb4d7887")]
[assembly: AssemblyProduct("TFWROracleRunner")]
[assembly: AssemblyTitle("TFWROracleRunner")]
[assembly: AssemblyVersion("0.2.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
internal sealed class NullableAttribute : Attribute
{
public readonly byte[] NullableFlags;
public NullableAttribute(byte P_0)
{
NullableFlags = new byte[1] { P_0 };
}
public NullableAttribute(byte[] P_0)
{
NullableFlags = P_0;
}
}
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
internal sealed class NullableContextAttribute : Attribute
{
public readonly byte Flag;
public NullableContextAttribute(byte P_0)
{
Flag = P_0;
}
}
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public readonly int Version;
public RefSafetyRulesAttribute(int P_0)
{
Version = P_0;
}
}
}
namespace TFWROracleRunner
{
public static class LeaderboardLogFormatter
{
public static string FormatStartLine(string scriptName)
{
return string.Format(CultureInfo.InvariantCulture, "[{0}] start", RunnerStateProtocol.NormalizeTargetScriptName(scriptName));
}
public static string FormatRunLine(string scriptName, int runIndex, TimeSpan timeSpan)
{
return string.Format(CultureInfo.InvariantCulture, "[{0}] run={1} time={2}", RunnerStateProtocol.NormalizeTargetScriptName(scriptName), runIndex, FormatClock(timeSpan));
}
public static string FormatSummaryLine(string scriptName, bool finished, int runCount, TimeSpan averageTime)
{
return string.Format(CultureInfo.InvariantCulture, "[{0}] finished={1} runs={2} average={3}", RunnerStateProtocol.NormalizeTargetScriptName(scriptName), finished ? "true" : "false", runCount, FormatClock(averageTime));
}
public static string FormatClock(TimeSpan timeSpan)
{
int num = (int)Math.Floor(timeSpan.TotalMinutes);
return string.Format(CultureInfo.InvariantCulture, "{0}:{1:00}.{2:000}", num, timeSpan.Seconds, timeSpan.Milliseconds);
}
}
public sealed class LeaderboardResultAutoCloseGate
{
private bool externalResultPendingClose;
public bool ExternalResultPendingClose => externalResultPendingClose;
public void MarkExternalLeaderboardObserved()
{
externalResultPendingClose = true;
}
public void Clear()
{
externalResultPendingClose = false;
}
}
internal sealed class OracleRunnerConfig
{
public ConfigEntry<bool> Enabled { get; }
public ConfigEntry<string> TargetScriptName { get; }
public ConfigEntry<bool> AutoPlayMainMenu { get; }
public ConfigEntry<int> StartupPollIntervalMs { get; }
public ConfigEntry<float> ScriptReadyTimeoutSeconds { get; }
public ConfigEntry<float> DefaultRequestTimeoutSeconds { get; }
public ConfigEntry<float> RequestStopGracePeriodSeconds { get; }
public OracleRunnerConfig(ConfigFile config)
{
Enabled = config.Bind<bool>("General", "Enabled", true, "是否启用自动权威执行器。");
TargetScriptName = config.Bind<string>("General", "TargetScriptName", "lb_start", "目标脚本窗口名,不带 .py。");
AutoPlayMainMenu = config.Bind<bool>("General", "AutoPlayMainMenu", false, "是否自动进入游戏。无人值守运行可开启;关闭时请手动进入工作区后再请求脚本。");
StartupPollIntervalMs = config.Bind<int>("Timing", "StartupPollIntervalMs", 250, "轮询主菜单、工作区和 state.json 的时间间隔(毫秒)。");
ScriptReadyTimeoutSeconds = config.Bind<float>("Timing", "ScriptReadyTimeoutSeconds", 30f, "等待脚本窗口出现的最长时间(秒)。仅用于单次 requested 请求启动期。");
DefaultRequestTimeoutSeconds = config.Bind<float>("Timing", "DefaultRequestTimeoutSeconds", 20f, "state.json 未显式提供 timeout_seconds 时,单次请求的默认执行超时(秒)。");
RequestStopGracePeriodSeconds = config.Bind<float>("Timing", "RequestStopGracePeriodSeconds", 3f, "收到覆盖请求或超时停止后,等待 StopMainExecution() 生效的提示窗口(秒)。");
}
}
[DataContract]
public sealed class OracleRunnerStateFile
{
[DataMember(Name = "request_id", IsRequired = true)]
public int RequestId { get; set; }
public RunnerRequestStatus Status { get; set; }
[DataMember(Name = "status", IsRequired = true)]
private string StatusText
{
get
{
return RunnerStateProtocol.ToStatusText(Status);
}
set
{
Status = RunnerStateProtocol.ParseStatusText(value);
}
}
[DataMember(Name = "target_script")]
public string? TargetScript { get; set; }
[DataMember(Name = "timeout_seconds")]
public double? TimeoutSeconds { get; set; }
[DataMember(Name = "started_at")]
public string? StartedAt { get; set; }
[DataMember(Name = "finished_at")]
public string? FinishedAt { get; set; }
[DataMember(Name = "last_error")]
public string? LastError { get; set; }
}
[BepInPlugin("mlj.tfwr.oracle-runner", "TFWR Oracle Runner", "0.2.0")]
public sealed class Plugin : BaseUnityPlugin
{
private enum StopReason
{
None,
Timeout,
Superseded,
StopRequested
}
[HarmonyPatch(typeof(Menu), "Play")]
private static class MenuPlayPatch
{
private static void Postfix()
{
Instance?.MarkMenuPlayed();
}
}
[HarmonyPatch(typeof(LeaderboardManager), "StopLeaderboardRun")]
private static class LeaderboardStopPatch
{
private static void Prefix(LeaderboardManager __instance, bool finished, string leaderboardName, TimeSpan timeSpan)
{
MainSim inst = MainSim.Inst;
if (!((Object)(object)inst == (Object)null))
{
Instance?.ObserveLeaderboardStopped(inst, finished, leaderboardName, timeSpan, inst.numLeaderboardRuns);
}
}
}
internal const string PluginGuid = "mlj.tfwr.oracle-runner";
internal const string PluginName = "TFWR Oracle Runner";
internal const string PluginVersion = "0.2.0";
private static Plugin? instance;
private static readonly FieldRef<MainSim, Simulation> SimRef = AccessTools.FieldRefAccess<MainSim, Simulation>("sim");
private static readonly FieldRef<MainSim, LeaderboardStartArgs> PrevLeaderboardStartRef = AccessTools.FieldRefAccess<MainSim, LeaderboardStartArgs>("prevLeaderboardStart");
private static readonly FieldRef<LeaderboardManager, GameObject> RunCancelledRef = AccessTools.FieldRefAccess<LeaderboardManager, GameObject>("runCancelled");
private static readonly FieldRef<LeaderboardManager, TextMeshProUGUI> FinishTimeTextRef = AccessTools.FieldRefAccess<LeaderboardManager, TextMeshProUGUI>("finishTimeText");
private static readonly FieldRef<Simulation, bool> SimulationHasErrorRef = AccessTools.FieldRefAccess<Simulation, bool>("hasError");
private static readonly FieldRef<CodeWindow, string> CodeWindowErrorStringRef = AccessTools.FieldRefAccess<CodeWindow, string>("errorString");
private static readonly FieldRef<CodeWindow, int> CodeWindowErrorStartIndexRef = AccessTools.FieldRefAccess<CodeWindow, int>("errorStartIndex");
private static readonly FieldRef<CodeWindow, int> CodeWindowErrorEndIndexRef = AccessTools.FieldRefAccess<CodeWindow, int>("errorEndIndex");
private OracleRunnerConfig runnerConfig;
private RunnerStateStore stateStore;
private Harmony? harmony;
private float pluginStartedAt;
private float nextStatePollAt;
private float nextStateWarningAt;
private float lastProgressLogAt;
private float lastItemSnapshotLogAt;
private float activeRequestStartedAt;
private float stopRequestedAt;
private readonly LeaderboardResultAutoCloseGate leaderboardResultAutoCloseGate = new LeaderboardResultAutoCloseGate();
private bool playInvoked;
private StopReason stopReason;
private OracleRunnerStateFile? activeRequest;
private OracleRunnerStateFile? pendingRequest;
private int lastHandledRequestId;
private bool wasLeaderboardRunning;
private int observedLeaderboardRuns;
private TimeSpan observedLeaderboardTotalTime = TimeSpan.Zero;
private string observedLeaderboardScriptName = string.Empty;
private ManualLogSource Log => ((BaseUnityPlugin)this).Logger;
private static Plugin? Instance => instance;
private void Awake()
{
//IL_004e: Unknown result type (might be due to invalid IL or missing references)
//IL_0058: Expected O, but got Unknown
instance = this;
runnerConfig = new OracleRunnerConfig(((BaseUnityPlugin)this).Config);
stateStore = new RunnerStateStore(Path.Combine(Paths.ConfigPath, "mlj.tfwr.oracle-runner.state.json"));
pluginStartedAt = Time.realtimeSinceStartup;
lastProgressLogAt = pluginStartedAt;
harmony = new Harmony("mlj.tfwr.oracle-runner");
harmony.PatchAll();
Log.LogInfo((object)($"enabled={runnerConfig.Enabled.Value} state_file={stateStore.StateFilePath} " + $"poll_ms={runnerConfig.StartupPollIntervalMs.Value} default_timeout={runnerConfig.DefaultRequestTimeoutSeconds.Value}s"));
}
private void OnDestroy()
{
Harmony? obj = harmony;
if (obj != null)
{
obj.UnpatchSelf();
}
if ((Object)(object)instance == (Object)(object)this)
{
instance = null;
}
}
private void Update()
{
if (!runnerConfig.Enabled.Value)
{
return;
}
MainSim inst = MainSim.Inst;
if ((Object)(object)inst == (Object)null)
{
MaybeLogProgress("等待 MainSim 初始化");
return;
}
if (!playInvoked && runnerConfig.AutoPlayMainMenu.Value && (Object)(object)inst.menu != (Object)null && ((Component)inst.menu).gameObject.activeInHierarchy)
{
inst.menu.Play();
playInvoked = true;
Log.LogInfo((object)"已调用 Menu.Play() 进入游戏工作区");
}
ObserveLeaderboardTiming(inst);
PollState(inst);
MonitorExecution(inst);
}
private void PollState(MainSim mainSim)
{
float realtimeSinceStartup = Time.realtimeSinceStartup;
if (realtimeSinceStartup < nextStatePollAt)
{
return;
}
nextStatePollAt = realtimeSinceStartup + Mathf.Max(0.05f, (float)runnerConfig.StartupPollIntervalMs.Value / 1000f);
try
{
OracleRunnerStateFile oracleRunnerStateFile = stateStore.Read();
EnsureStateFileReady(mainSim, oracleRunnerStateFile);
if (oracleRunnerStateFile != null)
{
ObserveStopRequestedState(mainSim, oracleRunnerStateFile);
ObserveRequestedState(mainSim, oracleRunnerStateFile);
}
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is SerializationException)
{
if (realtimeSinceStartup >= nextStateWarningAt)
{
nextStateWarningAt = realtimeSinceStartup + 5f;
Log.LogWarning((object)("状态文件同步失败,将在下次轮询重试:" + stateStore.StateFilePath + ";" + ex.Message));
}
}
}
private void EnsureStateFileReady(MainSim mainSim, OracleRunnerStateFile? current)
{
if (current == null)
{
stateStore.Write(RunnerStateProtocol.CreateIdle(lastHandledRequestId));
}
else if (activeRequest == null && !mainSim.IsExecuting() && current.Status == RunnerRequestStatus.Running)
{
stateStore.Write(RunnerStateProtocol.CreateIdle(current.RequestId));
}
}
private void ObserveRequestedState(MainSim mainSim, OracleRunnerStateFile observedState)
{
if (observedState.Status != RunnerRequestStatus.Requested)
{
return;
}
if (activeRequest == null)
{
if (observedState.RequestId > lastHandledRequestId)
{
TryStartRequest(mainSim, observedState);
}
}
else if (observedState.RequestId > activeRequest.RequestId)
{
pendingRequest = observedState;
if (stopReason == StopReason.None)
{
BeginStop(mainSim, StopReason.Superseded);
}
}
}
private void ObserveStopRequestedState(MainSim mainSim, OracleRunnerStateFile observedState)
{
if (activeRequest != null && observedState.Status == RunnerRequestStatus.StopRequested && observedState.RequestId == activeRequest.RequestId)
{
activeRequest.LastError = (string.IsNullOrWhiteSpace(observedState.LastError) ? "stop requested" : observedState.LastError);
if (stopReason == StopReason.None)
{
BeginStop(mainSim, StopReason.StopRequested);
}
}
}
private void TryStartRequest(MainSim mainSim, OracleRunnerStateFile requestedState)
{
CloseLeaderboardResultScreen(mainSim, "request_start");
if ((Object)(object)mainSim.workspace == (Object)null || !((Component)mainSim.workspace).gameObject.activeInHierarchy)
{
MaybeLogProgress("等待工作区激活");
return;
}
string text = RunnerStateProtocol.NormalizeTargetScriptName(requestedState.TargetScript ?? runnerConfig.TargetScriptName.Value);
CodeWindow val = FindTargetWindow(mainSim.workspace, text);
if ((Object)(object)val == (Object)null)
{
if (Time.realtimeSinceStartup - pluginStartedAt > runnerConfig.ScriptReadyTimeoutSeconds.Value)
{
MarkFailedWithOutput(requestedState, "script window not found: " + text, DateTimeOffset.UtcNow);
stateStore.Write(requestedState);
lastHandledRequestId = Math.Max(lastHandledRequestId, requestedState.RequestId);
}
else
{
MaybeLogProgress("等待脚本窗口 " + text);
}
return;
}
if (mainSim.IsExecuting())
{
MaybeLogProgress("当前已有执行,等待停止后再启动新请求");
return;
}
Node val2 = val.Parse();
if (val2 == null)
{
MarkFailedWithOutput(requestedState, BuildParseFailureMessage(val, text), DateTimeOffset.UtcNow);
stateStore.Write(requestedState);
lastHandledRequestId = Math.Max(lastHandledRequestId, requestedState.RequestId);
return;
}
RunnerStateProtocol.MarkRunning(requestedState, DateTimeOffset.UtcNow);
stateStore.Write(requestedState);
mainSim.StartMainExecution(val, val2);
activeRequest = requestedState;
activeRequestStartedAt = Time.realtimeSinceStartup;
lastHandledRequestId = Math.Max(lastHandledRequestId, requestedState.RequestId);
stopReason = StopReason.None;
Log.LogInfo((object)$"已启动脚本 {val.fileName} request_id={requestedState.RequestId}");
}
private void MonitorExecution(MainSim mainSim)
{
if (activeRequest == null)
{
return;
}
if (stopReason != StopReason.None)
{
CloseLeaderboardResultScreen(mainSim, "request_stop");
if (!mainSim.IsExecuting())
{
FinishStopping(mainSim);
return;
}
float num = Time.realtimeSinceStartup - stopRequestedAt;
if (num >= runnerConfig.RequestStopGracePeriodSeconds.Value)
{
MaybeLogItemSnapshot(mainSim, "stopping", "stop_elapsed=" + num.ToString("0.0", CultureInfo.InvariantCulture));
}
}
else if (HasSimulationError(mainSim))
{
MarkFailedWithOutput(activeRequest, "script runtime error; see output.txt Error block", DateTimeOffset.UtcNow);
stateStore.Write(activeRequest);
mainSim.StopMainExecution();
activeRequest = null;
stopReason = StopReason.None;
}
else if (!mainSim.IsExecuting())
{
FinishDone();
}
else
{
double num2 = activeRequest.TimeoutSeconds ?? ((double)runnerConfig.DefaultRequestTimeoutSeconds.Value);
if (num2 > 0.0 && (double)(Time.realtimeSinceStartup - activeRequestStartedAt) >= num2)
{
BeginStop(mainSim, StopReason.Timeout);
}
else
{
MaybeLogItemSnapshot(mainSim, "running", "timeout=" + num2.ToString("0.0", CultureInfo.InvariantCulture));
}
}
}
private void MaybeLogProgress(string message)
{
float realtimeSinceStartup = Time.realtimeSinceStartup;
float num = Mathf.Max(0.05f, (float)runnerConfig.StartupPollIntervalMs.Value / 1000f);
if (!(realtimeSinceStartup - lastProgressLogAt < num))
{
lastProgressLogAt = realtimeSinceStartup;
Log.LogInfo((object)message);
}
}
private void MaybeLogItemSnapshot(MainSim mainSim, string status, string detail)
{
if (activeRequest != null)
{
float realtimeSinceStartup = Time.realtimeSinceStartup;
if (!(realtimeSinceStartup - lastItemSnapshotLogAt < 1f))
{
lastItemSnapshotLogAt = realtimeSinceStartup;
Log.LogInfo((object)BuildItemSnapshotLine(mainSim, status, detail));
}
}
}
private string BuildItemSnapshotLine(MainSim mainSim, string status, string detail)
{
//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("item_snapshot request_id=");
stringBuilder.Append(activeRequest?.RequestId ?? 0);
stringBuilder.Append(" status=");
stringBuilder.Append(status);
if (!string.IsNullOrWhiteSpace(detail))
{
stringBuilder.Append(' ');
stringBuilder.Append(detail);
}
stringBuilder.Append(" real_elapsed=");
stringBuilder.Append((Time.realtimeSinceStartup - activeRequestStartedAt).ToString("0.0", CultureInfo.InvariantCulture));
Simulation val = SimRef.Invoke(mainSim);
if (val != null)
{
stringBuilder.Append(" game_time=");
Duration currentTime = val.CurrentTime;
stringBuilder.Append(((Duration)(ref currentTime)).Seconds.ToString("0.###", CultureInfo.InvariantCulture));
if (val.Execution != null)
{
stringBuilder.Append(" game_tick=");
stringBuilder.Append(val.Execution.GlobalOpCount.ToString("0.###", CultureInfo.InvariantCulture));
}
}
stringBuilder.Append(" leaderboard_runs=");
stringBuilder.Append(mainSim.numLeaderboardRuns.ToString(CultureInfo.InvariantCulture));
stringBuilder.Append(" leaderboard_total_time=");
stringBuilder.Append(((Duration)(ref mainSim.totalLeaderboardTime)).Seconds.ToString("0.###", CultureInfo.InvariantCulture));
if (!string.IsNullOrWhiteSpace(observedLeaderboardScriptName))
{
stringBuilder.Append(" leaderboard_script=");
stringBuilder.Append(observedLeaderboardScriptName);
}
ItemBlock inventory = mainSim.GetInventory();
foreach (ItemSO allItem in ResourceManager.GetAllItems())
{
stringBuilder.Append(' ');
stringBuilder.Append(allItem.itemName);
stringBuilder.Append('=');
stringBuilder.Append(inventory.GetNumber(allItem.itemId).ToString("0.###", CultureInfo.InvariantCulture));
}
return stringBuilder.ToString();
}
private void ObserveLeaderboardTiming(MainSim mainSim)
{
if ((Object)(object)mainSim.leaderboardManager == (Object)null)
{
return;
}
bool isRunning = mainSim.leaderboardManager.IsRunning;
Simulation sim = SimRef.Invoke(mainSim);
if (isRunning)
{
if (activeRequest != null)
{
leaderboardResultAutoCloseGate.MarkExternalLeaderboardObserved();
}
if (!wasLeaderboardRunning)
{
wasLeaderboardRunning = true;
observedLeaderboardRuns = 0;
observedLeaderboardTotalTime = TimeSpan.Zero;
observedLeaderboardScriptName = ResolveLeaderboardScriptName(mainSim, sim);
LogLeaderboardLine(LeaderboardLogFormatter.FormatStartLine(observedLeaderboardScriptName));
}
if (mainSim.numLeaderboardRuns > observedLeaderboardRuns)
{
TimeSpan timeSpan = ((Duration)(ref mainSim.totalLeaderboardTime)).ToTimeSpan();
TimeSpan timeSpan2 = timeSpan - observedLeaderboardTotalTime;
string line = LeaderboardLogFormatter.FormatRunLine(observedLeaderboardScriptName, mainSim.numLeaderboardRuns, timeSpan2);
LogLeaderboardLine(line);
observedLeaderboardRuns = mainSim.numLeaderboardRuns;
observedLeaderboardTotalTime = timeSpan;
}
return;
}
if (!wasLeaderboardRunning)
{
TryCloseExternalLeaderboardResultScreen(mainSim, "external_result_idle");
return;
}
if (observedLeaderboardRuns > 0)
{
bool flag = !RunCancelledRef.Invoke(mainSim.leaderboardManager).activeSelf;
TimeSpan averageTime = TimeSpan.FromTicks(observedLeaderboardTotalTime.Ticks / observedLeaderboardRuns);
string line2 = LeaderboardLogFormatter.FormatSummaryLine(observedLeaderboardScriptName, flag, observedLeaderboardRuns, averageTime);
LogLeaderboardLine(line2);
if (activeRequest != null)
{
leaderboardResultAutoCloseGate.MarkExternalLeaderboardObserved();
if (flag)
{
RunnerStateProtocol.MarkDone(activeRequest, DateTimeOffset.UtcNow);
}
else
{
MarkFailedWithOutput(activeRequest, "leaderboard cancelled", DateTimeOffset.UtcNow);
}
stateStore.Write(activeRequest);
activeRequest = null;
}
TryCloseExternalLeaderboardResultScreen(mainSim, "external_result_complete");
}
else if (mainSim.leaderboardManager.IsLeaderBoardScreenOpen)
{
if (!RunCancelledRef.Invoke(mainSim.leaderboardManager).activeSelf)
{
string scriptName = (string.IsNullOrWhiteSpace(observedLeaderboardScriptName) ? ResolveLeaderboardScriptName(mainSim, sim) : observedLeaderboardScriptName);
TimeSpan averageTime2 = ParseLeaderboardFinishTime(mainSim.leaderboardManager);
string line3 = LeaderboardLogFormatter.FormatSummaryLine(scriptName, finished: true, 1, averageTime2);
LogLeaderboardLine(line3);
if (activeRequest != null)
{
leaderboardResultAutoCloseGate.MarkExternalLeaderboardObserved();
RunnerStateProtocol.MarkDone(activeRequest, DateTimeOffset.UtcNow);
stateStore.Write(activeRequest);
activeRequest = null;
}
TryCloseExternalLeaderboardResultScreen(mainSim, "external_result_complete");
}
else
{
if (activeRequest != null)
{
leaderboardResultAutoCloseGate.MarkExternalLeaderboardObserved();
MarkFailedWithOutput(activeRequest, "leaderboard finished without completed runs", DateTimeOffset.UtcNow);
stateStore.Write(activeRequest);
activeRequest = null;
}
TryCloseExternalLeaderboardResultScreen(mainSim, "external_result_cancelled");
}
}
wasLeaderboardRunning = false;
observedLeaderboardRuns = 0;
observedLeaderboardTotalTime = TimeSpan.Zero;
observedLeaderboardScriptName = string.Empty;
}
private void TryCloseExternalLeaderboardResultScreen(MainSim mainSim, string reason)
{
if (leaderboardResultAutoCloseGate.ExternalResultPendingClose)
{
CloseLeaderboardResultScreen(mainSim, reason);
}
}
private bool CloseLeaderboardResultScreen(MainSim mainSim, string reason)
{
if ((Object)(object)mainSim.leaderboardManager == (Object)null || !mainSim.leaderboardManager.IsLeaderBoardScreenOpen)
{
return false;
}
mainSim.leaderboardManager.OkPressed();
leaderboardResultAutoCloseGate.Clear();
Log.LogInfo((object)("已自动确认 leaderboard 结果页面 reason=" + reason));
return true;
}
private static TimeSpan ParseLeaderboardFinishTime(LeaderboardManager leaderboardManager)
{
string[] array = ((TMP_Text)FinishTimeTextRef.Invoke(leaderboardManager)).text.Trim().Split(':', 2);
if (array.Length != 2 || !int.TryParse(array[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || !double.TryParse(array[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var result2))
{
return TimeSpan.Zero;
}
return TimeSpan.FromMinutes(result) + TimeSpan.FromSeconds(result2);
}
private void ObserveLeaderboardStopped(MainSim mainSim, bool finished, string leaderboardName, TimeSpan averageTime, int runCount)
{
if (activeRequest != null)
{
leaderboardResultAutoCloseGate.MarkExternalLeaderboardObserved();
string scriptName = (string.IsNullOrWhiteSpace(observedLeaderboardScriptName) ? ResolveLeaderboardScriptName(mainSim, SimRef.Invoke(mainSim)) : observedLeaderboardScriptName);
int runCount2 = Math.Max(1, runCount);
string line = LeaderboardLogFormatter.FormatSummaryLine(scriptName, finished, runCount2, averageTime);
LogLeaderboardLine(line);
if (finished)
{
RunnerStateProtocol.MarkDone(activeRequest, DateTimeOffset.UtcNow);
}
else
{
MarkFailedWithOutput(activeRequest, "leaderboard cancelled: " + leaderboardName, DateTimeOffset.UtcNow);
}
stateStore.Write(activeRequest);
activeRequest = null;
wasLeaderboardRunning = false;
observedLeaderboardRuns = 0;
observedLeaderboardTotalTime = TimeSpan.Zero;
observedLeaderboardScriptName = string.Empty;
}
}
private void BeginStop(MainSim mainSim, StopReason reason)
{
if (activeRequest != null)
{
stopReason = reason;
stopRequestedAt = Time.realtimeSinceStartup;
mainSim.StopMainExecution();
Log.LogInfo((object)($"请求停止当前执行 request_id={activeRequest.RequestId} reason={reason} " + $"pending={((pendingRequest != null) ? pendingRequest.RequestId : (-1))}"));
}
}
private void FinishStopping(MainSim mainSim)
{
if (activeRequest == null)
{
stopReason = StopReason.None;
return;
}
if (stopReason == StopReason.Timeout || stopReason == StopReason.StopRequested)
{
MarkFailedWithOutput(activeRequest, (stopReason == StopReason.Timeout) ? $"request timed out after {activeRequest.TimeoutSeconds ?? ((double)runnerConfig.DefaultRequestTimeoutSeconds.Value):0.###}s" : (activeRequest.LastError ?? "stop requested"), DateTimeOffset.UtcNow);
stateStore.Write(activeRequest);
}
OracleRunnerStateFile oracleRunnerStateFile = pendingRequest;
activeRequest = null;
pendingRequest = null;
stopReason = StopReason.None;
if (oracleRunnerStateFile != null)
{
TryStartRequest(mainSim, oracleRunnerStateFile);
}
}
private void FinishDone()
{
if (activeRequest != null)
{
RunnerStateProtocol.MarkDone(activeRequest, DateTimeOffset.UtcNow);
stateStore.Write(activeRequest);
Log.LogInfo((object)$"脚本执行完成 request_id={activeRequest.RequestId}");
activeRequest = null;
}
}
private static bool HasSimulationError(MainSim mainSim)
{
Simulation val = SimRef.Invoke(mainSim);
if (val != null)
{
return SimulationHasErrorRef.Invoke(val);
}
return false;
}
private static string BuildParseFailureMessage(CodeWindow targetWindow, string targetScriptName)
{
string text = CodeWindowErrorStringRef.Invoke(targetWindow);
int num = CodeWindowErrorStartIndexRef.Invoke(targetWindow);
int num2 = CodeWindowErrorEndIndexRef.Invoke(targetWindow);
if (string.IsNullOrWhiteSpace(text))
{
return "parse failed: " + targetScriptName;
}
return $"parse failed: {targetScriptName} error={text} start={num} end={num2}";
}
private static void MarkFailedWithOutput(OracleRunnerStateFile state, string message, DateTimeOffset finishedAt)
{
RunnerStateProtocol.MarkFailed(state, message, finishedAt);
WriteOracleErrorToOutput(state.RequestId, message);
}
private static void WriteOracleErrorToOutput(int requestId, string message)
{
string arg = message.Replace('\r', ' ').Replace('\n', ' ');
Logger.Log($"oracle_error request_id={requestId} {arg}");
}
private void LogLeaderboardLine(string line)
{
Log.LogInfo((object)line);
Logger.Log(line);
}
private CodeWindow? FindTargetWindow(Workspace workspace, string targetScriptName)
{
if (workspace.codeWindows.TryGetValue(targetScriptName, out var value))
{
return value;
}
foreach (KeyValuePair<string, CodeWindow> codeWindow in workspace.codeWindows)
{
if (string.Equals(codeWindow.Key, targetScriptName, StringComparison.OrdinalIgnoreCase))
{
return codeWindow.Value;
}
}
return null;
}
private void MarkMenuPlayed()
{
playInvoked = true;
}
private static string ResolveLeaderboardScriptName(MainSim mainSim, Simulation? sim)
{
LeaderboardStartArgs val = PrevLeaderboardStartRef.Invoke(mainSim);
if (!string.IsNullOrWhiteSpace(val?.fileName))
{
return val.fileName;
}
if (!string.IsNullOrWhiteSpace(sim?.leaderboardName))
{
return sim.leaderboardName;
}
return "leaderboard";
}
}
public enum RunnerRequestStatus
{
Idle,
Requested,
Running,
StopRequested,
Done,
Failed,
Superseded
}
public static class RunnerStateProtocol
{
public const string StateFileName = "mlj.tfwr.oracle-runner.state.json";
public static string NormalizeTargetScriptName(string? configuredName)
{
string text = (configuredName ?? string.Empty).Trim();
if (text.EndsWith(".py", StringComparison.OrdinalIgnoreCase))
{
string text2 = text;
text = text2.Substring(0, text2.Length - 3);
}
if (!string.IsNullOrWhiteSpace(text))
{
return text;
}
return "lb_start";
}
public static OracleRunnerStateFile CreateIdle(int requestId)
{
return new OracleRunnerStateFile
{
RequestId = requestId,
Status = RunnerRequestStatus.Idle,
TargetScript = null,
TimeoutSeconds = null,
StartedAt = null,
FinishedAt = null,
LastError = null
};
}
public static OracleRunnerStateFile CreateRequested(int requestId, string targetScript, double timeoutSeconds)
{
return new OracleRunnerStateFile
{
RequestId = requestId,
Status = RunnerRequestStatus.Requested,
TargetScript = NormalizeTargetScriptName(targetScript),
TimeoutSeconds = timeoutSeconds,
StartedAt = null,
FinishedAt = null,
LastError = null
};
}
public static void MarkSuperseded(OracleRunnerStateFile state, int supersededByRequestId, DateTimeOffset finishedAt)
{
state.Status = RunnerRequestStatus.Superseded;
state.FinishedAt = ToIso8601(finishedAt);
state.LastError = $"superseded by request_id={supersededByRequestId}";
}
public static void MarkRunning(OracleRunnerStateFile state, DateTimeOffset startedAt)
{
state.Status = RunnerRequestStatus.Running;
state.StartedAt = ToIso8601(startedAt);
state.FinishedAt = null;
state.LastError = null;
}
public static void MarkStopRequested(OracleRunnerStateFile state, string message)
{
state.Status = RunnerRequestStatus.StopRequested;
state.LastError = message;
}
public static void MarkDone(OracleRunnerStateFile state, DateTimeOffset finishedAt)
{
state.Status = RunnerRequestStatus.Done;
state.FinishedAt = ToIso8601(finishedAt);
state.LastError = null;
}
public static void MarkFailed(OracleRunnerStateFile state, string message, DateTimeOffset finishedAt)
{
state.Status = RunnerRequestStatus.Failed;
state.FinishedAt = ToIso8601(finishedAt);
state.LastError = message;
}
public static string ToIso8601(DateTimeOffset value)
{
return value.ToString("O");
}
public static string ToStatusText(RunnerRequestStatus status)
{
if (status == RunnerRequestStatus.StopRequested)
{
return "stop_requested";
}
return status.ToString().ToLowerInvariant();
}
public static RunnerRequestStatus ParseStatusText(string? text)
{
return (text ?? string.Empty).Trim().ToLowerInvariant() switch
{
"idle" => RunnerRequestStatus.Idle,
"requested" => RunnerRequestStatus.Requested,
"running" => RunnerRequestStatus.Running,
"stop_requested" => RunnerRequestStatus.StopRequested,
"done" => RunnerRequestStatus.Done,
"failed" => RunnerRequestStatus.Failed,
"superseded" => RunnerRequestStatus.Superseded,
_ => throw new SerializationException("未知请求状态:" + text),
};
}
}
public sealed class RunnerStateStore
{
private readonly string stateFilePath;
private readonly object gate = new object();
private OracleRunnerStateFile? pendingState;
public string StateFilePath => stateFilePath;
public RunnerStateStore(string stateFilePath)
{
this.stateFilePath = stateFilePath;
}
public OracleRunnerStateFile? Read()
{
lock (gate)
{
string directoryName = Path.GetDirectoryName(stateFilePath);
if (!string.IsNullOrWhiteSpace(directoryName))
{
Directory.CreateDirectory(directoryName);
}
string path = stateFilePath + ".lock";
using FileStream fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
try
{
OracleRunnerStateFile oracleRunnerStateFile = ReadCurrent();
if (pendingState == null)
{
return oracleRunnerStateFile;
}
if (oracleRunnerStateFile == null || (oracleRunnerStateFile.RequestId == pendingState.RequestId && (pendingState.Status != RunnerRequestStatus.Running || oracleRunnerStateFile.Status == RunnerRequestStatus.Requested || oracleRunnerStateFile.Status == RunnerRequestStatus.Running)))
{
WriteCurrent(pendingState);
oracleRunnerStateFile = pendingState;
}
pendingState = null;
return oracleRunnerStateFile;
}
finally
{
fileStream.Dispose();
File.Delete(path);
}
}
}
public void Write(OracleRunnerStateFile state)
{
lock (gate)
{
pendingState = new OracleRunnerStateFile
{
RequestId = state.RequestId,
Status = state.Status,
TargetScript = state.TargetScript,
TimeoutSeconds = state.TimeoutSeconds,
StartedAt = state.StartedAt,
FinishedAt = state.FinishedAt,
LastError = state.LastError
};
}
}
private OracleRunnerStateFile? ReadCurrent()
{
try
{
using FileStream stream = new FileStream(stateFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
OracleRunnerStateFile oracleRunnerStateFile = (new DataContractJsonSerializer(typeof(OracleRunnerStateFile)).ReadObject(stream) as OracleRunnerStateFile) ?? throw new SerializationException("状态文件必须是 JSON 对象。");
if (oracleRunnerStateFile.RequestId < 0 || (oracleRunnerStateFile.TimeoutSeconds.HasValue && (double.IsNaN(oracleRunnerStateFile.TimeoutSeconds.Value) || double.IsInfinity(oracleRunnerStateFile.TimeoutSeconds.Value) || oracleRunnerStateFile.TimeoutSeconds.Value < 0.0)))
{
throw new SerializationException("request_id 和 timeout_seconds 必须是非负有限数值。");
}
return oracleRunnerStateFile;
}
catch (FileNotFoundException)
{
return null;
}
}
private void WriteCurrent(OracleRunnerStateFile state)
{
string text = $"{stateFilePath}.{Guid.NewGuid():N}.tmp";
try
{
using (FileStream stream = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
new DataContractJsonSerializer(typeof(OracleRunnerStateFile)).WriteObject(stream, state);
}
if (File.Exists(stateFilePath))
{
File.Replace(text, stateFilePath, null, ignoreMetadataErrors: true);
}
else
{
File.Move(text, stateFilePath);
}
}
finally
{
File.Delete(text);
}
}
}
}