skills/csharp/SKILL.md
C# development standards. Use when writing C# code, .NET projects, Unity mods, or NuGet packages.
npx skillsauth add abix-/claude-blueprints csharpInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
dotnet build (debug), dotnet build -c Release (release)<TargetFramework> in csprojnetstandard2.1 (builds with any SDK 6+, output is always netstandard2.1)The .NET SDK version (6, 7, 8, 9) is the build tool. The <TargetFramework> in csproj is the output. They're independent:
netstandard2.1 -- Unity/game mods, maximum compatibilitynet6.0 through net9.0 -- standalone apps, pick latest stable<LangVersion> in csproj limits C# syntax features regardless of SDKBuilding a netstandard2.1 project with SDK 9 works fine -- the SDK is just the compiler.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="SomeLib">
<Private>false</Private> <!-- don't copy to output -->
<HintPath>path\to\SomeLib.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
<Private>false</Private> prevents copying referenced DLLs to output -- use when the host already has them (Unity, game runtime).
When modding games or extending libraries with internal types, use BepInEx AssemblyPublicizer:
<PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.2" PrivateAssets="all"/>
<Reference Include="SomeAssembly" Publicize="true">
<Private>false</Private>
<HintPath>$(GameDir)\SomeAssembly.dll</HintPath>
</Reference>
This makes all internal/private types accessible at compile time without modifying the DLL.
Game mods often use DI frameworks (Bindito, Zenject, VContainer). Constructor injection:
public class MyService
{
private readonly ISomeService _someService;
public MyService(ISomeService someService)
{
_someService = someService;
}
}
For embedding an HTTP API in a Unity game/mod:
private HttpListener _listener;
private Thread _listenerThread;
private ConcurrentQueue<PendingRequest> _pending = new ConcurrentQueue<PendingRequest>();
// start on background thread
_listener = new HttpListener();
_listener.Prefixes.Add("http://+:8085/");
_listenerThread = new Thread(ListenLoop) { IsBackground = true };
_listenerThread.Start();
// drain on main thread (Unity Update)
public void UpdateSingleton()
{
while (_pending.TryDequeue(out var req))
ProcessRequest(req);
}
Key: reads can run on the listener thread if data is thread-safe. Writes MUST queue to main thread.
When you derive a string from a reference that rarely changes, use a shared RefChanged helper to skip the derivation when the source hasn't changed. One pointer comparison per refresh instead of string allocation:
// DRY helper -- shared by all refresh code
private static bool RefChanged(ref object cached, object current)
{
if (ReferenceEquals(cached, current)) return false;
cached = current;
return true;
}
// in cached struct
public string Workplace;
public object LastWorkplaceRef;
// in refresh -- one-liner per field
var wp = c.Worker?.Workplace;
if (RefChanged(ref c.LastWorkplaceRef, wp))
c.Workplace = wp != null ? CleanName(wp.GameObject.name) : null;
Use for: workplace names, district names, recipe names, any string derived from a game object reference. The ReferenceEquals check is a single pointer comparison — essentially free.
Values that never change after entity creation (building coordinates, orientation, footprint tiles, effect radius) should be set ONCE in the add-time handler, not re-read every refresh:
// in AddToIndexes (runs once when entity is created)
cb.X = coords.x; cb.Y = coords.y; cb.Z = coords.z;
cb.Orientation = OrientNames[(int)bo.Orientation];
cb.EffectRadius = ec.GetComponent<RangedEffectBuildingSpec>()?.EffectRadius ?? 0;
// in RefreshCachedState -- only read values that actually change
c.Finished = c.BlockObject.IsFinished; // this changes
// c.X, c.Y, c.Z -- NEVER re-read (immutable)
Rule: if a value only changes when the entity is created or destroyed, it belongs in the add-time handler, not the per-second refresh.
For all JSON serialization, use a fluent TimberbotJw instead of Dictionary+Newtonsoft. Allocate once as a field, Reset() per request. Auto-handles commas via depth-aware state -- no manual separator tracking:
// field -- allocated once, reused across all requests
private TimberbotJw _jw = new TimberbotJw(200000);
// usage -- fluent chaining, auto-commas, nesting-aware
public string CollectItems()
{
var jw = _jw.Reset().OpenArr();
foreach (var item in _items.Read)
{
jw.OpenObj()
.Key("id").Int(item.Id)
.Key("name").Str(item.Name)
.Key("active").Bool(item.Active);
if (item.Progress > 0)
jw.Key("progress").Float(item.Progress, "F1");
jw.CloseObj();
}
jw.CloseArr();
return jw.ToString();
}
Key: AutoSep() inside Key()/OpenObj()/OpenArr() inserts commas automatically. No bool first tracking. Nested objects and arrays just work. Single shared _jw instance for all endpoints (serial on listener thread).
When you need to sort or compare intermediate results, use value tuples instead of anonymous objects. Avoids reflection for property access:
// BAD -- anonymous objects require reflection to sort
var results = new List<object>();
results.Add(new { x = 1, score = 5 });
results.Sort((a, b) => (int)a.GetType().GetProperty("score").GetValue(a) - ...);
// GOOD -- tuples give direct field access
var results = new List<(int x, int y, int score, bool valid)>();
results.Add((1, 2, 5, true));
results.Sort((a, b) => b.score - a.score);
When working with publicized internals where you don't know property names:
// temporary: dump all members
var members = obj.GetType()
.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(m => m.MemberType == MemberTypes.Property || m.MemberType == MemberTypes.Field)
.Select(m => m.Name);
entry["_debug"] = string.Join(", ", members);
Build, run, read the output, then replace with proper property access. Remove the reflection code after.
Available:
is pattern matching (obj is int v, obj is not null)switch expressionsnew() (Dictionary<string, object> d = new())x ??= default)using declarations (no braces)NOT available in netstandard2.1:
namespace Foo;) -- requires C# 10[1, 2, 3]) -- requires C# 12is int g fails if g exists in outer scope)foreach on non-IEnumerable: types like GoodAmount may look iterable but aren't -- check the actual typeGetComponent<T>() ambiguity: game frameworks (Unity, Timberborn) may have their own GetComponent that shadows Unity's. Use the right one.Publicize="true" doesn't mean types exist: the type must actually be in that DLL. If you get "type not found," check ls Managed/ | grep -i keyword to find the right DLL._ prefix: e.g. node._nominalPowerInput (original private field naming preserved)dotnet build # debug build
dotnet build -c Release # release build
dotnet clean && dotnet build # force full rebuild
new Dictionary, new List, LINQ, string.Format, ToString() on enums in per-frame codeStringBuilder reuseforeach over an interface (IEnumerable<T>, IReadOnlyList<T>) boxes the struct enumerator on the heap (~40 bytes). Safe in per-request code, avoid in per-frame:
// BAD in hot path -- boxes enumerator if AllInventories returns IEnumerable<T>
foreach (var inv in building.AllInventories) { ... }
// GOOD -- use indexer if the type supports it
var list = building.AllInventories;
for (int i = 0; i < list.Count; i++) { var inv = list[i]; ... }
Only matters in per-frame/per-second refresh paths. Per-request code (HTTP responses) can use foreach freely.
Avoids: Dictionary allocs per item, Newtonsoft reflection, intermediate string allocs. 10x+ faster than JsonConvert.SerializeObject(list). All endpoints use a single shared TimberbotJw instance -- serial on the listener thread, no concurrency concern.
Never use LINQ (.Select(), .Where(), .ToList(), .ToArray()) in per-frame or per-second code. Each LINQ call allocates iterator objects + closures. Use simple loops instead:
// BAD -- allocates iterator + anonymous objects + list
tile["occupants"] = occList.Select(o => new { o.name, o.z }).ToList();
// GOOD -- simple loop, explicit types
var stacked = new List<object>(occList.Count);
foreach (var o in occList)
stacked.Add(new Dictionary<string, object> { ["name"] = o.name, ["z"] = o.z });
tile["occupants"] = stacked;
LINQ is fine in per-request code and one-time initialization.
Deploy built DLL to a target folder automatically:
<Target Name="Deploy" AfterTargets="Build">
<MakeDir Directories="$(TargetDir)" />
<Copy SourceFiles="$(OutputPath)MyMod.dll" DestinationFolder="$(TargetDir)" />
</Target>
Internal methods return typed structs. JSON serialization happens ONLY at the HTTP boundary. Never pass JSON strings between internal methods -- use the struct directly.
// good: struct for internal use, ToJson() at HTTP boundary
public PlaceBuildingResult PlaceBuilding(...) { return new PlaceBuildingResult { Id = id }; }
// HTTP handler: return result.ToJson(jw);
// bad: returning JSON string, then parsing it with reflection
public object PlaceBuilding(...) { return _jw.Result(("id", id)); }
// caller: result.GetType().GetProperty("id") -- broken on strings
Trust the game engine's validators (IsValid, BlockValidator). Use them for error reasons instead of reimplementing:
BlockValidator.BlockConflictsWithExistingObject -- what's blocking a tileBlockObjectValidationService._blockObjectValidators -- 9 validators with reason stringsPreviewFactory.Create + Reposition + IsValid -- the player's green/red overlayPositionedBlocks.GetAllBlocks() -- world-space blocks after rotation, no manual rotation mathAll list endpoints accept format param. Server writes different output per format:
tools
AutoHotkey v2 scripting standards for Windows automation, hotkeys, and game macros. Built from the official AHK v2 docs and the AHK community conventions. v1 reached EOL in March 2024.
data-ai
Analyze why Claude made its previous response -- trace reasoning to system prompt, CLAUDE.md, memory, skills, or context
tools
development
Build, test, and release Timberbot mod to GitHub and Steam Workshop