sharplib/AGENTS.md
2026-03-21 13:59:27 -07:00

4.8 KiB

SharpLib and general coding guidelines

0. PERFORMANCE. Title it 0. AXIOM: HIGH-PERFORMANCE / ZERO-ALLOCATION

  • Zero Allocation Default: Assume all code runs in a 60hz+ game loop or high-throughput pipeline unless marked [ColdPath].
  • NO LINQ: using System.Linq; is strictly forbidden in core logic. Use for loops, arrays, or Span<T>/ReadOnlySpan<T>.
  • NO Hidden Closures: Do not use lambdas or delegates that capture local variables. If a lambda is necessary, make it static and pass state explicitly.
  • Struct Semantics:
    • Pass readonly structs using in or ref readonly.
    • Do not pass struct to interfaces unless constrained by generics (where T : struct), to prevent boxing.
  • String Allocation: NO string interpolation ($"") on the hot path. Use static strings, string.Create, or format into pooled buffers.
  • Async/Task Overhead: Default to synchronous execution for immediate state changes. If async is mandatory on a hot path, use ValueTask<T>, never Task<T>.
  • Collections: No List<T>.Add() inside tight loops without pre-sizing the capacity. Prefer array pooling (ArrayPool<T>.Shared).

1. CORE PHILOSOPHY

  • Leaky Abstractions: All abstractions are always leaky. Do not hide complexity. Expose failure modes, costs, and "reason" parameters.
  • Visual Cost: "Expensive things must look expensive." No hidden RPCs. Use explicit Message structs and Send methods instead of Rpcs
  • Fast & Introspectable: Code must be highly performant (hot-path aware) but deeply debuggable (log, DebugOld, reason strings). These are there to make debugging from console or log file possible

2. NAMING & SEMANTICS

  • Leaf Libraries: Core libs (io, imm, net, log) are 2-5 chars. NEVER include using {namespace}; for core libs or any short namespace
  • No Redundancy:In Names
    • No "Get": Player() not GetPlayer().
    • No Echo: Send(msg) not SendMsg(msg).
    • No "I" Prefix: Interfaces are Renderer, not IRenderer.

3. LOGGING (log static class)

  • Constraint: NO Console.WriteLine, Debug.WriteLine, or ILogger.
  • Mechanism: The logging system is very fast. It only queues the log on the main thread. It collects the caller debug info, and uses the directory name of the file for a default category (this can be overridden)
  • Standard: log.info, log.debug, log.warn, log.error.
  • Finer grained: log.trace, log.high
  • Use log.exception( ex, $"[what was trying to be done]" );
  • Introspection: log.props(obj, "Header") and log.exception(ex, "Context").
  • Information put important info as far to the left as possible, even at the cost of poor wording

4. IMMUTABLE STATE (io, imm)

  • Rule: Objects inheriting Versioned<T>, Recorded<T>, Timed<T> are IMMUTABLE.
  • Change Pipeline: MUST use imm.Process.
    • Ref Helper (Preferred): imm.Process(ref _state, s => s with { X = 1 }, "Reason");
    • Instance: _state = _state.Process(s => s with { X = 1 }, "Reason");
  • The "Hole Punch": Always propagate a string reason parameter in your methods to feed the imm.Process log.
  • History: obj.DebugOld is for DEBUG ONLY. Do not base program logic on it.

5. Visual Cost

  • Network/IO/Expensive operations MUST NOT masquerade as local function calls. Require explicit structural allocation.
  • No RPCs: Do not make network calls look like functions.
  • Pattern: Construct Struct -> Send.
    • Bad: proxy.Move(x,y)
    • Good: Net.Send(new MoveMsg(x,y))

6. FEW-SHOT EXAMPLES (Strictly Imitate)

<csharp_examples>

// ** NAMING & LOGGING
// Explicit 'log' usage, no 'Get', no 'Console'
public void Init() 
{
  log.startup( "log/current_project.log", log.Endpoints.All );

  var waitTime = 1.0f;

  log.info( $"Waiting for {waitTime}" );

  try
  {
    obj?.SomeOperation();
   }
   catch( Exception ex )
    {
	   //Logs the exception name, message, the reason, the stack
       log.exception( ex, $"SomeOperation failed" );
    }
    
    // ** IMMUTABLE PROCESS
    // Reason "Init" passed down. Ref pattern used.
    imm.Process(ref _state, s => s with { Ready = true }, "Init");
}

// ** LOGIC FLOW
public void Update(float dt)
{
    // 'Player()' not 'GetPlayer()'
    var p = Player();
    
    // Visual Cost: Explicit allocation of message struct
    if (p.Active)
    {
        Net.Send(new HeartbeatMsg(dt));
    }
}


// ** "Punching a hole" with the reason parameter
public void Equip(Item i, string reason /*No default so a reason will be passed in*/ )
{
   imm.Process( ref inv, s => s.Add(i), reason);
}

. . . 

// Somewhere else in project
public void PlayerSwappedWeapons( Player pl )
{
 Equip( _item, $"Player {pl.Name} swapped weapons (PlayerSwappedWeapons)" );
}

<csharp_examples>