Quick Start Guide

Every great game begins with a solid server. Let's build yours.

This guide walks you through building a working Altruist server step by step — starting with HTTP, then adding real-time communication. Every example is copy-pasteable and testable.


Step 1: HTTP Server in 3 Files

Program.cs

using Altruist;
await AltruistApplication.Run(args);

That's it. Altruist will:

  1. Load config.yml from your output directory
  2. Auto-discover and register all [Service] classes
  3. Auto-discover all controllers and portals
  4. Start the server based on your config

config.yml

altruist:
  server:
    http:
      host: "0.0.0.0"
      port: 3000

Note:

Make sure config.yml is copied to the output directory. In your .csproj file add:

<ItemGroup>
  <None Update="config.yml">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </None>
</ItemGroup>

HelloController.cs

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("hello")]
public class HelloController : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return Ok(new { message = "Hello from Altruist!", timestamp = DateTime.UtcNow });
    }
}

Run and test

dotnet run

Open your browser to http://localhost:3000/hello or run:

curl http://localhost:3000/hello

You'll see:

{ "message": "Hello from Altruist!", "timestamp": "2025-01-01T12:00:00Z" }

Note:

That's a full Altruist HTTP server — no Startup.cs, no builder.Build(), no middleware registration. The framework discovers your controllers, configures Kestrel, and runs everything from config.yml.


Step 2: Add WebSocket Communication

Now add real-time packet handling. Update your config and create a portal.

Update config.yml

altruist:
  server:
    http:
      host: "0.0.0.0"
      port: 3000
    transport:
      codec:
        provider: json
      websocket:
        enabled: true
        path: /ws
  persistence:
    cache:
      provider: inmemory

Define a Packet

Packets are the data structures sent between client and server:

using System.Text.Json.Serialization;
using Altruist;
using MessagePack;

[MessagePackObject]
public struct ChatPacket : IPacketBase
{
    [Key(0)]
    public uint MessageCode { get; set; }

    [Key(1)]
    [JsonPropertyName("text")]
    public string Text { get; set; }

    public ChatPacket()
    {
        MessageCode = 1000;
        Text = string.Empty;
    }
}

Note:

Framework packets use MessageCode 0–999. Your custom packets should start at 1000+.

Create a Portal

Portals are the entry points for client-server communication. Each portal handles events via [Gate] methods:

using Altruist;

[Portal("/ws")]
public class ChatPortal : AltruistPortal
{
    private readonly IAltruistRouter _router;

    public ChatPortal(IAltruistRouter router)
    {
        _router = router;
    }

    [Gate("chat")]
    public async Task OnChat(ChatPacket packet, string clientId)
    {
        // Echo back to the sender
        await _router.Client.SendAsync(clientId, new ChatPacket
        {
            MessageCode = 1000,
            Text = $"Server received: {packet.Text}"
        });

        // Broadcast to all connected clients
        await _router.Broadcast.SendAsync(new ChatPacket
        {
            MessageCode = 1000,
            Text = $"[{clientId[..8]}]: {packet.Text}"
        });
    }
}

Run and test

dotnet run

You should see output like:

 The Portals Are Open! Connect At:
----------------------------------------------------
 Address: ws://0.0.0.0:3000/ws
 Transport: WebSocket
----------------------------------------------------
 Welcome, traveler!

Now listening on: http://0.0.0.0:3000

Both HTTP and WebSocket are running:

Test from your browser console

Open your browser's developer console (F12) and paste:

const ws = new WebSocket("ws://localhost:3000/ws");
ws.onmessage = (e) => console.log("Received:", e.data);
ws.onopen = () => {
  console.log("Connected!");
  ws.send(JSON.stringify({ messageCode: 4, event: "chat", text: "Hello world!" }));
};

Note:

The initial message uses messageCode: 4 (the framework's routing packet code) with an event field that routes to the matching [Gate] handler. The framework decodes the payload into your packet type (ChatPacket).


Step 3: Switch to TCP

The same portal works over TCP — just change the config:

altruist:
  server:
    http:
      host: "0.0.0.0"
      port: 3000
    transport:
      codec:
        provider: json
      tcp:
        enabled: true
        port: 9050

Now ChatPortal handles TCP connections on port 9050 instead of WebSocket — no code changes needed. You can even enable both at the same time:

    transport:
      codec:
        provider: json
      websocket:
        enabled: true
        path: /ws
      tcp:
        enabled: true
        port: 9050

Note:

Portal code is transport-agnostic. Switching between WebSocket, TCP, and UDP is a single config change. This is one of Altruist's core design principles.


Multiple Portals

You can register multiple portals on the same path. Each handles different events:

[Portal("/ws")]
public class ChatPortal : AltruistPortal
{
    [Gate("chat")]
    public async Task OnChat(ChatPacket packet, string clientId) { ... }
}

[Portal("/ws")]
public class GamePortal : AltruistPortal
{
    [Gate("join")]
    public async Task OnJoin(JoinPacket packet, string clientId) { ... }

    [Gate("move")]
    public async Task OnMove(MovePacket packet, string clientId) { ... }
}

Both portals share the same connection endpoint but handle different events. The framework routes each incoming packet to the correct [Gate] based on the event field.


What's Next?