Skip to content
Edit on GitHub

Reproduce a bug headlessly

verifiedAgainst 119c2c3 · verifiedOn 2026-09-10 · the automated staleness banner is planned, not built.

Opening the full Unity Editor GUI to reproduce a bug, verify an asset pipeline step, or inspect an object state requires interactive window focus and cannot be scripted in CI. Most diagnostic and verification work in Voxamine is performed headlessly via Unity -batchmode -nographics -executeMethod.

Unlike worked examples W1–W5, which build permanent game features, this page teaches a core developer technique: authoring a throwaway diagnostic entry point, running it through the headless command line, and parsing logs back to repository file:line locations in seconds.

A headless diagnostic harness (HeadlessMeshAudit.cs) that:

  1. Opens the gameplay scene in batch mode without graphical overhead.
  2. Evaluates an in-memory system (e.g. verifying chunk meshing boundaries or terrain palette integrity).
  3. Emits structured diagnostic tokens ([diag] ...).
  4. Exits with a deterministic process code (0 for success, 1 for error) directly to the shell’s $?.
  5. Logs all output to an isolated, fresh log file for immediate grep triage.

Headless execution bypasses the entire rendering pipeline and window manager:

flowchart TD
  CLI["CLI Shell\nunity -batchmode -nographics -executeMethod"]
  BOOT["Unity Engine Bootstrap\nloads ProjectVersion.txt & assemblies"]
  LOCK["Lockfile Guard\nTemp/UnityLockfile check"]
  METHOD["Static Entry Point\nNamespace.Class.Method()"]
  LOG["Isolated Log File\nLogs/diagnostic.log"]
  EXIT["Process Exit Code\nEditorApplication.Exit(code) -> $?"]

  CLI --> LOCK
  LOCK --> BOOT
  BOOT --> METHOD
  METHOD --> LOG
  METHOD --> EXIT

  classDef cli fill:#1e1e24,stroke:#e58a63,stroke-width:1px,color:#f8f9fa;
  classDef engine fill:#0c4f48,stroke:#6fd8c6,stroke-width:1px,color:#f8f9fa;
  class CLI,EXIT cli;
  class BOOT,LOCK,METHOD,LOG engine;
  1. Explicit exit codes: The static method must call EditorApplication.Exit(0) on success and EditorApplication.Exit(1) on exception. Never rely on -quit alone; -quit masks internal failure states and can race with method completion.
  2. Quarantine to Editor assemblies: Diagnostic scripts live in Assets/_Game/Editor/ and are never packaged into standalone player builds.
  3. Single-lockfile rule: Only one Unity process (GUI or batch) can hold Temp/UnityLockfile at any time.

Read these files at pinned commit 119c2c3 before authoring:

# File Symbol / What to extract
1 Assets/_Game/Editor/VoxelWorkshop/Modules/ProjectDoctor/StandaloneReleaseBuilder.cs Command-line -executeMethod signature, argument extraction, and EditorApplication.Exit().
2 Assets/_Game/Editor/Gates/C1PGate.cs Headless verification gate execution pattern: [MenuItem] duality with batchmode runner.
3 Docs/BUILD_AND_TEST.md Canonical command-line arguments and batchmode flags across Linux/macOS/Windows.
Decision Choice Why
Graphics flag -nographics Use it for diagnostics that do not render. Omit it for HDRP capture, which needs a real graphics device.
Crash flag -silent-crashes Prevents Linux OS modal crash dialogues from hanging unattended terminal processes.
Log destination Dedicated Logs/<task>.log Prevents polluting the default Editor.log and guarantees a clean, single-session file.

Before launching a batch process, verify that no other Unity process is accessing the project:

Terminal window
# Check if a Unity process is currently running in this repository
pgrep -fl "Unity.*Minecraft-HD"
# Check for a stale lockfile
ls -l Temp/UnityLockfile 2>/dev/null

[!WARNING] If the Unity GUI is currently open on this project, batchmode will silently queue or block indefinitely until the GUI is closed. Close the GUI before launching headless diagnostic jobs.

Step 2: Write the diagnostic static method

Section titled “Step 2: Write the diagnostic static method”

Create a temporary or permanent diagnostic class in Assets/_Game/Editor/Diagnostics/HeadlessMeshAudit.cs:

using System;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using VoxelSandbox.Data.Blocks;
namespace VoxelSandbox.Editor.Diagnostics
{
public static class HeadlessMeshAudit
{
private const string Tag = "[diag-mesh]";
/// <summary>
/// Entry point: -executeMethod VoxelSandbox.Editor.Diagnostics.HeadlessMeshAudit.Run
/// </summary>
public static void Run()
{
try
{
Debug.Log($"{Tag} Starting headless mesh audit...");
// Optional: open a specific test or gameplay scene
// EditorSceneManager.OpenScene("Assets/_Game/Scenes/Gameplay.unity", OpenSceneMode.Single);
// Run your diagnostic assertions
int auditCount = 0;
var catalog = AssetDatabase.LoadAssetAtPath<BlockCatalog>("Assets/_Game/Data/Blocks/BlockCatalog.asset");
if (catalog == null)
{
throw new InvalidOperationException("BlockCatalog asset could not be loaded.");
}
Debug.Log($"{Tag} Loaded catalog with {catalog.Definitions.Count} blocks.");
// Check specific condition under investigation
for (int i = 0; i < catalog.Definitions.Count; i++)
{
var def = catalog.Definitions[i];
if (def != null)
{
auditCount++;
}
}
Debug.Log($"{Tag} PASS: Audited {auditCount} block definitions cleanly.");
EditorApplication.Exit(0);
}
catch (Exception exception)
{
Debug.LogError($"{Tag} FAIL: Diagnostic threw exception: {exception}");
EditorApplication.Exit(1);
}
}
}
}

