Files
LabApiPluginTemplate/CLAUDE.md
T

14 KiB

CLAUDE.md

This repo is a dotnet new project template (identity SCPSL.LabApi.PluginTemplate, shortName labapi-plugin), not a runnable plugin. It scaffolds SCP: Secret Laboratory LabAPI plugins.

The next section only applies while you're editing this template repository (i.e. a .template.config/ folder is present next to this file). It does not apply if you're instead looking at a plugin someone already generated from this template — that folder never contains .template.config/, and its EntryPoint.cs/Config.cs are ordinary project files at that point, not template scaffolding.

Repo is template source — read before touching any file

.template.config/template.json sets this folder's name as the template's sourceName and defines one text-replacement symbol, PluginAuthor. The dotnet templating engine does a literal, repo-wide string replacement at generation time — not semantic renaming — driven by that config:

  • The folder/project name (sourceName in template.json, currently this repo's own folder name) → whatever -n/--name the consumer passes to dotnet new.
  • The placeholder value of the PluginAuthor symbol (the string currently sitting in EntryPoint.cs's Author property) → the consumer's --PluginAuthor.

Open template.json to see the exact current literal for each — don't hardcode them again here or anywhere else in prose, because this file itself ships inside every generated project and any exact match gets rewritten too (self-referential text like "the project is called X" would silently turn into nonsense after generation).

Consequences:

  • Never introduce an exact match of one of those two literals elsewhere in the repo unless you intend it to be substituted too.
  • If you add a new thing that should be configurable at generation time, add a symbol to template.json (type: parameter, replaces: "<literal>") instead of hardcoding a magic string.
  • EntryPoint and Config (the class names) are not substituted and must stay literally named that — LabAPI's PluginLoader discovers plugins by type inheritance, not by name, and the file names EntryPoint.cs/Config.cs are a fixed part of this template's contract. Only the namespace tracks the project name.

Repo layout (only what affects decisions)

  • Repo root holds only template-source/maintenance files that must never ship in a generated project: .git/, .gitignore, .editorconfig, CLAUDE.md, and .template.config/. None of these are copied to generated output — see "content/ vs. repo root" below.
  • .template.config/template.json — template identity, the PluginAuthor symbol, a sources entry that maps ./content → the output root (this is what keeps the root-level files above out of every generated project), primaryOutputs (points at the project file using its pre-substitution literal name, relative to content/ — that's correct and expected, not a bug), and one postAction: the standard dotnet-templating "restore NuGet packages" action (id 210D431B-...; skipped in Visual Studio, which restores on its own).
  • .template.config/ide.host.json — Visual Studio-only UI hints for the New Project wizard. It does not rename the PluginAuthor symbol (a flag can't contain a space); it only maps the symbol's id to a human-readable name.text label shown in VS's UI, via isVisible: "true" (VS hides a symbol from that UI unless explicitly marked visible here). Ignored entirely by dotnet new on the CLI.
  • content/ — the actual buildable project, and the only folder the sources mapping in template.json copies into generated output: LabApiPluginTemplate.csproj, EntryPoint.cs, Config.cs, and content/CLAUDE.md. Add any new file that should ship with generated plugins in here, not at the repo root.
  • content/CLAUDE.md is a different file from this one (the repo-root CLAUDE.md, which is template-editing guidance and never ships). It's the project memory a generated plugin ships with, so Claude Code has LabAPI/toolchain/style context automatically in every plugin generated from this template, without depending on any C#/.NET/LabAPI Skill existing in the catalog (none currently does — re-check with SearchSkills/ToolSearch before assuming that's still true). Keep its LabAPI-contract/toolchain/style sections in sync with this file's equivalent sections when either changes; content/CLAUDE.md omits everything specific to editing this template repo (symbols, sources mapping, sourceName traps, the verification workflow below) since none of that applies to a generated plugin.
  • No .sln, no test project, no CI, no packaging (.nupkg) project, no GitHub Copilot instructions file currently exist in this folder. Don't assume any of them. This repo itself does have its own git history (see git log) — that's unrelated to whether a generated project gets one, which is what "Git on generation" below covers.

content/ vs. repo root — why the project isn't at the repo root

Early versions of this template had the .csproj/.cs files sitting directly next to .gitignore/.editorconfig/CLAUDE.md/.template.config/ at the repo root, with no sources entry in template.json. Because dotnet new copies everything under the mounted template root except .template.config/ by default, every one of those maintenance files silently ended up inside every generated plugin too.

The fix: content/ holds only what should be copied, and template.json declares "sources": [{ "source": "./content", "target": "./" }]. Files outside ./content (root-level .gitignore, .editorconfig, CLAUDE.md) are never touched by generation — no exclude-list to maintain.

Do not nest a folder named LabApiPluginTemplate inside content/ (e.g. content/LabApiPluginTemplate/EntryPoint.cs). sourceName substitution renames any folder whose name matches it, so an inner folder named after the project would get renamed too, producing a doubled-up output path like <output>/<ProjectName>/<ProjectName>/<ProjectName>.csproj instead of the flat <output>/<ProjectName>/<ProjectName>.csproj consumers expect. Keep the .csproj/ .cs files directly under content/.

Git on generation — why there's no automated git init

A generated project is not a git repository, and there is no postAction that tries to make it one. This was verified, not assumed — re-verified against the dotnet 10.0.400 SDK on this machine while working on this exact question:

  • dotnet new's only postAction capable of running an arbitrary process is the "Run script" action (id 3A7C4B45-...). Wiring it to run git init/git add/ git commit hangs forever in any non-interactive shell: the CLI's script-execution security gate demands an explicit Y typed at a Do you want to run this action [Y(yes)|N(no)]? prompt, once per script action. dotnet new create --help on this SDK exposes no --allow-scripts or equivalent flag to pre-approve it (unlike the NuGet-restore postAction, which the CLI treats as trusted and never gates). Do not reintroduce "Run script" postActions for this without re-verifying that gate no longer applies on whatever SDK is current then.
  • A previous revision compensated with a SkipGit symbol plus a "display manual instructions" postAction that just printed the git init one-liner. That has been removed — it wasn't automation, just a reminder to run a command any git user already knows, so it wasn't worth the extra symbol/postAction/ide.host.json entry.
  • Visual Studio's own "New Project" dialog already has a built-in "Create new Git repository" checkbox that works for any template, including this one, with zero template-side configuration — so VS users get real automatic git init for free. CLI users just run git init themselves after generating.

LabAPI contract (verified against northwood-studios/LabAPI source, not guessed)

Plugin<TConfig> (LabApi.Loader.Features.Plugins) requires implementing: Name, Description, Author, RequiredApiVersion (all abstract get-only) plus Enable()/Disable(). Overridable but optional: Version (defaults to the assembly's version), Priority (LoadPriority, defaults to Medium), ConfigFileName (defaults to "config.yml"). Config is a plain class with a public parameterless constructor and public get/set properties — the base class (de)serializes it via YamlDotNet automatically; don't hand-roll config I/O.

Never invent a LabAPI class/namespace/method/property/attribute. Before using one you haven't already seen in this repo, check the restored package (LabApi.dll + LabApi.xml under %userprofile%\.nuget\packages\northwood.labapi\<version>\lib\net48\) or https://github.com/northwood-studios/LabAPI. If you can't verify a member exists, say so instead of guessing. No Skill/plugin specific to C#, .NET, MSBuild, NuGet, or LabAPI exists in the Claude Code skill catalog as of this writing — check ToolSearch/SearchSkills for one before editing in case that has changed, but don't assume one exists.

Toolchain choices — don't "fix" these without being asked

  • The project's TargetFramework is netstandard2.1 on purpose, even though LabAPI's own docs and the Northwood.LabAPI NuGet package only target net48/net481. This is a deliberate compatibility choice (compiled output still loads under the game's Mono/.NET Framework 4.8 host) that trades away some netstandard2.1 BCL/language surface .NET Framework can't actually run (e.g. default interface members). Don't switch it to net48, don't add multi-targeting, unless the user asks.
  • LangVersion is pinned explicitly (currently 14.0) — don't let it drift to latest.
  • The .csproj suppresses exactly two warnings, both scoped as narrowly as MSBuild allows, because of the netstandard2.1/net48-only-package combination above: NU1701 (via a per-PackageReference <NoWarn>, not a project-wide one) and MSB3270 (via ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch=None). Don't broaden either into a blanket suppression and don't remove them without understanding why (both are explained here, not in the .csproj — this repo's code style forbids comments, including in the .csproj, see "Code style" below).
  • The Northwood.LabAPI PackageReference uses a floating Version="*" on purpose, so every dotnet restore always pulls whatever is the latest stable release on NuGet at that moment — there's no version to bump here. This is intentionally not wired to EntryPoint.cs's RequiredApiVersion, which can't be a floating value (System.Version has no such concept and would throw at runtime if it tried). RequiredApiVersion is a separate, manually-maintained literal representing the minimum LabAPI version the scaffolded code is known to work with — bump it by hand if you verify compatibility with a newer release, don't try to auto-sync it with the floating restore. Also be aware a floating version means a plugin generated today can restore a different (newer) LabAPI release than one generated last week, with no pinning — that's the deliberate trade-off of "always latest" over reproducibility.

Code style for anything you write here

  • No //, /* */, ///, or XML (<!-- -->) comments unless the user explicitly asks for comments/documentation in that request. No comments restating self-explanatory code. EntryPoint.cs/Config.cs/the .csproj are already comment-free — keep them that way and follow the same standard for any new file.
  • All text embedded in code defaults to English: string/log/console messages, exception messages, [Description]/config text, any user-facing string. Example: Logger.Info("Plugin initialized successfully"), not a Spanish equivalent.
  • This never applies to identifiers the user picked — project name, namespace, class/method/property/variable names stay exactly as requested (standard C# casing rules aside). Honor an explicit request for a specific piece of text in another language.

How to verify a change (no test suite exists — this is the actual check)

There is no automated test project. Verify any template change by actually generating a project from it and building that output. Replace <repo-name> below with this repo's actual folder name (the value you'd see in sourceName):

dotnet new uninstall .
dotnet new install .
dotnet new labapi-plugin -n VerifyPlugin -o ../_verify/VerifyPlugin
cd ../_verify/VerifyPlugin
dotnet build

Expect Build succeeded. 0 Warning(s) 0 Error(s). If it isn't 0/0, the change broke either the template's own warning suppressions or something in EntryPoint.cs/ Config.cs/the .csproj.

Notes:

  • Re-run dotnet new uninstall . + dotnet new install . after any edit to .template.config/template.json or the scaffolded files — the template cache keys off the installed path and stale content otherwise gets regenerated.
  • dotnet build also works directly inside content/ (the placeholder project is itself valid and compilable), but that only proves the unsubstituted placeholder compiles — it does not exercise symbol substitution or the sources mapping. Always additionally verify through an actual dotnet new generation before calling a change done.
  • If dotnet new install . warns about another installed template sharing this identity (SCPSL.LabApi.PluginTemplate) from a different path — e.g. this repo's own parent folder having been installed by an earlier mistaken dotnet new install . run one level up — that other install shadows this one and your edits won't be exercised. Run dotnet new uninstall <that other path> first.
  • On Windows, if cleaning up ../_verify fails with "resource busy", run dotnet build-server shutdown first, then retry; if it still won't delete, fall back to PowerShell's Remove-Item -Recurse -Force.
  • Delete the ../_verify scratch output when done; it isn't part of the template.