Initial commit: AIGame Unity client with URP, HybridCLR, YooAsset and tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-09-21 14:21:07 +08:00
co-authored by Cursor
commit f3f66ab063
931 changed files with 112366 additions and 0 deletions
@@ -0,0 +1,234 @@
using System;
namespace Unity.Pipeline
{
/// <summary>
/// Per-server progress-reporting state. Owned one-per-<see cref="BasePipelineServer"/> (the
/// same pattern as <c>Dispatcher</c>) so a test server's execution can never cross-attribute
/// progress with the live server's. See <see cref="CliProgress"/> for the public static
/// façade that arbitrary command code calls.
/// </summary>
internal sealed class CliProgressState
{
/// <summary>Immutable copy of the current progress state (thread-safe to hand across threads).</summary>
internal readonly struct Snapshot
{
public readonly bool HasReport;
public readonly string Title;
public readonly string Info;
public readonly long? Current;
public readonly long? Total;
public readonly double? Progress01;
public Snapshot(string title, string info, long? current, long? total, double? progress01)
{
HasReport = true;
Title = title;
Info = info;
Current = current;
Total = total;
Progress01 = progress01;
}
}
readonly object m_Gate = new object();
Snapshot m_Explicit;
string m_ExplicitOwnerId;
string m_CurrentOwnerId;
Snapshot m_Ambient;
int m_ActiveCount;
/// <summary>Whether any execution is currently in flight. Read by /api/progress.</summary>
internal bool IsActive
{
get
{
lock (m_Gate)
{
return m_ActiveCount > 0;
}
}
}
/// <summary>Marks one more execution in flight (see <see cref="EndExecutionCount"/>).</summary>
internal void BeginExecutionCount()
{
lock (m_Gate)
{
m_ActiveCount++;
}
}
/// <summary>
/// Marks one execution finished, resetting the explicit-report state when this was the
/// last one in flight. The decrement and the reset-on-zero check happen under the same
/// lock as <see cref="BeginExecution"/>, so a new execution starting in the gap can never
/// have its fresh BeginExecution/Report wiped by this reset — the previous implementation
/// decremented a separate counter and reset separately, leaving a window where a finishing
/// execution could decrement to zero, get preempted, and then wipe a new execution's
/// owner id and early report once it resumed.
/// </summary>
internal void EndExecutionCount()
{
lock (m_Gate)
{
m_ActiveCount--;
if (m_ActiveCount == 0)
{
m_Explicit = default;
m_ExplicitOwnerId = null;
m_CurrentOwnerId = null;
}
}
}
/// <summary>
/// Report the current task's progress so a connected CLI can render it live.
/// Safe to call from any thread, at any frequency (the server samples on poll).
/// </summary>
/// <param name="title">Short task title, e.g. "Generating World".</param>
/// <param name="info">Detail line, e.g. "Processing 42/100".</param>
/// <param name="current">Current step, when the task has countable steps.</param>
/// <param name="total">Total steps, when the task has countable steps.</param>
/// <param name="progress">Completion in the 0–1 range; omit for indeterminate tasks
/// (when omitted and both <paramref name="current"/> and <paramref name="total"/> are
/// present, the CLI derives a percentage from them).</param>
internal void Report(string title, string info = null, long? current = null, long? total = null, double? progress = null)
{
var clamped = progress.HasValue ? Math.Max(0d, Math.Min(1d, progress.Value)) : (double?)null;
lock (m_Gate)
{
m_Explicit = new Snapshot(title, info, current, total, clamped);
m_ExplicitOwnerId = m_CurrentOwnerId;
}
}
/// <summary>Clear an explicit <see cref="Report"/> (e.g. when the reported task finishes).</summary>
internal void Clear()
{
lock (m_Gate)
{
m_Explicit = default;
m_ExplicitOwnerId = null;
}
}
/// <summary>Ambient-source mirror (UnityEditor.Progress items). Explicit reports win.</summary>
internal void ReportAmbient(string title, string info, long? current, long? total, double? progress01)
{
lock (m_Gate)
{
m_Ambient = new Snapshot(title, info, current, total, progress01);
}
}
/// <summary>Clear the ambient mirror (no running UnityEditor.Progress items remain).</summary>
internal void ClearAmbient()
{
lock (m_Gate)
{
m_Ambient = default;
}
}
/// <summary>
/// Marks the start of the execution identified by <paramref name="ownerId"/> (a job id,
/// or a synthetic id for a plain <c>/api/exec</c>). Clears any leftover explicit report
/// from whatever ran before it and becomes the only owner <see cref="Current"/> will
/// accept explicit reports from until <see cref="EndExecution"/> is called — so a queued
/// execution can never surface the previous one's progress before reporting its own.
/// </summary>
internal void BeginExecution(string ownerId)
{
lock (m_Gate)
{
m_CurrentOwnerId = ownerId;
m_Explicit = default;
m_ExplicitOwnerId = null;
}
}
/// <summary>
/// Marks the end of the execution identified by <paramref name="ownerId"/>. A no-op if
/// some other execution is already current (e.g. called after a swallowed exception),
/// so callers can call this unconditionally in a <c>finally</c>.
/// </summary>
internal void EndExecution(string ownerId)
{
lock (m_Gate)
{
if (m_CurrentOwnerId == ownerId)
{
m_CurrentOwnerId = null;
}
}
}
/// <summary>
/// The snapshot the /api/progress endpoint serves: the explicit report, but only when it
/// was made by the currently executing owner (see <see cref="BeginExecution"/>); falls
/// back to the ambient mirror otherwise.
/// </summary>
internal Snapshot Current
{
get
{
lock (m_Gate)
{
var explicitValid = m_Explicit.HasReport && m_ExplicitOwnerId == m_CurrentOwnerId;
return explicitValid ? m_Explicit : m_Ambient;
}
}
}
}
/// <summary>
/// Structured progress reporting for long-running pipeline commands — the server side of the
/// CLI's terminal progress bars (CLI-488, coordinated with CLI-335).
///
/// While a command executes over <c>/api/exec</c>, the CLI polls <c>GET /api/progress</c> and
/// renders whatever is reported here (a progress bar in a terminal, NDJSON progress frames in
/// machine formats). Command authors call <see cref="Report"/> from any thread — including a
/// main thread that is blocked inside a long synchronous command, which is exactly the
/// <c>EditorUtility.DisplayProgressBar</c> scenario this exists for. The state is read by the
/// HTTP listener thread, never the main thread, so progress stays visible even while the
/// Editor is busy.
///
/// This is a thin static façade over the executing server's own <see cref="CliProgressState"/>
/// (resolved via <see cref="BasePipelineServer.CurrentServer"/>) so command code — which has
/// no reference to "which server is running me" — can keep calling this the same way
/// regardless of which server dispatched it.
///
/// Ambient <c>UnityEditor.Progress</c> items are mirrored in by the Editor assembly
/// (<c>EditorProgressMirror</c>), targeting the live server directly; an explicit
/// <see cref="Report"/> always wins over the ambient mirror. Every execution (a plain
/// <c>/api/exec</c> or a detached job) is bracketed with BeginExecution/EndExecution on the
/// server's own <see cref="CliProgressState"/>, which tags who the current explicit report is
/// allowed to belong to — so a job queued behind one that just finished never inherits its
/// stale progress before making its own first report, no matter how long that job takes to
/// get around to reporting anything.
/// </summary>
public static class CliProgress
{
/// <summary>
/// Report the current task's progress so a connected CLI can render it live.
/// Safe to call from any thread, at any frequency (the server samples on poll). A no-op
/// when called outside a command's own execution (no server to attribute it to).
/// </summary>
/// <param name="title">Short title for the operation.</param>
/// <param name="info">Optional detail text (e.g. current step).</param>
/// <param name="current">Current progress amount, if determinate.</param>
/// <param name="total">Total amount, if determinate.</param>
/// <param name="progress">Progress in [0, 1], used instead of current/total when set.</param>
public static void Report(string title, string info = null, long? current = null, long? total = null, double? progress = null)
{
BasePipelineServer.CurrentServer?.Progress.Report(title, info, current, total, progress);
}
/// <summary>Clear an explicit <see cref="Report"/> (e.g. when the reported task finishes).</summary>
public static void Clear()
{
BasePipelineServer.CurrentServer?.Progress.Clear();
}
}
}