Step 3: Extract custom command-line arguments (Optional)

Section titled “Step 3: Extract custom command-line arguments (Optional)”

If your diagnostic requires dynamic inputs (such as a world seed, chunk coordinate, or target file path), extract arguments directly from the raw process string:

private static string GetCustomArg(string flagName)
{
string[] args = Environment.GetCommandLineArgs();
for (int i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], flagName, StringComparison.OrdinalIgnoreCase))
{
return args[i + 1];
}
}
return null;
}

Run the headless command from the repository root:

Terminal window
/home/soulwax/Unity/Hub/Editor/6000.6.0f1/Editor/Unity \
-batchmode -nographics -silent-crashes \
-projectPath . \
-executeMethod VoxelSandbox.Editor.Diagnostics.HeadlessMeshAudit.Run \
-logFile Logs/mesh-audit.log
# Check the shell exit code immediately
echo "Exit Code: $?"
  • If $? is 0, your diagnostic passed.
  • If $? is 1 (or non-zero), the diagnostic caught an error or threw an unhandled exception.

When a headless run fails, do not open the 5,000-line log and scroll manually. Follow this 4-step triage sequence:

Compile errors (error CSxxxx) prevent -executeMethod from ever executing:

Terminal window
grep -n "error CS" Logs/mesh-audit.log

If compile errors exist, the output will give exact file and line numbers:

Assets/_Game/Editor/Diagnostics/HeadlessMeshAudit.cs(24,17): error CS0103: The name 'catalogg' does not exist in the current context

If compilation succeeded but the method failed:

Terminal window
grep -n -B 1 -A 10 "Exception:" Logs/mesh-audit.log

Look for the stack frame containing your code:

System.NullReferenceException: Object reference not set to an instance of an object
at VoxelSandbox.Editor.Diagnostics.HeadlessMeshAudit.Run () [0x00021] in Assets/_Game/Editor/Diagnostics/HeadlessMeshAudit.cs:33

To read only the output your harness emitted:

Terminal window
grep "\[diag-mesh\]" Logs/mesh-audit.log

Verify that running the harness produces:

  1. Exit code 0 on clean state.

  2. A single session log in Logs/mesh-audit.log starting with Unity header and ending with:

    [diag-mesh] Starting headless mesh audit...
    [diag-mesh] PASS: Audited <count> block definitions cleanly.
    Exiting without the bug being met

Use this checklist whenever isolating an issue headlessly:

  1. Locate the Unity binary: Check ProjectSettings/ProjectVersion.txt to match the exact editor patch version.
  2. Create the Editor class: Place in Assets/_Game/Editor/Diagnostics/ or the owning module’s Editor/ folder.
  3. Wrap in try/catch:
    • On success: EditorApplication.Exit(0).
    • On exception: log the full stack trace, then EditorApplication.Exit(1).
  4. Choose appropriate batch flags:
    • Always use -batchmode -nographics -silent-crashes.
    • Specify -logFile Logs/<name>.log.
    • Do not pass -quit when the method calls EditorApplication.Exit().
  5. Triage systematically: Run grep "error CS", then grep "Exception", then grep "\[your-tag\]".

Pitfall 1: Adding -quit alongside EditorApplication.Exit(code)

Section titled “Pitfall 1: Adding -quit alongside EditorApplication.Exit(code)”
  • Symptom: Bash reports random non-zero exit codes or the method is terminated mid-execution before logging results.
  • Cause: -quit instructs Unity to shut down asynchronously as soon as the initial command buffer clears, racing with your method’s synchronous execution.
  • Fix: Omit -quit. Let your method govern lifecycle via EditorApplication.Exit().

Pitfall 2: Silently blocking on Temp/UnityLockfile

Section titled “Pitfall 2: Silently blocking on Temp/UnityLockfile”
  • Symptom: The terminal command hangs forever with 0% CPU usage.
  • Cause: An open Unity GUI instance or a crashed previous batch run left an active lockfile in Temp/.
  • Fix: Close the GUI Editor and confirm no Unity process owns the project. Do not delete a lockfile while another Editor may still be starting or running; reopen Unity only after confirming the lock is stale.

Pitfall 3: Investigating exceptions before checking compile errors

Section titled “Pitfall 3: Investigating exceptions before checking compile errors”
  • Symptom: Spending 20 minutes chasing an old exception in the log when the method didn’t even run.
  • Cause: When C# scripts fail to compile, Unity logs the compile error and aborts without executing -executeMethod.
  • Fix: Always run grep "error CS" before searching for runtime exceptions.

Pitfall 4: Placing headless tools outside an Editor/ folder

Section titled “Pitfall 4: Placing headless tools outside an Editor/ folder”
  • Symptom: Standalone release build fails with The type or namespace name 'UnityEditor' could not be found.
  • Cause: Scripts referencing UnityEditor or EditorApplication were placed in a runtime assembly.
  • Fix: Ensure the file resides within an Editor/ folder governed by an editor-only assembly definition ("includePlatforms": ["Editor"]).

User-contributed notes

Corrections, clarifications, and practical tips for this page. Anonymous is fine — a name is optional. Basic Markdown works: **bold**, *italic*, `code`, and links.

Notes policy

Notes are lightly filtered for spam and may be edited or removed. Keep them about this page — no support requests, no personal data, nothing you would not publish. Links are limited and marked nofollow.

  1. Loading notes…