{"TotalCount":2,"Files":[{"Ident":"fobiat.sbox_mcp_server","Path":"Editor/SboxMcpServer.Editor.cs","FileName":"SboxMcpServer.Editor.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":342130,"Code":"//  s\u0026box MCP Server toolset, part two : what the editor believes, and making it notice\n//  a change on disk. See SboxMcpServer.cs for the header, licence and the engine-truth\n//  half of this same partial class.\n\n#nullable enable\n\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Microsoft.CodeAnalysis;\nusing Sandbox;\n\nnamespace Editor.Mcp;\n\npublic static partial class SboxMcpServer\n{\n\tpublic enum CompilerSlot\n\t{\n\t\tBoth,\n\t\tGame,\n\t\tEditor,\n\t}\n\n\n\t/// \u003Csummary\u003EReport which project the editor has open, where it sits on disk, and the compiler settings currently live in memory. Start here when an on-disk change is not taking effect: the settings reported are what Roslyn is actually using, which is not necessarily what the .sbproj on disk now says.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_info\u0022 )]\n\tpublic static ProjectInfo Info()\n\t{\n\t\tvar project = Open();\n\n\t\treturn new ProjectInfo\n\t\t{\n\t\t\tIdent = project.Config?.FullIdent,\n\t\t\tTitle = project.Config?.Title,\n\t\t\tType = project.Config?.Type,\n\t\t\tRootDirectory = project.RootDirectory?.FullName,\n\t\t\tConfigFilePath = project.ConfigFilePath,\n\t\t\tActive = project.Active,\n\t\t\tBroken = project.Broken,\n\t\t\tIsPublished = project.IsPublished,\n\t\t\tHasCompiler = project.HasCompiler,\n\t\t\tCompileSettings = LiveCompileSettings( project ),\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EList the project\u0027s compilers with their build state. A compiler sitting at NeedsBuild true while IsBuilding is false has work queued that nothing has started, which is what a stalled build looks like from the outside.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_compilers\u0022 )]\n\tpublic static CompilerList Compilers(\n\t\t[Description( \u0022Which compiler to report on.\u0022 )] CompilerSlot slot = CompilerSlot.Both )\n\t{\n\t\tvar project = Open();\n\n\t\treturn new CompilerList\n\t\t{\n\t\t\tCompilers = Slots( project, slot ).Select( Describe ).ToArray(),\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EAsk each compiler what source changes it has actually noticed since its last build. This is the direct answer to \u0022did my edit register\u0022, and it separates a file the compiler never saw from a file it saw and rejected.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_source_changes\u0022 )]\n\tpublic static SourceChangeList SourceChanges(\n\t\t[Description( \u0022Which compiler to report on.\u0022 )] CompilerSlot slot = CompilerSlot.Both )\n\t{\n\t\tvar project = Open();\n\n\t\tvar compilers = Slots( project, slot ).Select( Noticed ).ToArray();\n\n\t\treturn new SourceChangeList\n\t\t{\n\t\t\tCompilers = compilers,\n\n\t\t\t// An empty summary is genuinely ambiguous: the engine also returns {} when there is no\n\t\t\t// previous syntax tree to diff against, which is every compiler on a cold editor.\n\t\t\tHint = compilers.Length \u003E 0 \u0026\u0026 compilers.All( compiler =\u003E compiler.ChangeCount == 0 )\n\t\t\t\t? \u0022Every change set is empty. That means either nothing changed, or the compilers have no baseline to diff against yet because they have not built since the editor opened. Run project_build once, then ask again - a second empty answer is a real one.\u0022\n\t\t\t\t: null,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EReturn current compile diagnostics as structured rows with file and line, so errors can be read without scraping read_console. Errors sort first.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_compile_errors\u0022 )]\n\tpublic static DiagnosticList CompileErrors(\n\t\t[Description( \u0022Include warnings alongside errors.\u0022 )] bool includeWarnings = false,\n\t\t[Description( \u0022Maximum rows to return.\u0022 )] [Sandbox.Range( 1, 500 )] int limit = 50 )\n\t{\n\t\tvar raw = Project.CompileGroup?.BuildResult.Diagnostics;\n\n\t\tvar floor = includeWarnings ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error;\n\n\t\tvar rows = raw is null\n\t\t\t? Array.Empty\u003CDiagnosticRow\u003E()\n\t\t\t: raw.Where( diagnostic =\u003E diagnostic.Severity \u003E= floor )\n\t\t\t\t.OrderByDescending( diagnostic =\u003E diagnostic.Severity )\n\t\t\t\t.Take( limit )\n\t\t\t\t.Select( Flatten )\n\t\t\t\t.ToArray();\n\n\t\treturn new DiagnosticList\n\t\t{\n\t\t\tCount = rows.Length,\n\t\t\tDiagnostics = rows,\n\t\t\tHint = rows.Length == 0 ? \u0022No diagnostics. If a source edit still is not live, run project_source_changes, then project_assembly_freshness.\u0022 : null,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003ECompare what each compiler last built against what the process has actually loaded. Recompiling does not always cure a stale assembly: the editor goes on serving the version it loaded, and compile_status reads green the whole time.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_assembly_freshness\u0022 )]\n\tpublic static AssemblyFreshness AssemblyFreshnessOf()\n\t{\n\t\tvar project = Open();\n\t\tvar loaded = AppDomain.CurrentDomain.GetAssemblies();\n\n\t\tvar rows = Slots( project, CompilerSlot.Both )\n\t\t\t.Select( slot =\u003E\n\t\t\t{\n\t\t\t\tvar built = slot.Compiler.Output?.Version;\n\n\t\t\t\t// Hotloading leaves older copies behind under the same simple name, so the newest\n\t\t\t\t// one loaded is the one the process is serving\n\t\t\t\tvar copies = loaded\n\t\t\t\t\t.Select( assembly =\u003E assembly.GetName() )\n\t\t\t\t\t.Where( name =\u003E Same( name.Name, slot.Compiler.AssemblyName ) )\n\t\t\t\t\t.Select( name =\u003E name.Version )\n\t\t\t\t\t.Where( version =\u003E version is not null )\n\t\t\t\t\t.ToArray();\n\n\t\t\t\tvar newest = copies.Length == 0 ? null : copies.Max();\n\t\t\t\tvar stale = built is not null \u0026\u0026 newest is not null \u0026\u0026 built \u003E newest;\n\n\t\t\t\treturn new AssemblyFreshnessRow\n\t\t\t\t{\n\t\t\t\t\tSlot = slot.Label,\n\t\t\t\t\tName = slot.Compiler.Name,\n\t\t\t\t\tAssemblyName = slot.Compiler.AssemblyName,\n\t\t\t\t\tBuiltVersion = built?.ToString(),\n\t\t\t\t\tLoadedVersion = newest?.ToString(),\n\t\t\t\t\tLoadedCopies = copies.Length,\n\t\t\t\t\tStale = stale,\n\t\t\t\t\tHint = built is null ? \u0022This compiler has never produced a build, so there is nothing to compare.\u0022\n\t\t\t\t\t\t: newest is null ? \u0022Nothing by that assembly name is loaded. The build has not been hotloaded into the process at all.\u0022\n\t\t\t\t\t\t: stale ? \u0022The process is running an older build than the compiler produced. Rebuilding will not fix this - close and reopen the editor.\u0022\n\t\t\t\t\t\t: null,\n\t\t\t\t};\n\t\t\t} )\n\t\t\t.ToArray();\n\n\t\treturn new AssemblyFreshness\n\t\t{\n\t\t\tAssemblies = rows,\n\t\t\tAnyStale = rows.Any( row =\u003E row.Stale ),\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EResolve one or more content paths against everything currently mounted, applying the same _c suffix rule the engine applies when it loads a resource. A path that resolves to nothing does not throw at runtime: Model.Load hands back the engine\u0027s error model, so a typo in a .item or a prefab compiles clean, passes every headless test and ships an orange world.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_content_path\u0022 )]\n\tpublic static ContentPathResult ContentPath(\n\t\t[Description( \u0022One content path, or several separated by commas. For example \\\u0022models/citizen/citizen.vmdl\\\u0022.\u0022 )] string paths )\n\t{\n\t\tvar wanted = (paths ?? \u0022\u0022)\n\t\t\t.Split( \u0027,\u0027, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries )\n\t\t\t.ToArray();\n\n\t\tif ( wanted.Length == 0 )\n\t\t\tthrow new Exception( \u0022Pass at least one content path. Separate several with commas.\u0022 );\n\n\t\tvar rows = wanted.Select( path =\u003E\n\t\t{\n\t\t\t// ResourceLibrary.LoadGameResource does exactly this before it touches the filesystem\n\t\t\tvar compiled = path.EndsWith( \u0022_c\u0022, StringComparison.Ordinal );\n\t\t\tvar resolved = compiled ? path : path \u002B \u0022_c\u0022;\n\t\t\tvar exists = FileSystem.Mounted?.FileExists( resolved ) ?? false;\n\n\t\t\treturn new ContentPathRow\n\t\t\t{\n\t\t\t\tInput = path,\n\t\t\t\tResolved = resolved,\n\t\t\t\tExists = exists,\n\t\t\t\t// The asset system indexes source paths, so it wants the name without the suffix\n\t\t\t\tPackage = exists ? AssetSystem.FindByPath( compiled ? path[..^2] : path )?.Package?.FullIdent : null,\n\t\t\t\tHint = exists ? null : \u0022Nothing mounted provides this. Run project_content_search on its parent directory to find the real spelling - mounted packages often disagree with their own CDN manifests about the path prefix.\u0022,\n\t\t\t};\n\t\t} ).ToArray();\n\n\t\treturn new ContentPathResult\n\t\t{\n\t\t\tCount = rows.Length,\n\t\t\tFound = rows.Count( row =\u003E row.Exists ),\n\t\t\tMissing = rows.Count( row =\u003E !row.Exists ),\n\t\t\tPaths = rows,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EList files under a directory in the mounted content filesystem. This is the \u0022then what IS the right path\u0022 companion to project_content_path: mounted packages routinely disagree with their own CDN manifests about the path prefix, and the manifest spelling fails at runtime with no symptom at all.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_content_search\u0022 )]\n\tpublic static ContentSearchResult ContentSearch(\n\t\t[Description( \u0022Directory to search, for example \\\u0022models/citizen\\\u0022. Use \\\u0022/\\\u0022 for everything.\u0022 )] string directory = \u0022/\u0022,\n\t\t[Description( \u0022Filename pattern, for example \\\u0022*.vmdl_c\\\u0022. Compiled content carries the _c suffix.\u0022 )] string pattern = \u0022*\u0022,\n\t\t[Description( \u0022Recurse into subdirectories. A recursive search from \\\u0022/\\\u0022 walks every mounted package and is slow.\u0022 )] bool recursive = true,\n\t\t[Description( \u0022Maximum results.\u0022 )] [Sandbox.Range( 1, 500 )] int limit = 50 )\n\t{\n\t\tvar mounted = FileSystem.Mounted\n\t\t\t?? throw new Exception( \u0022Nothing is mounted yet. Wait for the editor to finish loading the project.\u0022 );\n\n\t\tvar found = mounted.FindFile( string.IsNullOrWhiteSpace( directory ) ? \u0022/\u0022 : directory, pattern, recursive ).ToArray();\n\t\tvar page = found.Take( limit ).ToArray();\n\n\t\treturn new ContentSearchResult\n\t\t{\n\t\t\tDirectory = directory,\n\t\t\tPattern = pattern,\n\t\t\tCount = page.Length,\n\t\t\tTotal = found.Length,\n\t\t\tFiles = page,\n\t\t\tHint = found.Length \u003E 0\n\t\t\t\t? \u0022These are relative to the directory searched. Pass a full path to project_content_path to confirm it resolves.\u0022\n\t\t\t\t: \u0022Nothing matched. Widen the pattern, or search the parent directory - the prefix is what usually differs from what a manifest says.\u0022,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EReconcile the package references written in the .sbproj against what is actually installed in the cloud cache. install_package mounts a package for this session and writes nothing to the project, so a package installed over MCP works perfectly until the editor restarts and then is simply gone.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_package_references\u0022 )]\n\tpublic static PackageReferenceReport PackageReferences()\n\t{\n\t\tvar project = Open();\n\t\tvar referenced = project.Config?.PackageReferences ?? new List\u003Cstring\u003E();\n\t\tvar installed = AssetSystem.GetInstalledPackages();\n\n\t\tvar rows = referenced.Select( ident =\u003E new PackageReferenceRow\n\t\t{\n\t\t\tIdent = ident,\n\t\t\tInstalled = AssetSystem.IsCloudInstalled( ident ),\n\t\t\tTitle = installed.FirstOrDefault( package =\u003E Same( package.FullIdent, ident ) )?.Title,\n\t\t} ).ToArray();\n\n\t\tvar extra = installed\n\t\t\t.Select( package =\u003E package.FullIdent )\n\t\t\t.Where( ident =\u003E ident is not null \u0026\u0026 !referenced.Any( reference =\u003E Same( reference, ident ) ) )\n\t\t\t.OrderBy( ident =\u003E ident, StringComparer.OrdinalIgnoreCase )\n\t\t\t.ToArray();\n\n\t\tvar missing = rows.Where( row =\u003E !row.Installed ).Select( row =\u003E row.Ident ).ToArray();\n\n\t\treturn new PackageReferenceReport\n\t\t{\n\t\t\tReferences = rows,\n\t\t\tInstalledNotReferenced = extra,\n\t\t\tHint = missing.Length \u003E 0\n\t\t\t\t? $\u0022Referenced but not installed: {string.Join( \u0022, \u0022, missing )}. Anything they provide will resolve to nothing at runtime.\u0022\n\t\t\t\t: extra.Length \u003E 0\n\t\t\t\t\t? \u0022Installed but not referenced. These are mounted for this session only - install_package writes nothing to the .sbproj, so they vanish at the next editor start. Add them to PackageReferences and run project_reload_config.\u0022\n\t\t\t\t\t: null,\n\t\t};\n\t}\n\n\n\t/// \u003Csummary\u003ERe-read the project\u0027s .sbproj from disk into the live config and recreate its compilers, so an externally edited Metadata.Compiler block actually reaches Roslyn. Nothing watches that file, so without this an on-disk config change silently never takes effect.\u003C/summary\u003E\n\t[McpTool( \u0022project_reload_config\u0022 )]\n\tpublic static ReloadConfigResult ReloadConfig()\n\t{\n\t\tvar project = Open();\n\n\t\tvar wasGame = project.Compiler;\n\t\tvar wasEditor = project.EditorCompiler;\n\n\t\tif ( Engine.CallOwned( project, \u0022LoadMinimal\u0022 ) is not true )\n\t\t\tthrow new Exception( \u0022Reloading the .sbproj failed, most likely a syntax error in it. Check read_console.\u0022 );\n\n\t\t// UpdateCompiler early-returns when CompilerHash still matches lastCompilerHash, and that\n\t\t// hash covers only compile settings, org, ident, type, the standalone flag and package\n\t\t// references. Zeroing it is what stops an edit to anything else being a silent no-op.\n\t\tEngine.SetOwned( project, \u0022lastCompilerHash\u0022, 0 );\n\t\tEngine.CallOwned( project, \u0022UpdateCompiler\u0022 );\n\n\t\tvar recreated = (project.Compiler is { } game \u0026\u0026 !ReferenceEquals( wasGame, game ))\n\t\t\t|| (project.EditorCompiler is { } editor \u0026\u0026 !ReferenceEquals( wasEditor, editor ));\n\n\t\treturn new ReloadConfigResult\n\t\t{\n\t\t\tReloaded = true,\n\t\t\tCompilersRecreated = recreated,\n\t\t\tCompileSettings = LiveCompileSettings( project ),\n\t\t\tHint = recreated\n\t\t\t\t? \u0022Compilers were recreated, so their file watchers are fresh too. Run project_build to compile against the settings above.\u0022\n\t\t\t\t: project.HasCompiler\n\t\t\t\t\t? \u0022The config reloaded but no compiler was recreated. That happens when the project loaded a precompiled assembly instead of building one.\u0022\n\t\t\t\t\t: \u0022The config reloaded and the project now has no compiler at all. Check Active and the code path in project_info - a config edit can deactivate a project.\u0022,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EDrop the cached ProjectSettings and push the reloaded input config back into the engine, so an externally edited Input.config, Platform.config or Collision.config actually takes effect. These are cached on first read and never invalidated, which is a separate trap from the .sbproj one.\u003C/summary\u003E\n\t[McpTool( \u0022project_reload_settings\u0022 )]\n\tpublic static ReloadSettingsResult ReloadSettings()\n\t{\n\t\tvar before = Sandbox.Input.GetActions()?.Count() ?? 0;\n\n\t\tEngine.CallShared( typeof( ProjectSettings ), \u0022ClearCache\u0022 );\n\n\t\t// Clearing the cache alone changes nothing here: Input.GetActions reads a static field that\n\t\t// only Input.ReadConfig ever assigns, and nothing calls it on a settings reload.\n\t\tEngine.CallShared( typeof( Sandbox.Input ), \u0022ReadConfig\u0022, new object?[] { ProjectSettings.Input } );\n\n\t\tvar after = Sandbox.Input.GetActions()?.Count() ?? 0;\n\n\t\treturn new ReloadSettingsResult\n\t\t{\n\t\t\tReloaded = true,\n\t\t\tActionsBefore = before,\n\t\t\tActionsAfter = after,\n\t\t\tHint = \u0022Every other config re-reads lazily on next access. Call project_input_actions to confirm Input.config parsed the way you meant it.\u0022,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EDispose and recreate every compiler, then start a build from the source on disk. Returns immediately.\u003C/summary\u003E\n\t[McpTool( \u0022project_rebuild\u0022 )]\n\tpublic static RebuildResult Rebuild()\n\t{\n\t\tRecreateCompilers();\n\n\t\treturn new RebuildResult\n\t\t{\n\t\t\tRebuilding = true,\n\t\t\tHint = \u0022Poll compile_status until IsBuilding is false, then call project_compile_errors.\u0022,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EBuild the project and wait for it to finish, then report success along with any errors. This is the one-shot version of project_rebuild followed by polling: reach for it when the question is simply whether the code compiles.\u003C/summary\u003E\n\t[McpTool( \u0022project_build\u0022 )]\n\tpublic static async Task\u003CBuildResult\u003E Build(\n\t\t[Description( \u0022Recreate compilers first, which also resets stale file watchers.\u0022 )] bool rebuild = true )\n\t{\n\t\tif ( rebuild )\n\t\t\tRecreateCompilers();\n\n\t\tvar building = Engine.CallShared( typeof( Project ), \u0022CompileAsync\u0022 ) as Task\n\t\t\t?? throw new Exception( \u0022Project.CompileAsync did not return a Task, engine API changed.\u0022 );\n\n\t\tawait building;\n\n\t\tvar succeeded = Engine.Peek( building, \u0022Result\u0022 ) is true;\n\n\t\treturn new BuildResult\n\t\t{\n\t\t\tSuccess = succeeded,\n\t\t\tErrors = succeeded ? null : CompileErrors(),\n\t\t\tHint = succeeded ? \u0022Built. If the change still is not live, run project_assembly_freshness.\u0022 : null,\n\t\t};\n\t}\n\n\n\tstatic Project Open()\n\t{\n\t\treturn Project.Current ?? throw new Exception( \u0022No project is open in the editor.\u0022 );\n\t}\n\n\tstatic void RecreateCompilers()\n\t{\n\t\tEngine.CallShared( typeof( Project ), \u0022RebuildCompilers\u0022 );\n\t}\n\n\tstatic (string Label, Compiler Compiler)[] Slots( Project project, CompilerSlot slot )\n\t{\n\t\tvar found = new List\u003C(string, Compiler)\u003E();\n\n\t\tif ( slot is CompilerSlot.Both or CompilerSlot.Game \u0026\u0026 project.Compiler is { } game )\n\t\t\tfound.Add( (\u0022Game\u0022, game) );\n\n\t\tif ( slot is CompilerSlot.Both or CompilerSlot.Editor \u0026\u0026 project.EditorCompiler is { } editor )\n\t\t\tfound.Add( (\u0022Editor\u0022, editor) );\n\n\t\treturn found.ToArray();\n\t}\n\n\tstatic Assembly[] ProjectAssemblies()\n\t{\n\t\tvar project = Project.Current;\n\t\tif ( project is null ) return Array.Empty\u003CAssembly\u003E();\n\n\t\tvar names = Slots( project, CompilerSlot.Both ).Select( slot =\u003E slot.Compiler.AssemblyName ).ToArray();\n\n\t\treturn AppDomain.CurrentDomain.GetAssemblies()\n\t\t\t.Where( assembly =\u003E names.Any( name =\u003E Same( assembly.GetName().Name, name ) ) )\n\t\t\t.ToArray();\n\t}\n\n\tstatic CompileSettingsInfo? LiveCompileSettings( Project project )\n\t{\n\t\tif ( project.Config is null ) return null;\n\n\t\tvar settings = Engine.CallOwned( project.Config, \u0022GetCompileSettings\u0022 );\n\t\tif ( settings is null ) return null;\n\n\t\treturn new CompileSettingsInfo\n\t\t{\n\t\t\tTreatWarningsAsErrors = Engine.Peek( settings, \u0022TreatWarningsAsErrors\u0022 ) as bool?,\n\t\t\tNullables = Engine.Peek( settings, \u0022Nullables\u0022 ) as bool?,\n\t\t\tRootNamespace = Engine.Peek( settings, \u0022RootNamespace\u0022 ) as string,\n\t\t\tDefineConstants = Engine.Peek( settings, \u0022DefineConstants\u0022 ) as string,\n\t\t\tNoWarn = Engine.Peek( settings, \u0022NoWarn\u0022 ) as string,\n\t\t\tWarningsAsErrors = Engine.Peek( settings, \u0022WarningsAsErrors\u0022 ) as string,\n\t\t};\n\t}\n\n\tstatic CompilerInfo Describe( (string Label, Compiler Compiler) slot )\n\t{\n\t\t// Compiler.BuildSuccess is Output?.Successful ?? false, which cannot tell a failed build\n\t\t// from one that has never run. Output itself can.\n\t\tvar success = slot.Compiler.Output?.Successful;\n\n\t\treturn new CompilerInfo\n\t\t{\n\t\t\tSlot = slot.Label,\n\t\t\tName = slot.Compiler.Name,\n\t\t\tAssemblyName = slot.Compiler.AssemblyName,\n\t\t\tIsBuilding = slot.Compiler.IsBuilding,\n\t\t\tNeedsBuild = slot.Compiler.NeedsBuild,\n\t\t\tSuccess = success,\n\t\t\tHint = success is null ? \u0022Never built. This is not a failure, it is an absence - run project_build.\u0022 : null,\n\t\t};\n\t}\n\n\tstatic SourceChangeInfo Noticed( (string Label, Compiler Compiler) slot )\n\t{\n\t\tvar changes = Engine.Hidden( slot.Compiler, \u0022ChangeSummary\u0022 ) as Dictionary\u003Cstring, object\u003E;\n\n\t\treturn new SourceChangeInfo\n\t\t{\n\t\t\tSlot = slot.Label,\n\t\t\tName = slot.Compiler.Name,\n\t\t\tChangeCount = changes?.Count ?? 0,\n\t\t\tChanges = changes,\n\t\t};\n\t}\n\n\n\tstatic DiagnosticRow Flatten( Diagnostic diagnostic )\n\t{\n\t\tvar span = diagnostic.Location.GetLineSpan();\n\t\tvar located = !string.IsNullOrEmpty( span.Path );\n\n\t\treturn new DiagnosticRow\n\t\t{\n\t\t\tId = diagnostic.Id,\n\t\t\tSeverity = diagnostic.Severity.ToString(),\n\t\t\tMessage = diagnostic.GetMessage(),\n\t\t\tFile = located ? span.Path : null,\n\t\t\tLine = located ? span.StartLinePosition.Line \u002B 1 : null,\n\t\t};\n\t}\n\n\n\tpublic class ProjectInfo\n\t{\n\t\tpublic string? Ident { get; set; }\n\t\tpublic string? Title { get; set; }\n\t\tpublic string? Type { get; set; }\n\t\tpublic string? RootDirectory { get; set; }\n\t\tpublic string? ConfigFilePath { get; set; }\n\t\tpublic bool Active { get; set; }\n\t\tpublic bool Broken { get; set; }\n\t\tpublic bool IsPublished { get; set; }\n\t\tpublic bool HasCompiler { get; set; }\n\t\tpublic CompileSettingsInfo? CompileSettings { get; set; }\n\t}\n\n\tpublic class CompileSettingsInfo\n\t{\n\t\tpublic bool? TreatWarningsAsErrors { get; set; }\n\t\tpublic bool? Nullables { get; set; }\n\t\tpublic string? RootNamespace { get; set; }\n\t\tpublic string? DefineConstants { get; set; }\n\t\tpublic string? NoWarn { get; set; }\n\t\tpublic string? WarningsAsErrors { get; set; }\n\t}\n\n\tpublic class CompilerList\n\t{\n\t\tpublic CompilerInfo[] Compilers { get; set; } = Array.Empty\u003CCompilerInfo\u003E();\n\t}\n\n\tpublic class CompilerInfo\n\t{\n\t\t[Description( \u0022Game or Editor.\u0022 )]\n\t\tpublic string? Slot { get; set; }\n\t\tpublic string? Name { get; set; }\n\t\tpublic string? AssemblyName { get; set; }\n\t\tpublic bool IsBuilding { get; set; }\n\t\tpublic bool NeedsBuild { get; set; }\n\t\t[Description( \u0022Whether the last build succeeded. Null when it has never built.\u0022 )]\n\t\tpublic bool? Success { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class SourceChangeList\n\t{\n\t\tpublic SourceChangeInfo[] Compilers { get; set; } = Array.Empty\u003CSourceChangeInfo\u003E();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class SourceChangeInfo\n\t{\n\t\tpublic string? Slot { get; set; }\n\t\tpublic string? Name { get; set; }\n\t\tpublic int ChangeCount { get; set; }\n\t\t[Description( \u0022The compiler\u0027s own change summary. Empty also means no baseline to diff against.\u0022 )]\n\t\tpublic Dictionary\u003Cstring, object\u003E? Changes { get; set; }\n\t}\n\n\tpublic class DiagnosticList\n\t{\n\t\tpublic int Count { get; set; }\n\t\tpublic DiagnosticRow[] Diagnostics { get; set; } = Array.Empty\u003CDiagnosticRow\u003E();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class DiagnosticRow\n\t{\n\t\t[Description( \u0022The diagnostic id, like \\\u0022CS0246\\\u0022.\u0022 )]\n\t\tpublic string? Id { get; set; }\n\t\tpublic string? Severity { get; set; }\n\t\tpublic string? Message { get; set; }\n\t\tpublic string? File { get; set; }\n\t\tpublic int? Line { get; set; }\n\t}\n\n\tpublic class AssemblyFreshness\n\t{\n\t\tpublic AssemblyFreshnessRow[] Assemblies { get; set; } = Array.Empty\u003CAssemblyFreshnessRow\u003E();\n\t\t[Description( \u0022True when any assembly the process is serving is older than what was built.\u0022 )]\n\t\tpublic bool AnyStale { get; set; }\n\t}\n\n\tpublic class AssemblyFreshnessRow\n\t{\n\t\tpublic string? Slot { get; set; }\n\t\tpublic string? Name { get; set; }\n\t\tpublic string? AssemblyName { get; set; }\n\t\t[Description( \u0022The version the compiler last produced. Null when it has never built.\u0022 )]\n\t\tpublic string? BuiltVersion { get; set; }\n\t\t[Description( \u0022The newest version of that assembly loaded in the process.\u0022 )]\n\t\tpublic string? LoadedVersion { get; set; }\n\t\t[Description( \u0022How many copies are loaded. More than one is normal after hotloads.\u0022 )]\n\t\tpublic int LoadedCopies { get; set; }\n\t\tpublic bool Stale { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class ContentPathResult\n\t{\n\t\tpublic int Count { get; set; }\n\t\tpublic int Found { get; set; }\n\t\tpublic int Missing { get; set; }\n\t\tpublic ContentPathRow[] Paths { get; set; } = Array.Empty\u003CContentPathRow\u003E();\n\t}\n\n\tpublic class ContentPathRow\n\t{\n\t\tpublic required string Input { get; set; }\n\t\t[Description( \u0022The path with the engine\u0027s _c suffix applied, which is what it actually opens.\u0022 )]\n\t\tpublic string? Resolved { get; set; }\n\t\t[Description( \u0022False means this loads the error model at runtime without throwing.\u0022 )]\n\t\tpublic bool Exists { get; set; }\n\t\t[Description( \u0022Which mounted package provides it.\u0022 )]\n\t\tpublic string? Package { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class ContentSearchResult\n\t{\n\t\tpublic string? Directory { get; set; }\n\t\tpublic string? Pattern { get; set; }\n\t\tpublic int Count { get; set; }\n\t\t[Description( \u0022How many matched before the limit was applied.\u0022 )]\n\t\tpublic int Total { get; set; }\n\t\t[Description( \u0022Paths relative to the directory searched.\u0022 )]\n\t\tpublic string[] Files { get; set; } = Array.Empty\u003Cstring\u003E();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class PackageReferenceReport\n\t{\n\t\tpublic PackageReferenceRow[] References { get; set; } = Array.Empty\u003CPackageReferenceRow\u003E();\n\t\t[Description( \u0022Installed in the cloud cache but absent from the .sbproj, so gone at next editor start.\u0022 )]\n\t\tpublic string[] InstalledNotReferenced { get; set; } = Array.Empty\u003Cstring\u003E();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class PackageReferenceRow\n\t{\n\t\tpublic required string Ident { get; set; }\n\t\tpublic bool Installed { get; set; }\n\t\tpublic string? Title { get; set; }\n\t}\n\n\tpublic class ReloadConfigResult\n\t{\n\t\tpublic bool Reloaded { get; set; }\n\t\t[Description( \u0022False means the reload changed nothing Roslyn cares about.\u0022 )]\n\t\tpublic bool CompilersRecreated { get; set; }\n\t\tpublic CompileSettingsInfo? CompileSettings { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class ReloadSettingsResult\n\t{\n\t\tpublic bool Reloaded { get; set; }\n\t\tpublic int ActionsBefore { get; set; }\n\t\t[Description( \u0022Unchanged after editing Input.config means the file did not parse. Check read_console.\u0022 )]\n\t\tpublic int ActionsAfter { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class RebuildResult\n\t{\n\t\tpublic bool Rebuilding { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class BuildResult\n\t{\n\t\tpublic bool Success { get; set; }\n\t\t[Description( \u0022The diagnostics that failed it. Null on success.\u0022 )]\n\t\tpublic DiagnosticList? Errors { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\n\tstatic class Engine\n\t{\n\t\tconst BindingFlags Shared = BindingFlags.Static | BindingFlags.NonPublic;\n\t\tconst BindingFlags Owned = BindingFlags.Instance | BindingFlags.NonPublic;\n\n\t\tpublic static Type Named( string fullName )\n\t\t{\n\t\t\treturn typeof( Project ).Assembly.GetType( fullName )\n\t\t\t\t?? throw new Exception( $\u0022{fullName} not found in Sandbox.Engine, engine API changed.\u0022 );\n\t\t}\n\n\t\tpublic static object? CallShared( Type owner, string name, object?[]? args = null )\n\t\t{\n\t\t\treturn Required( owner.GetMethod( name, Shared ), owner, name ).Invoke( null, args );\n\t\t}\n\n\t\tpublic static object? CallOwned( object target, string name, object?[]? args = null )\n\t\t{\n\t\t\tvar owner = target.GetType();\n\t\t\treturn Required( owner.GetMethod( name, Owned ), owner, name ).Invoke( target, args );\n\t\t}\n\n\t\tpublic static object? SharedField( Type owner, string name )\n\t\t{\n\t\t\treturn Required( owner.GetField( name, Shared ), owner, name ).GetValue( null );\n\t\t}\n\n\t\tpublic static void SetOwned( object target, string name, object? value )\n\t\t{\n\t\t\tvar owner = target.GetType();\n\t\t\tRequired( owner.GetField( name, Owned ), owner, name ).SetValue( target, value );\n\t\t}\n\n\t\tpublic static object? Hidden( object target, string name )\n\t\t{\n\t\t\tvar owner = target.GetType();\n\t\t\treturn Required( owner.GetProperty( name, Owned ), owner, name ).GetValue( target );\n\t\t}\n\n\t\tpublic static object? Peek( object? target, string name )\n\t\t{\n\t\t\treturn target?.GetType().GetProperty( name )?.GetValue( target );\n\t\t}\n\n\t\tpublic static object? Invoke( object? target, string name, object?[]? args = null )\n\t\t{\n\t\t\treturn target?.GetType().GetMethod( name )?.Invoke( target, args );\n\t\t}\n\n\t\tstatic T Required\u003CT\u003E( T? member, Type owner, string name ) where T : MemberInfo\n\t\t{\n\t\t\treturn member ?? throw new Exception( $\u0022{owner.Name}.{name} not found, engine API changed.\u0022 );\n\t\t}\n\t}\n}\n"},{"Ident":"fobiat.sbox_mcp_server","Path":"Editor/SboxMcpServer.cs","FileName":"SboxMcpServer.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":342130,"Code":"//  s\u0026box MCP Server toolset : eighteen MCP tools for the s\u0026box editor.\n//\n//  Ask the running engine what an API really is, ask the editor what it currently\n//  believes, and make it notice a change on disk. Drop this file (and its\n//  SboxMcpServer.Editor.cs partner) into a project\u0027s Editor/ folder; it registers\n//  as \u0022sbox_mcp_server\u0022.\n//\n//  Why each tool exists, which engine behaviours swallow an edit silently, and every\n//  internal member reached by reflection: editor-mcp/README.md.\n//\n//  fobiat (Kyle Tarff) \u003Ckyle@fobiat.dev\u003E  https://github.com/fobiat/sbox-skill\n//  MIT, see LICENSE. Engine 26.08.05, compile-checked by editor-mcp/compilecheck.\n\n#nullable enable\n\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Microsoft.CodeAnalysis;\nusing Sandbox;\n\nnamespace Editor.Mcp;\n\n/// \u003Csummary\u003EProject, config, content and compiler tools for driving an s\u0026amp;box project from outside the editor.\u003C/summary\u003E\n[McpToolset( \u0022sbox_mcp_server\u0022, \u0022Query the running engine for real type signatures, members, enum values, input actions and console commands, resolve content paths before they silently load the error model, read project, package and compiler state including assembly staleness, list compile errors, reload an externally edited .sbproj or ProjectSettings config, and rebuild from source on disk.\u0022 )]\npublic static partial class SboxMcpServer\n{\n\tpublic enum MemberKind\n\t{\n\t\tAll,\n\t\tMethods,\n\t\tProperties,\n\t\tFields,\n\t}\n\n\n\t/// \u003Csummary\u003ESearch the running engine for a type by name and report what it is. Ask this before writing an API you are not certain about.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_find_type\u0022 )]\n\tpublic static TypeSearch FindType(\n\t\t[Description( \u0022Type name or fragment, case insensitive. For example \\\u0022SceneTrace\\\u0022.\u0022 )] string name,\n\t\t[Description( \u0022Maximum results.\u0022 )] [Sandbox.Range( 1, 500 )] int limit = 20 )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( name ) )\n\t\t\tthrow new Exception( \u0022Pass a type name or fragment to search for.\u0022 );\n\n\t\tvar matches = KnownTypes()\n\t\t\t.Where( type =\u003E Matches( type.Name, name ) || Matches( type.FullName, name ) )\n\t\t\t.OrderBy( type =\u003E type.Name?.Length ?? int.MaxValue )\n\t\t\t.Take( limit )\n\t\t\t.Select( type =\u003E new FoundType\n\t\t\t{\n\t\t\t\tName = type.Name,\n\t\t\t\tNamespace = type.Namespace,\n\t\t\t\tKind = Shape( type ),\n\t\t\t\tBaseType = type.BaseType?.Name,\n\t\t\t\tMethods = type.Methods?.Length ?? 0,\n\t\t\t\tProperties = type.Properties?.Length ?? 0,\n\t\t\t\tDescription = type.Description,\n\t\t\t} )\n\t\t\t.ToArray();\n\n\t\treturn new TypeSearch\n\t\t{\n\t\t\tCount = matches.Length,\n\t\t\tTypes = matches,\n\t\t\tHint = matches.Length \u003E 0\n\t\t\t\t? \u0022Call project_type_members for the full signature list of one of these, or project_enum_values for an enum.\u0022\n\t\t\t\t: $\u0022Nothing matches \\\u0022{name}\\\u0022 in the loaded engine. Treat that as proof it does not exist rather than as a search that needs rewording.\u0022,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EList a type\u0027s methods and properties with real signatures, read from the running engine, and mark anything carrying [Obsolete]. This is the ground truth an API reference is only an approximation of, so prefer it whenever the two might disagree, and always for a type you are about to call something unfamiliar on.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_type_members\u0022 )]\n\tpublic static TypeMembers TypeMembersOf(\n\t\t[Description( \u0022Exact type name, for example \\\u0022SceneTrace\\\u0022.\u0022 )] string type,\n\t\t[Description( \u0022Only members whose name contains this. Optional.\u0022 )] string? filter = null,\n\t\t[Description( \u0022Restrict the listing to one kind of member.\u0022 )] MemberKind kind = MemberKind.All,\n\t\t[Description( \u0022Maximum members of each kind.\u0022 )] [Sandbox.Range( 1, 500 )] int limit = 60 )\n\t{\n\t\tvar found = Resolve( type );\n\n\t\tbool Wanted( string memberName ) =\u003E string.IsNullOrWhiteSpace( filter ) || Matches( memberName, filter );\n\n\t\tvar wantMethods = kind is MemberKind.All or MemberKind.Methods;\n\t\tvar wantProperties = kind is MemberKind.All or MemberKind.Properties;\n\n\t\treturn new TypeMembers\n\t\t{\n\t\t\tType = found.FullName,\n\t\t\tKind = Shape( found ),\n\t\t\tBaseType = found.BaseType?.FullName,\n\n\t\t\tMethods = !wantMethods ? Array.Empty\u003CMethodRow\u003E() : (found.Methods ?? Array.Empty\u003CMethodDescription\u003E())\n\t\t\t\t.Where( method =\u003E !method.IsSpecialName \u0026\u0026 Wanted( method.Name ) )\n\t\t\t\t.Take( limit )\n\t\t\t\t.Select( method =\u003E new MethodRow\n\t\t\t\t{\n\t\t\t\t\tName = method.Name,\n\t\t\t\t\tSignature = Signature( method ),\n\t\t\t\t\tStatic = method.IsStatic,\n\t\t\t\t\tObsolete = Deprecation( method ),\n\t\t\t\t\tDescription = method.Description,\n\t\t\t\t} )\n\t\t\t\t.ToArray(),\n\n\t\t\tProperties = !wantProperties ? Array.Empty\u003CPropertyRow\u003E() : (found.Properties ?? Array.Empty\u003CPropertyDescription\u003E())\n\t\t\t\t.Where( property =\u003E Wanted( property.Name ) )\n\t\t\t\t.Take( limit )\n\t\t\t\t.Select( property =\u003E new PropertyRow\n\t\t\t\t{\n\t\t\t\t\tName = property.Name,\n\t\t\t\t\tType = Readable( property.PropertyType ),\n\t\t\t\t\tAccess = property.CanRead \u0026\u0026 property.CanWrite ? \u0022get set\u0022 : property.CanRead ? \u0022get\u0022 : \u0022set\u0022,\n\t\t\t\t\tStatic = property.IsStatic,\n\t\t\t\t\tObsolete = Deprecation( property ),\n\t\t\t\t\tDescription = property.Description,\n\t\t\t\t} )\n\t\t\t\t.ToArray(),\n\n\t\t\tHint = found.IsEnum\n\t\t\t\t? \u0022This is an enum, so it has no methods or properties worth listing. Call project_enum_values for its named values.\u0022\n\t\t\t\t: null,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003ESearch every loaded type for members whose name contains a fragment, and report which type each one is declared on. Reach for this when you know roughly what a method is called but not what it hangs off, which is the case project_type_members cannot answer because it needs the exact type name up front.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_find_member\u0022 )]\n\tpublic static MemberSearch FindMember(\n\t\t[Description( \u0022Member name or fragment, case insensitive. For example \\\u0022RunTrace\\\u0022.\u0022 )] string name,\n\t\t[Description( \u0022Restrict the search to one kind of member.\u0022 )] MemberKind kind = MemberKind.All,\n\t\t[Description( \u0022Only members declared on types whose name contains this. Optional.\u0022 )] string? type = null,\n\t\t[Description( \u0022Maximum results.\u0022 )] [Sandbox.Range( 1, 500 )] int limit = 30 )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( name ) )\n\t\t\tthrow new Exception( \u0022Pass a member name or fragment to search for.\u0022 );\n\n\t\tbool WantedKind( MemberDescription member ) =\u003E kind switch\n\t\t{\n\t\t\tMemberKind.Methods =\u003E member.IsMethod,\n\t\t\tMemberKind.Properties =\u003E member.IsProperty,\n\t\t\tMemberKind.Fields =\u003E member.IsField,\n\t\t\t_ =\u003E member.IsMethod || member.IsProperty || member.IsField,\n\t\t};\n\n\t\tvar matches = KnownTypes()\n\t\t\t.Where( owner =\u003E string.IsNullOrWhiteSpace( type ) || Matches( owner.Name, type ) )\n\t\t\t// DeclaredMembers, not Members: an inherited member would otherwise repeat once per subclass\n\t\t\t.SelectMany( owner =\u003E (owner.DeclaredMembers ?? Array.Empty\u003CMemberDescription\u003E())\n\t\t\t\t.Where( member =\u003E WantedKind( member ) \u0026\u0026 Matches( member.Name, name ) )\n\t\t\t\t.Select( member =\u003E new FoundMember\n\t\t\t\t{\n\t\t\t\t\tType = owner.FullName,\n\t\t\t\t\tName = member.Name,\n\t\t\t\t\tKind = member.IsMethod ? \u0022method\u0022 : member.IsProperty ? \u0022property\u0022 : \u0022field\u0022,\n\t\t\t\t\tSignature = member is MethodDescription method ? Signature( method ) : null,\n\t\t\t\t\tStatic = member.IsStatic,\n\t\t\t\t\tObsolete = Deprecation( member ),\n\t\t\t\t\tDescription = member.Description,\n\t\t\t\t} ) )\n\t\t\t.OrderBy( member =\u003E member.Name.Length )\n\t\t\t.Take( limit )\n\t\t\t.ToArray();\n\n\t\treturn new MemberSearch\n\t\t{\n\t\t\tCount = matches.Length,\n\t\t\tMembers = matches,\n\t\t\tHint = matches.Length \u003E 0\n\t\t\t\t? \u0022Call project_type_members on one of these types for its full signature list.\u0022\n\t\t\t\t: $\u0022No member anywhere in the loaded engine contains \\\u0022{name}\\\u0022. Treat that as proof it does not exist.\u0022,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EList an enum\u0027s named values with their numeric values. Enums have no methods or properties, so project_type_members reports one as empty, which reads as \u0022the type does not exist\u0022 when it means \u0022wrong tool\u0022.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_enum_values\u0022 )]\n\tpublic static EnumValues EnumValuesOf(\n\t\t[Description( \u0022Exact enum name, for example \\\u0022HitboxTags\\\u0022.\u0022 )] string type )\n\t{\n\t\tvar found = Resolve( type );\n\n\t\tif ( !found.IsEnum )\n\t\t\tthrow new Exception( $\u0022\\\u0022{found.FullName}\\\u0022 is a {Shape( found )}, not an enum. Call project_type_members for it instead.\u0022 );\n\n\t\tvar description = EditorTypeLibrary.GetEnumDescription( found.TargetType )\n\t\t\t?? throw new Exception( $\u0022The engine has no enum description for \\\u0022{found.FullName}\\\u0022, which should not happen. Check read_console.\u0022 );\n\n\t\tvar entries = description.Select( entry =\u003E new EnumEntry\n\t\t{\n\t\t\tName = entry.Name,\n\t\t\tValue = entry.IntegerValue,\n\t\t\tTitle = entry.Title,\n\t\t\tGroup = entry.Group,\n\t\t\tDescription = entry.Description,\n\t\t} ).ToArray();\n\n\t\treturn new EnumValues\n\t\t{\n\t\t\tType = found.FullName,\n\t\t\tCount = entries.Length,\n\t\t\tValues = entries,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EList the input actions this project defines, with their keyboard and gamepad bindings. Input actions are strings resolved at runtime, so Input.Down( \u0022jump\u0022 ) on an action that does not exist compiles cleanly and silently never fires.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_input_actions\u0022 )]\n\tpublic static InputActionList InputActions()\n\t{\n\t\tvar actions = Sandbox.Input.GetActions()?.ToArray() ?? Array.Empty\u003CSandbox.InputAction\u003E();\n\n\t\treturn new InputActionList\n\t\t{\n\t\t\tCount = actions.Length,\n\t\t\tActions = actions.Select( action =\u003E new InputActionRow\n\t\t\t{\n\t\t\t\tName = action.Name,\n\t\t\t\tTitle = action.Title,\n\t\t\t\tGroup = action.GroupName,\n\t\t\t\tKeyboard = action.KeyboardCode,\n\t\t\t\tGamepad = action.GamepadCode.ToString(),\n\t\t\t} ).ToArray(),\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EList the console commands and convars the engine currently knows, optionally only the ones this project\u0027s own assemblies registered. A console command given the wrong argument form prints its usage and changes nothing, which from the outside is indistinguishable from having run, so check the real name and shape here before driving anything through console_command.\u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022project_console_commands\u0022 )]\n\tpublic static ConsoleCommandList ConsoleCommands(\n\t\t[Description( \u0022Only commands whose name contains this, case insensitive. Optional.\u0022 )] string? filter = null,\n\t\t[Description( \u0022Only commands registered by this project\u0027s own assemblies.\u0022 )] bool projectOnly = false,\n\t\t[Description( \u0022Maximum results.\u0022 )] [Sandbox.Range( 1, 500 )] int limit = 50 )\n\t{\n\t\tvar members = Engine.SharedField( Engine.Named( \u0022Sandbox.ConVarSystem\u0022 ), \u0022Members\u0022 ) as IDictionary\n\t\t\t?? throw new Exception( \u0022ConVarSystem.Members is not a dictionary any more, engine API changed.\u0022 );\n\n\t\tvar owned = ProjectAssemblies();\n\n\t\tbool FromProject( object command ) =\u003E\n\t\t\towned.Any( assembly =\u003E Engine.Invoke( command, \u0022IsFromAssembly\u0022, new object?[] { assembly } ) is true );\n\n\t\tvar rows = new List\u003CConsoleCommandRow\u003E();\n\n\t\tforeach ( var command in members.Values )\n\t\t{\n\t\t\tif ( command is null ) continue;\n\n\t\t\tvar name = Engine.Peek( command, \u0022Name\u0022 ) as string;\n\t\t\tif ( name is null || !(string.IsNullOrWhiteSpace( filter ) || Matches( name, filter )) ) continue;\n\n\t\t\tvar mine = FromProject( command );\n\t\t\tif ( projectOnly \u0026\u0026 !mine ) continue;\n\n\t\t\trows.Add( new ConsoleCommandRow\n\t\t\t{\n\t\t\t\tName = name,\n\t\t\t\tKind = Engine.Peek( command, \u0022IsConCommand\u0022 ) is true ? \u0022command\u0022 : \u0022convar\u0022,\n\t\t\t\tHelp = Engine.Peek( command, \u0022Help\u0022 ) as string,\n\t\t\t\tUsage = Engine.Invoke( command, \u0022BuildDescription\u0022 ) as string,\n\t\t\t\tIsAdmin = Engine.Peek( command, \u0022IsAdmin\u0022 ) is true,\n\t\t\t\tIsServer = Engine.Peek( command, \u0022IsServer\u0022 ) is true,\n\t\t\t\tIsCheat = Engine.Peek( command, \u0022IsCheat\u0022 ) is true,\n\t\t\t\tFromProject = mine,\n\t\t\t} );\n\t\t}\n\n\t\tvar page = rows.OrderBy( row =\u003E row.Name, StringComparer.OrdinalIgnoreCase ).Take( limit ).ToArray();\n\n\t\treturn new ConsoleCommandList\n\t\t{\n\t\t\tCount = page.Length,\n\t\t\tTotal = rows.Count,\n\t\t\tCommands = page,\n\t\t\tHint = page.Length == 0 \u0026\u0026 projectOnly\n\t\t\t\t? \u0022Nothing here came from this project. Either its assemblies have not loaded yet or nothing carries [ConVar].\u0022\n\t\t\t\t: null,\n\t\t};\n\t}\n\n\n\tstatic IEnumerable\u003CTypeDescription\u003E KnownTypes()\n\t{\n\t\tvar library = Sandbox.Internal.GlobalToolsNamespace.EditorTypeLibrary\n\t\t\t?? throw new Exception( \u0022EditorTypeLibrary is not available yet. Wait for the editor to finish loading.\u0022 );\n\n\t\treturn library.GetTypes\u003Cobject\u003E() ?? Enumerable.Empty\u003CTypeDescription\u003E();\n\t}\n\n\t/// \u003Csummary\u003EResolve a type by simple or full name, preferring a top-level match and naming the alternatives rather than silently answering about the wrong type.\u003C/summary\u003E\n\tstatic TypeDescription Resolve( string type )\n\t{\n\t\tvar matches = KnownTypes()\n\t\t\t.Where( candidate =\u003E Same( candidate.Name, type ) || Same( candidate.FullName, type ) )\n\t\t\t.ToArray();\n\n\t\tif ( matches.Length == 0 )\n\t\t\tthrow new Exception( $\u0022No type named \\\u0022{type}\\\u0022 in the loaded engine. Run project_find_type first.\u0022 );\n\n\t\tif ( matches.Length == 1 )\n\t\t\treturn matches[0];\n\n\t\tvar exact = matches.Where( candidate =\u003E Same( candidate.FullName, type ) ).ToArray();\n\t\tif ( exact.Length == 1 ) return exact[0];\n\n\t\t// A nested type\u0027s FullName carries a \u0027\u002B\u0027. Asking for \u0022SyncFlags\u0022 means Sandbox.SyncFlags,\n\t\t// not Terrain\u0027s nested one, and picking the first match answered about the wrong type.\n\t\tvar topLevel = matches.Where( candidate =\u003E candidate.FullName?.Contains( \u0027\u002B\u0027 ) != true ).ToArray();\n\t\tif ( topLevel.Length == 1 ) return topLevel[0];\n\n\t\tvar names = string.Join( \u0022, \u0022, matches.Select( candidate =\u003E candidate.FullName ).Take( 6 ) );\n\t\tthrow new Exception( $\u0022\\\u0022{type}\\\u0022 is ambiguous across {matches.Length} types: {names}. Pass a full name.\u0022 );\n\t}\n\n\tstatic string Shape( TypeDescription type )\n\t{\n\t\tif ( type.IsEnum ) return \u0022enum\u0022;\n\t\tif ( type.IsInterface ) return \u0022interface\u0022;\n\t\tif ( type.IsStatic ) return \u0022static class\u0022;\n\t\tif ( type.IsValueType ) return \u0022struct\u0022;\n\t\tif ( type.IsAbstract ) return \u0022abstract class\u0022;\n\t\treturn \u0022class\u0022;\n\t}\n\n\tstatic string Signature( MethodDescription method )\n\t{\n\t\tvar arguments = method.Parameters.Select( p =\u003E $\u0022{Readable( p.ParameterType )} {p.Name}\u0022 );\n\t\treturn $\u0022{Readable( method.ReturnType )} {method.Name}( {string.Join( \u0022, \u0022, arguments )} )\u0022;\n\t}\n\n\tstatic string Readable( Type? type )\n\t{\n\t\tif ( type is null ) return \u0022void\u0022;\n\t\tif ( !type.IsGenericType ) return type.Name;\n\n\t\tvar arguments = type.GetGenericArguments().Select( Readable );\n\t\treturn $\u0022{type.Name.Split( \u0027\u0060\u0027 )[0]}\u003C{string.Join( \u0022, \u0022, arguments )}\u003E\u0022;\n\t}\n\n\tstatic string? Deprecation( MemberDescription member )\n\t{\n\t\tvar obsolete = member.GetCustomAttribute\u003CObsoleteAttribute\u003E();\n\t\tif ( obsolete is null ) return null;\n\n\t\treturn string.IsNullOrWhiteSpace( obsolete.Message ) ? \u0022obsolete\u0022 : obsolete.Message;\n\t}\n\n\n\tstatic bool Matches( string? haystack, string? needle ) =\u003E\n\t\tneedle is not null \u0026\u0026 haystack?.Contains( needle, StringComparison.OrdinalIgnoreCase ) == true;\n\n\tstatic bool Same( string? a, string? b ) =\u003E string.Equals( a, b, StringComparison.OrdinalIgnoreCase );\n\n\n\tpublic class TypeSearch\n\t{\n\t\tpublic int Count { get; set; }\n\t\tpublic FoundType[] Types { get; set; } = Array.Empty\u003CFoundType\u003E();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class FoundType\n\t{\n\t\tpublic string? Name { get; set; }\n\t\tpublic string? Namespace { get; set; }\n\t\t[Description( \u0022class, struct, interface, enum, abstract class or static class.\u0022 )]\n\t\tpublic string? Kind { get; set; }\n\t\tpublic string? BaseType { get; set; }\n\t\tpublic int Methods { get; set; }\n\t\tpublic int Properties { get; set; }\n\t\tpublic string? Description { get; set; }\n\t}\n\n\tpublic class TypeMembers\n\t{\n\t\tpublic string? Type { get; set; }\n\t\tpublic string? Kind { get; set; }\n\t\tpublic string? BaseType { get; set; }\n\t\tpublic MethodRow[] Methods { get; set; } = Array.Empty\u003CMethodRow\u003E();\n\t\tpublic PropertyRow[] Properties { get; set; } = Array.Empty\u003CPropertyRow\u003E();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class MethodRow\n\t{\n\t\tpublic required string Name { get; set; }\n\t\t[Description( \u0022The signature as C# would write it, return type first.\u0022 )]\n\t\tpublic string? Signature { get; set; }\n\t\tpublic bool Static { get; set; }\n\t\t[Description( \u0022The [Obsolete] message when the member is deprecated, otherwise null. Do not write new code against it.\u0022 )]\n\t\tpublic string? Obsolete { get; set; }\n\t\tpublic string? Description { get; set; }\n\t}\n\n\tpublic class PropertyRow\n\t{\n\t\tpublic required string Name { get; set; }\n\t\tpublic string? Type { get; set; }\n\t\t[Description( \u0022\\\u0022get\\\u0022, \\\u0022set\\\u0022 or \\\u0022get set\\\u0022.\u0022 )]\n\t\tpublic string? Access { get; set; }\n\t\tpublic bool Static { get; set; }\n\t\t[Description( \u0022The [Obsolete] message when the member is deprecated, otherwise null. Do not write new code against it.\u0022 )]\n\t\tpublic string? Obsolete { get; set; }\n\t\tpublic string? Description { get; set; }\n\t}\n\n\tpublic class MemberSearch\n\t{\n\t\tpublic int Count { get; set; }\n\t\tpublic FoundMember[] Members { get; set; } = Array.Empty\u003CFoundMember\u003E();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class FoundMember\n\t{\n\t\t[Description( \u0022The type declaring this member. Pass it to project_type_members.\u0022 )]\n\t\tpublic string? Type { get; set; }\n\t\tpublic required string Name { get; set; }\n\t\t[Description( \u0022method, property or field.\u0022 )]\n\t\tpublic string? Kind { get; set; }\n\t\tpublic string? Signature { get; set; }\n\t\tpublic bool Static { get; set; }\n\t\tpublic string? Obsolete { get; set; }\n\t\tpublic string? Description { get; set; }\n\t}\n\n\tpublic class EnumValues\n\t{\n\t\tpublic string? Type { get; set; }\n\t\tpublic int Count { get; set; }\n\t\tpublic EnumEntry[] Values { get; set; } = Array.Empty\u003CEnumEntry\u003E();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class EnumEntry\n\t{\n\t\t[Description( \u0022The name to write in code.\u0022 )]\n\t\tpublic required string Name { get; set; }\n\t\tpublic long Value { get; set; }\n\t\tpublic string? Title { get; set; }\n\t\tpublic string? Group { get; set; }\n\t\tpublic string? Description { get; set; }\n\t}\n\n\tpublic class InputActionList\n\t{\n\t\tpublic int Count { get; set; }\n\t\tpublic InputActionRow[] Actions { get; set; } = Array.Empty\u003CInputActionRow\u003E();\n\t}\n\n\tpublic class InputActionRow\n\t{\n\t\t[Description( \u0022The exact string Input.Down and friends expect.\u0022 )]\n\t\tpublic required string Name { get; set; }\n\t\tpublic string? Title { get; set; }\n\t\tpublic string? Group { get; set; }\n\t\tpublic string? Keyboard { get; set; }\n\t\tpublic string? Gamepad { get; set; }\n\t}\n\n\tpublic class ConsoleCommandList\n\t{\n\t\tpublic int Count { get; set; }\n\t\t[Description( \u0022How many matched before the limit was applied.\u0022 )]\n\t\tpublic int Total { get; set; }\n\t\tpublic ConsoleCommandRow[] Commands { get; set; } = Array.Empty\u003CConsoleCommandRow\u003E();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class ConsoleCommandRow\n\t{\n\t\tpublic required string Name { get; set; }\n\t\t[Description( \u0022command or convar. A convar takes a value, a command takes arguments.\u0022 )]\n\t\tpublic string? Kind { get; set; }\n\t\tpublic string? Help { get; set; }\n\t\t[Description( \u0022For a convar, its current and default value alongside the help text.\u0022 )]\n\t\tpublic string? Usage { get; set; }\n\t\tpublic bool IsAdmin { get; set; }\n\t\tpublic bool IsServer { get; set; }\n\t\tpublic bool IsCheat { get; set; }\n\t\t[Description( \u0022Registered by this project\u0027s own code rather than by the engine.\u0022 )]\n\t\tpublic bool FromProject { get; set; }\n\t}\n}\n"}]}