chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
|
||||
|
||||
namespace MCPForUnity.Editor.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for checking package updates from GitHub or Asset Store metadata
|
||||
/// </summary>
|
||||
public class PackageUpdateService : IPackageUpdateService
|
||||
{
|
||||
private const int DefaultRequestTimeoutMs = 3000;
|
||||
private const string LastCheckDateKey = EditorPrefKeys.LastUpdateCheck;
|
||||
private const string CachedVersionKey = EditorPrefKeys.LatestKnownVersion;
|
||||
private const string LastBetaCheckDateKey = EditorPrefKeys.LastUpdateCheck + ".beta";
|
||||
private const string CachedBetaVersionKey = EditorPrefKeys.LatestKnownVersion + ".beta";
|
||||
private const string LastAssetStoreCheckDateKey = EditorPrefKeys.LastAssetStoreUpdateCheck;
|
||||
private const string CachedAssetStoreVersionKey = EditorPrefKeys.LatestKnownAssetStoreVersion;
|
||||
private const string MainPackageJsonUrl = "https://raw.githubusercontent.com/CoplayDev/unity-mcp/main/MCPForUnity/package.json";
|
||||
private const string BetaPackageJsonUrl = "https://raw.githubusercontent.com/CoplayDev/unity-mcp/beta/MCPForUnity/package.json";
|
||||
private const string AssetStoreVersionUrl = "https://gqoqjkkptwfbkwyssmnj.supabase.co/storage/v1/object/public/coplay-images/assetstoreversion.json";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UpdateCheckResult CheckForUpdate(string currentVersion)
|
||||
{
|
||||
bool isGitInstallation = IsGitInstallation();
|
||||
string gitBranch = isGitInstallation ? GetGitUpdateBranch(currentVersion) : "main";
|
||||
bool useBetaChannel = isGitInstallation && string.Equals(gitBranch, "beta", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string lastCheckKey = isGitInstallation
|
||||
? (useBetaChannel ? LastBetaCheckDateKey : LastCheckDateKey)
|
||||
: LastAssetStoreCheckDateKey;
|
||||
string cachedVersionKey = isGitInstallation
|
||||
? (useBetaChannel ? CachedBetaVersionKey : CachedVersionKey)
|
||||
: CachedAssetStoreVersionKey;
|
||||
|
||||
string lastCheckDate = EditorPrefs.GetString(lastCheckKey, "");
|
||||
string cachedLatestVersion = EditorPrefs.GetString(cachedVersionKey, "");
|
||||
|
||||
if (lastCheckDate == DateTime.Now.ToString("yyyy-MM-dd") && !string.IsNullOrEmpty(cachedLatestVersion))
|
||||
{
|
||||
return new UpdateCheckResult
|
||||
{
|
||||
CheckSucceeded = true,
|
||||
LatestVersion = cachedLatestVersion,
|
||||
UpdateAvailable = IsNewerVersion(cachedLatestVersion, currentVersion),
|
||||
Message = "Using cached version check"
|
||||
};
|
||||
}
|
||||
|
||||
string latestVersion = isGitInstallation
|
||||
? FetchLatestVersionFromGitHub(gitBranch)
|
||||
: FetchLatestVersionFromAssetStoreJson();
|
||||
|
||||
if (!string.IsNullOrEmpty(latestVersion))
|
||||
{
|
||||
// Cache the result
|
||||
EditorPrefs.SetString(lastCheckKey, DateTime.Now.ToString("yyyy-MM-dd"));
|
||||
EditorPrefs.SetString(cachedVersionKey, latestVersion);
|
||||
|
||||
return new UpdateCheckResult
|
||||
{
|
||||
CheckSucceeded = true,
|
||||
LatestVersion = latestVersion,
|
||||
UpdateAvailable = IsNewerVersion(latestVersion, currentVersion),
|
||||
Message = "Successfully checked for updates"
|
||||
};
|
||||
}
|
||||
|
||||
return new UpdateCheckResult
|
||||
{
|
||||
CheckSucceeded = false,
|
||||
UpdateAvailable = false,
|
||||
Message = isGitInstallation
|
||||
? "Failed to check for updates (network issue or offline)"
|
||||
: "Failed to check for Asset Store updates (network issue or offline)"
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UpdateCheckResult TryGetCachedResult(string currentVersion)
|
||||
{
|
||||
bool isGitInstallation = IsGitInstallation();
|
||||
string gitBranch = isGitInstallation ? GetGitUpdateBranch(currentVersion) : "main";
|
||||
bool useBetaChannel = isGitInstallation && string.Equals(gitBranch, "beta", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string lastCheckKey = isGitInstallation
|
||||
? (useBetaChannel ? LastBetaCheckDateKey : LastCheckDateKey)
|
||||
: LastAssetStoreCheckDateKey;
|
||||
string cachedVersionKey = isGitInstallation
|
||||
? (useBetaChannel ? CachedBetaVersionKey : CachedVersionKey)
|
||||
: CachedAssetStoreVersionKey;
|
||||
|
||||
string lastCheckDate = EditorPrefs.GetString(lastCheckKey, "");
|
||||
string cachedLatestVersion = EditorPrefs.GetString(cachedVersionKey, "");
|
||||
|
||||
if (lastCheckDate == DateTime.Now.ToString("yyyy-MM-dd") && !string.IsNullOrEmpty(cachedLatestVersion))
|
||||
{
|
||||
return new UpdateCheckResult
|
||||
{
|
||||
CheckSucceeded = true,
|
||||
LatestVersion = cachedLatestVersion,
|
||||
UpdateAvailable = IsNewerVersion(cachedLatestVersion, currentVersion),
|
||||
Message = "Using cached version check"
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UpdateCheckResult FetchAndCompare(string currentVersion)
|
||||
{
|
||||
bool isGitInstallation = IsGitInstallation();
|
||||
string gitBranch = isGitInstallation ? GetGitUpdateBranch(currentVersion) : "main";
|
||||
return FetchAndCompare(currentVersion, isGitInstallation, gitBranch);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public UpdateCheckResult FetchAndCompare(string currentVersion, bool isGitInstallation, string gitBranch)
|
||||
{
|
||||
string latestVersion = isGitInstallation
|
||||
? FetchLatestVersionFromGitHub(gitBranch)
|
||||
: FetchLatestVersionFromAssetStoreJson();
|
||||
|
||||
if (!string.IsNullOrEmpty(latestVersion))
|
||||
{
|
||||
return new UpdateCheckResult
|
||||
{
|
||||
CheckSucceeded = true,
|
||||
LatestVersion = latestVersion,
|
||||
UpdateAvailable = IsNewerVersion(latestVersion, currentVersion),
|
||||
Message = "Successfully checked for updates"
|
||||
};
|
||||
}
|
||||
|
||||
return new UpdateCheckResult
|
||||
{
|
||||
CheckSucceeded = false,
|
||||
UpdateAvailable = false,
|
||||
Message = isGitInstallation
|
||||
? "Failed to check for updates (network issue or offline)"
|
||||
: "Failed to check for Asset Store updates (network issue or offline)"
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CacheFetchResult(string currentVersion, string fetchedVersion)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fetchedVersion)) return;
|
||||
|
||||
bool isGitInstallation = IsGitInstallation();
|
||||
string gitBranch = isGitInstallation ? GetGitUpdateBranch(currentVersion) : "main";
|
||||
bool useBetaChannel = isGitInstallation && string.Equals(gitBranch, "beta", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string lastCheckKey = isGitInstallation
|
||||
? (useBetaChannel ? LastBetaCheckDateKey : LastCheckDateKey)
|
||||
: LastAssetStoreCheckDateKey;
|
||||
string cachedVersionKey = isGitInstallation
|
||||
? (useBetaChannel ? CachedBetaVersionKey : CachedVersionKey)
|
||||
: CachedAssetStoreVersionKey;
|
||||
|
||||
EditorPrefs.SetString(lastCheckKey, DateTime.Now.ToString("yyyy-MM-dd"));
|
||||
EditorPrefs.SetString(cachedVersionKey, fetchedVersion);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsNewerVersion(string version1, string version2)
|
||||
{
|
||||
if (!TryParseVersion(version1, out var left) || !TryParseVersion(version2, out var right))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return CompareVersions(left, right) > 0;
|
||||
}
|
||||
|
||||
private static int CompareVersions(ParsedVersion left, ParsedVersion right)
|
||||
{
|
||||
int cmp = left.Major.CompareTo(right.Major);
|
||||
if (cmp != 0) return cmp;
|
||||
|
||||
cmp = left.Minor.CompareTo(right.Minor);
|
||||
if (cmp != 0) return cmp;
|
||||
|
||||
cmp = left.Patch.CompareTo(right.Patch);
|
||||
if (cmp != 0) return cmp;
|
||||
|
||||
// Stable is newer than prerelease when core version matches.
|
||||
if (!left.IsPrerelease && right.IsPrerelease) return 1;
|
||||
if (left.IsPrerelease && !right.IsPrerelease) return -1;
|
||||
if (!left.IsPrerelease && !right.IsPrerelease) return 0;
|
||||
|
||||
cmp = GetPrereleaseRank(left.PrereleaseLabel).CompareTo(GetPrereleaseRank(right.PrereleaseLabel));
|
||||
if (cmp != 0) return cmp;
|
||||
|
||||
cmp = left.PrereleaseNumber.CompareTo(right.PrereleaseNumber);
|
||||
if (cmp != 0) return cmp;
|
||||
|
||||
return string.Compare(left.PrereleaseLabel, right.PrereleaseLabel, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static int GetPrereleaseRank(string label)
|
||||
{
|
||||
if (string.IsNullOrEmpty(label))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (label.ToLowerInvariant())
|
||||
{
|
||||
case "a":
|
||||
case "alpha":
|
||||
return 1;
|
||||
case "b":
|
||||
case "beta":
|
||||
return 2;
|
||||
case "rc":
|
||||
return 3;
|
||||
case "preview":
|
||||
case "pre":
|
||||
return 4;
|
||||
default:
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseVersion(string version, out ParsedVersion parsed)
|
||||
{
|
||||
parsed = default;
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string normalized = version.Trim().TrimStart('v', 'V');
|
||||
var match = Regex.Match(
|
||||
normalized,
|
||||
@"^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<label>[A-Za-z]+)(?:\.(?<number>\d+))?)?$");
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(match.Groups["major"].Value, out int major) ||
|
||||
!int.TryParse(match.Groups["minor"].Value, out int minor) ||
|
||||
!int.TryParse(match.Groups["patch"].Value, out int patch))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string prereleaseLabel = match.Groups["label"].Success ? match.Groups["label"].Value : string.Empty;
|
||||
int prereleaseNumber = 0;
|
||||
if (match.Groups["number"].Success)
|
||||
{
|
||||
int.TryParse(match.Groups["number"].Value, out prereleaseNumber);
|
||||
}
|
||||
|
||||
parsed = new ParsedVersion
|
||||
{
|
||||
Major = major,
|
||||
Minor = minor,
|
||||
Patch = patch,
|
||||
PrereleaseLabel = prereleaseLabel,
|
||||
PrereleaseNumber = prereleaseNumber,
|
||||
IsPrerelease = !string.IsNullOrEmpty(prereleaseLabel)
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsPreReleaseVersion(string version)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
return AssetPathUtility.IsPreReleaseVersion();
|
||||
}
|
||||
|
||||
return version.IndexOf('-', StringComparison.Ordinal) >= 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetGitUpdateBranch(string currentVersion)
|
||||
{
|
||||
try
|
||||
{
|
||||
var packageInfo = PackageInfo.FindForAssembly(typeof(PackageUpdateService).Assembly);
|
||||
string packageId = packageInfo?.packageId ?? string.Empty;
|
||||
|
||||
if (packageId.IndexOf("#beta", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return "beta";
|
||||
}
|
||||
|
||||
if (packageId.IndexOf("#main", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return "main";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall back to version-based inference below.
|
||||
}
|
||||
|
||||
return IsPreReleaseVersion(currentVersion) ? "beta" : "main";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual bool IsGitInstallation()
|
||||
{
|
||||
// Git packages are installed via Package Manager and have a package.json in Packages/
|
||||
// Asset Store packages are in Assets/
|
||||
string packageRoot = AssetPathUtility.GetMcpPackageRootPath();
|
||||
|
||||
if (string.IsNullOrEmpty(packageRoot))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the package is in Packages/ it's a PM install (likely Git)
|
||||
// If it's in Assets/ it's an Asset Store install
|
||||
return packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ClearCache()
|
||||
{
|
||||
EditorPrefs.DeleteKey(LastCheckDateKey);
|
||||
EditorPrefs.DeleteKey(CachedVersionKey);
|
||||
EditorPrefs.DeleteKey(LastBetaCheckDateKey);
|
||||
EditorPrefs.DeleteKey(CachedBetaVersionKey);
|
||||
EditorPrefs.DeleteKey(LastAssetStoreCheckDateKey);
|
||||
EditorPrefs.DeleteKey(CachedAssetStoreVersionKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the latest version from GitHub package.json for the requested branch.
|
||||
/// </summary>
|
||||
protected virtual string FetchLatestVersionFromGitHub(string branch)
|
||||
{
|
||||
try
|
||||
{
|
||||
// GitHub API endpoint (Option 1 - has rate limits):
|
||||
// https://api.github.com/repos/CoplayDev/unity-mcp/releases/latest
|
||||
//
|
||||
// We use Option 2 (package.json directly) because:
|
||||
// - No API rate limits (GitHub serves raw files freely)
|
||||
// - Simpler - just parse JSON for version field
|
||||
// - More reliable - doesn't require releases to be published
|
||||
// - Direct source of truth from the main branch
|
||||
|
||||
using (var client = CreateWebClient())
|
||||
{
|
||||
client.Headers.Add("User-Agent", "Unity-MCPForUnity-UpdateChecker");
|
||||
string packageJsonUrl = string.Equals(branch, "beta", StringComparison.OrdinalIgnoreCase)
|
||||
? BetaPackageJsonUrl
|
||||
: MainPackageJsonUrl;
|
||||
string jsonContent = client.DownloadString(packageJsonUrl);
|
||||
|
||||
var packageJson = JObject.Parse(jsonContent);
|
||||
string version = packageJson["version"]?.ToString();
|
||||
|
||||
return string.IsNullOrEmpty(version) ? null : version;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Silent fail - don't interrupt the user if network is unavailable
|
||||
McpLog.Info($"Update check failed (this is normal if offline): {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private struct ParsedVersion
|
||||
{
|
||||
public int Major;
|
||||
public int Minor;
|
||||
public int Patch;
|
||||
public string PrereleaseLabel;
|
||||
public int PrereleaseNumber;
|
||||
public bool IsPrerelease;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the latest Asset Store version from a hosted JSON file.
|
||||
/// </summary>
|
||||
protected virtual string FetchLatestVersionFromAssetStoreJson()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var client = CreateWebClient())
|
||||
{
|
||||
client.Headers.Add("User-Agent", "Unity-MCPForUnity-AssetStoreUpdateChecker");
|
||||
string jsonContent = client.DownloadString(AssetStoreVersionUrl);
|
||||
|
||||
var versionJson = JObject.Parse(jsonContent);
|
||||
string version = versionJson["version"]?.ToString();
|
||||
|
||||
return string.IsNullOrEmpty(version) ? null : version;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Silent fail - don't interrupt the user if network is unavailable
|
||||
McpLog.Info($"Asset Store update check failed (this is normal if offline): {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual WebClient CreateWebClient()
|
||||
{
|
||||
return new TimeoutWebClient(GetRequestTimeoutMs());
|
||||
}
|
||||
|
||||
protected virtual int GetRequestTimeoutMs()
|
||||
{
|
||||
return DefaultRequestTimeoutMs;
|
||||
}
|
||||
|
||||
private sealed class TimeoutWebClient : WebClient
|
||||
{
|
||||
private readonly int _timeoutMs;
|
||||
|
||||
public TimeoutWebClient(int timeoutMs)
|
||||
{
|
||||
_timeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
protected override WebRequest GetWebRequest(Uri address)
|
||||
{
|
||||
var request = base.GetWebRequest(address);
|
||||
if (request != null)
|
||||
{
|
||||
request.Timeout = _timeoutMs;
|
||||
|
||||
if (request is HttpWebRequest httpRequest)
|
||||
{
|
||||
httpRequest.ReadWriteTimeout = _timeoutMs;
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user