2026-07-17 14:48:24 +01:00
using System ;
using System.Diagnostics ;
using System.Globalization ;
using System.IO ;
using GameCult.Eve.UnityScene ;
using UnityEditor ;
2026-07-18 16:43:50 +01:00
using UnityEditor.SceneManagement ;
2026-07-17 14:48:24 +01:00
using UnityEngine ;
2026-07-18 16:43:50 +01:00
using UnityEngine.SceneManagement ;
2026-07-17 14:48:24 +01:00
using Debug = UnityEngine . Debug ;
namespace Aetheria.Editor
{
public sealed class AetheriaDaemonDevelopmentWindow : EditorWindow
{
private const string MenuPath = "Aetheria/Daemon Development" ;
private const string PortPreference = "Aetheria.DaemonDevelopment.Port" ;
private const string AutoStartPreference = "Aetheria.DaemonDevelopment.AutoStart" ;
private const string StopAfterPlayPreference = "Aetheria.DaemonDevelopment.StopAfterPlay" ;
2026-07-18 16:43:50 +01:00
internal const string ClientScenePath = "Assets/Aetheria.unity" ;
2026-07-17 14:48:24 +01:00
private Vector2 _scroll ;
[MenuItem(MenuPath)]
public static void Open ( ) = > GetWindow < AetheriaDaemonDevelopmentWindow > ( "Aetheria Daemon" ) ;
2026-07-18 16:43:50 +01:00
public static void StartAndPlay ( )
{
Open ( ) ;
EnsureClientSceneLoaded ( ) ;
AetheriaDaemonDevelopmentController . Start ( enterPlayWhenReady : true ) ;
}
public static void OpenClientScene ( )
{
Open ( ) ;
EnsureClientSceneLoaded ( ) ;
}
internal static bool IsClientSceneLoaded ( )
{
for ( var index = 0 ; index < SceneManager . sceneCount ; index + + )
{
if ( string . Equals ( SceneManager . GetSceneAt ( index ) . path , ClientScenePath , StringComparison . Ordinal ) )
return true ;
}
return false ;
}
internal static void EnsureClientSceneLoaded ( )
{
for ( var index = 0 ; index < SceneManager . sceneCount ; index + + )
{
var scene = SceneManager . GetSceneAt ( index ) ;
if ( ! string . Equals ( scene . path , ClientScenePath , StringComparison . Ordinal ) ) continue ;
SceneManager . SetActiveScene ( scene ) ;
return ;
}
var clientScene = EditorSceneManager . OpenScene ( ClientScenePath , OpenSceneMode . Additive ) ;
SceneManager . SetActiveScene ( clientScene ) ;
}
2026-07-17 14:48:24 +01:00
private void OnEnable ( )
{
minSize = new Vector2 ( 470 , 390 ) ;
AetheriaDaemonDevelopmentController . Changed + = Repaint ;
}
private void OnDisable ( ) = > AetheriaDaemonDevelopmentController . Changed - = Repaint ;
private void OnGUI ( )
{
_scroll = EditorGUILayout . BeginScrollView ( _scroll ) ;
EditorGUILayout . Space ( 8 ) ;
EditorGUILayout . LabelField ( "Authoritative daemon" , EditorStyles . boldLabel ) ;
EditorGUILayout . HelpBox (
"This window owns only the local development process. Gameplay, pause, stepping, and world truth remain daemon-owned Eve operations." ,
MessageType . Info ) ;
var port = EditorPrefs . GetInt ( PortPreference , 3076 ) ;
var nextPort = EditorGUILayout . IntField ( "CultMesh port" , port ) ;
if ( nextPort ! = port & & nextPort is > 0 and < = 65535 )
EditorPrefs . SetInt ( PortPreference , nextPort ) ;
var autoStart = EditorPrefs . GetBool ( AutoStartPreference , true ) ;
var nextAutoStart = EditorGUILayout . Toggle ( "Start before Play" , autoStart ) ;
if ( nextAutoStart ! = autoStart )
EditorPrefs . SetBool ( AutoStartPreference , nextAutoStart ) ;
var stopAfterPlay = EditorPrefs . GetBool ( StopAfterPlayPreference , false ) ;
var nextStopAfterPlay = EditorGUILayout . Toggle ( "Stop after Play" , stopAfterPlay ) ;
if ( nextStopAfterPlay ! = stopAfterPlay )
EditorPrefs . SetBool ( StopAfterPlayPreference , nextStopAfterPlay ) ;
EditorGUILayout . Space ( 6 ) ;
EditorGUILayout . LabelField ( "Status" , AetheriaDaemonDevelopmentController . Status ) ;
EditorGUILayout . LabelField ( "Endpoint" , AetheriaDaemonDevelopmentController . Endpoint ) ;
EditorGUILayout . LabelField ( "PID" , AetheriaDaemonDevelopmentController . ProcessIdText ) ;
EditorGUILayout . LabelField ( "State" , AetheriaDaemonDevelopmentController . StatePath , EditorStyles . wordWrappedLabel ) ;
EditorGUILayout . LabelField ( "CultLib source" , AetheriaDaemonDevelopmentController . CultLibRoot , EditorStyles . wordWrappedLabel ) ;
EditorGUILayout . LabelField ( "Ymir source" , AetheriaDaemonDevelopmentController . YmirRoot , EditorStyles . wordWrappedLabel ) ;
EditorGUILayout . Space ( 8 ) ;
2026-07-18 16:43:50 +01:00
if ( GUILayout . Button ( "Open Aetheria client scene" ) )
OpenClientScene ( ) ;
EditorGUILayout . Space ( 4 ) ;
2026-07-17 14:48:24 +01:00
using ( new EditorGUILayout . HorizontalScope ( ) )
{
GUI . enabled = ! AetheriaDaemonDevelopmentController . IsStarting & &
! AetheriaDaemonDevelopmentController . IsRunning ;
if ( GUILayout . Button ( "Start daemon" ) )
AetheriaDaemonDevelopmentController . Start ( enterPlayWhenReady : false ) ;
if ( GUILayout . Button ( "Start & Play" ) )
2026-07-18 16:43:50 +01:00
StartAndPlay ( ) ;
2026-07-17 14:48:24 +01:00
GUI . enabled = AetheriaDaemonDevelopmentController . IsRunning | |
AetheriaDaemonDevelopmentController . IsStarting ;
if ( GUILayout . Button ( "Stop" ) )
AetheriaDaemonDevelopmentController . Stop ( ) ;
GUI . enabled = true ;
}
using ( new EditorGUILayout . HorizontalScope ( ) )
{
GUI . enabled = AetheriaDaemonDevelopmentController . IsRunning ;
if ( GUILayout . Button ( "Restart" ) )
AetheriaDaemonDevelopmentController . Restart ( ) ;
GUI . enabled = ! AetheriaDaemonDevelopmentController . IsRunning & &
! AetheriaDaemonDevelopmentController . IsStarting ;
if ( GUILayout . Button ( "Reimport state & start" ) )
AetheriaDaemonDevelopmentController . Start ( enterPlayWhenReady : false , forceImport : true ) ;
GUI . enabled = true ;
}
EditorGUILayout . Space ( 12 ) ;
EditorGUILayout . LabelField ( "Authoritative simulation" , EditorStyles . boldLabel ) ;
using ( new EditorGUILayout . HorizontalScope ( ) )
{
GUI . enabled = EditorApplication . isPlaying & & AetheriaDaemonDevelopmentController . IsRunning ;
if ( GUILayout . Button ( "Pause daemon" ) )
AetheriaDaemonDevelopmentController . SubmitClockAction ( "simulation.pause" ) ;
if ( GUILayout . Button ( "Advance one step" ) )
AetheriaDaemonDevelopmentController . SubmitClockAction ( "simulation.step" ) ;
if ( GUILayout . Button ( "Resume real time" ) )
AetheriaDaemonDevelopmentController . SubmitClockAction ( "simulation.rate.realtime" ) ;
GUI . enabled = true ;
}
EditorGUILayout . HelpBox (
"Unity's Pause button is mirrored to simulation.pause. Unpause submits simulation.rate.realtime. Advance One Step remains available here while the Unity player is paused." ,
MessageType . None ) ;
EditorGUILayout . Space ( 12 ) ;
EditorGUILayout . LabelField ( "Rider" , EditorStyles . boldLabel ) ;
using ( new EditorGUILayout . HorizontalScope ( ) )
{
GUI . enabled = AetheriaDaemonDevelopmentController . IsRunning ;
if ( GUILayout . Button ( "Copy daemon PID" ) )
AetheriaDaemonDevelopmentController . CopyProcessId ( ) ;
GUI . enabled = true ;
if ( GUILayout . Button ( "Open daemon source" ) )
AetheriaDaemonDevelopmentController . OpenDaemonSource ( ) ;
}
EditorGUILayout . HelpBox (
"In Rider use Run | Attach to Process and select the copied Aetheria.State.Daemon PID. The daemon is built and launched from bin/Debug/net10.0, so source breakpoints bind to this checkout." ,
MessageType . None ) ;
EditorGUILayout . Space ( 12 ) ;
using ( new EditorGUILayout . HorizontalScope ( ) )
{
if ( GUILayout . Button ( "Open daemon log" ) )
AetheriaDaemonDevelopmentController . OpenLog ( ) ;
if ( GUILayout . Button ( "Open error log" ) )
AetheriaDaemonDevelopmentController . OpenErrorLog ( ) ;
}
if ( ! string . IsNullOrWhiteSpace ( AetheriaDaemonDevelopmentController . LastError ) )
EditorGUILayout . HelpBox ( AetheriaDaemonDevelopmentController . LastError , MessageType . Error ) ;
EditorGUILayout . EndScrollView ( ) ;
}
}
[InitializeOnLoad]
internal static class AetheriaDaemonDevelopmentController
{
private const string ProcessIdSessionKey = "Aetheria.DaemonDevelopment.ProcessId" ;
private const string PortPreference = "Aetheria.DaemonDevelopment.Port" ;
private const string AutoStartPreference = "Aetheria.DaemonDevelopment.AutoStart" ;
private const string StopAfterPlayPreference = "Aetheria.DaemonDevelopment.StopAfterPlay" ;
private static Process _launcher ;
private static Process _daemon ;
private static bool _enterPlayWhenReady ;
private static bool _restartWhenStopped ;
private static string _pendingClockAction = "" ;
private static string _status = "Stopped" ;
private static string _lastError = "" ;
public static event Action Changed ;
static AetheriaDaemonDevelopmentController ( )
{
EditorApplication . update + = Update ;
EditorApplication . quitting + = Stop ;
EditorApplication . playModeStateChanged + = OnPlayModeStateChanged ;
EditorApplication . pauseStateChanged + = OnPauseStateChanged ;
Reattach ( ) ;
ApplyClientEnvironment ( ) ;
}
public static bool IsRunning = > TryRefreshDaemon ( ) ;
public static bool IsStarting = > _launcher ! = null & & ! _launcher . HasExited ;
public static string Status = > _status ;
public static string LastError = > _lastError ;
2026-07-20 04:32:33 +01:00
public static string Endpoint = > $"cultnet+tcp://127.0.0.1:{Port}" ;
2026-07-17 14:48:24 +01:00
public static string ProcessIdText = > IsRunning ? _daemon . Id . ToString ( CultureInfo . InvariantCulture ) : "-" ;
public static string StatePath = > Path . Combine ( ProjectRoot , "Aetheria.Unity" , "Build" , "aetheria-unity-dev.cc" ) ;
2026-07-20 04:32:33 +01:00
public static string CultLibRoot = > ResolveSibling ( "CultLib-codex-cultmesh-reliability" , "CultLib-release" , "CultLib" ) ;
2026-07-17 14:48:24 +01:00
public static string YmirRoot = > ResolveSibling ( "Ymir-aetheria-integration" , "Ymir" ) ;
public static string EveUnityRoot = > ResolveSibling ( "EveUnity" ) ;
private static int Port = > EditorPrefs . GetInt ( PortPreference , 3076 ) ;
private static string ProjectRoot = > Path . GetFullPath ( Path . Combine ( Application . dataPath , ".." , ".." ) ) ;
private static string ArtifactsRoot = > Path . Combine ( ProjectRoot , "Aetheria.Unity" , "Build" , "DaemonDevelopment" ) ;
private static string PidPath = > Path . Combine ( ArtifactsRoot , "daemon.pid" ) ;
private static string LogPath = > Path . Combine ( ArtifactsRoot , "daemon.log" ) ;
private static string ErrorLogPath = > Path . Combine ( ArtifactsRoot , "daemon.error.log" ) ;
private static string LauncherLogPath = > Path . Combine ( ArtifactsRoot , "launcher.log" ) ;
private static string LauncherErrorPath = > Path . Combine ( ArtifactsRoot , "launcher.error.log" ) ;
public static void Start ( bool enterPlayWhenReady , bool forceImport = false )
{
if ( TryRefreshDaemon ( ) )
{
ApplyClientEnvironment ( ) ;
if ( enterPlayWhenReady & & ! EditorApplication . isPlaying )
EditorApplication . isPlaying = true ;
return ;
}
if ( IsStarting )
{
_enterPlayWhenReady | = enterPlayWhenReady ;
return ;
}
Directory . CreateDirectory ( ArtifactsRoot ) ;
File . Delete ( PidPath ) ;
_lastError = "" ;
_enterPlayWhenReady = enterPlayWhenReady ;
ApplyClientEnvironment ( ) ;
var script = Path . Combine ( ProjectRoot , "scripts" , "start-aetheria-daemon-editor.ps1" ) ;
var arguments = $"-NoProfile -ExecutionPolicy Bypass -File {Quote(script)} " +
$"-Root {Quote(ProjectRoot)} -State {Quote(StatePath)} -Port {Port} " +
$"-PidFile {Quote(PidPath)} -LogFile {Quote(LogPath)} -ErrorLogFile {Quote(ErrorLogPath)} " +
$"-CultLibRoot {Quote(CultLibRoot)} -YmirRoot {Quote(YmirRoot)} -EveUnityRoot {Quote(EveUnityRoot)}" +
( forceImport ? " -ForceImport" : "" ) ;
_launcher = Process . Start ( new ProcessStartInfo ( "powershell.exe" , arguments )
{
WorkingDirectory = ProjectRoot ,
UseShellExecute = false ,
CreateNoWindow = true ,
RedirectStandardOutput = true ,
RedirectStandardError = true
} ) ;
_launcher . OutputDataReceived + = ( _ , value ) = > AppendLine ( LauncherLogPath , value . Data ) ;
_launcher . ErrorDataReceived + = ( _ , value ) = > AppendLine ( LauncherErrorPath , value . Data ) ;
_launcher . BeginOutputReadLine ( ) ;
_launcher . BeginErrorReadLine ( ) ;
_status = forceImport ? "Reimporting state and building Debug daemon..." : "Building Debug daemon..." ;
Changed ? . Invoke ( ) ;
}
public static void Stop ( )
{
_enterPlayWhenReady = false ;
_pendingClockAction = "" ;
try
{
if ( _launcher ! = null & & ! _launcher . HasExited )
_launcher . Kill ( ) ;
}
catch ( Exception exception )
{
Debug . LogWarning ( $"Could not stop Aetheria daemon launcher: {exception.Message}" ) ;
}
try
{
if ( TryRefreshDaemon ( ) )
_daemon . Kill ( ) ;
}
catch ( Exception exception )
{
Debug . LogWarning ( $"Could not stop Aetheria daemon: {exception.Message}" ) ;
}
_launcher ? . Dispose ( ) ;
_launcher = null ;
_daemon ? . Dispose ( ) ;
_daemon = null ;
SessionState . EraseInt ( ProcessIdSessionKey ) ;
TryDelete ( PidPath ) ;
_status = "Stopped" ;
Changed ? . Invoke ( ) ;
}
public static void Restart ( )
{
_restartWhenStopped = true ;
Stop ( ) ;
}
public static void SubmitClockAction ( string actionId )
{
_pendingClockAction = actionId ? ? "" ;
TrySubmitPendingClockAction ( ) ;
}
public static void CopyProcessId ( )
{
if ( ! TryRefreshDaemon ( ) ) return ;
EditorGUIUtility . systemCopyBuffer = _daemon . Id . ToString ( CultureInfo . InvariantCulture ) ;
_status = $"Copied daemon PID {_daemon.Id} for Rider Attach to Process" ;
Changed ? . Invoke ( ) ;
}
public static void OpenDaemonSource ( )
{
var source = Path . Combine ( ProjectRoot , "Aetheria.State.Daemon" , "Program.cs" ) ;
EditorUtility . OpenWithDefaultApp ( source ) ;
}
public static void OpenLog ( ) = > OpenFile ( LogPath ) ;
public static void OpenErrorLog ( ) = > OpenFile ( ErrorLogPath ) ;
private static void Update ( )
{
if ( _restartWhenStopped )
{
_restartWhenStopped = false ;
Start ( enterPlayWhenReady : EditorApplication . isPlaying ) ;
return ;
}
if ( _launcher ! = null & & _launcher . HasExited )
{
var exitCode = _launcher . ExitCode ;
_launcher . Dispose ( ) ;
_launcher = null ;
if ( exitCode ! = 0 & & ! TryRefreshDaemon ( ) )
{
_lastError = ReadTail ( LauncherErrorPath , 24 ) ;
_status = $"Daemon preparation failed (exit {exitCode})" ;
_enterPlayWhenReady = false ;
Changed ? . Invoke ( ) ;
return ;
}
}
if ( TryRefreshDaemon ( ) & & IsEndpointReady ( ) )
{
if ( ! _status . StartsWith ( "Running" , StringComparison . Ordinal ) )
{
_status = $"Running Debug daemon (PID {_daemon.Id})" ;
Changed ? . Invoke ( ) ;
}
ApplyClientEnvironment ( ) ;
if ( _enterPlayWhenReady & & ! EditorApplication . isPlayingOrWillChangePlaymode )
{
_enterPlayWhenReady = false ;
EditorApplication . isPlaying = true ;
}
TrySubmitPendingClockAction ( ) ;
}
2026-07-19 01:13:33 +01:00
else if ( TryTakeDaemonExit ( out var exitCode ) )
2026-07-17 14:48:24 +01:00
{
_status = $"Daemon exited ({exitCode})" ;
_lastError = ReadTail ( ErrorLogPath , 24 ) ;
Changed ? . Invoke ( ) ;
}
}
private static void OnPlayModeStateChanged ( PlayModeStateChange state )
{
if ( state = = PlayModeStateChange . ExitingEditMode & &
2026-07-18 16:43:50 +01:00
! AetheriaDaemonDevelopmentWindow . IsClientSceneLoaded ( ) )
{
EditorApplication . isPlaying = false ;
EditorApplication . delayCall + = PrepareClientSceneAndPlay ;
}
else if ( state = = PlayModeStateChange . ExitingEditMode & &
EditorPrefs . GetBool ( AutoStartPreference , true ) & & ! TryRefreshDaemon ( ) )
2026-07-17 14:48:24 +01:00
{
EditorApplication . isPlaying = false ;
Start ( enterPlayWhenReady : true ) ;
}
else if ( state = = PlayModeStateChange . EnteredEditMode & &
EditorPrefs . GetBool ( StopAfterPlayPreference , false ) )
{
Stop ( ) ;
}
}
2026-07-18 16:43:50 +01:00
private static void PrepareClientSceneAndPlay ( )
{
AetheriaDaemonDevelopmentWindow . EnsureClientSceneLoaded ( ) ;
if ( EditorPrefs . GetBool ( AutoStartPreference , true ) )
Start ( enterPlayWhenReady : true ) ;
else
EditorApplication . isPlaying = true ;
}
2026-07-17 14:48:24 +01:00
private static void OnPauseStateChanged ( PauseState state )
{
if ( ! EditorApplication . isPlaying | | ! TryRefreshDaemon ( ) ) return ;
SubmitClockAction ( state = = PauseState . Paused
? "simulation.pause"
: "simulation.rate.realtime" ) ;
}
private static void TrySubmitPendingClockAction ( )
{
if ( string . IsNullOrWhiteSpace ( _pendingClockAction ) | | ! EditorApplication . isPlaying ) return ;
var bootstrap = UnityEngine . Object . FindFirstObjectByType < EveUnityPlayableWorldClientBootstrap > ( ) ;
var host = bootstrap ? . Host ;
var entityId = host ? . ActiveWorld ? . PlayerEntityId ;
if ( host = = null | | string . IsNullOrWhiteSpace ( entityId ) | | host . InputCapability = = null ) return ;
try
{
var request = host . SubmitAdvertisedActionIntent ( entityId , _pendingClockAction ) ;
_status = $"Submitted {_pendingClockAction} ({request.CommandId})" ;
_pendingClockAction = "" ;
Changed ? . Invoke ( ) ;
}
catch ( Exception exception )
{
_lastError = $"Could not submit daemon clock action '{_pendingClockAction}': {exception.Message}" ;
_pendingClockAction = "" ;
Changed ? . Invoke ( ) ;
}
}
private static bool TryRefreshDaemon ( )
{
2026-07-19 01:13:33 +01:00
if ( _daemon = = null )
TryAttachPidFile ( ) ;
2026-07-17 14:48:24 +01:00
if ( _daemon = = null ) return false ;
2026-07-19 01:13:33 +01:00
try
{
if ( _daemon . HasExited ) return false ;
if ( string . Equals ( _daemon . ProcessName , "Aetheria.State.Daemon" , StringComparison . OrdinalIgnoreCase ) )
return true ;
}
catch ( InvalidOperationException )
{
}
catch ( System . ComponentModel . Win32Exception )
{
}
ClearDaemonReference ( ) ;
return false ;
}
private static void ClearDaemonReference ( )
{
_daemon ? . Dispose ( ) ;
_daemon = null ;
SessionState . EraseInt ( ProcessIdSessionKey ) ;
}
private static bool TryTakeDaemonExit ( out int exitCode )
{
exitCode = 0 ;
if ( _daemon = = null ) return false ;
try
{
if ( ! _daemon . HasExited ) return false ;
exitCode = _daemon . ExitCode ;
}
catch ( InvalidOperationException )
{
ClearDaemonReference ( ) ;
return false ;
}
catch ( System . ComponentModel . Win32Exception )
{
ClearDaemonReference ( ) ;
return false ;
}
ClearDaemonReference ( ) ;
return true ;
2026-07-17 14:48:24 +01:00
}
private static void Reattach ( )
{
var processId = SessionState . GetInt ( ProcessIdSessionKey , - 1 ) ;
2026-07-19 01:13:33 +01:00
if ( processId > 0 & & TryAttach ( processId , "Reattached to" ) ) return ;
SessionState . EraseInt ( ProcessIdSessionKey ) ;
TryAttachPidFile ( ) ;
}
private static void TryAttachPidFile ( )
{
2026-07-17 14:48:24 +01:00
try
{
2026-07-19 01:13:33 +01:00
if ( ! File . Exists ( PidPath ) | |
! int . TryParse ( File . ReadAllText ( PidPath ) . Trim ( ) , out var processId ) )
return ;
TryAttach ( processId , "Attached to" ) ;
}
catch ( IOException )
{
// The launcher replaces the PID file atomically; the next liveness check retries.
}
}
private static bool TryAttach ( int processId , string statusPrefix )
{
try
{
var process = Process . GetProcessById ( processId ) ;
if ( process . HasExited | |
! string . Equals ( process . ProcessName , "Aetheria.State.Daemon" , StringComparison . OrdinalIgnoreCase ) )
{
process . Dispose ( ) ;
return false ;
}
_daemon ? . Dispose ( ) ;
_daemon = process ;
SessionState . SetInt ( ProcessIdSessionKey , processId ) ;
_status = $"{statusPrefix} Debug daemon (PID {processId})" ;
Changed ? . Invoke ( ) ;
return true ;
2026-07-17 14:48:24 +01:00
}
catch ( ArgumentException )
{
2026-07-19 01:13:33 +01:00
return false ;
}
catch ( InvalidOperationException )
{
return false ;
}
catch ( System . ComponentModel . Win32Exception )
{
return false ;
2026-07-17 14:48:24 +01:00
}
}
private static bool IsEndpointReady ( )
{
if ( ! File . Exists ( LogPath ) ) return false ;
try
{
var marker = $"Aetheria client CultMesh endpoint: {Endpoint}" ;
using var stream = new FileStream ( LogPath , FileMode . Open , FileAccess . Read , FileShare . ReadWrite ) ;
using var reader = new StreamReader ( stream ) ;
return reader . ReadToEnd ( ) . Contains ( marker , StringComparison . Ordinal ) ;
}
catch ( IOException )
{
return false ;
}
}
private static void ApplyClientEnvironment ( )
{
Environment . SetEnvironmentVariable ( "EVEUNITY_RENDEZVOUS_ENDPOINT" , Endpoint ) ;
Environment . SetEnvironmentVariable ( "EVEUNITY_SURFACE_ID" , "aetheria.pilot" ) ;
Environment . SetEnvironmentVariable ( "EVEUNITY_ASSET_CACHE_PATH" ,
Path . Combine ( ProjectRoot , "Aetheria.Unity" , "Build" , "AssetCache" ) ) ;
}
private static void AppendLine ( string path , string value )
{
if ( value = = null ) return ;
try { File . AppendAllText ( path , value + Environment . NewLine ) ; }
catch ( IOException ) { }
}
private static string ReadTail ( string path , int maximumLines )
{
if ( ! File . Exists ( path ) ) return "" ;
try
{
var lines = File . ReadAllLines ( path ) ;
return string . Join ( Environment . NewLine , lines , Math . Max ( 0 , lines . Length - maximumLines ) ,
Math . Min ( maximumLines , lines . Length ) ) ;
}
catch ( IOException exception )
{
return exception . Message ;
}
}
private static void OpenFile ( string path )
{
if ( File . Exists ( path ) ) EditorUtility . OpenWithDefaultApp ( path ) ;
else _lastError = $"File does not exist yet: {path}" ;
Changed ? . Invoke ( ) ;
}
private static string Quote ( string value ) = > $"\" { value . Replace ( "\"" , "\\\"" ) } \ "" ;
private static string ResolveSibling ( params string [ ] names )
{
var projectsRoot = Directory . GetParent ( ProjectRoot ) ? . FullName ? ? ProjectRoot ;
foreach ( var name in names )
{
var candidate = Path . Combine ( projectsRoot , name ) ;
if ( Directory . Exists ( candidate ) ) return candidate ;
}
return Path . Combine ( projectsRoot , names [ 0 ] ) ;
}
private static void TryDelete ( string path )
{
try { if ( File . Exists ( path ) ) File . Delete ( path ) ; }
catch ( IOException ) { }
}
}
}