using System;
using System.IO;
using System.Linq;
using Ashfall.Core;
using UnityEngine;
namespace Ashfall.Runtime
{
// Explicit opt-in test runner. It never uses the normal world save.
public sealed class SmokeHarness : MonoBehaviour
{
public static bool Enabled => Environment.GetCommandLineArgs().Contains("--smoke");
private GameSession session;
private float start, scenarioStart = -1;
private bool completed, captured;
private V2 initialVehicle;
private int errors;
private static string Output
{
get { var a = Environment.GetCommandLineArgs(); int i = Array.IndexOf(a, "--smoke-output"); return i >= 0 && i + 1 < a.Length ? a[i + 1] : Application.persistentDataPath; }
}
private void Start()
{
session = GetComponent<GameSession>(); start = Time.realtimeSinceStartup;
Application.logMessageReceived += Log;
}
private void Log(string message, string stack, LogType kind)
{ if (kind == LogType.Error || kind == LogType.Exception || kind == LogType.Assert) errors++; }
public static PlayerInput InputFor(GameSession s)
{
var result = new PlayerInput(); var p = s.LocalPlayer;
if (p == null || s.World == null) return result;
var v = s.World.Vehicle("crawler");
result.aim = v.position + new V2(30, 10);
if (p.stationId != "")
{
result.move = s.World.time < 18 ? new V2(0, 1) : new V2();
result.brake = s.World.time >= 18; result.fire = result.secondary = true;
}
else if (p.location.spaceId == v.spaceId)
{
int index = s.World.players.IndexOf(p);
V2 target = index == 3 && p.location.floor == 0 ? new V2(6, -2) : new V2(0, index == 2 ? -1 : 0);
V2 diff = target - p.location.position; result.move = diff.Length > .12f ? diff.Normalized : new V2();
}
return result;
}
private void Update()
{
if (completed) return;
if (Time.realtimeSinceStartup - start > 100) { Complete(false, "connection/scenario timeout"); return; }
if (!session.Running || session.LocalPlayer == null) return;
if (!session.IsAuthority)
{
if (session.World.time >= 29) Complete(session.World.players.Count(p => p.connected) == 4 && session.LocalPlayer.acknowledgedInput > 0 && errors == 0, "client received world and acknowledged inputs");
return;
}
var w = session.World; var sim = session.Simulation; var v = w.Vehicle("crawler");
if (scenarioStart < 0 && w.players.Count(p => p.connected) == 4)
{
scenarioStart = Time.realtimeSinceStartup; initialVehicle = v.position;
// Controlled fixtures isolate network and rendering behavior from traversal already covered by unit tests.
for (int i = 0; i < 4; i++)
{
var p = w.players[i]; p.location = WorldFactory.At(v.spaceId, i < 2 ? 1 : 0, new V2(i < 2 ? 0 : -1, 0));
if (i < 2)
{
var station = v.stations[i]; p.location = station.location.Copy();
var r = sim.Execute(p.id, new GameCommand { sequence = p.lastCommand + 1, kind = CommandKind.Interact, targetId = station.id });
if (!r.success) { Complete(false, r.message); return; }
}
}
sim.CriticalChange?.Invoke();
}
if (scenarioStart < 0) return;
if (w.time > 20 && !captured)
{
captured = true; Directory.CreateDirectory(Output);
if (SystemInfo.graphicsDeviceType != UnityEngine.Rendering.GraphicsDeviceType.Null)
CaptureCamera(Path.Combine(Output, "prototype-exterior.png"));
}
if (w.time < 32) return;
bool success = w.players.Count(p => p.connected) == 4 && V2.Distance(initialVehicle, v.position) > 4
&& v.cargo.Count("ammo") < 600 && w.players[2].location.floor == 0 && w.players[3].location.floor == 1
&& w.players.Skip(1).All(p => p.IsWorm && p.acknowledgedInput > 0) && errors == 0;
session.Save();
var restored = new SaveStore(session.SavePath, new UnityWorldCodec()).Load();
success &= restored.players.Count == 4 && restored.Vehicle("crawler").position.x == v.position.x
&& restored.players[3].location.floor == 1;
Complete(success, $"players={w.players.Count}, distance={V2.Distance(initialVehicle, v.position):0.0}, ammo={v.cargo.Count("ammo")}, floors={string.Join(",", w.players.Select(p => p.location.floor))}, errors={errors}");
}
private void Complete(bool success, string reason)
{
completed = true; Directory.CreateDirectory(Output);
string role = session != null && session.IsAuthority ? "host" : session?.PlayerId ?? "unknown";
File.WriteAllText(Path.Combine(Output, "smoke-" + role + ".txt"), (success ? "PASS " : "FAIL ") + reason);
Debug.Log("ASHFALL_SMOKE " + (success ? "PASS " : "FAIL ") + reason);
exitCode = success ? 0 : 1;
if (session != null && !session.IsAuthority && success) Invoke(nameof(BeginExit), 8);
else BeginExit();
}
private int exitCode;
private void BeginExit()
{
session?.Stop(); GetComponent<WorldView>()?.ReleasePhysics();
// Let asynchronous scene unloads and transport shutdown drain before quitting.
Invoke(nameof(Exit), .5f);
}
private void Exit() => Application.Quit(exitCode);
private void CaptureCamera(string path)
{
var camera = GetComponent<WorldView>().Camera;
var target = RenderTexture.GetTemporary(1280, 800, 24);
var oldTarget = camera.targetTexture; var oldActive = RenderTexture.active;
Texture2D pixels = null;
try
{
camera.targetTexture = target; camera.Render(); RenderTexture.active = target;
pixels = new Texture2D(1280, 800, TextureFormat.RGB24, false);
pixels.ReadPixels(new Rect(0, 0, 1280, 800), 0, 0); pixels.Apply();
File.WriteAllBytes(path, pixels.EncodeToPNG());
}
finally
{
camera.targetTexture = oldTarget; RenderTexture.active = oldActive;
RenderTexture.ReleaseTemporary(target); if (pixels != null) Destroy(pixels);
}
}
private void OnDestroy() => Application.logMessageReceived -= Log;
}
}
using System;
using System.IO;
using System.Linq;
using Ashfall.Core;
using UnityEngine;
namespace Ashfall.Runtime
{
// Explicit opt-in test runner. It never uses the normal world save.
public sealed class SmokeHarness : MonoBehaviour
{
public static bool Enabled => Environment.GetCommandLineArgs().Contains("--smoke");
private GameSession session;
private float start, scenarioStart = -1;
private bool completed, captured;
private V2 initialVehicle;
private int errors;
private static string Output
{
get { var a = Environment.GetCommandLineArgs(); int i = Array.IndexOf(a, "--smoke-output"); return i >= 0 && i + 1 < a.Length ? a[i + 1] : Application.persistentDataPath; }
}
private void Start()
{
session = GetComponent<GameSession>(); start = Time.realtimeSinceStartup;
Application.logMessageReceived += Log;
}
private void Log(string message, string stack, LogType kind)
{ if (kind == LogType.Error || kind == LogType.Exception || kind == LogType.Assert) errors++; }
public static PlayerInput InputFor(GameSession s)
{
var result = new PlayerInput(); var p = s.LocalPlayer;
if (p == null || s.World == null) return result;
var v = s.World.Vehicle("crawler");
result.aim = v.position + new V2(30, 10);
if (p.stationId != "")
{
result.move = s.World.time < 18 ? new V2(0, 1) : new V2();
result.brake = s.World.time >= 18; result.fire = result.secondary = true;
}
else if (p.location.spaceId == v.spaceId)
{
int index = s.World.players.IndexOf(p);
V2 target = index == 3 && p.location.floor == 0 ? new V2(6, -2) : new V2(0, index == 2 ? -1 : 0);
V2 diff = target - p.location.position; result.move = diff.Length > .12f ? diff.Normalized : new V2();
}
return result;
}
private void Update()
{
if (completed) return;
if (Time.realtimeSinceStartup - start > 100) { Complete(false, "connection/scenario timeout"); return; }
if (!session.Running || session.LocalPlayer == null) return;
if (!session.IsAuthority)
{
if (session.World.time >= 29) Complete(session.World.players.Count(p => p.connected) == 4 && session.LocalPlayer.acknowledgedInput > 0 && errors == 0, "client received world and acknowledged inputs");
return;
}
var w = session.World; var sim = session.Simulation; var v = w.Vehicle("crawler");
if (scenarioStart < 0 && w.players.Count(p => p.connected) == 4)
{
scenarioStart = Time.realtimeSinceStartup; initialVehicle = v.position;
// Controlled fixtures isolate network and rendering behavior from traversal already covered by unit tests.
for (int i = 0; i < 4; i++)
{
var p = w.players[i]; p.location = WorldFactory.At(v.spaceId, i < 2 ? 1 : 0, new V2(i < 2 ? 0 : -1, 0));
if (i < 2)
{
var station = v.stations[i]; p.location = station.location.Copy();
var r = sim.Execute(p.id, new GameCommand { sequence = p.lastCommand + 1, kind = CommandKind.Interact, targetId = station.id });
if (!r.success) { Complete(false, r.message); return; }
}
}
sim.CriticalChange?.Invoke();
}
if (scenarioStart < 0) return;
if (w.time > 20 && !captured)
{
captured = true; Directory.CreateDirectory(Output);
if (SystemInfo.graphicsDeviceType != UnityEngine.Rendering.GraphicsDeviceType.Null)
CaptureCamera(Path.Combine(Output, "prototype-exterior.png"));
}
if (w.time < 32) return;
bool success = w.players.Count(p => p.connected) == 4 && V2.Distance(initialVehicle, v.position) > 4
&& v.cargo.Count("ammo") < 600 && w.players[2].location.floor == 0 && w.players[3].location.floor == 1
&& w.players.Skip(1).All(p => p.IsWorm && p.acknowledgedInput > 0) && errors == 0;
session.Save();
var restored = new SaveStore(session.SavePath, new UnityWorldCodec()).Load();
success &= restored.players.Count == 4 && restored.Vehicle("crawler").position.x == v.position.x
&& restored.players[3].location.floor == 1;
Complete(success, $"players={w.players.Count}, distance={V2.Distance(initialVehicle, v.position):0.0}, ammo={v.cargo.Count("ammo")}, floors={string.Join(",", w.players.Select(p => p.location.floor))}, errors={errors}");
}
private void Complete(bool success, string reason)
{
completed = true; Directory.CreateDirectory(Output);
string role = session != null && session.IsAuthority ? "host" : session?.PlayerId ?? "unknown";
File.WriteAllText(Path.Combine(Output, "smoke-" + role + ".txt"), (success ? "PASS " : "FAIL ") + reason);
Debug.Log("ASHFALL_SMOKE " + (success ? "PASS " : "FAIL ") + reason);
exitCode = success ? 0 : 1;
if (session != null && !session.IsAuthority && success) Invoke(nameof(BeginExit), 8);
else BeginExit();
}
private int exitCode;
private void BeginExit()
{
session?.Stop(); GetComponent<WorldView>()?.ReleasePhysics();
// Let asynchronous scene unloads and transport shutdown drain before quitting.
Invoke(nameof(Exit), .5f);
}
private void Exit() => Application.Quit(exitCode);
private void CaptureCamera(string path)
{
var camera = GetComponent<WorldView>().Camera;
var target = RenderTexture.GetTemporary(1280, 800, 24);
var oldTarget = camera.targetTexture; var oldActive = RenderTexture.active;
Texture2D pixels = null;
try
{
camera.targetTexture = target; camera.Render(); RenderTexture.active = target;
pixels = new Texture2D(1280, 800, TextureFormat.RGB24, false);
pixels.ReadPixels(new Rect(0, 0, 1280, 800), 0, 0); pixels.Apply();
File.WriteAllBytes(path, pixels.EncodeToPNG());
}
finally
{
camera.targetTexture = oldTarget; RenderTexture.active = oldActive;
RenderTexture.ReleaseTemporary(target); if (pixels != null) Destroy(pixels);
}
}
private void OnDestroy() => Application.logMessageReceived -= Log;
}
}