diff --git a/Editor/Core/SplashControlPanel.cs b/Editor/Core/SplashControlPanel.cs
index 2b920a6..92945b5 100644
--- a/Editor/Core/SplashControlPanel.cs
+++ b/Editor/Core/SplashControlPanel.cs
@@ -60,6 +60,13 @@ namespace SplashEdit.EditorCode
private string _nativeInstallStatus = "";
private string _manualNativePath = "";
+ // ───── Release selector ─────
+ private int _selectedReleaseIndex = 0;
+ private string[] _releaseDisplayNames = new string[0];
+ private bool _isFetchingReleases;
+ private string _currentTag = "";
+ private bool _isSwitchingRelease;
+
// PCdrv serial host instance (for real hardware file serving)
private static PCdrvSerialHost _pcdrvHost;
@@ -89,6 +96,8 @@ namespace SplashEdit.EditorCode
RefreshToolchainStatus();
LoadSceneList();
_manualNativePath = SplashSettings.NativeProjectPath;
+ FetchGitHubReleases();
+ RefreshCurrentTag();
}
private void OnDisable()
@@ -215,44 +224,106 @@ namespace SplashEdit.EditorCode
}
else
{
- GUILayout.Label("Not found — clone from GitHub or set path manually", EditorStyles.miniLabel);
+ GUILayout.Label("Not found — download from GitHub or set path manually", EditorStyles.miniLabel);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(6);
- // ── Option 1: Auto-clone from GitHub ──
+ // ── Option 1: Download a release from GitHub ──
PSXEditorStyles.DrawSeparator(4, 4);
- GUILayout.Label("Clone from GitHub", PSXEditorStyles.SectionHeader);
- EditorGUILayout.BeginHorizontal();
- GUILayout.Label(PSXSplashInstaller.RepoUrl, EditorStyles.miniLabel);
- GUILayout.FlexibleSpace();
- EditorGUI.BeginDisabledGroup(_isInstallingNative || PSXSplashInstaller.IsInstalled());
- if (GUILayout.Button(PSXSplashInstaller.IsInstalled() ? "Installed" : "Clone", GUILayout.Width(80)))
+ GUILayout.Label("Download from GitHub", PSXEditorStyles.SectionHeader);
+
+ // Git availability check
+ if (!PSXSplashInstaller.IsGitAvailable())
{
- CloneNativeProject();
+ EditorGUILayout.HelpBox(
+ "git is required to download the native project (submodules need recursive clone).\n" +
+ "Install git from: https://git-scm.com/downloads",
+ MessageType.Warning);
+ }
+
+ // Release selector
+ EditorGUILayout.BeginHorizontal();
+ GUILayout.Label("Release:", GUILayout.Width(55));
+ if (_isFetchingReleases)
+ {
+ GUILayout.Label("Fetching releases...", EditorStyles.miniLabel);
+ }
+ else if (_releaseDisplayNames.Length == 0)
+ {
+ GUILayout.Label("No releases found", EditorStyles.miniLabel);
+ if (GUILayout.Button("Refresh", EditorStyles.miniButton, GUILayout.Width(60)))
+ FetchGitHubReleases();
+ }
+ else
+ {
+ _selectedReleaseIndex = EditorGUILayout.Popup(_selectedReleaseIndex, _releaseDisplayNames);
+ if (GUILayout.Button("↻", EditorStyles.miniButton, GUILayout.Width(22)))
+ FetchGitHubReleases();
}
- EditorGUI.EndDisabledGroup();
EditorGUILayout.EndHorizontal();
- if (_isInstallingNative)
+ // Current version display (when installed)
+ if (PSXSplashInstaller.IsInstalled() && !string.IsNullOrEmpty(_currentTag))
{
- GUILayout.Label(_nativeInstallStatus, PSXEditorStyles.InfoBox);
+ var prevColor = GUI.contentColor;
+ GUI.contentColor = PSXEditorStyles.Success;
+ GUILayout.Label($"Current version: {_currentTag}", EditorStyles.miniLabel);
+ GUI.contentColor = prevColor;
}
- // If already cloned, show version management
- if (PSXSplashInstaller.IsInstalled())
+ // Clone / Switch / Open buttons
+ EditorGUILayout.BeginHorizontal();
+ if (!PSXSplashInstaller.IsInstalled())
{
- EditorGUILayout.BeginHorizontal();
- if (GUILayout.Button("Fetch Latest", EditorStyles.miniButton, GUILayout.Width(90)))
+ // Not installed yet — show Clone button
+ EditorGUI.BeginDisabledGroup(
+ _isInstallingNative || _releaseDisplayNames.Length == 0 ||
+ !PSXSplashInstaller.IsGitAvailable());
+ if (GUILayout.Button("Download Release", GUILayout.Width(130)))
{
- FetchNativeLatest();
+ CloneNativeProject();
}
+ EditorGUI.EndDisabledGroup();
+ }
+ else
+ {
+ // Already installed — show Switch and Open buttons
+ EditorGUI.BeginDisabledGroup(
+ _isSwitchingRelease || _isInstallingNative ||
+ _releaseDisplayNames.Length == 0 || !PSXSplashInstaller.IsGitAvailable());
+ if (GUILayout.Button("Switch Release", EditorStyles.miniButton, GUILayout.Width(100)))
+ {
+ SwitchNativeRelease();
+ }
+ EditorGUI.EndDisabledGroup();
+
if (GUILayout.Button("Open Folder", EditorStyles.miniButton, GUILayout.Width(90)))
{
EditorUtility.RevealInFinder(PSXSplashInstaller.FullInstallPath);
}
- EditorGUILayout.EndHorizontal();
+ }
+ EditorGUILayout.EndHorizontal();
+
+ // Progress / status message
+ if (_isInstallingNative || _isSwitchingRelease)
+ {
+ GUILayout.Label(_nativeInstallStatus, PSXEditorStyles.InfoBox);
+ }
+
+ // Show release notes for selected release
+ if (_releaseDisplayNames.Length > 0 && _selectedReleaseIndex < PSXSplashInstaller.CachedReleases.Count)
+ {
+ var selected = PSXSplashInstaller.CachedReleases[_selectedReleaseIndex];
+ if (!string.IsNullOrEmpty(selected.Body))
+ {
+ EditorGUILayout.Space(2);
+ string trimmedNotes = selected.Body.Length > 200
+ ? selected.Body.Substring(0, 200) + "..."
+ : selected.Body;
+ GUILayout.Label(trimmedNotes, EditorStyles.wordWrappedMiniLabel);
+ }
}
EditorGUILayout.Space(6);
@@ -1840,34 +1911,99 @@ namespace SplashEdit.EditorCode
}
}
- // ───── Native Project Clone/Fetch ─────
+ // ───── Release fetching & management ─────
- private async void CloneNativeProject()
+ private async void FetchGitHubReleases()
{
- _isInstallingNative = true;
- _nativeInstallStatus = "Cloning psxsplash repository (this may take a minute)...";
+ _isFetchingReleases = true;
Repaint();
- Log("Cloning psxsplash native project from GitHub...", LogType.Log);
-
try
{
- bool success = await PSXSplashInstaller.Install();
- if (success)
+ var releases = await PSXSplashInstaller.FetchReleasesAsync();
+ if (releases.Count > 0)
{
- Log("psxsplash cloned successfully!", LogType.Log);
- _nativeInstallStatus = "";
- RefreshToolchainStatus();
+ _releaseDisplayNames = releases
+ .Select(r =>
+ {
+ string label = r.TagName;
+ if (!string.IsNullOrEmpty(r.Name) && r.Name != r.TagName)
+ label += $" — {r.Name}";
+ if (r.IsPrerelease)
+ label += " (pre-release)";
+ return label;
+ })
+ .ToArray();
+
+ // Try to select the currently checked-out tag
+ if (!string.IsNullOrEmpty(_currentTag))
+ {
+ int idx = releases.FindIndex(r => r.TagName == _currentTag);
+ if (idx >= 0) _selectedReleaseIndex = idx;
+ }
+
+ _selectedReleaseIndex = Mathf.Clamp(_selectedReleaseIndex, 0, _releaseDisplayNames.Length - 1);
}
else
{
- Log("Clone failed. Check console for errors.", LogType.Error);
- _nativeInstallStatus = "Clone failed — check console for details.";
+ _releaseDisplayNames = new string[0];
}
}
catch (Exception ex)
{
- Log($"Clone error: {ex.Message}", LogType.Error);
+ Log($"Failed to fetch releases: {ex.Message}", LogType.Warning);
+ }
+ finally
+ {
+ _isFetchingReleases = false;
+ Repaint();
+ }
+ }
+
+ private void RefreshCurrentTag()
+ {
+ _currentTag = PSXSplashInstaller.GetCurrentTag() ?? "";
+ }
+
+ // ───── Native Project Clone/Switch ─────
+
+ private async void CloneNativeProject()
+ {
+ if (_selectedReleaseIndex < 0 || _selectedReleaseIndex >= PSXSplashInstaller.CachedReleases.Count)
+ return;
+
+ string tag = PSXSplashInstaller.CachedReleases[_selectedReleaseIndex].TagName;
+
+ _isInstallingNative = true;
+ _nativeInstallStatus = $"Downloading psxsplash {tag} (this may take a minute)...";
+ Repaint();
+
+ Log($"Downloading psxsplash {tag} from GitHub...", LogType.Log);
+
+ try
+ {
+ bool success = await PSXSplashInstaller.InstallRelease(tag, msg =>
+ {
+ _nativeInstallStatus = msg;
+ Repaint();
+ });
+
+ if (success)
+ {
+ Log($"psxsplash {tag} downloaded successfully!", LogType.Log);
+ _nativeInstallStatus = "";
+ RefreshToolchainStatus();
+ RefreshCurrentTag();
+ }
+ else
+ {
+ Log("Download failed. Check console for errors.", LogType.Error);
+ _nativeInstallStatus = "Download failed — check console for details.";
+ }
+ }
+ catch (Exception ex)
+ {
+ Log($"Download error: {ex.Message}", LogType.Error);
_nativeInstallStatus = $"Error: {ex.Message}";
}
finally
@@ -1877,22 +2013,55 @@ namespace SplashEdit.EditorCode
}
}
- private async void FetchNativeLatest()
+ private async void SwitchNativeRelease()
{
- Log("Fetching latest changes...", LogType.Log);
+ if (_selectedReleaseIndex < 0 || _selectedReleaseIndex >= PSXSplashInstaller.CachedReleases.Count)
+ return;
+
+ string tag = PSXSplashInstaller.CachedReleases[_selectedReleaseIndex].TagName;
+
+ if (tag == _currentTag)
+ {
+ Log($"Already on {tag}.", LogType.Log);
+ return;
+ }
+
+ _isSwitchingRelease = true;
+ _nativeInstallStatus = $"Switching to {tag}...";
+ Repaint();
+
+ Log($"Switching native project to {tag}...", LogType.Log);
+
try
{
- bool success = await PSXSplashInstaller.FetchLatestAsync();
+ bool success = await PSXSplashInstaller.SwitchToReleaseAsync(tag, msg =>
+ {
+ _nativeInstallStatus = msg;
+ Repaint();
+ });
+
if (success)
- Log("Fetch complete. Use 'git pull' to apply updates.", LogType.Log);
+ {
+ Log($"Switched to {tag}.", LogType.Log);
+ _nativeInstallStatus = "";
+ RefreshCurrentTag();
+ }
else
- Log("Fetch failed.", LogType.Warning);
+ {
+ Log($"Failed to switch to {tag}.", LogType.Error);
+ _nativeInstallStatus = "Switch failed — check console for details.";
+ }
}
catch (Exception ex)
{
- Log($"Fetch error: {ex.Message}", LogType.Error);
+ Log($"Switch error: {ex.Message}", LogType.Error);
+ _nativeInstallStatus = $"Error: {ex.Message}";
+ }
+ finally
+ {
+ _isSwitchingRelease = false;
+ Repaint();
}
- Repaint();
}
// ═══════════════════════════════════════════════════════════════
diff --git a/Editor/PSXSplashInstaller.cs b/Editor/PSXSplashInstaller.cs
index 525c2a2..3aa09d3 100644
--- a/Editor/PSXSplashInstaller.cs
+++ b/Editor/PSXSplashInstaller.cs
@@ -1,149 +1,307 @@
using UnityEngine;
+using UnityEngine.Networking;
+using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
-using System.Collections.Generic;
using System.Threading.Tasks;
-using System;
namespace SplashEdit.EditorCode
{
-
+ ///
+ /// Manages downloading and updating the psxsplash native project from GitHub releases.
+ /// Uses the GitHub REST API (HTTP) to list releases and git to clone/checkout
+ /// (required for recursive submodule support).
+ ///
public static class PSXSplashInstaller
{
+ // ───── Public config ─────
+ public static readonly string RepoOwner = "psxsplash";
+ public static readonly string RepoName = "psxsplash";
public static readonly string RepoUrl = "https://github.com/psxsplash/psxsplash.git";
public static readonly string InstallPath = "Assets/psxsplash";
public static readonly string FullInstallPath;
+ private static readonly string GitHubApiReleasesUrl =
+ $"https://api.github.com/repos/{RepoOwner}/{RepoName}/releases";
+
+ // ───── Cached release list ─────
+ private static List _cachedReleases = new List();
+ private static bool _isFetchingReleases;
+
+ ///
+ /// Represents a GitHub release.
+ ///
+ [Serializable]
+ public class ReleaseInfo
+ {
+ public string TagName; // e.g. "v1.2.0"
+ public string Name; // human-readable name
+ public string Body; // release notes (markdown)
+ public string PublishedAt; // ISO 8601 date
+ public bool IsPrerelease;
+ public bool IsDraft;
+ }
+
static PSXSplashInstaller()
{
FullInstallPath = Path.Combine(Application.dataPath, "psxsplash");
}
+ // ═══════════════════════════════════════════════════════════════
+ // Queries
+ // ═══════════════════════════════════════════════════════════════
+
+ /// Is the native project cloned on disk?
public static bool IsInstalled()
{
return Directory.Exists(FullInstallPath) &&
Directory.EnumerateFileSystemEntries(FullInstallPath).Any();
}
- public static async Task Install()
- {
- if (IsInstalled()) return true;
+ /// Are we currently fetching releases from GitHub?
+ public static bool IsFetchingReleases => _isFetchingReleases;
+ /// Cached list of releases (call FetchReleasesAsync to populate).
+ public static IReadOnlyList CachedReleases => _cachedReleases;
+
+ ///
+ /// Returns the tag currently checked out, or null if unknown / not a git repo.
+ ///
+ public static string GetCurrentTag()
+ {
+ if (!IsInstalled()) return null;
try
{
- // Create the parent directory if it doesn't exist
- Directory.CreateDirectory(Application.dataPath);
-
- // Clone the repository
- var result = await RunGitCommandAsync($"clone --recursive {RepoUrl} \"{FullInstallPath}\"", Application.dataPath);
- return !result.Contains("error");
+ string result = RunGitCommandSync("describe --tags --exact-match HEAD", FullInstallPath);
+ return string.IsNullOrWhiteSpace(result) ? null : result.Trim();
}
- catch (Exception e)
+ catch
{
- UnityEngine.Debug.LogError($"Failed to install PSXSplash: {e.Message}");
- return false;
+ return null;
}
}
- public static async Task> GetBranchesWithLatestCommitsAsync()
- {
- if (!IsInstalled()) return new Dictionary();
+ // ═══════════════════════════════════════════════════════════════
+ // Fetch Releases (HTTP — no git required)
+ // ═══════════════════════════════════════════════════════════════
+ ///
+ /// Fetches the list of releases from the GitHub REST API.
+ /// Does NOT require git — uses UnityWebRequest.
+ ///
+ public static async Task> FetchReleasesAsync()
+ {
+ _isFetchingReleases = true;
try
{
- // Fetch all branches and tags
- await RunGitCommandAsync("fetch --all", FullInstallPath);
-
- // Get all remote branches
- var branchesOutput = await RunGitCommandAsync("branch -r", FullInstallPath);
- var branches = branchesOutput.Split('\n')
- .Where(b => !string.IsNullOrEmpty(b.Trim()))
- .Select(b => b.Trim().Replace("origin/", ""))
- .Where(b => !b.Contains("HEAD"))
- .ToList();
-
- var branchesWithCommits = new Dictionary();
-
- // Get the latest commit for each branch
- foreach (var branch in branches)
+ string json = await HttpGetAsync(GitHubApiReleasesUrl);
+ if (string.IsNullOrEmpty(json))
{
- var commitOutput = await RunGitCommandAsync($"log origin/{branch} -1 --pretty=format:%h", FullInstallPath);
- if (!string.IsNullOrEmpty(commitOutput))
- {
- branchesWithCommits[branch] = commitOutput.Trim();
- }
+ UnityEngine.Debug.LogWarning("[PSXSplashInstaller] Failed to fetch releases from GitHub.");
+ return _cachedReleases;
}
- return branchesWithCommits;
- }
- catch (Exception e)
- {
- UnityEngine.Debug.LogError($"Failed to get branches: {e.Message}");
- return new Dictionary();
- }
- }
-
- public static async Task> GetReleasesAsync()
- {
- if (!IsInstalled()) return new List();
-
- try
- {
- await RunGitCommandAsync("fetch --tags", FullInstallPath);
- var output = await RunGitCommandAsync("tag -l", FullInstallPath);
-
- return output.Split('\n')
- .Where(t => !string.IsNullOrEmpty(t.Trim()))
- .Select(t => t.Trim())
+ var releases = ParseReleasesJson(json);
+ // Filter out drafts, sort by newest first
+ releases = releases
+ .Where(r => !r.IsDraft)
+ .OrderByDescending(r => r.PublishedAt)
.ToList();
+
+ _cachedReleases = releases;
+ return releases;
}
- catch (Exception e)
+ catch (Exception ex)
{
- UnityEngine.Debug.LogError($"Failed to get releases: {e.Message}");
- return new List();
+ UnityEngine.Debug.LogError($"[PSXSplashInstaller] Error fetching releases: {ex.Message}");
+ return _cachedReleases;
+ }
+ finally
+ {
+ _isFetchingReleases = false;
}
}
- public static async Task CheckoutVersionAsync(string version)
+ // ═══════════════════════════════════════════════════════════════
+ // Install / Clone at a specific release tag
+ // ═══════════════════════════════════════════════════════════════
+
+ ///
+ /// Clones the repository at the specified release tag with --recursive.
+ /// Uses a shallow clone (--depth 1) for speed.
+ /// Requires git to be installed (submodules cannot be fetched via HTTP archives).
+ ///
+ /// The release tag to clone, e.g. "v1.2.0". If null, clones the default branch.
+ /// Optional progress callback.
+ public static async Task InstallRelease(string tag, Action onProgress = null)
{
- if (!IsInstalled()) return false;
+ if (IsInstalled())
+ {
+ onProgress?.Invoke("Already installed. Use SwitchToRelease to change version.");
+ return true;
+ }
+
+ if (!IsGitAvailable())
+ {
+ UnityEngine.Debug.LogError(
+ "[PSXSplashInstaller] git is required for recursive submodule clone but was not found on PATH.\n" +
+ "Please install git: https://git-scm.com/downloads");
+ return false;
+ }
try
{
- // If it's a branch name, checkout the branch
- // If it's a commit hash, checkout the commit
- var result = await RunGitCommandAsync($"checkout {version}", FullInstallPath);
- var result2 = await RunGitCommandAsync("submodule update --init --recursive", FullInstallPath);
+ Directory.CreateDirectory(Path.GetDirectoryName(FullInstallPath));
- return !result.Contains("error") && !result2.Contains("error");
+ string branchArg = string.IsNullOrEmpty(tag) ? "" : $"--branch {tag}";
+ string cmd = $"clone --recursive --depth 1 {branchArg} {RepoUrl} \"{FullInstallPath}\"";
+
+ onProgress?.Invoke($"Cloning {RepoUrl} at {tag ?? "HEAD"}...");
+ string result = await RunGitCommandAsync(cmd, Application.dataPath, onProgress);
+
+ if (!IsInstalled())
+ {
+ UnityEngine.Debug.LogError("[PSXSplashInstaller] Clone completed but directory is empty.");
+ return false;
+ }
+
+ onProgress?.Invoke("Clone complete.");
+ return true;
}
- catch (Exception e)
+ catch (Exception ex)
{
- UnityEngine.Debug.LogError($"Failed to checkout version: {e.Message}");
+ UnityEngine.Debug.LogError($"[PSXSplashInstaller] Clone failed: {ex.Message}");
return false;
}
}
+ ///
+ /// Switches an existing clone to a different release tag.
+ /// Fetches tags, checks out the tag, and updates submodules recursively.
+ ///
+ public static async Task SwitchToReleaseAsync(string tag, Action onProgress = null)
+ {
+ if (!IsInstalled())
+ {
+ UnityEngine.Debug.LogError("[PSXSplashInstaller] Not installed — clone first.");
+ return false;
+ }
+
+ if (!IsGitAvailable())
+ {
+ UnityEngine.Debug.LogError("[PSXSplashInstaller] git not found on PATH.");
+ return false;
+ }
+
+ try
+ {
+ onProgress?.Invoke("Fetching tags...");
+ await RunGitCommandAsync("fetch --tags --depth=1", FullInstallPath, onProgress);
+ await RunGitCommandAsync($"fetch origin tag {tag} --no-tags", FullInstallPath, onProgress);
+
+ onProgress?.Invoke($"Checking out {tag}...");
+ await RunGitCommandAsync($"checkout {tag}", FullInstallPath, onProgress);
+
+ onProgress?.Invoke("Updating submodules...");
+ await RunGitCommandAsync("submodule update --init --recursive", FullInstallPath, onProgress);
+
+ onProgress?.Invoke($"Switched to {tag}.");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ UnityEngine.Debug.LogError($"[PSXSplashInstaller] Switch failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Legacy compatibility: Install without specifying a tag (clones default branch).
+ ///
+ public static Task Install()
+ {
+ return InstallRelease(null);
+ }
+
+ ///
+ /// Fetches latest remote data (tags, branches).
+ /// Requires git.
+ ///
public static async Task FetchLatestAsync()
{
if (!IsInstalled()) return false;
try
{
- var result = await RunGitCommandAsync("fetch --all", FullInstallPath);
- return !result.Contains("error");
+ await RunGitCommandAsync("fetch --all --tags", FullInstallPath);
+ return true;
}
- catch (Exception e)
+ catch (Exception ex)
{
- UnityEngine.Debug.LogError($"Failed to fetch latest: {e.Message}");
+ UnityEngine.Debug.LogError($"[PSXSplashInstaller] Fetch failed: {ex.Message}");
return false;
}
}
- private static async Task RunGitCommandAsync(string arguments, string workingDirectory)
+ // ═══════════════════════════════════════════════════════════════
+ // Git helpers
+ // ═══════════════════════════════════════════════════════════════
+
+ ///
+ /// Checks whether git is available on the system PATH.
+ ///
+ public static bool IsGitAvailable()
{
- var processInfo = new ProcessStartInfo
+ try
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = "git",
+ Arguments = "--version",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+ using (var p = Process.Start(psi))
+ {
+ p.WaitForExit(5000);
+ return p.ExitCode == 0;
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static string RunGitCommandSync(string arguments, string workingDirectory)
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = "git",
+ Arguments = arguments,
+ WorkingDirectory = workingDirectory,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ using (var process = Process.Start(psi))
+ {
+ string output = process.StandardOutput.ReadToEnd();
+ process.WaitForExit(10000);
+ return output;
+ }
+ }
+
+ private static async Task RunGitCommandAsync(
+ string arguments, string workingDirectory, Action onProgress = null)
+ {
+ var psi = new ProcessStartInfo
{
FileName = "git",
Arguments = arguments,
@@ -156,47 +314,136 @@ namespace SplashEdit.EditorCode
using (var process = new Process())
{
- process.StartInfo = processInfo;
- var outputBuilder = new System.Text.StringBuilder();
- var errorBuilder = new System.Text.StringBuilder();
+ process.StartInfo = psi;
+ process.EnableRaisingEvents = true;
- process.OutputDataReceived += (sender, e) =>
+ var stdout = new System.Text.StringBuilder();
+ var stderr = new System.Text.StringBuilder();
+
+ process.OutputDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
- outputBuilder.AppendLine(e.Data);
+ {
+ stdout.AppendLine(e.Data);
+ onProgress?.Invoke(e.Data);
+ }
};
-
- process.ErrorDataReceived += (sender, e) =>
+ process.ErrorDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
- errorBuilder.AppendLine(e.Data);
+ {
+ stderr.AppendLine(e.Data);
+ // git writes progress to stderr (clone progress, etc.)
+ onProgress?.Invoke(e.Data);
+ }
};
+ var tcs = new TaskCompletionSource();
+ process.Exited += (s, e) => tcs.TrySetResult(process.ExitCode);
+
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
- var timeout = TimeSpan.FromMinutes(10);
- if (await Task.Run(() => process.WaitForExit((int)timeout.TotalMilliseconds)))
+ var timeoutTask = Task.Delay(TimeSpan.FromMinutes(10));
+ var completedTask = await Task.WhenAny(tcs.Task, timeoutTask);
+
+ if (completedTask == timeoutTask)
{
- process.WaitForExit(); // Ensure all output is processed
-
- string output = outputBuilder.ToString();
- string error = errorBuilder.ToString();
-
- if (!string.IsNullOrEmpty(error))
- {
- UnityEngine.Debug.LogError($"Git error: {error}");
- }
-
- return output;
+ try { process.Kill(); } catch { }
+ throw new TimeoutException("Git command timed out after 10 minutes.");
}
+
+ int exitCode = await tcs.Task;
+ process.Dispose();
+
+ string output = stdout.ToString();
+ string error = stderr.ToString();
+
+ if (exitCode != 0)
+ {
+ UnityEngine.Debug.LogError($"[git {arguments}] exit code {exitCode}\n{error}");
+ }
+
+ return output + error;
+ }
+ }
+
+ // ═══════════════════════════════════════════════════════════════
+ // HTTP helpers (no git needed)
+ // ═══════════════════════════════════════════════════════════════
+
+ private static Task HttpGetAsync(string url)
+ {
+ var tcs = new TaskCompletionSource();
+ var request = UnityWebRequest.Get(url);
+ request.SetRequestHeader("User-Agent", "SplashEdit-Unity");
+ request.SetRequestHeader("Accept", "application/vnd.github.v3+json");
+
+ var op = request.SendWebRequest();
+ op.completed += _ =>
+ {
+ if (request.result == UnityWebRequest.Result.Success)
+ tcs.TrySetResult(request.downloadHandler.text);
else
{
- process.Kill();
- throw new TimeoutException("Git command timed out");
+ UnityEngine.Debug.LogWarning($"[PSXSplashInstaller] HTTP GET {url} failed: {request.error}");
+ tcs.TrySetResult(null);
}
+ request.Dispose();
+ };
+
+ return tcs.Task;
+ }
+
+ // ═══════════════════════════════════════════════════════════════
+ // JSON parsing (minimal, avoids external dependency)
+ // ═══════════════════════════════════════════════════════════════
+
+ ///
+ /// Minimal JSON parser for the GitHub releases API response.
+ /// Uses Unity's JsonUtility via a wrapper since it can't parse top-level arrays.
+ ///
+ private static List ParseReleasesJson(string json)
+ {
+ var releases = new List();
+
+ string wrapped = "{\"items\":" + json + "}";
+ var wrapper = JsonUtility.FromJson(wrapped);
+
+ if (wrapper?.items == null) return releases;
+
+ foreach (var item in wrapper.items)
+ {
+ releases.Add(new ReleaseInfo
+ {
+ TagName = item.tag_name ?? "",
+ Name = item.name ?? item.tag_name ?? "",
+ Body = item.body ?? "",
+ PublishedAt = item.published_at ?? "",
+ IsPrerelease = item.prerelease,
+ IsDraft = item.draft
+ });
}
+
+ return releases;
+ }
+
+ [Serializable]
+ private class GitHubReleaseArrayWrapper
+ {
+ public GitHubReleaseJson[] items;
+ }
+
+ [Serializable]
+ private class GitHubReleaseJson
+ {
+ public string tag_name;
+ public string name;
+ public string body;
+ public string published_at;
+ public bool prerelease;
+ public bool draft;
}
}
}
\ No newline at end of file