4.8 KiB
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. Useforloops, arrays, orSpan<T>/ReadOnlySpan<T>. - NO Hidden Closures: Do not use lambdas or delegates that capture local variables. If a lambda is necessary, make it
staticand pass state explicitly. - Struct Semantics:
- Pass readonly structs using
inorref readonly. - Do not pass
structto interfaces unless constrained by generics (where T : struct), to prevent boxing.
- Pass readonly structs using
- 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>, neverTask<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
Sendmethods instead of Rpcs - Fast & Introspectable: Code must be highly performant (hot-path aware) but deeply debuggable (
log,DebugOld,reasonstrings). 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()notGetPlayer(). - No Echo:
Send(msg)notSendMsg(msg). - No "I" Prefix: Interfaces are
Renderer, notIRenderer.
- No "Get":
3. LOGGING (log static class)
- Constraint: NO
Console.WriteLine,Debug.WriteLine, orILogger. - 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")andlog.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");
- Ref Helper (Preferred):
- The "Hole Punch": Always propagate a
string reasonparameter in your methods to feed theimm.Processlog. - History:
obj.DebugOldis 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))
- Bad:
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>