using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; namespace Amanotes { [Serializable] internal class DevModeStateEntry { public string packageName; public string originalValue; } [Serializable] internal class DevModeState { private static readonly string stateDir = Path.Combine("Library", "AmaDev"); private static readonly string statePath = Path.Combine(stateDir, "dev-mode-state.json"); public List entries = new List(); internal static DevModeState Load() { if (!File.Exists(statePath)) return new DevModeState(); var json = File.ReadAllText(statePath); if (string.IsNullOrEmpty(json)) return new DevModeState(); var s = JsonUtility.FromJson(json); return s ?? new DevModeState(); } internal void Save() { if (!Directory.Exists(stateDir)) Directory.CreateDirectory(stateDir); File.WriteAllText(statePath, JsonUtility.ToJson(this, true)); } internal string GetOriginalValue(string packageName) { for (int i = 0; i < entries.Count; i++) if (entries[i].packageName == packageName) return entries[i].originalValue; return null; } internal void SetOriginalValue(string packageName, string originalValue) { for (int i = 0; i < entries.Count; i++) { if (entries[i].packageName != packageName) continue; entries[i].originalValue = originalValue; Save(); return; } entries.Add(new DevModeStateEntry { packageName = packageName, originalValue = originalValue }); Save(); } internal void RemoveEntry(string packageName) { for (int i = entries.Count - 1; i >= 0; i--) { if (entries[i].packageName != packageName) continue; entries.RemoveAt(i); Save(); return; } } internal bool IsInDevMode(string packageName) { for (int i = 0; i < entries.Count; i++) if (entries[i].packageName == packageName) return true; return false; } } [Serializable] internal class PackageLockEntry { public string name; public string version; public string source; public string url; public string displayName; public bool isDependency; } internal static class DevModeSwitcher { private const string LockPath = "Packages/packages-lock.json"; internal static List GetCustomPackages() { var result = new List(); if (!File.Exists(LockPath)) return result; var lockJson = File.ReadAllText(LockPath); var manifestJson = ManifestEditor.Read(); ParseDependencies(lockJson, manifestJson, result); return result; } internal static bool EnableDevMode(string packageName, string localPath, bool resolve = true) { if (string.IsNullOrEmpty(packageName) || string.IsNullOrEmpty(localPath)) return false; var error = ValidateLocalPath(packageName, localPath); if (error != null) { Debug.LogWarning($"[DevMode] {packageName}: {error}"); return false; } var json = ManifestEditor.Read(); var currentValue = ManifestEditor.GetValue(json, packageName); var state = DevModeState.Load(); if (!string.IsNullOrEmpty(currentValue)) { state.SetOriginalValue(packageName, currentValue); json = ManifestEditor.SetValue(json, packageName, "file:" + localPath); } else { var embeddedDir = FindEmbeddedPackageDir(packageName); if (!string.IsNullOrEmpty(embeddedDir)) { var pj = Path.Combine(embeddedDir, "package.json"); var bak = Path.Combine(embeddedDir, "package.json.bak"); if (File.Exists(pj)) File.Move(pj, bak); state.SetOriginalValue(packageName, "embedded:" + embeddedDir); } else { state.SetOriginalValue(packageName, "transitive"); } json = ManifestEditor.AddEntry(json, packageName, "file:" + localPath); } ManifestEditor.Write(json); if (resolve) Debug.Log($"[DevMode] {packageName} → dev: file:{localPath}"); if (resolve) PackageResolveScheduler.Schedule(); return true; } internal static bool DisableDevMode(string packageName, bool resolve = true) { if (string.IsNullOrEmpty(packageName)) return false; var state = DevModeState.Load(); var original = state.GetOriginalValue(packageName); if (string.IsNullOrEmpty(original)) { if (resolve) Debug.LogWarning($"[DevMode] No saved state for {packageName}"); return false; } var json = ManifestEditor.Read(); if (original == "transitive") { json = ManifestEditor.RemoveEntry(json, packageName); } else if (original.StartsWith("embedded:")) { var embeddedDir = original.Substring(9); var bak = Path.Combine(embeddedDir, "package.json.bak"); var pj = Path.Combine(embeddedDir, "package.json"); if (File.Exists(bak)) File.Move(bak, pj); json = ManifestEditor.RemoveEntry(json, packageName); } else if (original == "embedded") { json = ManifestEditor.RemoveEntry(json, packageName); } else { json = ManifestEditor.SetValue(json, packageName, original); } ManifestEditor.Write(json); state.RemoveEntry(packageName); if (resolve) Debug.Log($"[DevMode] {packageName} → restored: {original}"); if (resolve) PackageResolveScheduler.Schedule(); return true; } internal static List BatchEnableDevMode(List packageNames, DevModeConfig config) { var enabled = new List(); for (int i = 0; i < packageNames.Count; i++) { var pkg = packageNames[i]; var localPath = config.GetLocalPath(pkg); if (string.IsNullOrEmpty(localPath)) continue; var err = ValidateLocalPath(pkg, localPath); if (err != null) { Debug.LogWarning($"[DevMode] Skipping {pkg}: {err}"); continue; } if (EnableDevMode(pkg, localPath, resolve: false)) enabled.Add(pkg); } if (enabled.Count > 0) PackageResolveScheduler.Schedule(); return enabled; } internal static List BatchDisableDevMode(List packageNames) { var disabled = new List(); for (int i = 0; i < packageNames.Count; i++) { if (DisableDevMode(packageNames[i], resolve: false)) disabled.Add(packageNames[i]); } if (disabled.Count > 0) PackageResolveScheduler.Schedule(); return disabled; } internal static bool IsFileReference(string packageName) { var json = ManifestEditor.Read(); var val = ManifestEditor.GetValue(json, packageName); return !string.IsNullOrEmpty(val) && val.StartsWith("file:"); } internal static string GetManifestValuePublic(string packageName) { var json = ManifestEditor.Read(); return ManifestEditor.GetValue(json, packageName); } internal static string ValidateLocalPath(string packageName, string folderPath) { if (string.IsNullOrEmpty(folderPath)) return "Empty path"; var pjPath = Path.Combine(folderPath, "package.json"); if (!File.Exists(pjPath)) return "No package.json in folder"; var json = File.ReadAllText(pjPath); var m = Regex.Match(json, "\"name\"\\s*:\\s*\"([^\"]+)\""); if (!m.Success) return "Cannot read name from package.json"; if (m.Groups[1].Value != packageName) return $"Mismatch: found '{m.Groups[1].Value}', expected '{packageName}'"; return null; } internal static string TryGetPathFromClipboard(string packageName) { var clip = GUIUtility.systemCopyBuffer; if (string.IsNullOrEmpty(clip)) return null; clip = clip.Trim(); if (!Directory.Exists(clip)) return null; var pjPath = Path.Combine(clip, "package.json"); if (!File.Exists(pjPath)) return null; var json = File.ReadAllText(pjPath); var m = Regex.Match(json, "\"name\"\\s*:\\s*\"([^\"]+)\""); if (!m.Success) return null; if (m.Groups[1].Value != packageName) return null; return clip; } internal static string SearchForPackage(string packageName, DevModeConfig config) { if (string.IsNullOrEmpty(packageName)) return null; var roots = new List(); var projectDir = Path.GetDirectoryName(Path.GetFullPath("Packages/manifest.json")); if (!string.IsNullOrEmpty(projectDir)) { projectDir = Path.GetDirectoryName(projectDir); if (!string.IsNullOrEmpty(projectDir)) { var repoRoot = Path.GetDirectoryName(projectDir); if (!string.IsNullOrEmpty(repoRoot)) { var localPkgs = Path.GetFullPath(Path.Combine(repoRoot, "LocalPackages")); if (Directory.Exists(localPkgs)) roots.Add(NormPath(localPkgs)); } } } if (config != null) { for (int i = 0; i < config.searchPaths.Count; i++) { var sp = config.searchPaths[i]; if (string.IsNullOrEmpty(sp) || !Directory.Exists(sp)) continue; var norm = NormPath(sp); if (!roots.Contains(norm)) roots.Add(norm); } } for (int r = 0; r < roots.Count; r++) { string[] dirs; try { dirs = Directory.GetDirectories(roots[r]); } catch { continue; } for (int d = 0; d < dirs.Length; d++) { var pjPath = Path.Combine(dirs[d], "package.json"); if (!File.Exists(pjPath)) continue; try { var json = File.ReadAllText(pjPath); var m = Regex.Match(json, "\"name\"\\s*:\\s*\"([^\"]+)\""); if (m.Success && m.Groups[1].Value == packageName) return Path.GetFullPath(dirs[d]); } catch { } } } return null; } private static string FindEmbeddedPackageDir(string packageName) { var packagesDir = Path.GetFullPath("Packages"); if (!Directory.Exists(packagesDir)) return null; var dirs = Directory.GetDirectories(packagesDir); for (int i = 0; i < dirs.Length; i++) { var pj = Path.Combine(dirs[i], "package.json"); if (!File.Exists(pj)) continue; var json = File.ReadAllText(pj); var m = Regex.Match(json, "\"name\"\\s*:\\s*\"([^\"]+)\""); if (m.Success && m.Groups[1].Value == packageName) return dirs[i]; } return null; } private static string NormPath(string path) { return Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); } private static string ReadDisplayName(string packageName, string version) { var cachePath = Path.Combine("Library", "PackageCache", $"{packageName}@{version}", "package.json"); if (File.Exists(cachePath)) { var json = File.ReadAllText(cachePath); var m = Regex.Match(json, "\"displayName\"\\s*:\\s*\"([^\"]+)\""); if (m.Success) return m.Groups[1].Value; } var embeddedPath = Path.Combine("Packages", packageName, "package.json"); if (File.Exists(embeddedPath)) { var json = File.ReadAllText(embeddedPath); var m = Regex.Match(json, "\"displayName\"\\s*:\\s*\"([^\"]+)\""); if (m.Success) return m.Groups[1].Value; } if (version.StartsWith("file:")) { var localDir = version.Substring(5); var pjPath = Path.Combine(localDir, "package.json"); if (!Path.IsPathRooted(localDir)) pjPath = Path.Combine("Packages", localDir, "package.json"); if (File.Exists(pjPath)) { var json = File.ReadAllText(pjPath); var m = Regex.Match(json, "\"displayName\"\\s*:\\s*\"([^\"]+)\""); if (m.Success) return m.Groups[1].Value; } } return null; } private static void ParseDependencies(string lockJson, string manifestJson, List result) { var lines = lockJson.Split('\n'); int topBraceDepth = 0; bool foundTopDeps = false; string currentPkg = null; string version = null; string source = null; string url = null; int pkgBraceDepth = 0; for (int i = 0; i < lines.Length; i++) { var line = lines[i].Trim(); if (!foundTopDeps) { if (Regex.IsMatch(line, "^\"dependencies\"\\s*:")) foundTopDeps = true; continue; } int openCount = CountChar(line, '{'); int closeCount = CountChar(line, '}'); if (currentPkg == null) { topBraceDepth += openCount - closeCount; if (topBraceDepth <= 0) break; var m = Regex.Match(line, "^\"([^\"]+)\"\\s*:\\s*\\{"); if (!m.Success) continue; currentPkg = m.Groups[1].Value; version = null; source = null; url = null; pkgBraceDepth = openCount - closeCount; if (pkgBraceDepth <= 0) { EmitEntry(currentPkg, version, source, url, manifestJson, result); currentPkg = null; } continue; } pkgBraceDepth += openCount - closeCount; var vm = Regex.Match(line, "\"version\"\\s*:\\s*\"([^\"]+)\""); if (vm.Success) version = vm.Groups[1].Value; var sm = Regex.Match(line, "\"source\"\\s*:\\s*\"([^\"]+)\""); if (sm.Success) source = sm.Groups[1].Value; var um = Regex.Match(line, "\"url\"\\s*:\\s*\"([^\"]+)\""); if (um.Success) url = um.Groups[1].Value; if (pkgBraceDepth > 0) continue; EmitEntry(currentPkg, version, source, url, manifestJson, result); currentPkg = null; } } private static int CountChar(string s, char c) { int n = 0; for (int i = 0; i < s.Length; i++) if (s[i] == c) n++; return n; } private static void EmitEntry(string name, string version, string source, string url, string manifestJson, List result) { if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(source)) return; bool isDep = false; if (!IsCustomPackage(name, source, url, manifestJson)) { if (!IsTransitiveDependency(name, source, url, manifestJson)) return; isDep = true; } var entry = new PackageLockEntry { name = name, version = version ?? "", source = source, url = url ?? "", isDependency = isDep }; entry.displayName = ReadDisplayName(entry.name, entry.version); result.Add(entry); } private static bool IsCustomPackage(string name, string source, string url, string manifestJson) { if (source == "builtin") return false; if (name.StartsWith("com.unity.")) return false; if (source == "embedded" || source == "local" || source == "local-tarball") return true; if (source == "registry" && !string.IsNullOrEmpty(url)) { if (url.Contains("packages.unity.com")) return false; } if (!manifestJson.Contains($"\"{name}\"")) return false; return true; } private static bool IsTransitiveDependency(string name, string source, string url, string manifestJson) { if (source == "builtin") return false; if (name.StartsWith("com.unity.")) return false; if (manifestJson.Contains($"\"{name}\"")) return false; if (source == "registry" && !string.IsNullOrEmpty(url)) { if (url.Contains("packages.unity.com")) return false; } return true; } } }