sharplib/AGENTS.md

3.5 KiB

SharpLib and general coding guidelines

1. CORE PHILOSOPHY

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

2. NAMING & SEMANTICS

  • Leaf Libraries: Core libs (io, imm, net, log) are 2-5 chars. NEVER use using for them. Always type the prefix (e.g., imm.Process).
  • No Redundancy:
    • No "Get": Player() not GetPlayer().
    • No Echo: Send(msg) not SendMsg(msg).
    • No "I" Prefix: Interfaces are Renderer, not IRenderer.
  • Architecture: Slow/Pausable Realtime. Entity Component System (Pub/Sub).

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)
  • Functional Tracing:
    • var x = log.info( $"Got {log.var(Calc())} from the calculation" ); // This both logs the info and prints what happened
  • 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.logProps(obj, "Header") and log.exception(ex, "Context").
  • 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 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 Process log.
  • History: obj.DebugOld is for DEBUG ONLY. Do not base game logic on it.

5. Visual Cost

  • Unless something is a function call, it should look like a function call. For example RPCs are bad, sending a message is good.
  • 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))

FEW-SHOT EXAMPLES (Strictly Imitate)

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

  var waitTime = 1.0f;

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

  try
  {
    obj.SomeOperation();
   }
   catch( Exception ex )
    {
       log.exception( ex, $"" );
    }
    
    // 2. IMMUTABLE PROCESS
    // Reason "Init" passed down. Ref pattern used.
    imm.Process(ref _state, s => s with { Ready = true }, "Init");
}

// 3. 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));
    }
}

// 5. INTROSPECTION API
// "Punching a hole" with the reason parameter
public void Equip(Item i, string reason = "Equip")
{
   imm.Process( ref inv, s => s.Add(i), reason);
}