C# Core Engine Code Walkthrough

This document provides a detailed walkthrough of the unmanaged C# simulation engine (CrawlCipher.Core), detailing the state flow, physics tick, and FFI boundaries.


1. Project Structure

The engine is contained within three primary files under the CrawlCipher.Core/ directory:

  1. Types.cs: Defines the data structures, enums, and serializable representation of entities.
  2. GameCore.cs: Contains the core orchestrator (GameEngine) managing physics, ticks, energy, AI, and combat rules.
  3. FFIExports.cs: Exposes C-compatible pointers and structures to the dynamic linker for Rust interop.

2. Structural Memory Models (Types.cs)

To pass data across the Rust border without marshalling overhead, entities use two representations: Managed Classes (for internal C# simulation) and Unmanaged Structs (sequential layouts passed to Rust).

A. The Player Model

Within the engine, a player is defined as a class:

public class Player
{
    public int Id;
    public List<SegmentData> Body;         // List of X, Y positions for segments
    public int Energy;
    public int Score;
    public int Kills;
    public bool IsIdle;                    // True if manual move has not been input
    public bool IsAutopilot;               // True if continuous movement is toggled
    public WeaponType[] BodyWeapons;       // Parallel array mapping equipment to segments
}

B. Segment Data Layout

public class SegmentData
{
    public int X;
    public int Y;
    public Direction Dir;
    public string EquippedItemId;          // UUID reference to Backpack item
}

3. Simulation Loop & Physics (GameCore.cs)

The GameEngine class orchestrates the gameplay ticks. It manages a state machine defined by SimulationStateType:

 [Menu] --(StartGame)--> [Running] --(Pause)--> [Paused]
                            |
                     (Head Collision)
                            v
                       [GameOver]

The Update Cycle (Tick() Method)

When the Rust TUI calls Update(gamePtr), the engine executes the following sequentially:

  1. Input Resolution: Applies pending move commands to shift player directions.
  2. Snail Simulation: Bounces snail obstacles off walls and shifts their coordinates based on speed dividers.
  3. Bullet Propagation: Advances all bullet projectiles forward by BulletSpeedMultiplier cells.
  4. Collision Pass:
    • Checks if bullets intersect snake segments or snails (applies damage/severing).
    • Checks if snake heads hit food (triggers growth queue and restores energy).
    • Checks if snake heads hit walls or other snake segments (triggers death or wrap-around).
  5. Energy Regeneration: Restores energy based on state (manual idle vs autopilot moving).

4. Unmanaged Exports & Pinning (FFIExports.cs)

The C# engine is compiled to native code using NativeAOT. It exposes entry points annotated with [UnmanagedCallersOnly].

FFI Entry Points Registry

The unmanaged functions exposed to the dynamic linker (libloading) are mapped as follows:

Function SymbolReturn TypeArgumentsDescription
CreateGameIntPtrGrid dimensions, configuration optionsAllocates engine state on heap, returns GCHandle pointer.
DestroyGamevoidIntPtr gamePtrReleases the GCHandle pinning, allowing GC collection.
UpdatevoidIntPtr gamePtrAdvances game state by one simulation tick.
ProcessInputvoidIntPtr gamePtr, int type, int p1, int p2Queues or executes user command.
GetGameStateGameStateIntPtr gamePtrRetrieves global stats (ticks, state type).
GetPlayerStatePlayerStateIntPtr gamePtr, int playerIdRetrieves specific player metrics.
GetGridCellsintIntPtr gamePtr, IntPtr outCells, int maxFills array with active viewport cell data.

Heap Pinning Workflow

Because C# is a garbage-collected language, the engine instance must be pinned in memory to prevent the GC from reclaiming or relocating it:

[UnmanagedCallersOnly(EntryPoint = "CreateGame")]
public static unsafe IntPtr CreateGame(int width, int height)
{
    // 1. Allocate the C# managed engine instance
    var engine = new GameEngine(width, height);
    
    // 2. Allocate a GCHandle to root the instance
    GCHandle handle = GCHandle.Alloc(engine, GCHandleType.Normal);
    
    // 3. Return the unmanaged raw pointer to Rust
    return GCHandle.ToIntPtr(handle);
}

Every subsequent FFI call from Rust passes this IntPtr back, allowing C# to resolve the handle target:

[UnmanagedCallersOnly(EntryPoint = "Update")]
public static unsafe void Update(IntPtr gamePtr)
{
    GCHandle handle = GCHandle.FromIntPtr(gamePtr);
    var engine = (GameEngine)handle.Target!;
    engine.Tick();
}